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
This commit is contained in:
Chaohui Wang
2022-09-29 13:17:12 +08:00
parent 7075bc4faa
commit 9b65b1583d
7 changed files with 140 additions and 55 deletions

View File

@@ -17,19 +17,18 @@
package com.android.settings.spa.app.appsettings package com.android.settings.spa.app.appsettings
import android.app.ActivityManager import android.app.ActivityManager
import android.app.admin.DevicePolicyManager import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.os.UserManager
import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.Utils 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) { class AppButtonRepository(private val context: Context) {
private val packageManager = context.packageManager 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. * 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. // If the uninstallation intent is already queued, disable the button.
devicePolicyManager.isUninstallInQueue(app.packageName) -> true devicePolicyManager.isUninstallInQueue(app.packageName) -> true
RestrictedLockUtilsInternal.hasBaseUserRestriction( else -> app.isDisallowControl(context)
context, UserManager.DISALLOW_APPS_CONTROL, app.userId
) -> true
else -> false
} }
/** fun getHomePackageInfo(): HomePackages {
* Checks whether the given application is an active admin.
*/
fun isActiveAdmin(app: ApplicationInfo): Boolean =
devicePolicyManager.packageHasActiveAdmins(app.packageName, app.userId)
fun getHomePackageInfo(): AppUninstallButton.HomePackages {
val homePackages = mutableSetOf<String>() val homePackages = mutableSetOf<String>()
val homeActivities = ArrayList<ResolveInfo>() val homeActivities = ArrayList<ResolveInfo>()
val currentDefaultHome = packageManager.getHomeActivities(homeActivities) val currentDefaultHome = packageManager.getHomeActivities(homeActivities)
@@ -66,7 +55,7 @@ class AppButtonRepository(private val context: Context) {
homePackages.add(metaPackageName) homePackages.add(metaPackageName)
} }
} }
return AppUninstallButton.HomePackages(homePackages, currentDefaultHome) return HomePackages(homePackages, currentDefaultHome)
} }
private fun signaturesMatch(packageName1: String, packageName2: String): Boolean = try { 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 // e.g. named alternate package not found during lookup; this is an expected case sometimes
false false
} }
data class HomePackages(
val homePackages: Set<String>,
val currentDefaultHome: ComponentName?,
)
} }

View File

@@ -16,10 +16,7 @@
package com.android.settings.spa.app.appsettings package com.android.settings.spa.app.appsettings
import android.app.admin.DevicePolicyManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.UserManager
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowCircleDown import androidx.compose.material.icons.outlined.ArrowCircleDown
import androidx.compose.material.icons.outlined.HideSource 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.R
import com.android.settings.Utils import com.android.settings.Utils
import com.android.settings.overlay.FeatureFactory 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.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.spaprivileged.model.app.isDisabledUntilUsed
import com.android.settingslib.Utils as SettingsLibUtils
class AppDisableButton( class AppDisableButton(
private val packageInfoPresenter: PackageInfoPresenter, private val packageInfoPresenter: PackageInfoPresenter,
@@ -46,8 +45,8 @@ class AppDisableButton(
private val appButtonRepository = AppButtonRepository(context) private val appButtonRepository = AppButtonRepository(context)
private val resources = context.resources private val resources = context.resources
private val packageManager = context.packageManager private val packageManager = context.packageManager
private val userManager = context.getSystemService(UserManager::class.java)!! private val userManager = context.userManager
private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! private val devicePolicyManager = context.devicePolicyManager
private val applicationFeatureProvider = private val applicationFeatureProvider =
FeatureFactory.getFactory(context).getApplicationFeatureProvider(context) FeatureFactory.getFactory(context).getApplicationFeatureProvider(context)
@@ -84,7 +83,7 @@ class AppDisableButton(
SettingsLibUtils.isSystemPackage(resources, packageManager, packageInfo) -> false SettingsLibUtils.isSystemPackage(resources, packageManager, packageInfo) -> false
// If this is a device admin, it can't be disabled. // 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 // 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" // "disabling" is actually "downgrade to the system version + disable", and "downgrade"

View File

@@ -36,6 +36,7 @@ import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spaprivileged.model.app.hasFlag import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.app.userId
class AppForceStopButton( class AppForceStopButton(
@@ -61,7 +62,7 @@ class AppForceStopButton(
*/ */
private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when { private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when {
// User can't force stop device admin. // User can't force stop device admin.
appButtonRepository.isActiveAdmin(app) -> false app.isActiveAdmin(context) -> false
appButtonRepository.isDisallowControl(app) -> false appButtonRepository.isDisallowControl(app) -> false

View File

@@ -60,7 +60,7 @@ object AppSettingsProvider : SettingsPageProvider {
PackageInfoPresenter(context, packageName, userId, coroutineScope) PackageInfoPresenter(context, packageName, userId, coroutineScope)
} }
AppSettings(packageInfoPresenter) AppSettings(packageInfoPresenter)
packageInfoPresenter.PageCloser() packageInfoPresenter.PackageRemoveDetector()
} }
@Composable @Composable
@@ -77,7 +77,13 @@ object AppSettingsProvider : SettingsPageProvider {
@Composable @Composable
private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) { private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) {
val packageInfo = packageInfoPresenter.flow.collectAsState().value ?: return 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) } val appInfoProvider = remember { AppInfoProvider(packageInfo) }
appInfoProvider.AppInfo() appInfoProvider.AppInfo()
@@ -85,7 +91,6 @@ private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) {
AppButtons(packageInfoPresenter) AppButtons(packageInfoPresenter)
Category(title = stringResource(R.string.advanced_apps)) { Category(title = stringResource(R.string.advanced_apps)) {
val app = packageInfo.applicationInfo
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)
ModifySystemSettingsAppListProvider.InfoPageEntryItem(app) ModifySystemSettingsAppListProvider.InfoPageEntryItem(app)
PictureInPictureListProvider.InfoPageEntryItem(app) PictureInPictureListProvider.InfoPageEntryItem(app)

View File

@@ -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) }

View File

@@ -16,7 +16,6 @@
package com.android.settings.spa.app.appsettings package com.android.settings.spa.app.appsettings
import android.app.admin.DevicePolicyManager
import android.app.settings.SettingsEnums import android.app.settings.SettingsEnums
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
@@ -24,7 +23,6 @@ import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import android.os.UserManager
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import com.android.settings.R 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.RestrictedLockUtils
import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.spa.widget.button.ActionButton 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.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userHandle
import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.app.userId
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) { class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
private val context = packageInfoPresenter.context private val context = packageInfoPresenter.context
private val appButtonRepository = AppButtonRepository(context) private val appButtonRepository = AppButtonRepository(context)
private val userManager = context.getSystemService(UserManager::class.java)!!
private val overlayManager = context.getSystemService(OverlayManager::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? { fun getActionButton(packageInfo: PackageInfo): ActionButton? {
val app = packageInfo.applicationInfo val app = packageInfo.applicationInfo
@@ -52,8 +51,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
/** Gets whether a package can be uninstalled. */ /** Gets whether a package can be uninstalled. */
private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when { 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) -> false
!app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && userManager.users.size >= 2 -> false
// Not allow to uninstall DO/PO. // Not allow to uninstall DO/PO.
Utils.isProfileOrDeviceOwner(devicePolicyManager, app.packageName, app.userId) -> false Utils.isProfileOrDeviceOwner(devicePolicyManager, app.packageName, app.userId) -> false
@@ -101,7 +99,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
) { onUninstallClicked(app) } ) { onUninstallClicked(app) }
private fun onUninstallClicked(app: ApplicationInfo) { private fun onUninstallClicked(app: ApplicationInfo) {
if (appButtonRepository.isActiveAdmin(app)) { if (app.isActiveAdmin(context)) {
packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN) packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN)
val intent = Intent(context, DeviceAdminAdd::class.java).apply { val intent = Intent(context, DeviceAdminAdd::class.java).apply {
putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, app.packageName) putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, app.packageName)
@@ -115,20 +113,6 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin) RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin)
return return
} }
startUninstallActivity(app) packageInfoPresenter.startUninstallActivity()
}
data class HomePackages(
val homePackages: Set<String>,
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)
} }
} }

View File

@@ -23,6 +23,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composable 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 @Composable
fun PageCloser() { fun PackageRemoveDetector() {
val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED).apply { val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED).apply {
addDataScheme("package") addDataScheme("package")
} }
val navController = LocalNavController.current val navController = LocalNavController.current
DisposableBroadcastReceiverAsUser(userId, intentFilter) { intent -> DisposableBroadcastReceiverAsUser(userId, intentFilter) { intent ->
if (packageName == intent.data?.schemeSpecificPart) { 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. */ /** Clears this instant app. */
fun clearInstantApp() { fun clearInstantApp() {
logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP) logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)