am 1c5bf7f1: Merge "Make Context.getClassLoader() work." into mnc-dev

* commit '1c5bf7f1ddc8c9c9844a1bc1a7e4ce53dcc5d5bd':
  Make Context.getClassLoader() work.
This commit is contained in:
Deepanshu Gupta
2015-05-20 03:16:23 +00:00
committed by Android Git Automerger
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 {