Merge "Use codegen to catch LinkageErrors from plugins" into main
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 682a68f..a26cf12 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -23,9 +23,7 @@
}
java_library {
-
name: "SystemUIPluginLib",
-
srcs: [
"bcsmartspace/src/**/*.java",
"bcsmartspace/src/**/*.kt",
@@ -40,6 +38,8 @@
export_proguard_flags_files: true,
},
+ plugins: ["PluginAnnotationProcessor"],
+
// If you add a static lib here, you may need to also add the package to the ClassLoaderFilter
// in PluginInstance. That will ensure that loaded plugins have access to the related classes.
// You should also add it to proguard_common.flags so that proguard does not remove the portions
@@ -53,7 +53,6 @@
"SystemUILogLib",
"androidx.annotation_annotation",
],
-
}
android_app {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 8dc4815..6d27b6f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -21,7 +21,11 @@
import com.android.internal.annotations.Keep
import com.android.systemui.log.core.MessageBuffer
import com.android.systemui.plugins.Plugin
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
import java.io.PrintWriter
import java.util.Locale
import java.util.TimeZone
@@ -31,6 +35,7 @@
typealias ClockId = String
/** A Plugin which exposes the ClockProvider interface */
+@ProtectedInterface
@ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
interface ClockProviderPlugin : Plugin, ClockProvider {
companion object {
@@ -40,31 +45,42 @@
}
/** Interface for building clocks and providing information about those clocks */
+@ProtectedInterface
+@GeneratedImport("java.util.List")
+@GeneratedImport("java.util.ArrayList")
interface ClockProvider {
/** Initializes the clock provider with debug log buffers */
fun initialize(buffers: ClockMessageBuffers?)
+ @ProtectedReturn("return new ArrayList<ClockMetadata>();")
/** Returns metadata for all clocks this provider knows about */
fun getClocks(): List<ClockMetadata>
+ @ProtectedReturn("return null;")
/** Initializes and returns the target clock design */
- fun createClock(settings: ClockSettings): ClockController
+ fun createClock(settings: ClockSettings): ClockController?
+ @ProtectedReturn("return new ClockPickerConfig(\"\", \"\", \"\", null);")
/** Settings configuration parameters for the clock */
fun getClockPickerConfig(id: ClockId): ClockPickerConfig
}
/** Interface for controlling an active clock */
+@ProtectedInterface
interface ClockController {
+ @get:SimpleProperty
/** A small version of the clock, appropriate for smaller viewports */
val smallClock: ClockFaceController
+ @get:SimpleProperty
/** A large version of the clock, appropriate when a bigger viewport is available */
val largeClock: ClockFaceController
+ @get:SimpleProperty
/** Determines the way the hosting app should behave when rendering either clock face */
val config: ClockConfig
+ @get:SimpleProperty
/** Events that clocks may need to respond to */
val events: ClockEvents
@@ -76,19 +92,26 @@
}
/** Interface for a specific clock face version rendered by the clock */
+@ProtectedInterface
interface ClockFaceController {
+ @get:SimpleProperty
+ @Deprecated("Prefer use of layout")
/** View that renders the clock face */
val view: View
+ @get:SimpleProperty
/** Layout specification for this clock */
val layout: ClockFaceLayout
+ @get:SimpleProperty
/** Determines the way the hosting app should behave when rendering this clock face */
val config: ClockFaceConfig
+ @get:SimpleProperty
/** Events specific to this clock face */
val events: ClockFaceEvents
+ @get:SimpleProperty
/** Triggers for various animations */
val animations: ClockAnimations
}
@@ -107,14 +130,21 @@
data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
-/** Specifies layout information for the */
+/** Specifies layout information for the clock face */
+@ProtectedInterface
+@GeneratedImport("java.util.ArrayList")
+@GeneratedImport("android.view.View")
interface ClockFaceLayout {
+ @get:ProtectedReturn("return new ArrayList<View>();")
/** All clock views to add to the root constraint layout before applying constraints. */
val views: List<View>
+ @ProtectedReturn("return constraints;")
/** Custom constraints to apply to Lockscreen ConstraintLayout. */
fun applyConstraints(constraints: ConstraintSet): ConstraintSet
+ @ProtectedReturn("return constraints;")
+ /** Custom constraints to apply to preview ConstraintLayout. */
fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet
fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
@@ -145,7 +175,9 @@
}
/** Events that should call when various rendering parameters change */
+@ProtectedInterface
interface ClockEvents {
+ @get:ProtectedReturn("return false;")
/** Set to enable or disable swipe interaction */
var isReactiveTouchInteractionEnabled: Boolean
@@ -187,6 +219,7 @@
)
/** Methods which trigger various clock animations */
+@ProtectedInterface
interface ClockAnimations {
/** Runs an enter animation (if any) */
fun enter()
@@ -230,6 +263,7 @@
}
/** Events that have specific data about the related face */
+@ProtectedInterface
interface ClockFaceEvents {
/** Call every time tick */
fun onTimeTick()
@@ -270,7 +304,9 @@
/** Some data about a clock design */
data class ClockMetadata(val clockId: ClockId)
-data class ClockPickerConfig(
+data class ClockPickerConfig
+@JvmOverloads
+constructor(
val id: String,
/** Localized name of the clock */
@@ -338,7 +374,7 @@
/** Transition to AOD should move smartspace like large clock instead of small clock */
val useAlternateSmartspaceAODTransition: Boolean = false,
- /** Use ClockPickerConfig.isReactiveToTone instead */
+ /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
@Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
val isReactiveToTone: Boolean = true,
diff --git a/packages/SystemUI/plugin_core/Android.bp b/packages/SystemUI/plugin_core/Android.bp
index 521c019..31fbda5 100644
--- a/packages/SystemUI/plugin_core/Android.bp
+++ b/packages/SystemUI/plugin_core/Android.bp
@@ -24,8 +24,13 @@
java_library {
sdk_version: "current",
- name: "PluginCoreLib",
- srcs: ["src/**/*.java"],
+ name: "PluginAnnotationLib",
+ host_supported: true,
+ device_supported: true,
+ srcs: [
+ "src/**/annotations/*.java",
+ "src/**/annotations/*.kt",
+ ],
optimize: {
proguard_flags_files: ["proguard.flags"],
// Ensure downstream clients that reference this as a shared lib
@@ -37,3 +42,59 @@
// no compatibility issues with launcher
java_version: "1.8",
}
+
+java_library {
+ sdk_version: "current",
+ name: "PluginCoreLib",
+ device_supported: true,
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ exclude_srcs: [
+ "src/**/annotations/*.java",
+ "src/**/annotations/*.kt",
+ "src/**/processor/*.java",
+ "src/**/processor/*.kt",
+ ],
+ static_libs: [
+ "PluginAnnotationLib",
+ ],
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ // Ensure downstream clients that reference this as a shared lib
+ // inherit the appropriate flags to preserve annotations.
+ export_proguard_flags_files: true,
+ },
+
+ // Enforce that the library is built against java 8 so that there are
+ // no compatibility issues with launcher
+ java_version: "1.8",
+}
+
+java_library {
+ java_version: "1.8",
+ name: "PluginAnnotationProcessorLib",
+ host_supported: true,
+ device_supported: false,
+ srcs: [
+ "src/**/processor/*.java",
+ "src/**/processor/*.kt",
+ ],
+ plugins: ["auto_service_plugin"],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "auto_service_annotations",
+ "auto_common",
+ "PluginAnnotationLib",
+ "guava",
+ "jsr330",
+ ],
+}
+
+java_plugin {
+ name: "PluginAnnotationProcessor",
+ processor_class: "com.android.systemui.plugins.processor.ProtectedPluginProcessor",
+ static_libs: ["PluginAnnotationProcessorLib"],
+ java_version: "1.8",
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
index 8ff6c11..84040f9 100644
--- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
@@ -15,6 +15,7 @@
import android.content.Context;
+import com.android.systemui.plugins.annotations.ProtectedReturn;
import com.android.systemui.plugins.annotations.Requires;
/**
@@ -116,6 +117,8 @@
* @deprecated
* @see Requires
*/
+ @Deprecated
+ @ProtectedReturn(statement = "return -1;")
default int getVersion() {
// Default of -1 indicates the plugin supports the new Requires model.
return -1;
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt
new file mode 100644
index 0000000..425d00a
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.systemui.plugins
+
+/** Listener for events from proxy types generated by [ProtectedPluginProcessor]. */
+interface ProtectedPluginListener {
+ /**
+ * Called when a method call produces a [LinkageError] before returning. This callback is
+ * provided so that the host application can terminate the plugin or log the error as
+ * appropraite.
+ *
+ * @return true to terminate all methods within this object; false if the error is recoverable
+ * and the proxied plugin should continue to operate as normal.
+ */
+ fun onFail(className: String, methodName: String, failure: LinkageError): Boolean
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt
new file mode 100644
index 0000000..12a977d
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.systemui.plugins.annotations
+
+/**
+ * This annotation marks denotes that an interface should use a proxy layer to protect the plugin
+ * host from crashing due to [LinkageError]s originating within the plugin's implementation.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+annotation class ProtectedInterface
+
+/**
+ * This annotation specifies any additional imports that the processor will require when generating
+ * the proxy implementation for the target interface. The interface in question must still be
+ * annotated with [ProtectedInterface].
+ */
+@Repeatable
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+annotation class GeneratedImport(val extraImport: String)
+
+/**
+ * This annotation provides default values to return when the proxy implementation catches a
+ * [LinkageError]. The string specified should be a simple but valid java statement. In most cases
+ * it should be a return statement of the appropriate type, but in some cases throwing a known
+ * exception type may be preferred.
+ *
+ * This annotation is not required for methods that return void, but will behave the same way.
+ */
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ProtectedReturn(val statement: String)
+
+/**
+ * Some very simple properties and methods need not be protected by the proxy implementation. This
+ * annotation can be used to omit the normal try-catch wrapper the proxy is using. These members
+ * will instead be a direct passthrough.
+ *
+ * It should only be used for members where the plugin implementation is expected to be exceedingly
+ * simple. Any member marked with this annotation should be no more complex than kotlin's automatic
+ * properties, and make no other method calls whatsoever.
+ */
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class SimpleProperty
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
new file mode 100644
index 0000000..8266de5
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2024 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.systemui.plugins.processor
+
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+import com.android.systemui.plugins.annotations.SimpleProperty
+import com.google.auto.service.AutoService
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.PackageElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic.Kind
+import kotlin.collections.ArrayDeque
+
+/**
+ * [ProtectedPluginProcessor] generates a proxy implementation for interfaces annotated with
+ * [ProtectedInterface] which catches [LinkageError]s generated by the proxied target. This protects
+ * the plugin host from crashing due to out-of-date plugin code, where some call has changed so that
+ * the [ClassLoader] can no longer resolve it correctly.
+ *
+ * [PluginInstance] observes these failures via [ProtectedMethodListener] and unloads the plugin in
+ * question to prevent further issues. This persists through further load/unload requests.
+ *
+ * To centralize access to the proxy types, an additional type [PluginProtector] is also generated.
+ * This class provides static methods which wrap an instance of the target interface in the proxy
+ * type if it is not already an instance of the proxy.
+ */
+@AutoService(ProtectedPluginProcessor::class)
+class ProtectedPluginProcessor : AbstractProcessor() {
+ private lateinit var procEnv: ProcessingEnvironment
+
+ override fun init(procEnv: ProcessingEnvironment) {
+ this.procEnv = procEnv
+ }
+
+ override fun getSupportedAnnotationTypes(): Set<String> =
+ setOf("com.android.systemui.plugins.annotations.ProtectedInterface")
+
+ private data class TargetData(
+ val attribute: TypeElement,
+ val sourceType: Element,
+ val sourcePkg: String,
+ val sourceName: String,
+ val outputName: String,
+ )
+
+ override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
+ val targets = mutableMapOf<String, TargetData>() // keyed by fully-qualified source name
+ val additionalImports = mutableSetOf<String>()
+ for (attr in annotations) {
+ for (target in roundEnv.getElementsAnnotatedWith(attr)) {
+ val sourceName = "${target.simpleName}"
+ val outputName = "${sourceName}Protector"
+ val pkg = (target.getEnclosingElement() as PackageElement).qualifiedName.toString()
+ targets.put("$target", TargetData(attr, target, pkg, sourceName, outputName))
+
+ // This creates excessive imports, but it should be fine
+ additionalImports.add("$pkg.$sourceName")
+ additionalImports.add("$pkg.$outputName")
+ }
+ }
+
+ if (targets.size <= 0) return false
+ for ((_, sourceType, sourcePkg, sourceName, outputName) in targets.values) {
+ // Find all methods in this type and all super types to that need to be implemented
+ val types = ArrayDeque<TypeMirror>().apply { addLast(sourceType.asType()) }
+ val impAttrs = mutableListOf<GeneratedImport>()
+ val methods = mutableListOf<ExecutableElement>()
+ while (types.size > 0) {
+ val typeMirror = types.removeLast()
+ if (typeMirror.toString() == "java.lang.Object") continue
+ val type = procEnv.typeUtils.asElement(typeMirror)
+ for (member in type.enclosedElements) {
+ if (member.kind != ElementKind.METHOD) continue
+ methods.add(member as ExecutableElement)
+ }
+
+ impAttrs.addAll(type.getAnnotationsByType(GeneratedImport::class.java))
+ types.addAll(procEnv.typeUtils.directSupertypes(typeMirror))
+ }
+
+ val file = procEnv.filer.createSourceFile("$outputName")
+ TabbedWriter.writeTo(file.openWriter()) {
+ line("package $sourcePkg;")
+ line()
+
+ // Imports used by the proxy implementation
+ line("import android.util.Log;")
+ line("import java.lang.LinkageError;")
+ line("import com.android.systemui.plugins.ProtectedPluginListener;")
+ line()
+
+ // Imports of other generated types
+ if (additionalImports.size > 0) {
+ for (impTarget in additionalImports) {
+ line("import $impTarget;")
+ }
+ line()
+ }
+
+ // Imports declared via @GeneratedImport
+ if (impAttrs.size > 0) {
+ for (impAttr in impAttrs) {
+ line("import ${impAttr.extraImport};")
+ }
+ line()
+ }
+
+ braceBlock("public class $outputName implements $sourceName") {
+ line("private static final String CLASS = \"$sourceName\";")
+
+ // Static factory method to prevent wrapping the same object twice
+ parenBlock("public static $outputName protect") {
+ line("$sourceName instance,")
+ line("ProtectedPluginListener listener")
+ }
+ braceBlock {
+ line("if (instance instanceof $outputName)")
+ line(" return ($outputName)instance;")
+ line("return new $outputName(instance, listener);")
+ }
+ line()
+
+ // Member Fields
+ line("private $sourceName mInstance;")
+ line("private ProtectedPluginListener mListener;")
+ line("private boolean mHasError = false;")
+ line()
+
+ // Constructor
+ parenBlock("private $outputName") {
+ line("$sourceName instance,")
+ line("ProtectedPluginListener listener")
+ }
+ braceBlock {
+ line("mInstance = instance;")
+ line("mListener = listener;")
+ }
+ line()
+
+ // Method implementations
+ for (method in methods) {
+ val methodName = method.simpleName
+ val returnTypeName = method.returnType.toString()
+ val callArgs = StringBuilder()
+ var isFirst = true
+
+ line("@Override")
+ parenBlock("public $returnTypeName $methodName") {
+ // While copying the method signature for the proxy type, we also
+ // accumulate arguments for the nested callsite.
+ for (param in method.parameters) {
+ if (!isFirst) completeLine(",")
+ startLine("${param.asType()} ${param.simpleName}")
+ isFirst = false
+
+ if (callArgs.length > 0) callArgs.append(", ")
+ callArgs.append(param.simpleName)
+ }
+ }
+
+ val isVoid = method.returnType.kind == TypeKind.VOID
+ val nestedCall = "mInstance.$methodName($callArgs)"
+ val callStatement =
+ when {
+ isVoid -> "$nestedCall;"
+ targets.containsKey(returnTypeName) -> {
+ val targetType = targets.get(returnTypeName)!!.outputName
+ "return $targetType.protect($nestedCall, mListener);"
+ }
+ else -> "return $nestedCall;"
+ }
+
+ // Simple property methods forgo protection
+ val simpleAttr = method.getAnnotation(SimpleProperty::class.java)
+ if (simpleAttr != null) {
+ braceBlock {
+ line("final String METHOD = \"$methodName\";")
+ line(callStatement)
+ }
+ line()
+ continue
+ }
+
+ // Standard implementation wraps nested call in try-catch
+ braceBlock {
+ val retAttr = method.getAnnotation(ProtectedReturn::class.java)
+ val errorStatement =
+ when {
+ retAttr != null -> retAttr.statement
+ isVoid -> "return;"
+ else -> {
+ // Non-void methods must be annotated.
+ procEnv.messager.printMessage(
+ Kind.ERROR,
+ "$outputName.$methodName must be annotated with " +
+ "@ProtectedReturn or @SimpleProperty",
+ )
+ "throw ex;"
+ }
+ }
+
+ line("final String METHOD = \"$methodName\";")
+
+ // Return immediately if any previous call has failed.
+ braceBlock("if (mHasError)") { line(errorStatement) }
+
+ // Protect callsite in try/catch block
+ braceBlock("try") { line(callStatement) }
+
+ // Notify listener when a LinkageError is caught
+ braceBlock("catch (LinkageError ex)") {
+ line("Log.wtf(CLASS, \"Failed to execute: \" + METHOD, ex);")
+ line("mHasError = mListener.onFail(CLASS, METHOD, ex);")
+ line(errorStatement)
+ }
+ }
+ line()
+ }
+ }
+ }
+ }
+
+ // Write a centralized static factory type to its own file. This is for convience so that
+ // PluginInstance need not resolve each generated type at runtime as plugins are loaded.
+ val factoryFile = procEnv.filer.createSourceFile("PluginProtector")
+ TabbedWriter.writeTo(factoryFile.openWriter()) {
+ line("package com.android.systemui.plugins;")
+ line()
+
+ line("import java.util.Map;")
+ line("import java.util.ArrayList;")
+ line("import java.util.HashSet;")
+ line("import static java.util.Map.entry;")
+ line()
+
+ for (impTarget in additionalImports) {
+ line("import $impTarget;")
+ }
+ line()
+
+ braceBlock("public final class PluginProtector") {
+ line("private PluginProtector() { }")
+ line()
+
+ // Untyped factory SAM, private to this type.
+ braceBlock("private interface Factory") {
+ line("Object create(Object plugin, ProtectedPluginListener listener);")
+ }
+ line()
+
+ // Store a reference to each `protect` method in a map by interface type.
+ parenBlock("private static final Map<Class, Factory> sFactories = Map.ofEntries") {
+ var isFirst = true
+ for (target in targets.values) {
+ if (!isFirst) completeLine(",")
+ target.apply {
+ startLine("entry($sourceName.class, ")
+ appendLine("(p, h) -> $outputName.protect(($sourceName)p, h))")
+ }
+ isFirst = false
+ }
+ }
+ completeLine(";")
+ line()
+
+ // Lookup the relevant factory based on the instance type, if not found return null.
+ parenBlock("public static <T> T tryProtect") {
+ line("T target,")
+ line("ProtectedPluginListener listener")
+ }
+ braceBlock {
+ // Accumulate interfaces from type and all base types
+ line("HashSet<Class> interfaces = new HashSet<Class>();")
+ line("Class current = target.getClass();")
+ braceBlock("while (current != null)") {
+ braceBlock("for (Class cls : current.getInterfaces())") {
+ line("interfaces.add(cls);")
+ }
+ line("current = current.getSuperclass();")
+ }
+ line()
+
+ // Check if any of the interfaces are marked protectable
+ line("int candidateCount = 0;")
+ line("Factory candidateFactory = null;")
+ braceBlock("for (Class cls : interfaces)") {
+ line("Factory factory = sFactories.get(cls);")
+ braceBlock("if (factory != null)") {
+ line("candidateFactory = factory;")
+ line("candidateCount++;")
+ }
+ }
+ line()
+
+ // No match, return null
+ braceBlock("if (candidateFactory == null)") { line("return null;") }
+
+ // Multiple matches, not supported
+ braceBlock("if (candidateCount >= 2)") {
+ var error = "Plugin implements more than one protected interface"
+ line("throw new UnsupportedOperationException(\"$error\");")
+ }
+
+ // Call the factory and wrap the target object
+ line("return (T)candidateFactory.create(target, listener);")
+ }
+ line()
+
+ // Wraps the target with the appropriate generated proxy if it exists.
+ parenBlock("public static <T> T protectIfAble") {
+ line("T target,")
+ line("ProtectedPluginListener listener")
+ }
+ braceBlock {
+ line("T result = tryProtect(target, listener);")
+ line("return result != null ? result : target;")
+ }
+ line()
+ }
+ }
+
+ return true
+ }
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt
new file mode 100644
index 0000000..941b2c2
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 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.systemui.plugins.processor
+
+import java.io.BufferedWriter
+import java.io.Writer
+
+/**
+ * [TabbedWriter] is a convience class which tracks and writes correctly tabbed lines for generating
+ * source files. These files don't need to be correctly tabbed as they're ephemeral and not part of
+ * the source tree, but correct tabbing makes debugging much easier when the build fails.
+ */
+class TabbedWriter(writer: Writer) : AutoCloseable {
+ private val target = BufferedWriter(writer)
+ private var isInProgress = false
+ var tabCount: Int = 0
+ private set
+
+ override fun close() = target.close()
+
+ fun line() {
+ target.newLine()
+ isInProgress = false
+ }
+
+ fun line(str: String) {
+ if (isInProgress) {
+ target.newLine()
+ }
+
+ target.append(" ".repeat(tabCount))
+ target.append(str)
+ target.newLine()
+ isInProgress = false
+ }
+
+ fun completeLine(str: String) {
+ if (!isInProgress) {
+ target.newLine()
+ target.append(" ".repeat(tabCount))
+ }
+
+ target.append(str)
+ target.newLine()
+ isInProgress = false
+ }
+
+ fun startLine(str: String) {
+ if (isInProgress) {
+ target.newLine()
+ }
+
+ target.append(" ".repeat(tabCount))
+ target.append(str)
+ isInProgress = true
+ }
+
+ fun appendLine(str: String) {
+ if (!isInProgress) {
+ target.append(" ".repeat(tabCount))
+ }
+
+ target.append(str)
+ isInProgress = true
+ }
+
+ fun braceBlock(str: String = "", write: TabbedWriter.() -> Unit) {
+ block(str, " {", "}", true, write)
+ }
+
+ fun parenBlock(str: String = "", write: TabbedWriter.() -> Unit) {
+ block(str, "(", ")", false, write)
+ }
+
+ private fun block(
+ str: String,
+ start: String,
+ end: String,
+ newLineForEnd: Boolean,
+ write: TabbedWriter.() -> Unit,
+ ) {
+ appendLine(str)
+ completeLine(start)
+
+ tabCount++
+ this.write()
+ tabCount--
+
+ if (newLineForEnd) {
+ line(end)
+ } else {
+ startLine(end)
+ }
+ }
+
+ companion object {
+ fun writeTo(writer: Writer, write: TabbedWriter.() -> Unit) {
+ TabbedWriter(writer).use { it.write() }
+ }
+ }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
index 87cc86f..5a9e021 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -32,6 +32,8 @@
import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginProtector;
+import com.android.systemui.plugins.ProtectedPluginListener;
import dalvik.system.PathClassLoader;
@@ -49,7 +51,8 @@
*
* @param <T> The type of plugin that this contains.
*/
-public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
+public class PluginInstance<T extends Plugin>
+ implements PluginLifecycleManager, ProtectedPluginListener {
private static final String TAG = "PluginInstance";
private final Context mAppContext;
@@ -58,6 +61,7 @@
private final PluginFactory<T> mPluginFactory;
private final String mTag;
+ private boolean mHasError = false;
private BiConsumer<String, String> mLogConsumer = null;
private Context mPluginContext;
private T mPlugin;
@@ -87,6 +91,11 @@
return mTag;
}
+ /** */
+ public boolean hasError() {
+ return mHasError;
+ }
+
public void setLogFunc(BiConsumer logConsumer) {
mLogConsumer = logConsumer;
}
@@ -97,8 +106,21 @@
}
}
+ @Override
+ public synchronized boolean onFail(String className, String methodName, LinkageError failure) {
+ mHasError = true;
+ unloadPlugin();
+ mListener.onPluginDetached(this);
+ return true;
+ }
+
/** Alerts listener and plugin that the plugin has been created. */
public synchronized void onCreate() {
+ if (mHasError) {
+ log("Previous LinkageError detected for plugin class");
+ return;
+ }
+
boolean loadPlugin = mListener.onPluginAttached(this);
if (!loadPlugin) {
if (mPlugin != null) {
@@ -126,6 +148,12 @@
/** Alerts listener and plugin that the plugin is being shutdown. */
public synchronized void onDestroy() {
+ if (mHasError) {
+ // Detached in error handler
+ log("onDestroy - no-op");
+ return;
+ }
+
log("onDestroy");
unloadPlugin();
mListener.onPluginDetached(this);
@@ -134,20 +162,25 @@
/** Returns the current plugin instance (if it is loaded). */
@Nullable
public T getPlugin() {
- return mPlugin;
+ return mHasError ? null : mPlugin;
}
/**
* Loads and creates the plugin if it does not exist.
*/
public synchronized void loadPlugin() {
+ if (mHasError) {
+ log("Previous LinkageError detected for plugin class");
+ return;
+ }
+
if (mPlugin != null) {
log("Load request when already loaded");
return;
}
// Both of these calls take about 1 - 1.5 seconds in test runs
- mPlugin = mPluginFactory.createPlugin();
+ mPlugin = mPluginFactory.createPlugin(this);
mPluginContext = mPluginFactory.createPluginContext();
if (mPlugin == null || mPluginContext == null) {
Log.e(mTag, "Requested load, but failed");
@@ -364,20 +397,16 @@
}
/** Creates the related plugin object from the factory */
- public T createPlugin() {
+ public T createPlugin(ProtectedPluginListener listener) {
try {
ClassLoader loader = mClassLoaderFactory.get();
Class<T> instanceClass = (Class<T>) Class.forName(
mComponentName.getClassName(), true, loader);
T result = (T) mInstanceFactory.create(instanceClass);
Log.v(TAG, "Created plugin: " + result);
- return result;
- } catch (ClassNotFoundException ex) {
- Log.e(TAG, "Failed to load plugin", ex);
- } catch (IllegalAccessException ex) {
- Log.e(TAG, "Failed to load plugin", ex);
- } catch (InstantiationException ex) {
- Log.e(TAG, "Failed to load plugin", ex);
+ return PluginProtector.protectIfAble(result, listener);
+ } catch (ReflectiveOperationException ex) {
+ Log.wtf(TAG, "Failed to load plugin", ex);
}
return null;
}
@@ -397,7 +426,7 @@
/** Check Version and create VersionInfo for instance */
public VersionInfo checkVersion(T instance) {
if (instance == null) {
- instance = createPlugin();
+ instance = createPlugin(null);
}
return mVersionChecker.checkVersion(
(Class<T>) instance.getClass(), mPluginClass, instance);