Merge "Use toolkit instead of renderscript in SystemUI"
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index eea607b..5894803 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -54,6 +54,9 @@
 per-file Broadcast* = file:/BROADCASTS_OWNERS
 per-file ReceiverInfo* = file:/BROADCASTS_OWNERS
 
+# KeyguardManager
+per-file KeyguardManager.java = file:/services/core/java/com/android/server/locksettings/OWNERS
+
 # LocaleManager
 per-file *Locale* = file:/services/core/java/com/android/server/locales/OWNERS
 
@@ -93,7 +96,5 @@
 per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS
 per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
 
-# TODO(b/174932174): determine the ownership of KeyguardManager.java
-
 # Zygote
 per-file *Zygote* = file:/ZYGOTE_OWNERS
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index 13f7e5d..3a254c1 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -39,9 +39,13 @@
     }
 
     private static File getDirectory() {
-        // TODO(miguelaranda): figure out correct code path.
+        if ((System.getProperty("system.certs.enabled") != null)
+                && (System.getProperty("system.certs.enabled")).equals("true")) {
+            return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
+        }
         File updatable_dir = new File("/apex/com.android.conscrypt/cacerts");
-        if (updatable_dir.exists() && !(updatable_dir.list().length == 0)) {
+        if (updatable_dir.exists()
+                && !(updatable_dir.list().length == 0)) {
             return updatable_dir;
         }
         return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
index ca57c84..fceee4e 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -189,6 +189,9 @@
 
     /**
      * Show the view for the specified duration.
+     *
+     * <p>Note that toasts being sent from the background are rate limited, so avoid sending such
+     * toasts in quick succession.
      */
     public void show() {
         if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 076e4e1..1505ccc 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -185,8 +185,13 @@
     private static void preloadSharedLibraries() {
         Log.i(TAG, "Preloading shared libraries...");
         System.loadLibrary("android");
-        System.loadLibrary("compiler_rt");
         System.loadLibrary("jnigraphics");
+
+        // TODO(b/206676167): This library is only used for renderscript today. When renderscript is
+        // removed, this load can be removed as well.
+        if (!SystemProperties.getBoolean("config.disable_renderscript", false)) {
+            System.loadLibrary("compiler_rt");
+        }
     }
 
     native private static void nativePreloadAppProcessHALs();
diff --git a/media/java/android/media/tv/OWNERS b/media/java/android/media/tv/OWNERS
index fa04293..0b022e5 100644
--- a/media/java/android/media/tv/OWNERS
+++ b/media/java/android/media/tv/OWNERS
@@ -1,6 +1,7 @@
 quxiangfang@google.com
 shubang@google.com
 hgchen@google.com
+qingxun@google.com
 
 # For android remote service
 per-file ITvRemoteServiceInput.aidl = file:/media/lib/tvremote/OWNERS
diff --git a/tools/locked_region_code_injection/Android.bp b/tools/locked_region_code_injection/Android.bp
index a0cc446..954b816 100644
--- a/tools/locked_region_code_injection/Android.bp
+++ b/tools/locked_region_code_injection/Android.bp
@@ -19,3 +19,20 @@
         "ow2-asm-tree",
     ],
 }
+
+java_library_host {
+   name: "lockedregioncodeinjection_input",
+   manifest: "test/manifest.txt",
+   srcs: ["test/*/*.java"],
+   static_libs: [
+        "guava",
+        "ow2-asm",
+        "ow2-asm-analysis",
+        "ow2-asm-commons",
+        "ow2-asm-tree",
+        "hamcrest-library",
+        "hamcrest",
+        "platform-test-annotations",
+        "junit",
+    ],
+}
diff --git a/tools/locked_region_code_injection/OWNERS b/tools/locked_region_code_injection/OWNERS
new file mode 100644
index 0000000..bd43f17
--- /dev/null
+++ b/tools/locked_region_code_injection/OWNERS
@@ -0,0 +1,4 @@
+# Everyone in frameworks/base is included by default
+shayba@google.com
+shombert@google.com
+timmurray@google.com
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java
index 81a0773..2067bb4 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java
@@ -13,37 +13,51 @@
  */
 package lockedregioncodeinjection;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
+import static com.google.common.base.Preconditions.checkElementIndex;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.commons.TryCatchBlockSorter;
 import org.objectweb.asm.tree.AbstractInsnNode;
 import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.InsnNode;
 import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.LineNumberNode;
 import org.objectweb.asm.tree.MethodInsnNode;
 import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TypeInsnNode;
 import org.objectweb.asm.tree.TryCatchBlockNode;
 import org.objectweb.asm.tree.analysis.Analyzer;
 import org.objectweb.asm.tree.analysis.AnalyzerException;
 import org.objectweb.asm.tree.analysis.BasicValue;
 import org.objectweb.asm.tree.analysis.Frame;
 
-import static com.google.common.base.Preconditions.checkElementIndex;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
 
 /**
- * This visitor does two things:
+ * This visitor operates on two kinds of targets.  For a legacy target, it does the following:
  *
- * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and insert the corresponding pre
+ * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and inserts the corresponding pre
  * and post methods calls should it matches one of the given target type in the Configuration.
  *
  * 2. Find all methods that are synchronized and insert pre method calls in the beginning and post
  * method calls just before all return instructions.
+ *
+ * For a scoped target, it does the following:
+ *
+ * 1. Finds all the MONITOR_ENTER instructions in the byte code.  If the target of the opcode is
+ *    named in a --scope switch, then the pre method is invoked ON THE TARGET immediately after
+ *    MONITOR_ENTER opcode completes.
+ *
+ * 2. Finds all the MONITOR_EXIT instructions in the byte code.  If the target of the opcode is
+ *    named in a --scope switch, then the post method is invoked ON THE TARGET immediately before
+ *    MONITOR_EXIT opcode completes.
  */
 class LockFindingClassVisitor extends ClassVisitor {
     private String className = null;
@@ -73,12 +87,16 @@
     class LockFindingMethodVisitor extends MethodVisitor {
         private String owner;
         private MethodVisitor chain;
+        private final String className;
+        private final String methodName;
 
         public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) {
             super(Utils.ASM_VERSION, mn);
             assert owner != null;
             this.owner = owner;
             this.chain = chain;
+            className = owner;
+            methodName = mn.name;
         }
 
         @SuppressWarnings("unchecked")
@@ -93,6 +111,12 @@
                 for (LockTarget t : targets) {
                     if (t.getTargetDesc().equals("L" + owner + ";")) {
                         ownerMonitor = t;
+                        if (ownerMonitor.getScoped()) {
+                            final String emsg = String.format(
+                                "scoped targets do not support synchronized methods in %s.%s()",
+                                className, methodName);
+                            throw new RuntimeException(emsg);
+                        }
                     }
                 }
             }
@@ -118,9 +142,11 @@
                 AbstractInsnNode s = instructions.getFirst();
                 MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
                         ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false);
-                insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call);
+                insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, 0, call);
             }
 
+            boolean anyDup = false;
+
             for (int i = 0; i < instructions.size(); i++) {
                 AbstractInsnNode s = instructions.get(i);
 
@@ -131,9 +157,15 @@
                         LockTargetState state = (LockTargetState) operand;
                         for (int j = 0; j < state.getTargets().size(); j++) {
                             LockTarget target = state.getTargets().get(j);
-                            MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
-                                    target.getPreOwner(), target.getPreMethod(), "()V", false);
-                            insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call);
+                            MethodInsnNode call = methodCall(target, true);
+                            if (target.getScoped()) {
+                                TypeInsnNode cast = typeCast(target);
+                                i += insertInvokeAcquire(mn, frameMap, handlersMap, s, i,
+                                        call, cast);
+                                anyDup = true;
+                            } else {
+                                i += insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
+                            }
                         }
                     }
                 }
@@ -144,8 +176,9 @@
                     if (operand instanceof LockTargetState) {
                         LockTargetState state = (LockTargetState) operand;
                         for (int j = 0; j < state.getTargets().size(); j++) {
-                            // The instruction after a monitor_exit should be a label for the end of the implicit
-                            // catch block that surrounds the synchronized block to call monitor_exit when an exception
+                            // The instruction after a monitor_exit should be a label for
+                            // the end of the implicit catch block that surrounds the
+                            // synchronized block to call monitor_exit when an exception
                             // occurs.
                             checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL,
                                 "Expected to find label after monitor exit");
@@ -161,9 +194,16 @@
                                 "Expected label to be the end of monitor exit's try block");
 
                             LockTarget target = state.getTargets().get(j);
-                            MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
-                                    target.getPostOwner(), target.getPostMethod(), "()V", false);
-                            insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call);
+                            MethodInsnNode call = methodCall(target, false);
+                            if (target.getScoped()) {
+                                TypeInsnNode cast = typeCast(target);
+                                i += insertInvokeRelease(mn, frameMap, handlersMap, s, i,
+                                        call, cast);
+                                anyDup = true;
+                            } else {
+                                insertMethodCallAfter(mn, frameMap, handlersMap, label,
+                                        labelIndex, call);
+                            }
                         }
                     }
                 }
@@ -174,16 +214,116 @@
                     MethodInsnNode call =
                             new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(),
                                     ownerMonitor.getPostMethod(), "()V", false);
-                    insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
+                    insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, i, call);
                     i++; // Skip ahead. Otherwise, we will revisit this instruction again.
                 }
             }
+
+            if (anyDup) {
+                mn.maxStack++;
+            }
+
             super.visitEnd();
             mn.accept(chain);
         }
+
+        // Insert a call to a monitor pre handler.  The node and the index identify the
+        // monitorenter call itself.  Insert DUP immediately prior to the MONITORENTER.
+        // Insert the typecast and call (in that order) after the MONITORENTER.
+        public int insertInvokeAcquire(MethodNode mn, List<Frame> frameMap,
+                List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
+                MethodInsnNode call, TypeInsnNode cast) {
+            InsnList instructions = mn.instructions;
+
+            // Insert a DUP right before MONITORENTER, to capture the object being locked.
+            // Note that the object will be typed as java.lang.Object.
+            instructions.insertBefore(node, new InsnNode(Opcodes.DUP));
+            frameMap.add(index, frameMap.get(index));
+            handlersMap.add(index, handlersMap.get(index));
+
+            // Insert the call right after the MONITORENTER.  These entries are pushed after
+            // MONITORENTER so they are inserted in reverse order.  MONITORENTER should be
+            // the target of a try/catch block, which means it must be immediately
+            // followed by a label (which is part of the try/catch block definition).
+            // Move forward past the label so the invocation in inside the proper block.
+            // Throw an error if the next instruction is not a label.
+            node = node.getNext();
+            if (!(node instanceof LabelNode)) {
+                throw new RuntimeException(String.format("invalid bytecode sequence in %s.%s()",
+                                className, methodName));
+            }
+            node = node.getNext();
+            index = instructions.indexOf(node);
+
+            instructions.insertBefore(node, cast);
+            frameMap.add(index, frameMap.get(index));
+            handlersMap.add(index, handlersMap.get(index));
+
+            instructions.insertBefore(node, call);
+            frameMap.add(index, frameMap.get(index));
+            handlersMap.add(index, handlersMap.get(index));
+
+            return 3;
+        }
+
+        // Insert instructions completely before the current opcode.  This is slightly
+        // different from insertMethodCallBefore(), which inserts the call before MONITOREXIT
+        // but inserts the start and end labels after MONITOREXIT.
+        public int insertInvokeRelease(MethodNode mn, List<Frame> frameMap,
+                List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
+                MethodInsnNode call, TypeInsnNode cast) {
+            InsnList instructions = mn.instructions;
+
+            instructions.insertBefore(node, new InsnNode(Opcodes.DUP));
+            frameMap.add(index, frameMap.get(index));
+            handlersMap.add(index, handlersMap.get(index));
+
+            instructions.insertBefore(node, cast);
+            frameMap.add(index, frameMap.get(index));
+            handlersMap.add(index, handlersMap.get(index));
+
+            instructions.insertBefore(node, call);
+            frameMap.add(index, frameMap.get(index));
+            handlersMap.add(index, handlersMap.get(index));
+
+            return 3;
+        }
     }
 
-    public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
+    public static MethodInsnNode methodCall(LockTarget target, boolean pre) {
+        String spec = "()V";
+        if (!target.getScoped()) {
+            if (pre) {
+                return new MethodInsnNode(
+                    Opcodes.INVOKESTATIC, target.getPreOwner(), target.getPreMethod(), spec);
+            } else {
+                return new MethodInsnNode(
+                    Opcodes.INVOKESTATIC, target.getPostOwner(), target.getPostMethod(), spec);
+            }
+        } else {
+            if (pre) {
+                return new MethodInsnNode(
+                    Opcodes.INVOKEVIRTUAL, target.getPreOwner(), target.getPreMethod(), spec);
+            } else {
+                return new MethodInsnNode(
+                    Opcodes.INVOKEVIRTUAL, target.getPostOwner(), target.getPostMethod(), spec);
+            }
+        }
+    }
+
+    public static TypeInsnNode typeCast(LockTarget target) {
+        if (!target.getScoped()) {
+            return null;
+        } else {
+            // preOwner and postOwner return the same string for scoped targets.
+            return new TypeInsnNode(Opcodes.CHECKCAST, target.getPreOwner());
+        }
+    }
+
+    /**
+     * Insert a method call before the beginning or end of a synchronized method.
+     */
+    public static void insertMethodCallBeforeSync(MethodNode mn, List<Frame> frameMap,
             List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
             MethodInsnNode call) {
         List<TryCatchBlockNode> handlers = handlersMap.get(index);
@@ -226,6 +366,22 @@
         updateCatchHandler(mn, handlers, start, end, handlersMap);
     }
 
+    // Insert instructions completely before the current opcode.  This is slightly different from
+    // insertMethodCallBeforeSync(), which inserts the call before MONITOREXIT but inserts the
+    // start and end labels after MONITOREXIT.
+    public int insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
+            List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
+            MethodInsnNode call) {
+        InsnList instructions = mn.instructions;
+
+        instructions.insertBefore(node, call);
+        frameMap.add(index, frameMap.get(index));
+        handlersMap.add(index, handlersMap.get(index));
+
+        return 1;
+    }
+
+
     @SuppressWarnings("unchecked")
     public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers,
             LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) {
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java
index c5e59e3..5f62403 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java
@@ -21,14 +21,28 @@
 public class LockTarget {
     public static final LockTarget NO_TARGET = new LockTarget("", null, null);
 
+    // The lock which must be instrumented, in Java internal form (L<path>;).
     private final String targetDesc;
+    // The methods to be called when the lock is taken (released).  For non-scoped locks,
+    // these are fully qualified static methods.  For scoped locks, these are the
+    // unqualified names of a member method of the target lock.
     private final String pre;
     private final String post;
+    // If true, the pre and post methods are virtual on the target class.  The pre and post methods
+    // are both called while the lock is held.  If this field is false then the pre and post methods
+    // take no parameters and the post method is called after the lock is released.  This is legacy
+    // behavior.
+    private final boolean scoped;
 
-    public LockTarget(String targetDesc, String pre, String post) {
+    public LockTarget(String targetDesc, String pre, String post, boolean scoped) {
         this.targetDesc = targetDesc;
         this.pre = pre;
         this.post = post;
+        this.scoped = scoped;
+    }
+
+    public LockTarget(String targetDesc, String pre, String post) {
+        this(targetDesc, pre, post, false);
     }
 
     public String getTargetDesc() {
@@ -40,7 +54,11 @@
     }
 
     public String getPreOwner() {
-        return pre.substring(0, pre.lastIndexOf('.'));
+        if (scoped) {
+            return targetDesc.substring(1, targetDesc.length() - 1);
+        } else {
+            return pre.substring(0, pre.lastIndexOf('.'));
+        }
     }
 
     public String getPreMethod() {
@@ -52,10 +70,23 @@
     }
 
     public String getPostOwner() {
-        return post.substring(0, post.lastIndexOf('.'));
+        if (scoped) {
+            return targetDesc.substring(1, targetDesc.length() - 1);
+        } else {
+            return post.substring(0, post.lastIndexOf('.'));
+        }
     }
 
     public String getPostMethod() {
         return post.substring(post.lastIndexOf('.') + 1);
     }
+
+    public boolean getScoped() {
+        return scoped;
+    }
+
+    @Override
+    public String toString() {
+        return targetDesc + ":" + pre + ":" + post;
+    }
 }
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java
index 99d8418..5df0160 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java
@@ -13,10 +13,11 @@
  */
 package lockedregioncodeinjection;
 
-import java.util.List;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.tree.analysis.BasicValue;
 
+import java.util.List;
+
 public class LockTargetState extends BasicValue {
     private final List<LockTarget> lockTargets;
 
@@ -31,4 +32,10 @@
     public List<LockTarget> getTargets() {
         return lockTargets;
     }
+
+    @Override
+    public String toString() {
+        return "LockTargetState(" + getType().getDescriptor()
+                + ", " + lockTargets.size() + ")";
+    }
 }
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java
index 828cce7..d22ea23 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java
@@ -21,7 +21,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Collections;
+import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.zip.ZipEntry;
@@ -36,6 +36,7 @@
         String legacyTargets = null;
         String legacyPreMethods = null;
         String legacyPostMethods = null;
+        List<LockTarget> targets = new ArrayList<>();
         for (int i = 0; i < args.length; i++) {
             if ("-i".equals(args[i].trim())) {
                 i++;
@@ -52,23 +53,25 @@
             } else if ("--post".equals(args[i].trim())) {
                 i++;
                 legacyPostMethods = args[i].trim();
+            } else if ("--scoped".equals(args[i].trim())) {
+                i++;
+                targets.add(Utils.getScopedTarget(args[i].trim()));
             }
-
         }
 
-        // TODO(acleung): Better help message than asserts.
-        assert inJar != null;
-        assert outJar != null;
+        if (inJar == null) {
+            throw new RuntimeException("missing input jar path");
+        }
+        if (outJar == null) {
+            throw new RuntimeException("missing output jar path");
+        }
         assert legacyTargets == null || (legacyPreMethods != null && legacyPostMethods != null);
 
         ZipFile zipSrc = new ZipFile(inJar);
         ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar));
-        List<LockTarget> targets = null;
         if (legacyTargets != null) {
-            targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
-                    legacyPostMethods);
-        } else {
-            targets = Collections.emptyList();
+            targets.addAll(Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
+                                                                legacyPostMethods));
         }
 
         Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries();
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java
index b44e8b4..bfef106 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java
@@ -44,4 +44,27 @@
 
         return config;
     }
+
+    /**
+     * Returns a single {@link LockTarget} from a string.  The target is a comma-separated list of
+     * the target class, the request method, the release method, and a boolean which is true if this
+     * is a scoped target and false if this is a legacy target.  The boolean is optional and
+     * defaults to true.
+     */
+    public static LockTarget getScopedTarget(String arg) {
+        String[] c = arg.split(",");
+        if (c.length == 3) {
+          return new LockTarget(c[0], c[1], c[2], true);
+        } else if (c.length == 4) {
+            if (c[3].equals("true")) {
+                return new LockTarget(c[0], c[1], c[2], true);
+            } else if (c[3].equals("false")) {
+                return new LockTarget(c[0], c[1], c[2], false);
+            } else {
+                System.err.println("illegal target parameter \"" + c[3] + "\"");
+            }
+        }
+        // Fall through
+        throw new RuntimeException("invalid scoped target format");
+    }
 }
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java
index 31fa0bf..28f00b9 100644
--- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java
@@ -17,7 +17,10 @@
 import org.junit.Test;
 
 /**
- * To run the unit tests:
+ * To run the unit tests, first build the two necessary artifacts.  Do this explicitly as they are
+ * not generally retained by a normal "build all".  After lunching a target:
+ *   m lockedregioncodeinjection
+ *   m lockedregioncodeinjection_input
  *
  * <pre>
  * <code>
@@ -29,31 +32,25 @@
  * mkdir -p out
  * rm -fr out/*
  *
- * # Make booster
- * javac -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar src&#47;*&#47;*.java -d out/
- * pushd out
- * jar cfe lockedregioncodeinjection.jar lockedregioncodeinjection.Main *&#47;*.class
- * popd
- *
- * # Make unit tests.
- * javac -cp lib/junit-4.12.jar test&#47;*&#47;*.java -d out/
- *
- * pushd out
- * jar cfe test_input.jar lockedregioncodeinjection.Test *&#47;*.class
- * popd
+ * # Paths to the build artifacts.  These assume linux-x86; YMMV.
+ * ROOT=$TOP/out/host/linux-x86
+ * EXE=$ROOT/bin/lockedregioncodeinjection
+ * INPUT=$ROOT/frameworkd/lockedregioncodeinjection_input.jar
  *
  * # Run tool on unit tests.
- * java -ea -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar:out/lockedregioncodeinjection.jar \
- *     lockedregioncodeinjection.Main \
- *     -i out/test_input.jar -o out/test_output.jar \
+ * $EXE -i $INPUT -o out/test_output.jar \
  *     --targets 'Llockedregioncodeinjection/TestTarget;' \
  *     --pre     'lockedregioncodeinjection/TestTarget.boost' \
  *     --post    'lockedregioncodeinjection/TestTarget.unboost'
  *
  * # Run unit tests.
- * java -ea -cp lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar:out/test_output.jar \
+ * java -ea -cp out/test_output.jar \
  *     org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain
  * </code>
+ * OR
+ * <code>
+ * bash test/unit-test.sh
+ * </code>
  * </pre>
  */
 public class TestMain {
@@ -64,7 +61,9 @@
 
         Assert.assertEquals(TestTarget.boostCount, 0);
         Assert.assertEquals(TestTarget.unboostCount, 0);
-        Assert.assertEquals(TestTarget.unboostCount, 0);
+        Assert.assertEquals(TestTarget.invokeCount, 0);
+        Assert.assertEquals(TestTarget.boostCountLocked, 0);
+        Assert.assertEquals(TestTarget.unboostCountLocked, 0);
 
         synchronized (t) {
             Assert.assertEquals(TestTarget.boostCount, 1);
@@ -75,6 +74,8 @@
         Assert.assertEquals(TestTarget.boostCount, 1);
         Assert.assertEquals(TestTarget.unboostCount, 1);
         Assert.assertEquals(TestTarget.invokeCount, 1);
+        Assert.assertEquals(TestTarget.boostCountLocked, 0);
+        Assert.assertEquals(TestTarget.unboostCountLocked, 0);
     }
 
     @Test
@@ -84,12 +85,16 @@
 
         Assert.assertEquals(TestTarget.boostCount, 0);
         Assert.assertEquals(TestTarget.unboostCount, 0);
+        Assert.assertEquals(TestTarget.boostCountLocked, 0);
+        Assert.assertEquals(TestTarget.unboostCountLocked, 0);
 
         t.synchronizedCall();
 
         Assert.assertEquals(TestTarget.boostCount, 1);
         Assert.assertEquals(TestTarget.unboostCount, 1);
         Assert.assertEquals(TestTarget.invokeCount, 1);
+        Assert.assertEquals(TestTarget.boostCountLocked, 0);
+        Assert.assertEquals(TestTarget.unboostCountLocked, 0);
     }
 
     @Test
@@ -99,12 +104,16 @@
 
         Assert.assertEquals(TestTarget.boostCount, 0);
         Assert.assertEquals(TestTarget.unboostCount, 0);
+        Assert.assertEquals(TestTarget.boostCountLocked, 0);
+        Assert.assertEquals(TestTarget.unboostCountLocked, 0);
 
         t.synchronizedCallReturnInt();
 
         Assert.assertEquals(TestTarget.boostCount, 1);
         Assert.assertEquals(TestTarget.unboostCount, 1);
         Assert.assertEquals(TestTarget.invokeCount, 1);
+        Assert.assertEquals(TestTarget.boostCountLocked, 0);
+        Assert.assertEquals(TestTarget.unboostCountLocked, 0);
     }
 
     @Test
@@ -253,4 +262,125 @@
         Assert.assertEquals(TestTarget.invokeCount, 1);
     }
 
+    @Test
+    public void testScopedTarget() {
+        TestScopedTarget target = new TestScopedTarget();
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+
+        synchronized (target.scopedLock()) {
+            Assert.assertEquals(1, target.scopedLock().mLevel);
+        }
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+
+        synchronized (target.scopedLock()) {
+            synchronized (target.scopedLock()) {
+                Assert.assertEquals(2, target.scopedLock().mLevel);
+            }
+        }
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+    }
+
+    @Test
+    public void testScopedExceptionHandling() {
+        TestScopedTarget target = new TestScopedTarget();
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+
+        boolean handled;
+
+        // 1: an exception inside the block properly releases the lock.
+        handled = false;
+        try {
+            synchronized (target.scopedLock()) {
+                Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+                Assert.assertEquals(1, target.scopedLock().mLevel);
+                throw new RuntimeException();
+            }
+        } catch (RuntimeException e) {
+            Assert.assertEquals(0, target.scopedLock().mLevel);
+            handled = true;
+        }
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+        Assert.assertEquals(true, handled);
+        // Just verify that the lock can still be taken
+        Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
+
+        // 2: An exception inside the monitor enter function
+        handled = false;
+        target.throwOnEnter(true);
+        try {
+            synchronized (target.scopedLock()) {
+                // The exception was thrown inside monitorEnter(), so the code should
+                // never reach this point.
+                Assert.assertEquals(0, 1);
+            }
+        } catch (RuntimeException e) {
+            Assert.assertEquals(0, target.scopedLock().mLevel);
+            handled = true;
+        }
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+        Assert.assertEquals(true, handled);
+        // Just verify that the lock can still be taken
+        Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
+
+        // 3: An exception inside the monitor exit function
+        handled = false;
+        target.throwOnEnter(true);
+        try {
+            synchronized (target.scopedLock()) {
+                Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+                Assert.assertEquals(1, target.scopedLock().mLevel);
+            }
+        } catch (RuntimeException e) {
+            Assert.assertEquals(0, target.scopedLock().mLevel);
+            handled = true;
+        }
+        Assert.assertEquals(0, target.scopedLock().mLevel);
+        Assert.assertEquals(true, handled);
+        // Just verify that the lock can still be taken
+        Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
+    }
+
+    // Provide an in-class type conversion for the scoped target.
+    private Object untypedLock(TestScopedTarget target) {
+        return target.scopedLock();
+    }
+
+    @Test
+    public void testScopedLockTyping() {
+        TestScopedTarget target = new TestScopedTarget();
+        Assert.assertEquals(target.scopedLock().mLevel, 0);
+
+        // Scoped lock injection works on the static type of an object.  In general, it is
+        // a very bad idea to do type conversion on scoped locks, but the general rule is
+        // that conversions within a single method are recognized by the lock injection
+        // tool and injection occurs.  Conversions outside a single method are not
+        // recognized and injection does not occur.
+
+        // 1. Conversion occurs outside the class.  The visible type of the lock is Object
+        // in this block, so no injection takes place on 'untypedLock', even though the
+        // dynamic type is TestScopedLock.
+        synchronized (target.untypedLock()) {
+            Assert.assertEquals(0, target.scopedLock().mLevel);
+            Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock);
+            Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+        }
+
+        // 2. Conversion occurs inside the class but in another method.  The visible type
+        // of the lock is Object in this block, so no injection takes place on
+        // 'untypedLock', even though the dynamic type is TestScopedLock.
+        synchronized (untypedLock(target)) {
+            Assert.assertEquals(0, target.scopedLock().mLevel);
+            Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock);
+            Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+        }
+
+        // 3. Conversion occurs inside the method.  The compiler can determine the type of
+        // the lock within a single function, so injection does take place here.
+        Object untypedLock = target.scopedLock();
+        synchronized (untypedLock) {
+            Assert.assertEquals(1, target.scopedLock().mLevel);
+            Assert.assertEquals(true, untypedLock instanceof TestScopedLock);
+            Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+        }
+    }
 }
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java
new file mode 100644
index 0000000..7441d2b
--- /dev/null
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package lockedregioncodeinjection;
+
+public class TestScopedLock {
+    public int mEntered = 0;
+    public int mExited = 0;
+
+    public int mLevel = 0;
+    private final TestScopedTarget mTarget;
+
+    TestScopedLock(TestScopedTarget target) {
+        mTarget = target;
+    }
+
+    void monitorEnter() {
+        mLevel++;
+        mEntered++;
+        mTarget.enter(mLevel);
+    }
+
+    void monitorExit() {
+        mLevel--;
+        mExited++;
+        mTarget.exit(mLevel);
+    }
+}
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java
new file mode 100644
index 0000000..c80975e
--- /dev/null
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package lockedregioncodeinjection;
+
+public class TestScopedTarget {
+
+    public final TestScopedLock mLock;;
+
+    private boolean mNextEnterThrows = false;
+    private boolean mNextExitThrows = false;
+
+    TestScopedTarget() {
+        mLock = new TestScopedLock(this);
+    }
+
+    TestScopedLock scopedLock() {
+        return mLock;
+    }
+
+    Object untypedLock() {
+        return mLock;
+    }
+
+    void enter(int level) {
+        if (mNextEnterThrows) {
+            mNextEnterThrows = false;
+            throw new RuntimeException();
+        }
+    }
+
+    void exit(int level) {
+        if (mNextExitThrows) {
+            mNextExitThrows = false;
+            throw new RuntimeException();
+        }
+    }
+
+    void throwOnEnter(boolean b) {
+        mNextEnterThrows = b;
+    }
+
+    void throwOnExit(boolean b) {
+        mNextExitThrows = b;
+    }
+}
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java
index d1c8f34..e3ba6a7 100644
--- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java
@@ -19,8 +19,17 @@
   public static int invokeCount = 0;
   public static boolean nextUnboostThrows = false;
 
+  // If this is not null, then this is the lock under test.  The lock must not be held when boost()
+  // or unboost() are called.
+  public static Object mLock = null;
+  public static int boostCountLocked = 0;
+  public static int unboostCountLocked = 0;
+
   public static void boost() {
     boostCount++;
+    if (mLock != null && Thread.currentThread().holdsLock(mLock)) {
+      boostCountLocked++;
+    }
   }
 
   public static void unboost() {
@@ -29,6 +38,9 @@
       throw new RuntimeException();
     }
     unboostCount++;
+    if (mLock != null && Thread.currentThread().holdsLock(mLock)) {
+      unboostCountLocked++;
+    }
   }
 
   public static void invoke() {
diff --git a/tools/locked_region_code_injection/test/manifest.txt b/tools/locked_region_code_injection/test/manifest.txt
new file mode 100644
index 0000000..2314c18
--- /dev/null
+++ b/tools/locked_region_code_injection/test/manifest.txt
@@ -0,0 +1 @@
+Main-Class: org.junit.runner.JUnitCore
diff --git a/tools/locked_region_code_injection/test/unit-test.sh b/tools/locked_region_code_injection/test/unit-test.sh
new file mode 100755
index 0000000..9fa6f39
--- /dev/null
+++ b/tools/locked_region_code_injection/test/unit-test.sh
@@ -0,0 +1,98 @@
+#! /bin/bash
+#
+
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+# in compliance with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License
+# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+# or implied. See the License for the specific language governing permissions and limitations under
+# the License.
+
+# This script runs the tests for the lockedregioninjectioncode.  See
+# TestMain.java for the invocation.  The script expects that a full build has
+# already been done and artifacts are in $TOP/out.
+
+# Compute the default top of the workspace.  The following code copies the
+# strategy of croot.  (croot cannot be usd directly because it is a function and
+# functions are not carried over into subshells.)  This gives the correct answer
+# if run from inside a workspace.  If run from outside a workspace, supply TOP
+# on the command line.
+TOPFILE=build/make/core/envsetup.mk
+TOP=$(dirname $(realpath $0))
+while [[ ! $TOP = / && ! -f $TOP/$TOPFILE ]]; do
+  TOP=$(dirname $TOP)
+done
+# TOP is "/" if this script is located outside a workspace.
+
+# If the user supplied a top directory, use it instead
+if [[ -n $1 ]]; then
+  TOP=$1
+  shift
+fi
+if [[ -z $TOP || $TOP = / ]]; then
+  echo "usage: $0 <workspace-root>"
+  exit 1
+elif [[ ! -d $TOP ]]; then
+  echo "$TOP is not a directory"
+  exit 1
+elif [[ ! -d $TOP/prebuilts/misc/common ]]; then
+  echo "$TOP does not look like w workspace"
+  exit 1
+fi
+echo "Using workspace $TOP"
+
+# Pick up the current java compiler.  The lunch target is not very important,
+# since most, if not all, will use the same host binaries.
+pushd $TOP > /dev/null
+. build/envsetup.sh > /dev/null 2>&1
+lunch redfin-userdebug > /dev/null 2>&1
+popd > /dev/null
+
+# Bail on any error
+set -o pipefail
+trap 'exit 1' ERR
+
+# Create the two sources
+pushd $TOP > /dev/null
+m lockedregioncodeinjection
+m lockedregioncodeinjection_input
+popd > /dev/null
+
+# Create a temporary directory outside of the workspace.
+OUT=$TOP/out/host/test/lockedregioncodeinjection
+echo
+
+# Clean the directory
+if [[ -d $OUT ]]; then rm -r $OUT; fi
+mkdir -p $OUT
+
+ROOT=$TOP/out/host/linux-x86
+EXE=$ROOT/bin/lockedregioncodeinjection
+INP=$ROOT/framework/lockedregioncodeinjection_input.jar
+
+# Run tool on unit tests.
+$EXE \
+    -i $INP -o $OUT/test_output.jar \
+    --targets 'Llockedregioncodeinjection/TestTarget;' \
+    --pre     'lockedregioncodeinjection/TestTarget.boost' \
+    --post    'lockedregioncodeinjection/TestTarget.unboost' \
+    --scoped  'Llockedregioncodeinjection/TestScopedLock;,monitorEnter,monitorExit'
+
+# Run unit tests.
+java -ea -cp $OUT/test_output.jar \
+    org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain
+
+# Extract the class files and decompile them for possible post-analysis.
+pushd $OUT > /dev/null
+jar -x --file test_output.jar lockedregioncodeinjection
+for class in lockedregioncodeinjection/*.class; do
+  javap -c -v $class > ${class%.class}.asm
+done
+popd > /dev/null
+
+echo "artifacts are in $OUT"