diff options
9 files changed, 698 insertions, 20 deletions
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 682a68fdf846..a26cf1232636 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -23,9 +23,7 @@ package { } java_library { - name: "SystemUIPluginLib", - srcs: [ "bcsmartspace/src/**/*.java", "bcsmartspace/src/**/*.kt", @@ -40,6 +38,8 @@ java_library { 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 @@ java_library { "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 8dc4815b6f57..6d27b6f9637b 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 androidx.constraintlayout.widget.ConstraintSet 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 @@ import org.json.JSONObject 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 ClockProviderPlugin : Plugin, ClockProvider { } /** 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 ClockController { } /** 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 ClockMessageBuffers( 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 @@ class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { } /** 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 @@ data class ClockReactiveSetting( ) /** Methods which trigger various clock animations */ +@ProtectedInterface interface ClockAnimations { /** Runs an enter animation (if any) */ fun enter() @@ -230,6 +263,7 @@ interface ClockAnimations { } /** Events that have specific data about the related face */ +@ProtectedInterface interface ClockFaceEvents { /** Call every time tick */ fun onTimeTick() @@ -270,7 +304,9 @@ enum class ClockTickRate(val value: Int) { /** 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 @@ data class ClockConfig( /** 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 521c019d74f3..31fbda557279 100644 --- a/packages/SystemUI/plugin_core/Android.bp +++ b/packages/SystemUI/plugin_core/Android.bp @@ -24,8 +24,42 @@ package { java_library { sdk_version: "current", + 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 + // 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 { + sdk_version: "current", name: "PluginCoreLib", - srcs: ["src/**/*.java"], + 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 @@ -37,3 +71,30 @@ java_library { // 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 8ff6c114dded..84040f984ec5 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 @@ package com.android.systemui.plugins; import android.content.Context; +import com.android.systemui.plugins.annotations.ProtectedReturn; import com.android.systemui.plugins.annotations.Requires; /** @@ -116,6 +117,8 @@ public interface Plugin { * @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 000000000000..425d00abb899 --- /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 000000000000..12a977d9350e --- /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 000000000000..8266de54d557 --- /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 000000000000..941b2c2db4c1 --- /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 87cc86f18fdc..5a9e0215d71a 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.Plugin; 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 @@ import java.util.function.Supplier; * * @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 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager 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 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager return mTag; } + /** */ + public boolean hasError() { + return mHasError; + } + public void setLogFunc(BiConsumer logConsumer) { mLogConsumer = logConsumer; } @@ -97,8 +106,21 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager } } + @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 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager /** 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 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager /** 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 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager } /** 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 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager /** 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); |