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

Commit b85f4de6 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

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

parents f56d706c b0cf27ab
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()
    }
}