Loading src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +7 −4 Original line number Diff line number Diff line Loading @@ -119,16 +119,19 @@ object AppInfoSettingsProvider : SettingsPageProvider { @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 = { 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() Loading src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt +12 −3 Original line number Diff line number Diff line Loading @@ -23,8 +23,10 @@ import android.content.pm.ApplicationInfo 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 Loading @@ -33,6 +35,9 @@ import com.android.settingslib.spaprivileged.framework.common.devicePolicyManage 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 Loading @@ -43,7 +48,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) @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. */ Loading Loading @@ -90,11 +95,15 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) 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) { Loading src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt +41 −13 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.content.pm.PackageInfo 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 Loading @@ -40,6 +41,7 @@ import kotlinx.coroutines.CoroutineScope 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 Loading @@ -65,18 +67,42 @@ class PackageInfoPresenter( 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( 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, ), ).map { getPackageInfo() } ).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) /** Loading @@ -89,12 +115,14 @@ class PackageInfoPresenter( } 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) Loading tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt +52 −18 Original line number Diff line number Diff line Loading @@ -20,7 +20,9 @@ import android.app.ActivityManager 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 Loading Loading @@ -61,11 +63,57 @@ class PackageInfoPresenterTest { private val fakeFeatureFactory = FakeFeatureFactory() private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider @Test fun enable() = runBlocking { val packageInfoPresenter = 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 { packageInfoPresenter.enable() delay(100) Loading @@ -77,9 +125,6 @@ class PackageInfoPresenterTest { @Test fun disable() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.disable() delay(100) Loading @@ -91,9 +136,6 @@ class PackageInfoPresenterTest { @Test fun startUninstallActivity() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.startUninstallActivity() verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) Loading @@ -109,9 +151,6 @@ class PackageInfoPresenterTest { @Test fun clearInstantApp() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.clearInstantApp() delay(100) Loading @@ -121,9 +160,6 @@ class PackageInfoPresenterTest { @Test fun forceStop() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.forceStop() delay(100) Loading @@ -133,9 +169,6 @@ class PackageInfoPresenterTest { @Test fun logAction() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.logAction(123) verifyAction(123) Loading @@ -148,5 +181,6 @@ class PackageInfoPresenterTest { private companion object { const val PACKAGE_NAME = "package.name" const val USER_ID = 0 val PACKAGE_INFO = PackageInfo() } } Loading
src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +7 −4 Original line number Diff line number Diff line Loading @@ -119,16 +119,19 @@ object AppInfoSettingsProvider : SettingsPageProvider { @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 = { 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() Loading
src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt +12 −3 Original line number Diff line number Diff line Loading @@ -23,8 +23,10 @@ import android.content.pm.ApplicationInfo 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 Loading @@ -33,6 +35,9 @@ import com.android.settingslib.spaprivileged.framework.common.devicePolicyManage 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 Loading @@ -43,7 +48,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) @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. */ Loading Loading @@ -90,11 +95,15 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) 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) { Loading
src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt +41 −13 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.content.pm.PackageInfo 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 Loading @@ -40,6 +41,7 @@ import kotlinx.coroutines.CoroutineScope 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 Loading @@ -65,18 +67,42 @@ class PackageInfoPresenter( 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( 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, ), ).map { getPackageInfo() } ).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) /** Loading @@ -89,12 +115,14 @@ class PackageInfoPresenter( } 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) Loading
tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt +52 −18 Original line number Diff line number Diff line Loading @@ -20,7 +20,9 @@ import android.app.ActivityManager 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 Loading Loading @@ -61,11 +63,57 @@ class PackageInfoPresenterTest { private val fakeFeatureFactory = FakeFeatureFactory() private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider @Test fun enable() = runBlocking { val packageInfoPresenter = 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 { packageInfoPresenter.enable() delay(100) Loading @@ -77,9 +125,6 @@ class PackageInfoPresenterTest { @Test fun disable() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.disable() delay(100) Loading @@ -91,9 +136,6 @@ class PackageInfoPresenterTest { @Test fun startUninstallActivity() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.startUninstallActivity() verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) Loading @@ -109,9 +151,6 @@ class PackageInfoPresenterTest { @Test fun clearInstantApp() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.clearInstantApp() delay(100) Loading @@ -121,9 +160,6 @@ class PackageInfoPresenterTest { @Test fun forceStop() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.forceStop() delay(100) Loading @@ -133,9 +169,6 @@ class PackageInfoPresenterTest { @Test fun logAction() = runBlocking { val packageInfoPresenter = PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) packageInfoPresenter.logAction(123) verifyAction(123) Loading @@ -148,5 +181,6 @@ class PackageInfoPresenterTest { private companion object { const val PACKAGE_NAME = "package.name" const val USER_ID = 0 val PACKAGE_INFO = PackageInfo() } }