Merge remote-tracking branch 'upstream/master'
diff --git a/src/main/java/com/google/dexmaker/AppDataDirGuesser.java b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
new file mode 100644
index 0000000..2492ea0
--- /dev/null
+++ b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 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 com.google.dexmaker;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Uses heuristics to guess the application's private data directory.
+ */
+class AppDataDirGuesser {
+    public File guess() {
+        try {
+            ClassLoader classLoader = guessSuitableClassLoader();
+            // Check that we have an instance of the PathClassLoader.
+            Class<?> clazz = Class.forName("dalvik.system.PathClassLoader");
+            clazz.cast(classLoader);
+            // Use the toString() method to calculate the data directory.
+            String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader);
+            File[] results = guessPath(pathFromThisClassLoader);
+            if (results.length > 0) {
+                return results[0];
+            }
+        } catch (ClassCastException ignored) {
+        } catch (ClassNotFoundException ignored) {
+        }
+        return null;
+    }
+
+    private ClassLoader guessSuitableClassLoader() {
+        return AppDataDirGuesser.class.getClassLoader();
+    }
+
+    private String getPathFromThisClassLoader(ClassLoader classLoader) {
+        // Parsing toString() method: yuck.  But no other way to get the path.
+        // Strip out the bit between angle brackets, that's our path.
+        String result = classLoader.toString();
+        int index = result.lastIndexOf('[');
+        result = (index == -1) ? result : result.substring(index + 1);
+        index = result.indexOf(']');
+        return (index == -1) ? result : result.substring(0, index);
+    }
+
+    File[] guessPath(String input) {
+        List<File> results = new ArrayList<File>();
+        for (String potential : input.split(":")) {
+            if (!potential.startsWith("/data/app/")) {
+                continue;
+            }
+            int start = "/data/app/".length();
+            int end = potential.lastIndexOf(".apk");
+            if (end != potential.length() - 4) {
+                continue;
+            }
+            int dash = potential.indexOf("-");
+            if (dash != -1) {
+                end = dash;
+            }
+            File file = new File("/data/data/" + potential.substring(start, end) + "/cache");
+            if (isWriteableDirectory(file)) {
+                results.add(file);
+            }
+        }
+        return results.toArray(new File[results.size()]);
+    }
+
+    boolean isWriteableDirectory(File file) {
+        return file.isDirectory() && file.canWrite();
+    }
+}
diff --git a/src/main/java/com/google/dexmaker/Code.java b/src/main/java/com/google/dexmaker/Code.java
index 1562314..4ea5e67 100644
--- a/src/main/java/com/google/dexmaker/Code.java
+++ b/src/main/java/com/google/dexmaker/Code.java
@@ -587,7 +587,7 @@
     }
 
     /**
-     * Copies the value in {@code target} to the static field {@code fieldId}.
+     * Copies the value in the static field {@code fieldId} to {@code target}.
      */
     public <V> void sget(FieldId<?, V> fieldId, Local<V> target) {
         addInstruction(new ThrowingCstInsn(Rops.opGetStatic(target.type.ropType), sourcePosition,
@@ -782,8 +782,7 @@
     }
 
     /**
-     * Assigns {@code target} to the element of {@code array} at index {@code
-     * index}.
+     * Assigns the element at {@code index} in {@code array} to {@code target}.
      */
     public void aget(Local<?> target, Local<?> array, Local<Integer> index) {
         addInstruction(new ThrowingInsn(Rops.opAget(target.type.ropType), sourcePosition,
@@ -792,8 +791,7 @@
     }
 
     /**
-     * Sets the element at {@code index} in {@code array} the value in {@code
-     * source}.
+     * Assigns {@code source} to the element at {@code index} in {@code array}.
      */
     public void aput(Local<?> array, Local<Integer> index, Local<?> source) {
         addInstruction(new ThrowingInsn(Rops.opAput(source.type.ropType), sourcePosition,
diff --git a/src/main/java/com/google/dexmaker/DexMaker.java b/src/main/java/com/google/dexmaker/DexMaker.java
index 3566fb6..ae59740 100644
--- a/src/main/java/com/google/dexmaker/DexMaker.java
+++ b/src/main/java/com/google/dexmaker/DexMaker.java
@@ -326,20 +326,42 @@
     /**
      * Generates a dex file and loads its types into the current process.
      *
-     * <p>All parameters are optional; you may pass {@code null} and suitable
-     * defaults will be used.
+     * <h3>Picking a dex cache directory</h3>
+     * The {@code dexCache} should be an application-private directory. If
+     * you pass a world-writable directory like {@code /sdcard} a malicious app
+     * could inject code into your process. Most applications should use this:
+     * <pre>   {@code
      *
-     * <p>If you opt to provide your own {@code dexDir}, take care to ensure
-     * that it is not world-writable, otherwise a malicious app may be able
-     * to inject code into your process.  A suitable parameter is:
-     * {@code getApplicationContext().getDir("dx", Context.MODE_PRIVATE); }
+     *     File dexCache = getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
+     * }</pre>
+     * If the {@code dexCache} is null, this method will consult the {@code
+     * dexmaker.dexcache} system property. If that exists, it will be used for
+     * the dex cache. If it doesn't exist, this method will attempt to guess
+     * the application's private data directory as a last resort. If that fails,
+     * this method will fail with an unchecked exception. You can avoid the
+     * exception by either providing a non-null value or setting the system
+     * property.
      *
-     * @param parent the parent ClassLoader to be used when loading
-     *     our generated types
-     * @param dexDir the destination directory where generated and
-     *     optimized dex files will be written.
+     * @param parent the parent ClassLoader to be used when loading our
+     *     generated types
+     * @param dexCache the destination directory where generated and optimized
+     *     dex files will be written. If null, this class will try to guess the
+     *     application's private data dir.
      */
-    public ClassLoader generateAndLoad(ClassLoader parent, File dexDir) throws IOException {
+    public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException {
+        if (dexCache == null) {
+            String property = System.getProperty("dexmaker.dexcache");
+            if (property != null) {
+                dexCache = new File(property);
+            } else {
+                dexCache = new AppDataDirGuesser().guess();
+                if (dexCache == null) {
+                    throw new IllegalArgumentException("dexcache == null (and no default could be"
+                            + " found; consider setting the 'dexmaker.dexcache' system property)");
+                }
+            }
+        }
+
         byte[] dex = generate();
 
         /*
@@ -349,7 +371,7 @@
          *
          * TODO: load the dex from memory where supported.
          */
-        File result = File.createTempFile("Generated", ".jar", dexDir);
+        File result = File.createTempFile("Generated", ".jar", dexCache);
         result.deleteOnExit();
         JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
         jarOut.putNextEntry(new JarEntry(DexFormat.DEX_IN_JAR_NAME));
@@ -359,7 +381,7 @@
         try {
             return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
                     .getConstructor(String.class, String.class, String.class, ClassLoader.class)
-                    .newInstance(result.getPath(), dexDir.getAbsolutePath(), null, parent);
+                    .newInstance(result.getPath(), dexCache.getAbsolutePath(), null, parent);
         } catch (ClassNotFoundException e) {
             throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
         } catch (InvocationTargetException e) {
diff --git a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
index f5b9c39..639b3dc 100644
--- a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
+++ b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
@@ -42,6 +42,7 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
 
 /**
  * Creates dynamic proxies of concrete classes.
@@ -126,12 +127,12 @@
             = Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
 
     private final Class<T> baseClass;
-    // TODO: make DexMaker do the defaulting here
     private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
     private InvocationHandler handler;
     private File dexCache;
     private Class<?>[] constructorArgTypes = new Class[0];
     private Object[] constructorArgValues = new Object[0];
+    private Set<Class<?>> interfaces = new HashSet<Class<?>>();
 
     private ProxyBuilder(Class<T> clazz) {
         baseClass = clazz;
@@ -156,10 +157,25 @@
         return this;
     }
 
+    /**
+     * Sets the directory where executable code is stored. See {@link
+     * DexMaker#generateAndLoad DexMaker.generateAndLoad()} for guidance on
+     * choosing a secure location for the dex cache.
+     */
     public ProxyBuilder<T> dexCache(File dexCache) {
         this.dexCache = dexCache;
         return this;
     }
+    
+    public ProxyBuilder<T> implementing(Class<?>... interfaces) {
+        for (Class<?> i : interfaces) {
+            if (!i.isInterface()) {
+                throw new IllegalArgumentException("Not an interface: " + i.getName());
+            }
+            this.interfaces.add(i);
+        }
+        return this;
+    }
 
     public ProxyBuilder<T> constructorArgValues(Object... constructorArgValues) {
         this.constructorArgValues = constructorArgValues;
@@ -186,13 +202,13 @@
         check(handler != null, "handler == null");
         check(constructorArgTypes.length == constructorArgValues.length,
                 "constructorArgValues.length != constructorArgTypes.length");
-        Class<? extends T> proxyClass = getProxyClass();
+        Class<? extends T> proxyClass = buildProxyClass();
         Constructor<? extends T> constructor;
         try {
             constructor = proxyClass.getConstructor(constructorArgTypes);
         } catch (NoSuchMethodException e) {
-            // Thrown when the constructor to be called does not exist.
-            throw new IllegalArgumentException("could not find matching constructor", e);
+            throw new IllegalArgumentException("No constructor for " + baseClass.getName()
+                    + " with parameter types " + Arrays.toString(constructorArgTypes));
         }
         T result;
         try {
@@ -211,11 +227,15 @@
         return result;
     }
 
-    private Class<? extends T> getProxyClass() throws IOException {
+    // TODO: test coverage for this
+    // TODO: documentation for this
+    public Class<? extends T> buildProxyClass() throws IOException {
         // try the cache to see if we've generated this one before
         @SuppressWarnings("unchecked") // we only populate the map with matching types
         Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass);
-        if (proxyClass != null && proxyClass.getClassLoader().getParent() == parentClassLoader) {
+        if (proxyClass != null
+                && proxyClass.getClassLoader().getParent() == parentClassLoader
+                && interfaces.equals(asSet(proxyClass.getInterfaces()))) {
             return proxyClass; // cache hit!
         }
 
@@ -225,15 +245,17 @@
         TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";");
         TypeId<T> superType = TypeId.get(baseClass);
         generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
-        Method[] methodsToProxy = getMethodsToProxy(baseClass);
+        Method[] methodsToProxy = getMethodsToProxyRecursive();
         generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
-        dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType);
+        dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType,
+                getInterfacesAsTypeIds());
         ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
         try {
             proxyClass = loadClass(classLoader, generatedName);
         } catch (IllegalAccessError e) {
             // Thrown when the base class is not accessible.
-            throw new UnsupportedOperationException("cannot proxy inaccessible classes", e);
+            throw new UnsupportedOperationException(
+                    "cannot proxy inaccessible class " + baseClass, e);
         } catch (ClassNotFoundException e) {
             // Should not be thrown, we're sure to have generated this class.
             throw new AssertionError(e);
@@ -310,6 +332,21 @@
         }
     }
 
+    // TODO: test coverage for isProxyClass
+    
+    /**
+     * Returns true if {@code c} is a proxy class created by this builder.
+     */
+    public static boolean isProxyClass(Class<?> c) {
+        // TODO: use a marker interface instead?
+        try {
+            c.getDeclaredField(FIELD_NAME_HANDLER);
+            return true;
+        } catch (NoSuchFieldException e) {
+            return false;
+        }
+    }
+
     private static <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
             TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
         TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
@@ -441,14 +478,14 @@
             /*
              * And to allow calling the original super method, the following is also generated:
              *
-             *     public int super_doSomething(Bar param0, int param1) {
+             *     public String super$doSomething$java_lang_String(Bar param0, int param1) {
              *          int result = super.doSomething(param0, param1);
              *          return result;
              *     }
              */
-            String superName = "super_" + name;
+            // TODO: don't include a super_ method if the target is abstract!
             MethodId<G, ?> callsSuperMethod = generatedType.getMethod(
-                    resultType, superName, argTypes);
+                    resultType, superMethodName(method), argTypes);
             Code superCode = dexMaker.declare(callsSuperMethod, PUBLIC);
             Local<G> superThis = superCode.getThis(generatedType);
             Local<?>[] superArgs = new Local<?>[argClasses.length];
@@ -481,12 +518,24 @@
         return temp;
     }
 
-    public static Object callSuper(Object proxy, Method method, Object... args)
-            throws SecurityException, IllegalAccessException,
-            InvocationTargetException, NoSuchMethodException {
-        return proxy.getClass()
-                .getMethod("super_" + method.getName(), method.getParameterTypes())
-                .invoke(proxy, args);
+    public static Object callSuper(Object proxy, Method method, Object... args) throws Throwable {
+        try {
+            return proxy.getClass()
+                    .getMethod(superMethodName(method), method.getParameterTypes())
+                    .invoke(proxy, args);
+        } catch (InvocationTargetException e) {
+            throw e.getCause();
+        }
+    }
+
+    /**
+     * The super method must include the return type, otherwise its ambiguous
+     * for methods with covariant return types.
+     */
+    private static String superMethodName(Method method) {
+        String returnType = method.getReturnType().getName();
+        return "super$" + method.getName() + "$"
+                + returnType.replace('.', '_').replace('[', '_').replace(';', '_');
     }
 
     private static void check(boolean condition, String message) {
@@ -531,28 +580,28 @@
         return (Constructor<T>[]) clazz.getDeclaredConstructors();
     }
 
-    /**
-     * Gets all {@link Method} objects we can proxy in the hierarchy of the supplied class.
-     */
-    private static <T> Method[] getMethodsToProxy(Class<T> clazz) {
-        Set<MethodSetEntry> methodsToProxy = new HashSet<MethodSetEntry>();
-        for (Class<?> current = clazz; current != null; current = current.getSuperclass()) {
-            for (Method method : current.getDeclaredMethods()) {
-                if ((method.getModifiers() & Modifier.FINAL) != 0) {
-                    // Skip final methods, we can't override them.
-                    continue;
-                }
-                if ((method.getModifiers() & STATIC) != 0) {
-                    // Skip static methods, overriding them has no effect.
-                    continue;
-                }
-                if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) {
-                    // Skip finalize method, it's likely important that it execute as normal.
-                    continue;
-                }
-                methodsToProxy.add(new MethodSetEntry(method));
-            }
+    private TypeId<?>[] getInterfacesAsTypeIds() {
+        TypeId<?>[] result = new TypeId<?>[interfaces.size()];
+        int i = 0;
+        for (Class<?> implemented : interfaces) {
+            result[i++] = TypeId.get(implemented);
         }
+        return result;
+    }
+
+    /**
+     * Gets all {@link Method} objects we can proxy in the hierarchy of the
+     * supplied class.
+     */
+    private Method[] getMethodsToProxyRecursive() {
+        Set<MethodSetEntry> methodsToProxy = new HashSet<MethodSetEntry>();
+        for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) {
+            getMethodsToProxy(methodsToProxy, c);
+        }
+        for (Class<?> c : interfaces) {
+            getMethodsToProxy(methodsToProxy, c);
+        }
+
         Method[] results = new Method[methodsToProxy.size()];
         int i = 0;
         for (MethodSetEntry entry : methodsToProxy) {
@@ -561,6 +610,28 @@
         return results;
     }
 
+    private void getMethodsToProxy(Set<MethodSetEntry> sink, Class<?> c) {
+        for (Method method : c.getDeclaredMethods()) {
+            if ((method.getModifiers() & Modifier.FINAL) != 0) {
+                // Skip final methods, we can't override them.
+                continue;
+            }
+            if ((method.getModifiers() & STATIC) != 0) {
+                // Skip static methods, overriding them has no effect.
+                continue;
+            }
+            if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) {
+                // Skip finalize method, it's likely important that it execute as normal.
+                continue;
+            }
+            sink.add(new MethodSetEntry(method));
+        }
+        
+        for (Class<?> i : c.getInterfaces()) {
+            getMethodsToProxy(sink, i);
+        }
+    }
+
     private static <T> String getMethodNameForProxyOf(Class<T> clazz) {
         return clazz.getSimpleName() + "_Proxy";
     }
@@ -596,6 +667,10 @@
         }
     }
 
+    private static <T> Set<T> asSet(T... array) {
+        return new CopyOnWriteArraySet<T>(Arrays.asList(array));
+    }
+
     private static MethodId<?, ?> getUnboxMethodForPrimitive(Class<?> methodReturnType) {
         return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType);
     }
diff --git a/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
new file mode 100644
index 0000000..5c92f34
--- /dev/null
+++ b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 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 com.google.dexmaker;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+
+public final class AppDataDirGuesserTest extends TestCase {
+    public void testGuessCacheDir_SimpleExample() {
+        guessCacheDirFor("/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+        guessCacheDirFor("/data/app/a.b.c.tests.apk").shouldGive("/data/data/a.b.c.tests/cache");
+    }
+
+    public void testGuessCacheDir_MultipleResultsSeparatedByColon() {
+        guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+                .shouldGive("/data/data/a.b.c/cache", "/data/data/d.e.f/cache");
+    }
+
+    public void testGuessCacheDir_NotWriteableSkipped() {
+        guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+                .withNonWriteable("/data/data/a.b.c/cache")
+                .shouldGive("/data/data/d.e.f/cache");
+    }
+
+    public void testGuessCacheDir_StripHyphenatedSuffixes() {
+        guessCacheDirFor("/data/app/a.b.c-2.apk").shouldGive("/data/data/a.b.c/cache");
+    }
+
+    public void testGuessCacheDir_LeadingAndTrailingColonsIgnored() {
+        guessCacheDirFor("/data/app/a.b.c.apk:asdf:").shouldGive("/data/data/a.b.c/cache");
+        guessCacheDirFor(":asdf:/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+    }
+
+    public void testGuessCacheDir_InvalidInputsGiveEmptyArray() {
+        guessCacheDirFor("").shouldGive();
+    }
+
+    public void testGuessCacheDir_JarsIgnored() {
+        guessCacheDirFor("/data/app/a.b.c.jar").shouldGive();
+        guessCacheDirFor("/system/framework/android.test.runner.jar").shouldGive();
+    }
+
+    public void testGuessCacheDir_RealWorldExample() {
+        String realPath = "/system/framework/android.test.runner.jar:" +
+                "/data/app/com.google.android.voicesearch.tests-2.apk:" +
+                "/data/app/com.google.android.voicesearch-1.apk";
+        guessCacheDirFor(realPath)
+                .withNonWriteable("/data/data/com.google.android.voicesearch.tests/cache")
+                .shouldGive("/data/data/com.google.android.voicesearch/cache");
+    }
+
+    private interface TestCondition {
+        TestCondition withNonWriteable(String... files);
+        void shouldGive(String... files);
+    }
+
+    private TestCondition guessCacheDirFor(final String path) {
+        final Set<String> notWriteable = new HashSet<String>();
+        return new TestCondition() {
+            public void shouldGive(String... files) {
+                AppDataDirGuesser guesser = new AppDataDirGuesser() {
+                    @Override
+                    public boolean isWriteableDirectory(File file) {
+                        return !notWriteable.contains(file.getAbsolutePath());
+                    }
+                };
+                File[] results = guesser.guessPath(path);
+                assertNotNull("Null results for " + path, results);
+                assertEquals("Bad lengths for " + path, files.length, results.length);
+                for (int i = 0; i < files.length; ++i) {
+                    assertEquals("Element " + i, new File(files[i]), results[i]);
+                }
+            }
+
+            public TestCondition withNonWriteable(String... files) {
+                notWriteable.addAll(Arrays.asList(files));
+                return this;
+            }
+        };
+    }
+}
diff --git a/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java b/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
index d613053..1b65ea8 100644
--- a/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
+++ b/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
@@ -22,7 +22,10 @@
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.UndeclaredThrowableException;
+import java.util.Arrays;
 import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
 import junit.framework.AssertionFailedError;
 import junit.framework.TestCase;
 
@@ -164,7 +167,7 @@
         assertEquals(false, proxy.equals(proxy));
     }
 
-    public static class AllPrimitiveMethods {
+    public static class AllReturnTypes {
         public boolean getBoolean() { return true; }
         public int getInt() { return 1; }
         public byte getByte() { return 2; }
@@ -173,10 +176,12 @@
         public float getFloat() { return 5f; }
         public double getDouble() { return 6.0; }
         public char getChar() { return 'c'; }
+        public int[] getIntArray() { return new int[] { 8, 9 }; }
+        public String[] getStringArray() { return new String[] { "d", "e" }; }
     }
 
-    public void testAllPrimitiveReturnTypes() throws Throwable {
-        AllPrimitiveMethods proxy = proxyFor(AllPrimitiveMethods.class).build();
+    public void testAllReturnTypes() throws Throwable {
+        AllReturnTypes proxy = proxyFor(AllReturnTypes.class).build();
         fakeHandler.setFakeResult(false);
         assertEquals(false, proxy.getBoolean());
         fakeHandler.setFakeResult(8);
@@ -193,9 +198,13 @@
         assertEquals(13.0, proxy.getDouble());
         fakeHandler.setFakeResult('z');
         assertEquals('z', proxy.getChar());
+        fakeHandler.setFakeResult(new int[] { -1, -2 });
+        assertEquals("[-1, -2]", Arrays.toString(proxy.getIntArray()));
+        fakeHandler.setFakeResult(new String[] { "x", "y" });
+        assertEquals("[x, y]", Arrays.toString(proxy.getStringArray()));
     }
 
-    public static class PassThroughAllPrimitives {
+    public static class PassThroughAllTypes {
         public boolean getBoolean(boolean input) { return input; }
         public int getInt(int input) { return input; }
         public byte getByte(byte input) { return input; }
@@ -215,8 +224,8 @@
         }
     }
 
-    public void testPassThroughWorksForAllPrimitives() throws Exception {
-        PassThroughAllPrimitives proxy = proxyFor(PassThroughAllPrimitives.class)
+    public void testPassThroughWorksForAllTypes() throws Exception {
+        PassThroughAllTypes proxy = proxyFor(PassThroughAllTypes.class)
                 .handler(new InvokeSuperHandler())
                 .build();
         assertEquals(false, proxy.getBoolean(false));
@@ -244,12 +253,12 @@
         proxy.getNothing();
     }
 
-    public static class ExtendsAllPrimitiveMethods extends AllPrimitiveMethods {
+    public static class ExtendsAllReturnTypes extends AllReturnTypes {
         public int example() { return 0; }
     }
 
     public void testProxyWorksForSuperclassMethodsAlso() throws Throwable {
-        ExtendsAllPrimitiveMethods proxy = proxyFor(ExtendsAllPrimitiveMethods.class).build();
+        ExtendsAllReturnTypes proxy = proxyFor(ExtendsAllReturnTypes.class).build();
         fakeHandler.setFakeResult(99);
         assertEquals(99, proxy.example());
         assertEquals(99, proxy.getInt());
@@ -342,7 +351,7 @@
 
     public void testDefaultProxyHasSuperMethodToAccessOriginal() throws Exception {
         Object objectProxy = proxyFor(Object.class).build();
-        assertNotNull(objectProxy.getClass().getMethod("super_hashCode"));
+        assertNotNull(objectProxy.getClass().getMethod("super$hashCode$int"));
     }
 
     public static class PrintsOddAndValue {
@@ -371,6 +380,31 @@
         assertEquals("even 2", proxy.method(2));
         assertEquals("odd 3", proxy.method(3));
     }
+    
+    public void testCallSuperThrows() throws Exception {
+        InvocationHandler handler = new InvocationHandler() {
+            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+                return ProxyBuilder.callSuper(o, method, objects);
+            }
+        };
+
+        FooThrows fooThrows = proxyFor(FooThrows.class)
+                .handler(handler)
+                .build();
+
+        try {
+            fooThrows.foo();
+            fail();
+        } catch (IllegalStateException expected) {
+            assertEquals("boom!", expected.getMessage());
+        }
+    }
+    
+    public static class FooThrows {
+        public void foo() {
+            throw new IllegalStateException("boom!");
+        }
+    }
 
     public static class DoubleReturn {
         public double getValue() {
@@ -564,7 +598,166 @@
 
         assertTrue(a.getClass() != b.getClass());
     }
+    
+    public void testAbstractClassWithUndeclaredInterfaceMethod() throws Throwable {
+        DeclaresInterface declaresInterface = proxyFor(DeclaresInterface.class)
+                .build();
+        assertEquals("fake result", declaresInterface.call());
+        try {
+            ProxyBuilder.callSuper(declaresInterface, Callable.class.getMethod("call"));
+            fail();
+        } catch (AbstractMethodError expected) {
+        }
+    }
+    
+    public static abstract class DeclaresInterface implements Callable<String> {
+    }
 
+    public void testImplementingInterfaces() throws Throwable {
+        SimpleClass simpleClass = proxyFor(SimpleClass.class)
+                .implementing(Callable.class)
+                .implementing(Comparable.class)
+                .build();
+        assertEquals("fake result", simpleClass.simpleMethod());
+
+        Callable<?> asCallable = (Callable<?>) simpleClass;
+        assertEquals("fake result", asCallable.call());
+
+        Comparable<?> asComparable = (Comparable<?>) simpleClass;
+        fakeHandler.fakeResult = 3;
+        assertEquals(3, asComparable.compareTo(null));
+    }
+
+    public void testCallSuperWithInterfaceMethod() throws Throwable {
+        SimpleClass simpleClass = proxyFor(SimpleClass.class)
+                .implementing(Callable.class)
+                .build();
+        try {
+            ProxyBuilder.callSuper(simpleClass, Callable.class.getMethod("call"));
+            fail();
+        } catch (AbstractMethodError expected) {
+        } catch (NoSuchMethodError expected) {
+        }
+    }
+
+    public void testImplementInterfaceCallingThroughConcreteClass() throws Throwable {
+        InvocationHandler invocationHandler = new InvocationHandler() {
+            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+                assertEquals("a", ProxyBuilder.callSuper(o, method, objects));
+                return "b";
+            }
+        };
+        ImplementsCallable proxy = proxyFor(ImplementsCallable.class)
+                .implementing(Callable.class)
+                .handler(invocationHandler)
+                .build();
+        assertEquals("b", proxy.call());
+        assertEquals("a", ProxyBuilder.callSuper(
+                proxy, ImplementsCallable.class.getMethod("call")));
+    }
+    
+    /**
+     * This test is a bit unintuitive because it exercises the synthetic methods
+     * that support covariant return types. Calling 'Object call()' on the
+     * interface bridges to 'String call()', and so the super method appears to
+     * also be proxied.
+     */
+    public void testImplementInterfaceCallingThroughInterface() throws Throwable {
+        final AtomicInteger count = new AtomicInteger();
+
+        InvocationHandler invocationHandler = new InvocationHandler() {
+            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+                count.incrementAndGet();
+                return ProxyBuilder.callSuper(o, method, objects);
+            }
+        };
+
+        Callable<?> proxy = proxyFor(ImplementsCallable.class)
+                .implementing(Callable.class)
+                .handler(invocationHandler)
+                .build();
+
+        // the invocation handler is called twice!
+        assertEquals("a", proxy.call());
+        assertEquals(2, count.get());
+
+        // the invocation handler is called, even though this is a callSuper() call!
+        assertEquals("a", ProxyBuilder.callSuper(proxy, Callable.class.getMethod("call")));
+        assertEquals(3, count.get());
+    }
+    
+    public static class ImplementsCallable implements Callable<String> {
+        public String call() throws Exception {
+            return "a";
+        }
+    }
+
+    /**
+     * This test shows that our generated proxies follow the bytecode convention
+     * where methods can have the same name but unrelated return types. This is
+     * different from javac's convention where return types must be assignable
+     * in one direction or the other.
+     */
+    public void testInterfacesSameNamesDifferentReturnTypes() throws Throwable {
+        InvocationHandler handler = new InvocationHandler() {
+            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+                if (method.getReturnType() == void.class) {
+                    return null;
+                } else if (method.getReturnType() == String.class) {
+                    return "X";
+                } else if (method.getReturnType() == int.class) {
+                    return 3;
+                } else {
+                    throw new AssertionFailedError();
+                }
+            }
+        };
+        
+        Object o = proxyFor(Object.class)
+                .implementing(FooReturnsVoid.class, FooReturnsString.class, FooReturnsInt.class)
+                .handler(handler)
+                .build();
+        
+        FooReturnsVoid a = (FooReturnsVoid) o;
+        a.foo();
+        
+        FooReturnsString b = (FooReturnsString) o;
+        assertEquals("X", b.foo());
+        
+        FooReturnsInt c = (FooReturnsInt) o;
+        assertEquals(3, c.foo());
+    }
+    
+    public void testInterfacesSameNamesSameReturnType() throws Throwable {
+        Object o = proxyFor(Object.class)
+                .implementing(FooReturnsInt.class, FooReturnsInt2.class)
+                .build();
+        
+        fakeHandler.setFakeResult(3);
+
+        FooReturnsInt a = (FooReturnsInt) o;
+        assertEquals(3, a.foo());
+
+        FooReturnsInt2 b = (FooReturnsInt2) o;
+        assertEquals(3, b.foo());
+    }
+    
+    public interface FooReturnsVoid {
+        void foo();
+    }
+
+    public interface FooReturnsString {
+        String foo();
+    }
+
+    public interface FooReturnsInt {
+        int foo();
+    }
+    
+    public interface FooReturnsInt2 {
+        int foo();
+    }
+    
     private ClassLoader newPathClassLoader() throws Exception {
         return (ClassLoader) Class.forName("dalvik.system.PathClassLoader")
                 .getConstructor(String.class, ClassLoader.class)