From afedbc47b3c8f3fa9955496e6ceb7f5702dd90a3 Mon Sep 17 00:00:00 2001 From: Deepanshu Gupta Date: Mon, 18 May 2015 18:47:07 -0700 Subject: [PATCH] Make Context.getClassLoader() work. [DO NOT MERGE] Context.getClassLoader() is used by the LayoutInflater and can be used by custom views. However, when called from the LayoutInflater, this needs to return only the Framework classes. This is so that the IDE gets a chance to instantiate the custom views, which helps in better error reporting and better fallback in case of exceptions, like MockView. To workaround this need of the same method returning different results based on where it's called from, the method call in LayoutInflater is renamed to getFrameworkClassLoader() and the new method is injected in Context. The implementation of getFrameworkClassLoader() maintains the existing behaviour of getClassLoader(). Context.getClassLoader() is now modified to return classes from both Framework and the app namespace. Also, update the list of packages to search for Framework views. Change-Id: I1a6be4aa1fc5c1c5520b5440a348a52f10b6eb3b (cherry picked from commit f8ea750455eec81e4e6d877b3e18e29a86d4ec95) --- .../bridge/src/android/view/BridgeInflater.java | 7 +- .../layoutlib/bridge/android/BridgeContext.java | 19 ++- .../layoutlib/bridge/impl/RenderAction.java | 1 - .../tools/layoutlib/create/AsmAnalyzer.java | 2 +- .../tools/layoutlib/create/AsmGenerator.java | 11 +- .../android/tools/layoutlib/create/CreateInfo.java | 15 +++ .../tools/layoutlib/create/ICreateInfo.java | 30 +++-- .../layoutlib/create/InjectMethodRunnables.java | 52 ++++++++ .../layoutlib/create/InjectMethodsAdapter.java | 41 +++++++ .../create/ReplaceMethodCallsAdapter.java | 67 ++++++++--- .../tools/layoutlib/create/AsmGeneratorTest.java | 133 +++++++++++++++++++++ 11 files changed, 347 insertions(+), 31 deletions(-) create mode 100644 tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java create mode 100644 tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java index 2e649c3ad0f6..b495e261c667 100644 --- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -53,9 +53,14 @@ public final class BridgeInflater extends LayoutInflater { */ private static final String[] sClassPrefixList = { "android.widget.", - "android.webkit." + "android.webkit.", + "android.app." }; + public static String[] getClassPrefixList() { + return sClassPrefixList; + } + protected BridgeInflater(LayoutInflater original, Context newContext) { super(original, newContext); newContext = getBaseContext(newContext); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 2cbbeba2c2c4..ac3a3ce3aa8f 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -137,8 +137,9 @@ public final class BridgeContext extends Context { private final Stack mParserStack = new Stack(); private SharedPreferences mSharedPreferences; + private ClassLoader mClassLoader; - /** + /** * @param projectKey An Object identifying the project. This is used for the cache mechanism. * @param metrics the {@link DisplayMetrics}. * @param renderResources the configured resources (both framework and projects) for this @@ -461,7 +462,21 @@ public final class BridgeContext extends Context { @Override public ClassLoader getClassLoader() { - return this.getClass().getClassLoader(); + if (mClassLoader == null) { + mClassLoader = new ClassLoader(getClass().getClassLoader()) { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + for (String prefix : BridgeInflater.getClassPrefixList()) { + if (name.startsWith(prefix)) { + // These are framework classes and should not be loaded from the app. + throw new ClassNotFoundException(name + " not found"); + } + } + return BridgeContext.this.mLayoutlibCallback.findClass(name); + } + }; + } + return mClassLoader; } @Override diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java index 2b95488cd5f3..f3a0d582f976 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java @@ -275,7 +275,6 @@ public abstract class RenderAction extends FrameworkReso mContext.getRenderResources().setLogger(null); } ParserFactory.setParserFactory(null); - } public static BridgeContext getCurrentContext() { diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java index aa51c466a31e..c8b2b8448e21 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -728,7 +728,7 @@ public class AsmAnalyzer { // Check if method needs to replaced by a call to a different method. - if (ReplaceMethodCallsAdapter.isReplacementNeeded(owner, name, desc)) { + if (ReplaceMethodCallsAdapter.isReplacementNeeded(owner, name, desc, mOwnerClass)) { mReplaceMethodCallClasses.add(mOwnerClass); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java index bd6f0702266c..3aa7cdf89400 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -72,6 +72,9 @@ public class AsmGenerator { /** FQCN Names of classes to refactor. All reference to old-FQCN will be updated to new-FQCN. * map old-FQCN => new-FQCN */ private final HashMap mRefactorClasses; + /** Methods to inject. FQCN of class in which method should be injected => runnable that does + * the injection. */ + private final Map mInjectedMethodsMap; /** * Creates a new generator that can generate the output JAR with the stubbed classes. @@ -165,6 +168,8 @@ public class AsmGenerator { } returnTypes.add(binaryToInternalClassName(className)); } + + mInjectedMethodsMap = createInfo.getInjectedMethodsMap(); } /** @@ -337,7 +342,7 @@ public class AsmGenerator { ClassVisitor cv = cw; if (mReplaceMethodCallsClasses.contains(className)) { - cv = new ReplaceMethodCallsAdapter(cv); + cv = new ReplaceMethodCallsAdapter(cv, className); } cv = new RefactorClassAdapter(cv, mRefactorClasses); @@ -345,6 +350,10 @@ public class AsmGenerator { cv = new RenameClassAdapter(cv, className, newName); } + String binaryNewName = newName.replace('/', '.'); + if (mInjectedMethodsMap.keySet().contains(binaryNewName)) { + cv = new InjectMethodsAdapter(cv, mInjectedMethodsMap.get(binaryNewName)); + } cv = new TransformClassAdapter(mLog, mStubMethods, mDeleteReturns.get(className), newName, cv, stubNativesOnly); diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java index 98acd2f15657..f7139e9016d7 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -26,7 +26,9 @@ import com.android.tools.layoutlib.java.System_Delegate; import com.android.tools.layoutlib.java.UnsafeByteSequence; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; /** @@ -105,6 +107,7 @@ public final class CreateInfo implements ICreateInfo { return JAVA_PKG_CLASSES; } + @Override public Set getExcludedClasses() { String[] refactoredClasses = getJavaPkgClasses(); int count = refactoredClasses.length / 2 + EXCLUDED_CLASSES.length; @@ -115,6 +118,12 @@ public final class CreateInfo implements ICreateInfo { excludedClasses.addAll(Arrays.asList(EXCLUDED_CLASSES)); return excludedClasses; } + + @Override + public Map getInjectedMethodsMap() { + return INJECTED_METHODS; + } + //----- /** @@ -288,5 +297,11 @@ public final class CreateInfo implements ICreateInfo { private final static String[] DELETE_RETURNS = new String[] { null }; // separator, for next class/methods list. + + private final static Map INJECTED_METHODS = + new HashMap(1) {{ + put("android.content.Context", + InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); + }}; } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java index e49a6687502d..ac1063900cd2 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java @@ -16,6 +16,9 @@ package com.android.tools.layoutlib.create; +import org.objectweb.asm.ClassVisitor; + +import java.util.Map; import java.util.Set; /** @@ -27,33 +30,33 @@ public interface ICreateInfo { * Returns the list of class from layoutlib_create to inject in layoutlib. * The list can be empty but must not be null. */ - public abstract Class[] getInjectedClasses(); + Class[] getInjectedClasses(); /** * Returns the list of methods to rewrite as delegates. * The list can be empty but must not be null. */ - public abstract String[] getDelegateMethods(); + String[] getDelegateMethods(); /** * Returns the list of classes on which to delegate all native methods. * The list can be empty but must not be null. */ - public abstract String[] getDelegateClassNatives(); + String[] getDelegateClassNatives(); /** * Returns The list of methods to stub out. Each entry must be in the form * "package.package.OuterClass$InnerClass#MethodName". * The list can be empty but must not be null. */ - public abstract String[] getOverriddenMethods(); + String[] getOverriddenMethods(); /** * Returns the list of classes to rename, must be an even list: the binary FQCN * of class to replace followed by the new FQCN. * The list can be empty but must not be null. */ - public abstract String[] getRenamedClasses(); + String[] getRenamedClasses(); /** * Returns the list of classes for which the methods returning them should be deleted. @@ -62,7 +65,7 @@ public interface ICreateInfo { * the methods to delete. * The list can be empty but must not be null. */ - public abstract String[] getDeleteReturns(); + String[] getDeleteReturns(); /** * Returns the list of classes to refactor, must be an even list: the @@ -70,7 +73,18 @@ public interface ICreateInfo { * to the old class should be updated to the new class. * The list can be empty but must not be null. */ - public abstract String[] getJavaPkgClasses(); + String[] getJavaPkgClasses(); + + Set getExcludedClasses(); + + /** + * Returns a map from binary FQCN className to {@link InjectMethodRunnable} which will be + * called to inject methods into a class. + * Can be empty but must not be null. + */ + Map getInjectedMethodsMap(); - public abstract Set getExcludedClasses(); + abstract class InjectMethodRunnable { + public abstract void generateMethods(ClassVisitor cv); + } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java new file mode 100644 index 000000000000..39d46d705f42 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.create.ICreateInfo.InjectMethodRunnable; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; + +public class InjectMethodRunnables { + public static final ICreateInfo.InjectMethodRunnable CONTEXT_GET_FRAMEWORK_CLASS_LOADER + = new InjectMethodRunnable() { + @Override + public void generateMethods(ClassVisitor cv) { + // generated by compiling the class: + // class foo { public ClassLoader getFrameworkClassLoader() { return getClass().getClassLoader(); } } + // and then running ASMifier on it: + // java -classpath asm-debug-all-5.0.2.jar:. org.objectweb.asm.util.ASMifier foo + MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getFrameworkClassLoader", + "()Ljava/lang/ClassLoader;", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", + "()Ljava/lang/Class;"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", + "()Ljava/lang/ClassLoader;"); + mv.visitInsn(ARETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + // generated code ends. + } + }; +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java new file mode 100644 index 000000000000..ea2b9c900ad0 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 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.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.create.ICreateInfo.InjectMethodRunnable; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Injects methods into some classes. + */ +public class InjectMethodsAdapter extends ClassVisitor { + + private final ICreateInfo.InjectMethodRunnable mRunnable; + + public InjectMethodsAdapter(ClassVisitor cv, InjectMethodRunnable runnable) { + super(Opcodes.ASM4, cv); + mRunnable = runnable; + } + + @Override + public void visitEnd() { + mRunnable.generateMethods(this); + super.visitEnd(); + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java index 384d8ca6c01e..43691482dc66 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java @@ -62,14 +62,13 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { // Case 1: java.lang.System.arraycopy() METHOD_REPLACERS.add(new MethodReplacer() { @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && "arraycopy".equals(name) && ARRAYCOPY_DESCRIPTORS.contains(desc); } @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.desc = "(Ljava/lang/Object;ILjava/lang/Object;II)V"; } }); @@ -81,14 +80,13 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { Type.getMethodDescriptor(STRING, Type.getType(Locale.class)); @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LOCALE_CLASS.equals(owner) && "()Ljava/lang/String;".equals(desc) && ("toLanguageTag".equals(name) || "getScript".equals(name)); } @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.opcode = Opcodes.INVOKESTATIC; mi.owner = ANDROID_LOCALE_CLASS; mi.desc = LOCALE_TO_STRING; @@ -103,7 +101,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { Type.getType(Locale.class), STRING); @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LOCALE_CLASS.equals(owner) && ("adjustLanguageCode".equals(name) && desc.equals(STRING_TO_STRING) || "forLanguageTag".equals(name) && desc.equals(STRING_TO_LOCALE)); @@ -111,7 +109,6 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.owner = ANDROID_LOCALE_CLASS; } }); @@ -119,14 +116,13 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { // Case 4: java.lang.System.log?() METHOD_REPLACERS.add(new MethodReplacer() { @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && name.length() == 4 && name.startsWith("log"); } @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); assert mi.desc.equals("(Ljava/lang/String;Ljava/lang/Throwable;)V") || mi.desc.equals("(Ljava/lang/String;)V"); mi.name = "log"; @@ -142,7 +138,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { private final String LINKED_HASH_MAP = Type.getInternalName(LinkedHashMap.class); @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return LINKED_HASH_MAP.equals(owner) && "eldest".equals(name) && VOID_TO_MAP_ENTRY.equals(desc); @@ -150,26 +146,64 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.opcode = Opcodes.INVOKESTATIC; mi.owner = Type.getInternalName(LinkedHashMap_Delegate.class); mi.desc = Type.getMethodDescriptor( Type.getType(Map.Entry.class), Type.getType(LinkedHashMap.class)); } }); + + // Case 6: android.content.Context.getClassLoader() in LayoutInflater + METHOD_REPLACERS.add(new MethodReplacer() { + // When LayoutInflater asks for a class loader, we must return the class loader that + // cannot return app's custom views/classes. This is so that in case of any failure + // or exception when instantiating the views, the IDE can replace it with a mock view + // and have proper error handling. However, if a custom view asks for the class + // loader, we must return a class loader that can find app's custom views as well. + // Thus, we rewrite the call to get class loader in LayoutInflater to + // getFrameworkClassLoader and inject a new method in Context. This leaves the normal + // method: Context.getClassLoader() free to be used by the apps. + private final String VOID_TO_CLASS_LOADER = + Type.getMethodDescriptor(Type.getType(ClassLoader.class)); + + @Override + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { + return owner.equals("android/content/Context") && + sourceClass.equals("android/view/LayoutInflater") && + name.equals("getClassLoader") && + desc.equals(VOID_TO_CLASS_LOADER); + } + + @Override + public void replace(MethodInformation mi) { + mi.name = "getFrameworkClassLoader"; + } + }); } - public static boolean isReplacementNeeded(String owner, String name, String desc) { + /** + * If a method some.package.Class.Method(args) is called from some.other.Class, + * @param owner some/package/Class + * @param name Method + * @param desc (args)returnType + * @param sourceClass some/other/Class + * @return if the method invocation needs to be replaced by some other class. + */ + public static boolean isReplacementNeeded(String owner, String name, String desc, + String sourceClass) { for (MethodReplacer replacer : METHOD_REPLACERS) { - if (replacer.isNeeded(owner, name, desc)) { + if (replacer.isNeeded(owner, name, desc, sourceClass)) { return true; } } return false; } - public ReplaceMethodCallsAdapter(ClassVisitor cv) { + private final String mOriginalClassName; + + public ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName) { super(Opcodes.ASM4, cv); + mOriginalClassName = originalClassName; } @Override @@ -187,7 +221,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { for (MethodReplacer replacer : METHOD_REPLACERS) { - if (replacer.isNeeded(owner, name, desc)) { + if (replacer.isNeeded(owner, name, desc, mOriginalClassName)) { MethodInformation mi = new MethodInformation(opcode, owner, name, desc); replacer.replace(mi); opcode = mi.opcode; @@ -216,13 +250,12 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } private interface MethodReplacer { - public boolean isNeeded(String owner, String name, String desc); + boolean isNeeded(String owner, String name, String desc, String sourceClass); /** * Updates the MethodInformation with the new values of the method attributes - * opcode, owner, name and desc. - * */ - public void replace(MethodInformation mi); + void replace(MethodInformation mi); } } diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java index cf91386bb219..2c21470d6a2f 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -19,7 +19,9 @@ package com.android.tools.layoutlib.create; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.junit.After; @@ -32,13 +34,17 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -130,6 +136,11 @@ public class AsmGeneratorTest { // methods deleted from their return type. return new String[0]; } + + @Override + public Map getInjectedMethodsMap() { + return new HashMap(0); + } }; AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); @@ -200,6 +211,11 @@ public class AsmGeneratorTest { // methods deleted from their return type. return new String[0]; } + + @Override + public Map getInjectedMethodsMap() { + return new HashMap(0); + } }; AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); @@ -278,6 +294,11 @@ public class AsmGeneratorTest { // methods deleted from their return type. return new String[0]; } + + @Override + public Map getInjectedMethodsMap() { + return new HashMap(0); + } }; AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); @@ -303,6 +324,118 @@ public class AsmGeneratorTest { filesFound.keySet().toArray()); } + @Test + public void testMethodInjection() throws IOException, LogAbortException, + ClassNotFoundException, IllegalAccessException, InstantiationException, + NoSuchMethodException, InvocationTargetException { + ICreateInfo ci = new ICreateInfo() { + @Override + public Class[] getInjectedClasses() { + return new Class[0]; + } + + @Override + public String[] getDelegateMethods() { + return new String[0]; + } + + @Override + public String[] getDelegateClassNatives() { + return new String[0]; + } + + @Override + public String[] getOverriddenMethods() { + // methods to force override + return new String[0]; + } + + @Override + public String[] getRenamedClasses() { + // classes to rename (so that we can replace them) + return new String[0]; + } + + @Override + public String[] getJavaPkgClasses() { + // classes to refactor (so that we can replace them) + return new String[0]; + } + + @Override + public Set getExcludedClasses() { + return new HashSet(0); + } + + @Override + public String[] getDeleteReturns() { + // methods deleted from their return type. + return new String[0]; + } + + @Override + public Map getInjectedMethodsMap() { + HashMap map = + new HashMap(1); + map.put("mock_android.util.EmptyArray", + InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); + return map; + } + }; + + AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); + AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, + null, // derived from + new String[] { // include classes + "**" + }, + ci.getExcludedClasses(), + new String[] { /* include files */ + "mock_android/data/data*" + }); + aa.analyze(); + agen.generate(); + Map output = new TreeMap(); + Map filesFound = new TreeMap(); + parseZip(mOsDestJar, output, filesFound); + final String modifiedClass = "mock_android.util.EmptyArray"; + final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class"); + ZipFile zipFile = new ZipFile(mOsDestJar); + ZipEntry entry = zipFile.getEntry(modifiedClassPath); + assertNotNull(entry); + final byte[] bytes; + final InputStream inputStream = zipFile.getInputStream(entry); + try { + bytes = getByteArray(inputStream); + } finally { + inputStream.close(); + } + ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (name.equals(modifiedClass)) { + return defineClass(null, bytes, 0, bytes.length); + } + throw new ClassNotFoundException(name + " not found."); + } + }; + Class emptyArrayClass = classLoader.loadClass(modifiedClass); + Object emptyArrayInstance = emptyArrayClass.newInstance(); + Method method = emptyArrayClass.getMethod("getFrameworkClassLoader"); + Object cl = method.invoke(emptyArrayInstance); + assertEquals(classLoader, cl); + } + + private static byte[] getByteArray(InputStream stream) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = stream.read(buffer, 0, buffer.length)) > -1) { + bos.write(buffer, 0, read); + } + return bos.toByteArray(); + } + private void parseZip(String jarPath, Map classes, Map filesFound) throws IOException { -- 2.11.0