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);