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..147112e358e 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,9 +54,13 @@ 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 @@ -63,12 +68,15 @@ object AppSettingsProvider : SettingsPageProvider { } @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, + ) +}