Refresh the App Info Settings
When apk upgraded or downgraded.
And only close the page when the package is fully removed.
Bug: 314562958
Test: manual - on App Info Settings
Test: unit test
Change-Id: Ifdff714da99e31f9c5f237a0c3342de7a0797ec4
diff --git a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt
index 7615442..345d931 100644
--- a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt
@@ -23,7 +23,9 @@
import androidx.compose.material.icons.outlined.Report
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settingslib.RestrictedLockUtils
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
@@ -35,6 +37,9 @@
import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userId
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
class AppForceStopButton(
private val packageInfoPresenter: PackageInfoPresenter,
@@ -47,9 +52,13 @@
fun getActionButton(app: ApplicationInfo): ActionButton {
val dialogPresenter = confirmDialogPresenter()
return ActionButton(
- text = context.getString(R.string.force_stop),
+ text = stringResource(R.string.force_stop),
imageVector = Icons.Outlined.Report,
- enabled = isForceStopButtonEnable(app),
+ enabled = remember(app) {
+ flow {
+ emit(isForceStopButtonEnable(app))
+ }.flowOn(Dispatchers.Default)
+ }.collectAsStateWithLifecycle(false).value,
) { onForceStopButtonClicked(app, dialogPresenter) }
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index 3b7f579..6882963 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -32,10 +32,10 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavType
import androidx.navigation.navArgument
-import com.android.settings.flags.Flags
import com.android.settings.R
import com.android.settings.applications.AppInfoBase
import com.android.settings.applications.appinfo.AppInfoDashboardFragment
+import com.android.settings.flags.Flags
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
import com.android.settings.spa.app.appcompat.UserAspectRatioAppPreference
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
@@ -45,7 +45,6 @@
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
import com.android.settings.spa.app.specialaccess.VoiceActivationAppsListProvider
import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category
@@ -75,7 +74,7 @@
PackageInfoPresenter(context, packageName, userId, coroutineScope)
}
AppInfoSettings(packageInfoPresenter)
- packageInfoPresenter.PackageRemoveDetector()
+ packageInfoPresenter.PackageFullyRemovedEffect()
}
@Composable
@@ -120,8 +119,7 @@
@Composable
private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
- LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() })
- val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?: return
+ val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?:return
val app = checkNotNull(packageInfo.applicationInfo)
val featureFlags: FeatureFlags = FeatureFlagsImpl()
RegularScaffold(
@@ -131,7 +129,7 @@
AppInfoSettingsMoreOptions(packageInfoPresenter, app)
}
) {
- val appInfoProvider = remember { AppInfoProvider(packageInfo) }
+ val appInfoProvider = remember(packageInfo) { AppInfoProvider(packageInfo) }
appInfoProvider.AppInfo()
diff --git a/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt
index 760d375..c8e8d35 100644
--- a/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt
@@ -28,7 +28,6 @@
import androidx.navigation.navArgument
import com.android.settings.R
import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spaprivileged.model.app.toRoute
import com.android.settingslib.spaprivileged.template.app.AppInfoProvider
@@ -54,7 +53,7 @@
PackageInfoPresenter(context, packageName, userId, coroutineScope)
}
CloneAppInfoSettings(packageInfoPresenter)
- packageInfoPresenter.PackageRemoveDetector()
+ packageInfoPresenter.PackageFullyRemovedEffect()
}
@Composable
@@ -70,7 +69,6 @@
@Composable
private fun CloneAppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
- LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() })
val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?: return
RegularScaffold(
title = stringResource(R.string.application_info_label),
diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
index 6eee72e..1320f54 100644
--- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
+++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
@@ -32,14 +32,20 @@
import com.android.settingslib.spa.framework.compose.LocalNavController
import com.android.settingslib.spaprivileged.framework.common.activityManager
import com.android.settingslib.spaprivileged.framework.common.asUser
+import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverAsUserFlow
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
private const val TAG = "PackageInfoPresenter"
@@ -58,34 +64,33 @@
private val userHandle = UserHandle.of(userId)
val userContext by lazy { context.asUser(userHandle) }
val userPackageManager: PackageManager by lazy { userContext.packageManager }
- private val _flow: MutableStateFlow<PackageInfo?> = MutableStateFlow(null)
- val flow: StateFlow<PackageInfo?> = _flow
-
- fun reloadPackageInfo() {
- coroutineScope.launch(Dispatchers.IO) {
- _flow.value = getPackageInfo()
- }
- }
+ 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() }
+ .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), null)
/**
- * Detects the package removed event.
+ * Detects the package fully removed event, and close the current page.
*/
@Composable
- fun PackageRemoveDetector() {
- val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED).apply {
+ fun PackageFullyRemovedEffect() {
+ val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_FULLY_REMOVED).apply {
addDataScheme("package")
}
val navController = LocalNavController.current
DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
if (packageName == intent.data?.schemeSpecificPart) {
- val packageInfo = flow.value
- if (packageInfo != null && packageInfo.applicationInfo?.isSystemApp == true) {
- // System app still exists after uninstalling the updates, refresh the page.
- reloadPackageInfo()
- } else {
- navController.navigateBack()
- }
+ navController.navigateBack()
}
}
}
@@ -97,7 +102,6 @@
userPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0
)
- reloadPackageInfo()
}
}
@@ -108,7 +112,6 @@
userPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
)
- reloadPackageInfo()
}
}
@@ -123,7 +126,6 @@
logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)
coroutineScope.launch(Dispatchers.IO) {
userPackageManager.deletePackageAsUser(packageName, null, 0, userId)
- reloadPackageInfo()
}
}
@@ -133,7 +135,6 @@
coroutineScope.launch(Dispatchers.Default) {
Log.d(TAG, "Stopping package $packageName")
context.activityManager.forceStopPackageAsUser(packageName, userId)
- reloadPackageInfo()
}
}
@@ -141,7 +142,7 @@
metricsFeatureProvider.action(context, category, packageName)
}
- private fun getPackageInfo() =
+ private fun getPackageInfo(): PackageInfo? =
packageManagers.getPackageInfoAsUser(
packageName = packageName,
flags = PackageManager.MATCH_ANY_USER.toLong() or
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 6c5cb85..ecb540c 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,8 +20,6 @@
import android.app.settings.SettingsEnums
import android.content.Context
import android.content.Intent
-import android.content.pm.FakeFeatureFlagsImpl
-import android.content.pm.Flags
import android.content.pm.PackageManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -30,91 +28,79 @@
import com.android.settingslib.spaprivileged.framework.common.activityManager
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.doNothing
-import org.mockito.Mockito.verify
-import org.mockito.Spy
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
-import org.mockito.Mockito.`when` as whenever
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class PackageInfoPresenterTest {
- @get:Rule
- val mockito: MockitoRule = MockitoJUnit.rule()
- @Spy
- private val context: Context = ApplicationProvider.getApplicationContext()
+ private val mockPackageManager = mock<PackageManager>()
- @Mock
- private lateinit var packageManager: PackageManager
+ private val mockActivityManager = mock<ActivityManager>()
- @Mock
- private lateinit var activityManager: ActivityManager
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { packageManager } doReturn mockPackageManager
+ on { activityManager } doReturn mockActivityManager
+ doNothing().whenever(mock).startActivityAsUser(any(), any())
+ mock.mockAsUser()
+ }
- @Mock
- private lateinit var packageManagers: IPackageManagers
+ private val packageManagers = mock<IPackageManagers>()
private val fakeFeatureFactory = FakeFeatureFactory()
private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
- @Before
- fun setUp() {
- context.mockAsUser()
- whenever(context.packageManager).thenReturn(packageManager)
- whenever(context.activityManager).thenReturn(activityManager)
- }
-
@Test
- fun enable() = runTest {
- coroutineScope {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
+ fun enable() = runBlocking {
+ val packageInfoPresenter =
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
- packageInfoPresenter.enable()
- }
+ packageInfoPresenter.enable()
+ delay(100)
verifyAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
- verify(packageManager).setApplicationEnabledSetting(
+ verify(mockPackageManager).setApplicationEnabledSetting(
PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0
)
}
@Test
- fun disable() = runTest {
- coroutineScope {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
+ fun disable() = runBlocking {
+ val packageInfoPresenter =
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
- packageInfoPresenter.disable()
- }
+ packageInfoPresenter.disable()
+ delay(100)
verifyAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
- verify(packageManager).setApplicationEnabledSetting(
+ verify(mockPackageManager).setApplicationEnabledSetting(
PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
)
}
@Test
- fun startUninstallActivity() = runTest {
- doNothing().`when`(context).startActivityAsUser(any(), any())
+ fun startUninstallActivity() = runBlocking {
val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.startUninstallActivity()
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
- val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
- verify(context).startActivityAsUser(intentCaptor.capture(), any())
- with(intentCaptor.value) {
+ val intent = argumentCaptor<Intent> {
+ verify(context).startActivityAsUser(capture(), any())
+ }.firstValue
+ with(intent) {
assertThat(action).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE)
assertThat(data?.schemeSpecificPart).isEqualTo(PACKAGE_NAME)
assertThat(getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true)).isEqualTo(false)
@@ -122,76 +108,39 @@
}
@Test
- fun clearInstantApp() = runTest {
- coroutineScope {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
+ fun clearInstantApp() = runBlocking {
+ val packageInfoPresenter =
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
- packageInfoPresenter.clearInstantApp()
- }
+ packageInfoPresenter.clearInstantApp()
+ delay(100)
verifyAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP)
- verify(packageManager).deletePackageAsUser(PACKAGE_NAME, null, 0, USER_ID)
+ verify(mockPackageManager).deletePackageAsUser(PACKAGE_NAME, null, 0, USER_ID)
}
@Test
- fun forceStop() = runTest {
- coroutineScope {
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
+ fun forceStop() = runBlocking {
+ val packageInfoPresenter =
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
- packageInfoPresenter.forceStop()
- }
+ packageInfoPresenter.forceStop()
+ delay(100)
verifyAction(SettingsEnums.ACTION_APP_FORCE_STOP)
- verify(activityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID)
+ verify(mockActivityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID)
}
@Test
- fun logAction() = runTest {
+ fun logAction() = runBlocking {
val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
+ PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.logAction(123)
verifyAction(123)
}
- @Test
- fun reloadPackageInfo_archivingDisabled() = runTest {
- coroutineScope {
- val fakeFeatureFlags = FakeFeatureFlagsImpl()
- fakeFeatureFlags.setFlag(Flags.FLAG_ARCHIVING, false)
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers, fakeFeatureFlags)
-
- packageInfoPresenter.reloadPackageInfo()
- }
-
- val flags = PackageManager.MATCH_ANY_USER.toLong() or
- PackageManager.MATCH_DISABLED_COMPONENTS.toLong() or
- PackageManager.GET_PERMISSIONS.toLong()
- verify(packageManagers).getPackageInfoAsUser(PACKAGE_NAME, flags, USER_ID)
- }
-
- @Test
- fun reloadPackageInfo_archivingEnabled() = runTest {
- coroutineScope {
- val fakeFeatureFlags = FakeFeatureFlagsImpl()
- fakeFeatureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
- val packageInfoPresenter =
- PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers, fakeFeatureFlags)
-
- packageInfoPresenter.reloadPackageInfo()
- }
-
- val flags = PackageManager.MATCH_ANY_USER.toLong() or
- PackageManager.MATCH_DISABLED_COMPONENTS.toLong() or
- PackageManager.GET_PERMISSIONS.toLong() or
- PackageManager.MATCH_ARCHIVED_PACKAGES
- verify(packageManagers).getPackageInfoAsUser(PACKAGE_NAME, flags, USER_ID)
- }
-
private fun verifyAction(category: Int) {
verify(metricsFeatureProvider).action(context, category, PACKAGE_NAME)
}