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

Commit d17b1a38 authored by Mark Kim's avatar Mark Kim Committed by Android (Google) Code Review
Browse files

Merge "Add 'Archive' button to AppInfo screen" into main

parents 920037db 63f48ad2
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -3896,6 +3896,8 @@
    <string name="controls_label">Controls</string>
    <!-- Manage applications, text label for button to kill / force stop an application -->
    <string name="force_stop">Force stop</string>
    <!-- Manage applications, text label for button to archive an application. Archiving means uninstalling the app without deleting user's personal data and replacing the app with a stub app with minimum size. So, the user can unarchive the app later and not lose any personal data. -->
    <string name="archive">Archive</string>
    <!-- Manage applications, individual application info screen,label under Storage heading.  The total storage space taken up by this app. -->
    <string name="total_size_label">Total</string>
    <!-- Manage applications, individual application info screen, label under Storage heading. The amount of space taken up by the application itself (for example, the java compield files and things like that) -->
@@ -4006,6 +4008,11 @@
    <!-- Manage applications, text for Move button -->
    <string name="move_app">Move</string>
    <!-- Toast message when archiving an app failed. -->
    <string name="archiving_failed">Archiving failed</string>
    <!-- Toast message when archiving an app succeeded. -->
    <string name="archiving_succeeded">Archived <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
    <!-- Text of pop up message if the request for a "migrate primary storage" operation
         (see storage_menu_migrate) is denied as another is already in progress. [CHAR LIMIT=75] -->
    <string name="another_migration_already_in_progress">Another migration is already in progress.</string>
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller
import android.os.UserHandle
import android.util.Log
import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloudUpload
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn

class AppArchiveButton(packageInfoPresenter: PackageInfoPresenter) {
    private companion object {
        private const val LOG_TAG = "AppArchiveButton"
        private const val INTENT_ACTION = "com.android.settings.archive.action"
    }

    private val context = packageInfoPresenter.context
    private val appButtonRepository = AppButtonRepository(context)
    private val userPackageManager = packageInfoPresenter.userPackageManager
    private val packageInstaller = userPackageManager.packageInstaller
    private val packageName = packageInfoPresenter.packageName
    private val userHandle = UserHandle.of(packageInfoPresenter.userId)
    private var broadcastReceiverIsCreated = false

    @Composable
    fun getActionButton(app: ApplicationInfo): ActionButton {
        if (!broadcastReceiverIsCreated) {
            val intentFilter = IntentFilter(INTENT_ACTION)
            DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
                if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) {
                    onReceive(intent, app)
                }
            }
            broadcastReceiverIsCreated = true
        }
        return ActionButton(
            text = context.getString(R.string.archive),
            imageVector = Icons.Outlined.CloudUpload,
            enabled = remember(app) {
                flow {
                    emit(
                        app.isActionButtonEnabled() && appButtonRepository.isAllowUninstallOrArchive(
                            context,
                            app
                        )
                    )
                }.flowOn(Dispatchers.Default)
            }.collectAsStateWithLifecycle(false).value
        ) { onArchiveClicked(app) }
    }

    private fun ApplicationInfo.isActionButtonEnabled(): Boolean {
        return !isArchived
            && userPackageManager.isAppArchivable(packageName)
            // We apply the same device policy for both the uninstallation and archive
            // button.
            && !appButtonRepository.isUninstallBlockedByAdmin(this)
    }

    private fun onArchiveClicked(app: ApplicationInfo) {
        val intent = Intent(INTENT_ACTION)
        intent.setPackage(context.packageName)
        val pendingIntent = PendingIntent.getBroadcastAsUser(
            context, 0, intent,
            // FLAG_MUTABLE is required by PackageInstaller#requestArchive
            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE,
            userHandle
        )
        try {
            packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender, 0)
        } catch (e: Exception) {
            Log.e(LOG_TAG, "Request archive failed", e)
            Toast.makeText(
                context,
                context.getString(R.string.archiving_failed),
                Toast.LENGTH_SHORT
            ).show()
        }
    }

    private fun onReceive(intent: Intent, app: ApplicationInfo) {
        when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)) {
            PackageInstaller.STATUS_SUCCESS -> {
                val appLabel = userPackageManager.getApplicationLabel(app)
                Toast.makeText(
                    context,
                    context.getString(R.string.archiving_succeeded, appLabel),
                    Toast.LENGTH_SHORT
                ).show()
            }

            else -> {
                Log.e(LOG_TAG, "Request archiving failed for $packageName with code $status")
                Toast.makeText(
                    context,
                    context.getString(R.string.archiving_failed),
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }
}
+52 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.settings.spa.app.appinfo
import android.app.ActivityManager
import android.content.ComponentName
import android.content.Context
import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
@@ -26,7 +27,9 @@ import com.android.settingslib.RestrictedLockUtils
import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.Utils
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isDisallowControl
import com.android.settingslib.spaprivileged.model.app.userHandle
import com.android.settingslib.spaprivileged.model.app.userId

class AppButtonRepository(private val context: Context) {
@@ -77,6 +80,55 @@ class AppButtonRepository(private val context: Context) {
        false
    }

    /** Gets whether a package can be uninstalled or archived. */
    fun isAllowUninstallOrArchive(
        context: Context, app: ApplicationInfo
    ): Boolean {
        val overlayManager = checkNotNull(context.getSystemService(OverlayManager::class.java))
        when {
            !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && !app.isArchived -> return false

            com.android.settings.Utils.isProfileOrDeviceOwner(
                context.devicePolicyManager, app.packageName, app.userId
            ) -> return false

            isDisallowControl(app) -> return false

            uninstallDisallowedDueToHomeApp(app.packageName) -> return 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(overlayManager) -> return false

            else -> return 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 = 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(overlayManager: OverlayManager): Boolean =
        isResourceOverlay &&
            overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true

    data class HomePackages(
        val homePackages: Set<String>,
        val currentDefaultHome: ComponentName?,
+10 −2
Original line number Diff line number Diff line
@@ -30,7 +30,10 @@ import com.android.settingslib.spa.widget.button.ActionButtons
/**
 * @param featureFlags can be overridden in tests
 */
fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) {
fun AppButtons(
    packageInfoPresenter: PackageInfoPresenter,
    featureFlags: FeatureFlags = FeatureFlagsImpl()
) {
    if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
    val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
    ActionButtons(actionButtons = presenter.getActionButtons())
@@ -49,6 +52,7 @@ private class AppButtonsPresenter(
    private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
    private val appClearButton = AppClearButton(packageInfoPresenter)
    private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
    private val appArchiveButton = AppArchiveButton(packageInfoPresenter)

    @Composable
    fun getActionButtons() =
@@ -58,7 +62,11 @@ private class AppButtonsPresenter(

    @Composable
    private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
        if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
        if (featureFlags.archiving()) {
            appArchiveButton.getActionButton(app)
        } else {
            appLaunchButton.getActionButton(app)
        },
        appInstallButton.getActionButton(app),
        appDisableButton.getActionButton(app),
        appUninstallButton.getActionButton(app),
+1 −49
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.settings.spa.app.appinfo

import android.app.settings.SettingsEnums
import android.content.Intent
import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo
import android.os.UserHandle
import android.os.UserManager
@@ -28,11 +27,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.Utils
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userHandle
import kotlinx.coroutines.Dispatchers
@@ -42,7 +38,6 @@ import kotlinx.coroutines.flow.flowOn
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
    private val context = packageInfoPresenter.context
    private val appButtonRepository = AppButtonRepository(context)
    private val overlayManager = context.getSystemService(OverlayManager::class.java)!!
    private val userManager = context.getSystemService(UserManager::class.java)!!

    @Composable
@@ -51,49 +46,6 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
        return uninstallButton(app)
    }

    /** Gets whether a package can be uninstalled. */
    private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when {
        !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) -> false

        Utils.isProfileOrDeviceOwner(
            context.devicePolicyManager, app.packageName, packageInfoPresenter.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

    @Composable
    private fun uninstallButton(app: ApplicationInfo) = ActionButton(
        text = if (isCloneApp(app)) context.getString(R.string.delete) else
@@ -101,7 +53,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
        imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
        enabled = remember(app) {
            flow {
                emit(isUninstallButtonEnabled(app))
                emit(appButtonRepository.isAllowUninstallOrArchive(context, app))
            }.flowOn(Dispatchers.Default)
        }.collectAsStateWithLifecycle(false).value,
    ) { onUninstallClicked(app) }
Loading