Make Context.getClassLoader() work.

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
This commit is contained in:
Deepanshu Gupta
2015-05-18 18:47:07 -07:00
parent 21b5645733
commit f8ea750455
11 changed files with 347 additions and 31 deletions

View File

@@ -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);

View File

@@ -138,8 +138,9 @@ public final class BridgeContext extends Context {
private final Stack<BridgeXmlBlockParser> mParserStack = new Stack<BridgeXmlBlockParser>();
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
@@ -462,7 +463,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

View File

@@ -275,7 +275,6 @@ public abstract class RenderAction<T extends RenderParams> extends FrameworkReso
mContext.getRenderResources().setLogger(null);
}
ParserFactory.setParserFactory(null);
}
public static BridgeContext getCurrentContext() {

View File

@@ -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);
}
}

View File

@@ -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<String, String> mRefactorClasses;
/** Methods to inject. FQCN of class in which method should be injected => runnable that does
* the injection. */
private final Map<String, ICreateInfo.InjectMethodRunnable> 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);

View File

@@ -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<String> 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<String, InjectMethodRunnable> getInjectedMethodsMap() {
return INJECTED_METHODS;
}
//-----
/**
@@ -286,5 +295,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<String, InjectMethodRunnable> INJECTED_METHODS =
new HashMap<String, InjectMethodRunnable>(1) {{
put("android.content.Context",
InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER);
}};
}

View File

@@ -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();
public abstract Set<String> getExcludedClasses();
Set<String> 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<String, InjectMethodRunnable> getInjectedMethodsMap();
abstract class InjectMethodRunnable {
public abstract void generateMethods(ClassVisitor cv);
}
}

View File

@@ -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.
}
};
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, InjectMethodRunnable> getInjectedMethodsMap() {
return new HashMap<String, InjectMethodRunnable>(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<String, InjectMethodRunnable> getInjectedMethodsMap() {
return new HashMap<String, InjectMethodRunnable>(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<String, InjectMethodRunnable> getInjectedMethodsMap() {
return new HashMap<String, InjectMethodRunnable>(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<String> getExcludedClasses() {
return new HashSet<String>(0);
}
@Override
public String[] getDeleteReturns() {
// methods deleted from their return type.
return new String[0];
}
@Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
HashMap<String, InjectMethodRunnable> map =
new HashMap<String, InjectMethodRunnable>(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<String, ClassReader> output = new TreeMap<String, ClassReader>();
Map<String, InputStream> filesFound = new TreeMap<String, InputStream>();
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<String, ClassReader> classes,
Map<String, InputStream> filesFound) throws IOException {