summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorRaphael <raphael@google.com>2010-09-27 08:55:43 -0700
committerRaphael <raphael@google.com>2010-09-29 14:45:20 -0700
commitbc101806249eb883f89c4a770a8c27f9ac315837 (patch)
treed8ad184b4ceddb749a56ed53e00a1fd57ff154d0 /tools
parent1c1797acff49857b41ea1e3630d23d940882791c (diff)
downloadframeworks_base-bc101806249eb883f89c4a770a8c27f9ac315837.zip
frameworks_base-bc101806249eb883f89c4a770a8c27f9ac315837.tar.gz
frameworks_base-bc101806249eb883f89c4a770a8c27f9ac315837.tar.bz2
layoutlib_create: Generate delegate to implement native methods.
- Some new parameters are added to CreateInfo with the list of methods or classes to override with delegates. - DelegateClassAdapter and DelegateMethodAdapter do the work... see javadoc. Change-Id: I0657cd929837181d81c65db7051d8ccbdc59c741
Diffstat (limited to 'tools')
-rw-r--r--tools/layoutlib/create/README.txt17
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/LayoutlibDelegate.java27
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java153
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java93
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java94
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java319
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java65
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java30
-rw-r--r--tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java26
-rw-r--r--tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java21
-rw-r--r--tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java53
-rw-r--r--tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java12
-rw-r--r--tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java305
-rw-r--r--tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java27
-rw-r--r--tools/layoutlib/create/tests/com/android/tools/layoutlib/create/MockLog.java43
15 files changed, 1139 insertions, 146 deletions
diff --git a/tools/layoutlib/create/README.txt b/tools/layoutlib/create/README.txt
index c59e20d..65a64cd 100644
--- a/tools/layoutlib/create/README.txt
+++ b/tools/layoutlib/create/README.txt
@@ -195,5 +195,22 @@ example, the inner class Paint$Style in the Paint class should be discarded and
bridge will provide its own implementation.
+- References -
+--------------
+
+
+The JVM Specification 2nd edition:
+ http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html
+
+Understanding bytecode:
+ http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/
+
+Bytecode opcode list:
+ http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
+
+ASM user guide:
+ http://download.forge.objectweb.org/asm/asm-guide.pdf
+
+
--
end
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/LayoutlibDelegate.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/LayoutlibDelegate.java
new file mode 100644
index 0000000..9a48ea6
--- /dev/null
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/LayoutlibDelegate.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Denotes a method that has been converted to a delegate by layoutlib_create.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LayoutlibDelegate {
+}
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 7b55ed3e..590923f 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
@@ -28,9 +28,9 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
-import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
@@ -60,38 +60,55 @@ public class AsmGenerator {
* old-FQCN to rename and they get erased as they get renamed. At the end, classes still
* left here are not in the code base anymore and thus were not renamed. */
private HashSet<String> mClassesNotRenamed;
- /** A map { FQCN => map { list of return types to delete from the FQCN } }. */
+ /** A map { FQCN => set { list of return types to delete from the FQCN } }. */
private HashMap<String, Set<String>> mDeleteReturns;
+ /** A map { FQCN => set { method names } } of methods to rewrite as delegates.
+ * The special name {@link DelegateClassAdapter#ALL_NATIVES} can be used as in internal set. */
+ private final HashMap<String, Set<String>> mDelegateMethods;
/**
* Creates a new generator that can generate the output JAR with the stubbed classes.
- *
+ *
* @param log Output logger.
* @param osDestJar The path of the destination JAR to create.
- * @param injectClasses The list of class from layoutlib_create to inject in layoutlib.
- * @param stubMethods The list of methods to stub out. Each entry must be in the form
- * "package.package.OuterClass$InnerClass#MethodName".
- * @param renameClasses The list of classes to rename, must be an even list: the binary FQCN
- * of class to replace followed by the new FQCN.
- * @param deleteReturns List of classes for which the methods returning them should be deleted.
- * The array contains a list of null terminated section starting with the name of the class
- * to rename in which the methods are deleted, followed by a list of return types identifying
- * the methods to delete.
+ * @param createInfo Creation parameters. Must not be null.
*/
- public AsmGenerator(Log log, String osDestJar,
- Class<?>[] injectClasses,
- String[] stubMethods,
- String[] renameClasses, String[] deleteReturns) {
+ public AsmGenerator(Log log, String osDestJar, ICreateInfo createInfo) {
mLog = log;
mOsDestJar = osDestJar;
- mInjectClasses = injectClasses != null ? injectClasses : new Class<?>[0];
- mStubMethods = stubMethods != null ? new HashSet<String>(Arrays.asList(stubMethods)) :
- new HashSet<String>();
+ mInjectClasses = createInfo.getInjectedClasses();
+ mStubMethods = new HashSet<String>(Arrays.asList(createInfo.getOverriddenMethods()));
+
+ // Create the map/set of methods to change to delegates
+ mDelegateMethods = new HashMap<String, Set<String>>();
+ for (String signature : createInfo.getDelegateMethods()) {
+ int pos = signature.indexOf('#');
+ if (pos <= 0 || pos >= signature.length() - 1) {
+ continue;
+ }
+ String className = binaryToInternalClassName(signature.substring(0, pos));
+ String methodName = signature.substring(pos + 1);
+ Set<String> methods = mDelegateMethods.get(className);
+ if (methods == null) {
+ methods = new HashSet<String>();
+ mDelegateMethods.put(className, methods);
+ }
+ methods.add(methodName);
+ }
+ for (String className : createInfo.getDelegateClassNatives()) {
+ Set<String> methods = mDelegateMethods.get(className);
+ if (methods == null) {
+ methods = new HashSet<String>();
+ mDelegateMethods.put(className, methods);
+ }
+ methods.add(DelegateClassAdapter.ALL_NATIVES);
+ }
// Create the map of classes to rename.
mRenameClasses = new HashMap<String, String>();
mClassesNotRenamed = new HashSet<String>();
- int n = renameClasses == null ? 0 : renameClasses.length;
+ String[] renameClasses = createInfo.getRenamedClasses();
+ int n = renameClasses.length;
for (int i = 0; i < n; i += 2) {
assert i + 1 < n;
// The ASM class names uses "/" separators, whereas regular FQCN use "."
@@ -100,38 +117,37 @@ public class AsmGenerator {
mRenameClasses.put(oldFqcn, newFqcn);
mClassesNotRenamed.add(oldFqcn);
}
-
+
// create the map of renamed class -> return type of method to delete.
mDeleteReturns = new HashMap<String, Set<String>>();
- if (deleteReturns != null) {
- Set<String> returnTypes = null;
- String renamedClass = null;
- for (String className : deleteReturns) {
- // if we reach the end of a section, add it to the main map
- if (className == null) {
- if (returnTypes != null) {
- mDeleteReturns.put(renamedClass, returnTypes);
- }
-
- renamedClass = null;
- continue;
- }
-
- // if the renamed class is null, this is the beginning of a section
- if (renamedClass == null) {
- renamedClass = binaryToInternalClassName(className);
- continue;
- }
-
- // just a standard return type, we add it to the list.
- if (returnTypes == null) {
- returnTypes = new HashSet<String>();
+ String[] deleteReturns = createInfo.getDeleteReturns();
+ Set<String> returnTypes = null;
+ String renamedClass = null;
+ for (String className : deleteReturns) {
+ // if we reach the end of a section, add it to the main map
+ if (className == null) {
+ if (returnTypes != null) {
+ mDeleteReturns.put(renamedClass, returnTypes);
}
- returnTypes.add(binaryToInternalClassName(className));
+
+ renamedClass = null;
+ continue;
}
+
+ // if the renamed class is null, this is the beginning of a section
+ if (renamedClass == null) {
+ renamedClass = binaryToInternalClassName(className);
+ continue;
+ }
+
+ // just a standard return type, we add it to the list.
+ if (returnTypes == null) {
+ returnTypes = new HashSet<String>();
+ }
+ returnTypes.add(binaryToInternalClassName(className));
}
}
-
+
/**
* Returns the list of classes that have not been renamed yet.
* <p/>
@@ -163,12 +179,12 @@ public class AsmGenerator {
public void setDeps(Map<String, ClassReader> deps) {
mDeps = deps;
}
-
+
/** Gets the map of classes to output as-is, except if they have native methods */
public Map<String, ClassReader> getKeep() {
return mKeep;
}
-
+
/** Gets the map of dependencies that must be completely stubbed */
public Map<String, ClassReader> getDeps() {
return mDeps;
@@ -177,7 +193,7 @@ public class AsmGenerator {
/** Generates the final JAR */
public void generate() throws FileNotFoundException, IOException {
TreeMap<String, byte[]> all = new TreeMap<String, byte[]>();
-
+
for (Class<?> clazz : mInjectClasses) {
String name = classToEntryPath(clazz);
InputStream is = ClassLoader.getSystemResourceAsStream(name);
@@ -186,7 +202,7 @@ public class AsmGenerator {
name = classNameToEntryPath(transformName(cr.getClassName()));
all.put(name, b);
}
-
+
for (Entry<String, ClassReader> entry : mDeps.entrySet()) {
ClassReader cr = entry.getValue();
byte[] b = transform(cr, true /* stubNativesOnly */);
@@ -211,8 +227,8 @@ public class AsmGenerator {
/**
* Writes the JAR file.
- *
- * @param outStream The file output stream were to write the JAR.
+ *
+ * @param outStream The file output stream were to write the JAR.
* @param all The map of all classes to output.
* @throws IOException if an I/O error has occurred
*/
@@ -236,7 +252,7 @@ public class AsmGenerator {
String classNameToEntryPath(String className) {
return className.replaceAll("\\.", "/").concat(".class");
}
-
+
/**
* Utility method to get the JAR entry path from a Class name.
* e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class"
@@ -248,30 +264,32 @@ public class AsmGenerator {
name = "$" + clazz.getSimpleName() + name;
clazz = parent;
}
- return classNameToEntryPath(clazz.getCanonicalName() + name);
+ return classNameToEntryPath(clazz.getCanonicalName() + name);
}
/**
* Transforms a class.
* <p/>
* There are 3 kind of transformations:
- *
+ *
* 1- For "mock" dependencies classes, we want to remove all code from methods and replace
* by a stub. Native methods must be implemented with this stub too. Abstract methods are
* left intact. Modified classes must be overridable (non-private, non-final).
* Native methods must be made non-final, non-private.
- *
+ *
* 2- For "keep" classes, we want to rewrite all native methods as indicated above.
* If a class has native methods, it must also be made non-private, non-final.
- *
+ *
* Note that unfortunately static methods cannot be changed to non-static (since static and
* non-static are invoked differently.)
*/
byte[] transform(ClassReader cr, boolean stubNativesOnly) {
boolean hasNativeMethods = hasNativeMethods(cr);
+
+ // Get the class name, as an internal name (e.g. com/android/SomeClass$InnerClass)
String className = cr.getClassName();
-
+
String newName = transformName(className);
// transformName returns its input argument if there's no need to rename the class
if (newName != className) {
@@ -288,13 +306,24 @@ public class AsmGenerator {
// Rewrite the new class from scratch, without reusing the constant pool from the
// original class reader.
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
-
+
ClassVisitor rv = cw;
if (newName != className) {
rv = new RenameClassAdapter(cw, className, newName);
}
-
- TransformClassAdapter cv = new TransformClassAdapter(mLog, mStubMethods,
+
+ Set<String> delegateMethods = mDelegateMethods.get(className);
+ if (delegateMethods != null && !delegateMethods.isEmpty()) {
+ // If delegateMethods only contains one entry ALL_NATIVES and the class is
+ // known to have no native methods, just skip this step.
+ if (hasNativeMethods ||
+ !(delegateMethods.size() == 1 &&
+ delegateMethods.contains(DelegateClassAdapter.ALL_NATIVES))) {
+ rv = new DelegateClassAdapter(mLog, rv, className, delegateMethods);
+ }
+ }
+
+ TransformClassAdapter cv = new TransformClassAdapter(mLog, mStubMethods,
mDeleteReturns.get(className),
newName, rv,
stubNativesOnly, stubNativesOnly || hasNativeMethods);
@@ -323,7 +352,7 @@ public class AsmGenerator {
return newName + className.substring(pos);
}
}
-
+
return className;
}
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 2ed8641..92892784 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
@@ -16,11 +16,70 @@
package com.android.tools.layoutlib.create;
-public class CreateInfo {
+/**
+ * Describes the work to be done by {@link AsmGenerator}.
+ */
+public final class CreateInfo implements ICreateInfo {
+
+ /**
+ * Returns the list of class from layoutlib_create to inject in layoutlib.
+ * The list can be empty but must not be null.
+ */
+ public Class<?>[] getInjectedClasses() {
+ return INJECTED_CLASSES;
+ }
+
+ /**
+ * Returns the list of methods to rewrite as delegates.
+ * The list can be empty but must not be null.
+ */
+ public String[] getDelegateMethods() {
+ return DELEGATE_METHODS;
+ }
+
+ /**
+ * Returns the list of classes on which to delegate all native methods.
+ * The list can be empty but must not be null.
+ */
+ public String[] getDelegateClassNatives() {
+ return DELEGATE_CLASS_NATIVES;
+ }
+
+ /**
+ * 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 String[] getOverriddenMethods() {
+ return OVERRIDDEN_METHODS;
+ }
+
+ /**
+ * 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 String[] getRenamedClasses() {
+ return RENAMED_CLASSES;
+ }
+
+ /**
+ * Returns the list of classes for which the methods returning them should be deleted.
+ * The array contains a list of null terminated section starting with the name of the class
+ * to rename in which the methods are deleted, followed by a list of return types identifying
+ * the methods to delete.
+ * The list can be empty but must not be null.
+ */
+ public String[] getDeleteReturns() {
+ return DELETE_RETURNS;
+ }
+
+ //-----
+
/**
* The list of class from layoutlib_create to inject in layoutlib.
*/
- public final static Class<?>[] INJECTED_CLASSES = new Class<?>[] {
+ private final static Class<?>[] INJECTED_CLASSES = new Class<?>[] {
OverrideMethod.class,
MethodListener.class,
MethodAdapter.class,
@@ -28,19 +87,37 @@ public class CreateInfo {
};
/**
+ * The list of methods to rewrite as delegates.
+ */
+ private final static String[] DELEGATE_METHODS = new String[] {
+ // TODO: comment out once DelegateClass is working
+ // "android.view.View#isInEditMode",
+ // "android.content.res.Resources$Theme#obtainStyledAttributes",
+ };
+
+ /**
+ * The list of classes on which to delegate all native methods.
+ */
+ private final static String[] DELEGATE_CLASS_NATIVES = new String[] {
+ // TODO: comment out once DelegateClass is working
+ // "android.graphics.Paint"
+ };
+
+ /**
* The list of methods to stub out. Each entry must be in the form
* "package.package.OuterClass$InnerClass#MethodName".
*/
- public final static String[] OVERRIDDEN_METHODS = new String[] {
- "android.view.View#isInEditMode",
- "android.content.res.Resources$Theme#obtainStyledAttributes",
- };
+ private final static String[] OVERRIDDEN_METHODS = new String[] {
+ // TODO: remove once DelegateClass is working
+ "android.view.View#isInEditMode",
+ "android.content.res.Resources$Theme#obtainStyledAttributes",
+ };
/**
* The list of classes to rename, must be an even list: the binary FQCN
* of class to replace followed by the new FQCN.
*/
- public final static String[] RENAMED_CLASSES =
+ private final static String[] RENAMED_CLASSES =
new String[] {
"android.graphics.Bitmap", "android.graphics._Original_Bitmap",
"android.graphics.BitmapFactory", "android.graphics._Original_BitmapFactory",
@@ -69,7 +146,7 @@ public class CreateInfo {
* to rename in which the methods are deleted, followed by a list of return types identifying
* the methods to delete.
*/
- public final static String[] REMOVED_METHODS =
+ private final static String[] DELETE_RETURNS =
new String[] {
"android.graphics.Paint", // class to delete methods from
"android.graphics.Paint$Align", // list of type identifying methods to delete
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java
new file mode 100644
index 0000000..9cba8a0
--- /dev/null
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 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 org.objectweb.asm.ClassAdapter;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import java.util.Set;
+
+/**
+ * A {@link DelegateClassAdapter} can transform some methods from a class into
+ * delegates that defer the call to an associated delegate class.
+ * <p/>
+ * This is used to override specific methods and or all native methods in classes.
+ */
+public class DelegateClassAdapter extends ClassAdapter {
+
+ public final static String ALL_NATIVES = "<<all_natives>>";
+
+ private final String mClassName;
+ private final Set<String> mDelegateMethods;
+ private final Log mLog;
+
+ /**
+ * Creates a new {@link DelegateClassAdapter} that can transform some methods
+ * from a class into delegates that defer the call to an associated delegate class.
+ * <p/>
+ * This is used to override specific methods and or all native methods in classes.
+ *
+ * @param log The logger object. Must not be null.
+ * @param cv the class visitor to which this adapter must delegate calls.
+ * @param className The internal class name of the class to visit,
+ * e.g. <code>com/android/SomeClass$InnerClass</code>.
+ * @param delegateMethods The set of method names to modify and/or the
+ * special constant {@link #ALL_NATIVES} to convert all native methods.
+ */
+ public DelegateClassAdapter(Log log,
+ ClassVisitor cv,
+ String className,
+ Set<String> delegateMethods) {
+ super(cv);
+ mLog = log;
+ mClassName = className;
+ mDelegateMethods = delegateMethods;
+ }
+
+ //----------------------------------
+ // Methods from the ClassAdapter
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc,
+ String signature, String[] exceptions) {
+
+ boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
+ boolean isNative = (access & Opcodes.ACC_NATIVE) != 0;
+
+ boolean useDelegate = (isNative && mDelegateMethods.contains(ALL_NATIVES)) ||
+ mDelegateMethods.contains(name);
+
+ if (useDelegate) {
+ // remove native
+ access = access & ~Opcodes.ACC_NATIVE;
+ }
+
+ MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions);
+ if (useDelegate) {
+ DelegateMethodAdapter a = new DelegateMethodAdapter(mLog, mw, mClassName,
+ name, desc, isStatic);
+ if (isNative) {
+ // A native has no code to visit, so we need to generate it directly.
+ a.generateCode();
+ } else {
+ return a;
+ }
+ }
+ return mw;
+ }
+}
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java
new file mode 100644
index 0000000..21d6682
--- /dev/null
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2008 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.annotations.LayoutlibDelegate;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * This method adapter rewrites a method by discarding the original code and generating
+ * a call to a delegate. Original annotations are passed along unchanged.
+ * <p/>
+ * Calls are delegated to a class named <code>&lt;className&gt;_Delegate</code> with
+ * static methods matching the methods to be overridden here. The methods have the
+ * same return type. The argument type list is the same except the "this" reference is
+ * passed first for non-static methods.
+ * <p/>
+ * A new annotation is added.
+ * <p/>
+ * Note that native methods have, by definition, no code so there's nothing a visitor
+ * can visit. That means the caller must call {@link #generateCode()} directly for
+ * a native and use the visitor pattern for non-natives.
+ * <p/>
+ * Instances of this class are not re-usable. You need a new instance for each method.
+ */
+class DelegateMethodAdapter implements MethodVisitor {
+
+ /**
+ * Suffix added to delegate classes.
+ */
+ public static final String DELEGATE_SUFFIX = "_Delegate";
+
+ private static String CONSTRUCTOR = "<init>";
+ private static String CLASS_INIT = "<clinit>";
+
+ /** The parent method writer */
+ private MethodVisitor mParentVisitor;
+ /** Flag to output the first line number. */
+ private boolean mOutputFirstLineNumber = true;
+ /** The original method descriptor (return type + argument types.) */
+ private String mDesc;
+ /** True if the original method is static. */
+ private final boolean mIsStatic;
+ /** The internal class name (e.g. <code>com/android/SomeClass$InnerClass</code>.) */
+ private final String mClassName;
+ /** The method name. */
+ private final String mMethodName;
+ /** Logger object. */
+ private final Log mLog;
+ /** True if {@link #visitCode()} has been invoked. */
+ private boolean mVisitCodeCalled;
+
+ /**
+ * Creates a new {@link DelegateMethodAdapter} that will transform this method
+ * into a delegate call.
+ * <p/>
+ * See {@link DelegateMethodAdapter} for more details.
+ *
+ * @param log The logger object. Must not be null.
+ * @param mv the method visitor to which this adapter must delegate calls.
+ * @param className The internal class name of the class to visit,
+ * e.g. <code>com/android/SomeClass$InnerClass</code>.
+ * @param methodName The simple name of the method.
+ * @param desc A method descriptor (c.f. {@link Type#getReturnType(String)} +
+ * {@link Type#getArgumentTypes(String)})
+ * @param isStatic True if the method is declared static.
+ */
+ public DelegateMethodAdapter(Log log,
+ MethodVisitor mv,
+ String className,
+ String methodName,
+ String desc,
+ boolean isStatic) {
+ mLog = log;
+ mParentVisitor = mv;
+ mClassName = className;
+ mMethodName = methodName;
+ mDesc = desc;
+ mIsStatic = isStatic;
+
+ if (CONSTRUCTOR.equals(methodName) || CLASS_INIT.equals(methodName)) {
+ // We're going to simplify by not supporting constructors.
+ // The only trick with a constructor is to find the proper super constructor
+ // and call it (and deciding if we should mirror the original method call to
+ // a custom constructor or call a default one.)
+ throw new UnsupportedOperationException(
+ String.format("Delegate doesn't support overriding constructor %1$s:%2$s(%3$s)",
+ className, methodName, desc));
+ }
+ }
+
+ /**
+ * Generates the new code for the method.
+ * <p/>
+ * For native methods, this must be invoked directly by {@link DelegateClassAdapter}
+ * (since they have no code to visit).
+ * <p/>
+ * Otherwise for non-native methods the {@link DelegateClassAdapter} simply needs to
+ * return this instance of {@link DelegateMethodAdapter} and let the normal visitor pattern
+ * invoke it as part of the {@link ClassReader#accept(ClassVisitor, int)} workflow and then
+ * this method will be invoked from {@link MethodVisitor#visitEnd()}.
+ */
+ public void generateCode() {
+ /*
+ * The goal is to generate a call to a static delegate method.
+ * If this method is not-static, the first parameter will be this.
+ * All the parameters must be passed and then the eventual return type returned.
+ *
+ * Example, let's say we have a method such as
+ * public void method_1(int a, Object b, ArrayList<String> c) { ... }
+ *
+ * We'll want to create a body that calls a delegate method like this:
+ * TheClass_Delegate.method_1(this, a, b, c);
+ *
+ * The generated class name is the current class name with "_Delegate" appended to it.
+ * One thing to realize is that we don't care about generics -- since generic types
+ * are erased at runtime, they have no influence on the method being called.
+ */
+
+ // Add our annotation
+ AnnotationVisitor aw = mParentVisitor.visitAnnotation(
+ Type.getObjectType(Type.getInternalName(LayoutlibDelegate.class)).toString(),
+ true); // visible at runtime
+ aw.visitEnd();
+
+ if (!mVisitCodeCalled) {
+ // If this is a direct call to generateCode() as done by DelegateClassAdapter
+ // for natives, visitCode() hasn't been called yet.
+ mParentVisitor.visitCode();
+ mVisitCodeCalled = true;
+ }
+
+ int numVars = 0;
+
+ // Push "this" for an instance method, which is always ALOAD 0
+ if (!mIsStatic) {
+ mParentVisitor.visitVarInsn(Opcodes.ALOAD, numVars++);
+ }
+
+ // Push all other arguments
+ Type[] argTypes = Type.getArgumentTypes(mDesc);
+ for (Type t : argTypes) {
+ int size = t.getSize();
+ mParentVisitor.visitVarInsn(t.getOpcode(Opcodes.ILOAD), numVars);
+ numVars += size;
+ }
+
+ // Construct the descriptor of the delegate. For a static method, it's the same
+ // however for an instance method we need to pass the 'this' reference first
+ String desc = mDesc;
+ if (!mIsStatic && argTypes.length > 0) {
+ Type[] argTypes2 = new Type[argTypes.length + 1];
+
+ argTypes2[0] = Type.getObjectType(mClassName);
+ System.arraycopy(argTypes, 0, argTypes2, 1, argTypes.length);
+
+ desc = Type.getMethodDescriptor(Type.getReturnType(mDesc), argTypes2);
+ }
+
+ String delegateClassName = mClassName + DELEGATE_SUFFIX;
+
+ // Invoke the static delegate
+ mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
+ delegateClassName,
+ mMethodName,
+ desc);
+
+ Type returnType = Type.getReturnType(mDesc);
+ mParentVisitor.visitInsn(returnType.getOpcode(Opcodes.IRETURN));
+
+ mParentVisitor.visitMaxs(numVars, numVars);
+ mParentVisitor.visitEnd();
+
+ // For debugging now. Maybe we should collect these and store them in
+ // a text file for helping create the delegates. We could also compare
+ // the text file to a golden and break the build on unsupported changes
+ // or regressions. Even better we could fancy-print something that looks
+ // like the expected Java method declaration.
+ mLog.debug("Delegate: %1$s # %2$s %3$s", delegateClassName, mMethodName, desc);
+ }
+
+ /* Pass down to visitor writer. In this implementation, either do nothing. */
+ public void visitCode() {
+ mVisitCodeCalled = true;
+ mParentVisitor.visitCode();
+ }
+
+ /*
+ * visitMaxs is called just before visitEnd if there was any code to rewrite.
+ * Skip the original.
+ */
+ public void visitMaxs(int maxStack, int maxLocals) {
+ }
+
+ /**
+ * End of visiting. Generate the messaging code.
+ */
+ public void visitEnd() {
+ generateCode();
+ }
+
+ /* Writes all annotation from the original method. */
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ return mParentVisitor.visitAnnotation(desc, visible);
+ }
+
+ /* Writes all annotation default values from the original method. */
+ public AnnotationVisitor visitAnnotationDefault() {
+ return mParentVisitor.visitAnnotationDefault();
+ }
+
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc,
+ boolean visible) {
+ return mParentVisitor.visitParameterAnnotation(parameter, desc, visible);
+ }
+
+ /* Writes all attributes from the original method. */
+ public void visitAttribute(Attribute attr) {
+ mParentVisitor.visitAttribute(attr);
+ }
+
+ /*
+ * Only writes the first line number present in the original code so that source
+ * viewers can direct to the correct method, even if the content doesn't match.
+ */
+ public void visitLineNumber(int line, Label start) {
+ if (mOutputFirstLineNumber) {
+ mParentVisitor.visitLineNumber(line, start);
+ mOutputFirstLineNumber = false;
+ }
+ }
+
+ public void visitInsn(int opcode) {
+ // Skip original code.
+ }
+
+ public void visitLabel(Label label) {
+ // Skip original code.
+ }
+
+ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+ // Skip original code.
+ }
+
+ public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+ // Skip original code.
+ }
+
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ // Skip original code.
+ }
+
+ public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
+ // Skip original code.
+ }
+
+ public void visitIincInsn(int var, int increment) {
+ // Skip original code.
+ }
+
+ public void visitIntInsn(int opcode, int operand) {
+ // Skip original code.
+ }
+
+ public void visitJumpInsn(int opcode, Label label) {
+ // Skip original code.
+ }
+
+ public void visitLdcInsn(Object cst) {
+ // Skip original code.
+ }
+
+ public void visitLocalVariable(String name, String desc, String signature,
+ Label start, Label end, int index) {
+ // Skip original code.
+ }
+
+ public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+ // Skip original code.
+ }
+
+ public void visitMultiANewArrayInsn(String desc, int dims) {
+ // Skip original code.
+ }
+
+ public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) {
+ // Skip original code.
+ }
+
+ public void visitTypeInsn(int opcode, String type) {
+ // Skip original code.
+ }
+
+ public void visitVarInsn(int opcode, int var) {
+ // Skip original code.
+ }
+
+}
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
new file mode 100644
index 0000000..40c1706
--- /dev/null
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 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;
+
+/**
+ * Interface describing the work to be done by {@link AsmGenerator}.
+ */
+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();
+
+ /**
+ * Returns the list of methods to rewrite as delegates.
+ * The list can be empty but must not be null.
+ */
+ public abstract 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();
+
+ /**
+ * 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();
+
+ /**
+ * 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();
+
+ /**
+ * Returns the list of classes for which the methods returning them should be deleted.
+ * The array contains a list of null terminated section starting with the name of the class
+ * to rename in which the methods are deleted, followed by a list of return types identifying
+ * the methods to delete.
+ * The list can be empty but must not be null.
+ */
+ public abstract String[] getDeleteReturns();
+
+}
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java
index 303f097..4adaff9 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java
@@ -21,7 +21,28 @@ import java.util.ArrayList;
import java.util.Set;
-
+/**
+ * Entry point for the layoutlib_create tool.
+ * <p/>
+ * The tool does not currently rely on any external configuration file.
+ * Instead the configuration is mostly done via the {@link CreateInfo} class.
+ * <p/>
+ * For a complete description of the tool and its implementation, please refer to
+ * the "README.txt" file at the root of this project.
+ * <p/>
+ * For a quick test, invoke this as follows:
+ * <pre>
+ * $ make layoutlib
+ * </pre>
+ * which does:
+ * <pre>
+ * $ make layoutlib_create &lt;bunch of framework jars&gt;
+ * $ out/host/linux-x86/framework/bin/layoutlib_create \
+ * out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar \
+ * out/target/common/obj/JAVA_LIBRARIES/core_intermediates/classes.jar \
+ * out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar
+ * </pre>
+ */
public class Main {
public static void main(String[] args) {
@@ -42,12 +63,7 @@ public class Main {
}
try {
- AsmGenerator agen = new AsmGenerator(log, osDestJar[0],
- CreateInfo.INJECTED_CLASSES,
- CreateInfo.OVERRIDDEN_METHODS,
- CreateInfo.RENAMED_CLASSES,
- CreateInfo.REMOVED_METHODS
- );
+ AsmGenerator agen = new AsmGenerator(log, osDestJar[0], new CreateInfo());
AsmAnalyzer aa = new AsmAnalyzer(log, osJarPath, agen,
new String[] { "android.view.View" }, // derived from
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java
index e294d56..f2d9755 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java
@@ -26,7 +26,7 @@ import org.objectweb.asm.Type;
import java.util.Set;
/**
- * Class adapter that can stub some or all of the methods of the class.
+ * Class adapter that can stub some or all of the methods of the class.
*/
class TransformClassAdapter extends ClassAdapter {
@@ -41,12 +41,12 @@ class TransformClassAdapter extends ClassAdapter {
/**
* Creates a new class adapter that will stub some or all methods.
- * @param logger
- * @param stubMethods
+ * @param logger
+ * @param stubMethods list of method signatures to always stub out
* @param deleteReturns list of types that trigger the deletion of methods returning them.
* @param className The name of the class being modified
* @param cv The parent class writer visitor
- * @param stubNativesOnly True if only native methods should be stubbed. False if all
+ * @param stubNativesOnly True if only native methods should be stubbed. False if all
* methods should be stubbed.
* @param hasNative True if the method has natives, in which case its access should be
* changed.
@@ -67,10 +67,10 @@ class TransformClassAdapter extends ClassAdapter {
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
-
+
// This class might be being renamed.
name = mClassName;
-
+
// remove protected or private and set as public
access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED);
access |= Opcodes.ACC_PUBLIC;
@@ -82,7 +82,7 @@ class TransformClassAdapter extends ClassAdapter {
mIsInterface = ((access & Opcodes.ACC_INTERFACE) != 0);
super.visit(version, access, name, signature, superName, interfaces);
}
-
+
/* Visits the header of an inner class. */
@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
@@ -101,7 +101,7 @@ class TransformClassAdapter extends ClassAdapter {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
-
+
if (mDeleteReturns != null) {
Type t = Type.getReturnType(desc);
if (t.getSort() == Type.OBJECT) {
@@ -130,16 +130,16 @@ class TransformClassAdapter extends ClassAdapter {
(mStubAll ||
(access & Opcodes.ACC_NATIVE) != 0) ||
mStubMethods.contains(methodSignature)) {
-
+
boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
boolean isNative = (access & Opcodes.ACC_NATIVE) != 0;
// remove abstract, final and native
access = access & ~(Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL | Opcodes.ACC_NATIVE);
-
+
String invokeSignature = methodSignature + desc;
mLog.debug(" Stub: %s (%s)", invokeSignature, isNative ? "native" : "");
-
+
MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions);
return new StubMethodAdapter(mw, name, returnType(desc), invokeSignature,
isStatic, isNative);
@@ -149,7 +149,7 @@ class TransformClassAdapter extends ClassAdapter {
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
-
+
/* Visits a field. Makes it public. */
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature,
@@ -157,7 +157,7 @@ class TransformClassAdapter extends ClassAdapter {
// change access to public
access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
access |= Opcodes.ACC_PUBLIC;
-
+
return super.visitField(access, name, desc, signature, value);
}
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java
index 603284e..d6dba6a 100644
--- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java
@@ -22,7 +22,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import com.android.tools.layoutlib.create.AsmAnalyzer.DependencyVisitor;
-import com.android.tools.layoutlib.create.LogTest.MockLog;
import org.junit.After;
import org.junit.Before;
@@ -46,9 +45,9 @@ public class AsmAnalyzerTest {
@Before
public void setUp() throws Exception {
- mLog = new LogTest.MockLog();
+ mLog = new MockLog();
URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar");
-
+
mOsJarPath = new ArrayList<String>();
mOsJarPath.add(url.getFile());
@@ -69,9 +68,9 @@ public class AsmAnalyzerTest {
"mock_android.dummy.InnerTest$DerivingClass",
"mock_android.dummy.InnerTest$MyGenerics1",
"mock_android.dummy.InnerTest$MyIntEnum",
- "mock_android.dummy.InnerTest$MyStaticInnerClass",
- "mock_android.dummy.InnerTest$NotStaticInner1",
- "mock_android.dummy.InnerTest$NotStaticInner2",
+ "mock_android.dummy.InnerTest$MyStaticInnerClass",
+ "mock_android.dummy.InnerTest$NotStaticInner1",
+ "mock_android.dummy.InnerTest$NotStaticInner2",
"mock_android.view.View",
"mock_android.view.ViewGroup",
"mock_android.view.ViewGroup$LayoutParams",
@@ -83,7 +82,7 @@ public class AsmAnalyzerTest {
},
map.keySet().toArray());
}
-
+
@Test
public void testFindClass() throws IOException, LogAbortException {
Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath);
@@ -91,7 +90,7 @@ public class AsmAnalyzerTest {
ClassReader cr = mAa.findClass("mock_android.view.ViewGroup$LayoutParams",
zipClasses, found);
-
+
assertNotNull(cr);
assertEquals("mock_android/view/ViewGroup$LayoutParams", cr.getClassName());
assertArrayEquals(new String[] { "mock_android.view.ViewGroup$LayoutParams" },
@@ -172,14 +171,14 @@ public class AsmAnalyzerTest {
"mock_android.widget.TableLayout",
},
found.keySet().toArray());
-
+
for (String key : found.keySet()) {
ClassReader value = found.get(key);
assertNotNull("No value for " + key, value);
assertEquals(key, AsmAnalyzer.classReaderToClassName(value));
}
}
-
+
@Test
public void testDependencyVisitor() throws IOException, LogAbortException {
Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath);
@@ -190,7 +189,7 @@ public class AsmAnalyzerTest {
ClassReader cr = mAa.findClass("mock_android.widget.TableLayout", zipClasses, keep);
DependencyVisitor visitor = mAa.getVisitor(zipClasses, keep, new_keep, in_deps, out_deps);
-
+
// get first level dependencies
cr.accept(visitor, 0 /* flags */);
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 7cdf79a..f4ff389 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
@@ -20,8 +20,6 @@ package com.android.tools.layoutlib.create;
import static org.junit.Assert.assertArrayEquals;
-import com.android.tools.layoutlib.create.LogTest.MockLog;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -44,9 +42,9 @@ public class AsmGeneratorTest {
@Before
public void setUp() throws Exception {
- mLog = new LogTest.MockLog();
+ mLog = new MockLog();
URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar");
-
+
mOsJarPath = new ArrayList<String>();
mOsJarPath.add(url.getFile());
@@ -65,16 +63,41 @@ public class AsmGeneratorTest {
@Test
public void testClassRenaming() throws IOException, LogAbortException {
-
- AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar,
- null, // classes to inject in the final JAR
- null, // methods to force override
- new String[] { // classes to rename (so that we can replace them)
- "mock_android.view.View", "mock_android.view._Original_View",
- "not.an.actual.ClassName", "anoter.fake.NewClassName",
- },
- null // methods deleted from their return type.
- );
+
+ ICreateInfo ci = new ICreateInfo() {
+ public Class<?>[] getInjectedClasses() {
+ // classes to inject in the final JAR
+ return new Class<?>[0];
+ }
+
+ public String[] getDelegateMethods() {
+ return new String[0];
+ }
+
+ public String[] getDelegateClassNatives() {
+ return new String[0];
+ }
+
+ public String[] getOverriddenMethods() {
+ // methods to force override
+ return new String[0];
+ }
+
+ public String[] getRenamedClasses() {
+ // classes to rename (so that we can replace them)
+ return new String[] {
+ "mock_android.view.View", "mock_android.view._Original_View",
+ "not.an.actual.ClassName", "anoter.fake.NewClassName",
+ };
+ }
+
+ public String[] getDeleteReturns() {
+ // methods deleted from their return type.
+ return new String[0];
+ }
+ };
+
+ AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
null, // derived from
@@ -83,7 +106,7 @@ public class AsmGeneratorTest {
});
aa.analyze();
agen.generate();
-
+
Set<String> notRenamed = agen.getClassesNotRenamed();
assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray());
}
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java
index d6916ae..0135c40 100644
--- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java
@@ -33,8 +33,9 @@ public class ClassHasNativeVisitorTest {
@Test
public void testHasNative() throws IOException {
MockClassHasNativeVisitor cv = new MockClassHasNativeVisitor();
- ClassReader cr = new ClassReader(
- "com.android.tools.layoutlib.create.ClassHasNativeVisitorTest$ClassWithNative");
+ String className =
+ this.getClass().getCanonicalName() + "$" + ClassWithNative.class.getSimpleName();
+ ClassReader cr = new ClassReader(className);
cr.accept(cv, 0 /* flags */);
assertArrayEquals(new String[] { "native_method" }, cv.getMethodsFound());
@@ -44,14 +45,17 @@ public class ClassHasNativeVisitorTest {
@Test
public void testHasNoNative() throws IOException {
MockClassHasNativeVisitor cv = new MockClassHasNativeVisitor();
- ClassReader cr = new ClassReader(
- "com.android.tools.layoutlib.create.ClassHasNativeVisitorTest$ClassWithoutNative");
+ String className =
+ this.getClass().getCanonicalName() + "$" + ClassWithoutNative.class.getSimpleName();
+ ClassReader cr = new ClassReader(className);
cr.accept(cv, 0 /* flags */);
assertArrayEquals(new String[0], cv.getMethodsFound());
assertFalse(cv.hasNativeMethods());
}
+ //-------
+
/**
* Overrides {@link ClassHasNativeVisitor} to collec the name of the native methods found.
*/
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java
new file mode 100644
index 0000000..9ad2e6e
--- /dev/null
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2010 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.util.TraceClassVisitor;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashSet;
+
+public class DelegateClassAdapterTest {
+
+ private MockLog mLog;
+
+ private static final String CLASS_NAME =
+ DelegateClassAdapterTest.class.getCanonicalName() + "$" +
+ ClassWithNative.class.getSimpleName();
+
+ @Before
+ public void setUp() throws Exception {
+ mLog = new MockLog();
+ mLog.setVerbose(true); // capture debug error too
+ }
+
+ /**
+ * Tests that a class not being modified still works.
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testNoOp() throws Exception {
+ // create an instance of the class that will be modified
+ // (load the class in a distinct class loader so that we can trash its definition later)
+ ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { };
+ Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(CLASS_NAME);
+ ClassWithNative instance1 = clazz1.newInstance();
+ assertEquals(42, instance1.add(20, 22));
+ try {
+ instance1.callNativeInstance(10, 3.1415, new Object[0] );
+ fail("Test should have failed to invoke callTheNativeMethod [1]");
+ } catch (UnsatisfiedLinkError e) {
+ // This is expected to fail since the native method is not implemented.
+ }
+
+ // Now process it but tell the delegate to not modify any method
+ ClassWriter cw = new ClassWriter(0 /*flags*/);
+
+ HashSet<String> delegateMethods = new HashSet<String>();
+ String internalClassName = CLASS_NAME.replace('.', '/');
+ DelegateClassAdapter cv = new DelegateClassAdapter(
+ mLog, cw, internalClassName, delegateMethods);
+
+ ClassReader cr = new ClassReader(CLASS_NAME);
+ cr.accept(cv, 0 /* flags */);
+
+ // Load the generated class in a different class loader and try it again
+ final byte[] bytes = cw.toByteArray();
+
+ ClassLoader2 cl2 = new ClassLoader2(bytes) {
+ @Override
+ public void testModifiedInstance() throws Exception {
+ Class<?> clazz2 = loadClass(CLASS_NAME);
+ Object i2 = clazz2.newInstance();
+ assertNotNull(i2);
+ assertEquals(42, callAdd(i2, 20, 22));
+
+ try {
+ callCallNativeInstance(i2, 10, 3.1415, new Object[0]);
+ fail("Test should have failed to invoke callTheNativeMethod [2]");
+ } catch (InvocationTargetException e) {
+ // This is expected to fail since the native method has NOT been
+ // overridden here.
+ assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass());
+ }
+
+ // Check that the native method does NOT have the new annotation
+ Method[] m = clazz2.getDeclaredMethods();
+ assertEquals("native_instance", m[2].getName());
+ assertTrue(Modifier.isNative(m[2].getModifiers()));
+ Annotation[] a = m[2].getAnnotations();
+ assertEquals(0, a.length);
+ }
+ };
+ cl2.testModifiedInstance();
+ }
+
+ /**
+ * {@link DelegateMethodAdapter} does not support overriding constructors yet,
+ * so this should fail with an {@link UnsupportedOperationException}.
+ *
+ * Although not tested here, the message of the exception should contain the
+ * constructor signature.
+ */
+ @Test(expected=UnsupportedOperationException.class)
+ public void testConstructorsNotSupported() throws IOException {
+ ClassWriter cw = new ClassWriter(0 /*flags*/);
+
+ String internalClassName = CLASS_NAME.replace('.', '/');
+
+ HashSet<String> delegateMethods = new HashSet<String>();
+ delegateMethods.add("<init>");
+ DelegateClassAdapter cv = new DelegateClassAdapter(
+ mLog, cw, internalClassName, delegateMethods);
+
+ ClassReader cr = new ClassReader(CLASS_NAME);
+ cr.accept(cv, 0 /* flags */);
+ }
+
+ @Test
+ public void testDelegateNative() throws Exception {
+ ClassWriter cw = new ClassWriter(0 /*flags*/);
+ String internalClassName = CLASS_NAME.replace('.', '/');
+
+ HashSet<String> delegateMethods = new HashSet<String>();
+ delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
+ DelegateClassAdapter cv = new DelegateClassAdapter(
+ mLog, cw, internalClassName, delegateMethods);
+
+ ClassReader cr = new ClassReader(CLASS_NAME);
+ cr.accept(cv, 0 /* flags */);
+
+ // Load the generated class in a different class loader and try it
+ final byte[] bytes = cw.toByteArray();
+
+ try {
+ ClassLoader2 cl2 = new ClassLoader2(bytes) {
+ @Override
+ public void testModifiedInstance() throws Exception {
+ Class<?> clazz2 = loadClass(CLASS_NAME);
+ Object i2 = clazz2.newInstance();
+ assertNotNull(i2);
+
+ // Use reflection to access inner methods
+ assertEquals(42, callAdd(i2, 20, 22));
+
+ Object[] objResult = new Object[] { null };
+ int result = callCallNativeInstance(i2, 10, 3.1415, objResult);
+ assertEquals((int)(10 + 3.1415), result);
+ assertSame(i2, objResult[0]);
+
+ // Check that the native method now has the new annotation and is not native
+ Method[] m = clazz2.getDeclaredMethods();
+ assertEquals("native_instance", m[2].getName());
+ assertFalse(Modifier.isNative(m[2].getModifiers()));
+ Annotation[] a = m[2].getAnnotations();
+ assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName());
+ }
+ };
+ cl2.testModifiedInstance();
+
+ } catch (Throwable t) {
+ // For debugging, dump the bytecode of the class in case of unexpected error.
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ TraceClassVisitor tcv = new TraceClassVisitor(pw);
+
+ ClassReader cr2 = new ClassReader(bytes);
+ cr2.accept(tcv, 0 /* flags */);
+
+ String msg = "\n" + t.getClass().getCanonicalName();
+ if (t.getMessage() != null) {
+ msg += ": " + t.getMessage();
+ }
+ msg = msg + "\nBytecode dump:\n" + sw.toString();
+
+ // Re-throw exception with new message
+ RuntimeException ex = new RuntimeException(msg, t);
+ throw ex;
+ }
+ }
+
+ //-------
+
+ /**
+ * A class loader than can define and instantiate our dummy {@link ClassWithNative}.
+ * <p/>
+ * The trick here is that this class loader will test our modified version of ClassWithNative.
+ * Trying to do so in the original class loader generates all sort of link issues because
+ * there are 2 different definitions of the same class name. This class loader will
+ * define and load the class when requested by name and provide helpers to access the
+ * instance methods via reflection.
+ */
+ private abstract class ClassLoader2 extends ClassLoader {
+ private final byte[] mClassWithNative;
+
+ public ClassLoader2(byte[] classWithNative) {
+ super(null);
+ mClassWithNative = classWithNative;
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ try {
+ return super.findClass(name);
+ } catch (ClassNotFoundException e) {
+
+ if (CLASS_NAME.equals(name)) {
+ // Load the modified ClassWithNative from its bytes representation.
+ return defineClass(CLASS_NAME, mClassWithNative, 0, mClassWithNative.length);
+ }
+
+ try {
+ // Load everything else from the original definition into the new class loader.
+ ClassReader cr = new ClassReader(name);
+ ClassWriter cw = new ClassWriter(0);
+ cr.accept(cw, 0);
+ byte[] bytes = cw.toByteArray();
+ return defineClass(name, bytes, 0, bytes.length);
+
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+ }
+
+ /**
+ * Accesses {@link ClassWithNative#add(int, int)} via reflection.
+ */
+ public int callAdd(Object instance, int a, int b) throws Exception {
+ Method m = instance.getClass().getMethod("add",
+ new Class<?>[] { int.class, int.class });
+
+ Object result = m.invoke(instance, new Object[] { a, b });
+ return ((Integer) result).intValue();
+ }
+
+ /**
+ * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])}
+ * via reflection.
+ */
+ public int callCallNativeInstance(Object instance, int a, double d, Object[] o)
+ throws Exception {
+ Method m = instance.getClass().getMethod("callNativeInstance",
+ new Class<?>[] { int.class, double.class, Object[].class });
+
+ Object result = m.invoke(instance, new Object[] { a, d, o });
+ return ((Integer) result).intValue();
+ }
+
+ public abstract void testModifiedInstance() throws Exception;
+ }
+
+ /**
+ * Dummy test class with a native method.
+ * The native method is not defined and any attempt to invoke it will
+ * throw an {@link UnsatisfiedLinkError}.
+ */
+ public static class ClassWithNative {
+ public ClassWithNative() {
+ }
+
+ public int add(int a, int b) {
+ return a + b;
+ }
+
+ public int callNativeInstance(int a, double d, Object[] o) {
+ return native_instance(a, d, o);
+ }
+
+ private native int native_instance(int a, double d, Object[] o);
+ }
+
+ /**
+ * The delegate that receives the call to {@link ClassWithNative}'s overridden methods.
+ */
+ public static class ClassWithNative_Delegate {
+ public static int native_instance(ClassWithNative instance, int a, double d, Object[] o) {
+ if (o != null && o.length > 0) {
+ o[0] = instance;
+ }
+ return (int)(a + d);
+ }
+ }
+}
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java
index 3f13158..1a5f653 100644
--- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java
@@ -24,33 +24,8 @@ import org.junit.Test;
public class LogTest {
- public static class MockLog extends Log {
- StringBuilder mOut = new StringBuilder();
- StringBuilder mErr = new StringBuilder();
-
- public String getOut() {
- return mOut.toString();
- }
-
- public String getErr() {
- return mErr.toString();
- }
-
- @Override
- protected void outPrintln(String msg) {
- mOut.append(msg);
- mOut.append('\n');
- }
-
- @Override
- protected void errPrintln(String msg) {
- mErr.append(msg);
- mErr.append('\n');
- }
- }
-
private MockLog mLog;
-
+
@Before
public void setUp() throws Exception {
mLog = new MockLog();
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/MockLog.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/MockLog.java
new file mode 100644
index 0000000..de750a3
--- /dev/null
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/MockLog.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010 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;
+
+
+public class MockLog extends Log {
+ StringBuilder mOut = new StringBuilder();
+ StringBuilder mErr = new StringBuilder();
+
+ public String getOut() {
+ return mOut.toString();
+ }
+
+ public String getErr() {
+ return mErr.toString();
+ }
+
+ @Override
+ protected void outPrintln(String msg) {
+ mOut.append(msg);
+ mOut.append('\n');
+ }
+
+ @Override
+ protected void errPrintln(String msg) {
+ mErr.append(msg);
+ mErr.append('\n');
+ }
+}