Import SettingsLib/Graph library
Bug: 365886733
Flag: EXEMPT Import library
Test: m SettingsLibGraph
Change-Id: Iea1761383714c87699be9d323076ae0a5909aa9c
diff --git a/packages/SettingsLib/Graph/Android.bp b/packages/SettingsLib/Graph/Android.bp
index e2ed1e4..163b689 100644
--- a/packages/SettingsLib/Graph/Android.bp
+++ b/packages/SettingsLib/Graph/Android.bp
@@ -4,7 +4,7 @@
filegroup {
name: "SettingsLibGraph-srcs",
- srcs: ["src/**/*"],
+ srcs: ["src/**/*.kt"],
}
android_library {
@@ -14,8 +14,24 @@
],
srcs: [":SettingsLibGraph-srcs"],
static_libs: [
+ "SettingsLibGraph-proto-lite",
+ "SettingsLibIpc",
+ "SettingsLibMetadata",
+ "SettingsLibPreference",
"androidx.annotation_annotation",
+ "androidx.fragment_fragment",
"androidx.preference_preference",
],
kotlincflags: ["-Xjvm-default=all"],
}
+
+java_library {
+ name: "SettingsLibGraph-proto-lite",
+ srcs: ["graph.proto"],
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "core_current",
+ static_libs: ["libprotobuf-java-lite"],
+}
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto
new file mode 100644
index 0000000..e93d756
--- /dev/null
+++ b/packages/SettingsLib/Graph/graph.proto
@@ -0,0 +1,156 @@
+syntax = "proto3";
+
+package com.android.settingslib.graph;
+
+option java_package = "com.android.settingslib.graph.proto";
+option java_multiple_files = true;
+
+// Proto represents preference graph.
+message PreferenceGraphProto {
+ // Preference screens appear in the graph.
+ // Key: preference key of the PreferenceScreen. Value: PreferenceScreen.
+ map<string, PreferenceScreenProto> screens = 1;
+ // Roots of the graph.
+ // Each element is a preference key of the PreferenceScreen.
+ repeated string roots = 2;
+ // Activities appear in the graph.
+ // Key: activity class. Value: preference key of associated PreferenceScreen.
+ map<string, string> activity_screens = 3;
+}
+
+// Proto of PreferenceScreen.
+message PreferenceScreenProto {
+ // Intent to show the PreferenceScreen.
+ optional IntentProto intent = 1;
+ // Root of the PreferenceScreen hierarchy.
+ optional PreferenceGroupProto root = 2;
+ // If the preference screen provides complete hierarchy by source code.
+ optional bool complete_hierarchy = 3;
+}
+
+// Proto of PreferenceGroup.
+message PreferenceGroupProto {
+ // Self information of PreferenceGroup.
+ optional PreferenceProto preference = 1;
+ // A list of children.
+ repeated PreferenceOrGroupProto preferences = 2;
+}
+
+// Proto represents either PreferenceProto or PreferenceGroupProto.
+message PreferenceOrGroupProto {
+ oneof kind {
+ // It is a Preference.
+ PreferenceProto preference = 1;
+ // It is a PreferenceGroup.
+ PreferenceGroupProto group = 2;
+ }
+}
+
+// Proto of Preference.
+message PreferenceProto {
+ // Key of the preference.
+ optional string key = 1;
+ // Title of the preference.
+ optional TextProto title = 2;
+ // Summary of the preference.
+ optional TextProto summary = 3;
+ // Icon of the preference.
+ optional int32 icon = 4;
+ // Additional keywords for indexing.
+ optional int32 keywords = 5;
+ // Extras of the preference.
+ optional BundleProto extras = 6;
+ // Whether the preference is indexable.
+ optional bool indexable = 7;
+ // Whether the preference is enabled.
+ optional bool enabled = 8;
+ // Whether the preference is available/visible.
+ optional bool available = 9;
+ // Whether the preference is persistent.
+ optional bool persistent = 10;
+ // Whether the preference is restricted by managed configurations.
+ optional bool restricted = 11;
+ // Target of the preference action.
+ optional ActionTarget action_target = 12;
+ // Preference value (if present, it means `persistent` is true).
+ optional PreferenceValueProto value = 13;
+
+ // Target of an Intent
+ message ActionTarget {
+ oneof kind {
+ // Resolved key of the preference screen located in current app.
+ // This is resolved from android:fragment or activity of current app.
+ string key = 1;
+ // Unresolvable Intent that is either an unrecognized activity of current
+ // app or activity belongs to other app.
+ IntentProto intent = 2;
+ }
+ }
+}
+
+// Proto of string or string resource id.
+message TextProto {
+ oneof text {
+ int32 resource_id = 1;
+ string string = 2;
+ }
+}
+
+// Proto of preference value.
+message PreferenceValueProto {
+ oneof value {
+ bool boolean_value = 1;
+ }
+}
+
+// Proto of android.content.Intent
+message IntentProto {
+ // The action of the Intent.
+ optional string action = 1;
+
+ // The data attribute of the Intent, expressed as a URI.
+ optional string data = 2;
+
+ // The package attribute of the Intent, which may be set to force the
+ // detection of a particular application package that can handle the event.
+ optional string pkg = 3;
+
+ // The component attribute of the Intent, which may be set to force the
+ // detection of a particular component (app). If present, this must be a
+ // package name followed by a '/' and then followed by the class name.
+ optional string component = 4;
+
+ // Flags controlling how intent is handled. The value must be bitwise OR of
+ // intent flag constants defined by Android.
+ // http://developer.android.com/reference/android/content/Intent.html#setFlags(int)
+ optional int32 flags = 5;
+
+ // Extended data from the intent.
+ optional BundleProto extras = 6;
+
+ // The MIME type of the Intent (e.g. "text/plain").
+ //
+ // For more information, see
+ // https://developer.android.com/reference/android/content/Intent#setType(java.lang.String).
+ optional string mime_type = 7;
+}
+
+// Proto of android.os.Bundle
+message BundleProto {
+ // Bundle data.
+ map<string, BundleValue> values = 1;
+
+ message BundleValue {
+ // Bundle data value for the associated key name.
+ // Can be extended to support other types of bundled data.
+ oneof value {
+ string string_value = 1;
+ bytes bytes_value = 2;
+ int32 int_value = 3;
+ int64 long_value = 4;
+ bool boolean_value = 5;
+ double double_value = 6;
+ BundleProto bundle_value = 7;
+ }
+ }
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
new file mode 100644
index 0000000..04c2968
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.graph
+
+import android.app.Application
+import android.os.Bundle
+import com.android.settingslib.graph.proto.PreferenceGraphProto
+import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.MessageCodec
+import java.util.Locale
+
+/** API to get preference graph. */
+abstract class GetPreferenceGraphApiHandler(private val activityClasses: Set<String>) :
+ ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
+
+ override val requestCodec: MessageCodec<GetPreferenceGraphRequest>
+ get() = GetPreferenceGraphRequestCodec
+
+ override val responseCodec: MessageCodec<PreferenceGraphProto>
+ get() = PreferenceGraphProtoCodec
+
+ override suspend fun invoke(
+ application: Application,
+ myUid: Int,
+ callingUid: Int,
+ request: GetPreferenceGraphRequest,
+ ): PreferenceGraphProto {
+ val builderRequest =
+ if (request.activityClasses.isEmpty()) {
+ GetPreferenceGraphRequest(activityClasses, request.visitedScreens, request.locale)
+ } else {
+ request
+ }
+ return PreferenceGraphBuilder.of(application, builderRequest).build()
+ }
+}
+
+/**
+ * Request of [GetPreferenceGraphApiHandler].
+ *
+ * @param activityClasses activities of the preference graph
+ * @param visitedScreens keys of the visited preference screen
+ * @param locale locale of the preference graph
+ */
+data class GetPreferenceGraphRequest
+@JvmOverloads
+constructor(
+ val activityClasses: Set<String> = setOf(),
+ val visitedScreens: Set<String> = setOf(),
+ val locale: Locale? = null,
+ val includeValue: Boolean = true,
+)
+
+object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest> {
+ override fun encode(data: GetPreferenceGraphRequest): Bundle =
+ Bundle(3).apply {
+ putStringArray(KEY_ACTIVITIES, data.activityClasses.toTypedArray())
+ putStringArray(KEY_PREF_KEYS, data.visitedScreens.toTypedArray())
+ putString(KEY_LOCALE, data.locale?.toLanguageTag())
+ }
+
+ override fun decode(data: Bundle): GetPreferenceGraphRequest {
+ val activities = data.getStringArray(KEY_ACTIVITIES) ?: arrayOf()
+ val visitedScreens = data.getStringArray(KEY_PREF_KEYS) ?: arrayOf()
+ fun String?.toLocale() = if (this != null) Locale.forLanguageTag(this) else null
+ return GetPreferenceGraphRequest(
+ activities.toSet(),
+ visitedScreens.toSet(),
+ data.getString(KEY_LOCALE).toLocale(),
+ )
+ }
+
+ private const val KEY_ACTIVITIES = "activities"
+ private const val KEY_PREF_KEYS = "keys"
+ private const val KEY_LOCALE = "locale"
+}
+
+object PreferenceGraphProtoCodec : MessageCodec<PreferenceGraphProto> {
+ override fun encode(data: PreferenceGraphProto): Bundle =
+ Bundle(1).apply { putByteArray(KEY_GRAPH, data.toByteArray()) }
+
+ override fun decode(data: Bundle): PreferenceGraphProto =
+ PreferenceGraphProto.parseFrom(data.getByteArray(KEY_GRAPH)!!)
+
+ private const val KEY_GRAPH = "graph"
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
new file mode 100644
index 0000000..8c5d877
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -0,0 +1,445 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("DEPRECATION")
+
+package com.android.settingslib.graph
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import android.preference.PreferenceActivity
+import android.util.Log
+import androidx.fragment.app.Fragment
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import androidx.preference.TwoStatePreference
+import com.android.settingslib.graph.proto.PreferenceGraphProto
+import com.android.settingslib.graph.proto.PreferenceGroupProto
+import com.android.settingslib.graph.proto.PreferenceProto
+import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget
+import com.android.settingslib.graph.proto.PreferenceScreenProto
+import com.android.settingslib.graph.proto.TextProto
+import com.android.settingslib.metadata.BooleanValue
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceHierarchyNode
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceRestrictionProvider
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.metadata.PreferenceSummaryProvider
+import com.android.settingslib.metadata.PreferenceTitleProvider
+import com.android.settingslib.preference.PreferenceScreenFactory
+import com.android.settingslib.preference.PreferenceScreenProvider
+import java.util.Locale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+private const val TAG = "PreferenceGraphBuilder"
+
+/**
+ * Builder of preference graph.
+ *
+ * Only activity in current application is supported. To create preference graph across
+ * applications, use [crawlPreferenceGraph].
+ */
+class PreferenceGraphBuilder
+private constructor(private val context: Context, private val request: GetPreferenceGraphRequest) {
+ private val preferenceScreenFactory by lazy {
+ PreferenceScreenFactory(context.ofLocale(request.locale))
+ }
+ private val builder by lazy { PreferenceGraphProto.newBuilder() }
+ private val visitedScreens = mutableSetOf<String>().apply { addAll(request.visitedScreens) }
+ private val includeValue = request.includeValue
+
+ private suspend fun init() {
+ for (activityClass in request.activityClasses) {
+ add(activityClass)
+ }
+ }
+
+ fun build() = builder.build()
+
+ /** Adds an activity to the graph. */
+ suspend fun <T> add(activityClass: Class<T>) where T : Activity, T : PreferenceScreenProvider =
+ addPreferenceScreenProvider(activityClass)
+
+ /**
+ * Adds an activity to the graph.
+ *
+ * Reflection is used to create the instance. To avoid security vulnerability, the code ensures
+ * given [activityClassName] must be declared as an <activity> entry in AndroidManifest.xml.
+ */
+ suspend fun add(activityClassName: String) {
+ try {
+ val intent = Intent()
+ intent.setClassName(context, activityClassName)
+ if (context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ==
+ null) {
+ Log.e(TAG, "$activityClassName is not activity")
+ return
+ }
+ val activityClass = context.classLoader.loadClass(activityClassName)
+ if (addPreferenceScreenKeyProvider(activityClass)) return
+ if (PreferenceScreenProvider::class.java.isAssignableFrom(activityClass)) {
+ addPreferenceScreenProvider(activityClass)
+ } else {
+ Log.w(TAG, "$activityClass does not implement PreferenceScreenProvider")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Fail to add $activityClassName", e)
+ }
+ }
+
+ private suspend fun addPreferenceScreenKeyProvider(activityClass: Class<*>): Boolean {
+ if (!PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(activityClass)) {
+ return false
+ }
+ val key = getPreferenceScreenKey { activityClass.newInstance() } ?: return false
+ if (addPreferenceScreenFromRegistry(key, activityClass)) {
+ builder.addRoots(key)
+ return true
+ }
+ return false
+ }
+
+ private suspend fun getPreferenceScreenKey(newInstance: () -> Any): String? =
+ withContext(Dispatchers.Main) {
+ try {
+ val instance = newInstance()
+ if (instance is PreferenceScreenBindingKeyProvider) {
+ return@withContext instance.getPreferenceScreenBindingKey(context)
+ } else {
+ Log.w(TAG, "$instance is not PreferenceScreenKeyProvider")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "getPreferenceScreenKey failed", e)
+ }
+ null
+ }
+
+ private suspend fun addPreferenceScreenFromRegistry(
+ key: String,
+ activityClass: Class<*>,
+ ): Boolean {
+ val metadata = PreferenceScreenRegistry[key] ?: return false
+ if (!metadata.hasCompleteHierarchy()) return false
+ return addPreferenceScreenMetadata(metadata, activityClass)
+ }
+
+ private suspend fun addPreferenceScreenMetadata(
+ metadata: PreferenceScreenMetadata,
+ activityClass: Class<*>,
+ ): Boolean =
+ addPreferenceScreen(metadata.key, activityClass) {
+ preferenceScreenProto {
+ completeHierarchy = true
+ root = metadata.getPreferenceHierarchy(context).toProto(activityClass, true)
+ }
+ }
+
+ private suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
+ Log.d(TAG, "add $activityClass")
+ createPreferenceScreen { activityClass.newInstance() }
+ ?.let {
+ addPreferenceScreen(Intent(context, activityClass), activityClass, it)
+ builder.addRoots(it.key)
+ }
+ }
+
+ /**
+ * Creates [PreferenceScreen].
+ *
+ * Androidx Activity/Fragment instance must be created in main thread, otherwise an exception is
+ * raised.
+ */
+ private suspend fun createPreferenceScreen(newInstance: () -> Any): PreferenceScreen? =
+ withContext(Dispatchers.Main) {
+ try {
+ val instance = newInstance()
+ Log.d(TAG, "createPreferenceScreen $instance")
+ if (instance is PreferenceScreenProvider) {
+ return@withContext instance.createPreferenceScreen(preferenceScreenFactory)
+ } else {
+ Log.w(TAG, "$instance is not PreferenceScreenProvider")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "createPreferenceScreen failed", e)
+ }
+ return@withContext null
+ }
+
+ private suspend fun addPreferenceScreen(
+ intent: Intent,
+ activityClass: Class<*>,
+ preferenceScreen: PreferenceScreen?,
+ ) {
+ val key = preferenceScreen?.key
+ if (key.isNullOrEmpty()) {
+ Log.e(TAG, "$activityClass \"$preferenceScreen\" has no key")
+ return
+ }
+ @Suppress("CheckReturnValue")
+ addPreferenceScreen(key, activityClass) { preferenceScreen.toProto(intent, activityClass) }
+ }
+
+ private suspend fun addPreferenceScreen(
+ key: String,
+ activityClass: Class<*>,
+ preferenceScreenProvider: suspend () -> PreferenceScreenProto,
+ ): Boolean {
+ if (!visitedScreens.add(key)) {
+ Log.w(TAG, "$activityClass $key visited")
+ return false
+ }
+ val activityClassName = activityClass.name
+ val associatedKey = builder.getActivityScreensOrDefault(activityClassName, null)
+ if (associatedKey == null) {
+ builder.putActivityScreens(activityClassName, key)
+ } else if (associatedKey != key) {
+ Log.w(TAG, "Dup $activityClassName association, old: $associatedKey, new: $key")
+ }
+ builder.putScreens(key, preferenceScreenProvider())
+ return true
+ }
+
+ private suspend fun PreferenceScreen.toProto(
+ intent: Intent,
+ activityClass: Class<*>,
+ ): PreferenceScreenProto = preferenceScreenProto {
+ this.intent = intent.toProto()
+ root = (this@toProto as PreferenceGroup).toProto(activityClass)
+ }
+
+ private suspend fun PreferenceGroup.toProto(activityClass: Class<*>): PreferenceGroupProto =
+ preferenceGroupProto {
+ preference = (this@toProto as Preference).toProto(activityClass)
+ for (index in 0 until preferenceCount) {
+ val child = getPreference(index)
+ addPreferences(
+ preferenceOrGroupProto {
+ if (child is PreferenceGroup) {
+ group = child.toProto(activityClass)
+ } else {
+ preference = child.toProto(activityClass)
+ }
+ })
+ }
+ }
+
+ private suspend fun Preference.toProto(activityClass: Class<*>): PreferenceProto =
+ preferenceProto {
+ this@toProto.key?.let { key = it }
+ this@toProto.title?.let { title = textProto { string = it.toString() } }
+ this@toProto.summary?.let { summary = textProto { string = it.toString() } }
+ val preferenceExtras = peekExtras()
+ preferenceExtras?.let { extras = it.toProto() }
+ enabled = isEnabled
+ available = isVisible
+ persistent = isPersistent
+ if (includeValue && isPersistent && this@toProto is TwoStatePreference) {
+ value = preferenceValueProto { booleanValue = this@toProto.isChecked }
+ }
+ this@toProto.fragment.toActionTarget(activityClass, preferenceExtras)?.let {
+ actionTarget = it
+ return@preferenceProto
+ }
+ this@toProto.intent?.let { actionTarget = it.toActionTarget() }
+ }
+
+ private suspend fun PreferenceHierarchy.toProto(
+ activityClass: Class<*>,
+ isRoot: Boolean,
+ ): PreferenceGroupProto = preferenceGroupProto {
+ preference = toProto(this@toProto, activityClass, isRoot)
+ forEachAsync {
+ addPreferences(
+ preferenceOrGroupProto {
+ if (it is PreferenceHierarchy) {
+ group = it.toProto(activityClass, false)
+ } else {
+ preference = toProto(it, activityClass, false)
+ }
+ })
+ }
+ }
+
+ private suspend fun toProto(
+ node: PreferenceHierarchyNode,
+ activityClass: Class<*>,
+ isRoot: Boolean,
+ ) = preferenceProto {
+ val metadata = node.metadata
+ key = metadata.key
+ metadata.getTitleTextProto(isRoot)?.let { title = it }
+ if (metadata.summary != 0) {
+ summary = textProto { resourceId = metadata.summary }
+ } else {
+ (metadata as? PreferenceSummaryProvider)?.getSummary(context)?.let {
+ summary = textProto { string = it.toString() }
+ }
+ }
+ if (metadata.icon != 0) icon = metadata.icon
+ if (metadata.keywords != 0) keywords = metadata.keywords
+ val preferenceExtras = metadata.extras(context)
+ preferenceExtras?.let { extras = it.toProto() }
+ indexable = metadata.isIndexable(context)
+ enabled = metadata.isEnabled(context)
+ if (metadata is PreferenceAvailabilityProvider) {
+ available = metadata.isAvailable(context)
+ }
+ if (metadata is PreferenceRestrictionProvider) {
+ restricted = metadata.isRestricted(context)
+ }
+ persistent = metadata.isPersistent(context)
+ if (includeValue &&
+ persistent &&
+ metadata is BooleanValue &&
+ metadata is PersistentPreference<*>) {
+ metadata.storage(context).getValue(metadata.key, Boolean::class.javaObjectType)?.let {
+ value = preferenceValueProto { booleanValue = it }
+ }
+ }
+ if (metadata is PreferenceScreenMetadata) {
+ if (metadata.hasCompleteHierarchy()) {
+ @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata, activityClass)
+ } else {
+ metadata.fragmentClass()?.toActionTarget(activityClass, preferenceExtras)?.let {
+ actionTarget = it
+ }
+ }
+ }
+ metadata.intent(context)?.let { actionTarget = it.toActionTarget() }
+ }
+
+ private fun PreferenceMetadata.getTitleTextProto(isRoot: Boolean): TextProto? {
+ if (isRoot && this is PreferenceScreenMetadata) {
+ val titleRes = screenTitle
+ if (titleRes != 0) {
+ return textProto { resourceId = titleRes }
+ } else {
+ getScreenTitle(context)?.let {
+ return textProto { string = it.toString() }
+ }
+ }
+ } else {
+ val titleRes = title
+ if (titleRes != 0) {
+ return textProto { resourceId = titleRes }
+ }
+ }
+ return (this as? PreferenceTitleProvider)?.getTitle(context)?.let {
+ textProto { string = it.toString() }
+ }
+ }
+
+ private suspend fun String?.toActionTarget(
+ activityClass: Class<*>,
+ extras: Bundle?,
+ ): ActionTarget? {
+ if (this.isNullOrEmpty()) return null
+ try {
+ val fragmentClass = context.classLoader.loadClass(this)
+ if (Fragment::class.java.isAssignableFrom(fragmentClass)) {
+ @Suppress("UNCHECKED_CAST")
+ return (fragmentClass as Class<out Fragment>).toActionTarget(activityClass, extras)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Cannot loadClass $this", e)
+ }
+ return null
+ }
+
+ private suspend fun Class<out Fragment>.toActionTarget(
+ activityClass: Class<*>,
+ extras: Bundle?,
+ ): ActionTarget {
+ val startIntent = Intent(context, activityClass)
+ startIntent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, name)
+ extras?.let { startIntent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, it) }
+ if (!PreferenceScreenProvider::class.java.isAssignableFrom(this) &&
+ !PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(this)) {
+ return actionTargetProto { intent = startIntent.toProto() }
+ }
+ val fragment =
+ withContext(Dispatchers.Main) {
+ return@withContext try {
+ newInstance().apply { arguments = extras }
+ } catch (e: Exception) {
+ Log.e(TAG, "Fail to instantiate fragment ${this@toActionTarget}", e)
+ null
+ }
+ }
+ if (fragment is PreferenceScreenBindingKeyProvider) {
+ val screenKey = fragment.getPreferenceScreenBindingKey(context)
+ if (screenKey != null && addPreferenceScreenFromRegistry(screenKey, activityClass)) {
+ return actionTargetProto { key = screenKey }
+ }
+ }
+ if (fragment is PreferenceScreenProvider) {
+ val screen = fragment.createPreferenceScreen(preferenceScreenFactory)
+ if (screen != null) {
+ addPreferenceScreen(startIntent, activityClass, screen)
+ return actionTargetProto { key = screen.key }
+ }
+ }
+ return actionTargetProto { intent = startIntent.toProto() }
+ }
+
+ private suspend fun Intent.toActionTarget(): ActionTarget {
+ if (component?.packageName == "") {
+ setClassName(context, component!!.className)
+ }
+ resolveActivity(context.packageManager)?.let {
+ if (it.packageName == context.packageName) {
+ add(it.className)
+ }
+ }
+ return actionTargetProto { intent = toProto() }
+ }
+
+ companion object {
+ suspend fun of(context: Context, request: GetPreferenceGraphRequest) =
+ PreferenceGraphBuilder(context, request).also { it.init() }
+ }
+}
+
+@SuppressLint("AppBundleLocaleChanges")
+internal fun Context.ofLocale(locale: Locale?): Context {
+ if (locale == null) return this
+ val baseConfig: Configuration = resources.configuration
+ val baseLocale =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ baseConfig.locales[0]
+ } else {
+ baseConfig.locale
+ }
+ if (locale == baseLocale) {
+ return this
+ }
+ val newConfig = Configuration(baseConfig)
+ newConfig.setLocale(locale)
+ return createConfigurationContext(newConfig)
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenManager.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenManager.kt
deleted file mode 100644
index 9231f40..0000000
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenManager.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.android.settingslib.graph
-
-import androidx.annotation.StringRes
-import androidx.annotation.XmlRes
-import androidx.preference.Preference
-import androidx.preference.PreferenceManager
-import androidx.preference.PreferenceScreen
-
-/** Manager to create and initialize preference screen. */
-class PreferenceScreenManager(private val preferenceManager: PreferenceManager) {
- private val context = preferenceManager.context
- // the map will preserve order
- private val updaters = mutableMapOf<String, PreferenceUpdater>()
- private val screenUpdaters = mutableListOf<PreferenceScreenUpdater>()
-
- /** Creates an empty [PreferenceScreen]. */
- fun createPreferenceScreen(): PreferenceScreen =
- preferenceManager.createPreferenceScreen(context)
-
- /** Creates [PreferenceScreen] from resource. */
- fun createPreferenceScreen(@XmlRes xmlRes: Int): PreferenceScreen =
- preferenceManager.inflateFromResource(context, xmlRes, null)
-
- /** Adds updater for given preference. */
- fun addPreferenceUpdater(@StringRes key: Int, updater: PreferenceUpdater) =
- addPreferenceUpdater(context.getString(key), updater)
-
- /** Adds updater for given preference. */
- fun addPreferenceUpdater(
- key: String,
- updater: PreferenceUpdater,
- ): PreferenceScreenManager {
- updaters.put(key, updater)?.let { if (it != updater) throw IllegalArgumentException() }
- return this
- }
-
- /** Adds updater for preference screen. */
- fun addPreferenceScreenUpdater(updater: PreferenceScreenUpdater): PreferenceScreenManager {
- screenUpdaters.add(updater)
- return this
- }
-
- /** Adds a list of updaters for preference screen. */
- fun addPreferenceScreenUpdater(
- vararg updaters: PreferenceScreenUpdater,
- ): PreferenceScreenManager {
- screenUpdaters.addAll(updaters)
- return this
- }
-
- /** Updates preference screen with registered updaters. */
- fun updatePreferenceScreen(preferenceScreen: PreferenceScreen) {
- for ((key, updater) in updaters) {
- preferenceScreen.findPreference<Preference>(key)?.let { updater.updatePreference(it) }
- }
- for (updater in screenUpdaters) {
- updater.updatePreferenceScreen(preferenceScreen)
- }
- }
-}
-
-/** Updater of [Preference]. */
-interface PreferenceUpdater {
- fun updatePreference(preference: Preference)
-}
-
-/** Updater of [PreferenceScreen]. */
-interface PreferenceScreenUpdater {
- fun updatePreferenceScreen(preferenceScreen: PreferenceScreen)
-}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenProvider.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenProvider.kt
deleted file mode 100644
index 9e4c1f6..0000000
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenProvider.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.android.settingslib.graph
-
-import android.content.Context
-import androidx.preference.PreferenceScreen
-
-/**
- * Interface to provide [PreferenceScreen].
- *
- * It is expected to be implemented by Activity/Fragment and the implementation needs to use
- * [Context] APIs (e.g. `getContext()`, `getActivity()`) with caution: preference screen creation
- * could happen in background service, where the Activity/Fragment lifecycle callbacks (`onCreate`,
- * `onDestroy`, etc.) are not invoked.
- */
-interface PreferenceScreenProvider {
-
- /**
- * Creates [PreferenceScreen].
- *
- * Preference screen creation could happen in background service. The implementation MUST use
- * given [context] instead of APIs like `getContext()`, `getActivity()`, etc.
- */
- fun createPreferenceScreen(
- context: Context,
- preferenceScreenManager: PreferenceScreenManager,
- ): PreferenceScreen?
-}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
new file mode 100644
index 0000000..d9b9590
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.graph
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.android.settingslib.graph.proto.BundleProto
+import com.android.settingslib.graph.proto.BundleProto.BundleValue
+import com.android.settingslib.graph.proto.IntentProto
+import com.android.settingslib.graph.proto.TextProto
+import com.google.protobuf.ByteString
+
+fun TextProto.getText(context: Context): String? =
+ when {
+ hasResourceId() -> context.getString(resourceId)
+ hasString() -> string
+ else -> null
+ }
+
+fun Intent.toProto(): IntentProto = intentProto {
+ this@toProto.action?.let { action = it }
+ this@toProto.dataString?.let { data = it }
+ this@toProto.`package`?.let { pkg = it }
+ this@toProto.component?.let { component = it.flattenToShortString() }
+ this@toProto.flags.let { if (it != 0) flags = it }
+ this@toProto.extras?.let { extras = it.toProto() }
+ this@toProto.type?.let { mimeType = it }
+}
+
+fun Bundle.toProto(): BundleProto = bundleProto {
+ fun toProto(value: Any): BundleValue = bundleValueProto {
+ when (value) {
+ is String -> stringValue = value
+ is ByteArray -> bytesValue = ByteString.copyFrom(value)
+ is Int -> intValue = value
+ is Long -> longValue = value
+ is Boolean -> booleanValue = value
+ is Double -> doubleValue = value
+ is Bundle -> bundleValue = value.toProto()
+ else -> throw IllegalArgumentException("Unknown type: ${value.javaClass} $value")
+ }
+ }
+
+ for (key in keySet()) {
+ @Suppress("DEPRECATION") get(key)?.let { putValues(key, toProto(it)) }
+ }
+}
+
+fun BundleValue.stringify(): String =
+ when {
+ hasBooleanValue() -> "$valueCase"
+ hasBytesValue() -> "$bytesValue"
+ hasIntValue() -> "$intValue"
+ hasLongValue() -> "$longValue"
+ hasStringValue() -> stringValue
+ hasDoubleValue() -> "$doubleValue"
+ hasBundleValue() -> "$bundleValue"
+ else -> "Unknown"
+ }
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt
new file mode 100644
index 0000000..d7dae77
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.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.graph
+
+import com.android.settingslib.graph.proto.BundleProto
+import com.android.settingslib.graph.proto.BundleProto.BundleValue
+import com.android.settingslib.graph.proto.IntentProto
+import com.android.settingslib.graph.proto.PreferenceGroupProto
+import com.android.settingslib.graph.proto.PreferenceOrGroupProto
+import com.android.settingslib.graph.proto.PreferenceProto
+import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget
+import com.android.settingslib.graph.proto.PreferenceScreenProto
+import com.android.settingslib.graph.proto.PreferenceValueProto
+import com.android.settingslib.graph.proto.TextProto
+
+/** Returns root or null. */
+val PreferenceScreenProto.rootOrNull
+ get() = if (hasRoot()) root else null
+
+/** Kotlin DSL-style builder for [PreferenceScreenProto]. */
+@JvmSynthetic
+inline fun preferenceScreenProto(init: PreferenceScreenProto.Builder.() -> Unit) =
+ PreferenceScreenProto.newBuilder().also(init).build()
+
+/** Returns preference or null. */
+val PreferenceOrGroupProto.preferenceOrNull
+ get() = if (hasPreference()) preference else null
+
+/** Returns group or null. */
+val PreferenceOrGroupProto.groupOrNull
+ get() = if (hasGroup()) group else null
+
+/** Kotlin DSL-style builder for [PreferenceOrGroupProto]. */
+@JvmSynthetic
+inline fun preferenceOrGroupProto(init: PreferenceOrGroupProto.Builder.() -> Unit) =
+ PreferenceOrGroupProto.newBuilder().also(init).build()
+
+/** Returns preference or null. */
+val PreferenceGroupProto.preferenceOrNull
+ get() = if (hasPreference()) preference else null
+
+/** Kotlin DSL-style builder for [PreferenceGroupProto]. */
+@JvmSynthetic
+inline fun preferenceGroupProto(init: PreferenceGroupProto.Builder.() -> Unit) =
+ PreferenceGroupProto.newBuilder().also(init).build()
+
+/** Returns title or null. */
+val PreferenceProto.titleOrNull
+ get() = if (hasTitle()) title else null
+
+/** Returns summary or null. */
+val PreferenceProto.summaryOrNull
+ get() = if (hasSummary()) summary else null
+
+/** Returns actionTarget or null. */
+val PreferenceProto.actionTargetOrNull
+ get() = if (hasActionTarget()) actionTarget else null
+
+/** Kotlin DSL-style builder for [PreferenceProto]. */
+@JvmSynthetic
+inline fun preferenceProto(init: PreferenceProto.Builder.() -> Unit) =
+ PreferenceProto.newBuilder().also(init).build()
+
+/** Returns intent or null. */
+val ActionTarget.intentOrNull
+ get() = if (hasIntent()) intent else null
+
+/** Kotlin DSL-style builder for [ActionTarget]. */
+@JvmSynthetic
+inline fun actionTargetProto(init: ActionTarget.Builder.() -> Unit) =
+ ActionTarget.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [PreferenceValueProto]. */
+@JvmSynthetic
+inline fun preferenceValueProto(init: PreferenceValueProto.Builder.() -> Unit) =
+ PreferenceValueProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [TextProto]. */
+@JvmSynthetic
+inline fun textProto(init: TextProto.Builder.() -> Unit) = TextProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [IntentProto]. */
+@JvmSynthetic
+inline fun intentProto(init: IntentProto.Builder.() -> Unit) =
+ IntentProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [BundleProto]. */
+@JvmSynthetic
+inline fun bundleProto(init: BundleProto.Builder.() -> Unit) =
+ BundleProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [BundleValue]. */
+@JvmSynthetic
+inline fun bundleValueProto(init: BundleValue.Builder.() -> Unit) =
+ BundleValue.newBuilder().also(init).build()