From 9b65b1583d5588b83a6631f334bcc6daf5d718e9 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 29 Sep 2022 13:17:12 +0800 Subject: [PATCH] Add uninstall updates & uninstall for all users These are the items in the more options of App Settings page. Uninstall updates only shows up for updated system app. Uninstall for all users only shows up for primary user when a non-system app is installed on multiple users. Bug: 236346018 Test: Manual on App Settings page Change-Id: I7530ce5215ed921c0a2b767dce56cbfd9a2b0137 --- .../app/appsettings/AppButtonRepository.kt | 30 +++---- .../spa/app/appsettings/AppDisableButton.kt | 15 ++-- .../spa/app/appsettings/AppForceStopButton.kt | 3 +- .../spa/app/appsettings/AppSettings.kt | 11 ++- .../app/appsettings/AppSettingsMoreOptions.kt | 85 +++++++++++++++++++ .../spa/app/appsettings/AppUninstallButton.kt | 28 ++---- .../app/appsettings/PackageInfoPresenter.kt | 23 ++++- 7 files changed, 140 insertions(+), 55 deletions(-) create mode 100644 src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt diff --git a/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt index e1ee766603f..c5e84ae886d 100644 --- a/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt +++ b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt @@ -17,19 +17,18 @@ package com.android.settings.spa.app.appsettings import android.app.ActivityManager -import android.app.admin.DevicePolicyManager +import android.content.ComponentName import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo -import android.os.UserManager -import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.Utils -import com.android.settingslib.spaprivileged.model.app.userId +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.model.app.isDisallowControl class AppButtonRepository(private val context: Context) { private val packageManager = context.packageManager - private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + private val devicePolicyManager = context.devicePolicyManager /** * Checks whether the given application is disallowed from modifying. @@ -41,20 +40,10 @@ class AppButtonRepository(private val context: Context) { // If the uninstallation intent is already queued, disable the button. devicePolicyManager.isUninstallInQueue(app.packageName) -> true - RestrictedLockUtilsInternal.hasBaseUserRestriction( - context, UserManager.DISALLOW_APPS_CONTROL, app.userId - ) -> true - - else -> false + else -> app.isDisallowControl(context) } - /** - * Checks whether the given application is an active admin. - */ - fun isActiveAdmin(app: ApplicationInfo): Boolean = - devicePolicyManager.packageHasActiveAdmins(app.packageName, app.userId) - - fun getHomePackageInfo(): AppUninstallButton.HomePackages { + fun getHomePackageInfo(): HomePackages { val homePackages = mutableSetOf() val homeActivities = ArrayList() val currentDefaultHome = packageManager.getHomeActivities(homeActivities) @@ -66,7 +55,7 @@ class AppButtonRepository(private val context: Context) { homePackages.add(metaPackageName) } } - return AppUninstallButton.HomePackages(homePackages, currentDefaultHome) + return HomePackages(homePackages, currentDefaultHome) } private fun signaturesMatch(packageName1: String, packageName2: String): Boolean = try { @@ -75,4 +64,9 @@ class AppButtonRepository(private val context: Context) { // e.g. named alternate package not found during lookup; this is an expected case sometimes false } + + data class HomePackages( + val homePackages: Set, + val currentDefaultHome: ComponentName?, + ) } diff --git a/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt index 365da7c0036..cc5a76e2f99 100644 --- a/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt @@ -16,10 +16,7 @@ package com.android.settings.spa.app.appsettings -import android.app.admin.DevicePolicyManager -import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.os.UserManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowCircleDown import androidx.compose.material.icons.outlined.HideSource @@ -34,10 +31,12 @@ import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settings.Utils import com.android.settings.overlay.FeatureFactory -import com.android.settingslib.Utils as SettingsLibUtils import com.android.settingslib.spa.widget.button.ActionButton -import com.android.settingslib.spaprivileged.model.app.hasFlag +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.framework.common.userManager +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.isDisabledUntilUsed +import com.android.settingslib.Utils as SettingsLibUtils class AppDisableButton( private val packageInfoPresenter: PackageInfoPresenter, @@ -46,8 +45,8 @@ class AppDisableButton( private val appButtonRepository = AppButtonRepository(context) private val resources = context.resources private val packageManager = context.packageManager - private val userManager = context.getSystemService(UserManager::class.java)!! - private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + private val userManager = context.userManager + private val devicePolicyManager = context.devicePolicyManager private val applicationFeatureProvider = FeatureFactory.getFactory(context).getApplicationFeatureProvider(context) @@ -84,7 +83,7 @@ class AppDisableButton( SettingsLibUtils.isSystemPackage(resources, packageManager, packageInfo) -> false // If this is a device admin, it can't be disabled. - appButtonRepository.isActiveAdmin(app) -> false + app.isActiveAdmin(context) -> false // We don't allow disabling DO/PO on *any* users if it's a system app, because // "disabling" is actually "downgrade to the system version + disable", and "downgrade" diff --git a/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt index 49191637677..c34eff027d5 100644 --- a/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt @@ -36,6 +36,7 @@ import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spaprivileged.model.app.hasFlag +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.userId class AppForceStopButton( @@ -61,7 +62,7 @@ class AppForceStopButton( */ private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when { // User can't force stop device admin. - appButtonRepository.isActiveAdmin(app) -> false + app.isActiveAdmin(context) -> false appButtonRepository.isDisallowControl(app) -> false diff --git a/src/com/android/settings/spa/app/appsettings/AppSettings.kt b/src/com/android/settings/spa/app/appsettings/AppSettings.kt index 4c960e5a17a..615fa75d860 100644 --- a/src/com/android/settings/spa/app/appsettings/AppSettings.kt +++ b/src/com/android/settings/spa/app/appsettings/AppSettings.kt @@ -60,7 +60,7 @@ object AppSettingsProvider : SettingsPageProvider { PackageInfoPresenter(context, packageName, userId, coroutineScope) } AppSettings(packageInfoPresenter) - packageInfoPresenter.PageCloser() + packageInfoPresenter.PackageRemoveDetector() } @Composable @@ -77,7 +77,13 @@ object AppSettingsProvider : SettingsPageProvider { @Composable private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) { val packageInfo = packageInfoPresenter.flow.collectAsState().value ?: return - RegularScaffold(title = stringResource(R.string.application_info_label)) { + val app = packageInfo.applicationInfo + RegularScaffold( + title = stringResource(R.string.application_info_label), + actions = { + AppSettingsMoreOptions(packageInfoPresenter, app) + } + ) { val appInfoProvider = remember { AppInfoProvider(packageInfo) } appInfoProvider.AppInfo() @@ -85,7 +91,6 @@ private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) { AppButtons(packageInfoPresenter) Category(title = stringResource(R.string.advanced_apps)) { - val app = packageInfo.applicationInfo DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) ModifySystemSettingsAppListProvider.InfoPageEntryItem(app) PictureInPictureListProvider.InfoPageEntryItem(app) diff --git a/src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt b/src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt new file mode 100644 index 00000000000..d17ff33bf39 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt @@ -0,0 +1,85 @@ +/* + * 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.appsettings + +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +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.PackageManagers +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin +import com.android.settingslib.spaprivileged.model.app.isDisallowControl +import com.android.settingslib.spaprivileged.model.app.userId + +@Composable +fun AppSettingsMoreOptions(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 + ) + } + if (isProfileOrDeviceOwner) return + val shownUninstallUpdates = remember(app) { isShowUninstallUpdates(context, app) } + val shownUninstallForAllUsers = remember(app) { isShowUninstallForAllUsers(context, app) } + if (!shownUninstallUpdates && !shownUninstallForAllUsers) return + MoreOptionsAction { onDismissRequest -> + if (shownUninstallUpdates) { + DropdownMenuItem( + text = { Text(stringResource(R.string.app_factory_reset)) }, + onClick = { + onDismissRequest() + packageInfoPresenter.startUninstallActivity(forAllUsers = false) + }, + ) + } + if (shownUninstallForAllUsers) { + DropdownMenuItem( + text = { Text(stringResource(R.string.uninstall_all_users_text)) }, + onClick = { + onDismissRequest() + packageInfoPresenter.startUninstallActivity(forAllUsers = true) + }, + ) + } + } +} + +private fun isShowUninstallUpdates(context: Context, app: ApplicationInfo): Boolean = + app.isUpdatedSystemApp && context.userManager.isUserAdmin(app.userId) && + !app.isDisallowControl(context) && + !context.resources.getBoolean(R.bool.config_disable_uninstall_update) + +private fun isShowUninstallForAllUsers(context: Context, app: ApplicationInfo): Boolean = + app.userId == 0 && !app.isSystemApp && !app.isInstantApp && !app.isActiveAdmin(context) && + isOtherUserHasInstallPackage(context, app) + +private fun isOtherUserHasInstallPackage(context: Context, app: ApplicationInfo): Boolean = + context.userManager.aliveUsers + .filter { it.id != app.userId } + .any { PackageManagers.isPackageInstalledAsUser(app.packageName, it.id) } diff --git a/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt index 3458f1ddc5e..cdb2cd01e8f 100644 --- a/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt @@ -16,7 +16,6 @@ package com.android.settings.spa.app.appsettings -import android.app.admin.DevicePolicyManager import android.app.settings.SettingsEnums import android.content.ComponentName import android.content.Intent @@ -24,7 +23,6 @@ import android.content.om.OverlayManager import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.net.Uri -import android.os.UserManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import com.android.settings.R @@ -33,16 +31,17 @@ import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAd import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.model.app.hasFlag +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userId class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) { private val context = packageInfoPresenter.context private val appButtonRepository = AppButtonRepository(context) - private val userManager = context.getSystemService(UserManager::class.java)!! private val overlayManager = context.getSystemService(OverlayManager::class.java)!! - private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + private val devicePolicyManager = context.devicePolicyManager fun getActionButton(packageInfo: PackageInfo): ActionButton? { val app = packageInfo.applicationInfo @@ -52,8 +51,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) /** Gets whether a package can be uninstalled. */ private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when { - // When we have multiple users, there is a separate menu to uninstall for all users. - !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && userManager.users.size >= 2 -> false + !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) -> false // Not allow to uninstall DO/PO. Utils.isProfileOrDeviceOwner(devicePolicyManager, app.packageName, app.userId) -> false @@ -101,7 +99,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) ) { onUninstallClicked(app) } private fun onUninstallClicked(app: ApplicationInfo) { - if (appButtonRepository.isActiveAdmin(app)) { + if (app.isActiveAdmin(context)) { packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN) val intent = Intent(context, DeviceAdminAdd::class.java).apply { putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, app.packageName) @@ -115,20 +113,6 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin) return } - startUninstallActivity(app) - } - - data class HomePackages( - val homePackages: Set, - val currentDefaultHome: ComponentName?, - ) - - private fun startUninstallActivity(app: ApplicationInfo) { - val packageUri = Uri.parse("package:${app.packageName}") - packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) - val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri).apply { - putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, !app.hasFlag(ApplicationInfo.FLAG_INSTALLED)) - } - context.startActivityAsUser(intent, app.userHandle) + packageInfoPresenter.startUninstallActivity() } } diff --git a/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt index b164d7c5064..299c0a7dea1 100644 --- a/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt +++ b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.net.Uri import android.os.UserHandle import android.util.Log import androidx.compose.runtime.Composable @@ -65,17 +66,23 @@ class PackageInfoPresenter( } /** - * Closes the page when the package is uninstalled. + * Detects the package removed event. */ @Composable - fun PageCloser() { + fun PackageRemoveDetector() { val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED).apply { addDataScheme("package") } val navController = LocalNavController.current DisposableBroadcastReceiverAsUser(userId, intentFilter) { intent -> if (packageName == intent.data?.schemeSpecificPart) { - navController.navigateBack() + val packageInfo = flow.value + if (packageInfo != null && packageInfo.applicationInfo.isSystemApp) { + // System app still exists after uninstalling the updates, refresh the page. + notifyChange() + } else { + navController.navigateBack() + } } } } @@ -102,6 +109,16 @@ class PackageInfoPresenter( } } + /** Starts the uninstallation activity. */ + fun startUninstallActivity(forAllUsers: Boolean = false) { + logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) + val packageUri = Uri.parse("package:${packageName}") + val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri).apply { + putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, forAllUsers) + } + context.startActivityAsUser(intent, UserHandle.of(userId)) + } + /** Clears this instant app. */ fun clearInstantApp() { logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)