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