diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e368d09c3dc..1f7e037f03a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4579,6 +4579,13 @@ + + + - - - generateBatteryEntryListFromBatteryUsageStats( + final Context context, + @Nullable final BatteryUsageStats batteryUsageStats, + @Nullable BatteryAppListPreferenceController batteryAppListPreferenceController) { + if (batteryUsageStats == null) { + Log.w(TAG, "batteryUsageStats is null content"); + return null; + } + // Loads the battery consuming data. + final BatteryAppListPreferenceController controller = + batteryAppListPreferenceController == null + ? new BatteryAppListPreferenceController( + context, + /*preferenceKey=*/ null, + /*lifecycle=*/ null, + /*activity*=*/ null, + /*fragment=*/ null) + : batteryAppListPreferenceController; + + return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true); + } + /** * @return Returns the processed history map which has interpolated to every hour data. * The start and end timestamp must be the even hours. @@ -477,20 +527,14 @@ public final class DataProcessor { final Context context) { BatteryDiffData batteryDiffData = null; try { - final BatteryUsageStatsQuery batteryUsageStatsQuery = - new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build(); - final BatteryUsageStats batteryUsageStats = - context.getSystemService(BatteryStatsManager.class) - .getBatteryUsageStats(batteryUsageStatsQuery); - - if (batteryUsageStats == null) { - Log.w(TAG, "batteryUsageStats is null content"); - return null; - } - + final BatteryUsageStats batteryUsageStats = getBatteryUsageStats(context); final List batteryEntryList = - generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats); + generateBatteryEntryListFromBatteryUsageStats( + context, + batteryUsageStats, + /*batteryAppListPreferenceController=*/ null); batteryDiffData = generateBatteryDiffData(context, batteryEntryList, batteryUsageStats); + closeBatteryUsageStats(batteryUsageStats); } catch (RuntimeException e) { Log.e(TAG, "load batteryUsageStats:" + e); } @@ -498,20 +542,6 @@ public final class DataProcessor { return batteryDiffData; } - @Nullable - private static List generateBatteryEntryListFromBatteryUsageStats( - final Context context, final BatteryUsageStats batteryUsageStats) { - // Loads the battery consuming data. - final BatteryAppListPreferenceController controller = - new BatteryAppListPreferenceController( - context, - /*preferenceKey=*/ null, - /*lifecycle=*/ null, - /*activity*=*/ null, - /*fragment=*/ null); - return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true); - } - @Nullable private static List convertToBatteryHistEntry( @Nullable final List batteryEntryList, diff --git a/src/com/android/settings/spa/SpaDebugActivity.kt b/src/com/android/settings/spa/SpaDebugActivity.kt index 40fe1d5822b..d9a4add1eec 100644 --- a/src/com/android/settings/spa/SpaDebugActivity.kt +++ b/src/com/android/settings/spa/SpaDebugActivity.kt @@ -18,5 +18,8 @@ package com.android.settings.spa import com.android.settingslib.spa.framework.DebugActivity -class SpaDebugActivity : - DebugActivity(SpaEnvironment.settingsEntryRepository, SpaActivity::class.java) +class SpaDebugActivity : DebugActivity( + SpaEnvironment.settingsEntryRepository, + browseActivityClass = SpaActivity::class.java, + entryProviderAuthorities = "com.android.settings.spa.provider", +) diff --git a/src/com/android/settings/spa/SpaEntryProvider.kt b/src/com/android/settings/spa/SpaEntryProvider.kt new file mode 100644 index 00000000000..5fa7f564599 --- /dev/null +++ b/src/com/android/settings/spa/SpaEntryProvider.kt @@ -0,0 +1,24 @@ +/* + * 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 + +import com.android.settingslib.spa.framework.EntryProvider + +class SpaEntryProvider : EntryProvider( + SpaEnvironment.settingsEntryRepository, + browseActivityClass = SpaActivity::class.java, +) diff --git a/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt new file mode 100644 index 00000000000..e1ee766603f --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt @@ -0,0 +1,78 @@ +/* + * 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.app.ActivityManager +import android.app.admin.DevicePolicyManager +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 + +class AppButtonRepository(private val context: Context) { + private val packageManager = context.packageManager + private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + + /** + * Checks whether the given application is disallowed from modifying. + */ + fun isDisallowControl(app: ApplicationInfo): Boolean = when { + // Not allow to control the device provisioning package. + Utils.isDeviceProvisioningPackage(context.resources, app.packageName) -> true + + // 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 + } + + /** + * 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() + val homeActivities = ArrayList() + val currentDefaultHome = packageManager.getHomeActivities(homeActivities) + homeActivities.map { it.activityInfo }.forEach { + homePackages.add(it.packageName) + // Also make sure to include anything proxying for the home app + val metaPackageName = it.metaData?.getString(ActivityManager.META_HOME_ALTERNATE) + if (metaPackageName != null && signaturesMatch(metaPackageName, it.packageName)) { + homePackages.add(metaPackageName) + } + } + return AppUninstallButton.HomePackages(homePackages, currentDefaultHome) + } + + private fun signaturesMatch(packageName1: String, packageName2: String): Boolean = try { + packageManager.checkSignatures(packageName1, packageName2) >= PackageManager.SIGNATURE_MATCH + } catch (e: Exception) { + // e.g. named alternate package not found during lookup; this is an expected case sometimes + false + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppButtons.kt b/src/com/android/settings/spa/app/appsettings/AppButtons.kt new file mode 100644 index 00000000000..89f3b138df1 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppButtons.kt @@ -0,0 +1,59 @@ +/* + * 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.pm.PackageInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.button.ActionButtons +import kotlinx.coroutines.flow.map + +@Composable +fun AppButtons(packageInfoPresenter: PackageInfoPresenter) { + val appButtonsHolder = remember { AppButtonsHolder(packageInfoPresenter) } + appButtonsHolder.Dialogs() + ActionButtons(actionButtons = appButtonsHolder.rememberActionsButtons().value) +} + +private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPresenter) { + private val appLaunchButton = AppLaunchButton(context = packageInfoPresenter.context) + private val appDisableButton = AppDisableButton(packageInfoPresenter) + private val appUninstallButton = AppUninstallButton(packageInfoPresenter) + private val appForceStopButton = AppForceStopButton(packageInfoPresenter) + + @Composable + fun rememberActionsButtons() = remember { + packageInfoPresenter.flow.map { packageInfo -> + if (packageInfo != null) getActionButtons(packageInfo) else emptyList() + } + }.collectAsState(initial = emptyList()) + + private fun getActionButtons(packageInfo: PackageInfo): List = listOfNotNull( + appLaunchButton.getActionButton(packageInfo), + appDisableButton.getActionButton(packageInfo), + appUninstallButton.getActionButton(packageInfo), + appForceStopButton.getActionButton(packageInfo), + ) + + @Composable + fun Dialogs() { + appDisableButton.DisableConfirmDialog() + appForceStopButton.ForceStopConfirmDialog() + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt new file mode 100644 index 00000000000..6f5e671f8a8 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt @@ -0,0 +1,139 @@ +/* + * 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.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 +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +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.model.app.isDisabledUntilUsed + +class AppDisableButton( + private val packageInfoPresenter: PackageInfoPresenter, +) { + private val context = packageInfoPresenter.context + 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 applicationFeatureProvider = + FeatureFactory.getFactory(context).getApplicationFeatureProvider(context) + + private var openConfirmDialog by mutableStateOf(false) + + fun getActionButton(packageInfo: PackageInfo): ActionButton? { + val app = packageInfo.applicationInfo + if (!app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) return null + + return when { + app.enabled && !app.isDisabledUntilUsed() -> { + disableButton(enabled = isDisableButtonEnabled(packageInfo)) + } + + else -> enableButton() + } + } + + /** + * Gets whether a package can be disabled. + */ + private fun isDisableButtonEnabled(packageInfo: PackageInfo): Boolean { + val packageName = packageInfo.packageName + val app = packageInfo.applicationInfo + return when { + packageName in applicationFeatureProvider.keepEnabledPackages -> false + + // Home launcher apps need special handling. In system ones we don't risk downgrading + // because that can interfere with home-key resolution. + packageName in appButtonRepository.getHomePackageInfo().homePackages -> false + + // Try to prevent the user from bricking their phone by not allowing disabling of apps + // signed with the system certificate. + SettingsLibUtils.isSystemPackage(resources, packageManager, packageInfo) -> false + + // If this is a device admin, it can't be disabled. + appButtonRepository.isActiveAdmin(app) -> 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" + // will clear data on all users. + Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, packageName) -> false + + appButtonRepository.isDisallowControl(app) -> false + + // system/vendor resource overlays can never be disabled. + app.isResourceOverlay -> false + + else -> true + } + } + + private fun disableButton(enabled: Boolean) = ActionButton( + text = context.getString(R.string.disable_text), + imageVector = Icons.Outlined.HideSource, + enabled = enabled, + ) { openConfirmDialog = true } + + private fun enableButton() = ActionButton( + text = context.getString(R.string.enable_text), + imageVector = Icons.Outlined.ArrowCircleDown, + ) { packageInfoPresenter.enable() } + + @Composable + fun DisableConfirmDialog() { + if (!openConfirmDialog) return + AlertDialog( + onDismissRequest = { openConfirmDialog = false }, + confirmButton = { + TextButton( + onClick = { + openConfirmDialog = false + packageInfoPresenter.disable() + }, + ) { + Text(stringResource(R.string.app_disable_dlg_positive)) + } + }, + dismissButton = { + TextButton(onClick = { openConfirmDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + text = { + Text(stringResource(R.string.app_disable_dlg_text)) + }, + ) + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt new file mode 100644 index 00000000000..49191637677 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt @@ -0,0 +1,119 @@ +/* + * 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.app.settings.SettingsEnums +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.WarningAmber +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settingslib.RestrictedLockUtils +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.userId + +class AppForceStopButton( + private val packageInfoPresenter: PackageInfoPresenter, +) { + private val context = packageInfoPresenter.context + private val appButtonRepository = AppButtonRepository(context) + private val packageManager = context.packageManager + + private var openConfirmDialog by mutableStateOf(false) + + fun getActionButton(packageInfo: PackageInfo): ActionButton { + val app = packageInfo.applicationInfo + return ActionButton( + text = context.getString(R.string.force_stop), + imageVector = Icons.Outlined.WarningAmber, + enabled = isForceStopButtonEnable(app), + ) { onForceStopButtonClicked(app) } + } + + /** + * Gets whether a package can be force stopped. + */ + private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when { + // User can't force stop device admin. + appButtonRepository.isActiveAdmin(app) -> false + + appButtonRepository.isDisallowControl(app) -> false + + // If the app isn't explicitly stopped, then always show the force stop button. + else -> !app.hasFlag(ApplicationInfo.FLAG_STOPPED) + } + + private fun onForceStopButtonClicked(app: ApplicationInfo) { + packageInfoPresenter.logAction(SettingsEnums.ACTION_APP_INFO_FORCE_STOP) + getAdminRestriction(app)?.let { admin -> + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin) + return + } + openConfirmDialog = true + } + + private fun getAdminRestriction(app: ApplicationInfo): EnforcedAdmin? = when { + packageManager.isPackageStateProtected(app.packageName, app.userId) -> { + RestrictedLockUtilsInternal.getDeviceOwner(context) + } + + else -> RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, UserManager.DISALLOW_APPS_CONTROL, app.userId + ) + } + + @Composable + fun ForceStopConfirmDialog() { + if (!openConfirmDialog) return + AlertDialog( + onDismissRequest = { openConfirmDialog = false }, + confirmButton = { + TextButton( + onClick = { + openConfirmDialog = false + packageInfoPresenter.forceStop() + }, + ) { + Text(stringResource(R.string.okay)) + } + }, + dismissButton = { + TextButton(onClick = { openConfirmDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + title = { + Text(stringResource(R.string.force_stop_dlg_title)) + }, + text = { + Text(stringResource(R.string.force_stop_dlg_text)) + }, + ) + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt b/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt new file mode 100644 index 00000000000..a983b675938 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt @@ -0,0 +1,43 @@ +/* + * 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.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Launch +import com.android.settings.R +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spaprivileged.model.app.userHandle + +class AppLaunchButton(private val context: Context) { + private val packageManager = context.packageManager + + fun getActionButton(packageInfo: PackageInfo): ActionButton? = + packageManager.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent -> + launchButton(intent, packageInfo.applicationInfo) + } + + private fun launchButton(intent: Intent, app: ApplicationInfo) = ActionButton( + text = context.getString(R.string.launch_instant_app), + imageVector = Icons.Outlined.Launch, + ) { + context.startActivityAsUser(intent, app.userHandle) + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppSettings.kt b/src/com/android/settings/spa/app/appsettings/AppSettings.kt index d0a230f1ddd..4c960e5a17a 100644 --- a/src/com/android/settings/spa/app/appsettings/AppSettings.kt +++ b/src/com/android/settings/spa/app/appsettings/AppSettings.kt @@ -17,10 +17,12 @@ package com.android.settings.spa.app.appsettings import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.navigation.NavType import androidx.navigation.navArgument @@ -34,7 +36,6 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.Category -import com.android.settingslib.spaprivileged.model.app.PackageManagers import com.android.settingslib.spaprivileged.model.app.toRoute import com.android.settingslib.spaprivileged.template.app.AppInfoProvider @@ -53,22 +54,36 @@ object AppSettingsProvider : SettingsPageProvider { override fun Page(arguments: Bundle?) { val packageName = arguments!!.getString(PACKAGE_NAME)!! val userId = arguments.getInt(USER_ID) - remember { PackageManagers.getPackageInfoAsUser(packageName, userId) }?.let { - AppSettings(it) + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val packageInfoPresenter = remember { + PackageInfoPresenter(context, packageName, userId, coroutineScope) } + AppSettings(packageInfoPresenter) + packageInfoPresenter.PageCloser() } @Composable fun navigator(app: ApplicationInfo) = navigator(route = "$name/${app.toRoute()}") + + /** + * Gets the route to the App Settings page. + * + * Expose route to enable enter from non-SPA pages. + */ + fun getRoute(packageName: String, userId: Int): String = "$name/$packageName/$userId" } @Composable -private fun AppSettings(packageInfo: PackageInfo) { +private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) { + val packageInfo = packageInfoPresenter.flow.collectAsState().value ?: return RegularScaffold(title = stringResource(R.string.application_info_label)) { val appInfoProvider = remember { AppInfoProvider(packageInfo) } appInfoProvider.AppInfo() + AppButtons(packageInfoPresenter) + Category(title = stringResource(R.string.advanced_apps)) { val app = packageInfo.applicationInfo DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) diff --git a/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt new file mode 100644 index 00000000000..53b36b99d11 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt @@ -0,0 +1,134 @@ +/* + * 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.app.admin.DevicePolicyManager +import android.app.settings.SettingsEnums +import android.content.ComponentName +import android.content.Intent +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 +import com.android.settings.Utils +import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd +import com.android.settingslib.RestrictedLockUtils +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.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)!! + + fun getActionButton(packageInfo: PackageInfo): ActionButton? { + val app = packageInfo.applicationInfo + if (app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) return null + return uninstallButton(app = app, enabled = isUninstallButtonEnabled(app)) + } + + /** 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 + + // Not allow to uninstall DO/PO. + Utils.isProfileOrDeviceOwner(devicePolicyManager, app.packageName, app.userId) -> false + + appButtonRepository.isDisallowControl(app) -> false + + uninstallDisallowedDueToHomeApp(app.packageName) -> false + + // Resource overlays can be uninstalled iff they are public (installed on /data) and + // disabled. ("Enabled" means they are in use by resource management.) + app.isEnabledResourceOverlay() -> false + + else -> true + } + + /** + * Checks whether the given package cannot be uninstalled due to home app restrictions. + * + * Home launcher apps need special handling, we can't allow uninstallation of the only home + * app, and we don't want to allow uninstallation of an explicitly preferred one -- the user + * can go to Home settings and pick a different one, after which we'll permit uninstallation + * of the now-not-default one. + */ + private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean { + val homePackageInfo = appButtonRepository.getHomePackageInfo() + return when { + packageName !in homePackageInfo.homePackages -> false + + // Disallow uninstall when this is the only home app. + homePackageInfo.homePackages.size == 1 -> true + + // Disallow if this is the explicit default home app. + else -> packageName == homePackageInfo.currentDefaultHome?.packageName + } + } + + private fun ApplicationInfo.isEnabledResourceOverlay(): Boolean = + isResourceOverlay && + overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true + + private fun uninstallButton(app: ApplicationInfo, enabled: Boolean) = ActionButton( + text = context.getString(R.string.uninstall_text), + imageVector = Icons.Outlined.Delete, + enabled = enabled, + ) { onUninstallClicked(app) } + + private fun onUninstallClicked(app: ApplicationInfo) { + if (appButtonRepository.isActiveAdmin(app)) { + 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) + } + context.startActivityAsUser(intent, app.userHandle) + return + } + RestrictedLockUtilsInternal.checkIfUninstallBlocked( + context, app.packageName, app.userId + )?.let { admin -> + 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) + } +} diff --git a/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt new file mode 100644 index 00000000000..a00f83c9002 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt @@ -0,0 +1,129 @@ +/* + * 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.app.ActivityManager +import android.app.settings.SettingsEnums +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.UserHandle +import android.util.Log +import androidx.compose.runtime.Composable +import com.android.settings.overlay.FeatureFactory +import com.android.settingslib.spa.framework.compose.DisposableBroadcastReceiverAsUser +import com.android.settingslib.spa.framework.compose.LocalNavController +import com.android.settingslib.spaprivileged.model.app.PackageManagers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +private const val TAG = "PackageInfoPresenter" + +/** + * Presenter which helps to present the status change of [PackageInfo]. + */ +class PackageInfoPresenter( + val context: Context, + val packageName: String, + val userId: Int, + private val coroutineScope: CoroutineScope, +) { + private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider + private val packageManager by lazy { + context.createContextAsUser(UserHandle.of(userId), 0).packageManager + } + private val _flow: MutableStateFlow = MutableStateFlow(null) + + val flow: StateFlow = _flow + + init { + notifyChange() + } + + private fun notifyChange() { + coroutineScope.launch(Dispatchers.IO) { + _flow.value = getPackageInfo() + } + } + + /** + * Closes the page when the package is uninstalled. + */ + @Composable + fun PageCloser() { + 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() + } + } + } + + /** Enables this package. */ + fun enable() { + logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP) + coroutineScope.launch(Dispatchers.IO) { + packageManager.setApplicationEnabledSetting( + packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0 + ) + notifyChange() + } + } + + /** Disables this package. */ + fun disable() { + logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) + coroutineScope.launch(Dispatchers.IO) { + packageManager.setApplicationEnabledSetting( + packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 + ) + notifyChange() + } + } + + /** Force stops this package. */ + fun forceStop() { + logAction(SettingsEnums.ACTION_APP_FORCE_STOP) + coroutineScope.launch(Dispatchers.Default) { + val activityManager = context.getSystemService(ActivityManager::class.java)!! + Log.d(TAG, "Stopping package $packageName") + activityManager.forceStopPackageAsUser(packageName, userId) + notifyChange() + } + } + + fun logAction(category: Int) { + metricsFeatureProvider.action(context, category, packageName) + } + + private fun getPackageInfo() = + PackageManagers.getPackageInfoAsUser( + packageName = packageName, + flags = PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.GET_SIGNATURES or + PackageManager.GET_PERMISSIONS, + userId = userId, + ) +} diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 2571426be1e..942d5caf91d 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -1196,11 +1196,19 @@ public class UserSettings extends SettingsPreferenceFragment if (context == null) { return; } - final List users = mUserManager.getAliveUsers() - // Only users that can be switched to should show up here. - // e.g. Managed profiles appear under Accounts Settings instead - .stream().filter(UserInfo::supportsSwitchToByUser) - .collect(Collectors.toList()); + + List users; + if (mUserCaps.mUserSwitcherEnabled) { + // Only users that can be switched to should show up here. + // e.g. Managed profiles appear under Accounts Settings instead + users = mUserManager.getAliveUsers().stream() + .filter(UserInfo::supportsSwitchToByUser) + .collect(Collectors.toList()); + } else { + // Only current user will be displayed in case of multi-user switch is disabled + users = List.of(mUserManager.getUserInfo(context.getUserId())); + } + final ArrayList missingIcons = new ArrayList<>(); final ArrayList userPreferences = new ArrayList<>(); @@ -1276,7 +1284,6 @@ public class UserSettings extends SettingsPreferenceFragment userPreferences.add(pref); } - // Sort list of users by serialNum Collections.sort(userPreferences, UserPreference.SERIAL_NUMBER_COMPARATOR); @@ -1299,7 +1306,6 @@ public class UserSettings extends SettingsPreferenceFragment // Remove everything from mUserListCategory and add new users. mUserListCategory.removeAll(); - // If multi-user is disabled, just show top info and return. final Preference addUserOnLockScreen = getPreferenceScreen().findPreference( mAddUserWhenLockedPreferenceController.getPreferenceKey()); mAddUserWhenLockedPreferenceController.updateState(addUserOnLockScreen); @@ -1311,16 +1317,11 @@ public class UserSettings extends SettingsPreferenceFragment final Preference multiUserTopIntroPreference = getPreferenceScreen().findPreference( mMultiUserTopIntroPreferenceController.getPreferenceKey()); mMultiUserTopIntroPreferenceController.updateState(multiUserTopIntroPreference); - mUserListCategory.setVisible(mUserCaps.mUserSwitcherEnabled); updateGuestPreferences(); updateGuestCategory(context, users); updateAddUser(context); updateAddSupervisedUser(context); - if (!mUserCaps.mUserSwitcherEnabled) { - return; - } - for (UserPreference userPreference : userPreferences) { userPreference.setOrder(Preference.DEFAULT_ORDER); mUserListCategory.addPreference(userPreference); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java index c4832041314..df9d8655109 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -579,9 +579,13 @@ public class DataProcessorTest { ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 25.0, /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 3); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, + 3); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, + 0); } @Test @@ -674,9 +678,13 @@ public class DataProcessorTest { assertThat(resultMap.get(0).get(0)).isNotNull(); assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull(); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 2); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, + 2); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, + 0); } @Test @@ -739,9 +747,13 @@ public class DataProcessorTest { assertThat(resultMap.get(0).get(0)).isNotNull(); assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull(); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 1); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, + 1); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, + 0); } @Test @@ -814,9 +826,13 @@ public class DataProcessorTest { ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0, /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 20); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 1); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, + 1); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 1); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, + 1); } @Test @@ -889,9 +905,13 @@ public class DataProcessorTest { resultEntry = resultDiffData.getAppDiffEntryList().get(1); assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(0); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 2); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, + 2); verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0); + .action(mContext.getApplicationContext(), + SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, + 0); } @Test diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 1f8c0b40809..62a8a75a621 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -177,6 +177,7 @@ public class UserSettingsTest { doReturn(prefs).when(mMockPreferenceManager).getSharedPreferences(); doReturn(mContext).when(mMockPreferenceManager).getContext(); doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); + doReturn(ACTIVE_USER_ID).when(mContext).getUserId(); mFragment.mMePreference = mMePreference; mFragment.mAddUser = mAddUserPreference; @@ -443,6 +444,7 @@ public class UserSettingsTest { @Test public void updateUserList_userSwitcherDisabled_shouldNotShowAddUser() { + givenUsers(getAdminUser(true)); mUserCapabilities.mCanAddUser = true; mUserCapabilities.mUserSwitcherEnabled = false; @@ -453,6 +455,7 @@ public class UserSettingsTest { @Test public void updateUserList_userSwitcherDisabled_shouldNotShowAddGuest() { + givenUsers(getAdminUser(true)); mUserCapabilities.mCanAddGuest = true; mUserCapabilities.mUserSwitcherEnabled = false; @@ -511,6 +514,47 @@ public class UserSettingsTest { assertThat(userPref.getOnPreferenceClickListener()).isSameInstanceAs(mFragment); } + @Test + public void updateUserList_existingSecondaryUser_shouldAddOnlyCurrUser_MultiUserIsDisabled() { + givenUsers(getAdminUser(true), getSecondaryUser(false)); + mUserCapabilities.mUserSwitcherEnabled = false; + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(1)) + .addPreference(captor.capture()); + + List userPrefs = captor.getAllValues(); + assertThat(userPrefs.size()).isEqualTo(1); + assertThat(userPrefs.get(0)).isSameInstanceAs(mMePreference); + } + + @Test + public void updateUserList_existingSecondaryUser_shouldAddSecondaryUser_MultiUserIsEnabled() { + givenUsers(getAdminUser(true), getSecondaryUser(false)); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + + List userPrefs = captor.getAllValues(); + UserPreference adminPref = userPrefs.get(0); + UserPreference secondaryPref = userPrefs.get(1); + + assertThat(userPrefs.size()).isEqualTo(2); + assertThat(adminPref).isSameInstanceAs(mMePreference); + assertThat(secondaryPref.getUserId()).isEqualTo(INACTIVE_SECONDARY_USER_ID); + assertThat(secondaryPref.getTitle()).isEqualTo(SECONDARY_USER_NAME); + assertThat(secondaryPref.getIcon()).isNotNull(); + assertThat(secondaryPref.getKey()).isEqualTo("id=" + INACTIVE_SECONDARY_USER_ID); + assertThat(secondaryPref.isEnabled()).isEqualTo(true); + assertThat(secondaryPref.isSelectable()).isEqualTo(true); + assertThat(secondaryPref.getOnPreferenceClickListener()).isSameInstanceAs(mFragment); + } + @Test public void updateUserList_existingRestrictedUser_shouldAddRestrictedUserPreference() { givenUsers(getAdminUser(true), getRestrictedUser(false)); @@ -800,6 +844,8 @@ public class UserSettingsTest { List users = Arrays.asList(userInfo); doReturn(users).when(mUserManager).getUsers(); doReturn(users).when(mUserManager).getAliveUsers(); + users.forEach(user -> + doReturn(user).when(mUserManager).getUserInfo(user.id)); } private static void removeFlag(UserInfo userInfo, int flag) {