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
This commit is contained in:
Chaohui Wang
2023-12-03 18:00:51 +08:00
parent 0a32ca2bbc
commit de3fe3744f
5 changed files with 93 additions and 138 deletions

View File

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

View File

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

View File

@@ -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),

View File

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

View File

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