Rename android.os.Build so it can be dynamically generated

This will allow the android.os.Build class to be generated dynamically
from the Studio side.
In order to allow the "refactoring" of classses like android.os.Build,
Create needed to be modified.

While adding the test, I've also done a small clean-up of the
AsmGeneratorTest to re-use some code.

Bug: http://b.android.com/210345
Test: Added new test to AsmGeneratorTest
Change-Id: Ie4e1209c1c60b7a33cb427dbd556a9741ec8f8b3
This commit is contained in:
Diego Perez
2017-03-01 14:18:10 +00:00
parent 158cd7b1f2
commit 9a7be3a3eb
8 changed files with 222 additions and 277 deletions

View File

@@ -106,7 +106,7 @@ import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os._Original_Build.VERSION_CODES.JELLY_BEAN_MR1;
import static com.android.layoutlib.bridge.android.RenderParamsFlags.FLAG_KEY_APPLICATION_PACKAGE;
/**

View File

@@ -16,13 +16,13 @@
package com.android.layoutlib.bridge.bars;
import android.os.Build.VERSION_CODES;
import android.os._Original_Build.VERSION_CODES;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static android.os.Build.VERSION_CODES.*;
import static android.os._Original_Build.VERSION_CODES.*;
/**
* Various helper methods to simulate older versions of platform.

View File

@@ -49,7 +49,7 @@ import android.widget.TextView;
import java.io.IOException;
import java.io.InputStream;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os._Original_Build.VERSION_CODES.LOLLIPOP;
/**
* Base "bar" class for the window decor around the the edited layout.

View File

@@ -37,6 +37,7 @@ import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Class that generates a new JAR from a list of classes, some of which are to be kept as-is
@@ -127,7 +128,10 @@ public class AsmGenerator {
// Create the map of classes to rename.
mRenameClasses = new HashMap<>();
mClassesNotRenamed = new HashSet<>();
String[] renameClasses = createInfo.getRenamedClasses();
String[] renameClasses = Stream.concat(
Arrays.stream(createInfo.getRenamedClasses()),
Arrays.stream(createInfo.getRefactoredClasses()))
.toArray(String[]::new);
int n = renameClasses.length;
for (int i = 0; i < n; i += 2) {
assert i + 1 < n;
@@ -140,7 +144,10 @@ public class AsmGenerator {
// Create a map of classes to be refactored.
mRefactorClasses = new HashMap<>();
String[] refactorClasses = createInfo.getJavaPkgClasses();
String[] refactorClasses = Stream.concat(
Arrays.stream(createInfo.getJavaPkgClasses()),
Arrays.stream(createInfo.getRefactoredClasses()))
.toArray(String[]::new);
n = refactorClasses.length;
for (int i = 0; i < n; i += 2) {
assert i + 1 < n;

View File

@@ -36,65 +36,41 @@ import java.util.Set;
*/
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 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;
}
/**
* Returns the list of classes to refactor, must be an even list: the binary FQCN of class to
* replace followed by the new FQCN. All references to the old class should be updated to the
* new class. The list can be empty but must not be null.
*/
@Override
public String[] getJavaPkgClasses() {
return JAVA_PKG_CLASSES;
}
@Override
public String[] getRefactoredClasses() {
return REFACTOR_CLASSES;
}
@Override
public Set<String> getExcludedClasses() {
String[] refactoredClasses = getJavaPkgClasses();
@@ -333,10 +309,22 @@ public final class CreateInfo implements ICreateInfo {
"java.text.SimpleDateFormat", "android.icu.text.SimpleDateFormat",
};
/**
* List of classes to refactor. This is similar to combining {@link #getRenamedClasses()} and
* {@link #getJavaPkgClasses()}.
* Classes included here will be renamed and then all their references in any other classes
* will be also modified.
* FQCN of class to refactor followed by its new FQCN.
*/
private final static String[] REFACTOR_CLASSES =
new String[] {
"android.os.Build", "android.os._Original_Build",
};
private final static String[] EXCLUDED_CLASSES =
new String[] {
"android.preference.PreferenceActivity",
"org.kxml2.io.KXmlParser"
"org.kxml2.io.KXmlParser",
};
/**

View File

@@ -51,6 +51,15 @@ public interface ICreateInfo {
*/
String[] getRenamedClasses();
/**
* List of classes to refactor. This is similar to combining {@link #getRenamedClasses()} and
* {@link #getJavaPkgClasses()}.
* Classes included here will be renamed and then all their references in any other classes
* will be also modified.
* FQCN of class to refactor followed by its new FQCN.
*/
String[] getRefactoredClasses();
/**
* 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

View File

@@ -35,6 +35,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
@@ -54,8 +55,6 @@ import static org.junit.Assert.assertTrue;
* Unit tests for some methods of {@link AsmGenerator}.
*/
public class AsmGeneratorTest {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private MockLog mLog;
private ArrayList<String> mOsJarPath;
private String mOsDestJar;
@@ -81,6 +80,7 @@ public class AsmGeneratorTest {
@After
public void tearDown() throws Exception {
if (mTempFile != null) {
//noinspection ResultOfMethodCallIgnored
mTempFile.delete();
mTempFile = null;
}
@@ -89,23 +89,7 @@ public class AsmGeneratorTest {
@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 EMPTY_STRING_ARRAY;
}
@Override
public String[] getDelegateClassNatives() {
return EMPTY_STRING_ARRAY;
}
ICreateInfo ci = new CreateInfoAdapter() {
@Override
public String[] getRenamedClasses() {
// classes to rename (so that we can replace them)
@@ -114,37 +98,6 @@ public class AsmGeneratorTest {
"not.an.actual.ClassName", "anoter.fake.NewClassName",
};
}
@Override
public String[] getJavaPkgClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public Set<String> getExcludedClasses() {
return null;
}
@Override
public String[] getDeleteReturns() {
// methods deleted from their return type.
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedFields() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
};
AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
@@ -154,7 +107,7 @@ public class AsmGeneratorTest {
new String[] { // include classes
"**"
},
Collections.<String>emptySet() /* excluded classes */,
Collections.emptySet() /* excluded classes */,
new String[]{} /* include files */);
aa.analyze();
agen.generate();
@@ -165,8 +118,8 @@ public class AsmGeneratorTest {
}
@Test
public void testClassRefactoring() throws IOException, LogAbortException {
ICreateInfo ci = new ICreateInfo() {
public void testJavaClassRefactoring() throws IOException, LogAbortException {
ICreateInfo ci = new CreateInfoAdapter() {
@Override
public Class<?>[] getInjectedClasses() {
// classes to inject in the final JAR
@@ -175,22 +128,6 @@ public class AsmGeneratorTest {
};
}
@Override
public String[] getDelegateMethods() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getDelegateClassNatives() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getRenamedClasses() {
// classes to rename (so that we can replace them)
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getJavaPkgClasses() {
// classes to refactor (so that we can replace them)
@@ -203,27 +140,6 @@ public class AsmGeneratorTest {
public Set<String> getExcludedClasses() {
return Collections.singleton("java.lang.JavaClass");
}
@Override
public String[] getDeleteReturns() {
// methods deleted from their return type.
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedFields() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
};
AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
@@ -233,7 +149,7 @@ public class AsmGeneratorTest {
new String[] { // include classes
"**"
},
Collections.<String>emptySet(),
Collections.emptySet(),
new String[] { /* include files */
"mock_android/data/data*"
});
@@ -242,47 +158,64 @@ public class AsmGeneratorTest {
Map<String, ClassReader> output = new TreeMap<>();
Map<String, InputStream> filesFound = new TreeMap<>();
parseZip(mOsDestJar, output, filesFound);
boolean injectedClassFound = false;
RecordingClassVisitor cv = new RecordingClassVisitor();
for (ClassReader cr: output.values()) {
TestClassVisitor cv = new TestClassVisitor();
cr.accept(cv, 0);
injectedClassFound |= cv.mInjectedClassFound;
}
assertTrue(injectedClassFound);
assertTrue(cv.mVisitedClasses.contains(
"com/android/tools/layoutlib/create/dataclass/JavaClass"));
assertFalse(cv.mVisitedClasses.contains(
JAVA_CLASS_NAME));
assertArrayEquals(new String[] {"mock_android/data/dataFile"},
filesFound.keySet().toArray());
}
@Test
public void testClassExclusion() throws IOException, LogAbortException {
ICreateInfo ci = new ICreateInfo() {
public void testClassRefactoring() throws IOException, LogAbortException {
ICreateInfo ci = new CreateInfoAdapter() {
@Override
public Class<?>[] getInjectedClasses() {
return new Class<?>[0];
// classes to inject in the final JAR
return new Class<?>[] {
com.android.tools.layoutlib.create.dataclass.JavaClass.class
};
}
@Override
public String[] getDelegateMethods() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getDelegateClassNatives() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getRenamedClasses() {
// classes to rename (so that we can replace them)
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getJavaPkgClasses() {
public String[] getRefactoredClasses() {
// classes to refactor (so that we can replace them)
return EMPTY_STRING_ARRAY;
return new String[] {
"mock_android.view.View", "mock_android.view._Original_View",
};
}
};
AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
null, // derived from
new String[] { // include classes
"**"
},
Collections.emptySet(),
new String[] {});
aa.analyze();
agen.generate();
Map<String, ClassReader> output = new TreeMap<>();
parseZip(mOsDestJar, output, new TreeMap<>());
RecordingClassVisitor cv = new RecordingClassVisitor();
for (ClassReader cr: output.values()) {
cr.accept(cv, 0);
}
assertTrue(cv.mVisitedClasses.contains(
"mock_android/view/_Original_View"));
assertFalse(cv.mVisitedClasses.contains(
"mock_android/view/View"));
}
@Test
public void testClassExclusion() throws IOException, LogAbortException {
ICreateInfo ci = new CreateInfoAdapter() {
@Override
public Set<String> getExcludedClasses() {
Set<String> set = new HashSet<>(2);
@@ -290,27 +223,6 @@ public class AsmGeneratorTest {
set.add("java.lang.JavaClass");
return set;
}
@Override
public String[] getDeleteReturns() {
// methods deleted from their return type.
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedFields() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
};
AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
@@ -340,55 +252,7 @@ public class AsmGeneratorTest {
public void testMethodInjection() throws IOException, LogAbortException,
ClassNotFoundException, IllegalAccessException, InstantiationException,
NoSuchMethodException, InvocationTargetException {
ICreateInfo ci = new ICreateInfo() {
@Override
public Class<?>[] getInjectedClasses() {
return new Class<?>[0];
}
@Override
public String[] getDelegateMethods() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getDelegateClassNatives() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getRenamedClasses() {
// classes to rename (so that we can replace them)
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getJavaPkgClasses() {
// classes to refactor (so that we can replace them)
return EMPTY_STRING_ARRAY;
}
@Override
public Set<String> getExcludedClasses() {
return Collections.emptySet();
}
@Override
public String[] getDeleteReturns() {
// methods deleted from their return type.
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedFields() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedClasses() {
return EMPTY_STRING_ARRAY;
}
ICreateInfo ci = new CreateInfoAdapter() {
@Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.singletonMap("mock_android.util.EmptyArray",
@@ -474,97 +338,94 @@ public class AsmGeneratorTest {
}
}
private class TestClassVisitor extends ClassVisitor {
/**
* {@link ClassVisitor} that records every class that sees.
*/
private static class RecordingClassVisitor extends ClassVisitor {
private Set<String> mVisitedClasses = new HashSet<>();
boolean mInjectedClassFound = false;
TestClassVisitor() {
private RecordingClassVisitor() {
super(Main.ASM_VERSION);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
assertTrue(!getBase(name).equals(JAVA_CLASS_NAME));
if (name.equals("com/android/tools/layoutlib/create/dataclass/JavaClass")) {
mInjectedClassFound = true;
private void addClass(String className) {
if (className == null) {
return;
}
super.visit(version, access, name, signature, superName, interfaces);
int pos = className.indexOf('$');
if (pos > 0) {
// For inner classes, add also the base class
mVisitedClasses.add(className.substring(0, pos));
}
mVisitedClasses.add(className);
}
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
assertTrue(testType(Type.getType(desc)));
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
addClass(superName);
Arrays.stream(interfaces).forEach(this::addClass);
}
private void processType(Type type) {
switch (type.getSort()) {
case Type.OBJECT:
addClass(type.getInternalName());
break;
case Type.ARRAY:
addClass(type.getElementType().getInternalName());
break;
case Type.METHOD:
processType(type.getReturnType());
Arrays.stream(type.getArgumentTypes()).forEach(this::processType);
break;
}
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature,
Object value) {
processType(Type.getType(desc));
return super.visitField(access, name, desc, signature, value);
}
@SuppressWarnings("hiding")
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return new MethodVisitor(Main.ASM_VERSION, mv) {
@Override
public void visitFieldInsn(int opcode, String owner, String name,
String desc) {
assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME));
assertTrue(testType(Type.getType(desc)));
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
addClass(owner);
processType(Type.getType(desc));
super.visitFieldInsn(opcode, owner, name, desc);
}
@Override
public void visitLdcInsn(Object cst) {
if (cst instanceof Type) {
assertTrue(testType((Type)cst));
processType((Type) cst);
}
super.visitLdcInsn(cst);
}
@Override
public void visitTypeInsn(int opcode, String type) {
assertTrue(!getBase(type).equals(JAVA_CLASS_NAME));
addClass(type);
super.visitTypeInsn(opcode, type);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String desc, boolean itf) {
assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME));
assertTrue(testType(Type.getType(desc)));
public void visitMethodInsn(int opcode, String owner, String name, String desc,
boolean itf) {
addClass(owner);
processType(Type.getType(desc));
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
};
}
private boolean testType(Type type) {
int sort = type.getSort();
if (sort == Type.OBJECT) {
assertTrue(!getBase(type.getInternalName()).equals(JAVA_CLASS_NAME));
} else if (sort == Type.ARRAY) {
assertTrue(!getBase(type.getElementType().getInternalName())
.equals(JAVA_CLASS_NAME));
} else if (sort == Type.METHOD) {
boolean r = true;
for (Type t : type.getArgumentTypes()) {
r &= testType(t);
}
return r & testType(type.getReturnType());
}
return true;
}
private String getBase(String className) {
if (className == null) {
return null;
}
int pos = className.indexOf('$');
if (pos > 0) {
return className.substring(0, pos);
}
return className;
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2017 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.Collections;
import java.util.Map;
import java.util.Set;
class CreateInfoAdapter implements ICreateInfo {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
@Override
public Class<?>[] getInjectedClasses() {
return new Class<?>[0];
}
@Override
public String[] getDelegateMethods() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getDelegateClassNatives() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getRenamedClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getRefactoredClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getDeleteReturns() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getJavaPkgClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public Set<String> getExcludedClasses() {
return Collections.emptySet();
}
@Override
public String[] getPromotedFields() {
return EMPTY_STRING_ARRAY;
}
@Override
public String[] getPromotedClasses() {
return EMPTY_STRING_ARRAY;
}
@Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
}