diff options
author | 2025-01-21 15:41:20 +0800 | |
---|---|---|
committer | 2025-01-23 21:20:03 +0800 | |
commit | 5a87bd9aa9aae67e376acd27a2b30dd89a7473ea (patch) | |
tree | 46c4d2e41a1595329d60f0f775d8df4841736ac8 | |
parent | 76666b308c7f889293ec2f09d6e5bcb53d7dc6e0 (diff) |
[Catalyst] Support parameterized screens
Bug: 388420844
Flag: com.android.settings.flags.catalyst
Test: devtool
Change-Id: Iceca5d5b58187708156bbda8927e1622679d12ba
16 files changed, 375 insertions, 103 deletions
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index 33a7df4c6ba8..a834947144a0 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -26,6 +26,14 @@ message PreferenceScreenProto { optional PreferenceGroupProto root = 2; // If the preference screen provides complete hierarchy by source code. optional bool complete_hierarchy = 3; + // Parameterized screens (not recursive, provided on the top level only) + repeated ParameterizedPreferenceScreenProto parameterized_screens = 4; +} + +// Proto of parameterized preference screen +message ParameterizedPreferenceScreenProto { + optional BundleProto args = 1; + optional PreferenceScreenProto screen = 2; } // Proto of PreferenceGroup. diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt index adffd206d552..27ce1c7246e6 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt @@ -18,12 +18,14 @@ package com.android.settingslib.graph import android.app.Application import android.os.Bundle +import android.os.Parcelable import android.os.SystemClock import com.android.settingslib.graph.proto.PreferenceGraphProto import com.android.settingslib.ipc.ApiHandler import com.android.settingslib.ipc.ApiPermissionChecker import com.android.settingslib.ipc.MessageCodec import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger +import com.android.settingslib.metadata.PreferenceScreenCoordinate import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.preference.PreferenceScreenProvider import java.util.Locale @@ -59,10 +61,9 @@ class GetPreferenceGraphApiHandler( var success = false try { val builder = PreferenceGraphBuilder.of(application, callingPid, callingUid, request) - if (request.screenKeys.isEmpty()) { - PreferenceScreenRegistry.preferenceScreenMetadataFactories.forEachKeyAsync { - builder.addPreferenceScreenFromRegistry(it) - } + if (request.screens.isEmpty()) { + val factories = PreferenceScreenRegistry.preferenceScreenMetadataFactories + factories.forEachAsync { _, factory -> builder.addPreferenceScreen(factory) } for (provider in preferenceScreenProviders) { builder.addPreferenceScreenProvider(provider) } @@ -84,15 +85,15 @@ class GetPreferenceGraphApiHandler( /** * Request of [GetPreferenceGraphApiHandler]. * - * @param screenKeys screen keys of the preference graph - * @param visitedScreens keys of the visited preference screen + * @param screens screens of the preference graph + * @param visitedScreens visited preference screens * @param locale locale of the preference graph */ data class GetPreferenceGraphRequest @JvmOverloads constructor( - val screenKeys: Set<String> = setOf(), - val visitedScreens: Set<String> = setOf(), + val screens: Set<PreferenceScreenCoordinate> = setOf(), + val visitedScreens: Set<PreferenceScreenCoordinate> = setOf(), val locale: Locale? = null, val flags: Int = PreferenceGetterFlags.ALL, val includeValueDescriptor: Boolean = true, @@ -101,26 +102,32 @@ constructor( object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest> { override fun encode(data: GetPreferenceGraphRequest): Bundle = Bundle(4).apply { - putStringArray(KEY_SCREEN_KEYS, data.screenKeys.toTypedArray()) - putStringArray(KEY_VISITED_KEYS, data.visitedScreens.toTypedArray()) + putParcelableArray(KEY_SCREENS, data.screens.toTypedArray()) + putParcelableArray(KEY_VISITED_SCREENS, data.visitedScreens.toTypedArray()) putString(KEY_LOCALE, data.locale?.toLanguageTag()) putInt(KEY_FLAGS, data.flags) } + @Suppress("DEPRECATION") override fun decode(data: Bundle): GetPreferenceGraphRequest { - val screenKeys = data.getStringArray(KEY_SCREEN_KEYS) ?: arrayOf() - val visitedScreens = data.getStringArray(KEY_VISITED_KEYS) ?: arrayOf() + data.classLoader = PreferenceScreenCoordinate::class.java.classLoader + val screens = data.getParcelableArray(KEY_SCREENS) ?: arrayOf() + val visitedScreens = data.getParcelableArray(KEY_VISITED_SCREENS) ?: arrayOf() fun String?.toLocale() = if (this != null) Locale.forLanguageTag(this) else null + fun Array<Parcelable>.toScreenCoordinates() = + buildSet(size) { + for (element in this@toScreenCoordinates) add(element as PreferenceScreenCoordinate) + } return GetPreferenceGraphRequest( - screenKeys.toSet(), - visitedScreens.toSet(), + screens.toScreenCoordinates(), + visitedScreens.toScreenCoordinates(), data.getString(KEY_LOCALE).toLocale(), data.getInt(KEY_FLAGS), ) } - private const val KEY_SCREEN_KEYS = "k" - private const val KEY_VISITED_KEYS = "v" + private const val KEY_SCREENS = "s" + private const val KEY_VISITED_SCREENS = "v" private const val KEY_LOCALE = "l" private const val KEY_FLAGS = "f" } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt index a9958b975fc6..1d4e2c9e1bef 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt @@ -26,6 +26,7 @@ import com.android.settingslib.ipc.ApiPermissionChecker import com.android.settingslib.metadata.PreferenceCoordinate import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger +import com.android.settingslib.metadata.PreferenceScreenCoordinate import com.android.settingslib.metadata.PreferenceScreenRegistry /** @@ -105,8 +106,10 @@ class PreferenceGetterApiHandler( val errors = mutableMapOf<PreferenceCoordinate, Int>() val preferences = mutableMapOf<PreferenceCoordinate, PreferenceProto>() val flags = request.flags - for ((screenKey, coordinates) in request.preferences.groupBy { it.screenKey }) { - val screenMetadata = PreferenceScreenRegistry.create(application, screenKey) + val groups = + request.preferences.groupBy { PreferenceScreenCoordinate(it.screenKey, it.args) } + for ((screen, coordinates) in groups) { + val screenMetadata = PreferenceScreenRegistry.create(application, screen) if (screenMetadata == null) { val latencyMs = SystemClock.elapsedRealtime() - elapsedRealtime for (coordinate in coordinates) { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index c0d244989044..4290437b0d02 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -40,6 +40,7 @@ import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget import com.android.settingslib.graph.proto.PreferenceScreenProto import com.android.settingslib.graph.proto.TextProto +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.IntRangeValuePreference import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceAvailabilityProvider @@ -47,7 +48,10 @@ import com.android.settingslib.metadata.PreferenceHierarchy import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider +import com.android.settingslib.metadata.PreferenceScreenCoordinate import com.android.settingslib.metadata.PreferenceScreenMetadata +import com.android.settingslib.metadata.PreferenceScreenMetadataFactory +import com.android.settingslib.metadata.PreferenceScreenMetadataParameterizedFactory import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceTitleProvider @@ -72,15 +76,19 @@ private constructor( PreferenceScreenFactory(context.ofLocale(request.locale)) } private val builder by lazy { PreferenceGraphProto.newBuilder() } - private val visitedScreens = mutableSetOf<String>().apply { addAll(request.visitedScreens) } + private val visitedScreens = request.visitedScreens.toMutableSet() + private val screens = mutableMapOf<String, PreferenceScreenProto.Builder>() private suspend fun init() { - for (key in request.screenKeys) { - addPreferenceScreenFromRegistry(key) + for (screen in request.screens) { + PreferenceScreenRegistry.create(context, screen)?.let { addPreferenceScreen(it) } } } - fun build(): PreferenceGraphProto = builder.build() + fun build(): PreferenceGraphProto { + for ((key, screenBuilder) in screens) builder.putScreens(key, screenBuilder.build()) + return builder.build() + } /** * Adds an activity to the graph. @@ -138,19 +146,12 @@ private constructor( null } - suspend fun addPreferenceScreenFromRegistry(key: String): Boolean { - val metadata = PreferenceScreenRegistry.create(context, key) ?: return false - return addPreferenceScreenMetadata(metadata) + private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean { + val factory = + PreferenceScreenRegistry.preferenceScreenMetadataFactories[key] ?: return false + return addPreferenceScreen(factory) } - private suspend fun addPreferenceScreenMetadata(metadata: PreferenceScreenMetadata): Boolean = - addPreferenceScreen(metadata.key) { - preferenceScreenProto { - completeHierarchy = metadata.hasCompleteHierarchy() - root = metadata.getPreferenceHierarchy(context).toProto(metadata, true) - } - } - suspend fun addPreferenceScreenProvider(activityClass: Class<*>) { Log.d(TAG, "add $activityClass") createPreferenceScreen { activityClass.newInstance() } @@ -188,26 +189,52 @@ private constructor( Log.e(TAG, "\"$preferenceScreen\" has no key") return } - @Suppress("CheckReturnValue") addPreferenceScreen(key) { preferenceScreen.toProto(intent) } + val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) + @Suppress("CheckReturnValue") + addPreferenceScreen(key, args) { + this.intent = intent.toProto() + root = preferenceScreen.toProto() + } + } + + suspend fun addPreferenceScreen(factory: PreferenceScreenMetadataFactory): Boolean { + if (factory is PreferenceScreenMetadataParameterizedFactory) { + factory.parameters(context).collect { addPreferenceScreen(factory.create(context, it)) } + return true + } + return addPreferenceScreen(factory.create(context)) } + private suspend fun addPreferenceScreen(metadata: PreferenceScreenMetadata): Boolean = + addPreferenceScreen(metadata.key, metadata.arguments) { + completeHierarchy = metadata.hasCompleteHierarchy() + root = metadata.getPreferenceHierarchy(context).toProto(metadata, true) + } + private suspend fun addPreferenceScreen( key: String, - preferenceScreenProvider: suspend () -> PreferenceScreenProto, - ): Boolean = - if (visitedScreens.add(key)) { - builder.putScreens(key, preferenceScreenProvider()) - true - } else { - Log.w(TAG, "$key visited") - false + args: Bundle?, + init: suspend PreferenceScreenProto.Builder.() -> Unit, + ): Boolean { + if (!visitedScreens.add(PreferenceScreenCoordinate(key, args))) { + Log.w(TAG, "$key $args visited") + return false } - - private suspend fun PreferenceScreen.toProto(intent: Intent?): PreferenceScreenProto = - preferenceScreenProto { - intent?.let { this.intent = it.toProto() } - root = (this@toProto as PreferenceGroup).toProto() + if (args == null) { // normal screen + screens[key] = PreferenceScreenProto.newBuilder().also { init(it) } + } else if (args.isEmpty) { // parameterized screen with backward compatibility + val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() } + init(builder) + } else { // parameterized screen with non-empty arguments + val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() } + val parameterizedScreen = parameterizedPreferenceScreenProto { + setArgs(args.toProto()) + setScreen(PreferenceScreenProto.newBuilder().also { init(it) }) + } + builder.addParameterizedScreens(parameterizedScreen) } + return true + } private suspend fun PreferenceGroup.toProto(): PreferenceGroupProto = preferenceGroupProto { preference = (this@toProto as Preference).toProto() @@ -271,7 +298,7 @@ private constructor( .toProto(context, callingPid, callingUid, screenMetadata, isRoot, request.flags) .also { if (metadata is PreferenceScreenMetadata) { - @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata) + @Suppress("CheckReturnValue") addPreferenceScreen(metadata) } metadata.intent(context)?.resolveActivity(context.packageManager)?.let { if (it.packageName == context.packageName) { @@ -322,7 +349,7 @@ private constructor( val screenKey = screen?.key if (!screenKey.isNullOrEmpty()) { @Suppress("CheckReturnValue") - addPreferenceScreen(screenKey) { screen.toProto(null) } + addPreferenceScreen(screenKey, null) { root = screen.toProto() } return actionTargetProto { key = screenKey } } } catch (e: Exception) { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index a595f42a573d..60f9c6bb92a3 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -40,11 +40,12 @@ import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIV import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ -data class PreferenceSetterRequest( - val screenKey: String, - val key: String, +class PreferenceSetterRequest( + screenKey: String, + args: Bundle?, + key: String, val value: PreferenceValueProto, -) +) : PreferenceCoordinate(screenKey, args, key) /** Result of preference setter request. */ @IntDef( @@ -121,7 +122,7 @@ class PreferenceSetterApiHandler( metricsLogger?.logSetterApi( application, callingUid, - PreferenceCoordinate(request.screenKey, request.key), + request, null, null, PreferenceSetterResult.UNSUPPORTED, @@ -130,7 +131,7 @@ class PreferenceSetterApiHandler( return PreferenceSetterResult.UNSUPPORTED } val screenMetadata = - PreferenceScreenRegistry.create(application, request.screenKey) ?: return notFound() + PreferenceScreenRegistry.create(application, request) ?: return notFound() val key = request.key val metadata = screenMetadata.getPreferenceHierarchy(application).find(key) ?: return notFound() @@ -199,7 +200,7 @@ class PreferenceSetterApiHandler( metricsLogger?.logSetterApi( application, callingUid, - PreferenceCoordinate(request.screenKey, request.key), + request, screenMetadata, metadata, result, @@ -235,6 +236,7 @@ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { override fun encode(data: PreferenceSetterRequest) = Bundle(3).apply { putString(SCREEN_KEY, data.screenKey) + putBundle(ARGS, data.args) putString(KEY, data.key) putByteArray(null, data.value.toByteArray()) } @@ -242,10 +244,12 @@ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { override fun decode(data: Bundle) = PreferenceSetterRequest( data.getString(SCREEN_KEY)!!, + data.getBundle(ARGS), data.getString(KEY)!!, PreferenceValueProto.parseFrom(data.getByteArray(null)!!), ) private const val SCREEN_KEY = "s" private const val KEY = "k" + private const val ARGS = "a" } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt index adbe77318353..5f2a0d826407 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt @@ -19,6 +19,7 @@ package com.android.settingslib.graph import com.android.settingslib.graph.proto.BundleProto import com.android.settingslib.graph.proto.BundleProto.BundleValue import com.android.settingslib.graph.proto.IntentProto +import com.android.settingslib.graph.proto.ParameterizedPreferenceScreenProto import com.android.settingslib.graph.proto.PreferenceGroupProto import com.android.settingslib.graph.proto.PreferenceOrGroupProto import com.android.settingslib.graph.proto.PreferenceProto @@ -39,6 +40,12 @@ inline fun preferenceScreenProto( init: PreferenceScreenProto.Builder.() -> Unit ): PreferenceScreenProto = PreferenceScreenProto.newBuilder().also(init).build() +/** Kotlin DSL-style builder for [PreferenceScreenProto]. */ +inline fun parameterizedPreferenceScreenProto( + init: ParameterizedPreferenceScreenProto.Builder.() -> Unit +): ParameterizedPreferenceScreenProto = + ParameterizedPreferenceScreenProto.newBuilder().also(init).build() + /** Returns preference or null. */ val PreferenceOrGroupProto.preferenceOrNull get() = if (hasPreference()) preference else null diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt index 38b641336547..69b75adea9d3 100644 --- a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt +++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt @@ -33,6 +33,9 @@ import javax.tools.Diagnostic /** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { private val screens = mutableListOf<Screen>() + private val bundleType: TypeMirror by lazy { + processingEnv.elementUtils.getTypeElement("android.os.Bundle").asType() + } private val contextType: TypeMirror by lazy { processingEnv.elementUtils.getTypeElement("android.content.Context").asType() } @@ -83,19 +86,57 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this) return } - val constructorType = getConstructorType() - if (constructorType == null) { + fun reportConstructorError() = error( - "Class must be an object, or has single public constructor that " + - "accepts no parameter or a Context parameter", + "Must have only one public constructor: constructor(), " + + "constructor(Context), constructor(Bundle) or constructor(Context, Bundle)", this, ) + val constructor = findConstructor() + if (constructor == null || constructor.parameters.size > 2) { + reportConstructorError() return } + val constructorHasContextParameter = constructor.hasParameter(0, contextType) + var index = if (constructorHasContextParameter) 1 else 0 val annotation = annotationMirrors.single { it.isElement(annotationElement) } val key = annotation.fieldValue<String>("value")!! val overlay = annotation.fieldValue<Boolean>("overlay") == true - screens.add(Screen(key, overlay, qualifiedName.toString(), constructorType)) + val parameterized = annotation.fieldValue<Boolean>("parameterized") == true + var parametersHasContextParameter = false + if (parameterized) { + val parameters = findParameters() + if (parameters == null) { + error("require a static 'parameters()' or 'parameters(Context)' method", this) + return + } + parametersHasContextParameter = parameters + if (constructor.hasParameter(index, bundleType)) { + index++ + } else { + error( + "Parameterized screen constructor must be" + + "constructor(Bundle) or constructor(Context, Bundle)", + this, + ) + return + } + } + if (index == constructor.parameters.size) { + screens.add( + Screen( + key, + overlay, + parameterized, + annotation.fieldValue<Boolean>("parameterizedMigration") == true, + qualifiedName.toString(), + constructorHasContextParameter, + parametersHasContextParameter, + ) + ) + } else { + reportConstructorError() + } } private fun codegen() { @@ -116,10 +157,15 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { screens.sort() processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use { it.write("package $outputPkg;\n\n") + it.write("import android.content.Context;\n") + it.write("import android.os.Bundle;\n") it.write("import $PACKAGE.FixedArrayMap;\n") it.write("import $PACKAGE.FixedArrayMap.OrderedInitializer;\n") - it.write("import $PACKAGE.$FACTORY;\n\n") - it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n") + it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n") + it.write("import $PACKAGE.$FACTORY;\n") + it.write("import $PACKAGE.$PARAMETERIZED_FACTORY;\n") + it.write("import kotlinx.coroutines.flow.Flow;\n") + it.write("\n// Generated by annotation processor for @$ANNOTATION_NAME\n") it.write("public final class $outputClass {\n") it.write(" private $outputClass() {}\n\n") it.write(" public static FixedArrayMap<String, $FACTORY> $outputFun() {\n") @@ -127,10 +173,29 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { it.write(" return new FixedArrayMap<>($size, $outputClass::init);\n") it.write(" }\n\n") fun Screen.write() { - it.write(" screens.put(\"$key\", context -> new $klass(") - when (constructorType) { - ConstructorType.DEFAULT -> it.write("));") - ConstructorType.CONTEXT -> it.write("context));") + it.write(" screens.put(\"$key\", ") + if (parameterized) { + it.write("new $PARAMETERIZED_FACTORY() {\n") + it.write(" @Override public PreferenceScreenMetadata create") + it.write("(Context context, Bundle args) {\n") + it.write(" return new $klass(") + if (constructorHasContextParameter) it.write("context, ") + it.write("args);\n") + it.write(" }\n\n") + it.write(" @Override public Flow<Bundle> parameters(Context context) {\n") + it.write(" return $klass.parameters(") + if (parametersHasContextParameter) it.write("context") + it.write(");\n") + it.write(" }\n") + if (parameterizedMigration) { + it.write("\n @Override public boolean acceptEmptyArguments()") + it.write(" { return true; }\n") + } + it.write(" });") + } else { + it.write("context -> new $klass(") + if (constructorHasContextParameter) it.write("context") + it.write("));") } if (overlay) it.write(" // overlay") it.write("\n") @@ -159,7 +224,7 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { } private fun AnnotationMirror.isElement(element: TypeElement) = - processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType()) + annotationType.asElement().asType().isSameType(element.asType()) @Suppress("UNCHECKED_CAST") private fun <T> AnnotationMirror.fieldValue(name: String): T? = field(name)?.value as? T @@ -171,7 +236,7 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { return null } - private fun TypeElement.getConstructorType(): ConstructorType? { + private fun TypeElement.findConstructor(): ExecutableElement? { var constructor: ExecutableElement? = null for (element in enclosedElements) { if (element.kind != ElementKind.CONSTRUCTOR) continue @@ -179,16 +244,30 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { if (constructor != null) return null constructor = element as ExecutableElement } - return constructor?.parameters?.run { - when { - isEmpty() -> ConstructorType.DEFAULT - size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) -> - ConstructorType.CONTEXT - else -> null - } + return constructor + } + + private fun TypeElement.findParameters(): Boolean? { + for (element in enclosedElements) { + if (element.kind != ElementKind.METHOD) continue + if (!element.modifiers.contains(Modifier.PUBLIC)) continue + if (!element.modifiers.contains(Modifier.STATIC)) continue + if (!element.simpleName.contentEquals("parameters")) return null + val parameters = (element as ExecutableElement).parameters + if (parameters.isEmpty()) return false + if (parameters.size == 1 && parameters[0].asType().isSameType(contextType)) return true + error("parameters method should have no parameter or a Context parameter", element) + return null } + return null } + private fun ExecutableElement.hasParameter(index: Int, typeMirror: TypeMirror) = + index < parameters.size && parameters[index].asType().isSameType(typeMirror) + + private fun TypeMirror.isSameType(typeMirror: TypeMirror) = + processingEnv.typeUtils.isSameType(this, typeMirror) + private fun warn(msg: CharSequence) = processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg) @@ -198,8 +277,11 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { private data class Screen( val key: String, val overlay: Boolean, + val parameterized: Boolean, + val parameterizedMigration: Boolean, val klass: String, - val constructorType: ConstructorType, + val constructorHasContextParameter: Boolean, + val parametersHasContextParameter: Boolean, ) : Comparable<Screen> { override fun compareTo(other: Screen): Int { val diff = key.compareTo(other.key) @@ -207,17 +289,13 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { } } - private enum class ConstructorType { - DEFAULT, // default constructor with no parameter - CONTEXT, // constructor with a Context parameter - } - companion object { private const val PACKAGE = "com.android.settingslib.metadata" private const val ANNOTATION_NAME = "ProvidePreferenceScreen" private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME" private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata" private const val FACTORY = "PreferenceScreenMetadataFactory" + private const val PARAMETERIZED_FACTORY = "PreferenceScreenMetadataParameterizedFactory" private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions" private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME" diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt index 4bed795ea760..449c78ce8965 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt @@ -22,14 +22,27 @@ package com.android.settingslib.metadata * The annotated class must satisfy either condition: * - the primary constructor has no parameter * - the primary constructor has a single [android.content.Context] parameter + * - (parameterized) the primary constructor has a single [android.os.Bundle] parameter to override + * [PreferenceScreenMetadata.arguments] + * - (parameterized) the primary constructor has a [android.content.Context] and a + * [android.os.Bundle] parameter to override [PreferenceScreenMetadata.arguments] * * @param value unique preference screen key * @param overlay if true, current annotated screen will overlay the screen that has identical key + * @param parameterized if true, the screen relies on additional arguments to build its content + * @param parameterizedMigration whether the parameterized screen was a normal screen, in which case + * `Bundle.EMPTY` will be passed as arguments to take care of backward compatibility + * @see PreferenceScreenMetadata */ @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS) @MustBeDocumented -annotation class ProvidePreferenceScreen(val value: String, val overlay: Boolean = false) +annotation class ProvidePreferenceScreen( + val value: String, + val overlay: Boolean = false, + val parameterized: Boolean = false, + val parameterizedMigration: Boolean = false, // effective only when parameterized is true +) /** * Provides options for [ProvidePreferenceScreen] annotation processor. diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt index 876f6152cccd..3bd051dee41d 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt @@ -17,6 +17,7 @@ package com.android.settingslib.metadata import android.content.Context +import android.os.Bundle /** A node in preference hierarchy that is associated with [PreferenceMetadata]. */ open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) { @@ -54,8 +55,14 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata) * * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry] */ - operator fun String.unaryPlus() = - +PreferenceHierarchyNode(PreferenceScreenRegistry.create(context, this)!!) + operator fun String.unaryPlus() = addPreferenceScreen(this, null) + + /** + * Adds parameterized preference screen with given key (as a placeholder) to the hierarchy. + * + * @see String.unaryPlus + */ + infix fun String.args(args: Bundle) = createPreferenceScreenHierarchy(this, args) operator fun PreferenceHierarchyNode.unaryPlus() = also { children.add(it) } @@ -122,6 +129,14 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata) } /** + * Adds parameterized preference screen with given key (as a placeholder) to the hierarchy. + * + * @see addPreferenceScreen + */ + fun addParameterizedScreen(screenKey: String, args: Bundle) = + addPreferenceScreen(screenKey, args) + + /** * Adds preference screen with given key (as a placeholder) to the hierarchy. * * This is mainly to support Android Settings overlays. OEMs might want to custom some of the @@ -132,11 +147,13 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata) * * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry] */ - fun addPreferenceScreen(screenKey: String) { - children.add( - PreferenceHierarchy(context, PreferenceScreenRegistry.create(context, screenKey)!!) - ) - } + fun addPreferenceScreen(screenKey: String) = addPreferenceScreen(screenKey, null) + + private fun addPreferenceScreen(screenKey: String, args: Bundle?): PreferenceHierarchyNode = + createPreferenceScreenHierarchy(screenKey, args).also { children.add(it) } + + private fun createPreferenceScreenHierarchy(screenKey: String, args: Bundle?) = + PreferenceHierarchyNode(PreferenceScreenRegistry.create(context, screenKey, args)!!) /** Extensions to add more preferences to the hierarchy. */ operator fun PreferenceHierarchy.plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt index 84014f191f68..4fd13ede6803 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt @@ -17,13 +17,20 @@ package com.android.settingslib.metadata import android.content.Context +import android.os.Bundle /** Provides the associated preference screen key for binding. */ interface PreferenceScreenBindingKeyProvider { /** Returns the associated preference screen key. */ fun getPreferenceScreenBindingKey(context: Context): String? + + /** Returns the arguments to build preference screen. */ + fun getPreferenceScreenBindingArgs(context: Context): Bundle? } /** Extra key to provide the preference screen key for binding. */ const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key" + +/** Extra key to provide arguments for preference screen binding. */ +const val EXTRA_BINDING_SCREEN_ARGS = "settingslib:binding_screen_args" diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt index 850d4523e96e..7f1ded71e30a 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt @@ -18,12 +18,25 @@ package com.android.settingslib.metadata import android.content.Context import android.content.Intent +import android.os.Bundle import androidx.annotation.AnyThread import androidx.fragment.app.Fragment +import kotlinx.coroutines.flow.Flow -/** Metadata of preference screen. */ +/** + * Metadata of preference screen. + * + * For parameterized preference screen that relies on additional information (e.g. package name, + * language code) to build its content, the subclass must: + * - override [arguments] in constructor + * - add a static method `fun parameters(context: Context): List<Bundle>` (context is optional) to + * provide all possible arguments + */ @AnyThread interface PreferenceScreenMetadata : PreferenceMetadata { + /** Arguments to build the screen content. */ + val arguments: Bundle? + get() = null /** * The screen title resource, which precedes [getScreenTitle] if provided. @@ -65,7 +78,12 @@ interface PreferenceScreenMetadata : PreferenceMetadata { fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null } -/** Factory of [PreferenceScreenMetadata]. */ +/** + * Factory of [PreferenceScreenMetadata]. + * + * Annotation processor generates implementation of this interface based on + * [ProvidePreferenceScreen] when [ProvidePreferenceScreen.parameterized] is `false`. + */ fun interface PreferenceScreenMetadataFactory { /** @@ -75,3 +93,44 @@ fun interface PreferenceScreenMetadataFactory { */ fun create(context: Context): PreferenceScreenMetadata } + +/** + * Parameterized factory of [PreferenceScreenMetadata]. + * + * Annotation processor generates implementation of this interface based on + * [ProvidePreferenceScreen] when [ProvidePreferenceScreen.parameterized] is `true`. + */ +interface PreferenceScreenMetadataParameterizedFactory : PreferenceScreenMetadataFactory { + override fun create(context: Context) = create(context, Bundle.EMPTY) + + /** + * Creates a new [PreferenceScreenMetadata] with given arguments. + * + * @param context application context to create the PreferenceScreenMetadata + * @param args arguments to create the screen metadata, [Bundle.EMPTY] is reserved for the + * default case when screen is migrated from normal to parameterized + */ + fun create(context: Context, args: Bundle): PreferenceScreenMetadata + + /** + * Returns all possible arguments to create [PreferenceScreenMetadata]. + * + * Note that [Bundle.EMPTY] is a special arguments reserved for backward compatibility when a + * preference screen was a normal screen but migrated to parameterized screen later: + * 1. Set [ProvidePreferenceScreen.parameterizedMigration] to `true`, so that the generated + * [acceptEmptyArguments] will be `true`. + * 1. In the original [parameters] implementation, produce a [Bundle.EMPTY] for the default + * case. + * + * Do not use [Bundle.EMPTY] for other purpose. + */ + fun parameters(context: Context): Flow<Bundle> + + /** + * Returns true when the parameterized screen was a normal screen. + * + * The [PreferenceScreenMetadata] is expected to accept an empty arguments ([Bundle.EMPTY]) and + * take care of backward compatibility. + */ + fun acceptEmptyArguments(): Boolean = false +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt index c74b3151abb2..246310984db9 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt @@ -17,10 +17,13 @@ package com.android.settingslib.metadata import android.content.Context +import android.os.Bundle +import android.util.Log import com.android.settingslib.datastore.KeyValueStore /** Registry of all available preference screens in the app. */ object PreferenceScreenRegistry : ReadWritePermitProvider { + private const val TAG = "ScreenRegistry" /** Provider of key-value store. */ private lateinit var keyValueStoreProvider: KeyValueStoreProvider @@ -52,9 +55,28 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? = keyValueStoreProvider.getKeyValueStore(context, preference) - /** Creates [PreferenceScreenMetadata] of particular screen key. */ - fun create(context: Context, screenKey: String?): PreferenceScreenMetadata? = - screenKey?.let { preferenceScreenMetadataFactories[it]?.create(context.applicationContext) } + /** Creates [PreferenceScreenMetadata] of particular screen. */ + fun create(context: Context, screenCoordinate: PreferenceScreenCoordinate) = + create(context, screenCoordinate.screenKey, screenCoordinate.args) + + /** Creates [PreferenceScreenMetadata] of particular screen key with given arguments. */ + fun create(context: Context, screenKey: String?, args: Bundle?): PreferenceScreenMetadata? { + if (screenKey == null) return null + val factory = preferenceScreenMetadataFactories[screenKey] ?: return null + val appContext = context.applicationContext + if (factory is PreferenceScreenMetadataParameterizedFactory) { + if (args != null) return factory.create(appContext, args) + // In case the parameterized screen was a normal scree, it is expected to accept + // Bundle.EMPTY arguments and take care of backward compatibility. + if (factory.acceptEmptyArguments()) return factory.create(appContext) + Log.e(TAG, "screen $screenKey is parameterized but args is not provided") + return null + } else { + if (args == null) return factory.create(appContext) + Log.e(TAG, "screen $screenKey is not parameterized but args is provided") + return null + } + } /** * Sets the provider to check read write permit. Read and write requests are denied by default. diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt index 65fbe2b66e77..dbac17d4e8b8 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt @@ -22,6 +22,7 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import androidx.preference.TwoStatePreference +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceScreenMetadata @@ -35,9 +36,11 @@ interface PreferenceScreenBinding : PreferenceBinding { super.bind(preference, metadata) val context = preference.context val screenMetadata = metadata as PreferenceScreenMetadata + val extras = preference.extras // Pass the preference key to fragment, so that the fragment could find associated // preference screen registered in PreferenceScreenRegistry - preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key) + extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key) + screenMetadata.arguments?.let { extras.putBundle(EXTRA_BINDING_SCREEN_ARGS, it) } if (preference is PreferenceScreen) { val screenTitle = screenMetadata.screenTitle preference.title = diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt index ffe181d0c350..02f91c1bb50b 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt @@ -23,6 +23,7 @@ import android.util.Log import androidx.annotation.XmlRes import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceScreen +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider import com.android.settingslib.metadata.PreferenceScreenRegistry @@ -89,13 +90,19 @@ open class PreferenceFragment : @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0 protected fun getPreferenceScreenCreator(context: Context): PreferenceScreenCreator? = - (PreferenceScreenRegistry.create(context, getPreferenceScreenBindingKey(context)) - as? PreferenceScreenCreator) + (PreferenceScreenRegistry.create( + context, + getPreferenceScreenBindingKey(context), + getPreferenceScreenBindingArgs(context), + ) as? PreferenceScreenCreator) ?.run { if (isFlagEnabled(context)) this else null } override fun getPreferenceScreenBindingKey(context: Context): String? = arguments?.getString(EXTRA_BINDING_SCREEN_KEY) + override fun getPreferenceScreenBindingArgs(context: Context): Bundle? = + arguments?.getBundle(EXTRA_BINDING_SCREEN_ARGS) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) preferenceScreenBindingHelper?.onCreate() diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 4a6a589cd3c9..1cb8005ddae0 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -31,6 +31,7 @@ import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedDataObservable import com.android.settingslib.datastore.KeyedObservable import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceHierarchy @@ -227,14 +228,16 @@ class PreferenceScreenBindingHelper( /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic fun bind(preferenceScreen: PreferenceScreen) { - PreferenceScreenRegistry.create(preferenceScreen.context, preferenceScreen.key)?.run { + val context = preferenceScreen.context + val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) + PreferenceScreenRegistry.create(context, preferenceScreen.key, args)?.run { if (!hasCompleteHierarchy()) { val preferenceBindingFactory = (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return bindRecursively( preferenceScreen, preferenceBindingFactory, - getPreferenceHierarchy(preferenceScreen.context), + getPreferenceHierarchy(context), ) } } diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt index 211b3bdaea70..88c4fe6bf188 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt @@ -17,10 +17,12 @@ package com.android.settingslib.preference import android.content.Context +import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.PreferenceScreenRegistry /** Factory to create preference screen. */ @@ -81,8 +83,12 @@ class PreferenceScreenFactory { * * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy. */ - fun createBindingScreen(context: Context, screenKey: String?): PreferenceScreen? { - val metadata = PreferenceScreenRegistry.create(context, screenKey) ?: return null + fun createBindingScreen( + context: Context, + screenKey: String?, + args: Bundle?, + ): PreferenceScreen? { + val metadata = PreferenceScreenRegistry.create(context, screenKey, args) ?: return null if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) { return metadata.createPreferenceScreen(this) } @@ -94,8 +100,9 @@ class PreferenceScreenFactory { @JvmStatic fun createBindingScreen(preference: Preference): PreferenceScreen? { val context = preference.context + val args = preference.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) val preferenceScreenCreator = - (PreferenceScreenRegistry.create(context, preference.key) + (PreferenceScreenRegistry.create(context, preference.key, args) as? PreferenceScreenCreator) ?: return null if (!preferenceScreenCreator.hasCompleteHierarchy()) return null val factory = PreferenceScreenFactory(context) |