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

Commit 7144d95e authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "Add App Buttons to the App Settings page"

parents 9071feb4 7fc0b3a7
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