Merge "Add 'Archive' button to AppInfo screen" into main
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b76555b..9de79d4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -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>
diff --git a/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
new file mode 100644
index 0000000..913da65
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
@@ -0,0 +1,130 @@
+/*
+ * 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()
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt b/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
index 2383ddb..f01c31c 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
@@ -19,6 +19,7 @@
 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.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 @@
         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?,
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
index 307ff11..f6fafd7 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
@@ -30,7 +30,10 @@
 /**
  * @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 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 @@
 
     @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),
diff --git a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
index 5f6f097..ce72840 100644
--- a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
@@ -18,7 +18,6 @@
 
 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.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 @@
 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 @@
         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 @@
         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) }
diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
index a6bd8f0..8c802d1 100644
--- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
+++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
@@ -87,19 +87,9 @@
     ).filter(::isInterestedAppChange).filter(::isForThisApp)
 
     @VisibleForTesting
-    fun isInterestedAppChange(intent: Intent) = when {
-        intent.action != Intent.ACTION_PACKAGE_REMOVED -> true
-
-        // filter out the fully removed case, in which the page will be closed, so no need to
-        // refresh
-        intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) -> false
-
-        // filter out the updates are uninstalled (system app), which will followed by a replacing
-        // broadcast, we can refresh at that time
-        intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) -> false
-
-        else -> true // App archived
-    }
+    fun isInterestedAppChange(intent: Intent) =
+        intent.action != Intent.ACTION_PACKAGE_REMOVED ||
+            intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false)
 
     val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
         .map { getPackageInfo() }
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
new file mode 100644
index 0000000..cc5e365
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudUpload
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class AppArchiveButtonTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
+
+    private val packageInfoPresenter = mock<PackageInfoPresenter>()
+
+    private val userPackageManager = mock<PackageManager>()
+
+    private val packageInstaller = mock<PackageInstaller>()
+
+    private lateinit var appArchiveButton: AppArchiveButton
+
+    @Before
+    fun setUp() {
+        whenever(packageInfoPresenter.context).thenReturn(context)
+        whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
+        whenever(userPackageManager.packageInstaller).thenReturn(packageInstaller)
+        whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
+        appArchiveButton = AppArchiveButton(packageInfoPresenter)
+    }
+
+    @Test
+    fun appArchiveButton_whenIsArchived_isDisabled() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = true
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+        val actionButton = setContent(app)
+
+        assertThat(actionButton.enabled).isFalse()
+    }
+
+    @Test
+    fun appArchiveButton_whenIsNotAppArchivable_isDisabled() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = false
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(false)
+
+        val actionButton = setContent(app)
+
+        assertThat(actionButton.enabled).isFalse()
+    }
+
+    @Test
+    fun appArchiveButton_displaysRightTextAndIcon() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = false
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+        val actionButton = setContent(app)
+
+        assertThat(actionButton.text).isEqualTo(context.getString(R.string.archive))
+        assertThat(actionButton.imageVector).isEqualTo(Icons.Outlined.CloudUpload)
+    }
+
+    @Test
+    fun appArchiveButton_clicked() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = false
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+        val actionButton = setContent(app)
+        actionButton.onClick()
+
+        verify(packageInstaller).requestArchive(
+            eq(PACKAGE_NAME),
+            any(),
+            eq(0)
+        )
+    }
+
+    private fun setContent(app: ApplicationInfo): ActionButton {
+        lateinit var actionButton: ActionButton
+        composeTestRule.setContent {
+            actionButton = appArchiveButton.getActionButton(app)
+        }
+        return actionButton
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "package.name"
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
index e2f55ef..50094f2 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
@@ -22,8 +22,10 @@
 import android.content.pm.FakeFeatureFlagsImpl
 import android.content.pm.Flags
 import android.content.pm.PackageInfo
+import android.content.pm.PackageInstaller
 import android.content.pm.PackageManager
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
@@ -62,6 +64,9 @@
     @Mock
     private lateinit var packageManager: PackageManager
 
+    @Mock
+    private lateinit var packageInstaller: PackageInstaller
+
     private val featureFlags = FakeFeatureFlagsImpl()
 
     @Before
@@ -74,6 +79,7 @@
         whenever(packageInfoPresenter.context).thenReturn(context)
         whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
         whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
+        whenever(packageManager.packageInstaller).thenReturn(packageInstaller)
         whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
         whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
         featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
@@ -118,8 +124,24 @@
         composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
     }
 
-    private fun setContent() {
-        whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO))
+    @Test
+    fun uninstallButton_enabled_whenAppIsArchived() {
+        whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+        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.uninstall_text)).assertIsEnabled()
+    }
+
+    private fun setContent(packageInfo: PackageInfo = PACKAGE_INFO) {
+        whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(packageInfo))
         composeTestRule.setContent {
             AppButtons(packageInfoPresenter, featureFlags)
         }
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
index d81bb1a..5dd66e8 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
@@ -105,6 +105,7 @@
     fun isInterestedAppChange_archived_interested() {
         val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
             data = Uri.parse("package:$PACKAGE_NAME")
+            putExtra(Intent.EXTRA_ARCHIVAL, true)
         }
 
         val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)