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 a2f8372d6eb80..bed5806aadad2 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.Set; 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 @@ public class AsmGenerator { private final Map mInjectedMethodsMap; /** A map { FQCN => set { field names } } which should be promoted to public visibility */ private final Map> mPromotedFields; + /** A list of classes to be promoted to public visibility */ + private final Set mPromotedClasses; /** * Creates a new generator that can generate the output JAR with the stubbed classes. @@ -179,6 +182,9 @@ public class AsmGenerator { addToMap(createInfo.getPromotedFields(), mPromotedFields); mInjectedMethodsMap = createInfo.getInjectedMethodsMap(); + + mPromotedClasses = + Arrays.stream(createInfo.getPromotedClasses()).collect(Collectors.toSet()); } /** @@ -400,7 +406,11 @@ public class AsmGenerator { 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 741eb27558ed4..94302d328313b 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 @@ -112,6 +112,11 @@ public final class CreateInfo implements ICreateInfo { return PROMOTED_FIELDS; } + @Override + public String[] getPromotedClasses() { + return PROMOTED_CLASSES; + } + @Override public Map getInjectedMethodsMap() { return INJECTED_METHODS; @@ -343,6 +348,13 @@ public final class CreateInfo implements ICreateInfo { "android.graphics.FontFamily#mBuilderPtr" }; + /** + * 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 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 535a9a8c0b779..48abde4517e6a 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 @@ -77,6 +77,11 @@ public interface ICreateInfo { */ 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. 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 0000000000000..99e3089115d65 --- /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 mClassNames; + private static final int CLEAR_PRIVATE_MASK = ~(ACC_PRIVATE | ACC_PROTECTED); + + public PromoteClassClassAdapter(ClassVisitor cv, Set 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 05af0337a397f..ba778602546d1 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 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; public class PromoteFieldClassAdapter extends ClassVisitor { private final Set 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 fieldNames) { super(Main.ASM_VERSION, cv); @@ -43,7 +43,7 @@ public class PromoteFieldClassAdapter extends ClassVisitor { 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 0560d8aca1bd6..4d5d5d2c4a6e9 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 @@ -136,6 +136,11 @@ public class AsmGeneratorTest { return EMPTY_STRING_ARRAY; } + @Override + public String[] getPromotedClasses() { + return EMPTY_STRING_ARRAY; + } + @Override public Map getInjectedMethodsMap() { return Collections.emptyMap(); @@ -210,6 +215,11 @@ public class AsmGeneratorTest { return EMPTY_STRING_ARRAY; } + @Override + public String[] getPromotedClasses() { + return EMPTY_STRING_ARRAY; + } + @Override public Map getInjectedMethodsMap() { return Collections.emptyMap(); @@ -292,6 +302,11 @@ public class AsmGeneratorTest { return EMPTY_STRING_ARRAY; } + @Override + public String[] getPromotedClasses() { + return EMPTY_STRING_ARRAY; + } + @Override public Map getInjectedMethodsMap() { return Collections.emptyMap(); @@ -369,6 +384,11 @@ public class AsmGeneratorTest { return EMPTY_STRING_ARRAY; } + @Override + public String[] getPromotedClasses() { + return EMPTY_STRING_ARRAY; + } + @Override public Map getInjectedMethodsMap() { return Collections.singletonMap("mock_android.util.EmptyArray", 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 0000000000000..eeb0b10c1e2b4 --- /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 mLog = new LinkedList(); + + 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() { + { + 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() { + { + 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