Add an undocumented --incremental option to dx.

Change-Id: I48879b2f724e9b92c99c669803f9c8de01487327
diff --git a/dx/src/com/android/dx/cf/direct/ClassPathOpener.java b/dx/src/com/android/dx/cf/direct/ClassPathOpener.java
index 4e8c435..7621bf7 100644
--- a/dx/src/com/android/dx/cf/direct/ClassPathOpener.java
+++ b/dx/src/com/android/dx/cf/direct/ClassPathOpener.java
@@ -58,12 +58,13 @@
          * @param name {@code non-null;} filename of element. May not be a valid
          * filesystem path.
          *
+         * @param lastModified milliseconds since 1970-Jan-1 00:00:00 GMT
          * @param bytes {@code non-null;} file data
          * @return true on success. Result is or'd with all other results
          * from {@code processFileBytes} and returned to the caller
          * of {@code process()}.
          */
-        boolean processFileBytes(String name, byte[] bytes);
+        boolean processFileBytes(String name, long lastModified, byte[] bytes);
 
         /**
          * Informs consumer that an exception occurred while processing
@@ -131,7 +132,7 @@
             }
 
             byte[] bytes = FileUtils.readFile(file);
-            return consumer.processFileBytes(path, bytes);
+            return consumer.processFileBytes(path, file.lastModified(), bytes);
         } catch (Exception ex) {
             consumer.onException(ex);
             return false;
@@ -241,7 +242,7 @@
             in.close();
 
             byte[] bytes = baos.toByteArray();
-            any |= consumer.processFileBytes(path, bytes);
+            any |= consumer.processFileBytes(path, one.getTime(), bytes);
         }
 
         zip.close();
diff --git a/dx/src/com/android/dx/command/annotool/AnnotationLister.java b/dx/src/com/android/dx/command/annotool/AnnotationLister.java
index a29e5ba..6584b60 100644
--- a/dx/src/com/android/dx/command/annotool/AnnotationLister.java
+++ b/dx/src/com/android/dx/command/annotool/AnnotationLister.java
@@ -63,7 +63,7 @@
 
             opener = new ClassPathOpener(path, true,
                     new ClassPathOpener.Consumer() {
-                public boolean processFileBytes(String name, byte[] bytes) {
+                public boolean processFileBytes(String name, long lastModified, byte[] bytes) {
                     if (!name.endsWith(".class")) {
                         return true;
                     }
diff --git a/dx/src/com/android/dx/command/dexer/Main.java b/dx/src/com/android/dx/command/dexer/Main.java
index b268761..b261d31 100644
--- a/dx/src/com/android/dx/command/dexer/Main.java
+++ b/dx/src/com/android/dx/command/dexer/Main.java
@@ -17,10 +17,11 @@
 package com.android.dx.command.dexer;
 
 import com.android.dx.Version;
-import com.android.dx.cf.iface.ParseException;
 import com.android.dx.cf.direct.ClassPathOpener;
+import com.android.dx.cf.iface.ParseException;
 import com.android.dx.command.DxConsole;
 import com.android.dx.command.UsageException;
+import com.android.dx.dex.DexFormat;
 import com.android.dx.dex.cf.CfOptions;
 import com.android.dx.dex.cf.CfTranslator;
 import com.android.dx.dex.cf.CodeStatistics;
@@ -28,21 +29,26 @@
 import com.android.dx.dex.file.ClassDefItem;
 import com.android.dx.dex.file.DexFile;
 import com.android.dx.dex.file.EncodedMethod;
+import com.android.dx.io.DexBuffer;
+import com.android.dx.merge.DexMerger;
 import com.android.dx.rop.annotation.Annotation;
 import com.android.dx.rop.annotation.Annotations;
 import com.android.dx.rop.annotation.AnnotationsList;
 import com.android.dx.rop.cst.CstNat;
 import com.android.dx.rop.cst.CstUtf8;
-
+import com.android.dx.util.FileUtils;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.ExecutorService;
@@ -52,6 +58,7 @@
 import java.util.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
+import java.util.zip.ZipFile;
 
 /**
  * Main class for the class file translator.
@@ -95,12 +102,6 @@
         "lead to pain, suffering, grief, and lamentation.\n";
 
     /**
-     * {@code non-null;} name for the {@code .dex} file that goes into
-     * {@code .jar} files
-     */
-    private static final String DEX_IN_JAR_NAME = "classes.dex";
-
-    /**
      * {@code non-null;} name of the standard manifest file in {@code .jar}
      * files
      */
@@ -148,6 +149,9 @@
     /** true if any files are successfully processed */
     private static boolean anyFilesProcessed;
 
+    /** class files older than this must be defined in the target dex file. */
+    private static long minimumFileAge = 0;
+
     /**
      * This class is uninstantiable.
      */
@@ -159,7 +163,7 @@
      * Run and exit if something unexpected happened.
      * @param argArray the command line arguments
      */
-    public static void main(String[] argArray) {
+    public static void main(String[] argArray) throws IOException {
         Arguments arguments = new Arguments();
         arguments.parse(argArray);
 
@@ -174,7 +178,7 @@
      * @param arguments the data + parameters for the conversion
      * @return 0 if success > 0 otherwise.
      */
-    public static int run(Arguments arguments) {
+    public static int run(Arguments arguments) throws IOException {
         // Reset the error/warning count to start fresh.
         warnings = 0;
         errors = 0;
@@ -182,16 +186,29 @@
         args = arguments;
         args.makeCfOptions();
 
+        File outFile = new File(args.outName);
+        if (args.incremental && outFile.exists()) {
+            minimumFileAge = outFile.lastModified();
+        }
+
         if (!processAllFiles()) {
             return 1;
         }
 
+        if (args.incremental && !anyFilesProcessed) {
+            return 0; // this was a no-op incremental build
+        }
+
         byte[] outArray = writeDex();
 
         if (outArray == null) {
             return 2;
         }
 
+        if (args.incremental && outFile.exists()) {
+            outArray = merge(outArray, outFile);
+        }
+
         if (args.jarOutput) {
             // Effectively free up the (often massive) DexFile memory.
             outputDex = null;
@@ -199,12 +216,42 @@
             if (!createJar(args.outName, outArray)) {
                 return 3;
             }
+        } else if (args.outName != null) {
+            OutputStream out = openOutput(args.outName);
+            out.write(outArray);
+            closeOutput(out);
         }
 
         return 0;
     }
 
     /**
+     * Merges the dex files {@code update} and {@code base}, preferring
+     * {@code update}'s definition for types defined in both dex files. Returns
+     * the bytes of the merged dex file.
+     */
+    private static byte[] merge(byte[] update, File base) throws IOException {
+        DexBuffer dexA = new DexBuffer();
+        dexA.loadFrom(new ByteArrayInputStream(update));
+
+        DexBuffer dexB = new DexBuffer();
+        if (args.jarOutput) {
+            ZipFile zipFile = new ZipFile(base);
+            dexB.loadFrom(zipFile.getInputStream(zipFile.getEntry(DexFormat.DEX_IN_JAR_NAME)));
+            zipFile.close();
+        } else {
+            InputStream in = new FileInputStream(base);
+            dexB.loadFrom(in);
+            in.close();
+        }
+
+        DexBuffer merged = new DexMerger(dexA, dexB).merge();
+        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+        merged.writeTo(bytesOut);
+        return bytesOut.toByteArray();
+    }
+
+    /**
      * Constructs the output {@link DexFile}, fill it in with all the
      * specified classes, and populate the resources map if required.
      *
@@ -261,6 +308,10 @@
             return false;
         }
 
+        if (args.incremental && !anyFilesProcessed) {
+            return true;
+        }
+
         if (!(anyFilesProcessed || args.emptyOk)) {
             DxConsole.err.println("no classfiles specified");
             return false;
@@ -286,12 +337,12 @@
 
         opener = new ClassPathOpener(pathname, false,
                 new ClassPathOpener.Consumer() {
-            public boolean processFileBytes(String name, byte[] bytes) {
+            public boolean processFileBytes(String name, long lastModified, byte[] bytes) {
                 if (args.numThreads > 1) {
-                    threadPool.execute(new ParallelProcessor(name, bytes));
+                    threadPool.execute(new ParallelProcessor(name, lastModified, bytes));
                     return false;
                 } else {
-                    return Main.processFileBytes(name, bytes);
+                    return Main.processFileBytes(name, lastModified, bytes);
                 }
             }
             public void onException(Exception ex) {
@@ -320,7 +371,7 @@
      * @param bytes {@code non-null;} contents of the file
      * @return whether processing was successful
      */
-    private static boolean processFileBytes(String name, byte[] bytes) {
+    private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
         boolean isClass = name.endsWith(".class");
         boolean keepResources = (outputResources != null);
 
@@ -343,6 +394,9 @@
                     outputResources.put(fixedName, bytes);
                 }
             }
+            if (lastModified < minimumFileAge) {
+                return true;
+            }
             return processClass(fixedName, bytes);
         } else {
             synchronized (outputResources) {
@@ -426,9 +480,8 @@
     }
 
     /**
-     * Converts {@link #outputDex} into a {@code byte[]}, write
-     * it out to the proper file (if any), and also do whatever human-oriented
-     * dumping is required.
+     * Converts {@link #outputDex} into a {@code byte[]} and do whatever
+     * human-oriented dumping is required.
      *
      * @return {@code null-ok;} the converted {@code byte[]} or {@code null}
      * if there was a problem
@@ -437,7 +490,6 @@
         byte[] outArray = null;
 
         try {
-            OutputStream out = null;
             OutputStream humanOutRaw = null;
             OutputStreamWriter humanOut = null;
             try {
@@ -460,11 +512,6 @@
                      * and write it, dump it, etc.
                      */
                     outArray = outputDex.toDex(humanOut, args.verboseDump);
-
-                    if ((args.outName != null) && !args.jarOutput) {
-                        out = openOutput(args.outName);
-                        out.write(outArray);
-                    }
                 }
 
                 if (args.statistics) {
@@ -474,7 +521,6 @@
                 if (humanOut != null) {
                     humanOut.flush();
                 }
-                closeOutput(out);
                 closeOutput(humanOutRaw);
             }
         } catch (Exception ex) {
@@ -511,7 +557,7 @@
             OutputStream out = openOutput(fileName);
             JarOutputStream jarOut = new JarOutputStream(out, manifest);
 
-            outputResources.put(DEX_IN_JAR_NAME, dexArray);
+            outputResources.put(DexFormat.DEX_IN_JAR_NAME, dexArray);
 
             try {
                 for (Map.Entry<String, byte[]> e :
@@ -580,7 +626,7 @@
         createdBy += "dx " + Version.VERSION;
 
         attribs.put(CREATED_BY, createdBy);
-        attribs.putValue("Dex-Location", DEX_IN_JAR_NAME);
+        attribs.putValue("Dex-Location", DexFormat.DEX_IN_JAR_NAME);
 
         return manifest;
     }
@@ -816,6 +862,9 @@
         /** whether to keep local variable information */
         public boolean localInfo = true;
 
+        /** whether to merge with the output dex file if it exists. */
+        public boolean incremental = false;
+
         /** {@code non-null after {@link #parse};} file name arguments */
         public String[] fileNames;
 
@@ -885,9 +934,7 @@
                     keepClassesInJar = true;
                 } else if (arg.startsWith("--output=")) {
                     outName = arg.substring(arg.indexOf('=') + 1);
-                    if (outName.endsWith(".zip") ||
-                            outName.endsWith(".jar") ||
-                            outName.endsWith(".apk")) {
+                    if (FileUtils.hasArchiveSuffix(outName)) {
                         jarOutput = true;
                     } else if (outName.endsWith(".dex") ||
                                outName.equals("-")) {
@@ -923,6 +970,8 @@
                 } else if (arg.startsWith("--num-threads=")) {
                     arg = arg.substring(arg.indexOf('=') + 1);
                     numThreads = Integer.parseInt(arg);
+                } else if (arg.equals("--incremental")) {
+                    incremental = true;
                 } else {
                     System.err.println("unknown option: " + arg);
                     throw new UsageException();
@@ -973,6 +1022,7 @@
     private static class ParallelProcessor implements Runnable {
 
         String path;
+        long lastModified;
         byte[] bytes;
 
         /**
@@ -982,8 +1032,9 @@
          * filesystem path.
          * @param bytes {@code non-null;} file data
          */
-        private ParallelProcessor(String path, byte bytes[]) {
+        private ParallelProcessor(String path, long lastModified, byte bytes[]) {
             this.path = path;
+            this.lastModified = lastModified;
             this.bytes = bytes;
         }
 
@@ -992,7 +1043,7 @@
          * with the given path and bytes.
          */
         public void run() {
-            if (Main.processFileBytes(path, bytes)) {
+            if (Main.processFileBytes(path, lastModified, bytes)) {
                 anyFilesProcessed = true;
             }
         }
diff --git a/dx/src/com/android/dx/command/findusages/FindUsages.java b/dx/src/com/android/dx/command/findusages/FindUsages.java
index 59e5cce..9c62a5a 100644
--- a/dx/src/com/android/dx/command/findusages/FindUsages.java
+++ b/dx/src/com/android/dx/command/findusages/FindUsages.java
@@ -64,7 +64,7 @@
                     CodeReader.Instruction instruction, short[] instructions, int offset) {
                 int field = instructions[offset + 1];
                 if (fieldIds.contains(field)) {
-                    out.println("Field referenced by " + location() + " " + instruction);
+                    out.println(location() + ": field reference (" + instruction + ")");
                 }
             }
         });
@@ -74,7 +74,7 @@
                     CodeReader.Instruction instruction, short[] instructions, int offset) {
                 int methodId = instructions[offset + 1];
                 if (methodIds.contains(methodId)) {
-                    out.println("Method referenced by " + location() + " " + instruction);
+                    out.println(location() + ": method reference (" + instruction + ")");
                 }
             }
         });
@@ -84,7 +84,7 @@
         String className = dex.typeNames().get(currentClass.getTypeIndex());
         if (currentMethod != null) {
             MethodId methodId = dex.methodIds().get(currentMethod.getMethodIndex());
-            return className + "#" + dex.strings().get(methodId.getNameIndex());
+            return className + "." + dex.strings().get(methodId.getNameIndex());
         } else {
             return className;
         }
@@ -109,14 +109,14 @@
             ClassData classData = dex.readClassData(classDef);
             for (ClassData.Field field : classData.allFields()) {
                 if (fieldIds.contains(field.getFieldIndex())) {
-                    out.println("Field declared by " + location());
+                    out.println(location() + " field declared");
                 }
             }
 
             for (ClassData.Method method : classData.allMethods()) {
                 currentMethod = method;
                 if (methodIds.contains(method.getMethodIndex())) {
-                    out.println("Method declared by " + location());
+                    out.println(location() + " method declared");
                 }
                 if (method.getCodeOffset() != 0) {
                     codeReader.visitAll(dex.readCode(method).getInstructions());
diff --git a/dx/src/com/android/dx/command/findusages/Main.java b/dx/src/com/android/dx/command/findusages/Main.java
index 4b70daf..c3c203a 100644
--- a/dx/src/com/android/dx/command/findusages/Main.java
+++ b/dx/src/com/android/dx/command/findusages/Main.java
@@ -16,9 +16,13 @@
 
 package com.android.dx.command.findusages;
 
+import com.android.dx.dex.DexFormat;
 import com.android.dx.io.DexBuffer;
+import com.android.dx.util.FileUtils;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipFile;
 
 public final class Main {
     public static void main(String[] args) throws IOException {
@@ -27,7 +31,14 @@
         String memberName = args[2];
 
         DexBuffer dex = new DexBuffer();
-        dex.loadFrom(new File(dexFile));
+        if (FileUtils.hasArchiveSuffix(dexFile)) {
+            ZipFile zip = new ZipFile(dexFile);
+            InputStream in = zip.getInputStream(zip.getEntry(DexFormat.DEX_IN_JAR_NAME));
+            dex.loadFrom(in);
+            zip.close();
+        } else {
+            dex.loadFrom(new File(dexFile));
+        }
 
         new FindUsages(dex, declaredBy, memberName, System.out).findUsages();
     }
diff --git a/dx/src/com/android/dx/dex/DexFormat.java b/dx/src/com/android/dx/dex/DexFormat.java
index 2ad01b2..4b83901 100644
--- a/dx/src/com/android/dx/dex/DexFormat.java
+++ b/dx/src/com/android/dx/dex/DexFormat.java
@@ -19,6 +19,7 @@
 public final class DexFormat {
     private DexFormat() {}
 
+    public static final String DEX_IN_JAR_NAME = "classes.dex";
     public static final String MAGIC = "dex\n035\0";
     public static final int ENDIAN_TAG = 0x12345678;
 }
diff --git a/dx/src/com/android/dx/io/DexBuffer.java b/dx/src/com/android/dx/io/DexBuffer.java
index 7f21702..833f18d 100644
--- a/dx/src/com/android/dx/io/DexBuffer.java
+++ b/dx/src/com/android/dx/io/DexBuffer.java
@@ -125,7 +125,8 @@
         }
 
         this.data = bytesOut.toByteArray();
-        tableOfContents.readFrom(this);
+        this.length = data.length;
+        this.tableOfContents.readFrom(this);
     }
 
     public void loadFrom(File file) throws IOException {
diff --git a/dx/src/com/android/dx/merge/DexMerger.java b/dx/src/com/android/dx/merge/DexMerger.java
index a5c7874..fce0091 100644
--- a/dx/src/com/android/dx/merge/DexMerger.java
+++ b/dx/src/com/android/dx/merge/DexMerger.java
@@ -37,7 +37,6 @@
 public final class DexMerger {
     private static final Logger logger = Logger.getLogger(DexMerger.class.getName());
 
-    private final File dexOut;
     private final DexBuffer dexWriter = new DexBuffer();
     private final DexBuffer.Section headerWriter;
     private final DexBuffer.Section idsDefsWriter;
@@ -54,26 +53,19 @@
     private final DexBuffer.Section annotationsDirectoryWriter;
     private final TableOfContents contentsOut;
 
-    private final DexBuffer dexA = new DexBuffer();
-    private final DexBuffer dexB = new DexBuffer();
+    private final DexBuffer dexA;
+    private final DexBuffer dexB;
     private final IndexMap aIndexMap;
     private final IndexMap bIndexMap;
     private final InstructionTransformer aInstructionTransformer;
     private final InstructionTransformer bInstructionTransformer;
 
-    public DexMerger(File dexOut, File a, File b) throws IOException {
-        if (!a.exists() || !b.exists()) {
-            throw new IllegalArgumentException();
-        }
-
-        this.dexOut = dexOut;
-
-        dexA.loadFrom(a);
-        dexB.loadFrom(b);
+    public DexMerger(DexBuffer dexA, DexBuffer dexB) throws IOException {
+        this.dexA = dexA;
+        this.dexB = dexB;
 
         TableOfContents aContents = dexA.getTableOfContents();
         TableOfContents bContents = dexB.getTableOfContents();
-
         aIndexMap = new IndexMap(dexWriter, aContents);
         bIndexMap = new IndexMap(dexWriter, bContents);
         aInstructionTransformer = new InstructionTransformer(aIndexMap);
@@ -172,7 +164,7 @@
         contentsOut.dataSize = dexWriter.getLength() - contentsOut.dataOff;
     }
 
-    public void merge() throws IOException {
+    public DexBuffer merge() throws IOException {
         long start = System.nanoTime();
 
         mergeStringIds();
@@ -191,11 +183,19 @@
 
         // close (and flush) the result, then reopen to generate and write the hashes
         new DexHasher().writeHashes(dexWriter);
-        dexWriter.writeTo(dexOut);
 
         long elapsed = System.nanoTime() - start;
-        logger.info(String.format("Merged. Result length=%.1fKiB. Took %.1fs",
-                dexOut.length() / 1024f, elapsed / 1000000000f));
+        logger.info(String.format("Merged dex A (%d defs/%.1fKiB) with dex B "
+                + "(%d defs/%.1fKiB). Result is %d defs/%.1fKiB. Took %.1fs",
+                dexA.getTableOfContents().classDefs.size,
+                dexA.getLength() / 1024f,
+                dexB.getTableOfContents().classDefs.size,
+                dexB.getLength() / 1024f,
+                contentsOut.classDefs.size,
+                dexWriter.getLength() / 1024f,
+                elapsed / 1000000000f));
+
+        return dexWriter;
     }
 
     /**
@@ -410,7 +410,9 @@
 
         // Strip nulls from the end
         int firstNull = Arrays.asList(sortableTypes).indexOf(null);
-        return Arrays.copyOfRange(sortableTypes, 0, firstNull);
+        return firstNull != -1
+                ? Arrays.copyOfRange(sortableTypes, 0, firstNull)
+                : sortableTypes;
     }
 
     /**
@@ -623,7 +625,13 @@
             return;
         }
 
-        new DexMerger(new File(args[0]), new File(args[1]), new File(args[2])).merge();
+        DexBuffer dexA = new DexBuffer();
+        dexA.loadFrom(new File(args[1]));
+        DexBuffer dexB = new DexBuffer();
+        dexB.loadFrom(new File(args[2]));
+
+        DexBuffer merged = new DexMerger(dexA, dexB).merge();
+        merged.writeTo(new File(args[0]));
     }
 
     private static void printUsage() {
diff --git a/dx/src/com/android/dx/util/FileUtils.java b/dx/src/com/android/dx/util/FileUtils.java
index 098c5ab..bcf6729 100644
--- a/dx/src/com/android/dx/util/FileUtils.java
+++ b/dx/src/com/android/dx/util/FileUtils.java
@@ -89,4 +89,13 @@
 
         return result;
     }
+
+    /**
+     * Returns true if {@code fileName} names a .zip, .jar, or .apk.
+     */
+    public static boolean hasArchiveSuffix(String fileName) {
+        return fileName.endsWith(".zip")
+                || fileName.endsWith(".jar")
+                || fileName.endsWith(".apk");
+    }
 }
diff --git a/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java b/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java
index 2cac97d..5b43455 100644
--- a/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java
+++ b/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java
@@ -16,6 +16,7 @@
 
 package com.android.dx.merge;
 
+import com.android.dx.io.DexBuffer;
 import dalvik.system.PathClassLoader;
 import java.io.File;
 import java.io.FileInputStream;
@@ -91,10 +92,13 @@
     }
 
     public ClassLoader mergeAndLoad(String dexAResource, String dexBResource) throws IOException {
-        File dexA = resourceToFile(dexAResource);
-        File dexB = resourceToFile(dexBResource);
+        DexBuffer dexA = new DexBuffer();
+        DexBuffer dexB = new DexBuffer();
+        dexA.loadFrom(resourceToFile(dexAResource));
+        dexB.loadFrom(resourceToFile(dexBResource));
+        DexBuffer merged = new DexMerger(dexA, dexB).merge();
         File mergedDex = File.createTempFile("DexMergeTest", ".classes.dex");
-        new DexMerger(mergedDex, dexA, dexB).merge();
+        merged.writeTo(mergedDex);
         File mergedJar = dexToJar(mergedDex);
         return new PathClassLoader(mergedJar.getPath(), getClass().getClassLoader());
     }
diff --git a/dx/tests/118-find-usages/Foo.java b/dx/tests/118-find-usages/Foo.java
new file mode 100644
index 0000000..d5dc0bd
--- /dev/null
+++ b/dx/tests/118-find-usages/Foo.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.util.AbstractList;
+import java.util.ArrayList;
+
+public final class Foo {
+
+    public void writeStreamTokenizerNval() {
+        new StreamTokenizer((Reader) null).nval = 5;
+    }
+
+    public double readStreamTokenizerNval() {
+        return new StreamTokenizer((Reader) null).nval;
+    }
+
+    public void callStringValueOf() {
+        String.valueOf(5);
+    }
+
+    public void callIntegerValueOf() {
+        Integer.valueOf("5");
+    }
+
+    public void callArrayListRemoveIndex() {
+        new ArrayList<String>().remove(5);
+    }
+
+    public void callArrayListRemoveValue() {
+        new ArrayList<String>().remove("5");
+    }
+
+    static class MyList<T> extends AbstractList<T> {
+        @Override public T get(int index) {
+            return null;
+        }
+        @Override public int size() {
+            return 0;
+        }
+        @Override public boolean remove(Object o) {
+            return false;
+        }
+    }
+}
diff --git a/dx/tests/118-find-usages/expected.txt b/dx/tests/118-find-usages/expected.txt
new file mode 100644
index 0000000..458a7ac
--- /dev/null
+++ b/dx/tests/118-find-usages/expected.txt
@@ -0,0 +1,9 @@
+StreamTokenizer.nval
+Field referenced by LFoo;#readStreamTokenizerNval iget-wide vA, vB, field@CCCC
+Field referenced by LFoo;#writeStreamTokenizerNval iput-wide vA, vB, field@CCCC
+ArrayList.remove()
+Method referenced by LFoo;#callArrayListRemoveIndex invoke-virtual {vD, vE, vF, vG, vA}, meth@CCCC
+Method referenced by LFoo;#callArrayListRemoveValue invoke-virtual {vD, vE, vF, vG, vA}, meth@CCCC
+Collection.remove()
+String.valueOf()
+Method referenced by LFoo;#callStringValueOf invoke-static {vD, vE, vF, vG, vA}, meth@CCCC
diff --git a/dx/tests/118-find-usages/info.txt b/dx/tests/118-find-usages/info.txt
new file mode 100644
index 0000000..2a4e8a6
--- /dev/null
+++ b/dx/tests/118-find-usages/info.txt
@@ -0,0 +1,3 @@
+Creates a .dex file and runs find usages on it to find references and declarations.
+
+The expected output assumes this bug has not yet been fixed: http://b/3366285
\ No newline at end of file
diff --git a/dx/tests/118-find-usages/run b/dx/tests/118-find-usages/run
new file mode 100644
index 0000000..22f38cc
--- /dev/null
+++ b/dx/tests/118-find-usages/run
@@ -0,0 +1,30 @@
+#!/bin/bash
+#
+# Copyright (C) 2011 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.
+
+$JAVAC -d . *.java
+dx --output=foo.dex --dex *.class
+
+echo "StreamTokenizer.nval"
+dx --find-usages foo.dex "Ljava/io/StreamTokenizer;" nval
+
+echo "ArrayList.remove()"
+dx --find-usages foo.dex "Ljava/util/ArrayList;" remove
+
+echo "Collection.remove()"
+dx --find-usages foo.dex "Ljava/util/Collection;" remove
+
+echo "String.valueOf()"
+dx --find-usages foo.dex "Ljava/lang/String;" valueOf