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

This commit is contained in:
Mark Kim
2023-12-08 10:25:58 +00:00
committed by Android (Google) Code Review
9 changed files with 363 additions and 66 deletions

View File

@@ -3896,6 +3896,8 @@
<string name="controls_label">Controls</string> <string name="controls_label">Controls</string>
<!-- Manage applications, text label for button to kill / force stop an application --> <!-- Manage applications, text label for button to kill / force stop an application -->
<string name="force_stop">Force stop</string> <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. --> <!-- 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> <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) --> <!-- 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 --> <!-- Manage applications, text for Move button -->
<string name="move_app">Move</string> <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 <!-- 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] --> (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> <string name="another_migration_already_in_progress">Another migration is already in progress.</string>

View File

@@ -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()
}
}
}
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.spa.app.appinfo
import android.app.ActivityManager import android.app.ActivityManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
@@ -26,7 +27,9 @@ import com.android.settingslib.RestrictedLockUtils
import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.Utils import com.android.settingslib.Utils
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager 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.isDisallowControl
import com.android.settingslib.spaprivileged.model.app.userHandle
import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.app.userId
class AppButtonRepository(private val context: Context) { class AppButtonRepository(private val context: Context) {
@@ -77,6 +80,55 @@ class AppButtonRepository(private val context: Context) {
false 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( data class HomePackages(
val homePackages: Set<String>, val homePackages: Set<String>,
val currentDefaultHome: ComponentName?, val currentDefaultHome: ComponentName?,

View File

@@ -30,7 +30,10 @@ import com.android.settingslib.spa.widget.button.ActionButtons
/** /**
* @param featureFlags can be overridden in tests * @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 if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) } val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
ActionButtons(actionButtons = presenter.getActionButtons()) ActionButtons(actionButtons = presenter.getActionButtons())
@@ -49,6 +52,7 @@ private class AppButtonsPresenter(
private val appUninstallButton = AppUninstallButton(packageInfoPresenter) private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
private val appClearButton = AppClearButton(packageInfoPresenter) private val appClearButton = AppClearButton(packageInfoPresenter)
private val appForceStopButton = AppForceStopButton(packageInfoPresenter) private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
private val appArchiveButton = AppArchiveButton(packageInfoPresenter)
@Composable @Composable
fun getActionButtons() = fun getActionButtons() =
@@ -58,7 +62,11 @@ private class AppButtonsPresenter(
@Composable @Composable
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull( 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), appInstallButton.getActionButton(app),
appDisableButton.getActionButton(app), appDisableButton.getActionButton(app),
appUninstallButton.getActionButton(app), appUninstallButton.getActionButton(app),

View File

@@ -18,7 +18,6 @@ package com.android.settings.spa.app.appinfo
import android.app.settings.SettingsEnums import android.app.settings.SettingsEnums
import android.content.Intent import android.content.Intent
import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
@@ -28,11 +27,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R import com.android.settings.R
import com.android.settings.Utils
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
import com.android.settingslib.spa.widget.button.ActionButton 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.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userHandle
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -42,7 +38,6 @@ import kotlinx.coroutines.flow.flowOn
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) { class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
private val context = packageInfoPresenter.context private val context = packageInfoPresenter.context
private val appButtonRepository = AppButtonRepository(context) private val appButtonRepository = AppButtonRepository(context)
private val overlayManager = context.getSystemService(OverlayManager::class.java)!!
private val userManager = context.getSystemService(UserManager::class.java)!! private val userManager = context.getSystemService(UserManager::class.java)!!
@Composable @Composable
@@ -51,49 +46,6 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
return uninstallButton(app) 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 @Composable
private fun uninstallButton(app: ApplicationInfo) = ActionButton( private fun uninstallButton(app: ApplicationInfo) = ActionButton(
text = if (isCloneApp(app)) context.getString(R.string.delete) else 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), imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
enabled = remember(app) { enabled = remember(app) {
flow { flow {
emit(isUninstallButtonEnabled(app)) emit(appButtonRepository.isAllowUninstallOrArchive(context, app))
}.flowOn(Dispatchers.Default) }.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(false).value, }.collectAsStateWithLifecycle(false).value,
) { onUninstallClicked(app) } ) { onUninstallClicked(app) }

View File

@@ -87,19 +87,9 @@ class PackageInfoPresenter(
).filter(::isInterestedAppChange).filter(::isForThisApp) ).filter(::isInterestedAppChange).filter(::isForThisApp)
@VisibleForTesting @VisibleForTesting
fun isInterestedAppChange(intent: Intent) = when { fun isInterestedAppChange(intent: Intent) =
intent.action != Intent.ACTION_PACKAGE_REMOVED -> true intent.action != Intent.ACTION_PACKAGE_REMOVED ||
intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false)
// 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
}
val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow) val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
.map { getPackageInfo() } .map { getPackageInfo() }

View File

@@ -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"
}
}

View File

@@ -22,8 +22,10 @@ import android.content.pm.ApplicationInfo
import android.content.pm.FakeFeatureFlagsImpl import android.content.pm.FakeFeatureFlagsImpl
import android.content.pm.Flags import android.content.pm.Flags
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
@@ -62,6 +64,9 @@ class AppButtonsTest {
@Mock @Mock
private lateinit var packageManager: PackageManager private lateinit var packageManager: PackageManager
@Mock
private lateinit var packageInstaller: PackageInstaller
private val featureFlags = FakeFeatureFlagsImpl() private val featureFlags = FakeFeatureFlagsImpl()
@Before @Before
@@ -74,6 +79,7 @@ class AppButtonsTest {
whenever(packageInfoPresenter.context).thenReturn(context) whenever(packageInfoPresenter.context).thenReturn(context)
whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME) whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager) whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
whenever(packageManager.packageInstaller).thenReturn(packageInstaller)
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO) whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false) whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
featureFlags.setFlag(Flags.FLAG_ARCHIVING, true) featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
@@ -118,8 +124,24 @@ class AppButtonsTest {
composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed() composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
} }
private fun setContent() { @Test
whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO)) 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 { composeTestRule.setContent {
AppButtons(packageInfoPresenter, featureFlags) AppButtons(packageInfoPresenter, featureFlags)
} }

View File

@@ -105,6 +105,7 @@ class PackageInfoPresenterTest {
fun isInterestedAppChange_archived_interested() { fun isInterestedAppChange_archived_interested() {
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply { val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
data = Uri.parse("package:$PACKAGE_NAME") data = Uri.parse("package:$PACKAGE_NAME")
putExtra(Intent.EXTRA_ARCHIVAL, true)
} }
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent) val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)