Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b0cf27ab authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Update the App Info Settings when package archived

Listen to the following actions,
- Intent.ACTION_PACKAGE_CHANGED for App enabled / disabled
- Intent.ACTION_PACKAGE_REMOVED for App archived
- Intent.ACTION_PACKAGE_REPLACED for App updated
                                     App updates are uninstalled
- Intent.ACTION_PACKAGE_RESTARTED for App force-stopped

Also,
- Prevent AppInfoSettings flaky, by moving package info null into
  RegularScaffold.
- Offload uninstallButton's enabled from main thread.

Bug: 314562958
Test: manual - All apps > app detail
Change-Id: Iaf210eb9e9b4ace1aa9079cdbb2d7430de4dd75f
parent 1a7a4d24
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -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()
+12 −3
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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. */
@@ -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) {
+41 −13
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)

    /**
@@ -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)
+52 −18
Original line number Diff line number Diff line
@@ -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
@@ -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)

@@ -77,9 +125,6 @@ class PackageInfoPresenterTest {

    @Test
    fun disable() = runBlocking {
        val packageInfoPresenter =
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

        packageInfoPresenter.disable()
        delay(100)

@@ -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)
@@ -109,9 +151,6 @@ class PackageInfoPresenterTest {

    @Test
    fun clearInstantApp() = runBlocking {
        val packageInfoPresenter =
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

        packageInfoPresenter.clearInstantApp()
        delay(100)

@@ -121,9 +160,6 @@ class PackageInfoPresenterTest {

    @Test
    fun forceStop() = runBlocking {
        val packageInfoPresenter =
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

        packageInfoPresenter.forceStop()
        delay(100)

@@ -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)
@@ -148,5 +181,6 @@ class PackageInfoPresenterTest {
    private companion object {
        const val PACKAGE_NAME = "package.name"
        const val USER_ID = 0
        val PACKAGE_INFO = PackageInfo()
    }
}