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)