diff --git a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt index 7615442d780..345d931f9c0 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.Icons 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.spa.widget.dialog.rememberAlertDialogPresenter 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 @@ class AppForceStopButton( 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 3b7f579c740..6882963d7ce 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.fragment.app.Fragment 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.ModifySystemSettingsAppListPro 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 @@ object AppInfoSettingsProvider : SettingsPageProvider { PackageInfoPresenter(context, packageName, userId, coroutineScope) } AppInfoSettings(packageInfoPresenter) - packageInfoPresenter.PackageRemoveDetector() + packageInfoPresenter.PackageFullyRemovedEffect() } @Composable @@ -120,8 +119,7 @@ object AppInfoSettingsProvider : SettingsPageProvider { @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 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { 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 760d375f6db..c8e8d35d768 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.NavType 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 @@ object CloneAppInfoSettingsProvider : SettingsPageProvider { PackageInfoPresenter(context, packageName, userId, coroutineScope) } CloneAppInfoSettings(packageInfoPresenter) - packageInfoPresenter.PackageRemoveDetector() + packageInfoPresenter.PackageFullyRemovedEffect() } @Composable @@ -70,7 +69,6 @@ object CloneAppInfoSettingsProvider : SettingsPageProvider { @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 6eee72edb1c..1320f5404e8 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.settings.spa.app.startUninstallActivity 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 @@ class PackageInfoPresenter( private val userHandle = UserHandle.of(userId) val userContext by lazy { context.asUser(userHandle) } val userPackageManager: PackageManager by lazy { userContext.packageManager } - private val _flow: MutableStateFlow = MutableStateFlow(null) - val flow: StateFlow = _flow - - fun reloadPackageInfo() { - coroutineScope.launch(Dispatchers.IO) { - _flow.value = getPackageInfo() - } - } + val flow: StateFlow = 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 @@ class PackageInfoPresenter( userPackageManager.setApplicationEnabledSetting( packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0 ) - reloadPackageInfo() } } @@ -108,7 +112,6 @@ class PackageInfoPresenter( userPackageManager.setApplicationEnabledSetting( packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 ) - reloadPackageInfo() } } @@ -123,7 +126,6 @@ class PackageInfoPresenter( logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP) coroutineScope.launch(Dispatchers.IO) { userPackageManager.deletePackageAsUser(packageName, null, 0, userId) - reloadPackageInfo() } } @@ -133,7 +135,6 @@ class PackageInfoPresenter( coroutineScope.launch(Dispatchers.Default) { Log.d(TAG, "Stopping package $packageName") context.activityManager.forceStopPackageAsUser(packageName, userId) - reloadPackageInfo() } } @@ -141,7 +142,7 @@ class PackageInfoPresenter( 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 6c5cb854ea8..ecb540cd84a 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.ActivityManager 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.settings.testutils.mockAsUser 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() - @Mock - private lateinit var packageManager: PackageManager + private val mockActivityManager = mock() - @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() 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 { + 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 @@ class PackageInfoPresenterTest { } @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) }