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

Commit 288b38a3 authored by Yilin Cai's avatar Yilin Cai
Browse files

Adjust "AppOpPermissionListModel" and "AppOpsController" for SPA migration.

The adjustments are for the WiFi control permission page migration.

Extensions including:
- A broader permission NETWORK_SETTINGS should trump CHANGE_WIFI_STATE.
- CHANGE_WIFI_STATE is not at protection level "appop", thus the method
  "getAppOpPermissionPackages" returns an empty list for permission
  "CHANGE_WIFI_STATE" in "AppOpPermissionListModel.transform". We need
  to check requested permissions instead.
- WiFi control sets mode to "MODE_IGNORED" instead of "MODE_ERRORED"
  when disallows.

Test: atest SpaPrivilegedLibTests:AppOpsControllerTest
Test: atest SpaPrivilegedLibTests:AppOpPermissionAppListTest
Bug: 262206181
Change-Id: Ib566ff3a28458e18014ea9456cd3870cf68ff973
parent 613dc260
Loading
Loading
Loading
Loading
+9 −10
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.settingslib.spaprivileged.model.app

import android.app.AppOpsManager.MODE_ALLOWED
import android.app.AppOpsManager.MODE_ERRORED
import android.app.AppOpsManager.Mode
import android.content.Context
import android.content.pm.ApplicationInfo
@@ -33,14 +32,14 @@ interface IAppOpsController {

    fun setAllowed(allowed: Boolean)

    @Mode
    fun getMode(): Int
    @Mode fun getMode(): Int
}

class AppOpsController(
    context: Context,
    private val app: ApplicationInfo,
    private val op: Int,
    private val modeForNotAllowed: Int,
    private val setModeByUid: Boolean = false,
) : IAppOpsController {
    private val appOpsManager = context.appOpsManager
@@ -49,7 +48,7 @@ class AppOpsController(
        get() = _mode

    override fun setAllowed(allowed: Boolean) {
        val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED
        val mode = if (allowed) MODE_ALLOWED else modeForNotAllowed
        if (setModeByUid) {
            appOpsManager.setUidMode(op, app.uid, mode)
        } else {
@@ -58,10 +57,10 @@ class AppOpsController(
        _mode.postValue(mode)
    }

    @Mode
    override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName)
    @Mode override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName)

    private val _mode = object : MutableLiveData<Int>() {
    private val _mode =
        object : MutableLiveData<Int>() {
            override fun onActive() {
                postValue(getMode())
            }
+76 −26
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.settingslib.spaprivileged.template.app

import android.app.AppOpsManager.MODE_ALLOWED
import android.app.AppOpsManager.MODE_DEFAULT
import android.app.AppOpsManager.MODE_ERRORED
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
@@ -25,6 +26,8 @@ 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.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.util.asyncMapItem
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.AppRecord
@@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.map

data class AppOpPermissionRecord(
    override val app: ApplicationInfo,
    val hasRequestBroaderPermission: Boolean,
    val hasRequestPermission: Boolean,
    var appOpsController: IAppOpsController,
) : AppRecord
@@ -49,10 +53,27 @@ abstract class AppOpPermissionListModel(
    abstract val appOp: Int
    abstract val permission: String

    /**
     * When set, specifies the broader permission who trumps the [permission].
     *
     * When trumped, the [permission] is not changeable and model shows the [permission] as allowed.
     */
    open val broaderPermission: String? = null

    /**
     * Indicates whether [permission] has protection level appop flag.
     *
     * If true, it uses getAppOpPermissionPackages() to fetch bits to decide whether the permission
     * is requested.
     */
    open val permissionHasAppopFlag: Boolean = true

    open val modeForNotAllowed: Int = MODE_ERRORED

    /**
     * Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed.
     *
     * Security related app-ops should be set with setUidMode() instead of setMode().
     * Security or privacy related app-ops should be set with setUidMode() instead of setMode().
     */
    open val setModeByUid = false

@@ -60,31 +81,54 @@ abstract class AppOpPermissionListModel(
    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 ->
    private fun createAppOpsController(app: ApplicationInfo) =
        AppOpsController(
            context = context,
            app = app,
            op = appOp,
            setModeByUid = setModeByUid,
            modeForNotAllowed = modeForNotAllowed,
        )

    private fun createRecord(
        app: ApplicationInfo,
        hasRequestPermission: Boolean
    ): AppOpPermissionRecord =
        with(packageManagers) {
            AppOpPermissionRecord(
                app = app,
                    hasRequestPermission = app.packageName in packageNames,
                hasRequestBroaderPermission =
                    broaderPermission?.let { app.hasRequestPermission(it) } ?: false,
                hasRequestPermission = hasRequestPermission,
                appOpsController = createAppOpsController(app),
            )
        }
        }

    override fun transformItem(app: ApplicationInfo) = AppOpPermissionRecord(
    override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
        if (permissionHasAppopFlag) {
            userIdFlow
                .map { userId -> packageManagers.getAppOpPermissionPackages(userId, permission) }
                .combine(appListFlow) { packageNames, appList ->
                    appList.map { app ->
                        createRecord(
                            app = app,
        hasRequestPermission = with(packageManagers) { app.hasRequestPermission(permission) },
        appOpsController = createAppOpsController(app),
                            hasRequestPermission = app.packageName in packageNames,
                        )
                    }
                }
        } else {
            appListFlow.asyncMapItem { app ->
                with(packageManagers) { createRecord(app, app.hasRequestPermission(permission)) }
            }
        }

    private fun createAppOpsController(app: ApplicationInfo) = AppOpsController(
        context = context,
    override fun transformItem(app: ApplicationInfo) =
        with(packageManagers) {
            createRecord(
                app = app,
        op = appOp,
        setModeByUid = setModeByUid,
                hasRequestPermission = app.hasRequestPermission(permission),
            )
        }

    override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
        recordListFlow.filterItem(::isChangeable)
@@ -95,15 +139,19 @@ abstract class AppOpPermissionListModel(
     */
    @Composable
    override fun isAllowed(record: AppOpPermissionRecord): State<Boolean?> {
        if (record.hasRequestBroaderPermission) {
            // Broader permission trumps the specific permission.
            return stateOf(true)
        }

        val mode = record.appOpsController.mode.observeAsState()
        return remember {
            derivedStateOf {
                when (mode.value) {
                    null -> null
                    MODE_ALLOWED -> true
                    MODE_DEFAULT -> with(packageManagers) {
                        record.app.hasGrantPermission(permission)
                    }
                    MODE_DEFAULT ->
                        with(packageManagers) { record.app.hasGrantPermission(permission) }
                    else -> false
                }
            }
@@ -111,7 +159,9 @@ abstract class AppOpPermissionListModel(
    }

    override fun isChangeable(record: AppOpPermissionRecord) =
        record.hasRequestPermission && record.app.packageName !in notChangeablePackages
        record.hasRequestPermission &&
            !record.hasRequestBroaderPermission &&
            record.app.packageName !in notChangeablePackages

    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
        record.appOpsController.setAllowed(newAllowed)
+47 −20
Original line number Diff line number Diff line
@@ -31,21 +31,18 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.Mockito.`when` as whenever

@RunWith(AndroidJUnit4::class)
class AppOpsControllerTest {
    @get:Rule
    val mockito: MockitoRule = MockitoJUnit.rule()
    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()

    @Spy
    private val context: Context = ApplicationProvider.getApplicationContext()
    @Spy private val context: Context = ApplicationProvider.getApplicationContext()

    @Mock
    private lateinit var appOpsManager: AppOpsManager
    @Mock private lateinit var appOpsManager: AppOpsManager

    @Before
    fun setUp() {
@@ -54,7 +51,13 @@ class AppOpsControllerTest {

    @Test
    fun setAllowed_setToTrue() {
        val controller = AppOpsController(context = context, app = APP, op = OP)
        val controller =
            AppOpsController(
                context = context,
                app = APP,
                op = OP,
                modeForNotAllowed = MODE_ERRORED
            )

        controller.setAllowed(true)

@@ -63,7 +66,13 @@ class AppOpsControllerTest {

    @Test
    fun setAllowed_setToFalse() {
        val controller = AppOpsController(context = context, app = APP, op = OP)
        val controller =
            AppOpsController(
                context = context,
                app = APP,
                op = OP,
                modeForNotAllowed = MODE_ERRORED
            )

        controller.setAllowed(false)

@@ -73,7 +82,13 @@ class AppOpsControllerTest {
    @Test
    fun setAllowed_setToTrueByUid() {
        val controller =
            AppOpsController(context = context, app = APP, op = OP, setModeByUid = true)
            AppOpsController(
                context = context,
                app = APP,
                op = OP,
                modeForNotAllowed = MODE_ERRORED,
                setModeByUid = true
            )

        controller.setAllowed(true)

@@ -83,7 +98,13 @@ class AppOpsControllerTest {
    @Test
    fun setAllowed_setToFalseByUid() {
        val controller =
            AppOpsController(context = context, app = APP, op = OP, setModeByUid = true)
            AppOpsController(
                context = context,
                app = APP,
                op = OP,
                modeForNotAllowed = MODE_ERRORED,
                setModeByUid = true
            )

        controller.setAllowed(false)

@@ -92,10 +113,15 @@ class AppOpsControllerTest {

    @Test
    fun getMode() {
        whenever(
            appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName)
        ).thenReturn(MODE_ALLOWED)
        val controller = AppOpsController(context = context, app = APP, op = OP)
        whenever(appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName))
            .thenReturn(MODE_ALLOWED)
        val controller =
            AppOpsController(
                context = context,
                app = APP,
                op = OP,
                modeForNotAllowed = MODE_ERRORED
            )

        val mode = controller.getMode()

@@ -104,7 +130,8 @@ class AppOpsControllerTest {

    private companion object {
        const val OP = 1
        val APP = ApplicationInfo().apply {
        val APP =
            ApplicationInfo().apply {
                packageName = "package.name"
                uid = 123
            }
+138 −76
Original line number Diff line number Diff line
@@ -39,28 +39,23 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.Mockito.`when` as whenever

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class AppOpPermissionAppListTest {
    @get:Rule
    val mockito: MockitoRule = MockitoJUnit.rule()
    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()

    @get:Rule
    val composeTestRule = createComposeRule()
    @get:Rule val composeTestRule = createComposeRule()

    @Spy
    private val context: Context = ApplicationProvider.getApplicationContext()
    @Spy private val context: Context = ApplicationProvider.getApplicationContext()

    @Mock
    private lateinit var packageManagers: IPackageManagers
    @Mock private lateinit var packageManagers: IPackageManagers

    @Mock
    private lateinit var appOpsManager: AppOpsManager
    @Mock private lateinit var appOpsManager: AppOpsManager

    private lateinit var listModel: TestAppOpPermissionAppListModel

@@ -79,9 +74,7 @@ class AppOpPermissionAppListTest {

    @Test
    fun transformItem_hasRequestPermission() = runTest {
        with(packageManagers) {
            whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(true)
        }
        with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(true) }

        val record = listModel.transformItem(APP)

@@ -90,22 +83,44 @@ class AppOpPermissionAppListTest {

    @Test
    fun transformItem_notRequestPermission() = runTest {
        with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) }

        val record = listModel.transformItem(APP)

        assertThat(record.hasRequestPermission).isFalse()
    }

    @Test
    fun transformItem_hasRequestBroaderPermission() = runTest {
        listModel.broaderPermission = BROADER_PERMISSION
        with(packageManagers) {
            whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false)
            whenever(APP.hasRequestPermission(BROADER_PERMISSION)).thenReturn(true)
        }

        val record = listModel.transformItem(APP)

        assertThat(record.hasRequestPermission).isFalse()
        assertThat(record.hasRequestBroaderPermission).isTrue()
    }

    @Test
    fun filter() = runTest {
    fun transformItem_notRequestBroaderPermission() = runTest {
        listModel.broaderPermission = BROADER_PERMISSION
        with(packageManagers) {
            whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false)
            whenever(APP.hasRequestPermission(BROADER_PERMISSION)).thenReturn(false)
        }

        val record = listModel.transformItem(APP)

        assertThat(record.hasRequestPermission).isFalse()
    }
        val record = AppOpPermissionRecord(

    @Test
    fun filter() = runTest {
        with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) }
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = false,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )
@@ -118,8 +133,10 @@ class AppOpPermissionAppListTest {

    @Test
    fun isAllowed_allowed() {
        val record = AppOpPermissionRecord(
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED),
            )
@@ -131,11 +148,11 @@ class AppOpPermissionAppListTest {

    @Test
    fun isAllowed_defaultAndHasGrantPermission() {
        with(packageManagers) {
            whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true)
        }
        val record = AppOpPermissionRecord(
        with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true) }
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )
@@ -147,11 +164,11 @@ class AppOpPermissionAppListTest {

    @Test
    fun isAllowed_defaultAndNotGrantPermission() {
        with(packageManagers) {
            whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false)
        }
        val record = AppOpPermissionRecord(
        with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false) }
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )
@@ -161,10 +178,32 @@ class AppOpPermissionAppListTest {
        assertThat(isAllowed).isFalse()
    }

    @Test
    fun isAllowed_broaderPermissionTrumps() {
        listModel.broaderPermission = BROADER_PERMISSION
        with(packageManagers) {
            whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false)
            whenever(APP.hasGrantPermission(BROADER_PERMISSION)).thenReturn(true)
        }
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = true,
                hasRequestPermission = false,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
            )

        val isAllowed = getIsAllowed(record)

        assertThat(isAllowed).isTrue()
    }

    @Test
    fun isAllowed_notAllowed() {
        val record = AppOpPermissionRecord(
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
            )
@@ -176,8 +215,10 @@ class AppOpPermissionAppListTest {

    @Test
    fun isChangeable_notRequestPermission() {
        val record = AppOpPermissionRecord(
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = false,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )
@@ -189,8 +230,10 @@ class AppOpPermissionAppListTest {

    @Test
    fun isChangeable_notChangeablePackages() {
        val record = AppOpPermissionRecord(
        val record =
            AppOpPermissionRecord(
                app = NOT_CHANGEABLE_APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )
@@ -202,8 +245,10 @@ class AppOpPermissionAppListTest {

    @Test
    fun isChangeable_hasRequestPermissionAndChangeable() {
        val record = AppOpPermissionRecord(
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )
@@ -213,11 +258,29 @@ class AppOpPermissionAppListTest {
        assertThat(isChangeable).isTrue()
    }

    @Test
    fun isChangeable_broaderPermissionTrumps() {
        listModel.broaderPermission = BROADER_PERMISSION
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = true,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
            )

        val isChangeable = listModel.isChangeable(record)

        assertThat(isChangeable).isFalse()
    }

    @Test
    fun setAllowed() {
        val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
        val record = AppOpPermissionRecord(
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = appOpsController,
            )
@@ -239,9 +302,7 @@ class AppOpPermissionAppListTest {

    private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? {
        lateinit var isAllowedState: State<Boolean?>
        composeTestRule.setContent {
            isAllowedState = listModel.isAllowed(record)
        }
        composeTestRule.setContent { isAllowedState = listModel.isAllowed(record) }
        return isAllowedState.value
    }

@@ -250,8 +311,12 @@ class AppOpPermissionAppListTest {
        override val pageTitleResId = R.string.test_app_op_permission_title
        override val switchTitleResId = R.string.test_app_op_permission_switch_title
        override val footerResId = R.string.test_app_op_permission_footer

        override val appOp = AppOpsManager.OP_MANAGE_MEDIA
        override val permission = PERMISSION
        override val permissionHasAppopFlag = true
        override var broaderPermission: String? = null

        override var setModeByUid = false
    }

@@ -259,12 +324,9 @@ class AppOpPermissionAppListTest {
        const val USER_ID = 0
        const val PACKAGE_NAME = "package.name"
        const val PERMISSION = "PERMISSION"
        val APP = ApplicationInfo().apply {
            packageName = PACKAGE_NAME
        }
        val NOT_CHANGEABLE_APP = ApplicationInfo().apply {
            packageName = "android"
        }
        const val BROADER_PERMISSION = "BROADER_PERMISSION"
        val APP = ApplicationInfo().apply { packageName = PACKAGE_NAME }
        val NOT_CHANGEABLE_APP = ApplicationInfo().apply { packageName = "android" }
    }
}