From 63f48ad2c6691d2e38075c785bbb53bf58160984 Mon Sep 17 00:00:00 2001 From: Mark Kim Date: Mon, 30 Oct 2023 20:56:44 +0000 Subject: [PATCH] Add 'Archive' button to AppInfo screen Disable 'Archive' button whenever 'Uninstall' button is disabled. Test: AppArchiveButtonTest, AppButtonsTest Bug: 304256700 Change-Id: I9671905eca2cb71a5bf30bf29be83e5305a48ef4 --- res/values/strings.xml | 7 + .../spa/app/appinfo/AppArchiveButton.kt | 130 +++++++++++++++++ .../spa/app/appinfo/AppButtonRepository.kt | 52 +++++++ .../settings/spa/app/appinfo/AppButtons.kt | 12 +- .../spa/app/appinfo/AppUninstallButton.kt | 50 +------ .../spa/app/appinfo/PackageInfoPresenter.kt | 16 +-- .../spa/app/appinfo/AppArchiveButtonTest.kt | 135 ++++++++++++++++++ .../spa/app/appinfo/AppButtonsTest.kt | 26 +++- .../app/appinfo/PackageInfoPresenterTest.kt | 1 + 9 files changed, 363 insertions(+), 66 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index 5795aeaeffa..20d96b5859a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3896,6 +3896,8 @@ Controls Force stop + + Archive Total @@ -4006,6 +4008,11 @@ Move + + Archiving failed + + Archived %1$s + Another migration is already in progress. 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 00000000000..913da65a03a --- /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 2383ddb2a87..f01c31c01a6 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 @@ 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, 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 307ff11b5ff..f6fafd7f0bf 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 @@ 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 = 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 5f6f097116a..ce7284088f2 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 @@ 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) } diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt index a6bd8f0e64b..8c802d1d0ea 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 @@ class PackageInfoPresenter( ).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 = 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 00000000000..cc5e365ac3a --- /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() + + private val userPackageManager = mock() + + private val packageInstaller = mock() + + 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 e2f55ef8979..50094f25f46 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.ApplicationInfo 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 @@ class AppButtonsTest { @Mock private lateinit var packageManager: PackageManager + @Mock + private lateinit var packageInstaller: PackageInstaller + private val featureFlags = FakeFeatureFlagsImpl() @Before @@ -74,6 +79,7 @@ class AppButtonsTest { 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 @@ class AppButtonsTest { 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 d81bb1a0bea..5dd66e8fff5 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 @@ class PackageInfoPresenterTest { 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)