[Catalyst] Support Set API

Bug: 351941813
Flag: EXEMPT library
Test: Manual
Change-Id: I3620be77f3bb9354876946b0f4ef5006cf6f6d7f
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
index 04c2968..fdffe5d 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -21,10 +21,11 @@
 import com.android.settingslib.graph.proto.PreferenceGraphProto
 import com.android.settingslib.ipc.ApiHandler
 import com.android.settingslib.ipc.MessageCodec
+import com.android.settingslib.metadata.PreferenceScreenRegistry
 import java.util.Locale
 
 /** API to get preference graph. */
-abstract class GetPreferenceGraphApiHandler(private val activityClasses: Set<String>) :
+abstract class GetPreferenceGraphApiHandler :
     ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
 
     override val requestCodec: MessageCodec<GetPreferenceGraphRequest>
@@ -40,8 +41,9 @@
         request: GetPreferenceGraphRequest,
     ): PreferenceGraphProto {
         val builderRequest =
-            if (request.activityClasses.isEmpty()) {
-                GetPreferenceGraphRequest(activityClasses, request.visitedScreens, request.locale)
+            if (request.screenKeys.isEmpty()) {
+                val keys = PreferenceScreenRegistry.preferenceScreens.keys
+                GetPreferenceGraphRequest(keys, request.visitedScreens, request.locale)
             } else {
                 request
             }
@@ -52,14 +54,14 @@
 /**
  * Request of [GetPreferenceGraphApiHandler].
  *
- * @param activityClasses activities of the preference graph
+ * @param screenKeys screen keys 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 screenKeys: Set<String> = setOf(),
     val visitedScreens: Set<String> = setOf(),
     val locale: Locale? = null,
     val includeValue: Boolean = true,
@@ -68,25 +70,25 @@
 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())
+            putStringArray(KEY_SCREEN_KEYS, data.screenKeys.toTypedArray())
+            putStringArray(KEY_VISITED_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()
+        val screenKeys = data.getStringArray(KEY_SCREEN_KEYS) ?: arrayOf()
+        val visitedScreens = data.getStringArray(KEY_VISITED_KEYS) ?: arrayOf()
         fun String?.toLocale() = if (this != null) Locale.forLanguageTag(this) else null
         return GetPreferenceGraphRequest(
-            activities.toSet(),
+            screenKeys.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"
+    private const val KEY_SCREEN_KEYS = "k"
+    private const val KEY_VISITED_KEYS = "v"
+    private const val KEY_LOCALE = "l"
 }
 
 object PreferenceGraphProtoCodec : MessageCodec<PreferenceGraphProto> {
@@ -96,5 +98,5 @@
     override fun decode(data: Bundle): PreferenceGraphProto =
         PreferenceGraphProto.parseFrom(data.getByteArray(KEY_GRAPH)!!)
 
-    private const val KEY_GRAPH = "graph"
+    private const val KEY_GRAPH = "g"
 }
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index 8fb16d8..5e78a29 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -19,14 +19,12 @@
 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
@@ -53,18 +51,13 @@
 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
+import java.util.Locale
 
 private const val TAG = "PreferenceGraphBuilder"
 
-/**
- * Builder of preference graph.
- *
- * Only activity in current application is supported. To create preference graph across
- * applications, use [crawlPreferenceGraph].
- */
+/** Builder of preference graph. */
 class PreferenceGraphBuilder
 private constructor(private val context: Context, private val request: GetPreferenceGraphRequest) {
     private val preferenceScreenFactory by lazy {
@@ -75,21 +68,13 @@
     private val includeValue = request.includeValue
 
     private suspend fun init() {
-        for (activityClass in request.activityClasses) {
-            add(activityClass)
-        }
-        // Temporarily add all screens
-        for (key in PreferenceScreenRegistry.preferenceScreens.keys) {
-            addPreferenceScreenFromRegistry(key, Activity::class.java)
+        for (key in request.screenKeys) {
+            addPreferenceScreenFromRegistry(key)
         }
     }
 
     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.
      *
@@ -100,8 +85,10 @@
         try {
             val intent = Intent()
             intent.setClassName(context, activityClassName)
-            if (context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ==
-                null) {
+            if (
+                context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ==
+                    null
+            ) {
                 Log.e(TAG, "$activityClassName is not activity")
                 return
             }
@@ -122,7 +109,7 @@
             return false
         }
         val key = getPreferenceScreenKey { activityClass.newInstance() } ?: return false
-        if (addPreferenceScreenFromRegistry(key, activityClass)) {
+        if (addPreferenceScreenFromRegistry(key)) {
             builder.addRoots(key)
             return true
         }
@@ -144,23 +131,16 @@
             null
         }
 
-    private suspend fun addPreferenceScreenFromRegistry(
-        key: String,
-        activityClass: Class<*>,
-    ): Boolean {
+    private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
         val metadata = PreferenceScreenRegistry[key] ?: return false
-        if (!metadata.hasCompleteHierarchy()) return false
-        return addPreferenceScreenMetadata(metadata, activityClass)
+        return addPreferenceScreenMetadata(metadata)
     }
 
-    private suspend fun addPreferenceScreenMetadata(
-        metadata: PreferenceScreenMetadata,
-        activityClass: Class<*>,
-    ): Boolean =
-        addPreferenceScreen(metadata.key, activityClass) {
+    private suspend fun addPreferenceScreenMetadata(metadata: PreferenceScreenMetadata): Boolean =
+        addPreferenceScreen(metadata.key) {
             preferenceScreenProto {
-                completeHierarchy = true
-                root = metadata.getPreferenceHierarchy(context).toProto(activityClass, true)
+                completeHierarchy = metadata.hasCompleteHierarchy()
+                root = metadata.getPreferenceHierarchy(context).toProto(true)
             }
         }
 
@@ -168,7 +148,7 @@
         Log.d(TAG, "add $activityClass")
         createPreferenceScreen { activityClass.newInstance() }
             ?.let {
-                addPreferenceScreen(Intent(context, activityClass), activityClass, it)
+                addPreferenceScreen(Intent(context, activityClass), it)
                 builder.addRoots(it.key)
             }
     }
@@ -195,106 +175,85 @@
             return@withContext null
         }
 
-    private suspend fun addPreferenceScreen(
-        intent: Intent,
-        activityClass: Class<*>,
-        preferenceScreen: PreferenceScreen?,
-    ) {
+    private suspend fun addPreferenceScreen(intent: Intent, preferenceScreen: PreferenceScreen?) {
         val key = preferenceScreen?.key
         if (key.isNullOrEmpty()) {
-            Log.e(TAG, "$activityClass \"$preferenceScreen\" has no key")
+            Log.e(TAG, "\"$preferenceScreen\" has no key")
             return
         }
-        @Suppress("CheckReturnValue")
-        addPreferenceScreen(key, activityClass) { preferenceScreen.toProto(intent, activityClass) }
+        @Suppress("CheckReturnValue") addPreferenceScreen(key) { preferenceScreen.toProto(intent) }
     }
 
     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)
-                        }
-                    })
-            }
+    ): Boolean =
+        if (visitedScreens.add(key)) {
+            builder.putScreens(key, preferenceScreenProvider())
+            true
+        } else {
+            Log.w(TAG, "$key visited")
+            false
         }
 
-    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 PreferenceScreen.toProto(intent: Intent?): PreferenceScreenProto =
+        preferenceScreenProto {
+            intent?.let { this.intent = it.toProto() }
+            root = (this@toProto as PreferenceGroup).toProto()
         }
 
-    private suspend fun PreferenceHierarchy.toProto(
-        activityClass: Class<*>,
-        isRoot: Boolean,
-    ): PreferenceGroupProto = preferenceGroupProto {
-        preference = toProto(this@toProto, activityClass, isRoot)
-        forEachAsync {
+    private suspend fun PreferenceGroup.toProto(): PreferenceGroupProto = preferenceGroupProto {
+        preference = (this@toProto as Preference).toProto()
+        for (index in 0 until preferenceCount) {
+            val child = getPreference(index)
             addPreferences(
                 preferenceOrGroupProto {
-                    if (it is PreferenceHierarchy) {
-                        group = it.toProto(activityClass, false)
+                    if (child is PreferenceGroup) {
+                        group = child.toProto()
                     } else {
-                        preference = toProto(it, activityClass, false)
+                        preference = child.toProto()
                     }
-                })
+                }
+            )
         }
     }
 
-    private suspend fun toProto(
-        node: PreferenceHierarchyNode,
-        activityClass: Class<*>,
-        isRoot: Boolean,
-    ) = preferenceProto {
+    private suspend fun Preference.toProto(): 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(preferenceExtras)?.let {
+            actionTarget = it
+            return@preferenceProto
+        }
+        this@toProto.intent?.let { actionTarget = it.toActionTarget() }
+    }
+
+    private suspend fun PreferenceHierarchy.toProto(isRoot: Boolean): PreferenceGroupProto =
+        preferenceGroupProto {
+            preference = toProto(this@toProto, isRoot)
+            forEachAsync {
+                addPreferences(
+                    preferenceOrGroupProto {
+                        if (it is PreferenceHierarchy) {
+                            group = it.toProto(false)
+                        } else {
+                            preference = toProto(it, false)
+                        }
+                    }
+                )
+            }
+        }
+
+    private suspend fun toProto(node: PreferenceHierarchyNode, isRoot: Boolean) = preferenceProto {
         val metadata = node.metadata
         key = metadata.key
         metadata.getTitleTextProto(isRoot)?.let { title = it }
@@ -318,22 +277,18 @@
             restricted = metadata.isRestricted(context)
         }
         persistent = metadata.isPersistent(context)
-        if (includeValue &&
-            persistent &&
-            metadata is BooleanValue &&
-            metadata is PersistentPreference<*>) {
+        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
-                }
-            }
+            @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata)
         }
         metadata.intent(context)?.let { actionTarget = it.toActionTarget() }
     }
@@ -359,16 +314,13 @@
         }
     }
 
-    private suspend fun String?.toActionTarget(
-        activityClass: Class<*>,
-        extras: Bundle?,
-    ): ActionTarget? {
+    private suspend fun String?.toActionTarget(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)
+                return (fragmentClass as Class<out Fragment>).toActionTarget(extras)
             }
         } catch (e: Exception) {
             Log.e(TAG, "Cannot loadClass $this", e)
@@ -376,16 +328,12 @@
         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() }
+    private suspend fun Class<out Fragment>.toActionTarget(extras: Bundle?): ActionTarget? {
+        if (
+            !PreferenceScreenProvider::class.java.isAssignableFrom(this) &&
+                !PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(this)
+        ) {
+            return null
         }
         val fragment =
             withContext(Dispatchers.Main) {
@@ -398,18 +346,24 @@
             }
         if (fragment is PreferenceScreenBindingKeyProvider) {
             val screenKey = fragment.getPreferenceScreenBindingKey(context)
-            if (screenKey != null && addPreferenceScreenFromRegistry(screenKey, activityClass)) {
+            if (screenKey != null && addPreferenceScreenFromRegistry(screenKey)) {
                 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 }
+            try {
+                val screen = fragment.createPreferenceScreen(preferenceScreenFactory)
+                val screenKey = screen?.key
+                if (!screenKey.isNullOrEmpty()) {
+                    @Suppress("CheckReturnValue")
+                    addPreferenceScreen(screenKey) { screen.toProto(null) }
+                    return actionTargetProto { key = screenKey }
+                }
+            } catch (e: Exception) {
+                Log.e(TAG, "Fail to createPreferenceScreen for $fragment", e)
             }
         }
-        return actionTargetProto { intent = startIntent.toProto() }
+        return null
     }
 
     private suspend fun Intent.toActionTarget(): ActionTarget {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
new file mode 100644
index 0000000..d8db1bb
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -0,0 +1,170 @@
+/*
+ * 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 androidx.annotation.IntDef
+import com.android.settingslib.graph.proto.PreferenceValueProto
+import com.android.settingslib.ipc.ApiDescriptor
+import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.IntMessageCodec
+import com.android.settingslib.ipc.MessageCodec
+import com.android.settingslib.metadata.BooleanValue
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceRestrictionProvider
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.metadata.ReadWritePermit
+
+/** Request to set preference value. */
+data class PreferenceSetterRequest(
+    val screenKey: String,
+    val key: String,
+    val value: PreferenceValueProto,
+)
+
+/** Result of preference setter request. */
+@IntDef(
+    PreferenceSetterResult.OK,
+    PreferenceSetterResult.UNSUPPORTED,
+    PreferenceSetterResult.DISABLED,
+    PreferenceSetterResult.UNAVAILABLE,
+    PreferenceSetterResult.INVALID_REQUEST,
+    PreferenceSetterResult.INTERNAL_ERROR,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class PreferenceSetterResult {
+    companion object {
+        /** Set preference value successfully. */
+        const val OK = 0
+        /** Set preference value is unsupported on the preference. */
+        const val UNSUPPORTED = 1
+        /** Preference is disabled and cannot set preference value. */
+        const val DISABLED = 2
+        /** Preference is restricted by managed configuration and cannot set preference value. */
+        const val RESTRICTED = 3
+        /** Preference is unavailable and cannot set preference value. */
+        const val UNAVAILABLE = 4
+        /** Require (runtime/special) app permission from user explicitly. */
+        const val REQUIRE_APP_PERMISSION = 5
+        /** Require explicit user agreement (e.g. terms of service). */
+        const val REQUIRE_USER_AGREEMENT = 6
+        /** Disallow to set preference value (e.g. uid not allowed). */
+        const val DISALLOW = 7
+        /** Request is invalid. */
+        const val INVALID_REQUEST = 8
+        /** Internal error happened when persist preference value. */
+        const val INTERNAL_ERROR = 9
+    }
+}
+
+/** Preference setter API descriptor. */
+class PreferenceSetterApiDescriptor(override val id: Int) :
+    ApiDescriptor<PreferenceSetterRequest, Int> {
+
+    override val requestCodec: MessageCodec<PreferenceSetterRequest>
+        get() = PreferenceSetterRequestCodec
+
+    override val responseCodec: MessageCodec<Int>
+        get() = IntMessageCodec
+}
+
+/** Preference setter API implementation. */
+class PreferenceSetterApiHandler(override val id: Int) : ApiHandler<PreferenceSetterRequest, Int> {
+
+    override fun hasPermission(
+        application: Application,
+        myUid: Int,
+        callingUid: Int,
+        request: PreferenceSetterRequest,
+    ): Boolean = true
+
+    override suspend fun invoke(
+        application: Application,
+        myUid: Int,
+        callingUid: Int,
+        request: PreferenceSetterRequest,
+    ): Int {
+        val screenMetadata =
+            PreferenceScreenRegistry[request.screenKey] ?: return PreferenceSetterResult.UNSUPPORTED
+        val key = request.key
+        val metadata =
+            screenMetadata.getPreferenceHierarchy(application).find(key)
+                ?: return PreferenceSetterResult.UNSUPPORTED
+        if (metadata !is PersistentPreference<*>) return PreferenceSetterResult.UNSUPPORTED
+        if (!metadata.isEnabled(application)) return PreferenceSetterResult.DISABLED
+        if (metadata is PreferenceRestrictionProvider && metadata.isRestricted(application)) {
+            return PreferenceSetterResult.RESTRICTED
+        }
+        if (metadata is PreferenceAvailabilityProvider && !metadata.isAvailable(application)) {
+            return PreferenceSetterResult.UNAVAILABLE
+        }
+        val storage = metadata.storage(application)
+        val value = request.value
+        try {
+            if (value.hasBooleanValue()) {
+                if (metadata !is BooleanValue) return PreferenceSetterResult.INVALID_REQUEST
+                val booleanValue = value.booleanValue
+                @Suppress("UNCHECKED_CAST")
+                val booleanPreference = metadata as PersistentPreference<Boolean>
+                when (
+                    booleanPreference.getWritePermit(application, booleanValue, myUid, callingUid)
+                ) {
+                    ReadWritePermit.ALLOW -> {}
+                    ReadWritePermit.DISALLOW -> return PreferenceSetterResult.DISALLOW
+                    ReadWritePermit.REQUIRE_APP_PERMISSION ->
+                        return PreferenceSetterResult.REQUIRE_APP_PERMISSION
+                    ReadWritePermit.REQUIRE_USER_AGREEMENT ->
+                        return PreferenceSetterResult.REQUIRE_USER_AGREEMENT
+                    else -> return PreferenceSetterResult.INTERNAL_ERROR
+                }
+                storage.setValue(key, Boolean::class.javaObjectType, booleanValue)
+                return PreferenceSetterResult.OK
+            }
+        } catch (e: Exception) {
+            return PreferenceSetterResult.INTERNAL_ERROR
+        }
+        return PreferenceSetterResult.INVALID_REQUEST
+    }
+
+    override val requestCodec: MessageCodec<PreferenceSetterRequest>
+        get() = PreferenceSetterRequestCodec
+
+    override val responseCodec: MessageCodec<Int>
+        get() = IntMessageCodec
+}
+
+/** Message codec for [PreferenceSetterRequest]. */
+object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> {
+    override fun encode(data: PreferenceSetterRequest) =
+        Bundle(3).apply {
+            putString(SCREEN_KEY, data.screenKey)
+            putString(KEY, data.key)
+            putByteArray(null, data.value.toByteArray())
+        }
+
+    override fun decode(data: Bundle) =
+        PreferenceSetterRequest(
+            data.getString(SCREEN_KEY)!!,
+            data.getString(KEY)!!,
+            PreferenceValueProto.parseFrom(data.getByteArray(null)!!),
+        )
+
+    private const val SCREEN_KEY = "s"
+    private const val KEY = "k"
+}
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
index 95661c9..6e38df1 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
@@ -21,8 +21,8 @@
 import com.android.settingslib.graph.GetPreferenceGraphRequest
 
 /** Api to get preference graph. */
-internal class PreferenceGraphApi(activityClasses: Set<String>) :
-    GetPreferenceGraphApiHandler(activityClasses) {
+internal class PreferenceGraphApi : GetPreferenceGraphApiHandler() {
+
     override val id: Int
         get() = API_GET_PREFERENCE_GRAPH
 
@@ -31,7 +31,5 @@
         myUid: Int,
         callingUid: Int,
         request: GetPreferenceGraphRequest,
-    ): Boolean {
-        return true // TODO: add permission check
-    }
+    ) = true
 }
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
index d382dad..8ebb145 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.service
 
+import com.android.settingslib.graph.PreferenceSetterApiHandler
 import com.android.settingslib.ipc.ApiHandler
 import com.android.settingslib.ipc.MessengerService
 import com.android.settingslib.ipc.PermissionChecker
@@ -31,7 +32,10 @@
     name: String = "PreferenceService",
 ) :
     MessengerService(
-        listOf<ApiHandler<*, *>>(PreferenceGraphApi(setOf())),
+        listOf<ApiHandler<*, *>>(
+            PreferenceGraphApi(),
+            PreferenceSetterApiHandler(API_PREFERENCE_SETTER),
+        ),
         permissionChecker,
         name,
     )
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
index 8f03111..1f38a66 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
@@ -19,3 +19,4 @@
 const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE"
 
 internal const val API_GET_PREFERENCE_GRAPH = 1
+internal const val API_PREFERENCE_SETTER = 2