Support GetMetadata for Preference Service

Bug: 379750656
Flag: com.android.settingslib.flags.settings_catalyst
Test: unit test
Change-Id: Ia9b438360b60ff509a259df0a079ec4d745fb595
diff --git a/src/com/android/settings/service/PreferenceService.kt b/src/com/android/settings/service/PreferenceService.kt
index 3a67762..d07e78d 100644
--- a/src/com/android/settings/service/PreferenceService.kt
+++ b/src/com/android/settings/service/PreferenceService.kt
@@ -16,6 +16,7 @@
 
 package com.android.settings.service
 
+import android.app.Application
 import android.os.Binder
 import android.os.OutcomeReceiver
 import android.os.Process
@@ -26,38 +27,49 @@
 import android.service.settings.preferences.SetValueRequest
 import android.service.settings.preferences.SetValueResult
 import android.service.settings.preferences.SettingsPreferenceService
+import com.android.settingslib.graph.GetPreferenceGraphApiHandler
+import com.android.settingslib.graph.GetPreferenceGraphRequest
 import com.android.settingslib.graph.PreferenceGetterApiHandler
+import com.android.settingslib.graph.PreferenceGetterFlags
 import com.android.settingslib.graph.PreferenceSetterApiHandler
 import com.android.settingslib.ipc.ApiPermissionChecker
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.launch
 import java.lang.Exception
 
 class PreferenceService : SettingsPreferenceService() {
 
-    private val scope = CoroutineScope(Job() + Dispatchers.Main)
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
 
     private val getApiHandler = PreferenceGetterApiHandler(1, ApiPermissionChecker.alwaysAllow())
     private val setApiHandler = PreferenceSetterApiHandler(2, ApiPermissionChecker.alwaysAllow())
+    private val graphApi = GraphProvider(3)
 
     override fun onGetAllPreferenceMetadata(
         request: MetadataRequest,
         callback: OutcomeReceiver<MetadataResult, Exception>
     ) {
-        // TODO(379750656): Update graph API to be usable outside SettingsLib
-        callback.onError(UnsupportedOperationException("Not yet supported"))
+        scope.launch {
+            val graphProto = graphApi.invoke(application, Process.myUid(), Binder.getCallingUid(),
+                GetPreferenceGraphRequest(
+                    includeValue = false,
+                    flags = PreferenceGetterFlags.METADATA
+                ))
+            val result = transformCatalystGetMetadataResponse(this@PreferenceService, graphProto)
+            callback.onResult(result)
+        }
     }
 
     override fun onGetPreferenceValue(
         request: GetValueRequest,
         callback: OutcomeReceiver<GetValueResult, Exception>
     ) {
-        scope.launch(Dispatchers.IO) {
+        scope.launch {
             val apiRequest = transformFrameworkGetValueRequest(request)
             val response = getApiHandler.invoke(application, Process.myUid(),
-                Binder.getCallingPid(), apiRequest)
+                Binder.getCallingUid(), apiRequest)
             val result = transformCatalystGetValueResponse(
                 this@PreferenceService,
                 request,
@@ -75,7 +87,7 @@
         request: SetValueRequest,
         callback: OutcomeReceiver<SetValueResult, Exception>
     ) {
-        scope.launch(Dispatchers.IO) {
+        scope.launch {
             val apiRequest = transformFrameworkSetValueRequest(request)
             if (apiRequest == null) {
                 callback.onResult(
@@ -83,10 +95,20 @@
                 )
             } else {
                 val response = setApiHandler.invoke(application, Process.myUid(),
-                    Binder.getCallingPid(), apiRequest)
+                    Binder.getCallingUid(), apiRequest)
 
                 callback.onResult(transformCatalystSetValueResponse(response))
             }
         }
     }
+
+    // Basic implementation - we already have permission to access Graph for Metadata via superclass
+    private class GraphProvider(override val id: Int) : GetPreferenceGraphApiHandler(emptySet()) {
+        override fun hasPermission(
+            application: Application,
+            myUid: Int,
+            callingUid: Int,
+            request: GetPreferenceGraphRequest
+        ) = true
+    }
 }
diff --git a/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt b/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt
index 7a4c7fc..18307e0 100644
--- a/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt
+++ b/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.service.settings.preferences.GetValueRequest
 import android.service.settings.preferences.GetValueResult
+import android.service.settings.preferences.MetadataResult
 import android.service.settings.preferences.SetValueRequest
 import android.service.settings.preferences.SetValueResult
 import android.service.settings.preferences.SettingsPreferenceMetadata
@@ -34,9 +35,55 @@
 import com.android.settingslib.graph.proto.PreferenceProto
 import com.android.settingslib.graph.proto.PreferenceValueProto
 import com.android.settingslib.graph.getText
+import com.android.settingslib.graph.proto.PreferenceGraphProto
+import com.android.settingslib.graph.proto.PreferenceOrGroupProto
 import com.android.settingslib.graph.toIntent
 import com.android.settingslib.metadata.SensitivityLevel
 
+/** Transform Catalyst Graph result to Framework GET METADATA result */
+fun transformCatalystGetMetadataResponse(
+    context: Context,
+    graph: PreferenceGraphProto
+): MetadataResult {
+    val preferences = mutableSetOf<PreferenceWithScreen>()
+    // recursive function to visit all nodes in preference group
+    fun traverseGroupOrPref(
+        screenKey: String,
+        groupOrPref: PreferenceOrGroupProto,
+    ) {
+        when (groupOrPref.kindCase) {
+            PreferenceOrGroupProto.KindCase.PREFERENCE ->
+                preferences.add(
+                    PreferenceWithScreen(screenKey, groupOrPref.preference)
+                )
+            PreferenceOrGroupProto.KindCase.GROUP -> {
+                for (child in groupOrPref.group.preferencesList) {
+                    traverseGroupOrPref(screenKey, child)
+                }
+            }
+            else -> {}
+        }
+    }
+    // traverse all screens and all preferences on screen
+    for ((screenKey, screen) in graph.screensMap) {
+        for (groupOrPref in screen.root.preferencesList) {
+            traverseGroupOrPref(screenKey, groupOrPref)
+        }
+    }
+
+    return if (preferences.isNotEmpty()) {
+        MetadataResult.Builder(MetadataResult.RESULT_OK)
+            .setMetadataList(
+                preferences.map {
+                    it.preference.toMetadata(context, it.screenKey)
+                }
+            )
+            .build()
+    } else {
+        MetadataResult.Builder(MetadataResult.RESULT_UNSUPPORTED).build()
+    }
+}
+
 /** Translate Framework GET VALUE request to Catalyst GET VALUE request */
 fun transformFrameworkGetValueRequest(
     request: GetValueRequest,
@@ -133,6 +180,11 @@
     return SetValueResult.Builder(resultCode).build()
 }
 
+private data class PreferenceWithScreen(
+    val screenKey: String,
+    val preference: PreferenceProto,
+)
+
 private fun PreferenceProto.toMetadata(
     context: Context,
     screenKey: String
diff --git a/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt b/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt
index f064b22..7631a00 100644
--- a/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt
+++ b/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.settings.service
 
-import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
 import android.platform.test.annotations.RequiresFlagsEnabled
@@ -24,6 +23,7 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.service.settings.preferences.GetValueRequest
 import android.service.settings.preferences.GetValueResult
+import android.service.settings.preferences.MetadataResult
 import android.service.settings.preferences.SetValueRequest
 import android.service.settings.preferences.SetValueResult
 import android.service.settings.preferences.SettingsPreferenceMetadata
@@ -37,9 +37,15 @@
 import com.android.settingslib.graph.PreferenceGetterFlags
 import com.android.settingslib.graph.PreferenceGetterResponse
 import com.android.settingslib.graph.PreferenceSetterResult
+import com.android.settingslib.graph.preferenceGroupProto
+import com.android.settingslib.graph.preferenceOrGroupProto
+import com.android.settingslib.graph.preferenceProto
+import com.android.settingslib.graph.preferenceScreenProto
+import com.android.settingslib.graph.proto.PreferenceGraphProto
 import com.android.settingslib.graph.proto.PreferenceProto
 import com.android.settingslib.graph.proto.PreferenceValueProto
 import com.android.settingslib.graph.proto.TextProto
+import com.android.settingslib.graph.textProto
 import com.android.settingslib.graph.toProto
 import com.android.settingslib.metadata.SensitivityLevel
 import com.google.common.truth.Truth.assertThat
@@ -55,6 +61,73 @@
     val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
 
     @Test
+    fun transformCatalystGetMetadataResponse_emptyGraph_returnsFrameworkResponseWithError() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        val graphProto = PreferenceGraphProto.newBuilder().build()
+        val fResult = transformCatalystGetMetadataResponse(context, graphProto)
+        with(fResult) {
+            assertThat(resultCode).isEqualTo(MetadataResult.RESULT_UNSUPPORTED)
+            assertThat(metadataList).isEmpty()
+        }
+    }
+
+    @Test
+    fun transformCatalystGetMetadataResponse_populatedGraph_returnsFrameworkResponseWithSuccess() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        val screen = preferenceScreenProto {
+            root = preferenceGroupProto {
+                addAllPreferences(
+                    listOf(
+                        preferenceOrGroupProto {
+                            group = preferenceGroupProto {
+                                addPreferences(
+                                    preferenceOrGroupProto {
+                                        preference = preferenceProto {
+                                            key = "key1"
+                                            title = textProto { string = "title1" }
+                                            enabled = true
+                                        }
+                                    }
+                                )
+                            }
+                        },
+                        preferenceOrGroupProto {
+                            preference = preferenceProto {
+                                key = "key2"
+                                title = textProto { string = "title2" }
+                                enabled = false
+                            }
+                        }
+                    )
+                )
+            }
+        }
+        val graphProto = PreferenceGraphProto.newBuilder().putScreens("screen", screen).build()
+
+        val fResult = transformCatalystGetMetadataResponse(context, graphProto)
+        with(fResult) {
+            assertThat(resultCode).isEqualTo(MetadataResult.RESULT_OK)
+            assertThat(metadataList.size).isEqualTo(2)
+        }
+        assertThat(
+            fResult.metadataList.any {
+                it.key == "key1" &&
+                it.screenKey == "screen" &&
+                it.title == "title1" &&
+                it.isEnabled == true
+            }
+        ).isTrue()
+        assertThat(
+            fResult.metadataList.any {
+                it.key == "key2" &&
+                it.screenKey == "screen" &&
+                it.title == "title2" &&
+                it.isEnabled == false
+            }
+        ).isTrue()
+    }
+
+    @Test
     fun transformFrameworkGetValueRequest_returnsValidCatalystRequest() {
         val fRequest = GetValueRequest.Builder("screen", "pref").build()
         val cRequest = transformFrameworkGetValueRequest(fRequest)