Import SettingsLib/Preference library
Bug: 332201912
Flag: NONE Import library
Test: N/A
Change-Id: I88dec4dd08ecda9361983db75b88cd3fc4a6675b
diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp
new file mode 100644
index 0000000..9665dbd
--- /dev/null
+++ b/packages/SettingsLib/Preference/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibPreference-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibPreference",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibPreference-srcs"],
+ static_libs: [
+ "SettingsLibDataStore",
+ "SettingsLibMetadata",
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Preference/AndroidManifest.xml b/packages/SettingsLib/Preference/AndroidManifest.xml
new file mode 100644
index 0000000..2d7f7ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/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.preference">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
new file mode 100644
index 0000000..9be0e71
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.DialogPreference
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SeekBarPreference
+import com.android.settingslib.metadata.DiscreteIntValue
+import com.android.settingslib.metadata.DiscreteValue
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.RangeValue
+
+/** Binding of preference widget and preference metadata. */
+interface PreferenceBinding {
+
+ /**
+ * Provides a new [Preference] widget instance.
+ *
+ * By default, it returns a new [Preference] object. Subclass could override this method to
+ * provide customized widget and do **one-off** initialization (e.g.
+ * [Preference.setOnPreferenceClickListener]). To update widget everytime when state is changed,
+ * override the [bind] method.
+ *
+ * Notes:
+ * - DO NOT set any properties defined in [PreferenceMetadata]. For example,
+ * title/summary/icon/extras/isEnabled/isVisible/isPersistent/dependency. These properties
+ * will be reset by [bind].
+ * - Override [bind] if needed to provide more information for customized widget.
+ */
+ fun createWidget(context: Context): Preference = Preference(context)
+
+ /**
+ * Binds preference widget with given metadata.
+ *
+ * Whenever metadata state is changed, this callback is invoked to update widget. By default,
+ * the common states like title, summary, enabled, etc. are already applied. Subclass should
+ * override this method to bind more data (e.g. read preference value from storage and apply it
+ * to widget).
+ *
+ * @param preference preference widget created by [createWidget]
+ * @param metadata metadata to apply
+ */
+ fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ metadata.apply {
+ preference.key = key
+ if (icon != 0) {
+ preference.setIcon(icon)
+ } else {
+ preference.icon = null
+ }
+ val context = preference.context
+ preference.peekExtras()?.clear()
+ extras(context)?.let { preference.extras.putAll(it) }
+ preference.title = getPreferenceTitle(context)
+ preference.summary = getPreferenceSummary(context)
+ preference.isEnabled = isEnabled(context)
+ preference.isVisible =
+ (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
+ preference.isPersistent = isPersistent(context)
+ metadata.order(context)?.let { preference.order = it }
+ // PreferenceRegistry will notify dependency change, so we do not need to set
+ // dependency here. This simplifies dependency management and avoid the
+ // IllegalStateException when call Preference.setDependency
+ preference.dependency = null
+ if (preference !is PreferenceScreen) { // avoid recursive loop when build graph
+ preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name
+ preference.intent = intent(context)
+ }
+ if (preference is DialogPreference) {
+ preference.dialogTitle = preference.title
+ }
+ if (preference is ListPreference && this is DiscreteValue<*>) {
+ preference.setEntries(valuesDescription)
+ if (this is DiscreteIntValue) {
+ val intValues = context.resources.getIntArray(values)
+ preference.entryValues = Array(intValues.size) { intValues[it].toString() }
+ } else {
+ preference.setEntryValues(values)
+ }
+ } else if (preference is SeekBarPreference && this is RangeValue) {
+ preference.min = minValue
+ preference.max = maxValue
+ preference.seekBarIncrement = incrementStep
+ }
+ }
+ }
+}
+
+/** Abstract preference screen to provide preference hierarchy and binding factory. */
+interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider {
+
+ val preferenceBindingFactory: PreferenceBindingFactory
+ get() = DefaultPreferenceBindingFactory
+
+ override fun createPreferenceScreen(factory: PreferenceScreenFactory) =
+ factory.getOrCreatePreferenceScreen().apply {
+ inflatePreferenceHierarchy(preferenceBindingFactory, getPreferenceHierarchy(context))
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
new file mode 100644
index 0000000..4c2e1ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.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.preference
+
+import com.android.settingslib.metadata.PreferenceGroup
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.SwitchPreference
+
+/** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */
+interface PreferenceBindingFactory {
+
+ /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */
+ fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding?
+}
+
+/** Default [PreferenceBindingFactory]. */
+object DefaultPreferenceBindingFactory : PreferenceBindingFactory {
+
+ override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+ metadata as? PreferenceBinding
+ ?: when (metadata) {
+ is SwitchPreference -> SwitchPreferenceBinding.INSTANCE
+ is PreferenceGroup -> PreferenceGroupBinding.INSTANCE
+ is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE
+ else -> DefaultPreferenceBinding
+ }
+}
+
+/** A preference key based binding factory. */
+class KeyedPreferenceBindingFactory(private val bindings: Map<String, PreferenceBinding>) :
+ PreferenceBindingFactory {
+
+ override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+ bindings[metadata.key] ?: DefaultPreferenceBindingFactory.getPreferenceBinding(metadata)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
new file mode 100644
index 0000000..ede970e
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceTitleProvider
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceScreenBinding : PreferenceBinding {
+
+ override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ super.bind(preference, metadata)
+ val context = preference.context
+ val screenMetadata = metadata as PreferenceScreenMetadata
+ // 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)
+ if (preference is PreferenceScreen) {
+ val screenTitle = screenMetadata.screenTitle
+ preference.title =
+ if (screenTitle != 0) {
+ context.getString(screenTitle)
+ } else {
+ screenMetadata.getScreenTitle(context)
+ ?: (this as? PreferenceTitleProvider)?.getTitle(context)
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic val INSTANCE = object : PreferenceScreenBinding {}
+ }
+}
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceGroupBinding : PreferenceBinding {
+
+ override fun createWidget(context: Context) = PreferenceCategory(context)
+
+ companion object {
+ @JvmStatic val INSTANCE = object : PreferenceGroupBinding {}
+ }
+}
+
+/** A boolean value type preference associated with [SwitchPreferenceCompat]. */
+interface SwitchPreferenceBinding : PreferenceBinding {
+
+ override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context)
+
+ override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ super.bind(preference, metadata)
+ (metadata as? PersistentPreference<*>)
+ ?.storage(preference.context)
+ ?.getValue(metadata.key, Boolean::class.javaObjectType)
+ ?.let { (preference as SwitchPreferenceCompat).isChecked = it }
+ }
+
+ companion object {
+ @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {}
+ }
+}
+
+/** Default [PreferenceBinding] for [Preference]. */
+object DefaultPreferenceBinding : PreferenceBinding
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
new file mode 100644
index 0000000..02acfca
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.preference
+
+import androidx.preference.PreferenceDataStore
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Adapter to translate [KeyValueStore] into [PreferenceDataStore]. */
+class PreferenceDataStoreAdapter(private val keyValueStore: KeyValueStore) : PreferenceDataStore() {
+
+ override fun getBoolean(key: String, defValue: Boolean): Boolean =
+ keyValueStore.getValue(key, Boolean::class.javaObjectType) ?: defValue
+
+ override fun getFloat(key: String, defValue: Float): Float =
+ keyValueStore.getValue(key, Float::class.javaObjectType) ?: defValue
+
+ override fun getInt(key: String, defValue: Int): Int =
+ keyValueStore.getValue(key, Int::class.javaObjectType) ?: defValue
+
+ override fun getLong(key: String, defValue: Long): Long =
+ keyValueStore.getValue(key, Long::class.javaObjectType) ?: defValue
+
+ override fun getString(key: String, defValue: String?): String? =
+ keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
+
+ override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
+ (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
+
+ override fun putBoolean(key: String, value: Boolean) =
+ keyValueStore.setValue(key, Boolean::class.javaObjectType, value)
+
+ override fun putFloat(key: String, value: Float) =
+ keyValueStore.setValue(key, Float::class.javaObjectType, value)
+
+ override fun putInt(key: String, value: Int) =
+ keyValueStore.setValue(key, Int::class.javaObjectType, value)
+
+ override fun putLong(key: String, value: Long) =
+ keyValueStore.setValue(key, Long::class.javaObjectType, value)
+
+ override fun putString(key: String, value: String?) =
+ keyValueStore.setValue(key, String::class.javaObjectType, value)
+
+ override fun putStringSet(key: String, values: Set<String>?) =
+ keyValueStore.setValue(key, Set::class.javaObjectType, values)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
new file mode 100644
index 0000000..2072009
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.XmlRes
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively
+
+/** Fragment to display a preference screen. */
+open class PreferenceFragment :
+ PreferenceFragmentCompat(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
+
+ private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ preferenceScreen = createPreferenceScreen()
+ }
+
+ fun createPreferenceScreen(): PreferenceScreen? =
+ createPreferenceScreen(PreferenceScreenFactory(this))
+
+ override fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? {
+ val context = factory.context
+ fun createPreferenceScreenFromResource() =
+ factory.inflate(getPreferenceScreenResId(context))
+
+ if (!usePreferenceScreenMetadata()) return createPreferenceScreenFromResource()
+
+ val screenKey = getPreferenceScreenBindingKey(context)
+ val screenCreator =
+ (PreferenceScreenRegistry[screenKey] as? PreferenceScreenCreator)
+ ?: return createPreferenceScreenFromResource()
+
+ val preferenceBindingFactory = screenCreator.preferenceBindingFactory
+ val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context)
+ val preferenceScreen =
+ if (screenCreator.hasCompleteHierarchy()) {
+ factory.getOrCreatePreferenceScreen().apply {
+ inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy)
+ }
+ } else {
+ createPreferenceScreenFromResource()?.also {
+ bindRecursively(it, preferenceBindingFactory, preferenceHierarchy)
+ } ?: return null
+ }
+ preferenceScreenBindingHelper =
+ PreferenceScreenBindingHelper(
+ context,
+ preferenceBindingFactory,
+ preferenceScreen,
+ preferenceHierarchy,
+ )
+ return preferenceScreen
+ }
+
+ /**
+ * Returns if preference screen metadata can be used to set up preference screen.
+ *
+ * This is for flagging purpose. If false (e.g. flag is disabled), xml resource is used to build
+ * preference screen.
+ */
+ protected open fun usePreferenceScreenMetadata(): Boolean = true
+
+ /** Returns the xml resource to create preference screen. */
+ @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0
+
+ override fun getPreferenceScreenBindingKey(context: Context): String? =
+ arguments?.getString(EXTRA_BINDING_SCREEN_KEY)
+
+ override fun onDestroy() {
+ preferenceScreenBindingHelper?.close()
+ super.onDestroy()
+ }
+
+ companion object {
+ /** Returns [PreferenceFragment] instance to display the preference screen of given key. */
+ fun of(screenKey: String): PreferenceFragment? {
+ val screenMetadata = PreferenceScreenRegistry[screenKey] ?: return null
+ if (
+ screenMetadata is PreferenceScreenCreator && screenMetadata.hasCompleteHierarchy()
+ ) {
+ return PreferenceFragment().apply {
+ arguments = Bundle().apply { putString(EXTRA_BINDING_SCREEN_KEY, screenKey) }
+ }
+ }
+ return null
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
new file mode 100644
index 0000000..5ef7823
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.preference
+
+import androidx.preference.PreferenceDataStore
+import androidx.preference.PreferenceGroup
+import com.android.settingslib.datastore.KeyValueStore
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceMetadata
+
+/** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */
+fun PreferenceGroup.inflatePreferenceHierarchy(
+ preferenceBindingFactory: PreferenceBindingFactory,
+ hierarchy: PreferenceHierarchy,
+ storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
+) {
+ fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this)
+
+ hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) }
+ hierarchy.forEach {
+ val metadata = it.metadata
+ val preferenceBinding = metadata.preferenceBinding() ?: return@forEach
+ val widget = preferenceBinding.createWidget(context)
+ if (it is PreferenceHierarchy) {
+ val preferenceGroup = widget as PreferenceGroup
+ // MUST add preference before binding, otherwise exception is raised when add child
+ addPreference(preferenceGroup)
+ preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it)
+ } else {
+ preferenceBinding.bind(widget, metadata)
+ (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
+ widget.preferenceDataStore =
+ storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
+ }
+ // MUST add preference after binding for persistent preference to get initial value
+ // (preference key is set within bind method)
+ addPreference(widget)
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
new file mode 100644
index 0000000..3610894
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.KeyedDataObservable
+import com.android.settingslib.datastore.KeyedObservable
+import com.android.settingslib.datastore.KeyedObserver
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceLifecycleProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableMultimap
+import java.util.concurrent.Executor
+
+/**
+ * Helper to bind preferences on given [preferenceScreen].
+ *
+ * When there is any preference change event detected (e.g. preference value changed, runtime
+ * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update
+ * widget UI.
+ */
+class PreferenceScreenBindingHelper(
+ context: Context,
+ private val preferenceBindingFactory: PreferenceBindingFactory,
+ private val preferenceScreen: PreferenceScreen,
+ preferenceHierarchy: PreferenceHierarchy,
+) : KeyedDataObservable<String>(), AutoCloseable {
+
+ private val handler = Handler(Looper.getMainLooper())
+ private val executor =
+ object : Executor {
+ override fun execute(command: Runnable) {
+ handler.post(command)
+ }
+ }
+
+ private val preferences: ImmutableMap<String, PreferenceMetadata>
+ private val dependencies: ImmutableMultimap<String, String>
+ private val storages = mutableSetOf<KeyedObservable<String>>()
+
+ private val preferenceObserver: KeyedObserver<String?>
+
+ private val storageObserver =
+ KeyedObserver<String?> { key, _ ->
+ if (key != null) {
+ notifyChange(key, CHANGE_REASON_VALUE)
+ }
+ }
+
+ private val stateObserver =
+ object : PreferenceLifecycleProvider.PreferenceStateObserver {
+ override fun onPreferenceStateChanged(preference: PreferenceMetadata) {
+ notifyChange(preference.key, CHANGE_REASON_STATE)
+ }
+ }
+
+ init {
+ val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>()
+ val dependenciesBuilder = ImmutableMultimap.builder<String, String>()
+ fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) {
+ dependenciesBuilder.put(key, dependency.key)
+ }
+
+ fun PreferenceMetadata.add() {
+ preferencesBuilder.put(key, this)
+ dependencyOfEnabledState(context)?.addDependency(this)
+ if (this is PreferenceLifecycleProvider) onAttach(context, stateObserver)
+ if (this is PersistentPreference<*>) storages.add(storage(context))
+ }
+
+ fun PreferenceHierarchy.addPreferences() {
+ metadata.add()
+ forEach {
+ if (it is PreferenceHierarchy) {
+ it.addPreferences()
+ } else {
+ it.metadata.add()
+ }
+ }
+ }
+
+ preferenceHierarchy.addPreferences()
+ this.preferences = preferencesBuilder.buildOrThrow()
+ this.dependencies = dependenciesBuilder.build()
+
+ preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
+ addObserver(preferenceObserver, executor)
+ for (storage in storages) storage.addObserver(storageObserver, executor)
+ }
+
+ private fun onPreferenceChange(key: String?, reason: Int) {
+ if (key == null) return
+
+ // bind preference to update UI
+ preferenceScreen.findPreference<Preference>(key)?.let {
+ preferenceBindingFactory.bind(it, preferences[key])
+ }
+
+ // check reason to avoid potential infinite loop
+ if (reason != CHANGE_REASON_DEPENDENT) {
+ notifyDependents(key, mutableSetOf())
+ }
+ }
+
+ /** Notifies dependents recursively. */
+ private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
+ if (!notifiedKeys.add(key)) return
+ for (dependency in dependencies[key]) {
+ notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+ notifyDependents(dependency, notifiedKeys)
+ }
+ }
+
+ override fun close() {
+ removeObserver(preferenceObserver)
+ val context = preferenceScreen.context
+ for (preference in preferences.values) {
+ if (preference is PreferenceLifecycleProvider) preference.onDetach(context)
+ }
+ for (storage in storages) storage.removeObserver(storageObserver)
+ }
+
+ companion object {
+ /** Preference value is changed. */
+ private const val CHANGE_REASON_VALUE = 0
+ /** Preference state (title/summary, enable state, etc.) is changed. */
+ private const val CHANGE_REASON_STATE = 1
+ /** Dependent preference state is changed. */
+ private const val CHANGE_REASON_DEPENDENT = 2
+
+ /** Updates preference screen that has incomplete hierarchy. */
+ @JvmStatic
+ fun bind(preferenceScreen: PreferenceScreen) {
+ PreferenceScreenRegistry[preferenceScreen.key]?.run {
+ if (!hasCompleteHierarchy()) {
+ val preferenceBindingFactory =
+ (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return
+ bindRecursively(
+ preferenceScreen,
+ preferenceBindingFactory,
+ getPreferenceHierarchy(preferenceScreen.context),
+ )
+ }
+ }
+ }
+
+ internal fun bindRecursively(
+ preferenceScreen: PreferenceScreen,
+ preferenceBindingFactory: PreferenceBindingFactory,
+ preferenceHierarchy: PreferenceHierarchy,
+ ) =
+ preferenceScreen.bindRecursively(
+ preferenceBindingFactory,
+ preferenceHierarchy.getAllPreferences().associateBy { it.key },
+ )
+
+ private fun PreferenceGroup.bindRecursively(
+ preferenceBindingFactory: PreferenceBindingFactory,
+ preferences: Map<String, PreferenceMetadata>,
+ ) {
+ preferenceBindingFactory.bind(this, preferences[key])
+ val count = preferenceCount
+ for (index in 0 until count) {
+ val preference = getPreference(index)
+ if (preference is PreferenceGroup) {
+ preference.bindRecursively(preferenceBindingFactory, preferences)
+ } else {
+ preferenceBindingFactory.bind(preference, preferences[preference.key])
+ }
+ }
+ }
+
+ private fun PreferenceBindingFactory.bind(
+ preference: Preference,
+ metadata: PreferenceMetadata?,
+ ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
new file mode 100644
index 0000000..7f99d7a
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+
+/** Factory to create preference screen. */
+class PreferenceScreenFactory {
+ /** Preference manager to create/inflate preference screen. */
+ val preferenceManager: PreferenceManager
+
+ /**
+ * Optional existing hierarchy to merge the new hierarchies into.
+ *
+ * Provide existing hierarchy will preserve the internal state (e.g. scrollbar position) for
+ * [PreferenceFragmentCompat].
+ */
+ private val rootScreen: PreferenceScreen?
+
+ /**
+ * Factory constructor from preference fragment.
+ *
+ * The fragment must be within a valid lifecycle.
+ */
+ constructor(preferenceFragment: PreferenceFragmentCompat) {
+ preferenceManager = preferenceFragment.preferenceManager
+ rootScreen = preferenceFragment.preferenceScreen
+ }
+
+ /** Factory constructor from [Context]. */
+ constructor(context: Context) : this(PreferenceManager(context))
+
+ /** Factory constructor from [PreferenceManager]. */
+ constructor(preferenceManager: PreferenceManager) {
+ this.preferenceManager = preferenceManager
+ rootScreen = null
+ }
+
+ /** Context of the factory to create preference screen. */
+ val context: Context
+ get() = preferenceManager.context
+
+ /** Returns the existing hierarchy or create a new empty preference screen. */
+ fun getOrCreatePreferenceScreen(): PreferenceScreen =
+ rootScreen ?: preferenceManager.createPreferenceScreen(context)
+
+ /**
+ * Inflates [PreferenceScreen] from xml resource.
+ *
+ * @param xmlRes The resource ID of the XML to inflate
+ * @return The root hierarchy (if one was not provided, the new hierarchy's root)
+ */
+ fun inflate(xmlRes: Int): PreferenceScreen? =
+ if (xmlRes != 0) {
+ preferenceManager.inflateFromResource(preferenceManager.context, xmlRes, rootScreen)
+ } else {
+ rootScreen
+ }
+
+ /**
+ * Creates [PreferenceScreen] of given key.
+ *
+ * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy.
+ */
+ fun createBindingScreen(screenKey: String?): PreferenceScreen? {
+ val metadata = PreferenceScreenRegistry[screenKey] ?: return null
+ if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) {
+ return metadata.createPreferenceScreen(this)
+ }
+ return null
+ }
+
+ companion object {
+ /** Creates [PreferenceScreen] from [PreferenceScreenRegistry]. */
+ @JvmStatic
+ fun createBindingScreen(preference: Preference): PreferenceScreen? {
+ val preferenceScreenCreator =
+ (PreferenceScreenRegistry[preference.key] as? PreferenceScreenCreator)
+ ?: return null
+ if (!preferenceScreenCreator.hasCompleteHierarchy()) return null
+ val factory = PreferenceScreenFactory(preference.context)
+ val preferenceScreen = preferenceScreenCreator.createPreferenceScreen(factory)
+ factory.preferenceManager.setPreferences(preferenceScreen)
+ return preferenceScreen
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
new file mode 100644
index 0000000..0573292
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.PreferenceScreen
+
+/**
+ * Interface to provide [PreferenceScreen].
+ *
+ * When implemented by Activity/Fragment, the Activity/Fragment [Context] APIs (e.g. `getContext()`,
+ * `getActivity()`) MUST not be used: preference screen creation could happen in background service,
+ * where the Activity/Fragment lifecycle callbacks (`onCreate`, `onDestroy`, etc.) are not invoked
+ * and context APIs return null.
+ */
+interface PreferenceScreenProvider {
+
+ /**
+ * Creates [PreferenceScreen].
+ *
+ * Preference screen creation could happen in background service. The implementation MUST use
+ * [PreferenceScreenFactory.context] to obtain context.
+ */
+ fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen?
+}