Add App Buttons to the App Settings page

Including the following,
- Launch
- Disable
- Uninstall
- Force stop

Bug: 236346018
Test: Manual with Settings App
Change-Id: Iecfc2b97cdda4ff0ba5080b4287cc4542ffc57ad
This commit is contained in:
Chaohui Wang
2022-09-26 11:01:05 +08:00
parent 2b38f742e1
commit 7fc0b3a70b
8 changed files with 714 additions and 5 deletions

View File

@@ -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<String>()
val homeActivities = ArrayList<ResolveInfo>()
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
}
}

View File

@@ -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<ActionButton> = listOfNotNull(
appLaunchButton.getActionButton(packageInfo),
appDisableButton.getActionButton(packageInfo),
appUninstallButton.getActionButton(packageInfo),
appForceStopButton.getActionButton(packageInfo),
)
@Composable
fun Dialogs() {
appDisableButton.DisableConfirmDialog()
appForceStopButton.ForceStopConfirmDialog()
}
}

View File

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

View File

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

View File

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

View File

@@ -17,10 +17,12 @@
package com.android.settings.spa.app.appsettings package com.android.settings.spa.app.appsettings
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.navArgument 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.framework.compose.navigator
import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category 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.model.app.toRoute
import com.android.settingslib.spaprivileged.template.app.AppInfoProvider import com.android.settingslib.spaprivileged.template.app.AppInfoProvider
@@ -53,9 +54,13 @@ object AppSettingsProvider : SettingsPageProvider {
override fun Page(arguments: Bundle?) { override fun Page(arguments: Bundle?) {
val packageName = arguments!!.getString(PACKAGE_NAME)!! val packageName = arguments!!.getString(PACKAGE_NAME)!!
val userId = arguments.getInt(USER_ID) val userId = arguments.getInt(USER_ID)
remember { PackageManagers.getPackageInfoAsUser(packageName, userId) }?.let { val context = LocalContext.current
AppSettings(it) val coroutineScope = rememberCoroutineScope()
val packageInfoPresenter = remember {
PackageInfoPresenter(context, packageName, userId, coroutineScope)
} }
AppSettings(packageInfoPresenter)
packageInfoPresenter.PageCloser()
} }
@Composable @Composable
@@ -63,12 +68,15 @@ object AppSettingsProvider : SettingsPageProvider {
} }
@Composable @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)) { RegularScaffold(title = stringResource(R.string.application_info_label)) {
val appInfoProvider = remember { AppInfoProvider(packageInfo) } val appInfoProvider = remember { AppInfoProvider(packageInfo) }
appInfoProvider.AppInfo() appInfoProvider.AppInfo()
AppButtons(packageInfoPresenter)
Category(title = stringResource(R.string.advanced_apps)) { Category(title = stringResource(R.string.advanced_apps)) {
val app = packageInfo.applicationInfo val app = packageInfo.applicationInfo
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)

View File

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

@@ -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<PackageInfo?> = MutableStateFlow(null)
val flow: StateFlow<PackageInfo?> = _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,
)
}