OSDN Git Service

Add ability to promote classes to public visibility
authorDiego Perez <diegoperez@google.com>
Tue, 24 Jan 2017 15:55:53 +0000 (15:55 +0000)
committerDiego Perez <diegoperez@google.com>
Thu, 2 Feb 2017 10:34:38 +0000 (10:34 +0000)
Test: Added new PromoteClassClassAdapterTest
Change-Id: I30f9ee259d39e2b2768c1ceb45aa2161983c5a5e
(cherry picked from commit 294f0850f7623737899c9ea0b03cebc2cf7e4176)

tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java
tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java
tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteClassClassAdapter.java [new file with mode: 0644]
tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java
tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java
tools/layoutlib/create/tests/com/android/tools/layoutlib/create/PromoteClassClassAdapterTest.java [new file with mode: 0644]

index a2f8372..bed5806 100644 (file)
@@ -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<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 @@ 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();
     }
 
index 741eb27..94302d3 100644 (file)
@@ -113,6 +113,11 @@ public final class CreateInfo implements ICreateInfo {
     }
 
     @Override
+    public String[] getPromotedClasses() {
+        return PROMOTED_CLASSES;
+    }
+
+    @Override
     public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
         return INJECTED_METHODS;
     }
@@ -344,6 +349,13 @@ public final class CreateInfo implements ICreateInfo {
     };
 
     /**
+     * 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
index 535a9a8..48abde4 100644 (file)
@@ -78,6 +78,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.
      * 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 (file)
index 0000000..99e3089
--- /dev/null
@@ -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);
+    }
+}
index 05af033..ba77860 100644 (file)
@@ -31,7 +31,7 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
 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 @@ 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);
index 0560d8a..4d5d5d2 100644 (file)
@@ -137,6 +137,11 @@ public class AsmGeneratorTest {
             }
 
             @Override
+            public String[] getPromotedClasses() {
+                return EMPTY_STRING_ARRAY;
+            }
+
+            @Override
             public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
                 return Collections.emptyMap();
             }
@@ -211,6 +216,11 @@ public class AsmGeneratorTest {
             }
 
             @Override
+            public String[] getPromotedClasses() {
+                return EMPTY_STRING_ARRAY;
+            }
+
+            @Override
             public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
                 return Collections.emptyMap();
             }
@@ -293,6 +303,11 @@ public class AsmGeneratorTest {
             }
 
             @Override
+            public String[] getPromotedClasses() {
+                return EMPTY_STRING_ARRAY;
+            }
+
+            @Override
             public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
                 return Collections.emptyMap();
             }
@@ -370,6 +385,11 @@ public class AsmGeneratorTest {
             }
 
             @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 (file)
index 0000000..eeb0b10
--- /dev/null
@@ -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