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

Commit 8882a30e authored by Chaohui Wang's avatar Chaohui Wang Committed by Automerger Merge Worker
Browse files

Merge "Create AppOpsPermissionController" into 24D1-dev am: dcefab81

parents 551d50fb dcefab81
Loading
Loading
Loading
Loading
+62 −0
Original line number Diff line number Diff line
/*
 * 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.spaprivileged.model.app

import android.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

interface IAppOpsPermissionController {
    val isAllowedFlow: Flow<Boolean>
    fun setAllowed(allowed: Boolean)
}

class AppOpsPermissionController(
    context: Context,
    private val app: ApplicationInfo,
    appOps: AppOps,
    private val permission: String,
    private val packageManagers: IPackageManagers = PackageManagers,
    private val appOpsController: IAppOpsController = AppOpsController(context, app, appOps),
) : IAppOpsPermissionController {
    override val isAllowedFlow: Flow<Boolean> = appOpsController.modeFlow.map { mode ->
        when (mode) {
            AppOpsManager.MODE_ALLOWED -> true

            AppOpsManager.MODE_DEFAULT -> {
                with(packageManagers) { app.hasGrantPermission(permission) }
            }

            else -> false
        }
    }.conflate().onEach { Log.d(TAG, "isAllowed: $it") }.flowOn(Dispatchers.Default)

    override fun setAllowed(allowed: Boolean) {
        appOpsController.setAllowed(allowed)
    }

    private companion object {
        private const val TAG = "AppOpsPermissionControl"
    }
}
+21 −44
Original line number Diff line number Diff line
@@ -20,13 +20,14 @@ import android.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.framework.util.asyncMapItem
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spaprivileged.model.app.AppOps
import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.AppOpsPermissionController
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
import kotlinx.coroutines.flow.Flow
@@ -37,7 +38,7 @@ data class AppOpPermissionRecord(
    override val app: ApplicationInfo,
    val hasRequestBroaderPermission: Boolean,
    val hasRequestPermission: Boolean,
    var appOpsController: IAppOpsController,
    var appOpsPermissionController: IAppOpsPermissionController,
) : AppRecord

abstract class AppOpPermissionListModel(
@@ -70,8 +71,8 @@ abstract class AppOpPermissionListModel(
    private val notChangeablePackages =
        setOf("android", "com.android.systemui", context.packageName)

    private fun createAppOpsController(app: ApplicationInfo) =
        AppOpsController(context, app, appOps)
    private fun createAppOpsPermissionController(app: ApplicationInfo) =
        AppOpsPermissionController(context, app, appOps, permission)

    private fun createRecord(
        app: ApplicationInfo,
@@ -84,7 +85,7 @@ abstract class AppOpPermissionListModel(
                    app.hasRequestPermission(it)
                } ?: false,
                hasRequestPermission = hasRequestPermission,
                appOpsController = createAppOpsController(app),
                appOpsPermissionController = createAppOpsPermissionController(app),
            )
        }

@@ -117,51 +118,27 @@ abstract class AppOpPermissionListModel(
    override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
        recordListFlow.filterItem(::isChangeable)

    @Composable
    override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? =
        isAllowed(
            record = record,
            appOpsController = record.appOpsController,
            permission = permission,
            packageManagers = packageManagers,
        )

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

    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
        record.appOpsController.setAllowed(newAllowed)
    }
}

    /**
     * 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
internal fun isAllowed(
    record: AppOpPermissionRecord,
    appOpsController: IAppOpsController,
    permission: String,
    packageManagers: IPackageManagers = PackageManagers,
): () -> Boolean? {
    override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? {
        if (record.hasRequestBroaderPermission) {
            // Broader permission trumps the specific permission.
            return { true }
        }

    val mode = appOpsController.modeFlow.collectAsStateWithLifecycle(initialValue = null)
    return {
        when (mode.value) {
            null -> null
            AppOpsManager.MODE_ALLOWED -> true
            AppOpsManager.MODE_DEFAULT -> {
                with(packageManagers) { record.app.hasGrantPermission(permission) }
        val isAllowed by record.appOpsPermissionController.isAllowedFlow
            .collectAsStateWithLifecycle(initialValue = null)
        return { isAllowed }
    }

            else -> false
        }
    override fun isChangeable(record: AppOpPermissionRecord) =
        record.hasRequestPermission &&
            !record.hasRequestBroaderPermission &&
            record.app.packageName !in notChangeablePackages

    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
        record.appOpsPermissionController.setAllowed(newAllowed)
    }
}
+167 −0
Original line number Diff line number Diff line
/*
 * 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.spaprivileged.model.app

import android.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.framework.common.appOpsManager
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify

@RunWith(AndroidJUnit4::class)
class AppOpsPermissionControllerTest {

    private val appOpsManager = mock<AppOpsManager>()
    private val packageManager = mock<PackageManager>()
    private val packageManagers = mock<IPackageManagers>()
    private val appOpsController = mock<IAppOpsController>()

    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
        on { appOpsManager } doReturn appOpsManager
        on { packageManager } doReturn packageManager
    }

    @Test
    fun isAllowedFlow_appOpsAllowed_returnTrue() = runBlocking {
        appOpsController.stub {
            on { modeFlow } doReturn flowOf(AppOpsManager.MODE_ALLOWED)
        }
        val controller = AppOpsPermissionController(
            context = context,
            app = APP,
            appOps = AppOps(op = OP),
            permission = PERMISSION,
            appOpsController = appOpsController,
        )

        val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull()

        assertThat(isAllowed).isTrue()
    }

    @Test
    fun isAllowedFlow_appOpsDefaultAndPermissionGranted_returnTrue() = runBlocking {
        appOpsController.stub {
            on { modeFlow } doReturn flowOf(AppOpsManager.MODE_DEFAULT)
        }
        packageManagers.stub {
            on { APP.hasGrantPermission(PERMISSION) } doReturn true
        }
        val controller = AppOpsPermissionController(
            context = context,
            app = APP,
            appOps = AppOps(op = OP),
            permission = PERMISSION,
            packageManagers = packageManagers,
            appOpsController = appOpsController,
        )

        val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull()

        assertThat(isAllowed).isTrue()
    }

    @Test
    fun isAllowedFlow_appOpsDefaultAndPermissionNotGranted_returnFalse() = runBlocking {
        appOpsController.stub {
            on { modeFlow } doReturn flowOf(AppOpsManager.MODE_DEFAULT)
        }
        packageManagers.stub {
            on { APP.hasGrantPermission(PERMISSION) } doReturn false
        }
        val controller = AppOpsPermissionController(
            context = context,
            app = APP,
            appOps = AppOps(op = OP),
            permission = PERMISSION,
            packageManagers = packageManagers,
            appOpsController = appOpsController,
        )

        val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull()

        assertThat(isAllowed).isFalse()
    }

    @Test
    fun isAllowedFlow_appOpsError_returnFalse() = runBlocking {
        appOpsController.stub {
            on { modeFlow } doReturn flowOf(AppOpsManager.MODE_ERRORED)
        }
        val controller = AppOpsPermissionController(
            context = context,
            app = APP,
            appOps = AppOps(op = OP),
            permission = PERMISSION,
            appOpsController = appOpsController,
        )

        val isAllowed = controller.isAllowedFlow.firstWithTimeoutOrNull()

        assertThat(isAllowed).isFalse()
    }

    @Test
    fun setAllowed_notSetModeByUid() {
        val controller = AppOpsPermissionController(
            context = context,
            app = APP,
            appOps = AppOps(op = OP, setModeByUid = false),
            permission = PERMISSION,
        )

        controller.setAllowed(true)

        verify(appOpsManager).setMode(OP, APP.uid, APP.packageName, AppOpsManager.MODE_ALLOWED)
    }

    @Test
    fun setAllowed_setModeByUid() {
        val controller = AppOpsPermissionController(
            context = context,
            app = APP,
            appOps = AppOps(op = OP, setModeByUid = true),
            permission = PERMISSION,
        )

        controller.setAllowed(true)

        verify(appOpsManager).setUidMode(OP, APP.uid, AppOpsManager.MODE_ALLOWED)
    }

    private companion object {
        const val OP = 1
        const val PERMISSION = "Permission"
        val APP = ApplicationInfo().apply {
            packageName = "package.name"
            uid = 123
        }
    }
}
+14 −48
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.framework.common.appOpsManager
import com.android.settingslib.spaprivileged.model.app.AppOps
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.test.R
import com.google.common.truth.Truth.assertThat
@@ -119,7 +119,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = false,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val recordListFlow = listModel.filter(flowOf(USER_ID), flowOf(listOf(record)))
@@ -135,7 +135,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED),
                appOpsPermissionController = FakeAppOpsPermissionController(true),
            )

        val isAllowed = getIsAllowed(record)
@@ -143,38 +143,6 @@ class AppOpPermissionAppListTest {
        assertThat(isAllowed).isTrue()
    }

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

        val isAllowed = getIsAllowed(record)

        assertThat(isAllowed).isTrue()
    }

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

        val isAllowed = getIsAllowed(record)

        assertThat(isAllowed).isFalse()
    }

    @Test
    fun isAllowed_broaderPermissionTrumps() {
        listModel.broaderPermission = BROADER_PERMISSION
@@ -187,7 +155,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = true,
                hasRequestPermission = false,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val isAllowed = getIsAllowed(record)
@@ -202,7 +170,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val isAllowed = getIsAllowed(record)
@@ -217,7 +185,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = false,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val isChangeable = listModel.isChangeable(record)
@@ -232,7 +200,7 @@ class AppOpPermissionAppListTest {
                app = NOT_CHANGEABLE_APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val isChangeable = listModel.isChangeable(record)
@@ -247,7 +215,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val isChangeable = listModel.isChangeable(record)
@@ -263,7 +231,7 @@ class AppOpPermissionAppListTest {
                app = APP,
                hasRequestBroaderPermission = true,
                hasRequestPermission = true,
                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
                appOpsPermissionController = FakeAppOpsPermissionController(false),
            )

        val isChangeable = listModel.isChangeable(record)
@@ -273,18 +241,18 @@ class AppOpPermissionAppListTest {

    @Test
    fun setAllowed() {
        val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
        val appOpsPermissionController = FakeAppOpsPermissionController(false)
        val record =
            AppOpPermissionRecord(
                app = APP,
                hasRequestBroaderPermission = false,
                hasRequestPermission = true,
                appOpsController = appOpsController,
                appOpsPermissionController = appOpsPermissionController,
            )

        listModel.setAllowed(record = record, newAllowed = true)

        assertThat(appOpsController.setAllowedCalledWith).isTrue()
        assertThat(appOpsPermissionController.setAllowedCalledWith).isTrue()
    }

    private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? {
@@ -314,14 +282,12 @@ class AppOpPermissionAppListTest {
    }
}

private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController {
private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController {
    var setAllowedCalledWith: Boolean? = null

    override val modeFlow = flowOf(fakeMode)
    override val isAllowedFlow = flowOf(allowed)

    override fun setAllowed(allowed: Boolean) {
        setAllowedCalledWith = allowed
    }

    override fun getMode() = fakeMode
}