aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnatol Pomozov <anatol.pomozov@gmail.com>2012-01-18 10:37:44 -0800
committerAnatol Pomozov <anatol.pomozov@gmail.com>2012-01-18 10:37:44 -0800
commitf225da938fc7f1fe77813cee6ceb33fea3d19f06 (patch)
tree6f548a0603b9b8f7df72dc91ac1b89a7b1d74fd9
parent01f8aeca8a11dbecc160e1ebe2c92f1dc30c2d44 (diff)
parent8ec4b1db3afa51730508be7064d2111b723ac2cd (diff)
downloaddexmaker-f225da938fc7f1fe77813cee6ceb33fea3d19f06.tar.gz
Merge remote-tracking branch 'upstream/master'
-rw-r--r--src/main/java/com/google/dexmaker/AppDataDirGuesser.java85
-rw-r--r--src/main/java/com/google/dexmaker/Code.java8
-rw-r--r--src/main/java/com/google/dexmaker/DexMaker.java48
-rw-r--r--src/main/java/com/google/dexmaker/stock/ProxyBuilder.java147
-rw-r--r--src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java98
-rw-r--r--src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java211
6 files changed, 534 insertions, 63 deletions
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 @@ public final class Code {
}
/**
- * 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 @@ public final class Code {
}
/**
- * 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 @@ public final class Code {
}
/**
- * 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 @@ public final class DexMaker {
/**
* 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 @@ public final class DexMaker {
*
* 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 @@ public final class DexMaker {
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.HashMap;
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 @@ public final class ProxyBuilder<T> {
= 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 @@ public final class ProxyBuilder<T> {
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 @@ public final class ProxyBuilder<T> {
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 @@ public final class ProxyBuilder<T> {
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 @@ public final class ProxyBuilder<T> {
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 @@ public final class ProxyBuilder<T> {
}
}
+ // 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 @@ public final class ProxyBuilder<T> {
/*
* 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 @@ public final class ProxyBuilder<T> {
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 @@ public final class ProxyBuilder<T> {
return (Constructor<T>[]) clazz.getDeclaredConstructors();
}
+ 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.
+ * Gets all {@link Method} objects we can proxy in the hierarchy of the
+ * supplied class.
*/
- private static <T> Method[] getMethodsToProxy(Class<T> clazz) {
+ private Method[] getMethodsToProxyRecursive() {
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));
- }
+ 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 @@ public final class ProxyBuilder<T> {
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 @@ public final class ProxyBuilder<T> {
}
}
+ 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.io.IOException;
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 @@ public class ProxyBuilderTest extends TestCase {
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 class ProxyBuilderTest extends TestCase {
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 @@ public class ProxyBuilderTest extends TestCase {
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 class ProxyBuilderTest extends TestCase {
}
}
- 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 @@ public class ProxyBuilderTest extends TestCase {
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 class ProxyBuilderTest extends TestCase {
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 @@ public class ProxyBuilderTest extends TestCase {
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 @@ public class ProxyBuilderTest extends TestCase {
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)