Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7fc0b3a7 authored by Chaohui Wang's avatar Chaohui Wang
Browse files

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
parent 2b38f742
Loading
Loading
Loading
Loading
+78 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+59 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}
+139 −0
Original line number Diff line number Diff line
/*
 * 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))
            },
        )
    }
}
+119 −0
Original line number Diff line number Diff line
/*
 * 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))
            },
        )
    }
}
+43 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
Loading