Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 572f75db authored by Chaohui Wang's avatar Chaohui Wang
Browse files

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
parent d50b929a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ android_library {
        "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",
+7 −4
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.content.Context
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 @@ class AppOpsController(
) {
    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() {
+32 −11
Original line number Diff line number Diff line
@@ -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
}
+2 −1
Original line number Diff line number Diff line
@@ -49,7 +49,8 @@ fun AppInfo(packageName: String, userId: Int) {
            ),
        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)
        }
+101 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}