Loading src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +72 −28 Original line number Diff line number Diff line Loading @@ -18,8 +18,10 @@ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.pm.ApplicationInfo import android.os.UserManager import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.State import androidx.compose.runtime.produceState import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.android.settings.R Loading @@ -27,48 +29,90 @@ import com.android.settings.Utils import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers import com.android.settingslib.spaprivileged.model.app.isDisallowControl import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.scaffold.RestrictedMenuItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @Composable fun AppInfoSettingsMoreOptions(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) { val context = LocalContext.current // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear data // on all users. We also don't allow uninstalling for all users if it's DO/PO for any user. val isProfileOrDeviceOwner = remember(app) { Utils.isProfileOrDeviceOwner( context.userManager, context.devicePolicyManager, app.packageName ) fun AppInfoSettingsMoreOptions( packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo, packageManagers: IPackageManagers = PackageManagers, ) { val state = app.produceState(packageManagers).value ?: return when { // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. We also don't allow uninstalling for all users if it's DO/PO for any // user. state.isProfileOrDeviceOwner -> return !state.shownUninstallUpdates && !state.shownUninstallForAllUsers -> return } if (isProfileOrDeviceOwner) return val shownUninstallUpdates = remember(app) { isShowUninstallUpdates(context, app) } val shownUninstallForAllUsers = remember(app) { isShowUninstallForAllUsers(context, app) } if (!shownUninstallUpdates && !shownUninstallForAllUsers) return MoreOptionsAction { if (shownUninstallUpdates) { MenuItem(text = stringResource(R.string.app_factory_reset)) { val restrictions = Restrictions(userId = app.userId, keys = listOf(UserManager.DISALLOW_APPS_CONTROL)) if (state.shownUninstallUpdates) { RestrictedMenuItem( text = stringResource(R.string.app_factory_reset), restrictions = restrictions, ) { packageInfoPresenter.startUninstallActivity(forAllUsers = false) } } if (shownUninstallForAllUsers) { MenuItem(text = stringResource(R.string.uninstall_all_users_text)) { if (state.shownUninstallForAllUsers) { RestrictedMenuItem( text = stringResource(R.string.uninstall_all_users_text), restrictions = restrictions, ) { packageInfoPresenter.startUninstallActivity(forAllUsers = true) } } } } private fun isShowUninstallUpdates(context: Context, app: ApplicationInfo): Boolean = app.isUpdatedSystemApp && context.userManager.isUserAdmin(app.userId) && !app.isDisallowControl(context) && private data class AppInfoSettingsMoreOptionsState( val isProfileOrDeviceOwner: Boolean, val shownUninstallUpdates: Boolean, val shownUninstallForAllUsers: Boolean, ) @Composable private fun ApplicationInfo.produceState( packageManagers: IPackageManagers, ): State<AppInfoSettingsMoreOptionsState?> { val context = LocalContext.current return produceState<AppInfoSettingsMoreOptionsState?>(initialValue = null, this) { withContext(Dispatchers.IO) { value = AppInfoSettingsMoreOptionsState( isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner( context.userManager, context.devicePolicyManager, packageName ), shownUninstallUpdates = isShowUninstallUpdates(context), shownUninstallForAllUsers = isShowUninstallForAllUsers( userManager = context.userManager, packageManagers = packageManagers, ), ) } } } private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean = isUpdatedSystemApp && context.userManager.isUserAdmin(userId) && !context.resources.getBoolean(R.bool.config_disable_uninstall_update) private fun isShowUninstallForAllUsers(context: Context, app: ApplicationInfo): Boolean = app.userId == 0 && !app.isSystemApp && !app.isInstantApp && isOtherUserHasInstallPackage(context, app) private fun ApplicationInfo.isShowUninstallForAllUsers( userManager: UserManager, packageManagers: IPackageManagers, ): Boolean = userId == 0 && !isSystemApp && !isInstantApp && isOtherUserHasInstallPackage(userManager, packageManagers) private fun isOtherUserHasInstallPackage(context: Context, app: ApplicationInfo): Boolean = context.userManager.aliveUsers .filter { it.id != app.userId } .any { PackageManagers.isPackageInstalledAsUser(app.packageName, it.id) } private fun ApplicationInfo.isOtherUserHasInstallPackage( userManager: UserManager, packageManagers: IPackageManagers, ): Boolean = userManager.aliveUsers .filter { it.id != userId } .any { packageManagers.isPackageInstalledAsUser(packageName, it.id) } tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt 0 → 100644 +161 −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.settings.spa.app.appinfo import android.app.admin.DevicePolicyManager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.UserInfo import android.os.UserManager import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settings.Utils import com.android.settingslib.spa.testutils.delay import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.userId import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoSession import org.mockito.Spy import org.mockito.quality.Strictness import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class AppInfoSettingsMoreOptionsTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var mockSession: MockitoSession @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageInfoPresenter: PackageInfoPresenter @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var userManager: UserManager @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Spy private var resources = context.resources @Mock private lateinit var packageManagers: IPackageManagers @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() .initMocks(this) .mockStatic(Utils::class.java) .strictness(Strictness.LENIENT) .startMocking() whenever(packageInfoPresenter.context).thenReturn(context) whenever(context.packageManager).thenReturn(packageManager) whenever(context.userManager).thenReturn(userManager) whenever(context.devicePolicyManager).thenReturn(devicePolicyManager) whenever(Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, PACKAGE_NAME)) .thenReturn(false) } @After fun tearDown() { mockSession.finishMocking() } @Test fun whenProfileOrDeviceOwner_notDisplayed() { whenever(Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, PACKAGE_NAME)) .thenReturn(true) setContent(ApplicationInfo()) composeTestRule.onRoot().assertIsNotDisplayed() } @Test fun uninstallUpdates_updatedSystemAppAndUserAdmin_displayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID flags = ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP } whenever(userManager.isUserAdmin(app.userId)).thenReturn(true) whenever(resources.getBoolean(R.bool.config_disable_uninstall_update)).thenReturn(false) setContent(app) composeTestRule.onRoot().performClick() composeTestRule.waitUntilExists(hasText(context.getString(R.string.app_factory_reset))) } @Test fun uninstallForAllUsers_regularAppAndPrimaryUser_displayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID } whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER)) whenever(packageManagers.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID)) .thenReturn(true) setContent(app) composeTestRule.onRoot().performClick() composeTestRule.waitUntilExists( hasText(context.getString(R.string.uninstall_all_users_text)) ) } private fun setContent(app: ApplicationInfo) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { AppInfoSettingsMoreOptions(packageInfoPresenter, app, packageManagers) } } composeTestRule.delay() } private companion object { const val PACKAGE_NAME = "package.name" const val UID = 123 const val OTHER_USER_ID = 10 val OTHER_USER = UserInfo(OTHER_USER_ID, "Other user", 0) } } Loading
src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +72 −28 Original line number Diff line number Diff line Loading @@ -18,8 +18,10 @@ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.pm.ApplicationInfo import android.os.UserManager import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.State import androidx.compose.runtime.produceState import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.android.settings.R Loading @@ -27,48 +29,90 @@ import com.android.settings.Utils import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers import com.android.settingslib.spaprivileged.model.app.isDisallowControl import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.scaffold.RestrictedMenuItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @Composable fun AppInfoSettingsMoreOptions(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) { val context = LocalContext.current // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear data // on all users. We also don't allow uninstalling for all users if it's DO/PO for any user. val isProfileOrDeviceOwner = remember(app) { Utils.isProfileOrDeviceOwner( context.userManager, context.devicePolicyManager, app.packageName ) fun AppInfoSettingsMoreOptions( packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo, packageManagers: IPackageManagers = PackageManagers, ) { val state = app.produceState(packageManagers).value ?: return when { // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. We also don't allow uninstalling for all users if it's DO/PO for any // user. state.isProfileOrDeviceOwner -> return !state.shownUninstallUpdates && !state.shownUninstallForAllUsers -> return } if (isProfileOrDeviceOwner) return val shownUninstallUpdates = remember(app) { isShowUninstallUpdates(context, app) } val shownUninstallForAllUsers = remember(app) { isShowUninstallForAllUsers(context, app) } if (!shownUninstallUpdates && !shownUninstallForAllUsers) return MoreOptionsAction { if (shownUninstallUpdates) { MenuItem(text = stringResource(R.string.app_factory_reset)) { val restrictions = Restrictions(userId = app.userId, keys = listOf(UserManager.DISALLOW_APPS_CONTROL)) if (state.shownUninstallUpdates) { RestrictedMenuItem( text = stringResource(R.string.app_factory_reset), restrictions = restrictions, ) { packageInfoPresenter.startUninstallActivity(forAllUsers = false) } } if (shownUninstallForAllUsers) { MenuItem(text = stringResource(R.string.uninstall_all_users_text)) { if (state.shownUninstallForAllUsers) { RestrictedMenuItem( text = stringResource(R.string.uninstall_all_users_text), restrictions = restrictions, ) { packageInfoPresenter.startUninstallActivity(forAllUsers = true) } } } } private fun isShowUninstallUpdates(context: Context, app: ApplicationInfo): Boolean = app.isUpdatedSystemApp && context.userManager.isUserAdmin(app.userId) && !app.isDisallowControl(context) && private data class AppInfoSettingsMoreOptionsState( val isProfileOrDeviceOwner: Boolean, val shownUninstallUpdates: Boolean, val shownUninstallForAllUsers: Boolean, ) @Composable private fun ApplicationInfo.produceState( packageManagers: IPackageManagers, ): State<AppInfoSettingsMoreOptionsState?> { val context = LocalContext.current return produceState<AppInfoSettingsMoreOptionsState?>(initialValue = null, this) { withContext(Dispatchers.IO) { value = AppInfoSettingsMoreOptionsState( isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner( context.userManager, context.devicePolicyManager, packageName ), shownUninstallUpdates = isShowUninstallUpdates(context), shownUninstallForAllUsers = isShowUninstallForAllUsers( userManager = context.userManager, packageManagers = packageManagers, ), ) } } } private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean = isUpdatedSystemApp && context.userManager.isUserAdmin(userId) && !context.resources.getBoolean(R.bool.config_disable_uninstall_update) private fun isShowUninstallForAllUsers(context: Context, app: ApplicationInfo): Boolean = app.userId == 0 && !app.isSystemApp && !app.isInstantApp && isOtherUserHasInstallPackage(context, app) private fun ApplicationInfo.isShowUninstallForAllUsers( userManager: UserManager, packageManagers: IPackageManagers, ): Boolean = userId == 0 && !isSystemApp && !isInstantApp && isOtherUserHasInstallPackage(userManager, packageManagers) private fun isOtherUserHasInstallPackage(context: Context, app: ApplicationInfo): Boolean = context.userManager.aliveUsers .filter { it.id != app.userId } .any { PackageManagers.isPackageInstalledAsUser(app.packageName, it.id) } private fun ApplicationInfo.isOtherUserHasInstallPackage( userManager: UserManager, packageManagers: IPackageManagers, ): Boolean = userManager.aliveUsers .filter { it.id != userId } .any { packageManagers.isPackageInstalledAsUser(packageName, it.id) }
tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt 0 → 100644 +161 −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.settings.spa.app.appinfo import android.app.admin.DevicePolicyManager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.UserInfo import android.os.UserManager import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settings.Utils import com.android.settingslib.spa.testutils.delay import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.userId import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoSession import org.mockito.Spy import org.mockito.quality.Strictness import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class AppInfoSettingsMoreOptionsTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var mockSession: MockitoSession @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageInfoPresenter: PackageInfoPresenter @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var userManager: UserManager @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Spy private var resources = context.resources @Mock private lateinit var packageManagers: IPackageManagers @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() .initMocks(this) .mockStatic(Utils::class.java) .strictness(Strictness.LENIENT) .startMocking() whenever(packageInfoPresenter.context).thenReturn(context) whenever(context.packageManager).thenReturn(packageManager) whenever(context.userManager).thenReturn(userManager) whenever(context.devicePolicyManager).thenReturn(devicePolicyManager) whenever(Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, PACKAGE_NAME)) .thenReturn(false) } @After fun tearDown() { mockSession.finishMocking() } @Test fun whenProfileOrDeviceOwner_notDisplayed() { whenever(Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, PACKAGE_NAME)) .thenReturn(true) setContent(ApplicationInfo()) composeTestRule.onRoot().assertIsNotDisplayed() } @Test fun uninstallUpdates_updatedSystemAppAndUserAdmin_displayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID flags = ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP } whenever(userManager.isUserAdmin(app.userId)).thenReturn(true) whenever(resources.getBoolean(R.bool.config_disable_uninstall_update)).thenReturn(false) setContent(app) composeTestRule.onRoot().performClick() composeTestRule.waitUntilExists(hasText(context.getString(R.string.app_factory_reset))) } @Test fun uninstallForAllUsers_regularAppAndPrimaryUser_displayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID } whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER)) whenever(packageManagers.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID)) .thenReturn(true) setContent(app) composeTestRule.onRoot().performClick() composeTestRule.waitUntilExists( hasText(context.getString(R.string.uninstall_all_users_text)) ) } private fun setContent(app: ApplicationInfo) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { AppInfoSettingsMoreOptions(packageInfoPresenter, app, packageManagers) } } composeTestRule.delay() } private companion object { const val PACKAGE_NAME = "package.name" const val UID = 123 const val OTHER_USER_ID = 10 val OTHER_USER = UserInfo(OTHER_USER_ID, "Other user", 0) } }