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

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