diff options
| author | 2024-05-21 05:23:09 +0000 | |
|---|---|---|
| committer | 2024-05-21 05:23:09 +0000 | |
| commit | dcefab81bc74a772d9fcc28a10bfc38aaa566a56 (patch) | |
| tree | 70469c82764936efa928c8676e95ed2e38748014 | |
| parent | 23b478138ea70086e452db07099dd04dd388d7e8 (diff) | |
| parent | 632b6d1512e5f85fbf27c0faeca54916895c9598 (diff) | |
Merge "Create AppOpsPermissionController" into 24D1-dev
4 files changed, 264 insertions, 92 deletions
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionController.kt new file mode 100644 index 000000000000..9350f98841a4 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionController.kt @@ -0,0 +1,62 @@ +/* + * 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" + } +} 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 index 37b1d731d548..120b75ecb666 100644 --- 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 @@ -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,14 +118,20 @@ abstract class AppOpPermissionListModel( override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) = recordListFlow.filterItem(::isChangeable) + /** + * 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): () -> Boolean? = - isAllowed( - record = record, - appOpsController = record.appOpsController, - permission = permission, - packageManagers = packageManagers, - ) + override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? { + if (record.hasRequestBroaderPermission) { + // Broader permission trumps the specific permission. + return { true } + } + val isAllowed by record.appOpsPermissionController.isAllowedFlow + .collectAsStateWithLifecycle(initialValue = null) + return { isAllowed } + } override fun isChangeable(record: AppOpPermissionRecord) = record.hasRequestPermission && @@ -132,36 +139,6 @@ abstract class AppOpPermissionListModel( 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? { - 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) } - } - - else -> false - } + record.appOpsPermissionController.setAllowed(newAllowed) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt new file mode 100644 index 000000000000..9f80b92548d2 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt @@ -0,0 +1,167 @@ +/* + * 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 + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index 07ccdd5a3695..9d12fc7611cb 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt @@ -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) @@ -144,38 +144,6 @@ class AppOpPermissionAppListTest { } @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 with(packageManagers) { @@ -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 } |