Merge "Import SettingsLib/Metadata library" into main
diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp
new file mode 100644
index 0000000..207637f
--- /dev/null
+++ b/packages/SettingsLib/Metadata/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibMetadata-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibMetadata",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibMetadata-srcs"],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.fragment_fragment",
+ "guava",
+ "SettingsLibDataStore",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Metadata/AndroidManifest.xml b/packages/SettingsLib/Metadata/AndroidManifest.xml
new file mode 100644
index 0000000..1c801e6
--- /dev/null
+++ b/packages/SettingsLib/Metadata/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.metadata">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Metadata/processor/Android.bp b/packages/SettingsLib/Metadata/processor/Android.bp
new file mode 100644
index 0000000..d8acc76
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/Android.bp
@@ -0,0 +1,11 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_plugin {
+ name: "SettingsLibMetadata-processor",
+ srcs: ["src/**/*.kt"],
+ processor_class: "com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor",
+ java_resource_dirs: ["resources"],
+ visibility: ["//visibility:public"],
+}
diff --git a/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..762a01a
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor
\ No newline at end of file
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
new file mode 100644
index 0000000..620d717
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.settingslib.metadata
+
+import java.util.TreeMap
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic
+
+/** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
+class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
+ private val screens = TreeMap<String, ConstructorType>()
+ private val overlays = mutableMapOf<String, String>()
+ private val contextType: TypeMirror by lazy {
+ processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
+ }
+
+ private var options: Map<String, Any?>? = null
+ private lateinit var annotationElement: TypeElement
+ private lateinit var optionsElement: TypeElement
+ private lateinit var screenType: TypeMirror
+
+ override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS)
+
+ override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
+
+ override fun init(processingEnv: ProcessingEnvironment) {
+ super.init(processingEnv)
+ val elementUtils = processingEnv.elementUtils
+ annotationElement = elementUtils.getTypeElement(ANNOTATION)
+ optionsElement = elementUtils.getTypeElement(OPTIONS)
+ screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType()
+ }
+
+ override fun process(
+ annotations: MutableSet<out TypeElement>,
+ roundEnv: RoundEnvironment,
+ ): Boolean {
+ roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run {
+ if (options != null) error("@$OPTIONS_NAME is already specified: $options", this)
+ options =
+ annotationMirrors
+ .single { it.isElement(optionsElement) }
+ .elementValues
+ .entries
+ .associate { it.key.simpleName.toString() to it.value.value }
+ }
+ for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) {
+ (element as? TypeElement)?.process()
+ }
+ if (roundEnv.processingOver()) codegen()
+ return false
+ }
+
+ private fun TypeElement.process() {
+ if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) {
+ error("@$ANNOTATION_NAME must be added to non abstract class", this)
+ return
+ }
+ if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) {
+ error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
+ return
+ }
+ val constructorType = getConstructorType()
+ if (constructorType == null) {
+ error(
+ "Class must be an object, or has single public constructor that " +
+ "accepts no parameter or a Context parameter",
+ this,
+ )
+ return
+ }
+ val screenQualifiedName = qualifiedName.toString()
+ screens[screenQualifiedName] = constructorType
+ val annotation = annotationMirrors.single { it.isElement(annotationElement) }
+ val overlay = annotation.getOverlay()
+ if (overlay != null) {
+ overlays.put(overlay, screenQualifiedName)?.let {
+ error("$overlay has been overlaid by $it", this)
+ }
+ }
+ }
+
+ private fun codegen() {
+ val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR
+ if (collector.isEmpty()) return
+ val parts = collector.split('/')
+ if (parts.size == 3) {
+ generateCode(parts[0], parts[1], parts[2])
+ } else {
+ throw IllegalArgumentException(
+ "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format"
+ )
+ }
+ }
+
+ private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) {
+ for ((overlay, screen) in overlays) {
+ if (screens.remove(overlay) == null) {
+ warn("$overlay is overlaid by $screen but not annotated with @$ANNOTATION_NAME")
+ } else {
+ processingEnv.messager.printMessage(
+ Diagnostic.Kind.NOTE,
+ "$overlay is overlaid by $screen",
+ )
+ }
+ }
+ processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
+ it.write("package $outputPkg;\n\n")
+ it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n")
+ it.write("// 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 java.util.List<$PREFERENCE_SCREEN_METADATA> " +
+ "$outputFun(android.content.Context context) {\n"
+ )
+ it.write(
+ " java.util.ArrayList<$PREFERENCE_SCREEN_METADATA> screens = " +
+ "new java.util.ArrayList<>(${screens.size});\n"
+ )
+ for ((screen, constructorType) in screens) {
+ when (constructorType) {
+ ConstructorType.DEFAULT -> it.write(" screens.add(new $screen());\n")
+ ConstructorType.CONTEXT -> it.write(" screens.add(new $screen(context));\n")
+ ConstructorType.SINGLETON -> it.write(" screens.add($screen.INSTANCE);\n")
+ }
+ }
+ for ((overlay, screen) in overlays) {
+ it.write(" // $overlay is overlaid by $screen\n")
+ }
+ it.write(" return screens;\n")
+ it.write(" }\n")
+ it.write("}")
+ }
+ }
+
+ private fun AnnotationMirror.isElement(element: TypeElement) =
+ processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType())
+
+ private fun AnnotationMirror.getOverlay(): String? {
+ for ((key, value) in elementValues) {
+ if (key.simpleName.contentEquals("overlay")) {
+ return if (value.isDefaultClassValue(key)) null else value.value.toString()
+ }
+ }
+ return null
+ }
+
+ private fun AnnotationValue.isDefaultClassValue(key: ExecutableElement) =
+ processingEnv.typeUtils.isSameType(
+ value as TypeMirror,
+ key.defaultValue.value as TypeMirror,
+ )
+
+ private fun TypeElement.getConstructorType(): ConstructorType? {
+ var constructor: ExecutableElement? = null
+ for (element in enclosedElements) {
+ if (element.isKotlinObject()) return ConstructorType.SINGLETON
+ if (element.kind != ElementKind.CONSTRUCTOR) continue
+ if (!element.modifiers.contains(Modifier.PUBLIC)) continue
+ 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
+ }
+ }
+ }
+
+ private fun Element.isKotlinObject() =
+ kind == ElementKind.FIELD &&
+ modifiers.run { contains(Modifier.PUBLIC) && contains(Modifier.STATIC) } &&
+ simpleName.toString() == "INSTANCE"
+
+ private fun warn(msg: CharSequence) =
+ processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
+
+ private fun error(msg: CharSequence, element: Element) =
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element)
+
+ private enum class ConstructorType {
+ DEFAULT, // default constructor with no parameter
+ CONTEXT, // constructor with a Context parameter
+ SINGLETON, // Kotlin object class
+ }
+
+ 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 OPTIONS_NAME = "ProvidePreferenceScreenOptions"
+ private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
+ private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get"
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
new file mode 100644
index 0000000..ea20a74
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.settingslib.metadata
+
+import kotlin.reflect.KClass
+
+/**
+ * Annotation to provide preference screen.
+ *
+ * The annotated class must satisfy either condition:
+ * - the primary constructor has no parameter
+ * - the primary constructor has a single [android.content.Context] parameter
+ * - it is a Kotlin object class
+ *
+ * @param overlay if specified, current annotated screen will overlay the given screen
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreen(
+ val overlay: KClass<out PreferenceScreenMetadata> = PreferenceScreenMetadata::class,
+)
+
+/**
+ * Provides options for [ProvidePreferenceScreen] annotation processor.
+ *
+ * @param codegenCollector generated collector class (format: "pkg/class/method"), an empty string
+ * means do not generate code
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreenOptions(
+ val codegenCollector: String = "com.android.settingslib.metadata/PreferenceScreenCollector/get",
+)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
new file mode 100644
index 0000000..51a8580
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.settingslib.metadata
+
+import android.content.Context
+import androidx.annotation.ArrayRes
+import androidx.annotation.IntDef
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Permit of read and write request. */
+@IntDef(
+ ReadWritePermit.ALLOW,
+ ReadWritePermit.DISALLOW,
+ ReadWritePermit.REQUIRE_APP_PERMISSION,
+ ReadWritePermit.REQUIRE_USER_AGREEMENT,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class ReadWritePermit {
+ companion object {
+ /** Allow to read/write value. */
+ const val ALLOW = 0
+ /** Disallow to read/write value (e.g. uid not allowed). */
+ const val DISALLOW = 1
+ /** Require (runtime/special) app permission from user explicitly. */
+ const val REQUIRE_APP_PERMISSION = 2
+ /** Require explicit user agreement (e.g. terms of service). */
+ const val REQUIRE_USER_AGREEMENT = 3
+ }
+}
+
+/** Preference interface that has a value persisted in datastore. */
+interface PersistentPreference<T> {
+
+ /**
+ * Returns the key-value storage of the preference.
+ *
+ * The default implementation returns the storage provided by
+ * [PreferenceScreenRegistry.getKeyValueStore].
+ */
+ fun storage(context: Context): KeyValueStore =
+ PreferenceScreenRegistry.getKeyValueStore(context, this as PreferenceMetadata)!!
+
+ /**
+ * Returns if the external application (identified by [callingUid]) has permission to read
+ * preference value.
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted or isAvailable.
+ */
+ @ReadWritePermit
+ fun getReadPermit(context: Context, myUid: Int, callingUid: Int): Int =
+ PreferenceScreenRegistry.getReadPermit(
+ context,
+ myUid,
+ callingUid,
+ this as PreferenceMetadata,
+ )
+
+ /**
+ * Returns if the external application (identified by [callingUid]) has permission to write
+ * preference with given [value].
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted or isAvailable.
+ */
+ @ReadWritePermit
+ fun getWritePermit(context: Context, value: T?, myUid: Int, callingUid: Int): Int =
+ PreferenceScreenRegistry.getWritePermit(
+ context,
+ value,
+ myUid,
+ callingUid,
+ this as PreferenceMetadata,
+ )
+}
+
+/** Descriptor of values. */
+sealed interface ValueDescriptor {
+
+ /** Returns if given value (represented by index) is valid. */
+ fun isValidValue(context: Context, index: Int): Boolean
+}
+
+/**
+ * A boolean type value.
+ *
+ * A zero value means `False`, otherwise it is `True`.
+ */
+interface BooleanValue : ValueDescriptor {
+ override fun isValidValue(context: Context, index: Int) = true
+}
+
+/** Value falls into a given array. */
+interface DiscreteValue<T> : ValueDescriptor {
+ @get:ArrayRes val values: Int
+
+ @get:ArrayRes val valuesDescription: Int
+
+ fun getValue(context: Context, index: Int): T
+}
+
+/**
+ * Value falls into a text array, whose element is [CharSequence] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteTextValue : DiscreteValue<CharSequence> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getTextArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): CharSequence =
+ context.resources.getTextArray(values)[index]
+}
+
+/**
+ * Value falls into a string array, whose element is [String] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteStringValue : DiscreteValue<String> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getStringArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): String =
+ context.resources.getStringArray(values)[index]
+}
+
+/**
+ * Value falls into an integer array.
+ *
+ * [values] resource is `<integer-array>`.
+ */
+interface DiscreteIntValue : DiscreteValue<Int> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getIntArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): Int =
+ context.resources.getIntArray(values)[index]
+}
+
+/** Value is between a range. */
+interface RangeValue : ValueDescriptor {
+ /** The lower bound (inclusive) of the range. */
+ val minValue: Int
+
+ /** The upper bound (inclusive) of the range. */
+ val maxValue: Int
+
+ /** The increment step within the range. 0 means unset, which implies step size is 1. */
+ val incrementStep: Int
+ get() = 0
+
+ override fun isValidValue(context: Context, index: Int) = index in minValue..maxValue
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
new file mode 100644
index 0000000..4503738
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.settingslib.metadata
+
+/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
+open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata)
+
+/**
+ * Preference hierarchy describes the structure of preferences recursively.
+ *
+ * A root hierarchy represents a preference screen. A sub-hierarchy represents a preference group.
+ */
+class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) :
+ PreferenceHierarchyNode(metadata) {
+
+ private val children = mutableListOf<PreferenceHierarchyNode>()
+
+ /** Adds a preference to the hierarchy. */
+ operator fun PreferenceMetadata.unaryPlus() {
+ children.add(PreferenceHierarchyNode(this))
+ }
+
+ /**
+ * 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
+ * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+ * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+ * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+ * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+ *
+ * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+ */
+ operator fun String.unaryPlus() {
+ children.add(PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!))
+ }
+
+ /** Adds a preference to the hierarchy. */
+ fun add(metadata: PreferenceMetadata) {
+ children.add(PreferenceHierarchyNode(metadata))
+ }
+
+ /** Adds a preference group to the hierarchy. */
+ operator fun PreferenceGroup.unaryPlus() = PreferenceHierarchy(this).also { children.add(it) }
+
+ /** Adds a preference group and returns its preference hierarchy. */
+ fun addGroup(metadata: PreferenceGroup): PreferenceHierarchy =
+ PreferenceHierarchy(metadata).also { children.add(it) }
+
+ /**
+ * 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
+ * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+ * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+ * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+ * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+ *
+ * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+ */
+ fun addPreferenceScreen(screenKey: String) {
+ children.add(PreferenceHierarchy(PreferenceScreenRegistry[screenKey]!!))
+ }
+
+ /** Extensions to add more preferences to the hierarchy. */
+ operator fun plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
+
+ /** Traversals preference hierarchy and applies given action. */
+ fun forEach(action: (PreferenceHierarchyNode) -> Unit) {
+ for (it in children) action(it)
+ }
+
+ /** Traversals preference hierarchy and applies given action. */
+ suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
+ for (it in children) action(it)
+ }
+
+ /** Finds the [PreferenceMetadata] object associated with given key. */
+ fun find(key: String): PreferenceMetadata? {
+ if (metadata.key == key) return metadata
+ for (child in children) {
+ if (child is PreferenceHierarchy) {
+ val result = child.find(key)
+ if (result != null) return result
+ } else {
+ if (child.metadata.key == key) return child.metadata
+ }
+ }
+ return null
+ }
+
+ /** Returns all the [PreferenceMetadata]s appear in the hierarchy. */
+ fun getAllPreferences(): List<PreferenceMetadata> =
+ mutableListOf<PreferenceMetadata>().also { getAllPreferences(it) }
+
+ private fun getAllPreferences(result: MutableList<PreferenceMetadata>) {
+ result.add(metadata)
+ for (child in children) {
+ if (child is PreferenceHierarchy) {
+ child.getAllPreferences(result)
+ } else {
+ result.add(child.metadata)
+ }
+ }
+ }
+}
+
+/**
+ * Builder function to create [PreferenceHierarchy] in
+ * [DSL](https://kotlinlang.org/docs/type-safe-builders.html) manner.
+ */
+fun preferenceHierarchy(metadata: PreferenceMetadata, init: PreferenceHierarchy.() -> Unit) =
+ PreferenceHierarchy(metadata).also(init)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
new file mode 100644
index 0000000..f39f3a0
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.settingslib.metadata
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.annotation.AnyThread
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+/**
+ * Interface provides preference metadata (title, summary, icon, etc.).
+ *
+ * Besides the existing APIs, subclass could integrate with following interface to provide more
+ * information:
+ * - [PreferenceTitleProvider]: provide dynamic title content
+ * - [PreferenceSummaryProvider]: provide dynamic summary content (e.g. based on preference value)
+ * - [PreferenceAvailabilityProvider]: provide preference availability (e.g. based on flag)
+ * - [PreferenceLifecycleProvider]: provide the lifecycle callbacks and notify state change
+ *
+ * Notes:
+ * - UI framework support:
+ * - This class does not involve any UI logic, it is the data layer.
+ * - Subclass could integrate with datastore and UI widget to provide UI layer. For instance,
+ * `PreferenceBinding` supports Jetpack Preference binding.
+ * - Datastore:
+ * - Subclass should implement the [PersistentPreference] to note that current preference is
+ * persistent in datastore.
+ * - It is always recommended to support back up preference value changed by user. Typically,
+ * the back up and restore happen within datastore, the [allowBackup] API is to mark if
+ * current preference value should be backed up (backup allowed by default).
+ * - Preference indexing for search:
+ * - Override [isIndexable] API to mark if preference is indexable (enabled by default).
+ * - If [isIndexable] returns true, preference title and summary will be indexed with cache.
+ * More indexing data could be provided through [keywords].
+ * - Settings search will cache the preference title/summary/keywords for indexing. The cache is
+ * invalidated when system locale changed, app upgraded, etc.
+ * - Dynamic content is not suitable to be cached for indexing. Subclass that implements
+ * [PreferenceTitleProvider] / [PreferenceSummaryProvider] will not have its title / summary
+ * indexed.
+ */
+@AnyThread
+interface PreferenceMetadata {
+
+ /** Preference key. */
+ val key: String
+
+ /**
+ * Preference title resource id.
+ *
+ * Implement [PreferenceTitleProvider] if title is generated dynamically.
+ */
+ val title: Int
+ @StringRes get() = 0
+
+ /**
+ * Preference summary resource id.
+ *
+ * Implement [PreferenceSummaryProvider] if summary is generated dynamically (e.g. summary is
+ * provided per preference value)
+ */
+ val summary: Int
+ @StringRes get() = 0
+
+ /** Icon of the preference. */
+ val icon: Int
+ @DrawableRes get() = 0
+
+ /** Additional keywords for indexing. */
+ val keywords: Int
+ @StringRes get() = 0
+
+ /**
+ * Return the extras Bundle object associated with this preference.
+ *
+ * It is used to provide more information for metadata.
+ */
+ fun extras(context: Context): Bundle? = null
+
+ /**
+ * Returns if preference is indexable, default value is `true`.
+ *
+ * Return `false` only when the preference is always unavailable on current device. If it is
+ * conditional available, override [PreferenceAvailabilityProvider].
+ */
+ fun isIndexable(context: Context): Boolean = true
+
+ /**
+ * Returns if preference is enabled.
+ *
+ * UI framework normally does not allow user to interact with the preference widget when it is
+ * disabled.
+ *
+ * [dependencyOfEnabledState] is provided to support dependency, the [shouldDisableDependents]
+ * value of dependent preference is used to decide enabled state.
+ */
+ fun isEnabled(context: Context): Boolean {
+ val dependency = dependencyOfEnabledState(context) ?: return true
+ return !dependency.shouldDisableDependents(context)
+ }
+
+ /** Returns the key of depended preference to decide the enabled state. */
+ fun dependencyOfEnabledState(context: Context): PreferenceMetadata? = null
+
+ /** Returns whether this preference's dependents should be disabled. */
+ fun shouldDisableDependents(context: Context): Boolean = !isEnabled(context)
+
+ /** Returns if the preference is persistent in datastore. */
+ fun isPersistent(context: Context): Boolean = this is PersistentPreference<*>
+
+ /**
+ * Returns if preference value backup is allowed (by default returns `true` if preference is
+ * persistent).
+ */
+ fun allowBackup(context: Context): Boolean = isPersistent(context)
+
+ /** Returns preference intent. */
+ fun intent(context: Context): Intent? = null
+
+ /** Returns preference order. */
+ fun order(context: Context): Int? = null
+
+ /**
+ * Returns the preference title.
+ *
+ * Implement [PreferenceTitleProvider] interface if title content is generated dynamically.
+ */
+ fun getPreferenceTitle(context: Context): CharSequence? =
+ when {
+ title != 0 -> context.getText(title)
+ this is PreferenceTitleProvider -> getTitle(context)
+ else -> null
+ }
+
+ /**
+ * Returns the preference summary.
+ *
+ * Implement [PreferenceSummaryProvider] interface if summary content is generated dynamically
+ * (e.g. summary is provided per preference value).
+ */
+ fun getPreferenceSummary(context: Context): CharSequence? =
+ when {
+ summary != 0 -> context.getText(summary)
+ this is PreferenceSummaryProvider -> getSummary(context)
+ else -> null
+ }
+}
+
+/** Metadata of preference group. */
+@AnyThread
+open class PreferenceGroup(override val key: String, override val title: Int) : PreferenceMetadata
+
+/** Metadata of preference screen. */
+@AnyThread
+interface PreferenceScreenMetadata : PreferenceMetadata {
+
+ /**
+ * The screen title resource, which precedes [getScreenTitle] if provided.
+ *
+ * By default, screen title is same with [title].
+ */
+ val screenTitle: Int
+ get() = title
+
+ /** Returns dynamic screen title, use [screenTitle] whenever possible. */
+ fun getScreenTitle(context: Context): CharSequence? = null
+
+ /** Returns the fragment class to show the preference screen. */
+ fun fragmentClass(): Class<out Fragment>?
+
+ /**
+ * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen.
+ *
+ * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen.
+ * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is
+ * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in
+ * [getPreferenceHierarchy] will only be used to bind UI widgets.
+ */
+ fun hasCompleteHierarchy(): Boolean = true
+
+ /**
+ * Returns the hierarchy of preference screen.
+ *
+ * The implementation MUST include all preferences into the hierarchy regardless of the runtime
+ * conditions. DO NOT check any condition (except compile time flag) before adding a preference.
+ */
+ fun getPreferenceHierarchy(context: Context): PreferenceHierarchy
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
new file mode 100644
index 0000000..84014f1
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.settingslib.metadata
+
+import android.content.Context
+
+/** Provides the associated preference screen key for binding. */
+interface PreferenceScreenBindingKeyProvider {
+
+ /** Returns the associated preference screen key. */
+ fun getPreferenceScreenBindingKey(context: Context): String?
+}
+
+/** Extra key to provide the preference screen key for binding. */
+const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key"
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
new file mode 100644
index 0000000..48798da
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.settingslib.metadata
+
+import android.content.Context
+import com.android.settingslib.datastore.KeyValueStore
+import com.google.common.base.Supplier
+import com.google.common.base.Suppliers
+import com.google.common.collect.ImmutableMap
+
+private typealias PreferenceScreenMap = ImmutableMap<String, PreferenceScreenMetadata>
+
+/** Registry of all available preference screens in the app. */
+object PreferenceScreenRegistry : ReadWritePermitProvider {
+
+ /** Provider of key-value store. */
+ private lateinit var keyValueStoreProvider: KeyValueStoreProvider
+
+ private var preferenceScreensSupplier: Supplier<PreferenceScreenMap> = Supplier {
+ ImmutableMap.of()
+ }
+
+ private val preferenceScreens: PreferenceScreenMap
+ get() = preferenceScreensSupplier.get()
+
+ private var readWritePermitProvider: ReadWritePermitProvider? = null
+
+ /** Sets the [KeyValueStoreProvider]. */
+ fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) {
+ this.keyValueStoreProvider = keyValueStoreProvider
+ }
+
+ /**
+ * Returns the key-value store for given preference.
+ *
+ * Must call [setKeyValueStoreProvider] before invoking this method, otherwise
+ * [NullPointerException] is raised.
+ */
+ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? =
+ keyValueStoreProvider.getKeyValueStore(context, preference)
+
+ /** Sets supplier to provide available preference screens. */
+ fun setPreferenceScreensSupplier(supplier: Supplier<List<PreferenceScreenMetadata>>) {
+ preferenceScreensSupplier =
+ Suppliers.memoize {
+ val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+ for (screen in supplier.get()) screensBuilder.put(screen.key, screen)
+ screensBuilder.buildOrThrow()
+ }
+ }
+
+ /** Sets available preference screens. */
+ fun setPreferenceScreens(vararg screens: PreferenceScreenMetadata) {
+ val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+ for (screen in screens) screensBuilder.put(screen.key, screen)
+ preferenceScreensSupplier = Suppliers.ofInstance(screensBuilder.buildOrThrow())
+ }
+
+ /** Returns [PreferenceScreenMetadata] of particular key. */
+ operator fun get(key: String?): PreferenceScreenMetadata? =
+ if (key != null) preferenceScreens[key] else null
+
+ /**
+ * Sets the provider to check read write permit. Read and write requests are denied by default.
+ */
+ fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider?) {
+ this.readWritePermitProvider = readWritePermitProvider
+ }
+
+ override fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) =
+ readWritePermitProvider?.getReadPermit(context, myUid, callingUid, preference)
+ ?: ReadWritePermit.DISALLOW
+
+ override fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) =
+ readWritePermitProvider?.getWritePermit(context, value, myUid, callingUid, preference)
+ ?: ReadWritePermit.DISALLOW
+}
+
+/** Provider of [KeyValueStore]. */
+fun interface KeyValueStoreProvider {
+
+ /**
+ * Returns the key-value store for given preference.
+ *
+ * Here are some use cases:
+ * - provide the default storage for all preferences
+ * - determine the storage per preference keys or the interfaces implemented by the preference
+ */
+ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore?
+}
+
+/** Provider of read and write permit. */
+interface ReadWritePermitProvider {
+
+ @ReadWritePermit
+ fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ): Int
+
+ @ReadWritePermit
+ fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ): Int
+
+ companion object {
+ @JvmField
+ val ALLOW_ALL_READ_WRITE =
+ object : ReadWritePermitProvider {
+ override fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) = ReadWritePermit.ALLOW
+
+ override fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) = ReadWritePermit.ALLOW
+ }
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
new file mode 100644
index 0000000..a3aa85d
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.settingslib.metadata
+
+import android.content.Context
+
+/**
+ * Interface to provide dynamic preference title.
+ *
+ * Implement this interface implies that the preference title should not be cached for indexing.
+ */
+interface PreferenceTitleProvider {
+
+ /** Provides preference title. */
+ fun getTitle(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide dynamic preference summary.
+ *
+ * Implement this interface implies that the preference summary should not be cached for indexing.
+ */
+interface PreferenceSummaryProvider {
+
+ /** Provides preference summary. */
+ fun getSummary(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide the state of preference availability.
+ *
+ * UI framework normally does not show the preference widget if it is unavailable.
+ */
+interface PreferenceAvailabilityProvider {
+
+ /** Returns if the preference is available. */
+ fun isAvailable(context: Context): Boolean
+}
+
+/**
+ * Interface to provide the managed configuration state of the preference.
+ *
+ * See [Managed configurations](https://developer.android.com/work/managed-configurations) for the
+ * Android Enterprise support.
+ */
+interface PreferenceRestrictionProvider {
+
+ /** Returns if preference is restricted by managed configs. */
+ fun isRestricted(context: Context): Boolean
+}
+
+/**
+ * Preference lifecycle to deal with preference state.
+ *
+ * Implement this interface when preference depends on runtime conditions.
+ */
+interface PreferenceLifecycleProvider {
+
+ /**
+ * Called when preference is attached to UI.
+ *
+ * Subclass could override this API to register runtime condition listeners, and invoke
+ * `onPreferenceStateChanged(this)` on the given [preferenceStateObserver] to update UI when
+ * internal state (e.g. availability, enabled state, title, summary) is changed.
+ */
+ fun onAttach(context: Context, preferenceStateObserver: PreferenceStateObserver)
+
+ /**
+ * Called when preference is detached from UI.
+ *
+ * Clean up and release resource.
+ */
+ fun onDetach(context: Context)
+
+ /** Observer of preference state. */
+ interface PreferenceStateObserver {
+
+ /** Callbacks when preference state is changed. */
+ fun onPreferenceStateChanged(preference: PreferenceMetadata)
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
new file mode 100644
index 0000000..ad996c7
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.settingslib.metadata
+
+import android.content.Context
+import androidx.annotation.StringRes
+
+/**
+ * Common base class for preferences that have two selectable states, save a boolean value, and may
+ * have dependent preferences that are enabled/disabled based on the current state.
+ */
+interface TwoStatePreference : PreferenceMetadata, PersistentPreference<Boolean>, BooleanValue {
+
+ override fun shouldDisableDependents(context: Context) =
+ storage(context).getValue(key, Boolean::class.javaObjectType) != true ||
+ super.shouldDisableDependents(context)
+}
+
+/** A preference that provides a two-state toggleable option. */
+open class SwitchPreference
+@JvmOverloads
+constructor(
+ override val key: String,
+ @StringRes override val title: Int = 0,
+ @StringRes override val summary: Int = 0,
+) : TwoStatePreference