Merge "Update the App Info Settings when package archived" into main

This commit is contained in:
Chaohui Wang
2023-12-06 10:11:33 +00:00
committed by Android (Google) Code Review
4 changed files with 112 additions and 38 deletions

View File

@@ -119,16 +119,19 @@ object AppInfoSettingsProvider : SettingsPageProvider {
@Composable @Composable
private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?:return val packageInfoState = packageInfoPresenter.flow.collectAsStateWithLifecycle()
val app = checkNotNull(packageInfo.applicationInfo)
val featureFlags: FeatureFlags = FeatureFlagsImpl() val featureFlags: FeatureFlags = FeatureFlagsImpl()
RegularScaffold( RegularScaffold(
title = stringResource(R.string.application_info_label), title = stringResource(R.string.application_info_label),
actions = { actions = {
if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app) packageInfoState.value?.applicationInfo?.let { app ->
AppInfoSettingsMoreOptions(packageInfoPresenter, 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) } val appInfoProvider = remember(packageInfo) { AppInfoProvider(packageInfo) }
appInfoProvider.AppInfo() appInfoProvider.AppInfo()

View File

@@ -23,8 +23,10 @@ import android.content.pm.ApplicationInfo
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R import com.android.settings.R
import com.android.settings.Utils import com.android.settings.Utils
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
@@ -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.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userHandle 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) { class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
private val context = packageInfoPresenter.context private val context = packageInfoPresenter.context
@@ -43,7 +48,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
@Composable @Composable
fun getActionButton(app: ApplicationInfo): ActionButton? { fun getActionButton(app: ApplicationInfo): ActionButton? {
if (app.isSystemApp || app.isInstantApp) return null if (app.isSystemApp || app.isInstantApp) return null
return uninstallButton(app = app, enabled = isUninstallButtonEnabled(app)) return uninstallButton(app)
} }
/** Gets whether a package can be uninstalled. */ /** Gets whether a package can be uninstalled. */
@@ -90,11 +95,15 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
@Composable @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 text = if (isCloneApp(app)) context.getString(R.string.delete) else
context.getString(R.string.uninstall_text), context.getString(R.string.uninstall_text),
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete), 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) } ) { onUninstallClicked(app) }
private fun onUninstallClicked(app: ApplicationInfo) { private fun onUninstallClicked(app: ApplicationInfo) {

View File

@@ -26,6 +26,7 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.app.startUninstallActivity import com.android.settings.spa.app.startUninstallActivity
@@ -40,6 +41,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -65,18 +67,42 @@ class PackageInfoPresenter(
val userContext by lazy { context.asUser(userHandle) } val userContext by lazy { context.asUser(userHandle) }
val userPackageManager: PackageManager by lazy { userContext.packageManager } val userPackageManager: PackageManager by lazy { userContext.packageManager }
val flow: StateFlow<PackageInfo?> = merge( private val appChangeFlow = context.broadcastReceiverAsUserFlow(
flowOf(null), // kick an initial value intentFilter = IntentFilter().apply {
context.broadcastReceiverAsUserFlow( // App enabled / disabled
intentFilter = IntentFilter().apply { addAction(Intent.ACTION_PACKAGE_CHANGED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addAction(Intent.ACTION_PACKAGE_REPLACED) // App archived
addAction(Intent.ACTION_PACKAGE_RESTARTED) addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}, // App updated / the updates are uninstalled (system app)
userHandle = userHandle, addAction(Intent.ACTION_PACKAGE_REPLACED)
),
).map { getPackageInfo() } // App force-stopped
addAction(Intent.ACTION_PACKAGE_RESTARTED)
addDataScheme("package")
},
userHandle = userHandle,
).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) .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, null)
/** /**
@@ -89,12 +115,14 @@ class PackageInfoPresenter(
} }
val navController = LocalNavController.current val navController = LocalNavController.current
DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent -> DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
if (packageName == intent.data?.schemeSpecificPart) { if (isForThisApp(intent)) {
navController.navigateBack() navController.navigateBack()
} }
} }
} }
private fun isForThisApp(intent: Intent) = packageName == intent.data?.schemeSpecificPart
/** Enables this package. */ /** Enables this package. */
fun enable() { fun enable() {
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP) logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)

View File

@@ -20,7 +20,9 @@ import android.app.ActivityManager
import android.app.settings.SettingsEnums import android.app.settings.SettingsEnums
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.testutils.FakeFeatureFactory import com.android.settings.testutils.FakeFeatureFactory
@@ -61,11 +63,57 @@ class PackageInfoPresenterTest {
private val fakeFeatureFactory = FakeFeatureFactory() private val fakeFeatureFactory = FakeFeatureFactory()
private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
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 @Test
fun enable() = runBlocking { fun enable() = runBlocking {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.enable() packageInfoPresenter.enable()
delay(100) delay(100)
@@ -77,9 +125,6 @@ class PackageInfoPresenterTest {
@Test @Test
fun disable() = runBlocking { fun disable() = runBlocking {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.disable() packageInfoPresenter.disable()
delay(100) delay(100)
@@ -91,9 +136,6 @@ class PackageInfoPresenterTest {
@Test @Test
fun startUninstallActivity() = runBlocking { fun startUninstallActivity() = runBlocking {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.startUninstallActivity() packageInfoPresenter.startUninstallActivity()
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
@@ -109,9 +151,6 @@ class PackageInfoPresenterTest {
@Test @Test
fun clearInstantApp() = runBlocking { fun clearInstantApp() = runBlocking {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.clearInstantApp() packageInfoPresenter.clearInstantApp()
delay(100) delay(100)
@@ -121,9 +160,6 @@ class PackageInfoPresenterTest {
@Test @Test
fun forceStop() = runBlocking { fun forceStop() = runBlocking {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.forceStop() packageInfoPresenter.forceStop()
delay(100) delay(100)
@@ -133,9 +169,6 @@ class PackageInfoPresenterTest {
@Test @Test
fun logAction() = runBlocking { fun logAction() = runBlocking {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
packageInfoPresenter.logAction(123) packageInfoPresenter.logAction(123)
verifyAction(123) verifyAction(123)
@@ -148,5 +181,6 @@ class PackageInfoPresenterTest {
private companion object { private companion object {
const val PACKAGE_NAME = "package.name" const val PACKAGE_NAME = "package.name"
const val USER_ID = 0 const val USER_ID = 0
val PACKAGE_INFO = PackageInfo()
} }
} }