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

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

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

parents 2368e260 e3b456ef
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -138,6 +138,7 @@
    <uses-permission android:name="android.permission.CUSTOMIZE_SYSTEM_UI" />
    <uses-permission android:name="android.permission.REMAP_MODIFIER_KEYS" />
    <uses-permission android:name="android.permission.ACCESS_GPU_SERVICE" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

    <application
            android:name=".SettingsApplication"
+8 −0
Original line number Diff line number Diff line
@@ -3898,6 +3898,8 @@
    <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, text label for button to restore an application. Restoring means installing the archived app. -->
    <string name="restore">Restore</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) -->
@@ -4012,6 +4014,12 @@
    <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>
    <!-- Toast message when restoring an app failed. -->
    <string name="restoring_failed">Restoring failed</string>
    <!-- Toast message when restoring an app succeeded. -->
    <string name="restoring_succeeded">Restored <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
    <!-- Toast message when restoring an app has started. -->
    <string name="restoring_in_progress">Restoring <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] -->
+6 −1
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ private class AppButtonsPresenter(
    private val appClearButton = AppClearButton(packageInfoPresenter)
    private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
    private val appArchiveButton = AppArchiveButton(packageInfoPresenter)
    private val appRestoreButton = AppRestoreButton(packageInfoPresenter)

    @Composable
    fun getActionButtons() =
@@ -63,7 +64,11 @@ private class AppButtonsPresenter(
    @Composable
    private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
        if (featureFlags.archiving()) {
            if (app.isArchived) {
                appRestoreButton.getActionButton(app)
            } else {
                appArchiveButton.getActionButton(app)
            }
        } else {
            appLaunchButton.getActionButton(app)
        },
+135 −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.CloudDownload
import androidx.compose.runtime.Composable
import com.android.settings.R
import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser

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

    private val context = packageInfoPresenter.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.restore),
            imageVector = Icons.Outlined.CloudDownload,
            enabled = app.isArchived
        ) { onRestoreClicked(app) }
    }

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

    private fun onReceive(intent: Intent, app: ApplicationInfo) {
        when (val unarchiveStatus =
            intent.getIntExtra(PackageInstaller.EXTRA_UNARCHIVE_STATUS, Int.MIN_VALUE)) {
            PackageInstaller.STATUS_PENDING_USER_ACTION -> {
                Log.e(
                    LOG_TAG,
                    "Request unarchiving failed for $packageName with code $unarchiveStatus"
                )
                Toast.makeText(
                    context,
                    context.getString(R.string.restoring_failed),
                    Toast.LENGTH_SHORT
                ).show()
            }

            PackageInstaller.STATUS_SUCCESS -> {
                val appLabel = userPackageManager.getApplicationLabel(app)
                Toast.makeText(
                    context,
                    context.getString(R.string.restoring_succeeded, appLabel),
                    Toast.LENGTH_SHORT
                ).show()
            }

            else -> {
                Log.e(
                    LOG_TAG,
                    "Request unarchiving failed for $packageName with code $unarchiveStatus"
                )
                val errorDialogIntent =
                    intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
                if (errorDialogIntent != null) {
                    context.startActivityAsUser(errorDialogIntent, userHandle)
                } else {
                    Toast.makeText(
                        context,
                        context.getString(R.string.restoring_failed),
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
    }
}
+32 −0
Original line number Diff line number Diff line
@@ -140,6 +140,38 @@ class AppButtonsTest {
        composeTestRule.onNodeWithText(context.getString(R.string.uninstall_text)).assertIsEnabled()
    }

    @Test
    fun archiveButton_displayed_whenAppIsNotArchived() {
        featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
        val packageInfo = PackageInfo().apply {
            applicationInfo = ApplicationInfo().apply {
                packageName = PACKAGE_NAME
                isArchived = false
            }
            packageName = PACKAGE_NAME
        }
        setContent(packageInfo)

        composeTestRule.onNodeWithText(context.getString(R.string.archive)).assertIsDisplayed()
        composeTestRule.onNodeWithText(context.getString(R.string.restore)).assertIsNotDisplayed()
    }

    @Test
    fun restoreButton_displayed_whenAppIsArchived() {
        featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
        val packageInfo = PackageInfo().apply {
            applicationInfo = ApplicationInfo().apply {
                packageName = PACKAGE_NAME
                isArchived = true
            }
            packageName = PACKAGE_NAME
        }
        setContent(packageInfo)

        composeTestRule.onNodeWithText(context.getString(R.string.restore)).assertIsDisplayed()
        composeTestRule.onNodeWithText(context.getString(R.string.archive)).assertIsNotDisplayed()
    }

    private fun setContent(packageInfo: PackageInfo = PACKAGE_INFO) {
        whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(packageInfo))
        composeTestRule.setContent {
Loading