summaryrefslogtreecommitdiff
path: root/layoutlib
diff options
context:
space:
mode:
authorJerome Gaillard <jgaillard@google.com>2021-05-04 19:30:56 +0100
committerJerome Gaillard <jgaillard@google.com>2021-05-10 13:56:53 +0000
commit7ee3b509fe7fccc27948bc68d2ceec397d4d810e (patch)
tree55c66c0848a3941725c4c02eee27ca94749ae42a /layoutlib
parent2c8b7c55cd16e88c58b24e9ad5d995f90ac1424b (diff)
downloadidea-7ee3b509fe7fccc27948bc68d2ceec397d4d810e.tar.gz
Create a design tools plugin
This new plugin is contains: - the designer module - the nav-editor module - the compose-designer module - the customview module - the layoutlib module - the layoutlib jar That corresponds to everything that needs direct access to layoutlib classes. It depends on the android plugin, and implements a new extension point, layoutLibraryProvider, defined in LayoutLibraryLoader. That allows the android plugin to still have access to a LayoutLibrary, while preventing it from accessing the internals of layoutlib. Bug: 187316741 Test: existing tests Change-Id: I264d82e14cb206fa7e1443cb85a0e2bc1f79c200
Diffstat (limited to 'layoutlib')
-rw-r--r--layoutlib/BUILD15
-rw-r--r--layoutlib/intellij.android.layoutlib.iml1
-rw-r--r--layoutlib/intellij.android.layoutlib.tests.iml14
-rw-r--r--layoutlib/src/META-INF/layoutlib.xml (renamed from layoutlib/src/META-INF/plugin.xml)12
-rw-r--r--layoutlib/src/com/android/layoutlib/LayoutlibClassLoader.java108
-rw-r--r--layoutlib/src/com/android/layoutlib/LayoutlibProvider.kt30
-rw-r--r--layoutlib/testSrc/com/android/layoutlib/LayoutlibClassLoaderTest.java87
-rw-r--r--layoutlib/testSrc/com/android/layoutlib/LayoutlibPrebuiltTest.kt47
-rw-r--r--layoutlib/testSrc/com/android/layoutlib/TestBuild.java41
9 files changed, 347 insertions, 8 deletions
diff --git a/layoutlib/BUILD b/layoutlib/BUILD
index 1aefed9ec93..7e56559133a 100644
--- a/layoutlib/BUILD
+++ b/layoutlib/BUILD
@@ -10,6 +10,21 @@ iml_module(
deps = [
"//prebuilts/studio/intellij-sdk:studio-sdk",
"//tools/base/layoutlib-api:studio.android.sdktools.layoutlib-api[module]",
+ "//tools/adt/idea/layoutlib-loader:intellij.android.layoutlib-loader[module]",
"//tools/adt/idea/.idea/libraries:layoutlib",
],
)
+
+# managed by go/iml_to_build
+iml_module(
+ name = "intellij.android.layoutlib.tests",
+ iml_files = ["intellij.android.layoutlib.tests.iml"],
+ test_srcs = ["testSrc"],
+ visibility = ["//visibility:public"],
+ # do not sort: must match IML order
+ deps = [
+ "//prebuilts/studio/intellij-sdk:studio-sdk",
+ "//tools/adt/idea/layoutlib:intellij.android.layoutlib[module, test]",
+ "//tools/adt/idea/.idea/libraries:layoutlib[test]",
+ ],
+)
diff --git a/layoutlib/intellij.android.layoutlib.iml b/layoutlib/intellij.android.layoutlib.iml
index d6ed5c8a505..070e027727f 100644
--- a/layoutlib/intellij.android.layoutlib.iml
+++ b/layoutlib/intellij.android.layoutlib.iml
@@ -9,6 +9,7 @@
<orderEntry type="library" name="studio-sdk" level="project" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="android.sdktools.layoutlib-api" />
+ <orderEntry type="module" module-name="intellij.android.layoutlib-loader" />
<orderEntry type="library" name="layoutlib" level="project" />
</component>
</module> \ No newline at end of file
diff --git a/layoutlib/intellij.android.layoutlib.tests.iml b/layoutlib/intellij.android.layoutlib.tests.iml
new file mode 100644
index 00000000000..a4ef8d3eeaa
--- /dev/null
+++ b/layoutlib/intellij.android.layoutlib.tests.iml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$/testSrc">
+ <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="studio-sdk" level="project" />
+ <orderEntry type="module" module-name="intellij.android.layoutlib" scope="TEST" />
+ <orderEntry type="library" scope="TEST" name="layoutlib" level="project" />
+ </component>
+</module> \ No newline at end of file
diff --git a/layoutlib/src/META-INF/plugin.xml b/layoutlib/src/META-INF/layoutlib.xml
index 5b0b1b975f7..3cb64d86951 100644
--- a/layoutlib/src/META-INF/plugin.xml
+++ b/layoutlib/src/META-INF/layoutlib.xml
@@ -1,5 +1,5 @@
<!--
- ~ Copyright (C) 2019 The Android Open Source Project
+ ~ Copyright (C) 2021 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.
@@ -14,12 +14,9 @@
~ limitations under the License.
-->
<idea-plugin>
- <id>com.android.layoutlib</id>
- <name>Layoutlib</name>
- <version>1.0</version>
- <vendor>Google</vendor>
-
- <description>Provides a library for rendering Android resources</description>
+ <extensions defaultExtensionNs="com.android.tools.idea.layoutlib">
+ <layoutLibraryProvider implementation="com.android.layoutlib.LayoutlibProvider"/>
+ </extensions>
<application-components>
<component>
@@ -27,5 +24,4 @@
<headless-implementation-class/>
</component>
</application-components>
-
</idea-plugin> \ No newline at end of file
diff --git a/layoutlib/src/com/android/layoutlib/LayoutlibClassLoader.java b/layoutlib/src/com/android/layoutlib/LayoutlibClassLoader.java
new file mode 100644
index 00000000000..35790f71062
--- /dev/null
+++ b/layoutlib/src/com/android/layoutlib/LayoutlibClassLoader.java
@@ -0,0 +1,108 @@
+/*
+ * 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.layoutlib;
+
+import android.os._Original_Build;
+import com.google.common.annotations.VisibleForTesting;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.org.objectweb.asm.ClassReader;
+import org.jetbrains.org.objectweb.asm.ClassWriter;
+import org.jetbrains.org.objectweb.asm.commons.ClassRemapper;
+import org.jetbrains.org.objectweb.asm.commons.Remapper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.function.BiConsumer;
+
+/**
+ * {@link ClassLoader} used for Layoutlib. Currently it only generates {@code android.os.Build} dynamically by copying the class in
+ * {@link _Original_Build}.
+ * By generating {@code android.os.Build} dynamically, we avoid to have it in the classpath of the plugins. Some plugins check for the
+ * existence of the class in order to detect if they are running Android. This is just a workaround for that.
+ */
+public class LayoutlibClassLoader extends ClassLoader {
+ private static final Logger LOG = Logger.getInstance(LayoutlibClassLoader.class);
+
+ LayoutlibClassLoader(@NotNull ClassLoader parent) {
+ super(parent);
+
+ // Define the android.os.Build and all inner classes by renaming everything in android.os._Original_Build
+ generate(_Original_Build.class, (className, classBytes) -> defineClass(className, classBytes, 0, classBytes.length));
+ }
+
+ @NotNull
+ private static String toBinaryClassName(@NotNull String name) {
+ return name.replace('.', '/');
+ }
+
+ @NotNull
+ private static String toClassName(@NotNull String name) {
+ return name.replace('/', '.');
+ }
+
+ /**
+ * Creates a copy of the passed class, replacing its name with "android.os.Build".
+ */
+ @VisibleForTesting
+ static void generate(@NotNull Class<?> originalBuildClass, @NotNull BiConsumer<String, byte[]> defineClass) {
+ ClassLoader loader = originalBuildClass.getClassLoader();
+ String originalBuildClassName = originalBuildClass.getName();
+ String originalBuildBinaryClassName = toBinaryClassName(originalBuildClassName);
+ Deque<String> pendingClasses = new LinkedList<>();
+ pendingClasses.push(originalBuildClassName);
+
+ Remapper remapper = new Remapper() {
+ @Override
+ public String map(String typeName) {
+ if (typeName.startsWith(originalBuildBinaryClassName)) {
+ return "android/os/Build" + StringUtil.trimStart(typeName, originalBuildBinaryClassName);
+ }
+
+ return typeName;
+ }
+ };
+
+ while (!pendingClasses.isEmpty()) {
+ String name = pendingClasses.pop();
+
+ String newName = "android.os.Build" + StringUtil.trimStart(name, originalBuildClassName);
+ String binaryName = toBinaryClassName(name);
+
+ try (InputStream is = loader.getResourceAsStream(binaryName + ".class")) {
+ ClassWriter writer = new ClassWriter(0);
+ ClassReader reader = new ClassReader(is);
+ ClassRemapper classRemapper = new ClassRemapper(writer, remapper) {
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ if (outerName != null && outerName.startsWith(binaryName)) {
+ pendingClasses.push(toClassName(name));
+ }
+ super.visitInnerClass(name, outerName, innerName, access);
+ }
+ };
+ reader.accept(classRemapper, 0);
+ defineClass.accept(newName, writer.toByteArray());
+ }
+ catch (IOException e) {
+ LOG.warn("Unable to define android.os.Build", e);
+ }
+ }
+ }
+}
diff --git a/layoutlib/src/com/android/layoutlib/LayoutlibProvider.kt b/layoutlib/src/com/android/layoutlib/LayoutlibProvider.kt
new file mode 100644
index 00000000000..93d531ee89f
--- /dev/null
+++ b/layoutlib/src/com/android/layoutlib/LayoutlibProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 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.layoutlib
+
+import com.android.layoutlib.bridge.Bridge
+import com.android.tools.idea.layoutlib.LayoutLibrary
+import com.android.tools.idea.layoutlib.LayoutLibraryLoader
+
+class LayoutlibProvider : LayoutLibraryLoader.LayoutLibraryProvider() {
+ override fun getLibrary(): LayoutLibrary {
+ return LayoutLibrary.load(Bridge(), LayoutlibClassLoader(LayoutlibProvider::class.java.classLoader))
+ }
+
+ override fun getFrameworkRClass(): Class<*> {
+ return com.android.internal.R::class.java
+ }
+} \ No newline at end of file
diff --git a/layoutlib/testSrc/com/android/layoutlib/LayoutlibClassLoaderTest.java b/layoutlib/testSrc/com/android/layoutlib/LayoutlibClassLoaderTest.java
new file mode 100644
index 00000000000..f7919e750be
--- /dev/null
+++ b/layoutlib/testSrc/com/android/layoutlib/LayoutlibClassLoaderTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.layoutlib;
+
+import com.google.common.io.CharSource;
+import org.jetbrains.org.objectweb.asm.ClassReader;
+import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class LayoutlibClassLoaderTest {
+
+ /**
+ * Simplify the output from the ASM Textifier so we do not get the comments or Opcodes into the output
+ */
+ private static String simplify(String s) {
+ try {
+ return CharSource.wrap(s).readLines().stream().filter(l -> !l.trim().isEmpty() && !l.startsWith(" ") && !l.trim().startsWith("//"))
+ .collect(Collectors.joining("\n"));
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+
+ @Test
+ public void generateBuildFile() {
+ Map<String, String> definedClasses = new HashMap<>();
+ LayoutlibClassLoader.generate(TestBuild.class, (name, classBytes) -> {
+ StringWriter writer = new StringWriter();
+ ClassReader reader = new ClassReader(classBytes);
+ TraceClassVisitor visitor = new TraceClassVisitor(new PrintWriter(writer));
+ reader.accept(visitor, 0);
+
+ definedClasses.put(name, simplify(writer.toString()));
+ });
+
+ Assert.assertEquals(3, definedClasses.size()); // Outer class + 2 inner classes
+ Assert.assertEquals("public class android/os/Build {\n" +
+ " public static INNERCLASS android/os/Build$InnerClass2 android/os/Build InnerClass2\n" +
+ " public static INNERCLASS android/os/Build$InnerClass android/os/Build InnerClass\n" +
+ " public final static Ljava/lang/String; TEST_FIELD = \"TestValue\"\n" +
+ " public <init>()V\n" +
+ " private static privateMethod()Ljava/lang/String;\n" +
+ " public static getSerial()Ljava/lang/String;\n" +
+ "}",
+ definedClasses.get("android.os.Build"));
+
+ Assert.assertEquals("public class android/os/Build$InnerClass {\n" +
+ " public static INNERCLASS android/os/Build$InnerClass android/os/Build InnerClass\n" +
+ " public final static Ljava/lang/String; TEST_INNER_FIELD = \"TestInnerValue\"\n" +
+ " public final static I INNER_VALUE = 1\n" +
+ " public <init>()V\n" +
+ "}",
+ definedClasses.get("android.os.Build$InnerClass"));
+
+ Assert.assertEquals("public class android/os/Build$InnerClass2 {\n" +
+ " public static INNERCLASS android/os/Build$InnerClass2 android/os/Build InnerClass2\n" +
+ " public final static Ljava/lang/String; TEST_INNER_FIELD2\n" +
+ " public <init>()V\n" +
+ " static <clinit>()V\n" +
+ "}",
+ definedClasses.get("android.os.Build$InnerClass2"));
+ }
+} \ No newline at end of file
diff --git a/layoutlib/testSrc/com/android/layoutlib/LayoutlibPrebuiltTest.kt b/layoutlib/testSrc/com/android/layoutlib/LayoutlibPrebuiltTest.kt
new file mode 100644
index 00000000000..9d646de7e1c
--- /dev/null
+++ b/layoutlib/testSrc/com/android/layoutlib/LayoutlibPrebuiltTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 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.layoutlib
+
+import com.android.layoutlib.bridge.BridgeConstants
+import org.junit.Test
+import java.net.URL
+import java.util.jar.JarFile
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class LayoutPrebuiltTest {
+ // Regression test for b/109738602
+ @Test
+ fun jarContents() {
+ val classUrl = BridgeConstants::class.java.getResource(BridgeConstants::class.simpleName + ".class")
+ assertEquals("jar", classUrl.protocol)
+ val jarUrl = URL(classUrl.path.substringBefore("!"))
+ val jarFile = JarFile(jarUrl.file)
+ val jarEntryNames = jarFile.entries().asSequence()
+ .map { it.name }
+ .toSet()
+
+ // Sanity check to make sure the file contains some data
+ assertTrue(jarEntryNames.contains("android/R.class"))
+ assertTrue(jarEntryNames.contains("android/R\$layout.class"))
+
+ // Check that the jar does not contain classes in sun.** or java.**zs
+ assertFalse(jarEntryNames.any {
+ it.startsWith("sun/") || it.startsWith("java/")
+ })
+ }
+} \ No newline at end of file
diff --git a/layoutlib/testSrc/com/android/layoutlib/TestBuild.java b/layoutlib/testSrc/com/android/layoutlib/TestBuild.java
new file mode 100644
index 00000000000..5b03c78381b
--- /dev/null
+++ b/layoutlib/testSrc/com/android/layoutlib/TestBuild.java
@@ -0,0 +1,41 @@
+/*
+ * 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.layoutlib;
+
+public class TestBuild {
+ public static final String TEST_FIELD = "TestValue";
+
+ private static String privateMethod() {
+ return "SerialNumber";
+ }
+
+ public static String getSerial() {
+ return "#" + privateMethod();
+ }
+
+ public static class InnerClass {
+ public static final String TEST_INNER_FIELD = "TestInnerValue";
+ public static final int INNER_VALUE = 1;
+ }
+
+ public static class InnerClass2 {
+ public static final String TEST_INNER_FIELD2;
+
+ static {
+ TEST_INNER_FIELD2 = "TestInnerValue2";
+ }
+ }
+}