diff options
Diffstat (limited to 'tools/layoutlib/create')
46 files changed, 6704 insertions, 0 deletions
diff --git a/tools/layoutlib/create/.classpath b/tools/layoutlib/create/.classpath new file mode 100644 index 0000000..dbc4cfd --- /dev/null +++ b/tools/layoutlib/create/.classpath @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry excluding="mock_android/" kind="src" path="tests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/create/.project b/tools/layoutlib/create/.project new file mode 100644 index 0000000..e100d17 --- /dev/null +++ b/tools/layoutlib/create/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_create</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/create/.settings/README.txt b/tools/layoutlib/create/.settings/README.txt new file mode 100644 index 0000000..9120b20 --- /dev/null +++ b/tools/layoutlib/create/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file diff --git a/tools/layoutlib/create/.settings/org.eclipse.jdt.core.prefs b/tools/layoutlib/create/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..5381a0e --- /dev/null +++ b/tools/layoutlib/create/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,93 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/tools/layoutlib/create/Android.mk b/tools/layoutlib/create/Android.mk new file mode 100644 index 0000000..9bd48ab --- /dev/null +++ b/tools/layoutlib/create/Android.mk @@ -0,0 +1,28 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAR_MANIFEST := manifest.txt +LOCAL_STATIC_JAVA_LIBRARIES := \ + asm-4.0 + +LOCAL_MODULE := layoutlib_create + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/tools/layoutlib/create/README.txt b/tools/layoutlib/create/README.txt new file mode 100644 index 0000000..894611b --- /dev/null +++ b/tools/layoutlib/create/README.txt @@ -0,0 +1,240 @@ +# Copyright (C) 2008 The Android Open Source Project + + +- Description - +--------------- + +Layoutlib_create generates a JAR library used by the Eclipse graphical layout editor +to perform layout. + + +- Usage - +--------- + + ./layoutlib_create path/to/android.jar destination.jar + + +- Design Overview - +------------------- + +Layoutlib_create uses the "android.jar" containing all the Java code used by Android +as generated by the Android build, right before the classes are converted to a DEX format. + +The Android JAR can't be used directly in Eclipse: +- it contains references to native code (which we want to avoid in Eclipse), +- some classes need to be overridden, for example all the drawing code that is + replaced by Java 2D calls in Eclipse. +- some of the classes that need to be changed are final and/or we need access + to their private internal state. + +Consequently this tool: +- parses the input JAR, +- modifies some of the classes directly using some bytecode manipulation, +- filters some packages and removes those we don't want in the output JAR, +- injects some new classes, +- generates a modified JAR file that is suitable for the Android plugin + for Eclipse to perform rendering. + +The ASM library is used to do the bytecode modification using its visitor pattern API. + +The layoutlib_create is *NOT* generic. There is no configuration file. Instead all the +configuration is done in the main() method and the CreateInfo structure is expected to +change with the Android platform as new classes are added, changed or removed. + +The resulting JAR is used by layoutlib_bridge (a.k.a. "the bridge"), also part of the +platform, that provides all the necessary missing implementation for rendering graphics +in Eclipse. + + + +- Implementation Notes - +------------------------ + +The tool works in two phases: +- first analyze the input jar (AsmAnalyzer class) +- then generate the output jar (AsmGenerator class), + + +- Analyzer +---------- + +The goal of the analyzer is to create a graph of all the classes from the input JAR +with their dependencies and then only keep the ones we want. + +To do that, the analyzer is created with a list of base classes to keep -- everything +that derives from these is kept. Currently the one such class is android.view.View: +since we want to render layouts, anything that is sort of a view needs to be kept. + +The analyzer is also given a list of class names to keep in the output. +This is done using shell-like glob patterns that filter on the fully-qualified +class names, for example "android.*.R**" ("*" does not matches dots whilst "**" does, +and "." and "$" are interpreted as-is). +In practice we almost but not quite request the inclusion of full packages. + +With this information, the analyzer parses the input zip to find all the classes. +All classes deriving from the requested bases classes are kept. +All classes which name matched the glob pattern are kept. +The analysis then finds all the dependencies of the classes that are to be kept +using an ASM visitor on the class, the field types, the method types and annotations types. +Classes that belong to the current JRE are excluded. + +The output of the analyzer is a set of ASM ClassReader instances which are then +fed to the generator. + + +- Generator +----------- + +The generator is constructed from a CreateInfo struct that acts as a config file +and lists: +- the classes to inject in the output JAR -- these classes are directly implemented + in layoutlib_create and will be used to interface with the renderer in Eclipse. +- specific methods to override (see method stubs details below). +- specific methods for which to delegate calls. +- specific methods to remove based on their return type. +- specific classes to rename. + +Each of these are specific strategies we use to be able to modify the Android code +to fit within the Eclipse renderer. These strategies are explained beow. + +The core method of the generator is transform(): it takes an input ASM ClassReader +and modifies it to produce a byte array suitable for the final JAR file. + +The first step of the transformation is changing the name of the class in case +we requested the class to be renamed. This uses the RenameClassAdapter to also rename +all inner classes and references in methods and types. Note that other classes are +not transformed and keep referencing the original name. + +The TransformClassAdapter is then used to process the potentially renamed class. +All protected or private classes are market as public. +All classes are made non-final. +Interfaces are left as-is. + +If a method has a return type that must be erased, the whole method is skipped. +Methods are also changed from protected/private to public. +The code of the methods is then kept as-is, except for native methods which are +replaced by a stub. Methods that are to be overridden are also replaced by a stub. + +The transformed class is then fed through the DelegateClassAdapter to implement +method delegates. + +Finally fields are also visited and changed from protected/private to public. + + +- Method stubs +-------------- + +As indicated above, all native and overridden methods are replaced by a stub. +We don't have the code to replace with in layoutlib_create. +Instead the StubMethodAdapter replaces the code of the method by a call to +OverrideMethod.invokeX(). When using the final JAR, the bridge can register +listeners from these overridden method calls based on the method signatures. + +The listeners are currently pretty basic: we only pass the signature of the +method being called, its caller object and a flag indicating whether the +method was native. We do not currently provide the parameters. The listener +can however specify the return value of the overridden method. + +This strategy is now obsolete and replaced by the method delegates. + + +- Strategies +------------ + +We currently have 4 strategies to deal with overriding the rendering code +and make it run in Eclipse. Most of these strategies are implemented hand-in-hand +by the bridge (which runs in Eclipse) and the generator. + + +1- Class Injection + +This is the easiest: we currently inject 4 classes, namely: +- OverrideMethod and its associated MethodListener and MethodAdapter are used + to intercept calls to some specific methods that are stubbed out and change + their return value. +- CreateInfo class, which configured the generator. Not used yet, but could + in theory help us track what the generator changed. + + +2- Overriding methods + +As explained earlier, the creator doesn't have any replacement code for +methods to override. Instead it removes the original code and replaces it +by a call to a specific OveriddeMethod.invokeX(). The bridge then registers +a listener on the method signature and can provide an implementation. + +This strategy is now obsolete and replaced by the method delegates. +See strategy 5 below. + + +3- Renaming classes + +This simply changes the name of a class in its definition, as well as all its +references in internal inner classes and methods. +Calls from other classes are not modified -- they keep referencing the original +class name. This allows the bridge to literally replace an implementation. + +An example will make this easier: android.graphics.Paint is the main drawing +class that we need to replace. To do so, the generator renames Paint to _original_Paint. +Later the bridge provides its own replacement version of Paint which will be used +by the rest of the Android stack. The replacement version of Paint can still use +(either by inheritance or delegation) all the original non-native code of _original_Paint +if it so desires. + +Some of the Android classes are basically wrappers over native objects and since +we don't have the native code in Eclipse, we need to provide a full alternate +implementation. Sub-classing doesn't work as some native methods are static and +we don't control object creation. + +This won't rename/replace the inner static methods of a given class. + + +4- Method erasure based on return type + +This is mostly an implementation detail of the bridge: in the Paint class +mentioned above, some inner static classes are used to pass around +attributes (e.g. FontMetrics, or the Style enum) and all the original implementation +is native. + +In this case we have a strategy that tells the generator that anything returning, for +example, the inner class Paint$Style in the Paint class should be discarded and the +bridge will provide its own implementation. + + +5- Method Delegates + +This strategy is used to override method implementations. +Given a method SomeClass.MethodName(), 1 or 2 methods are generated: +a- A copy of the original method named SomeClass.MethodName_Original(). + The content is the original method as-is from the reader. + This step is omitted if the method is native, since it has no Java implementation. +b- A brand new implementation of SomeClass.MethodName() which calls to a + non-existing static method named SomeClass_Delegate.MethodName(). + The implementation of this 'delegate' method is done in layoutlib_brigde. + +The delegate method is a static method. +If the original method is non-static, the delegate method receives the original 'this' +as its first argument. If the original method is an inner non-static method, it also +receives the inner 'this' as the second argument. + + + +- 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/manifest.txt b/tools/layoutlib/create/manifest.txt new file mode 100644 index 0000000..238e7f9 --- /dev/null +++ b/tools/layoutlib/create/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.tools.layoutlib.create.Main 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/annotations/Nullable.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/Nullable.java new file mode 100644 index 0000000..0689c92 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/Nullable.java @@ -0,0 +1,35 @@ +/* + * 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 parameter or field can be null. + * <p/> + * When decorating a method call parameter, this denotes the parameter can + * legitimately be null and the method will gracefully deal with it. Typically used + * on optional parameters. + * <p/> + * When decorating a method, this denotes the method might legitimately return null. + * <p/> + * This is a marker annotation and it has no specific attributes. + */ +@Retention(RetentionPolicy.SOURCE) +public @interface Nullable { +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/VisibleForTesting.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/VisibleForTesting.java new file mode 100644 index 0000000..e4e016b --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/VisibleForTesting.java @@ -0,0 +1,50 @@ +/* + * 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 that the class, method or field has its visibility relaxed so + * that unit tests can access it. + * <p/> + * The <code>visibility</code> argument can be used to specific what the original + * visibility should have been if it had not been made public or package-private for testing. + * The default is to consider the element private. + */ +@Retention(RetentionPolicy.SOURCE) +public @interface VisibleForTesting { + /** + * Intended visibility if the element had not been made public or package-private for + * testing. + */ + enum Visibility { + /** The element should be considered protected. */ + PROTECTED, + /** The element should be considered package-private. */ + PACKAGE, + /** The element should be considered private. */ + PRIVATE + } + + /** + * Intended visibility if the element had not been made public or package-private for testing. + * If not specified, one should assume the element originally intended to be private. + */ + Visibility visibility() default Visibility.PRIVATE; +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java new file mode 100644 index 0000000..412695f --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -0,0 +1,845 @@ +/* + * 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 org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Analyzes the input JAR using the ASM java bytecode manipulation library + * to list the desired classes and their dependencies. + */ +public class AsmAnalyzer { + + // Note: a bunch of stuff has package-level access for unit tests. Consider it private. + + /** Output logger. */ + private final Log mLog; + /** The input source JAR to parse. */ + private final List<String> mOsSourceJar; + /** The generator to fill with the class list and dependency list. */ + private final AsmGenerator mGen; + /** Keep all classes that derive from these one (these included). */ + private final String[] mDeriveFrom; + /** Glob patterns of classes to keep, e.g. "com.foo.*" */ + private final String[] mIncludeGlobs; + + /** + * Creates a new analyzer. + * + * @param log The log output. + * @param osJarPath The input source JARs to parse. + * @param gen The generator to fill with the class list and dependency list. + * @param deriveFrom Keep all classes that derive from these one (these included). + * @param includeGlobs Glob patterns of classes to keep, e.g. "com.foo.*" + * ("*" does not matches dots whilst "**" does, "." and "$" are interpreted as-is) + */ + public AsmAnalyzer(Log log, List<String> osJarPath, AsmGenerator gen, + String[] deriveFrom, String[] includeGlobs) { + mLog = log; + mGen = gen; + mOsSourceJar = osJarPath != null ? osJarPath : new ArrayList<String>(); + mDeriveFrom = deriveFrom != null ? deriveFrom : new String[0]; + mIncludeGlobs = includeGlobs != null ? includeGlobs : new String[0]; + } + + /** + * Starts the analysis using parameters from the constructor. + * Fills the generator with classes & dependencies found. + */ + public void analyze() throws IOException, LogAbortException { + + AsmAnalyzer visitor = this; + + Map<String, ClassReader> zipClasses = parseZip(mOsSourceJar); + mLog.info("Found %d classes in input JAR%s.", zipClasses.size(), + mOsSourceJar.size() > 1 ? "s" : ""); + + Map<String, ClassReader> found = findIncludes(zipClasses); + Map<String, ClassReader> deps = findDeps(zipClasses, found); + + if (mGen != null) { + mGen.setKeep(found); + mGen.setDeps(deps); + } + } + + /** + * Parses a JAR file and returns a list of all classes founds using a map + * class name => ASM ClassReader. Class names are in the form "android.view.View". + */ + Map<String,ClassReader> parseZip(List<String> jarPathList) throws IOException { + TreeMap<String, ClassReader> classes = new TreeMap<String, ClassReader>(); + + for (String jarPath : jarPathList) { + ZipFile zip = new ZipFile(jarPath); + Enumeration<? extends ZipEntry> entries = zip.entries(); + ZipEntry entry; + while (entries.hasMoreElements()) { + entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + ClassReader cr = new ClassReader(zip.getInputStream(entry)); + String className = classReaderToClassName(cr); + classes.put(className, cr); + } + } + } + + return classes; + } + + /** + * Utility that returns the fully qualified binary class name for a ClassReader. + * E.g. it returns something like android.view.View. + */ + static String classReaderToClassName(ClassReader classReader) { + if (classReader == null) { + return null; + } else { + return classReader.getClassName().replace('/', '.'); + } + } + + /** + * Utility that returns the fully qualified binary class name from a path-like FQCN. + * E.g. it returns android.view.View from android/view/View. + */ + static String internalToBinaryClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('/', '.'); + } + } + + /** + * Process the "includes" arrays. + * <p/> + * This updates the in_out_found map. + */ + Map<String, ClassReader> findIncludes(Map<String, ClassReader> zipClasses) + throws LogAbortException { + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + mLog.debug("Find classes to include."); + + for (String s : mIncludeGlobs) { + findGlobs(s, zipClasses, found); + } + for (String s : mDeriveFrom) { + findClassesDerivingFrom(s, zipClasses, found); + } + + return found; + } + + + /** + * Uses ASM to find the class reader for the given FQCN class name. + * If found, insert it in the in_out_found map. + * Returns the class reader object. + */ + ClassReader findClass(String className, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + ClassReader classReader = zipClasses.get(className); + if (classReader == null) { + throw new LogAbortException("Class %s not found by ASM in %s", + className, mOsSourceJar); + } + + inOutFound.put(className, classReader); + return classReader; + } + + /** + * Insert in the inOutFound map all classes found in zipClasses that match the + * given glob pattern. + * <p/> + * The glob pattern is not a regexp. It only accepts the "*" keyword to mean + * "anything but a period". The "." and "$" characters match themselves. + * The "**" keyword means everything including ".". + * <p/> + * Examples: + * <ul> + * <li>com.foo.* matches all classes in the package com.foo but NOT sub-packages. + * <li>com.foo*.*$Event matches all internal Event classes in a com.foo*.* class. + * </ul> + */ + void findGlobs(String globPattern, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + // transforms the glob pattern in a regexp: + // - escape "." with "\." + // - replace "*" by "[^.]*" + // - escape "$" with "\$" + // - add end-of-line match $ + globPattern = globPattern.replaceAll("\\$", "\\\\\\$"); + globPattern = globPattern.replaceAll("\\.", "\\\\."); + // prevent ** from being altered by the next rule, then process the * rule and finally + // the real ** rule (which is now @) + globPattern = globPattern.replaceAll("\\*\\*", "@"); + globPattern = globPattern.replaceAll("\\*", "[^.]*"); + globPattern = globPattern.replaceAll("@", ".*"); + globPattern += "$"; + + Pattern regexp = Pattern.compile(globPattern); + + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String class_name = entry.getKey(); + if (regexp.matcher(class_name).matches()) { + findClass(class_name, zipClasses, inOutFound); + } + } + } + + /** + * Checks all the classes defined in the JarClassName instance and uses BCEL to + * determine if they are derived from the given FQCN super class name. + * Inserts the super class and all the class objects found in the map. + */ + void findClassesDerivingFrom(String super_name, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + ClassReader super_clazz = findClass(super_name, zipClasses, inOutFound); + + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String className = entry.getKey(); + if (super_name.equals(className)) { + continue; + } + ClassReader classReader = entry.getValue(); + ClassReader parent_cr = classReader; + while (parent_cr != null) { + String parent_name = internalToBinaryClassName(parent_cr.getSuperName()); + if (parent_name == null) { + // not found + break; + } else if (super_name.equals(parent_name)) { + inOutFound.put(className, classReader); + break; + } + parent_cr = zipClasses.get(parent_name); + } + } + } + + /** + * Instantiates a new DependencyVisitor. Useful for unit tests. + */ + DependencyVisitor getVisitor(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inKeep, + Map<String, ClassReader> outKeep, + Map<String, ClassReader> inDeps, + Map<String, ClassReader> outDeps) { + return new DependencyVisitor(zipClasses, inKeep, outKeep, inDeps, outDeps); + } + + /** + * Finds all dependencies for all classes in keepClasses which are also + * listed in zipClasses. Returns a map of all the dependencies found. + */ + Map<String, ClassReader> findDeps(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutKeepClasses) { + + TreeMap<String, ClassReader> deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> temp = new TreeMap<String, ClassReader>(); + + DependencyVisitor visitor = getVisitor(zipClasses, + inOutKeepClasses, new_keep, + deps, new_deps); + + for (ClassReader cr : inOutKeepClasses.values()) { + cr.accept(visitor, 0 /* flags */); + } + + while (new_deps.size() > 0 || new_keep.size() > 0) { + deps.putAll(new_deps); + inOutKeepClasses.putAll(new_keep); + + temp.clear(); + temp.putAll(new_deps); + temp.putAll(new_keep); + new_deps.clear(); + new_keep.clear(); + mLog.debug("Found %1$d to keep, %2$d dependencies.", + inOutKeepClasses.size(), deps.size()); + + for (ClassReader cr : temp.values()) { + cr.accept(visitor, 0 /* flags */); + } + } + + mLog.info("Found %1$d classes to keep, %2$d class dependencies.", + inOutKeepClasses.size(), deps.size()); + + return deps; + } + + + + // ---------------------------------- + + /** + * Visitor to collect all the type dependencies from a class. + */ + public class DependencyVisitor extends ClassVisitor { + + /** All classes found in the source JAR. */ + private final Map<String, ClassReader> mZipClasses; + /** Classes from which dependencies are to be found. */ + private final Map<String, ClassReader> mInKeep; + /** Dependencies already known. */ + private final Map<String, ClassReader> mInDeps; + /** New dependencies found by this visitor. */ + private final Map<String, ClassReader> mOutDeps; + /** New classes to keep as-is found by this visitor. */ + private final Map<String, ClassReader> mOutKeep; + + /** + * Creates a new visitor that will find all the dependencies for the visited class. + * Types which are already in the zipClasses, keepClasses or inDeps are not marked. + * New dependencies are marked in outDeps. + * + * @param zipClasses All classes found in the source JAR. + * @param inKeep Classes from which dependencies are to be found. + * @param inDeps Dependencies already known. + * @param outDeps New dependencies found by this visitor. + */ + public DependencyVisitor(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inKeep, + Map<String, ClassReader> outKeep, + Map<String,ClassReader> inDeps, + Map<String,ClassReader> outDeps) { + super(Opcodes.ASM4); + mZipClasses = zipClasses; + mInKeep = inKeep; + mOutKeep = outKeep; + mInDeps = inDeps; + mOutDeps = outDeps; + } + + /** + * Considers the given class name as a dependency. + * If it does, add to the mOutDeps map. + */ + public void considerName(String className) { + if (className == null) { + return; + } + + className = internalToBinaryClassName(className); + + // exclude classes that have already been found + if (mInKeep.containsKey(className) || + mOutKeep.containsKey(className) || + mInDeps.containsKey(className) || + mOutDeps.containsKey(className)) { + return; + } + + // exclude classes that are not part of the JAR file being examined + ClassReader cr = mZipClasses.get(className); + if (cr == null) { + return; + } + + try { + // exclude classes that are part of the default JRE (the one executing this program) + if (getClass().getClassLoader().loadClass(className) != null) { + return; + } + } catch (ClassNotFoundException e) { + // ignore + } + + // accept this class: + // - android classes are added to dependencies + // - non-android classes are added to the list of classes to keep as-is (they don't need + // to be stubbed). + if (className.indexOf("android") >= 0) { // TODO make configurable + mOutDeps.put(className, cr); + } else { + mOutKeep.put(className, cr); + } + } + + /** + * Considers this array of names using considerName(). + */ + public void considerNames(String[] classNames) { + if (classNames != null) { + for (String className : classNames) { + considerName(className); + } + } + } + + /** + * Considers this signature or type signature by invoking the {@link SignatureVisitor} + * on it. + */ + public void considerSignature(String signature) { + if (signature != null) { + SignatureReader sr = new SignatureReader(signature); + // SignatureReader.accept will call accessType so we don't really have + // to differentiate where the signature comes from. + sr.accept(new MySignatureVisitor()); + } + } + + /** + * Considers this {@link Type}. For arrays, the element type is considered. + * If the type is an object, it's internal name is considered. + */ + public void considerType(Type t) { + if (t != null) { + if (t.getSort() == Type.ARRAY) { + t = t.getElementType(); + } + if (t.getSort() == Type.OBJECT) { + considerName(t.getInternalName()); + } + } + } + + /** + * Considers a descriptor string. The descriptor is converted to a {@link Type} + * and then considerType() is invoked. + */ + public void considerDesc(String desc) { + if (desc != null) { + try { + Type t = Type.getType(desc); + considerType(t); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + } + + + // --------------------------------------------------- + // --- ClassVisitor, FieldVisitor + // --------------------------------------------------- + + // Visits a class header + @Override + public void visit(int version, int access, String name, + String signature, String superName, String[] interfaces) { + // signature is the signature of this class. May be null if the class is not a generic + // one, and does not extend or implement generic classes or interfaces. + + if (signature != null) { + considerSignature(signature); + } + + // superName is the internal of name of the super class (see getInternalName). + // For interfaces, the super class is Object. May be null but only for the Object class. + considerName(superName); + + // interfaces is the internal names of the class's interfaces (see getInternalName). + // May be null. + considerNames(interfaces); + } + + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + + private class MyFieldVisitor extends FieldVisitor { + + public MyFieldVisitor() { + super(Opcodes.ASM4); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // desc is the field's descriptor (see Type). + considerDesc(desc); + + // signature is the field's signature. May be null if the field's type does not use + // generic types. + considerSignature(signature); + + return new MyFieldVisitor(); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // name is the internal name of an inner class (see getInternalName). + considerName(name); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + // desc is the method's descriptor (see Type). + considerDesc(desc); + // signature is the method's signature. May be null if the method parameters, return + // type and exceptions do not use generic types. + considerSignature(signature); + + return new MyMethodVisitor(); + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + @Override + public void visitSource(String source, String debug) { + // pass + } + + + // --------------------------------------------------- + // --- MethodVisitor + // --------------------------------------------------- + + private class MyMethodVisitor extends MethodVisitor { + + public MyMethodVisitor() { + super(Opcodes.ASM4); + } + + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return new MyAnnotationVisitor(); + } + + @Override + public void visitCode() { + // pass + } + + // field instruction + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + // name is the field's name. + considerName(name); + // desc is the field's descriptor (see Type). + considerDesc(desc); + } + + @Override + public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) { + // pass + } + + @Override + public void visitIincInsn(int var, int increment) { + // pass -- an IINC instruction + } + + @Override + public void visitInsn(int opcode) { + // pass -- a zero operand instruction + } + + @Override + public void visitIntInsn(int opcode, int operand) { + // pass -- a single int operand instruction + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + // pass -- a jump instruction + } + + @Override + public void visitLabel(Label label) { + // pass -- a label target + } + + // instruction to load a constant from the stack + @Override + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + considerType((Type) cst); + } + } + + @Override + public void visitLineNumber(int line, Label start) { + // pass + } + + @Override + public void visitLocalVariable(String name, String desc, + String signature, Label start, Label end, int index) { + // desc is the type descriptor of this local variable. + considerDesc(desc); + // signature is the type signature of this local variable. May be null if the local + // variable type does not use generic types. + considerSignature(signature); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // pass -- a lookup switch instruction + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + // pass + } + + // instruction that invokes a method + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + + // owner is the internal name of the method's owner class + considerName(owner); + // desc is the method's descriptor (see Type). + considerDesc(desc); + } + + // instruction multianewarray, whatever that is + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + + // desc an array type descriptor. + considerDesc(desc); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // pass -- table switch instruction + + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // type is the internal name of the type of exceptions handled by the handler, + // or null to catch any exceptions (for "finally" blocks). + considerName(type); + } + + // type instruction + @Override + public void visitTypeInsn(int opcode, String type) { + // type is the operand of the instruction to be visited. This operand must be the + // internal name of an object or array class. + considerName(type); + } + + @Override + public void visitVarInsn(int opcode, int var) { + // pass -- local variable instruction + } + } + + private class MySignatureVisitor extends SignatureVisitor { + + public MySignatureVisitor() { + super(Opcodes.ASM4); + } + + // --------------------------------------------------- + // --- SignatureVisitor + // --------------------------------------------------- + + private String mCurrentSignatureClass = null; + + // Starts the visit of a signature corresponding to a class or interface type + @Override + public void visitClassType(String name) { + mCurrentSignatureClass = name; + considerName(name); + } + + // Visits an inner class + @Override + public void visitInnerClassType(String name) { + if (mCurrentSignatureClass != null) { + mCurrentSignatureClass += "$" + name; + considerName(mCurrentSignatureClass); + } + } + + @Override + public SignatureVisitor visitArrayType() { + return new MySignatureVisitor(); + } + + @Override + public void visitBaseType(char descriptor) { + // pass -- a primitive type, ignored + } + + @Override + public SignatureVisitor visitClassBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitExceptionType() { + return new MySignatureVisitor(); + } + + @Override + public void visitFormalTypeParameter(String name) { + // pass + } + + @Override + public SignatureVisitor visitInterface() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitParameterType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitReturnType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitSuperclass() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + return new MySignatureVisitor(); + } + + @Override + public void visitTypeVariable(String name) { + // pass + } + + @Override + public void visitTypeArgument() { + // pass + } + } + + + // --------------------------------------------------- + // --- AnnotationVisitor + // --------------------------------------------------- + + private class MyAnnotationVisitor extends AnnotationVisitor { + + public MyAnnotationVisitor() { + super(Opcodes.ASM4); + } + + // Visits a primitive value of an annotation + @Override + public void visit(String name, Object value) { + // value is the actual value, whose type must be Byte, Boolean, Character, Short, + // Integer, Long, Float, Double, String or Type + if (value instanceof Type) { + considerType((Type) value); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + // desc is the class descriptor of the nested annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public AnnotationVisitor visitArray(String name) { + return new MyAnnotationVisitor(); + } + + @Override + public void visitEnum(String name, String desc, String value) { + // desc is the class descriptor of the enumeration class. + considerDesc(desc); + } + } + } +} 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 new file mode 100644 index 0000000..a9ede26 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -0,0 +1,370 @@ +/* + * 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 org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +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.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * Class that generates a new JAR from a list of classes, some of which are to be kept as-is + * and some of which are to be stubbed partially or totally. + */ +public class AsmGenerator { + + /** Output logger. */ + private final Log mLog; + /** The path of the destination JAR to create. */ + private final String mOsDestJar; + /** List of classes to inject in the final JAR from _this_ archive. */ + private final Class<?>[] mInjectClasses; + /** The set of methods to stub out. */ + private final Set<String> mStubMethods; + /** All classes to output as-is, except if they have native methods. */ + private Map<String, ClassReader> mKeep; + /** All dependencies that must be completely stubbed. */ + private Map<String, ClassReader> mDeps; + /** Counter of number of classes renamed during transform. */ + private int mRenameCount; + /** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */ + private final HashMap<String, String> mRenameClasses; + /** FQCN Names of "old" classes that were NOT renamed. This starts with the full list of + * 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 => 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 createInfo Creation parameters. Must not be null. + */ + public AsmGenerator(Log log, String osDestJar, ICreateInfo createInfo) { + mLog = log; + mOsDestJar = osDestJar; + 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()) { + className = binaryToInternalClassName(className); + 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>(); + 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 "." + String oldFqcn = binaryToInternalClassName(renameClasses[i]); + String newFqcn = binaryToInternalClassName(renameClasses[i + 1]); + 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>>(); + 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); + } + + 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/> + * The names are "internal class names" rather than FQCN, i.e. they use "/" instead "." + * as package separators. + */ + public Set<String> getClassesNotRenamed() { + return mClassesNotRenamed; + } + + /** + * Utility that returns the internal ASM class name from a fully qualified binary class + * name. E.g. it returns android/view/View from android.view.View. + */ + String binaryToInternalClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('.', '/'); + } + } + + /** Sets the map of classes to output as-is, except if they have native methods */ + public void setKeep(Map<String, ClassReader> keep) { + mKeep = keep; + } + + /** Sets the map of dependencies that must be completely stubbed */ + 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; + } + + /** 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); + ClassReader cr = new ClassReader(is); + byte[] b = transform(cr, true /* stubNativesOnly */); + 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 */); + String name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + for (Entry<String, ClassReader> entry : mKeep.entrySet()) { + ClassReader cr = entry.getValue(); + byte[] b = transform(cr, true /* stubNativesOnly */); + String name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + mLog.info("# deps classes: %d", mDeps.size()); + mLog.info("# keep classes: %d", mKeep.size()); + mLog.info("# renamed : %d", mRenameCount); + + createJar(new FileOutputStream(mOsDestJar), all); + mLog.info("Created JAR file %s", mOsDestJar); + } + + /** + * Writes the JAR file. + * + * @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 + */ + void createJar(FileOutputStream outStream, Map<String,byte[]> all) throws IOException { + JarOutputStream jar = new JarOutputStream(outStream); + for (Entry<String, byte[]> entry : all.entrySet()) { + String name = entry.getKey(); + JarEntry jar_entry = new JarEntry(name); + jar.putNextEntry(jar_entry); + jar.write(entry.getValue()); + jar.closeEntry(); + } + jar.flush(); + jar.close(); + } + + /** + * Utility method that converts a fully qualified java name into a JAR entry path + * e.g. for the input "android.view.View" it returns "android/view/View.class" + */ + 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" + */ + private String classToEntryPath(Class<?> clazz) { + String name = ""; + Class<?> parent; + while ((parent = clazz.getEnclosingClass()) != null) { + name = "$" + clazz.getSimpleName() + name; + clazz = parent; + } + 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) { + mRenameCount++; + // This class is being renamed, so remove it from the list of classes not renamed. + mClassesNotRenamed.remove(className); + } + + mLog.debug("Transform %s%s%s%s", className, + newName == className ? "" : " (renamed to " + newName + ")", + hasNativeMethods ? " -- has natives" : "", + stubNativesOnly ? " -- stub natives only" : ""); + + // 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); + } + + ClassVisitor cv = new TransformClassAdapter(mLog, mStubMethods, + mDeleteReturns.get(className), + newName, rv, + stubNativesOnly, stubNativesOnly || hasNativeMethods); + + 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))) { + cv = new DelegateClassAdapter(mLog, cv, className, delegateMethods); + } + } + + cr.accept(cv, 0 /* flags */); + return cw.toByteArray(); + } + + /** + * Should this class be renamed, this returns the new name. Otherwise it returns the + * original name. + * + * @param className The internal ASM name of the class that may have to be renamed + * @return A new transformed name or the original input argument. + */ + String transformName(String className) { + String newName = mRenameClasses.get(className); + if (newName != null) { + return newName; + } + int pos = className.indexOf('$'); + if (pos > 0) { + // Is this an inner class of a renamed class? + String base = className.substring(0, pos); + newName = mRenameClasses.get(base); + if (newName != null) { + return newName + className.substring(pos); + } + } + + return className; + } + + /** + * Returns true if a class has any native methods. + */ + boolean hasNativeMethods(ClassReader cr) { + ClassHasNativeVisitor cv = new ClassHasNativeVisitor(); + cr.accept(cv, 0 /* flags */); + return cv.hasNativeMethods(); + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java new file mode 100644 index 0000000..2c955fd --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java @@ -0,0 +1,102 @@ +/* + * 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.VisibleForTesting; +import com.android.tools.layoutlib.annotations.VisibleForTesting.Visibility; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Indicates if a class contains any native methods. + */ +public class ClassHasNativeVisitor extends ClassVisitor { + public ClassHasNativeVisitor() { + super(Opcodes.ASM4); + } + + private boolean mHasNativeMethods = false; + + public boolean hasNativeMethods() { + return mHasNativeMethods; + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void setHasNativeMethods(boolean hasNativeMethods, String methodName) { + mHasNativeMethods = hasNativeMethods; + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + // pass + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // pass + return null; + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + @Override + public void visitEnd() { + // pass + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // pass + return null; + } + + @Override + public void visitInnerClass(String name, String outerName, + String innerName, int access) { + // pass + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + if ((access & Opcodes.ACC_NATIVE) != 0) { + setHasNativeMethods(true, name); + } + return null; + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + @Override + public void visitSource(String source, String debug) { + // pass + } + +} 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 new file mode 100644 index 0000000..d955040 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -0,0 +1,207 @@ +/* + * 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; + +/** + * 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. + */ + @Override + 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. + */ + @Override + 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. + */ + @Override + 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. + * <p/> + * This usage is deprecated. Please use method 'delegates' instead. + */ + @Override + 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. + */ + @Override + 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. + */ + @Override + public String[] getDeleteReturns() { + return DELETE_RETURNS; + } + + //----- + + /** + * The list of class from layoutlib_create to inject in layoutlib. + */ + private final static Class<?>[] INJECTED_CLASSES = new Class<?>[] { + OverrideMethod.class, + MethodListener.class, + MethodAdapter.class, + ICreateInfo.class, + CreateInfo.class, + LayoutlibDelegate.class + }; + + /** + * The list of methods to rewrite as delegates. + */ + public final static String[] DELEGATE_METHODS = new String[] { + "android.app.Fragment#instantiate", //(Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)Landroid/app/Fragment;", + "android.content.res.Resources$Theme#obtainStyledAttributes", + "android.content.res.Resources$Theme#resolveAttribute", + "android.content.res.TypedArray#getValueAt", + "android.graphics.BitmapFactory#finishDecode", + "android.os.Handler#sendMessageAtTime", + "android.os.HandlerThread#run", + "android.os.Build#getString", + "android.text.format.DateFormat#is24HourFormat", + "android.view.Choreographer#getRefreshRate", + "android.view.Display#updateDisplayInfoLocked", + "android.view.LayoutInflater#rInflate", + "android.view.LayoutInflater#parseInclude", + "android.view.View#isInEditMode", + "android.view.ViewRootImpl#isInTouchMode", + "android.view.WindowManagerGlobal#getWindowManagerService", + "android.view.inputmethod.InputMethodManager#getInstance", + "com.android.internal.util.XmlUtils#convertValueToInt", + "com.android.internal.textservice.ITextServicesManager$Stub#asInterface", + }; + + /** + * The list of classes on which to delegate all native methods. + */ + public final static String[] DELEGATE_CLASS_NATIVES = new String[] { + "android.animation.PropertyValuesHolder", + "android.graphics.AvoidXfermode", + "android.graphics.Bitmap", + "android.graphics.BitmapFactory", + "android.graphics.BitmapShader", + "android.graphics.BlurMaskFilter", + "android.graphics.Canvas", + "android.graphics.ColorFilter", + "android.graphics.ColorMatrixColorFilter", + "android.graphics.ComposePathEffect", + "android.graphics.ComposeShader", + "android.graphics.CornerPathEffect", + "android.graphics.DashPathEffect", + "android.graphics.DiscretePathEffect", + "android.graphics.DrawFilter", + "android.graphics.EmbossMaskFilter", + "android.graphics.LayerRasterizer", + "android.graphics.LightingColorFilter", + "android.graphics.LinearGradient", + "android.graphics.MaskFilter", + "android.graphics.Matrix", + "android.graphics.NinePatch", + "android.graphics.Paint", + "android.graphics.PaintFlagsDrawFilter", + "android.graphics.Path", + "android.graphics.PathDashPathEffect", + "android.graphics.PathEffect", + "android.graphics.PixelXorXfermode", + "android.graphics.PorterDuffColorFilter", + "android.graphics.PorterDuffXfermode", + "android.graphics.RadialGradient", + "android.graphics.Rasterizer", + "android.graphics.Region", + "android.graphics.Shader", + "android.graphics.SumPathEffect", + "android.graphics.SweepGradient", + "android.graphics.Typeface", + "android.graphics.Xfermode", + "android.os.SystemClock", + "android.text.AndroidBidi", + "android.util.FloatMath", + "android.view.Display", + "libcore.icu.ICU", + }; + + /** + * The list of methods to stub out. Each entry must be in the form + * "package.package.OuterClass$InnerClass#MethodName". + * This usage is deprecated. Please use method 'delegates' instead. + */ + private final static String[] OVERRIDDEN_METHODS = new String[] { + }; + + /** + * The list of classes to rename, must be an even list: the binary FQCN + * of class to replace followed by the new FQCN. + */ + private final static String[] RENAMED_CLASSES = + new String[] { + "android.os.ServiceManager", "android.os._Original_ServiceManager", + "android.util.LruCache", "android.util._Original_LruCache", + "android.view.SurfaceView", "android.view._Original_SurfaceView", + "android.view.accessibility.AccessibilityManager", "android.view.accessibility._Original_AccessibilityManager", + "android.webkit.WebView", "android.webkit._Original_WebView", + "com.android.internal.policy.PolicyManager", "com.android.internal.policy._Original_PolicyManager", + }; + + /** + * 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. + */ + private final static String[] DELETE_RETURNS = + new String[] { + null }; // separator, for next class/methods list. +} + 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..927be97 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java @@ -0,0 +1,133 @@ +/* + * 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.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 ClassVisitor { + + /** Suffix added to original methods. */ + private static final String ORIGINAL_SUFFIX = "_Original"; + private static String CONSTRUCTOR = "<init>"; + private static String CLASS_INIT = "<clinit>"; + + 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(Opcodes.ASM4, 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) { + // Not creating a delegate for this method, pass it as-is from the reader + // to the writer. + return super.visitMethod(access, name, desc, signature, exceptions); + } + + if (useDelegate) { + if (CONSTRUCTOR.equals(name) || CLASS_INIT.equals(name)) { + // We don't currently support generating delegates for constructors. + throw new UnsupportedOperationException( + String.format( + "Delegate doesn't support overriding constructor %1$s:%2$s(%3$s)", //$NON-NLS-1$ + mClassName, name, desc)); + } + } + + if (isNative) { + // Remove native flag + access = access & ~Opcodes.ACC_NATIVE; + MethodVisitor mwDelegate = super.visitMethod(access, name, desc, signature, exceptions); + + DelegateMethodAdapter2 a = new DelegateMethodAdapter2( + mLog, null /*mwOriginal*/, mwDelegate, mClassName, name, desc, isStatic); + + // A native has no code to visit, so we need to generate it directly. + a.generateDelegateCode(); + + return mwDelegate; + } + + // Given a non-native SomeClass.MethodName(), we want to generate 2 methods: + // - A copy of the original method named SomeClass.MethodName_Original(). + // The content is the original method as-is from the reader. + // - A brand new implementation of SomeClass.MethodName() which calls to a + // non-existing method named SomeClass_Delegate.MethodName(). + // The implementation of this 'delegate' method is done in layoutlib_brigde. + + int accessDelegate = access; + // change access to public for the original one + if (Main.sOptions.generatePublicAccess) { + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + } + + MethodVisitor mwOriginal = super.visitMethod(access, name + ORIGINAL_SUFFIX, + desc, signature, exceptions); + MethodVisitor mwDelegate = super.visitMethod(accessDelegate, name, + desc, signature, exceptions); + + DelegateMethodAdapter2 a = new DelegateMethodAdapter2( + mLog, mwOriginal, mwDelegate, mClassName, name, desc, isStatic); + return a; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter2.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter2.java new file mode 100644 index 0000000..0000b22 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter2.java @@ -0,0 +1,461 @@ +/* + * 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 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; + +import java.util.ArrayList; + +/** + * This method adapter generates delegate methods. + * <p/> + * Given a method {@code SomeClass.MethodName()}, this generates 1 or 2 methods: + * <ul> + * <li> A copy of the original method named {@code SomeClass.MethodName_Original()}. + * The content is the original method as-is from the reader. + * This step is omitted if the method is native, since it has no Java implementation. + * <li> A brand new implementation of {@code SomeClass.MethodName()} which calls to a + * non-existing method named {@code SomeClass_Delegate.MethodName()}. + * The implementation of this 'delegate' method is done in layoutlib_brigde. + * </ul> + * A method visitor is generally constructed to generate a single method; however + * here we might want to generate one or two depending on the context. To achieve + * that, the visitor here generates the 'original' method and acts as a no-op if + * no such method exists (e.g. when the original is a native method). + * The delegate method is generated after the {@code visitEnd} of the original method + * or by having the class adapter <em>directly</em> call {@link #generateDelegateCode()} + * for native methods. + * <p/> + * When generating the 'delegate', the implementation generates a call to a class + * class named <code><className>_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 to these 'delegate' methods so that we can easily find them + * for automated testing. + * <p/> + * This class isn't intended to be generic or reusable. + * It is called by {@link DelegateClassAdapter}, which takes care of properly initializing + * the two method writers for the original and the delegate class, as needed, with their + * expected names. + * <p/> + * The class adapter also takes care of calling {@link #generateDelegateCode()} directly for + * a native and use the visitor pattern for non-natives. + * Note that native methods have, by definition, no code so there's nothing a visitor + * can visit. + * <p/> + * Instances of this class are not re-usable. + * The class adapter creates a new instance for each method. + */ +class DelegateMethodAdapter2 extends MethodVisitor { + + /** Suffix added to delegate classes. */ + public static final String DELEGATE_SUFFIX = "_Delegate"; + + /** The parent method writer to copy of the original method. + * Null when dealing with a native original method. */ + private MethodVisitor mOrgWriter; + /** The parent method writer to generate the delegating method. Never null. */ + private MethodVisitor mDelWriter; + /** 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; + + /** Array used to capture the first line number information from the original method + * and duplicate it in the delegate. */ + private Object[] mDelegateLineNumber; + + /** + * Creates a new {@link DelegateMethodAdapter2} that will transform this method + * into a delegate call. + * <p/> + * See {@link DelegateMethodAdapter2} for more details. + * + * @param log The logger object. Must not be null. + * @param mvOriginal The parent method writer to copy of the original method. + * Must be {@code null} when dealing with a native original method. + * @param mvDelegate The parent method writer to generate the delegating method. + * Must never be null. + * @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 DelegateMethodAdapter2(Log log, + MethodVisitor mvOriginal, + MethodVisitor mvDelegate, + String className, + String methodName, + String desc, + boolean isStatic) { + super(Opcodes.ASM4); + mLog = log; + mOrgWriter = mvOriginal; + mDelWriter = mvDelegate; + mClassName = className; + mMethodName = methodName; + mDesc = desc; + mIsStatic = isStatic; + } + + /** + * 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 DelegateMethodAdapter2} 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 generateDelegateCode() { + /* + * The goal is to generate a call to a static delegate method. + * If this method is non-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 myMethod(int a, Object b, ArrayList<String> c) { ... } + * + * We'll want to create a body that calls a delegate method like this: + * TheClass_Delegate.myMethod(this, a, b, c); + * + * If the method is non-static and the class name is an inner class (e.g. has $ in its + * last segment), we want to push the 'this' of the outer class first: + * OuterClass_InnerClass_Delegate.myMethod( + * OuterClass.this, + * OuterClass$InnerClass.this, + * a, b, c); + * + * Only one level of inner class is supported right now, for simplicity and because + * we don't need more. + * + * 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 build time, they have no influence on the method name being called. + */ + + // Add our annotation + AnnotationVisitor aw = mDelWriter.visitAnnotation( + Type.getObjectType(Type.getInternalName(LayoutlibDelegate.class)).toString(), + true); // visible at runtime + if (aw != null) { + aw.visitEnd(); + } + + mDelWriter.visitCode(); + + if (mDelegateLineNumber != null) { + Object[] p = mDelegateLineNumber; + mDelWriter.visitLineNumber((Integer) p[0], (Label) p[1]); + } + + ArrayList<Type> paramTypes = new ArrayList<Type>(); + String delegateClassName = mClassName + DELEGATE_SUFFIX; + boolean pushedArg0 = false; + int maxStack = 0; + + // Check if the last segment of the class name has inner an class. + // Right now we only support one level of inner classes. + Type outerType = null; + int slash = mClassName.lastIndexOf('/'); + int dol = mClassName.lastIndexOf('$'); + if (dol != -1 && dol > slash && dol == mClassName.indexOf('$')) { + String outerClass = mClassName.substring(0, dol); + outerType = Type.getObjectType(outerClass); + + // Change a delegate class name to "com/foo/Outer_Inner_Delegate" + delegateClassName = delegateClassName.replace('$', '_'); + } + + // For an instance method (e.g. non-static), push the 'this' preceded + // by the 'this' of any outer class, if any. + if (!mIsStatic) { + + if (outerType != null) { + // The first-level inner class has a package-protected member called 'this$0' + // that points to the outer class. + + // Push this.getField("this$0") on the call stack. + mDelWriter.visitVarInsn(Opcodes.ALOAD, 0); // var 0 = this + mDelWriter.visitFieldInsn(Opcodes.GETFIELD, + mClassName, // class where the field is defined + "this$0", // field name + outerType.getDescriptor()); // type of the field + maxStack++; + paramTypes.add(outerType); + + } + + // Push "this" for the instance method, which is always ALOAD 0 + mDelWriter.visitVarInsn(Opcodes.ALOAD, 0); + maxStack++; + pushedArg0 = true; + paramTypes.add(Type.getObjectType(mClassName)); + } + + // Push all other arguments. Start at arg 1 if we already pushed 'this' above. + Type[] argTypes = Type.getArgumentTypes(mDesc); + int maxLocals = pushedArg0 ? 1 : 0; + for (Type t : argTypes) { + int size = t.getSize(); + mDelWriter.visitVarInsn(t.getOpcode(Opcodes.ILOAD), maxLocals); + maxLocals += size; + maxStack += size; + paramTypes.add(t); + } + + // Construct the descriptor of the delegate based on the parameters + // we pushed on the call stack. The return type remains unchanged. + String desc = Type.getMethodDescriptor( + Type.getReturnType(mDesc), + paramTypes.toArray(new Type[paramTypes.size()])); + + // Invoke the static delegate + mDelWriter.visitMethodInsn(Opcodes.INVOKESTATIC, + delegateClassName, + mMethodName, + desc); + + Type returnType = Type.getReturnType(mDesc); + mDelWriter.visitInsn(returnType.getOpcode(Opcodes.IRETURN)); + + mDelWriter.visitMaxs(maxStack, maxLocals); + mDelWriter.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. */ + @Override + public void visitCode() { + if (mOrgWriter != null) { + mOrgWriter.visitCode(); + } + } + + /* + * visitMaxs is called just before visitEnd if there was any code to rewrite. + */ + @Override + public void visitMaxs(int maxStack, int maxLocals) { + if (mOrgWriter != null) { + mOrgWriter.visitMaxs(maxStack, maxLocals); + } + } + + /** End of visiting. Generate the delegating code. */ + @Override + public void visitEnd() { + if (mOrgWriter != null) { + mOrgWriter.visitEnd(); + } + generateDelegateCode(); + } + + /* Writes all annotation from the original method. */ + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (mOrgWriter != null) { + return mOrgWriter.visitAnnotation(desc, visible); + } else { + return null; + } + } + + /* Writes all annotation default values from the original method. */ + @Override + public AnnotationVisitor visitAnnotationDefault() { + if (mOrgWriter != null) { + return mOrgWriter.visitAnnotationDefault(); + } else { + return null; + } + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + if (mOrgWriter != null) { + return mOrgWriter.visitParameterAnnotation(parameter, desc, visible); + } else { + return null; + } + } + + /* Writes all attributes from the original method. */ + @Override + public void visitAttribute(Attribute attr) { + if (mOrgWriter != null) { + mOrgWriter.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. + */ + @Override + public void visitLineNumber(int line, Label start) { + // Capture the first line values for the new delegate method + if (mDelegateLineNumber == null) { + mDelegateLineNumber = new Object[] { line, start }; + } + if (mOrgWriter != null) { + mOrgWriter.visitLineNumber(line, start); + } + } + + @Override + public void visitInsn(int opcode) { + if (mOrgWriter != null) { + mOrgWriter.visitInsn(opcode); + } + } + + @Override + public void visitLabel(Label label) { + if (mOrgWriter != null) { + mOrgWriter.visitLabel(label); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (mOrgWriter != null) { + mOrgWriter.visitTryCatchBlock(start, end, handler, type); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (mOrgWriter != null) { + mOrgWriter.visitMethodInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (mOrgWriter != null) { + mOrgWriter.visitFieldInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (mOrgWriter != null) { + mOrgWriter.visitFrame(type, nLocal, local, nStack, stack); + } + } + + @Override + public void visitIincInsn(int var, int increment) { + if (mOrgWriter != null) { + mOrgWriter.visitIincInsn(var, increment); + } + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (mOrgWriter != null) { + mOrgWriter.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (mOrgWriter != null) { + mOrgWriter.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitLdcInsn(Object cst) { + if (mOrgWriter != null) { + mOrgWriter.visitLdcInsn(cst); + } + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + if (mOrgWriter != null) { + mOrgWriter.visitLocalVariable(name, desc, signature, start, end, index); + } + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (mOrgWriter != null) { + mOrgWriter.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + if (mOrgWriter != null) { + mOrgWriter.visitMultiANewArrayInsn(desc, dims); + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + if (mOrgWriter != null) { + mOrgWriter.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (mOrgWriter != null) { + mOrgWriter.visitTypeInsn(opcode, type); + } + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (mOrgWriter != null) { + mOrgWriter.visitVarInsn(opcode, var); + } + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java new file mode 100644 index 0000000..c988c70 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java @@ -0,0 +1,787 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.annotations.VisibleForTesting; +import com.android.tools.layoutlib.annotations.VisibleForTesting.Visibility; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Analyzes the input JAR using the ASM java bytecode manipulation library + * to list the classes and their dependencies. A "dependency" is a class + * used by another class. + */ +public class DependencyFinder { + + // Note: a bunch of stuff has package-level access for unit tests. Consider it private. + + /** Output logger. */ + private final Log mLog; + + /** + * Creates a new analyzer. + * + * @param log The log output. + */ + public DependencyFinder(Log log) { + mLog = log; + } + + /** + * Starts the analysis using parameters from the constructor. + * + * @param osJarPath The input source JARs to parse. + * @return A pair: [0]: map { class FQCN => set of FQCN class dependencies }. + * [1]: map { missing class FQCN => set of FQCN class that uses it. } + */ + public List<Map<String, Set<String>>> findDeps(List<String> osJarPath) throws IOException { + + Map<String, ClassReader> zipClasses = parseZip(osJarPath); + mLog.info("Found %d classes in input JAR%s.", + zipClasses.size(), + osJarPath.size() > 1 ? "s" : ""); + + Map<String, Set<String>> deps = findClassesDeps(zipClasses); + + Map<String, Set<String>> missing = findMissingClasses(deps, zipClasses.keySet()); + + List<Map<String, Set<String>>> result = new ArrayList<Map<String,Set<String>>>(2); + result.add(deps); + result.add(missing); + return result; + } + + /** + * Prints dependencies to the current logger, found stuff and missing stuff. + */ + public void printAllDeps(List<Map<String, Set<String>>> result) { + assert result.size() == 2; + Map<String, Set<String>> deps = result.get(0); + Map<String, Set<String>> missing = result.get(1); + + // Print all dependences found in the format: + // +Found: <FQCN from zip> + // uses: FQCN + + mLog.info("++++++ %d Entries found in source JARs", deps.size()); + mLog.info(""); + + for (Entry<String, Set<String>> entry : deps.entrySet()) { + mLog.info( "+Found : %s", entry.getKey()); + for (String dep : entry.getValue()) { + mLog.info(" uses: %s", dep); + } + + mLog.info(""); + } + + + // Now print all missing dependences in the format: + // -Missing <FQCN>: + // used by: <FQCN> + + mLog.info(""); + mLog.info("------ %d Entries missing from source JARs", missing.size()); + mLog.info(""); + + for (Entry<String, Set<String>> entry : missing.entrySet()) { + mLog.info( "-Missing : %s", entry.getKey()); + for (String dep : entry.getValue()) { + mLog.info(" used by: %s", dep); + } + + mLog.info(""); + } + } + + /** + * Prints only a summary of the missing dependencies to the current logger. + */ + public void printMissingDeps(List<Map<String, Set<String>>> result) { + assert result.size() == 2; + @SuppressWarnings("unused") Map<String, Set<String>> deps = result.get(0); + Map<String, Set<String>> missing = result.get(1); + + for (String fqcn : missing.keySet()) { + mLog.info("%s", fqcn); + } + } + + // ---------------- + + /** + * Parses a JAR file and returns a list of all classes founds using a map + * class name => ASM ClassReader. Class names are in the form "android.view.View". + */ + Map<String,ClassReader> parseZip(List<String> jarPathList) throws IOException { + TreeMap<String, ClassReader> classes = new TreeMap<String, ClassReader>(); + + for (String jarPath : jarPathList) { + ZipFile zip = new ZipFile(jarPath); + Enumeration<? extends ZipEntry> entries = zip.entries(); + ZipEntry entry; + while (entries.hasMoreElements()) { + entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + ClassReader cr = new ClassReader(zip.getInputStream(entry)); + String className = classReaderToClassName(cr); + classes.put(className, cr); + } + } + } + + return classes; + } + + /** + * Utility that returns the fully qualified binary class name for a ClassReader. + * E.g. it returns something like android.view.View. + */ + static String classReaderToClassName(ClassReader classReader) { + if (classReader == null) { + return null; + } else { + return classReader.getClassName().replace('/', '.'); + } + } + + /** + * Utility that returns the fully qualified binary class name from a path-like FQCN. + * E.g. it returns android.view.View from android/view/View. + */ + static String internalToBinaryClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('/', '.'); + } + } + + /** + * Finds all dependencies for all classes in keepClasses which are also + * listed in zipClasses. Returns a map of all the dependencies found. + */ + Map<String, Set<String>> findClassesDeps(Map<String, ClassReader> zipClasses) { + + // The dependencies that we'll collect. + // It's a map Class name => uses class names. + Map<String, Set<String>> dependencyMap = new TreeMap<String, Set<String>>(); + + DependencyVisitor visitor = getVisitor(); + + int count = 0; + try { + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String name = entry.getKey(); + + TreeSet<String> set = new TreeSet<String>(); + dependencyMap.put(name, set); + visitor.setDependencySet(set); + + ClassReader cr = entry.getValue(); + cr.accept(visitor, 0 /* flags */); + + visitor.setDependencySet(null); + + mLog.debugNoln("Visited %d classes\r", ++count); + } + } finally { + mLog.debugNoln("\n"); + } + + return dependencyMap; + } + + /** + * Computes which classes FQCN were found as dependencies that are NOT listed + * in the original JAR classes. + * + * @param deps The map { FQCN => dependencies[] } returned by {@link #findClassesDeps(Map)}. + * @param zipClasses The set of all classes FQCN found in the JAR files. + * @return A map { FQCN not found in the zipClasses => classes using it } + */ + private Map<String, Set<String>> findMissingClasses( + Map<String, Set<String>> deps, + Set<String> zipClasses) { + Map<String, Set<String>> missing = new TreeMap<String, Set<String>>(); + + for (Entry<String, Set<String>> entry : deps.entrySet()) { + String name = entry.getKey(); + + for (String dep : entry.getValue()) { + if (!zipClasses.contains(dep)) { + // This dependency doesn't exist in the zip classes. + Set<String> set = missing.get(dep); + if (set == null) { + set = new TreeSet<String>(); + missing.put(dep, set); + } + set.add(name); + } + } + + } + + return missing; + } + + + // ---------------------------------- + + /** + * Instantiates a new DependencyVisitor. Useful for unit tests. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + DependencyVisitor getVisitor() { + return new DependencyVisitor(); + } + + /** + * Visitor to collect all the type dependencies from a class. + */ + public class DependencyVisitor extends ClassVisitor { + + private Set<String> mCurrentDepSet; + + /** + * Creates a new visitor that will find all the dependencies for the visited class. + */ + public DependencyVisitor() { + super(Opcodes.ASM4); + } + + /** + * Sets the {@link Set} where to record direct dependencies for this class. + * This will change before each {@link ClassReader#accept(ClassVisitor, int)} call. + */ + public void setDependencySet(Set<String> set) { + mCurrentDepSet = set; + } + + /** + * Considers the given class name as a dependency. + */ + public void considerName(String className) { + if (className == null) { + return; + } + + className = internalToBinaryClassName(className); + + try { + // exclude classes that are part of the default JRE (the one executing this program) + if (getClass().getClassLoader().loadClass(className) != null) { + return; + } + } catch (ClassNotFoundException e) { + // ignore + } + + // Add it to the dependency set for the currently visited class, as needed. + assert mCurrentDepSet != null; + if (mCurrentDepSet != null) { + mCurrentDepSet.add(className); + } + } + + /** + * Considers this array of names using considerName(). + */ + public void considerNames(String[] classNames) { + if (classNames != null) { + for (String className : classNames) { + considerName(className); + } + } + } + + /** + * Considers this signature or type signature by invoking the {@link SignatureVisitor} + * on it. + */ + public void considerSignature(String signature) { + if (signature != null) { + SignatureReader sr = new SignatureReader(signature); + // SignatureReader.accept will call accessType so we don't really have + // to differentiate where the signature comes from. + sr.accept(new MySignatureVisitor()); + } + } + + /** + * Considers this {@link Type}. For arrays, the element type is considered. + * If the type is an object, it's internal name is considered. + */ + public void considerType(Type t) { + if (t != null) { + if (t.getSort() == Type.ARRAY) { + t = t.getElementType(); + } + if (t.getSort() == Type.OBJECT) { + considerName(t.getInternalName()); + } + } + } + + /** + * Considers a descriptor string. The descriptor is converted to a {@link Type} + * and then considerType() is invoked. + */ + public boolean considerDesc(String desc) { + if (desc != null) { + try { + if (desc.length() > 0 && desc.charAt(0) == '(') { + // This is a method descriptor with arguments and a return type. + Type t = Type.getReturnType(desc); + considerType(t); + + for (Type arg : Type.getArgumentTypes(desc)) { + considerType(arg); + } + + } else { + Type t = Type.getType(desc); + considerType(t); + } + return true; + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + return false; + } + + + // --------------------------------------------------- + // --- ClassVisitor, FieldVisitor + // --------------------------------------------------- + + // Visits a class header + @Override + public void visit(int version, int access, String name, + String signature, String superName, String[] interfaces) { + // signature is the signature of this class. May be null if the class is not a generic + // one, and does not extend or implement generic classes or interfaces. + + if (signature != null) { + considerSignature(signature); + } + + // superName is the internal of name of the super class (see getInternalName). + // For interfaces, the super class is Object. May be null but only for the Object class. + considerName(superName); + + // interfaces is the internal names of the class's interfaces (see getInternalName). + // May be null. + considerNames(interfaces); + } + + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + + private class MyFieldVisitor extends FieldVisitor { + + public MyFieldVisitor() { + super(Opcodes.ASM4); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // desc is the field's descriptor (see Type). + considerDesc(desc); + + // signature is the field's signature. May be null if the field's type does not use + // generic types. + considerSignature(signature); + + return new MyFieldVisitor(); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // name is the internal name of an inner class (see getInternalName). + // Note: outerName/innerName seems to be null when we're reading the + // _Original_ClassName classes generated by layoutlib_create. + if (outerName != null) { + considerName(name); + } + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + // desc is the method's descriptor (see Type). + considerDesc(desc); + // signature is the method's signature. May be null if the method parameters, return + // type and exceptions do not use generic types. + considerSignature(signature); + + return new MyMethodVisitor(); + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + @Override + public void visitSource(String source, String debug) { + // pass + } + + + // --------------------------------------------------- + // --- MethodVisitor + // --------------------------------------------------- + + private class MyMethodVisitor extends MethodVisitor { + + public MyMethodVisitor() { + super(Opcodes.ASM4); + } + + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return new MyAnnotationVisitor(); + } + + @Override + public void visitCode() { + // pass + } + + // field instruction + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + // name is the field's name. + // desc is the field's descriptor (see Type). + considerDesc(desc); + } + + @Override + public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) { + // pass + } + + @Override + public void visitIincInsn(int var, int increment) { + // pass -- an IINC instruction + } + + @Override + public void visitInsn(int opcode) { + // pass -- a zero operand instruction + } + + @Override + public void visitIntInsn(int opcode, int operand) { + // pass -- a single int operand instruction + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + // pass -- a jump instruction + } + + @Override + public void visitLabel(Label label) { + // pass -- a label target + } + + // instruction to load a constant from the stack + @Override + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + considerType((Type) cst); + } + } + + @Override + public void visitLineNumber(int line, Label start) { + // pass + } + + @Override + public void visitLocalVariable(String name, String desc, + String signature, Label start, Label end, int index) { + // desc is the type descriptor of this local variable. + considerDesc(desc); + // signature is the type signature of this local variable. May be null if the local + // variable type does not use generic types. + considerSignature(signature); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // pass -- a lookup switch instruction + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + // pass + } + + // instruction that invokes a method + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + + // owner is the internal name of the method's owner class + if (!considerDesc(owner) && owner.indexOf('/') != -1) { + considerName(owner); + } + // desc is the method's descriptor (see Type). + considerDesc(desc); + } + + // instruction multianewarray, whatever that is + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + + // desc an array type descriptor. + considerDesc(desc); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // pass -- table switch instruction + + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // type is the internal name of the type of exceptions handled by the handler, + // or null to catch any exceptions (for "finally" blocks). + considerName(type); + } + + // type instruction + @Override + public void visitTypeInsn(int opcode, String type) { + // type is the operand of the instruction to be visited. This operand must be the + // internal name of an object or array class. + considerName(type); + } + + @Override + public void visitVarInsn(int opcode, int var) { + // pass -- local variable instruction + } + } + + private class MySignatureVisitor extends SignatureVisitor { + + public MySignatureVisitor() { + super(Opcodes.ASM4); + } + + // --------------------------------------------------- + // --- SignatureVisitor + // --------------------------------------------------- + + private String mCurrentSignatureClass = null; + + // Starts the visit of a signature corresponding to a class or interface type + @Override + public void visitClassType(String name) { + mCurrentSignatureClass = name; + considerName(name); + } + + // Visits an inner class + @Override + public void visitInnerClassType(String name) { + if (mCurrentSignatureClass != null) { + mCurrentSignatureClass += "$" + name; + considerName(mCurrentSignatureClass); + } + } + + @Override + public SignatureVisitor visitArrayType() { + return new MySignatureVisitor(); + } + + @Override + public void visitBaseType(char descriptor) { + // pass -- a primitive type, ignored + } + + @Override + public SignatureVisitor visitClassBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitExceptionType() { + return new MySignatureVisitor(); + } + + @Override + public void visitFormalTypeParameter(String name) { + // pass + } + + @Override + public SignatureVisitor visitInterface() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitParameterType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitReturnType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitSuperclass() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + return new MySignatureVisitor(); + } + + @Override + public void visitTypeVariable(String name) { + // pass + } + + @Override + public void visitTypeArgument() { + // pass + } + } + + + // --------------------------------------------------- + // --- AnnotationVisitor + // --------------------------------------------------- + + private class MyAnnotationVisitor extends AnnotationVisitor { + + public MyAnnotationVisitor() { + super(Opcodes.ASM4); + } + + // Visits a primitive value of an annotation + @Override + public void visit(String name, Object value) { + // value is the actual value, whose type must be Byte, Boolean, Character, Short, + // Integer, Long, Float, Double, String or Type + if (value instanceof Type) { + considerType((Type) value); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + // desc is the class descriptor of the nested annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public AnnotationVisitor visitArray(String name) { + return new MyAnnotationVisitor(); + } + + @Override + public void visitEnum(String name, String desc, String value) { + // desc is the class descriptor of the enumeration class. + considerDesc(desc); + } + } + } +} 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/Log.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java new file mode 100644 index 0000000..c3ba591 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java @@ -0,0 +1,72 @@ +/* + * 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 java.io.PrintWriter; +import java.io.StringWriter; + +public class Log { + + private boolean mVerbose = false; + + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + public void debug(String format, Object... args) { + if (mVerbose) { + info(format, args); + } + } + + /** Similar to debug() but doesn't do a \n automatically. */ + public void debugNoln(String format, Object... args) { + if (mVerbose) { + String s = String.format(format, args); + System.out.print(s); + } + } + + public void info(String format, Object... args) { + String s = String.format(format, args); + outPrintln(s); + } + + public void error(String format, Object... args) { + String s = String.format(format, args); + errPrintln(s); + } + + public void exception(Throwable t, String format, Object... args) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + error(format + "\n" + sw.toString(), args); + } + + /** for unit testing */ + protected void errPrintln(String msg) { + System.err.println(msg); + } + + /** for unit testing */ + protected void outPrintln(String msg) { + System.out.println(msg); + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java new file mode 100644 index 0000000..dc4b4a7 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java @@ -0,0 +1,32 @@ +/* + * 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; + +public class LogAbortException extends Exception { + + private final String mFormat; + private final Object[] mArgs; + + public LogAbortException(String format, Object... args) { + mFormat = format; + mArgs = args; + } + + public void error(Log log) { + log.error(mFormat, mArgs); + } +} 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 new file mode 100644 index 0000000..9cd74db --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -0,0 +1,212 @@ +/* + * 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 java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +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 <bunch of framework jars> + * $ java -jar out/host/linux-x86/framework/layoutlib_create.jar \ + * 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 class Options { + public boolean generatePublicAccess = true; + public boolean listAllDeps = false; + public boolean listOnlyMissingDeps = false; + } + + public static final Options sOptions = new Options(); + + public static void main(String[] args) { + + Log log = new Log(); + + ArrayList<String> osJarPath = new ArrayList<String>(); + String[] osDestJar = { null }; + + if (!processArgs(log, args, osJarPath, osDestJar)) { + log.error("Usage: layoutlib_create [-v] [-p] output.jar input.jar ..."); + log.error("Usage: layoutlib_create [-v] [--list-deps|--missing-deps] input.jar ..."); + System.exit(1); + } + + if (sOptions.listAllDeps || sOptions.listOnlyMissingDeps) { + System.exit(listDeps(osJarPath, log)); + + } else { + System.exit(createLayoutLib(osDestJar[0], osJarPath, log)); + } + + + System.exit(1); + } + + private static int createLayoutLib(String osDestJar, ArrayList<String> osJarPath, Log log) { + log.info("Output: %1$s", osDestJar); + for (String path : osJarPath) { + log.info("Input : %1$s", path); + } + + try { + AsmGenerator agen = new AsmGenerator(log, osDestJar, new CreateInfo()); + + AsmAnalyzer aa = new AsmAnalyzer(log, osJarPath, agen, + new String[] { // derived from + "android.view.View", + "android.app.Fragment" + }, + new String[] { // include classes + "android.*", // for android.R + "android.util.*", + "com.android.internal.util.*", + "android.view.*", + "android.widget.*", + "com.android.internal.widget.*", + "android.text.**", + "android.graphics.*", + "android.graphics.drawable.*", + "android.content.*", + "android.content.res.*", + "org.apache.harmony.xml.*", + "com.android.internal.R**", + "android.pim.*", // for datepicker + "android.os.*", // for android.os.Handler + "android.database.ContentObserver", // for Digital clock + }); + aa.analyze(); + agen.generate(); + + // Throw an error if any class failed to get renamed by the generator + // + // IMPORTANT: if you're building the platform and you get this error message, + // it means the renameClasses[] array in AsmGenerator needs to be updated: some + // class should have been renamed but it was not found in the input JAR files. + Set<String> notRenamed = agen.getClassesNotRenamed(); + if (notRenamed.size() > 0) { + // (80-column guide below for error formatting) + // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + log.error( + "ERROR when running layoutlib_create: the following classes are referenced\n" + + "by tools/layoutlib/create but were not actually found in the input JAR files.\n" + + "This may be due to some platform classes having been renamed."); + for (String fqcn : notRenamed) { + log.error("- Class not found: %s", fqcn.replace('/', '.')); + } + for (String path : osJarPath) { + log.info("- Input JAR : %1$s", path); + } + return 1; + } + + return 0; + } catch (IOException e) { + log.exception(e, "Failed to load jar"); + } catch (LogAbortException e) { + e.error(log); + } + + return 1; + } + + private static int listDeps(ArrayList<String> osJarPath, Log log) { + DependencyFinder df = new DependencyFinder(log); + try { + List<Map<String, Set<String>>> result = df.findDeps(osJarPath); + if (sOptions.listAllDeps) { + df.printAllDeps(result); + } else if (sOptions.listOnlyMissingDeps) { + df.printMissingDeps(result); + } + } catch (IOException e) { + log.exception(e, "Failed to load jar"); + } + + return 0; + } + + /** + * Returns true if args where properly parsed. + * Returns false if program should exit with command-line usage. + * <p/> + * Note: the String[0] is an output parameter wrapped in an array, since there is no + * "out" parameter support. + */ + private static boolean processArgs(Log log, String[] args, + ArrayList<String> osJarPath, String[] osDestJar) { + boolean needs_dest = true; + for (int i = 0; i < args.length; i++) { + String s = args[i]; + if (s.equals("-v")) { + log.setVerbose(true); + } else if (s.equals("-p")) { + sOptions.generatePublicAccess = false; + } else if (s.equals("--list-deps")) { + sOptions.listAllDeps = true; + needs_dest = false; + } else if (s.equals("--missing-deps")) { + sOptions.listOnlyMissingDeps = true; + needs_dest = false; + } else if (!s.startsWith("-")) { + if (needs_dest && osDestJar[0] == null) { + osDestJar[0] = s; + } else { + osJarPath.add(s); + } + } else { + log.error("Unknow argument: %s", s); + return false; + } + } + + if (osJarPath.isEmpty()) { + log.error("Missing parameter: path to input jar"); + return false; + } + if (needs_dest && osDestJar[0] == null) { + log.error("Missing parameter: path to output jar"); + return false; + } + + sOptions.generatePublicAccess = false; + + return true; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java new file mode 100644 index 0000000..7d1e4cf --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java @@ -0,0 +1,97 @@ +/* + * 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; + + +/** + * An adapter to make it easier to use {@link MethodListener}. + * <p/> + * The adapter calls the void {@link #onInvokeV(String, boolean, Object)} listener + * for all types (I, L, F, D and A), returning 0 or null as appropriate. + */ +public class MethodAdapter implements MethodListener { + /** + * A stub method is being invoked. + * <p/> + * Known limitation: caller arguments are not available. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + @Override + public void onInvokeV(String signature, boolean isNative, Object caller) { + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. + * @see #onInvokeV(String, boolean, Object) + * @return an integer, or a boolean, or a short or a byte. + */ + @Override + public int onInvokeI(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. + * @see #onInvokeV(String, boolean, Object) + * @return a long. + */ + @Override + public long onInvokeL(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. + * @see #onInvokeV(String, boolean, Object) + * @return a float. + */ + @Override + public float onInvokeF(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. + * @see #onInvokeV(String, boolean, Object) + * @return a double. + */ + @Override + public double onInvokeD(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. + * @see #onInvokeV(String, boolean, Object) + * @return an object. + */ + @Override + public Object onInvokeA(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return null; + } +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java new file mode 100644 index 0000000..6fc2b24 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java @@ -0,0 +1,76 @@ +/* + * 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; + + +/** + * Interface to allow a method invocation to be listened upon. + * <p/> + * This is used by {@link OverrideMethod} to register a listener for methods that + * have been stubbed by the {@link AsmGenerator}. At runtime the stub will call either a + * default global listener or a specific listener based on the method signature. + */ +public interface MethodListener { + /** + * A stub method is being invoked. + * <p/> + * Known limitation: caller arguments are not available. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public void onInvokeV(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. + * @see #onInvokeV(String, boolean, Object) + * @return an integer, or a boolean, or a short or a byte. + */ + public int onInvokeI(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. + * @see #onInvokeV(String, boolean, Object) + * @return a long. + */ + public long onInvokeL(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. + * @see #onInvokeV(String, boolean, Object) + * @return a float. + */ + public float onInvokeF(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. + * @see #onInvokeV(String, boolean, Object) + * @return a double. + */ + public double onInvokeD(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. + * @see #onInvokeV(String, boolean, Object) + * @return an object. + */ + public Object onInvokeA(String signature, boolean isNative, Object caller); +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java new file mode 100644 index 0000000..a6aff99 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java @@ -0,0 +1,151 @@ +/* + * 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 java.util.HashMap; + +/** + * Allows stub methods from LayoutLib to be overriden at runtime. + * <p/> + * Implementation note: all types required by this class(inner/outer classes & interfaces) + * must be referenced by the injectClass argument to {@link AsmGenerator} in Main.java; + * Otherwise they won't be accessible in layoutlib.jar at runtime. + */ +public final class OverrideMethod { + + /** Map of method overridden. */ + private static HashMap<String, MethodListener> sMethods = new HashMap<String, MethodListener>(); + /** Default listener for all method not listed in sMethods. Nothing if null. */ + private static MethodListener sDefaultListener = null; + + /** + * Sets the default listener for all methods not specifically handled. + * Null means to do nothing. + */ + public static void setDefaultListener(MethodListener listener) { + sDefaultListener = listener; + } + + /** + * Defines or reset a listener for the given method signature. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V" + * @param listener The new listener. Removes it if null. + */ + public static void setMethodListener(String signature, MethodListener listener) { + if (listener == null) { + sMethods.remove(signature); + } else { + sMethods.put(signature, listener); + } + } + + /** + * Invokes the specific listener for the given signature or the default one if defined. + * <p/> + * This version invokes the method listener for the void return type. + * <p/> + * Note: this is not intended to be used by the LayoutLib Bridge. It is intended to be called + * by the stubbed methods generated by the LayoutLib_create tool. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public static void invokeV(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + i.onInvokeV(signature, isNative, caller); + } else if (sDefaultListener != null) { + sDefaultListener.onInvokeV(signature, isNative, caller); + } + } + + /** + * Invokes the specific listener for the int return type. + * @see #invokeV(String, boolean, Object) + */ + public static int invokeI(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeI(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeI(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the long return type. + * @see #invokeV(String, boolean, Object) + */ + public static long invokeL(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeL(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeL(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the float return type. + * @see #invokeV(String, boolean, Object) + */ + public static float invokeF(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeF(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeF(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the double return type. + * @see #invokeV(String, boolean, Object) + */ + public static double invokeD(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeD(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeD(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the object return type. + * @see #invokeV(String, boolean, Object) + */ + public static Object invokeA(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeA(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeA(signature, isNative, caller); + } + return null; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java new file mode 100644 index 0000000..383cbb8 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java @@ -0,0 +1,463 @@ +/* + * 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 org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.objectweb.asm.signature.SignatureWriter; + +/** + * This class visitor renames a class from a given old name to a given new name. + * The class visitor will also rename all inner classes and references in the methods. + * <p/> + * + * For inner classes, this handles only the case where the outer class name changes. + * The inner class name should remain the same. + */ +public class RenameClassAdapter extends ClassVisitor { + + + private final String mOldName; + private final String mNewName; + private String mOldBase; + private String mNewBase; + + /** + * Creates a class visitor that renames a class from a given old name to a given new name. + * The class visitor will also rename all inner classes and references in the methods. + * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). + */ + public RenameClassAdapter(ClassWriter cv, String oldName, String newName) { + super(Opcodes.ASM4, cv); + mOldBase = mOldName = oldName; + mNewBase = mNewName = newName; + + int pos = mOldName.indexOf('$'); + if (pos > 0) { + mOldBase = mOldName.substring(0, pos); + } + pos = mNewName.indexOf('$'); + if (pos > 0) { + mNewBase = mNewName.substring(0, pos); + } + + assert (mOldBase == null && mNewBase == null) || (mOldBase != null && mNewBase != null); + } + + + /** + * Renames a type descriptor, e.g. "Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the input string as-is. + */ + String renameTypeDesc(String desc) { + if (desc == null) { + return null; + } + + return renameType(Type.getType(desc)); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the internal name of the input type. + */ + String renameType(Type type) { + if (type == null) { + return null; + } + + if (type.getSort() == Type.OBJECT) { + String in = type.getInternalName(); + return "L" + renameInternalType(in) + ";"; + } else if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(); + for (int n = type.getDimensions(); n > 0; n--) { + sb.append('['); + } + sb.append(renameType(type.getElementType())); + return sb.toString(); + } + return type.getDescriptor(); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;". + * This is like renameType() except that it returns a Type object. + * If the type doesn't need to be renamed, returns the input type object. + */ + Type renameTypeAsType(Type type) { + if (type == null) { + return null; + } + + if (type.getSort() == Type.OBJECT) { + String in = type.getInternalName(); + String newIn = renameInternalType(in); + if (newIn != in) { + return Type.getType("L" + newIn + ";"); + } + } else if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(); + for (int n = type.getDimensions(); n > 0; n--) { + sb.append('['); + } + sb.append(renameType(type.getElementType())); + return Type.getType(sb.toString()); + } + return type; + } + + /** + * Renames an internal type name, e.g. "com.package.MyClass". + * If the type doesn't need to be renamed, returns the input string as-is. + * <p/> + * The internal type of some of the MethodVisitor turns out to be a type + descriptor sometimes so descriptors are renamed too. + */ + String renameInternalType(String type) { + if (type == null) { + return null; + } + + if (type.equals(mOldName)) { + return mNewName; + } + + if (mOldBase != mOldName && type.equals(mOldBase)) { + return mNewBase; + } + + int pos = type.indexOf('$'); + if (pos == mOldBase.length() && type.startsWith(mOldBase)) { + return mNewBase + type.substring(pos); + } + + // The internal type of some of the MethodVisitor turns out to be a type + // descriptor sometimes. This is the case with visitTypeInsn(type) and + // visitMethodInsn(owner). We try to detect it and adjust it here. + if (type.indexOf(';') > 0) { + type = renameTypeDesc(type); + } + + return type; + } + + /** + * Renames a method descriptor, i.e. applies renameType to all arguments and to the + * return value. + */ + String renameMethodDesc(String desc) { + if (desc == null) { + return null; + } + + Type[] args = Type.getArgumentTypes(desc); + + StringBuilder sb = new StringBuilder("("); + for (Type arg : args) { + String name = renameType(arg); + sb.append(name); + } + sb.append(')'); + + Type ret = Type.getReturnType(desc); + String name = renameType(ret); + sb.append(name); + + return sb.toString(); + } + + + /** + * Renames the ClassSignature handled by ClassVisitor.visit + * or the MethodTypeSignature handled by ClassVisitor.visitMethod. + */ + String renameTypeSignature(String sig) { + if (sig == null) { + return null; + } + SignatureReader reader = new SignatureReader(sig); + SignatureWriter writer = new SignatureWriter(); + reader.accept(new RenameSignatureAdapter(writer)); + sig = writer.toString(); + return sig; + } + + + /** + * Renames the FieldTypeSignature handled by ClassVisitor.visitField + * or MethodVisitor.visitLocalVariable. + */ + String renameFieldSignature(String sig) { + if (sig == null) { + return null; + } + SignatureReader reader = new SignatureReader(sig); + SignatureWriter writer = new SignatureWriter(); + reader.acceptType(new RenameSignatureAdapter(writer)); + sig = writer.toString(); + return sig; + } + + + //---------------------------------- + // Methods from the ClassAdapter + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + name = renameInternalType(name); + superName = renameInternalType(superName); + signature = renameTypeSignature(signature); + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + assert outerName.equals(mOldName); + outerName = renameInternalType(outerName); + name = outerName + "$" + innerName; + super.visitInnerClass(name, outerName, innerName, access); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + desc = renameMethodDesc(desc); + signature = renameTypeSignature(signature); + MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions); + return new RenameMethodAdapter(mw); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + desc = renameTypeDesc(desc); + return super.visitAnnotation(desc, visible); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + desc = renameTypeDesc(desc); + signature = renameFieldSignature(signature); + return super.visitField(access, name, desc, signature, value); + } + + + //---------------------------------- + + /** + * A method visitor that renames all references from an old class name to a new class name. + */ + public class RenameMethodAdapter extends MethodVisitor { + + /** + * Creates a method visitor that renames all references from a given old name to a given new + * name. The method visitor will also rename all inner classes. + * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). + */ + public RenameMethodAdapter(MethodVisitor mv) { + super(Opcodes.ASM4, mv); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + desc = renameTypeDesc(desc); + + return super.visitAnnotation(desc, visible); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + desc = renameTypeDesc(desc); + + return super.visitParameterAnnotation(parameter, desc, visible); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + type = renameInternalType(type); + + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + owner = renameInternalType(owner); + desc = renameTypeDesc(desc); + + super.visitFieldInsn(opcode, owner, name, desc); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + owner = renameInternalType(owner); + desc = renameMethodDesc(desc); + + super.visitMethodInsn(opcode, owner, name, desc); + } + + @Override + public void visitLdcInsn(Object cst) { + // If cst is a Type, this means the code is trying to pull the .class constant + // for this class, so it needs to be renamed too. + if (cst instanceof Type) { + cst = renameTypeAsType((Type) cst); + } + super.visitLdcInsn(cst); + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + desc = renameTypeDesc(desc); + + super.visitMultiANewArrayInsn(desc, dims); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + type = renameInternalType(type); + + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + desc = renameTypeDesc(desc); + signature = renameFieldSignature(signature); + + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + } + + //---------------------------------- + + public class RenameSignatureAdapter extends SignatureVisitor { + + private final SignatureVisitor mSv; + + public RenameSignatureAdapter(SignatureVisitor sv) { + super(Opcodes.ASM4); + mSv = sv; + } + + @Override + public void visitClassType(String name) { + name = renameInternalType(name); + mSv.visitClassType(name); + } + + @Override + public void visitInnerClassType(String name) { + name = renameInternalType(name); + mSv.visitInnerClassType(name); + } + + @Override + public SignatureVisitor visitArrayType() { + SignatureVisitor sv = mSv.visitArrayType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitBaseType(char descriptor) { + mSv.visitBaseType(descriptor); + } + + @Override + public SignatureVisitor visitClassBound() { + SignatureVisitor sv = mSv.visitClassBound(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitEnd() { + mSv.visitEnd(); + } + + @Override + public SignatureVisitor visitExceptionType() { + SignatureVisitor sv = mSv.visitExceptionType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitFormalTypeParameter(String name) { + mSv.visitFormalTypeParameter(name); + } + + @Override + public SignatureVisitor visitInterface() { + SignatureVisitor sv = mSv.visitInterface(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + SignatureVisitor sv = mSv.visitInterfaceBound(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitParameterType() { + SignatureVisitor sv = mSv.visitParameterType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitReturnType() { + SignatureVisitor sv = mSv.visitReturnType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitSuperclass() { + SignatureVisitor sv = mSv.visitSuperclass(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitTypeArgument() { + mSv.visitTypeArgument(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + SignatureVisitor sv = mSv.visitTypeArgument(wildcard); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitTypeVariable(String name) { + mSv.visitTypeVariable(name); + } + + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java new file mode 100644 index 0000000..51e7535 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java @@ -0,0 +1,376 @@ +/* + * 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 org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +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 stub depending on the return type. Original annotations are passed along unchanged. + */ +class StubMethodAdapter extends MethodVisitor { + + private static String CONSTRUCTOR = "<init>"; + private static String CLASS_INIT = "<clinit>"; + + /** The parent method writer */ + private MethodVisitor mParentVisitor; + /** The method return type. Can be null. */ + private Type mReturnType; + /** Message to be printed by stub methods. */ + private String mInvokeSignature; + /** Flag to output the first line number. */ + private boolean mOutputFirstLineNumber = true; + /** Flag that is true when implementing a constructor, to accept all original + * code calling the original super constructor. */ + private boolean mIsInitMethod = false; + + private boolean mMessageGenerated; + private final boolean mIsStatic; + private final boolean mIsNative; + + public StubMethodAdapter(MethodVisitor mv, String methodName, Type returnType, + String invokeSignature, boolean isStatic, boolean isNative) { + super(Opcodes.ASM4); + mParentVisitor = mv; + mReturnType = returnType; + mInvokeSignature = invokeSignature; + mIsStatic = isStatic; + mIsNative = isNative; + + if (CONSTRUCTOR.equals(methodName) || CLASS_INIT.equals(methodName)) { + mIsInitMethod = true; + } + } + + private void generateInvoke() { + /* Generates the code: + * OverrideMethod.invoke("signature", mIsNative ? true : false, null or this); + */ + mParentVisitor.visitLdcInsn(mInvokeSignature); + // push true or false + mParentVisitor.visitInsn(mIsNative ? Opcodes.ICONST_1 : Opcodes.ICONST_0); + // push null or this + if (mIsStatic) { + mParentVisitor.visitInsn(Opcodes.ACONST_NULL); + } else { + mParentVisitor.visitVarInsn(Opcodes.ALOAD, 0); + } + + int sort = mReturnType != null ? mReturnType.getSort() : Type.VOID; + switch(sort) { + case Type.VOID: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeV", + "(Ljava/lang/String;ZLjava/lang/Object;)V"); + mParentVisitor.visitInsn(Opcodes.RETURN); + break; + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeI", + "(Ljava/lang/String;ZLjava/lang/Object;)I"); + switch(sort) { + case Type.BOOLEAN: + Label l1 = new Label(); + mParentVisitor.visitJumpInsn(Opcodes.IFEQ, l1); + mParentVisitor.visitInsn(Opcodes.ICONST_1); + mParentVisitor.visitInsn(Opcodes.IRETURN); + mParentVisitor.visitLabel(l1); + mParentVisitor.visitInsn(Opcodes.ICONST_0); + break; + case Type.CHAR: + mParentVisitor.visitInsn(Opcodes.I2C); + break; + case Type.BYTE: + mParentVisitor.visitInsn(Opcodes.I2B); + break; + case Type.SHORT: + mParentVisitor.visitInsn(Opcodes.I2S); + break; + } + mParentVisitor.visitInsn(Opcodes.IRETURN); + break; + case Type.LONG: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeL", + "(Ljava/lang/String;ZLjava/lang/Object;)J"); + mParentVisitor.visitInsn(Opcodes.LRETURN); + break; + case Type.FLOAT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeF", + "(Ljava/lang/String;ZLjava/lang/Object;)F"); + mParentVisitor.visitInsn(Opcodes.FRETURN); + break; + case Type.DOUBLE: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeD", + "(Ljava/lang/String;ZLjava/lang/Object;)D"); + mParentVisitor.visitInsn(Opcodes.DRETURN); + break; + case Type.ARRAY: + case Type.OBJECT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeA", + "(Ljava/lang/String;ZLjava/lang/Object;)Ljava/lang/Object;"); + mParentVisitor.visitTypeInsn(Opcodes.CHECKCAST, mReturnType.getInternalName()); + mParentVisitor.visitInsn(Opcodes.ARETURN); + break; + } + + } + + private void generatePop() { + /* Pops the stack, depending on the return type. + */ + switch(mReturnType != null ? mReturnType.getSort() : Type.VOID) { + case Type.VOID: + break; + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + case Type.FLOAT: + case Type.ARRAY: + case Type.OBJECT: + mParentVisitor.visitInsn(Opcodes.POP); + break; + case Type.LONG: + case Type.DOUBLE: + mParentVisitor.visitInsn(Opcodes.POP2); + break; + } + } + + /* Pass down to visitor writer. In this implementation, either do nothing. */ + @Override + public void visitCode() { + mParentVisitor.visitCode(); + } + + /* + * visitMaxs is called just before visitEnd if there was any code to rewrite. + * For non-constructor, generate the messaging code and the return statement + * if it hasn't been done before. + */ + @Override + public void visitMaxs(int maxStack, int maxLocals) { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + } + mParentVisitor.visitMaxs(maxStack, maxLocals); + } + + /** + * End of visiting. + * For non-constructor, generate the messaging code and the return statement + * if it hasn't been done before. + */ + @Override + public void visitEnd() { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + mParentVisitor.visitMaxs(1, 1); + } + mParentVisitor.visitEnd(); + } + + /* Writes all annotation from the original method. */ + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + return mParentVisitor.visitAnnotation(desc, visible); + } + + /* Writes all annotation default values from the original method. */ + @Override + public AnnotationVisitor visitAnnotationDefault() { + return mParentVisitor.visitAnnotationDefault(); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + return mParentVisitor.visitParameterAnnotation(parameter, desc, visible); + } + + /* Writes all attributes from the original method. */ + @Override + 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. + */ + @Override + public void visitLineNumber(int line, Label start) { + if (mIsInitMethod || mOutputFirstLineNumber) { + mParentVisitor.visitLineNumber(line, start); + mOutputFirstLineNumber = false; + } + } + + /** + * For non-constructor, rewrite existing "return" instructions to write the message. + */ + @Override + public void visitInsn(int opcode) { + if (mIsInitMethod) { + switch (opcode) { + case Opcodes.RETURN: + case Opcodes.ARETURN: + case Opcodes.DRETURN: + case Opcodes.FRETURN: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + // Pop the last word from the stack since invoke will generate its own return. + generatePop(); + generateInvoke(); + mMessageGenerated = true; + //$FALL-THROUGH$ + default: + mParentVisitor.visitInsn(opcode); + } + } + } + + @Override + public void visitLabel(Label label) { + if (mIsInitMethod) { + mParentVisitor.visitLabel(label); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTryCatchBlock(start, end, handler, type); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitMethodInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitFieldInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (mIsInitMethod) { + mParentVisitor.visitFrame(type, nLocal, local, nStack, stack); + } + } + + @Override + public void visitIincInsn(int var, int increment) { + if (mIsInitMethod) { + mParentVisitor.visitIincInsn(var, increment); + } + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (mIsInitMethod) { + mParentVisitor.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (mIsInitMethod) { + mParentVisitor.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitLdcInsn(Object cst) { + if (mIsInitMethod) { + mParentVisitor.visitLdcInsn(cst); + } + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + if (mIsInitMethod) { + mParentVisitor.visitLocalVariable(name, desc, signature, start, end, index); + } + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + if (mIsInitMethod) { + mParentVisitor.visitMultiANewArrayInsn(desc, dims); + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTypeInsn(opcode, type); + } + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (mIsInitMethod) { + mParentVisitor.visitVarInsn(opcode, var); + } + } + +} 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 new file mode 100644 index 0000000..d45a183 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java @@ -0,0 +1,183 @@ +/* + * 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 org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.Set; + +/** + * Class adapter that can stub some or all of the methods of the class. + */ +class TransformClassAdapter extends ClassVisitor { + + /** True if all methods should be stubbed, false if only native ones must be stubbed. */ + private final boolean mStubAll; + /** True if the class is an interface. */ + private boolean mIsInterface; + private final String mClassName; + private final Log mLog; + private final Set<String> mStubMethods; + private Set<String> mDeleteReturns; + + /** + * Creates a new class adapter that will stub some or all methods. + * @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 + * methods should be stubbed. + * @param hasNative True if the method has natives, in which case its access should be + * changed. + */ + public TransformClassAdapter(Log logger, Set<String> stubMethods, + Set<String> deleteReturns, String className, ClassVisitor cv, + boolean stubNativesOnly, boolean hasNative) { + super(Opcodes.ASM4, cv); + mLog = logger; + mStubMethods = stubMethods; + mClassName = className; + mStubAll = !stubNativesOnly; + mIsInterface = false; + mDeleteReturns = deleteReturns; + } + + /* Visits the class header. */ + @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 + if (Main.sOptions.generatePublicAccess) { + access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); + access |= Opcodes.ACC_PUBLIC; + } + // remove final + access = access & ~Opcodes.ACC_FINAL; + // note: leave abstract classes as such + // don't try to implement stub for interfaces + + 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) { + // remove protected or private and set as public + if (Main.sOptions.generatePublicAccess) { + access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); + access |= Opcodes.ACC_PUBLIC; + } + // remove final + access = access & ~Opcodes.ACC_FINAL; + // note: leave abstract classes as such + // don't try to implement stub for interfaces + + super.visitInnerClass(name, outerName, innerName, access); + } + + /* Visits a method. */ + @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) { + String returnType = t.getInternalName(); + if (returnType != null) { + if (mDeleteReturns.contains(returnType)) { + return null; + } + } + } + } + + String methodSignature = mClassName.replace('/', '.') + "#" + name; + + // change access to public + if (Main.sOptions.generatePublicAccess) { + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + } + + // remove final + access = access & ~Opcodes.ACC_FINAL; + + // stub this method if they are all to be stubbed or if it is a native method + // and don't try to stub interfaces nor abstract non-native methods. + if (!mIsInterface && + ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != Opcodes.ACC_ABSTRACT) && + (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); + + } else { + mLog.debug(" Keep: %s %s", name, desc); + 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, + Object value) { + // change access to public + if (Main.sOptions.generatePublicAccess) { + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + } + return super.visitField(access, name, desc, signature, value); + } + + /** + * Extracts the return {@link Type} of this descriptor. + */ + Type returnType(String desc) { + if (desc != null) { + try { + return Type.getReturnType(desc); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + return null; + } +} 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 new file mode 100644 index 0000000..d6dba6a --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java @@ -0,0 +1,227 @@ +/* + * 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.android.tools.layoutlib.create.AsmAnalyzer.DependencyVisitor; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +/** + * Unit tests for some methods of {@link AsmAnalyzer}. + */ +public class AsmAnalyzerTest { + + private MockLog mLog; + private ArrayList<String> mOsJarPath; + private AsmAnalyzer mAa; + + @Before + public void setUp() throws Exception { + mLog = new MockLog(); + URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); + + mOsJarPath = new ArrayList<String>(); + mOsJarPath.add(url.getFile()); + + mAa = new AsmAnalyzer(mLog, mOsJarPath, null /* gen */, + null /* deriveFrom */, null /* includeGlobs */ ); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testParseZip() throws IOException { + Map<String, ClassReader> map = mAa.parseZip(mOsJarPath); + + assertArrayEquals(new String[] { + "mock_android.dummy.InnerTest", + "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.view.View", + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams", + "mock_android.widget.LinearLayout", + "mock_android.widget.LinearLayout$LayoutParams", + "mock_android.widget.TableLayout", + "mock_android.widget.TableLayout$LayoutParams" + }, + map.keySet().toArray()); + } + + @Test + public void testFindClass() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + 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" }, + found.keySet().toArray()); + assertArrayEquals(new ClassReader[] { cr }, found.values().toArray()); + } + + @Test + public void testFindGlobs() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + // this matches classes, a package match returns nothing + found.clear(); + mAa.findGlobs("mock_android.view", zipClasses, found); + + assertArrayEquals(new String[] { }, + found.keySet().toArray()); + + // a complex glob search. * is a search pattern that matches names, not dots + mAa.findGlobs("mock_android.*.*Group$*Layout*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + // a complex glob search. ** is a search pattern that matches names including dots + mAa.findGlobs("mock_android.**Group*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + // matches a single class + found.clear(); + mAa.findGlobs("mock_android.view.View", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View" + }, + found.keySet().toArray()); + + // matches everyting inside the given package but not sub-packages + found.clear(); + mAa.findGlobs("mock_android.view.*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + 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 testFindClassesDerivingFrom() throws LogAbortException, IOException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + mAa.findClassesDerivingFrom("mock_android.view.View", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.widget.LinearLayout", + "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); + TreeMap<String, ClassReader> keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> in_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> out_deps = new TreeMap<String, ClassReader>(); + + 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 */); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup", + "mock_android.widget.TableLayout$LayoutParams", + }, + out_deps.keySet().toArray()); + + in_deps.putAll(out_deps); + out_deps.clear(); + + // get second level dependencies + for (ClassReader cr2 : in_deps.values()) { + cr2.accept(visitor, 0 /* flags */); + } + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams", + }, + out_deps.keySet().toArray()); + + in_deps.putAll(out_deps); + out_deps.clear(); + + // get third level dependencies (there are none) + for (ClassReader cr2 : in_deps.values()) { + cr2.accept(visitor, 0 /* flags */); + } + + assertArrayEquals(new String[] { }, out_deps.keySet().toArray()); + } +} 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 new file mode 100644 index 0000000..7b76a5b --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -0,0 +1,119 @@ +/* + * 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 static org.junit.Assert.assertArrayEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Set; + +/** + * Unit tests for some methods of {@link AsmGenerator}. + */ +public class AsmGeneratorTest { + + private MockLog mLog; + private ArrayList<String> mOsJarPath; + private String mOsDestJar; + private File mTempFile; + + @Before + public void setUp() throws Exception { + mLog = new MockLog(); + URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); + + mOsJarPath = new ArrayList<String>(); + mOsJarPath.add(url.getFile()); + + mTempFile = File.createTempFile("mock", "jar"); + mOsDestJar = mTempFile.getAbsolutePath(); + mTempFile.deleteOnExit(); + } + + @After + public void tearDown() throws Exception { + if (mTempFile != null) { + mTempFile.delete(); + mTempFile = null; + } + } + + @Test + public void testClassRenaming() throws IOException, LogAbortException { + + ICreateInfo ci = new ICreateInfo() { + @Override + public Class<?>[] getInjectedClasses() { + // classes to inject in the final JAR + 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[] { + "mock_android.view.View", "mock_android.view._Original_View", + "not.an.actual.ClassName", "anoter.fake.NewClassName", + }; + } + + @Override + 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 + new String[] { // include classes + "**" + }); + 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 new file mode 100644 index 0000000..0135c40 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java @@ -0,0 +1,102 @@ +/* + * 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.*; + +import org.junit.Test; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.util.ArrayList; + + +/** + * Tests {@link ClassHasNativeVisitor}. + */ +public class ClassHasNativeVisitorTest { + + @Test + public void testHasNative() throws IOException { + MockClassHasNativeVisitor cv = new MockClassHasNativeVisitor(); + 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()); + assertTrue(cv.hasNativeMethods()); + } + + @Test + public void testHasNoNative() throws IOException { + MockClassHasNativeVisitor cv = new MockClassHasNativeVisitor(); + 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. + */ + private static class MockClassHasNativeVisitor extends ClassHasNativeVisitor { + private ArrayList<String> mMethodsFound = new ArrayList<String>(); + + public String[] getMethodsFound() { + return mMethodsFound.toArray(new String[mMethodsFound.size()]); + } + + @Override + protected void setHasNativeMethods(boolean hasNativeMethods, String methodName) { + if (hasNativeMethods) { + mMethodsFound.add(methodName); + } + super.setHasNativeMethods(hasNativeMethods, methodName); + } + } + + /** + * Dummy test class with a native method. + */ + public static class ClassWithNative { + public ClassWithNative() { + } + + public void callTheNativeMethod() { + native_method(); + } + + private native void native_method(); + } + + /** + * Dummy test class with no native method. + */ + public static class ClassWithoutNative { + public ClassWithoutNative() { + } + + public void someMethod() { + } + } +} 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..6e120ce --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java @@ -0,0 +1,463 @@ +/* + * 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 com.android.tools.layoutlib.create.dataclass.ClassWithNative; +import com.android.tools.layoutlib.create.dataclass.OuterClass; +import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass; + +import org.junit.Before; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class DelegateClassAdapterTest { + + private MockLog mLog; + + private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getCanonicalName(); + private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName(); + private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" + + InnerClass.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 Throwable { + // 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(NATIVE_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 = NATIVE_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cv = new DelegateClassAdapter( + mLog, cw, internalClassName, delegateMethods); + + ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); + cr.accept(cv, 0 /* flags */); + + // Load the generated class in a different class loader and try it again + + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + Class<?> clazz2 = loadClass(NATIVE_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.add(NATIVE_CLASS_NAME, cw); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + + /** + * {@link DelegateMethodAdapter2} 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 = NATIVE_CLASS_NAME.replace('.', '/'); + + HashSet<String> delegateMethods = new HashSet<String>(); + delegateMethods.add("<init>"); + DelegateClassAdapter cv = new DelegateClassAdapter( + mLog, cw, internalClassName, delegateMethods); + + ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); + cr.accept(cv, 0 /* flags */); + } + + @Test + public void testDelegateNative() throws Throwable { + ClassWriter cw = new ClassWriter(0 /*flags*/); + String internalClassName = NATIVE_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(NATIVE_CLASS_NAME); + cr.accept(cv, 0 /* flags */); + + // Load the generated class in a different class loader and try it + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + Class<?> clazz2 = loadClass(NATIVE_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.add(NATIVE_CLASS_NAME, cw); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + + @Test + public void testDelegateInner() throws Throwable { + // We'll delegate the "get" method of both the inner and outer class. + HashSet<String> delegateMethods = new HashSet<String>(); + delegateMethods.add("get"); + delegateMethods.add("privateMethod"); + + // Generate the delegate for the outer class. + ClassWriter cwOuter = new ClassWriter(0 /*flags*/); + String outerClassName = OUTER_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cvOuter = new DelegateClassAdapter( + mLog, cwOuter, outerClassName, delegateMethods); + ClassReader cr = new ClassReader(OUTER_CLASS_NAME); + cr.accept(cvOuter, 0 /* flags */); + + // Generate the delegate for the inner class. + ClassWriter cwInner = new ClassWriter(0 /*flags*/); + String innerClassName = INNER_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cvInner = new DelegateClassAdapter( + mLog, cwInner, innerClassName, delegateMethods); + cr = new ClassReader(INNER_CLASS_NAME); + cr.accept(cvInner, 0 /* flags */); + + // Load the generated classes in a different class loader and try them + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + + // Check the outer class + Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME); + Object o2 = outerClazz2.newInstance(); + assertNotNull(o2); + + // The original Outer.get returns 1+10+20, + // but the delegate makes it return 4+10+20 + assertEquals(4+10+20, callGet(o2, 10, 20)); + assertEquals(1+10+20, callGet_Original(o2, 10, 20)); + + // The original Outer has a private method that is + // delegated. We should be able to call both the delegate + // and the original (which is now public). + assertEquals("outerPrivateMethod", + callMethod(o2, "privateMethod_Original", false /*makePublic*/)); + + // The original method is private, so by default we can't access it + boolean gotIllegalAccessException = false; + try { + callMethod(o2, "privateMethod", false /*makePublic*/); + } catch(IllegalAccessException e) { + gotIllegalAccessException = true; + } + assertTrue(gotIllegalAccessException); + // Try again, but now making it accessible + assertEquals("outerPrivate_Delegate", + callMethod(o2, "privateMethod", true /*makePublic*/)); + + // Check the inner class. Since it's not a static inner class, we need + // to use the hidden constructor that takes the outer class as first parameter. + Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME); + Constructor<?> innerCons = innerClazz2.getConstructor( + new Class<?>[] { outerClazz2 }); + Object i2 = innerCons.newInstance(new Object[] { o2 }); + assertNotNull(i2); + + // The original Inner.get returns 3+10+20, + // but the delegate makes it return 6+10+20 + assertEquals(6+10+20, callGet(i2, 10, 20)); + assertEquals(3+10+20, callGet_Original(i2, 10, 20)); + } + }; + cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray()); + cl2.add(INNER_CLASS_NAME, cwInner.toByteArray()); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + + //------- + + /** + * A class loader than can define and instantiate our modified classes. + * <p/> + * The trick here is that this class loader will test our <em>modified</em> version + * of the classes, the one with the delegate calls. + * <p/> + * 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 Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>(); + + public ClassLoader2() { + super(null); + } + + public ClassLoader2 add(String className, byte[] definition) { + mClassDefs.put(className, definition); + return this; + } + + public ClassLoader2 add(String className, ClassWriter rewrittenClass) { + mClassDefs.put(className, rewrittenClass.toByteArray()); + return this; + } + + private Set<Entry<String, byte[]>> getByteCode() { + return mClassDefs.entrySet(); + } + + @SuppressWarnings("unused") + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + try { + return super.findClass(name); + } catch (ClassNotFoundException e) { + + byte[] def = mClassDefs.get(name); + if (def != null) { + // Load the modified ClassWithNative from its bytes representation. + return defineClass(name, def, 0, def.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 OuterClass#get} or {@link InnerClass#get}via reflection. + */ + public int callGet(Object instance, int a, long b) throws Exception { + Method m = instance.getClass().getMethod("get", + new Class<?>[] { int.class, long.class } ); + + Object result = m.invoke(instance, new Object[] { a, b }); + return ((Integer) result).intValue(); + } + + /** + * Accesses the "_Original" methods for {@link OuterClass#get} + * or {@link InnerClass#get}via reflection. + */ + public int callGet_Original(Object instance, int a, long b) throws Exception { + Method m = instance.getClass().getMethod("get_Original", + new Class<?>[] { int.class, long.class } ); + + Object result = m.invoke(instance, new Object[] { a, b }); + return ((Integer) result).intValue(); + } + + /** + * Accesses the any declared method that takes no parameter via reflection. + */ + @SuppressWarnings("unchecked") + public <T> T callMethod(Object instance, String methodName, boolean makePublic) throws Exception { + Method m = instance.getClass().getDeclaredMethod(methodName, (Class<?>[])null); + + boolean wasAccessible = m.isAccessible(); + if (makePublic && !wasAccessible) { + m.setAccessible(true); + } + + Object result = m.invoke(instance, (Object[])null); + + if (makePublic && !wasAccessible) { + m.setAccessible(false); + } + + return (T) result; + } + + /** + * 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; + } + + /** + * For debugging, it's useful to dump the content of the generated classes + * along with the exception that was generated. + * + * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor + * class and associated utilities which are found in the ASM source jar. Since we don't + * want that dependency in the source code, we only put it manually for development and + * access the TraceClassVisitor via reflection if present. + * + * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()} + * @param cl2 The {@link ClassLoader2} instance with the generated bytecode. + * @return Either original {@code t} or a new wrapper {@link Throwable} + */ + private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) { + try { + // For debugging, dump the bytecode of the class in case of unexpected error + // if we can find the TraceClassVisitor class. + Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor"); + + StringBuilder sb = new StringBuilder(); + sb.append('\n').append(t.getClass().getCanonicalName()); + if (t.getMessage() != null) { + sb.append(": ").append(t.getMessage()); + } + + for (Entry<String, byte[]> entry : cl2.getByteCode()) { + String className = entry.getKey(); + byte[] bytes = entry.getValue(); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw); + Constructor<?> cons = tcvClass.getConstructor(new Class<?>[] { pw.getClass() }); + Object tcv = cons.newInstance(new Object[] { pw }); + ClassReader cr2 = new ClassReader(bytes); + cr2.accept((ClassVisitor) tcv, 0 /* flags */); + + sb.append("\nBytecode dump: <").append(className).append(">:\n") + .append(sw.toString()); + } + + // Re-throw exception with new message + RuntimeException ex = new RuntimeException(sb.toString(), t); + return ex; + } catch (Throwable ignore) { + // In case of problem, just throw the original exception as-is. + return t; + } + } + +} 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 new file mode 100644 index 0000000..1a5f653 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java @@ -0,0 +1,88 @@ +/* + * 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 static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LogTest { + + private MockLog mLog; + + @Before + public void setUp() throws Exception { + mLog = new MockLog(); + } + + @After + public void tearDown() throws Exception { + // pass + } + + @Test + public void testDebug() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.setVerbose(false); + mLog.debug("Test %d", 42); + assertEquals("", mLog.getOut()); + + mLog.setVerbose(true); + mLog.debug("Test %d", 42); + + assertEquals("Test 42\n", mLog.getOut()); + assertEquals("", mLog.getErr()); + } + + @Test + public void testInfo() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.info("Test %d", 43); + + assertEquals("Test 43\n", mLog.getOut()); + assertEquals("", mLog.getErr()); + } + + @Test + public void testError() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.error("Test %d", 44); + + assertEquals("", mLog.getOut()); + assertEquals("Test 44\n", mLog.getErr()); + } + + @Test + public void testException() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + Exception e = new Exception("My Exception"); + mLog.exception(e, "Test %d", 44); + + assertEquals("", mLog.getOut()); + assertTrue(mLog.getErr().startsWith("Test 44\njava.lang.Exception: My Exception")); + } +} 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'); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java new file mode 100644 index 0000000..90c6a9c --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java @@ -0,0 +1,120 @@ +/* + * 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 static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * + */ +public class RenameClassAdapterTest { + + private RenameClassAdapter mOuter; + private RenameClassAdapter mInner; + + @Before + public void setUp() throws Exception { + mOuter = new RenameClassAdapter(null, // cv + "com.pack.Old", + "org.blah.New"); + + mInner = new RenameClassAdapter(null, // cv + "com.pack.Old$Inner", + "org.blah.New$Inner"); + } + + @After + public void tearDown() throws Exception { + } + + /** + * Renames a type, e.g. "Lcom.package.My;" + * If the type doesn't need to be renamed, returns the input string as-is. + */ + @Test + public void testRenameTypeDesc() { + + // primitive types are left untouched + assertEquals("I", mOuter.renameTypeDesc("I")); + assertEquals("D", mOuter.renameTypeDesc("D")); + assertEquals("V", mOuter.renameTypeDesc("V")); + + // object types that need no renaming are left untouched + assertEquals("Lcom.package.MyClass;", mOuter.renameTypeDesc("Lcom.package.MyClass;")); + assertEquals("Lcom.package.MyClass;", mInner.renameTypeDesc("Lcom.package.MyClass;")); + + // object types that match the requirements + assertEquals("Lorg.blah.New;", mOuter.renameTypeDesc("Lcom.pack.Old;")); + assertEquals("Lorg.blah.New$Inner;", mInner.renameTypeDesc("Lcom.pack.Old$Inner;")); + // inner classes match the base type which is being renamed + assertEquals("Lorg.blah.New$Other;", mOuter.renameTypeDesc("Lcom.pack.Old$Other;")); + assertEquals("Lorg.blah.New$Other;", mInner.renameTypeDesc("Lcom.pack.Old$Other;")); + + // arrays + assertEquals("[Lorg.blah.New;", mOuter.renameTypeDesc("[Lcom.pack.Old;")); + assertEquals("[[Lorg.blah.New;", mOuter.renameTypeDesc("[[Lcom.pack.Old;")); + + assertEquals("[Lorg.blah.New;", mInner.renameTypeDesc("[Lcom.pack.Old;")); + assertEquals("[[Lorg.blah.New;", mInner.renameTypeDesc("[[Lcom.pack.Old;")); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the internal name of the input type. + */ + @Test + public void testRenameType() { + // Skip. This is actually tested by testRenameTypeDesc above. + } + + /** + * Renames an internal type name, e.g. "com.package.MyClass". + * If the type doesn't need to be renamed, returns the input string as-is. + */ + @Test + public void testRenameInternalType() { + // a descriptor is not left untouched + assertEquals("Lorg.blah.New;", mOuter.renameInternalType("Lcom.pack.Old;")); + assertEquals("Lorg.blah.New$Inner;", mOuter.renameInternalType("Lcom.pack.Old$Inner;")); + + // an actual FQCN + assertEquals("org.blah.New", mOuter.renameInternalType("com.pack.Old")); + assertEquals("org.blah.New$Inner", mOuter.renameInternalType("com.pack.Old$Inner")); + + assertEquals("org.blah.New$Other", mInner.renameInternalType("com.pack.Old$Other")); + assertEquals("org.blah.New$Other", mInner.renameInternalType("com.pack.Old$Other")); + } + + /** + * Renames a method descriptor, i.e. applies renameType to all arguments and to the + * return value. + */ + @Test + public void testRenameMethodDesc() { + assertEquals("(IDLorg.blah.New;[Lorg.blah.New$Inner;)Lorg.blah.New$Other;", + mOuter.renameMethodDesc("(IDLcom.pack.Old;[Lcom.pack.Old$Inner;)Lcom.pack.Old$Other;")); + } + + + +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative.java new file mode 100644 index 0000000..c314853 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative.java @@ -0,0 +1,45 @@ +/* + * 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.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * Dummy test class with a native method. + * The native method is not defined and any attempt to invoke it will + * throw an {@link UnsatisfiedLinkError}. + * + * Used by {@link DelegateClassAdapterTest}. + */ +public class ClassWithNative { + public ClassWithNative() { + } + + public int add(int a, int b) { + return a + b; + } + + // Note: it's good to have a long or double for testing parameters since they take + // 2 slots in the stack/locals maps. + + 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); +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative_Delegate.java new file mode 100644 index 0000000..a3d4dc6 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative_Delegate.java @@ -0,0 +1,34 @@ +/* + * 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.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * The delegate that receives the call to {@link ClassWithNative_Delegate}'s overridden methods. + * + * Used by {@link DelegateClassAdapterTest}. + */ +public 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/dataclass/OuterClass.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java new file mode 100644 index 0000000..f083e76 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2011 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.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * Test class with an inner class. + * + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass { + private int mOuterValue = 1; + public OuterClass() { + } + + // Outer.get returns 1 + a + b + // Note: it's good to have a long or double for testing parameters since they take + // 2 slots in the stack/locals maps. + public int get(int a, long b) { + return mOuterValue + a + (int) b; + } + + public class InnerClass { + public InnerClass() { + } + + // Inner.get returns 2 + 1 + a + b + public int get(int a, long b) { + return 2 + mOuterValue + a + (int) b; + } + } + + @SuppressWarnings("unused") + private String privateMethod() { + return "outerPrivateMethod"; + } +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_Delegate.java new file mode 100644 index 0000000..774be8e --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_Delegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011 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.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass_Delegate { + // The delegate override of Outer.get returns 4 + a + b + public static int get(OuterClass instance, int a, long b) { + return 4 + a + (int) b; + } + + public static String privateMethod(OuterClass instance) { + return "outerPrivate_Delegate"; + } +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_InnerClass_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_InnerClass_Delegate.java new file mode 100644 index 0000000..b472220 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_InnerClass_Delegate.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; +import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass; + +/** + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass_InnerClass_Delegate { + // The delegate override of Inner.get return 6 + a + b + public static int get(OuterClass outer, InnerClass inner, int a, long b) { + return 6 + a + (int) b; + } +} diff --git a/tools/layoutlib/create/tests/data/mock_android.jar b/tools/layoutlib/create/tests/data/mock_android.jar Binary files differnew file mode 100644 index 0000000..a7ea74f --- /dev/null +++ b/tools/layoutlib/create/tests/data/mock_android.jar diff --git a/tools/layoutlib/create/tests/data/mock_android.jardesc b/tools/layoutlib/create/tests/data/mock_android.jardesc new file mode 100644 index 0000000..95f7591 --- /dev/null +++ b/tools/layoutlib/create/tests/data/mock_android.jardesc @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="WINDOWS-1252" standalone="no"?> +<jardesc> + <jar path="C:/ralf/google/src/raphael-lapdroid/device/tools/layoutlib/create/tests/data/mock_android.jar"/> + <options buildIfNeeded="true" compress="true" descriptionLocation="/layoutlib_create/tests/data/mock_android.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/> + <storedRefactorings deprecationInfo="true" structuralOnly="false"/> + <selectedProjects/> + <manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true"> + <sealing sealJar="false"> + <packagesToSeal/> + <packagesToUnSeal/> + </sealing> + </manifest> + <selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false"> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.widget"/> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.view"/> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.dummy"/> + </selectedElements> +</jardesc> diff --git a/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java b/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java new file mode 100644 index 0000000..e355ead --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.dummy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +public class InnerTest { + + private int mSomeField; + private MyStaticInnerClass mInnerInstance; + private MyIntEnum mTheIntEnum; + private MyGenerics1<int[][], InnerTest, MyIntEnum, float[]> mGeneric1; + + public class NotStaticInner2 extends NotStaticInner1 { + + } + + public class NotStaticInner1 { + + public void someThing() { + mSomeField = 2; + mInnerInstance = null; + } + + } + + private static class MyStaticInnerClass { + + } + + private static class DerivingClass extends InnerTest { + + } + + // enums are a kind of inner static class + public enum MyIntEnum { + VALUE0(0), + VALUE1(1), + VALUE2(2); + + MyIntEnum(int myInt) { + this.myInt = myInt; + } + final int myInt; + } + + public static class MyGenerics1<T, U, V, W> { + public MyGenerics1() { + int a = 1; + } + } + + public <X> void genericMethod1(X a, X[] a) { + } + + public <X, Y> void genericMethod2(X a, List<Y> b) { + } + + public <X, Y> void genericMethod3(X a, List<Y extends InnerTest> b) { + } + + public <T extends InnerTest> void genericMethod4(T[] a, Collection<T> b, Collection<?> c) { + Iterator<T> i = b.iterator(); + } + + public void someMethod(InnerTest self) { + mSomeField = self.mSomeField; + MyStaticInnerClass m = new MyStaticInnerClass(); + mInnerInstance = m; + mTheIntEnum = null; + mGeneric1 = new MyGenerics1(); + genericMethod(new DerivingClass[0], new ArrayList<DerivingClass>(), new ArrayList<InnerTest>()); + } +} diff --git a/tools/layoutlib/create/tests/mock_android/view/View.java b/tools/layoutlib/create/tests/mock_android/view/View.java new file mode 100644 index 0000000..a80a98d --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/view/View.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.view; + +public class View { + +} diff --git a/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java b/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java new file mode 100644 index 0000000..466470f --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.view; + +public class ViewGroup extends View { + + public class MarginLayoutParams extends LayoutParams { + + } + + public class LayoutParams { + + } + +} diff --git a/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java b/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java new file mode 100644 index 0000000..3870a63 --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.widget; + +import mock_android.view.ViewGroup; + +public class LinearLayout extends ViewGroup { + + public class LayoutParams extends mock_android.view.ViewGroup.LayoutParams { + + } + +} diff --git a/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java b/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java new file mode 100644 index 0000000..e455e7d --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.widget; + +import mock_android.view.ViewGroup; + +public class TableLayout extends ViewGroup { + + public class LayoutParams extends MarginLayoutParams { + + } + +} |