/* * 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.appinfo import android.app.AppOpsManager import android.app.ecm.EnhancedConfirmationManager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.UserManager import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue 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.settings.applications.appinfo.AppInfoDashboardFragment import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.scaffold.RestrictedMenuItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext @Composable fun AppInfoSettingsMoreOptions( packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo, packageManagers: IPackageManagers = PackageManagers, ) { val state = app.produceState(packageManagers).value ?: return var restrictedSettingsAllowed by rememberSaveable { mutableStateOf(false) } if (!state.shownUninstallUpdates && !state.shownUninstallForAllUsers && !(state.shouldShowAccessRestrictedSettings && !restrictedSettingsAllowed) ) return MoreOptionsAction { val restrictions = Restrictions(userId = app.userId, keys = listOf(UserManager.DISALLOW_APPS_CONTROL)) if (state.shownUninstallUpdates) { RestrictedMenuItem( text = stringResource(R.string.app_factory_reset), restrictions = restrictions, ) { packageInfoPresenter.startUninstallActivity(forAllUsers = false) } } if (state.shownUninstallForAllUsers) { RestrictedMenuItem( text = stringResource(R.string.uninstall_all_users_text), restrictions = restrictions, ) { packageInfoPresenter.startUninstallActivity(forAllUsers = true) } } if (state.shouldShowAccessRestrictedSettings && !restrictedSettingsAllowed) { MenuItem(text = stringResource(R.string.app_restricted_settings_lockscreen_title)) { app.allowRestrictedSettings(packageInfoPresenter.context) { restrictedSettingsAllowed = true } } } } } private fun ApplicationInfo.allowRestrictedSettings(context: Context, onSuccess: () -> Unit) { AppInfoDashboardFragment.showLockScreen(context) { if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled() && android.security.Flags.extendEcmToAllSettings()) { val manager = context.getSystemService(EnhancedConfirmationManager::class.java)!! manager.clearRestriction(packageName) } else { context.appOpsManager.setMode( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, AppOpsManager.MODE_ALLOWED, ) } onSuccess() val toastString = context.getString( R.string.toast_allows_restricted_settings_successfully, loadLabel(context.packageManager), ) Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() } } private data class AppInfoSettingsMoreOptionsState( val shownUninstallUpdates: Boolean, val shownUninstallForAllUsers: Boolean, val shouldShowAccessRestrictedSettings: Boolean, ) @Composable private fun ApplicationInfo.produceState( packageManagers: IPackageManagers, ): State { val context = LocalContext.current return produceState(initialValue = null, this) { withContext(Dispatchers.IO) { value = getMoreOptionsState(context, packageManagers) } } } private suspend fun ApplicationInfo.getMoreOptionsState( context: Context, packageManagers: IPackageManagers, ) = coroutineScope { val shownUninstallUpdatesDeferred = async { isShowUninstallUpdates(context) } val shownUninstallForAllUsersDeferred = async { isShowUninstallForAllUsers( userManager = context.userManager, packageManagers = packageManagers, ) } val shouldShowAccessRestrictedSettingsDeferred = async { shouldShowAccessRestrictedSettings(context) } val isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner(context.userManager, context.devicePolicyManager, packageName) AppInfoSettingsMoreOptionsState( // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. shownUninstallUpdates = !isProfileOrDeviceOwner && shownUninstallUpdatesDeferred.await(), // We also don't allow uninstalling for all users if it's DO/PO for any user. shownUninstallForAllUsers = !isProfileOrDeviceOwner && shownUninstallForAllUsersDeferred.await(), shouldShowAccessRestrictedSettings = shouldShowAccessRestrictedSettingsDeferred.await(), ) } private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean = isUpdatedSystemApp && context.userManager.isUserAdmin(userId) && !context.resources.getBoolean(R.bool.config_disable_uninstall_update) private fun ApplicationInfo.isShowUninstallForAllUsers( userManager: UserManager, packageManagers: IPackageManagers, ): Boolean = userId == 0 && !isSystemApp && !isInstantApp && isOtherUserHasInstallPackage(userManager, packageManagers) private fun ApplicationInfo.isOtherUserHasInstallPackage( userManager: UserManager, packageManagers: IPackageManagers, ): Boolean = userManager.aliveUsers .filter { it.id != userId } .filter { !Utils.shouldHideUser(it.userHandle, userManager) } .any { packageManagers.isPackageInstalledAsUser(packageName, it.id) } private fun ApplicationInfo.shouldShowAccessRestrictedSettings(context: Context): Boolean { return if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled() && android.security.Flags.extendEcmToAllSettings()) { val manager = context.getSystemService(EnhancedConfirmationManager::class.java)!! try { manager.isClearRestrictionAllowed(packageName) } catch (e: PackageManager.NameNotFoundException) { // Package might have been archived false } } else { context.appOpsManager.noteOpNoThrow( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, null, null ) == AppOpsManager.MODE_IGNORED } }