Add ability to promote classes to public visibility
Test: Added new PromoteClassClassAdapterTest
Change-Id: I30f9ee259d39e2b2768c1ceb45aa2161983c5a5e
(cherry picked from commit 294f0850f7623737899c9ea0b03cebc2cf7e4176)
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java
index a2f8372..bed5806a 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java
@@ -36,6 +36,7 @@
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
+import java.util.stream.Collectors;
/**
* Class that generates a new JAR from a list of classes, some of which are to be kept as-is
@@ -78,6 +79,8 @@
private final Map<String, ICreateInfo.InjectMethodRunnable> mInjectedMethodsMap;
/** A map { FQCN => set { field names } } which should be promoted to public visibility */
private final Map<String, Set<String>> mPromotedFields;
+ /** A list of classes to be promoted to public visibility */
+ private final Set<String> mPromotedClasses;
/**
* Creates a new generator that can generate the output JAR with the stubbed classes.
@@ -179,6 +182,9 @@
addToMap(createInfo.getPromotedFields(), mPromotedFields);
mInjectedMethodsMap = createInfo.getInjectedMethodsMap();
+
+ mPromotedClasses =
+ Arrays.stream(createInfo.getPromotedClasses()).collect(Collectors.toSet());
}
/**
@@ -400,7 +406,11 @@
if (promoteFields != null && !promoteFields.isEmpty()) {
cv = new PromoteFieldClassAdapter(cv, promoteFields);
}
+ if (!mPromotedClasses.isEmpty()) {
+ cv = new PromoteClassClassAdapter(cv, mPromotedClasses);
+ }
cr.accept(cv, 0);
+
return cw.toByteArray();
}
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
index 741eb27..94302d3 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
@@ -113,6 +113,11 @@
}
@Override
+ public String[] getPromotedClasses() {
+ return PROMOTED_CLASSES;
+ }
+
+ @Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return INJECTED_METHODS;
}
@@ -344,6 +349,13 @@
};
/**
+ * List of classes to be promoted to public visibility. Prefer using PROMOTED_FIELDS to this
+ * if possible.
+ */
+ private final static String[] PROMOTED_CLASSES = new String[] {
+ };
+
+ /**
* 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
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
index 535a9a8..48abde4 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java
@@ -78,6 +78,11 @@
String[] getPromotedFields();
/**
+ * Returns a list of classes to be promoted to public visibility.
+ */
+ String[] getPromotedClasses();
+
+ /**
* Returns a map from binary FQCN className to {@link InjectMethodRunnable} which will be
* called to inject methods into a class.
* Can be empty but must not be null.
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteClassClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteClassClassAdapter.java
new file mode 100644
index 0000000..99e3089
--- /dev/null
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteClassClassAdapter.java
@@ -0,0 +1,64 @@
+/*
+ * 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 org.objectweb.asm.ClassVisitor;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+
+/**
+ * Promotes given classes to public visibility.
+ */
+public class PromoteClassClassAdapter extends ClassVisitor {
+
+ private final Set<String> mClassNames;
+ private static final int CLEAR_PRIVATE_MASK = ~(ACC_PRIVATE | ACC_PROTECTED);
+
+ public PromoteClassClassAdapter(ClassVisitor cv, Set<String> classNames) {
+ super(Main.ASM_VERSION, cv);
+ mClassNames =
+ classNames.stream().map(name -> name.replace(".", "/")).collect(Collectors.toSet());
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature, String superName,
+ String[] interfaces) {
+ if (mClassNames.contains(name)) {
+ if ((access & ACC_PUBLIC) == 0) {
+ access = (access & CLEAR_PRIVATE_MASK) | ACC_PUBLIC;
+ }
+ }
+
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ if (mClassNames.contains(name)) {
+ if ((access & ACC_PUBLIC) == 0) {
+ access = (access & CLEAR_PRIVATE_MASK) | ACC_PUBLIC;
+ }
+ }
+
+ super.visitInnerClass(name, outerName, innerName, access);
+ }
+}
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java
index 05af033..ba77860 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java
@@ -31,7 +31,7 @@
public class PromoteFieldClassAdapter extends ClassVisitor {
private final Set<String> mFieldNames;
- private static final int ACC_NOT_PUBLIC = ~(ACC_PRIVATE | ACC_PROTECTED);
+ private static final int CLEAR_PRIVATE_MASK = ~(ACC_PRIVATE | ACC_PROTECTED);
public PromoteFieldClassAdapter(ClassVisitor cv, Set<String> fieldNames) {
super(Main.ASM_VERSION, cv);
@@ -43,7 +43,7 @@
Object value) {
if (mFieldNames.contains(name)) {
if ((access & ACC_PUBLIC) == 0) {
- access = (access & ACC_NOT_PUBLIC) | ACC_PUBLIC;
+ access = (access & CLEAR_PRIVATE_MASK) | ACC_PUBLIC;
}
}
return super.visitField(access, name, desc, signature, value);
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java
index 0560d8a..4d5d5d2 100644
--- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java
@@ -137,6 +137,11 @@
}
@Override
+ public String[] getPromotedClasses() {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ @Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
@@ -211,6 +216,11 @@
}
@Override
+ public String[] getPromotedClasses() {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ @Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
@@ -293,6 +303,11 @@
}
@Override
+ public String[] getPromotedClasses() {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ @Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.emptyMap();
}
@@ -370,6 +385,11 @@
}
@Override
+ public String[] getPromotedClasses() {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ @Override
public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
return Collections.singletonMap("mock_android.util.EmptyArray",
InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER);
diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/PromoteClassClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/PromoteClassClassAdapterTest.java
new file mode 100644
index 0000000..eeb0b10
--- /dev/null
+++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/PromoteClassClassAdapterTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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 org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringJoiner;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * {@link ClassVisitor} that logs all the calls to the different visit methods so they can be later
+ * inspected.
+ */
+class LoggingClassVisitor extends ClassVisitor {
+ List<String> mLog = new LinkedList<String>();
+
+ public LoggingClassVisitor() {
+ super(Main.ASM_VERSION);
+ }
+
+ public LoggingClassVisitor(ClassVisitor cv) {
+ super(Main.ASM_VERSION, cv);
+ }
+
+ private static String formatAccess(int access) {
+ StringJoiner modifiers = new StringJoiner(",");
+
+ if ((access & Opcodes.ACC_PUBLIC) != 0) {
+ modifiers.add("public");
+ }
+ if ((access & Opcodes.ACC_PRIVATE) != 0) {
+ modifiers.add("private");
+ }
+ if ((access & Opcodes.ACC_PROTECTED) != 0) {
+ modifiers.add("protected");
+ }
+ if ((access & Opcodes.ACC_STATIC) != 0) {
+ modifiers.add("static");
+ }
+ if ((access & Opcodes.ACC_FINAL) != 0) {
+ modifiers.add("static");
+ }
+
+ return "[" + modifiers.toString() + "]";
+ }
+
+ private void log(String method, String format, Object...args) {
+ mLog.add(
+ String.format("[%s] - %s", method, String.format(format, (Object[]) args))
+ );
+ }
+
+ @Override
+ public void visitOuterClass(String owner, String name, String desc) {
+ log(
+ "visitOuterClass",
+ "owner=%s, name=%s, desc=%s",
+ owner, name, desc
+ );
+
+ super.visitOuterClass(owner, name, desc);
+ }
+
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ log(
+ "visitInnerClass",
+ "name=%s, outerName=%s, innerName=%s, access=%s",
+ name, outerName, innerName, formatAccess(access)
+ );
+
+ super.visitInnerClass(name, outerName, innerName, access);
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature, String superName,
+ String[] interfaces) {
+ log(
+ "visit",
+ "version=%d, access=%s, name=%s, signature=%s, superName=%s, interfaces=%s",
+ version, formatAccess(access), name, signature, superName, Arrays.toString(interfaces)
+ );
+
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+}
+
+class PackageProtectedClass {}
+
+public class PromoteClassClassAdapterTest {
+ private static class PrivateClass {}
+ private static class ClassWithPrivateInnerClass {
+ private class InnerPrivateClass {}
+ }
+
+ @Test
+ public void testInnerClassPromotion() throws IOException {
+ ClassReader reader = new ClassReader(PrivateClass.class.getName());
+ LoggingClassVisitor log = new LoggingClassVisitor();
+
+ PromoteClassClassAdapter adapter = new PromoteClassClassAdapter(log, new HashSet<String>() {
+ {
+ add("com.android.tools.layoutlib.create.PromoteClassClassAdapterTest$PrivateClass");
+ add("com.android.tools.layoutlib.create" +
+ ".PromoteClassClassAdapterTest$ClassWithPrivateInnerClass$InnerPrivateClass");
+ }
+ });
+ reader.accept(adapter, 0);
+ assertTrue(log.mLog.contains(
+ "[visitInnerClass] - " +
+ "name=com/android/tools/layoutlib/create" +
+ "/PromoteClassClassAdapterTest$PrivateClass, " +
+ "outerName=com/android/tools/layoutlib/create" +
+ "/PromoteClassClassAdapterTest, innerName=PrivateClass, access=[public,static]"));
+
+ // Test inner of inner class
+ log.mLog.clear();
+ reader = new ClassReader(ClassWithPrivateInnerClass.class.getName());
+ reader.accept(adapter, 0);
+
+ assertTrue(log.mLog.contains("[visitInnerClass] - " +
+ "name=com/android/tools/layoutlib/create" +
+ "/PromoteClassClassAdapterTest$ClassWithPrivateInnerClass$InnerPrivateClass, " +
+ "outerName=com/android/tools/layoutlib/create" +
+ "/PromoteClassClassAdapterTest$ClassWithPrivateInnerClass, " +
+ "innerName=InnerPrivateClass, access=[public]"));
+
+ }
+
+ @Test
+ public void testProtectedClassPromotion() throws IOException {
+ ClassReader reader = new ClassReader(PackageProtectedClass.class.getName());
+ LoggingClassVisitor log = new LoggingClassVisitor();
+
+ PromoteClassClassAdapter adapter = new PromoteClassClassAdapter(log, new HashSet<String>() {
+ {
+ add("com.android.tools.layoutlib.create.PackageProtectedClass");
+ }
+ });
+
+ reader.accept(adapter, 0);
+ assertTrue(log.mLog.contains("[visit] - version=52, access=[public], " +
+ "name=com/android/tools/layoutlib/create/PackageProtectedClass, signature=null, " +
+ "superName=java/lang/Object, interfaces=[]"));
+
+ }
+}
\ No newline at end of file