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

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

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
parent 0a32ca2b
Loading
Loading
Loading
Loading
+11 −2
Original line number Diff line number Diff line
@@ -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) }
    }

+4 −6
Original line number Diff line number Diff line
@@ -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,7 +119,6 @@ object AppInfoSettingsProvider : SettingsPageProvider {

@Composable
private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
    LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() })
    val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?:return
    val app = checkNotNull(packageInfo.applicationInfo)
    val featureFlags: FeatureFlags = FeatureFlagsImpl()
@@ -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()

+1 −3
Original line number Diff line number Diff line
@@ -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),
+25 −24
Original line number Diff line number Diff line
@@ -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,37 +64,36 @@ 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()
            }
        }
    }
    }

    /** Enables this package. */
    fun enable() {
@@ -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
+52 −103
Original line number Diff line number Diff line
@@ -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 {
    fun enable() = runBlocking {
        val packageInfoPresenter =
                PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

        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 {
    fun disable() = runBlocking {
        val packageInfoPresenter =
                PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

        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 {
    fun clearInstantApp() = runBlocking {
        val packageInfoPresenter =
                PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

        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 {
    fun forceStop() = runBlocking {
        val packageInfoPresenter =
                PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
            PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)

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