Update the App Info Settings when package archived
Listen to the following actions,
- Intent.ACTION_PACKAGE_CHANGED for App enabled / disabled
- Intent.ACTION_PACKAGE_REMOVED for App archived
- Intent.ACTION_PACKAGE_REPLACED for App updated
App updates are uninstalled
- Intent.ACTION_PACKAGE_RESTARTED for App force-stopped
Also,
- Prevent AppInfoSettings flaky, by moving package info null into
RegularScaffold.
- Offload uninstallButton's enabled from main thread.
Bug: 314562958
Test: manual - All apps > app detail
Change-Id: Iaf210eb9e9b4ace1aa9079cdbb2d7430de4dd75f
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index 6882963..85e59de 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -119,16 +119,19 @@
@Composable
private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
- val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?:return
- val app = checkNotNull(packageInfo.applicationInfo)
+ val packageInfoState = packageInfoPresenter.flow.collectAsStateWithLifecycle()
val featureFlags: FeatureFlags = FeatureFlagsImpl()
RegularScaffold(
title = stringResource(R.string.application_info_label),
actions = {
- if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app)
- AppInfoSettingsMoreOptions(packageInfoPresenter, app)
+ packageInfoState.value?.applicationInfo?.let { app ->
+ if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app)
+ AppInfoSettingsMoreOptions(packageInfoPresenter, app)
+ }
}
) {
+ val packageInfo = packageInfoState.value ?: return@RegularScaffold
+ val app = packageInfo.applicationInfo ?: return@RegularScaffold
val appInfoProvider = remember(packageInfo) { AppInfoProvider(packageInfo) }
appInfoProvider.AppInfo()
diff --git a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
index 6b3535b..5f6f097 100644
--- a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
@@ -23,8 +23,10 @@
import android.os.UserHandle
import android.os.UserManager
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
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
@@ -33,6 +35,9 @@
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
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
private val context = packageInfoPresenter.context
@@ -43,7 +48,7 @@
@Composable
fun getActionButton(app: ApplicationInfo): ActionButton? {
if (app.isSystemApp || app.isInstantApp) return null
- return uninstallButton(app = app, enabled = isUninstallButtonEnabled(app))
+ return uninstallButton(app)
}
/** Gets whether a package can be uninstalled. */
@@ -90,11 +95,15 @@
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
@Composable
- private fun uninstallButton(app: ApplicationInfo, enabled: Boolean) = ActionButton(
+ private fun uninstallButton(app: ApplicationInfo) = ActionButton(
text = if (isCloneApp(app)) context.getString(R.string.delete) else
context.getString(R.string.uninstall_text),
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
- enabled = enabled,
+ enabled = remember(app) {
+ flow {
+ emit(isUninstallButtonEnabled(app))
+ }.flowOn(Dispatchers.Default)
+ }.collectAsStateWithLifecycle(false).value,
) { onUninstallClicked(app) }
private fun onUninstallClicked(app: ApplicationInfo) {
diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
index 8d0f0bb..a6bd8f0 100644
--- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
+++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
@@ -26,6 +26,7 @@
import android.content.pm.PackageManager
import android.os.UserHandle
import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.app.startUninstallActivity
@@ -40,6 +41,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -65,18 +67,42 @@
val userContext by lazy { context.asUser(userHandle) }
val userPackageManager: PackageManager by lazy { userContext.packageManager }
- val flow: StateFlow<PackageInfo?> = merge(
- flowOf(null), // kick an initial value
- context.broadcastReceiverAsUserFlow(
- intentFilter = IntentFilter().apply {
- addAction(Intent.ACTION_PACKAGE_CHANGED)
- addAction(Intent.ACTION_PACKAGE_REPLACED)
- addAction(Intent.ACTION_PACKAGE_RESTARTED)
- addDataScheme("package")
- },
- userHandle = userHandle,
- ),
- ).map { getPackageInfo() }
+ private val appChangeFlow = context.broadcastReceiverAsUserFlow(
+ intentFilter = IntentFilter().apply {
+ // App enabled / disabled
+ addAction(Intent.ACTION_PACKAGE_CHANGED)
+
+ // App archived
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+
+ // App updated / the updates are uninstalled (system app)
+ addAction(Intent.ACTION_PACKAGE_REPLACED)
+
+ // App force-stopped
+ addAction(Intent.ACTION_PACKAGE_RESTARTED)
+
+ addDataScheme("package")
+ },
+ userHandle = userHandle,
+ ).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
+ }
+
+ val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
+ .map { getPackageInfo() }
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, null)
/**
@@ -89,12 +115,14 @@
}
val navController = LocalNavController.current
DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
- if (packageName == intent.data?.schemeSpecificPart) {
+ if (isForThisApp(intent)) {
navController.navigateBack()
}
}
}
+ private fun isForThisApp(intent: Intent) = packageName == intent.data?.schemeSpecificPart
+
/** Enables this package. */
fun enable() {
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
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 ecb540c..d81bb1a 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
@@ -20,7 +20,9 @@
import android.app.settings.SettingsEnums
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageInfo
import android.content.pm.PackageManager
+import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.testutils.FakeFeatureFactory
@@ -61,11 +63,57 @@
private val fakeFeatureFactory = FakeFeatureFactory()
private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
+ private val packageInfoPresenter =
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
+
+ @Test
+ fun isInterestedAppChange_packageChanged_isInterested() {
+ val intent = Intent(Intent.ACTION_PACKAGE_CHANGED).apply {
+ data = Uri.parse("package:$PACKAGE_NAME")
+ }
+
+ val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
+
+ assertThat(isInterestedAppChange).isTrue()
+ }
+
+ @Test
+ fun isInterestedAppChange_fullyRemoved_notInterested() {
+ val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
+ data = Uri.parse("package:$PACKAGE_NAME")
+ putExtra(Intent.EXTRA_DATA_REMOVED, true)
+ }
+
+ val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
+
+ assertThat(isInterestedAppChange).isFalse()
+ }
+
+ @Test
+ fun isInterestedAppChange_removedBeforeReplacing_notInterested() {
+ val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
+ data = Uri.parse("package:$PACKAGE_NAME")
+ putExtra(Intent.EXTRA_REPLACING, true)
+ }
+
+ val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
+
+ assertThat(isInterestedAppChange).isFalse()
+ }
+
+ @Test
+ fun isInterestedAppChange_archived_interested() {
+ val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
+ data = Uri.parse("package:$PACKAGE_NAME")
+ }
+
+ val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
+
+ assertThat(isInterestedAppChange).isTrue()
+ }
+
@Test
fun enable() = runBlocking {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
-
packageInfoPresenter.enable()
delay(100)
@@ -77,9 +125,6 @@
@Test
fun disable() = runBlocking {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
-
packageInfoPresenter.disable()
delay(100)
@@ -91,9 +136,6 @@
@Test
fun startUninstallActivity() = runBlocking {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
-
packageInfoPresenter.startUninstallActivity()
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
@@ -109,9 +151,6 @@
@Test
fun clearInstantApp() = runBlocking {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
-
packageInfoPresenter.clearInstantApp()
delay(100)
@@ -121,9 +160,6 @@
@Test
fun forceStop() = runBlocking {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
-
packageInfoPresenter.forceStop()
delay(100)
@@ -133,9 +169,6 @@
@Test
fun logAction() = runBlocking {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
-
packageInfoPresenter.logAction(123)
verifyAction(123)
@@ -148,5 +181,6 @@
private companion object {
const val PACKAGE_NAME = "package.name"
const val USER_ID = 0
+ val PACKAGE_INFO = PackageInfo()
}
}