diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt index 0e965ac0f9b..fbdde0b982c 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt @@ -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 @@ -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 { + val context = LocalContext.current + return produceState(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) } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt new file mode 100644 index 00000000000..318debab238 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt @@ -0,0 +1,161 @@ +/* + * 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) + } +}