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