Add AppOpPermissionAppList for SpaPrivilegedLib

Create AppOpPermissionAppList for the App List which controls whether
a given appops permission can be grant for a particular app.

Bug: 235727273
Test: Manual with Settings App
Change-Id: Ie3c9cd3777362ce320728e7c224a0c57886024dd
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index a4928e6..6871f21 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -27,6 +27,7 @@
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
         "androidx.compose.runtime_runtime",
+        "androidx.compose.runtime_runtime-livedata",
         "androidx.compose.ui_ui-tooling-preview",
         "androidx.navigation_navigation-compose",
         "com.google.android.material_material",
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
index 8876f66..93ba4f7 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
@@ -24,6 +24,7 @@
 import android.content.pm.ApplicationInfo
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
 
 class AppOpsController(
     context: Context,
@@ -32,21 +33,23 @@
 ) {
     private val appOpsManager = checkNotNull(context.getSystemService(AppOpsManager::class.java))
 
+    val mode: LiveData<Int>
+        get() = _mode
     val isAllowed: LiveData<Boolean>
-        get() = _isAllowed
+        get() = Transformations.map(_mode) { it == MODE_ALLOWED }
 
     fun setAllowed(allowed: Boolean) {
         val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED
         appOpsManager.setMode(op, app.uid, app.packageName, mode)
-        _isAllowed.postValue(allowed)
+        _mode.postValue(mode)
     }
 
     @Mode
     fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName)
 
-    private val _isAllowed = object : MutableLiveData<Boolean>() {
+    private val _mode = object : MutableLiveData<Int>() {
         override fun onActive() {
-            postValue(getMode() == MODE_ALLOWED)
+            postValue(getMode())
         }
 
         override fun onInactive() {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index e521edd..ba8af54 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -16,31 +16,52 @@
 
 package com.android.settingslib.spaprivileged.model.app
 
+import android.app.AppGlobals
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageInfo
+import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
 import android.content.pm.PackageManager
 import android.util.Log
+import com.android.settingslib.spa.framework.util.asyncFilter
 
 private const val TAG = "PackageManagers"
 
 object PackageManagers {
-    fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo =
-        PackageManager.getPackageInfoAsUserCached(packageName, 0, userId)
+    private val iPackageManager by lazy { AppGlobals.getPackageManager() }
+
+    fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? =
+        getPackageInfoAsUser(packageName, 0, userId)
 
     fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo =
         PackageManager.getApplicationInfoAsUserCached(packageName, 0, userId)
 
-    fun hasRequestPermission(app: ApplicationInfo, permission: String): Boolean {
-        val packageInfo = try {
-            PackageManager.getPackageInfoAsUserCached(
-                app.packageName, PackageManager.GET_PERMISSIONS.toLong(), app.userId
-            )
-        } catch (e: PackageManager.NameNotFoundException) {
-            Log.w(TAG, "getPackageInfoAsUserCached() failed", e)
-            return false
-        }
+    fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
+        val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
         return packageInfo?.requestedPermissions?.let {
             permission in it
         } ?: false
     }
+
+    fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
+        val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+            ?: return false
+        val index = packageInfo.requestedPermissions.indexOf(permission)
+        return index >= 0 &&
+            packageInfo.requestedPermissionsFlags[index].hasFlag(REQUESTED_PERMISSION_GRANTED)
+    }
+
+    suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> =
+        iPackageManager.getAppOpPermissionPackages(permission, userId).asIterable().asyncFilter {
+            iPackageManager.isPackageAvailable(it, userId)
+        }.toSet()
+
+    private fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
+        try {
+            PackageManager.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(TAG, "getPackageInfoAsUserCached() failed", e)
+            null
+        }
+
+    private fun Int.hasFlag(flag: Int) = (this and flag) > 0
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index 99deb70..f51d2db 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -49,7 +49,8 @@
             ),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
-        val packageInfo = remember { PackageManagers.getPackageInfoAsUser(packageName, userId) }
+        val packageInfo =
+            remember { PackageManagers.getPackageInfoAsUser(packageName, userId) } ?: return
         Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) {
             AppIcon(app = packageInfo.applicationInfo, size = SettingsDimension.appIconInfoSize)
         }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
new file mode 100644
index 0000000..c6f41d3
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 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.spaprivileged.template.app
+
+import android.app.AppOpsManager.MODE_ALLOWED
+import android.app.AppOpsManager.MODE_DEFAULT
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import com.android.settingslib.spaprivileged.model.app.AppOpsController
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.android.settingslib.spaprivileged.model.app.PackageManagers
+import com.android.settingslib.spaprivileged.model.app.PackageManagers.hasGrantPermission
+import com.android.settingslib.spaprivileged.model.app.PackageManagers.hasRequestPermission
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+data class AppOpPermissionRecord(
+    override val app: ApplicationInfo,
+    val hasRequestPermission: Boolean,
+    var appOpsController: AppOpsController,
+) : AppRecord
+
+abstract class AppOpPermissionListModel(private val context: Context) :
+    TogglePermissionAppListModel<AppOpPermissionRecord> {
+
+    abstract val appOp: Int
+    abstract val permission: String
+
+    private val notChangeablePackages =
+        setOf("android", "com.android.systemui", context.packageName)
+
+    override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+        userIdFlow.map { userId ->
+            PackageManagers.getAppOpPermissionPackages(userId, permission)
+        }.combine(appListFlow) { packageNames, appList ->
+            appList.map { app ->
+                AppOpPermissionRecord(
+                    app = app,
+                    hasRequestPermission = app.packageName in packageNames,
+                    appOpsController = AppOpsController(context = context, app = app, op = appOp),
+                )
+            }
+        }
+
+    override fun transformItem(app: ApplicationInfo) = AppOpPermissionRecord(
+        app = app,
+        hasRequestPermission = app.hasRequestPermission(permission),
+        appOpsController = AppOpsController(context = context, app = app, op = appOp),
+    )
+
+    override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
+        recordListFlow.map { recordList ->
+            recordList.filter { it.hasRequestPermission }
+        }
+
+    /**
+     * Defining the default behavior as permissible as long as the package requested this permission
+     * (This means pre-M gets approval during install time; M apps gets approval during runtime).
+     */
+    @Composable
+    override fun isAllowed(record: AppOpPermissionRecord): State<Boolean?> {
+        val mode = record.appOpsController.mode.observeAsState()
+        return remember {
+            derivedStateOf {
+                when (mode.value) {
+                    null -> null
+                    MODE_ALLOWED -> true
+                    MODE_DEFAULT -> record.app.hasGrantPermission(permission)
+                    else -> false
+                }
+            }
+        }
+    }
+
+    override fun isChangeable(record: AppOpPermissionRecord) =
+        record.hasRequestPermission && record.app.packageName !in notChangeablePackages
+
+    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
+        record.appOpsController.setAllowed(newAllowed)
+    }
+}