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

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

Support App List match any user for admin

If set, an app will displayed in owner's App List even it not installed
for the owner (not installed on every profile of the owner).

Bug: 277299765
Test: Manually with All Apps when multiple users is on
Test: Unit test
Change-Id: I81119e20ee46b4dc3bfebe8d10c0978f439de2eb
parent 216d0637
Loading
Loading
Loading
Loading
+51 −6
Original line number Diff line number Diff line
@@ -20,9 +20,12 @@ import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.ResolveInfo
import com.android.internal.R
import com.android.settingslib.spaprivileged.framework.common.userManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -33,7 +36,11 @@ import kotlinx.coroutines.runBlocking
 */
internal interface AppListRepository {
    /** Loads the list of [ApplicationInfo]. */
    suspend fun loadApps(userId: Int, showInstantApps: Boolean): List<ApplicationInfo>
    suspend fun loadApps(
        userId: Int,
        showInstantApps: Boolean = false,
        matchAnyUserForAdmin: Boolean = false,
    ): List<ApplicationInfo>

    /** Gets the flow of predicate that could used to filter system app. */
    fun showSystemPredicate(
@@ -61,10 +68,12 @@ object AppListRepositoryUtil {

internal class AppListRepositoryImpl(private val context: Context) : AppListRepository {
    private val packageManager = context.packageManager
    private val userManager = context.userManager

    override suspend fun loadApps(
        userId: Int,
        showInstantApps: Boolean,
        matchAnyUserForAdmin: Boolean,
    ): List<ApplicationInfo> = coroutineScope {
        val hiddenSystemModulesDeferred = async {
            packageManager.getInstalledModules(0)
@@ -75,12 +84,8 @@ internal class AppListRepositoryImpl(private val context: Context) : AppListRepo
        val hideWhenDisabledPackagesDeferred = async {
            context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
        }
        val flags = PackageManager.ApplicationInfoFlags.of(
            (PackageManager.MATCH_DISABLED_COMPONENTS or
                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
        )
        val installedApplicationsAsUser =
            packageManager.getInstalledApplicationsAsUser(flags, userId)
            getInstalledApplications(userId, matchAnyUserForAdmin)

        val hiddenSystemModules = hiddenSystemModulesDeferred.await()
        val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
@@ -89,6 +94,46 @@ internal class AppListRepositoryImpl(private val context: Context) : AppListRepo
        }
    }

    private suspend fun getInstalledApplications(
        userId: Int,
        matchAnyUserForAdmin: Boolean,
    ): List<ApplicationInfo> {
        val regularFlags = ApplicationInfoFlags.of(
            (PackageManager.MATCH_DISABLED_COMPONENTS or
                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
        )
        return if (!matchAnyUserForAdmin || !userManager.getUserInfo(userId).isAdmin) {
            packageManager.getInstalledApplicationsAsUser(regularFlags, userId)
        } else {
            coroutineScope {
                val deferredPackageNamesInChildProfiles =
                    userManager.getProfileIdsWithDisabled(userId)
                        .filter { it != userId }
                        .map {
                            async {
                                packageManager.getInstalledApplicationsAsUser(regularFlags, it)
                                    .map { it.packageName }
                            }
                        }
                val adminFlags = ApplicationInfoFlags.of(
                    PackageManager.MATCH_ANY_USER.toLong() or regularFlags.value
                )
                val allInstalledApplications =
                    packageManager.getInstalledApplicationsAsUser(adminFlags, userId)
                val packageNamesInChildProfiles = deferredPackageNamesInChildProfiles
                    .awaitAll()
                    .flatten()
                    .toSet()
                // If an app is for a child profile and not installed on the owner, not display as
                // 'not installed for this user' in the owner. This will prevent duplicates of work
                // only apps showing up in the personal profile.
                allInstalledApplications.filter {
                    it.installed || it.packageName !in packageNamesInChildProfiles
                }
            }
        }
    }

    override fun showSystemPredicate(
        userIdFlow: Flow<Int>,
        showSystemFlow: Flow<Boolean>,
+6 −2
Original line number Diff line number Diff line
@@ -80,12 +80,15 @@ internal open class AppListViewModelImpl<T : AppRecord>(
    private val scope = viewModelScope + Dispatchers.IO

    private val userSubGraphsFlow = appListConfig.flow.map { config ->
        config.userIds.map { userId -> UserSubGraph(userId, config.showInstantApps) }
        config.userIds.map { userId ->
            UserSubGraph(userId, config.showInstantApps, config.matchAnyUserForAdmin)
        }
    }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

    private inner class UserSubGraph(
        private val userId: Int,
        private val showInstantApps: Boolean,
        private val matchAnyUserForAdmin: Boolean,
    ) {
        private val userIdFlow = flowOf(userId)

@@ -110,7 +113,8 @@ internal open class AppListViewModelImpl<T : AppRecord>(

        fun reloadApps() {
            scope.launch {
                appsStateFlow.value = appListRepository.loadApps(userId, showInstantApps)
                appsStateFlow.value =
                    appListRepository.loadApps(userId, showInstantApps, matchAnyUserForAdmin)
            }
        }
    }
+1 −0
Original line number Diff line number Diff line
@@ -62,6 +62,7 @@ private const val CONTENT_TYPE_HEADER = "header"
data class AppListConfig(
    val userIds: List<Int>,
    val showInstantApps: Boolean,
    val matchAnyUserForAdmin: Boolean,
)

data class AppListState(
+2 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ fun <T : AppRecord> AppListPage(
    title: String,
    listModel: AppListModel<T>,
    showInstantApps: Boolean = false,
    matchAnyUserForAdmin: Boolean = false,
    primaryUserOnly: Boolean = false,
    noItemMessage: String? = null,
    moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
@@ -59,6 +60,7 @@ fun <T : AppRecord> AppListPage(
                config = AppListConfig(
                    userIds = userGroup.userInfos.map { it.id },
                    showInstantApps = showInstantApps,
                    matchAnyUserForAdmin = matchAnyUserForAdmin,
                ),
                listModel = listModel,
                state = AppListState(
+131 −32
Original line number Diff line number Diff line
@@ -23,9 +23,13 @@ import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.PackageManager.ResolveInfoFlags
import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
import android.content.res.Resources
import android.os.UserManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.R
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
@@ -35,10 +39,13 @@ import org.junit.Before
import org.junit.Rule
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.anyInt
import org.mockito.Mockito.eq
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
@@ -49,8 +56,8 @@ class AppListRepositoryTest {
    @get:Rule
    val mockito: MockitoRule = MockitoJUnit.rule()

    @Mock
    private lateinit var context: Context
    @Spy
    private val context: Context = ApplicationProvider.getApplicationContext()

    @Mock
    private lateinit var resources: Resources
@@ -58,6 +65,9 @@ class AppListRepositoryTest {
    @Mock
    private lateinit var packageManager: PackageManager

    @Mock
    private lateinit var userManager: UserManager

    private lateinit var repository: AppListRepository

    @Before
@@ -66,36 +76,116 @@ class AppListRepositoryTest {
        whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames))
            .thenReturn(emptyArray())
        whenever(context.packageManager).thenReturn(packageManager)
        whenever(context.userManager).thenReturn(userManager)
        whenever(packageManager.getInstalledModules(anyInt())).thenReturn(emptyList())
        whenever(
            packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
            packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), anyInt())
        ).thenReturn(emptyList())
        whenever(userManager.getUserInfo(ADMIN_USER_ID)).thenReturn(UserInfo().apply {
            flags = UserInfo.FLAG_ADMIN
        })
        whenever(userManager.getProfileIdsWithDisabled(ADMIN_USER_ID))
            .thenReturn(intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID))

        repository = AppListRepositoryImpl(context)
    }

    private fun mockInstalledApplications(apps: List<ApplicationInfo>) {
    private fun mockInstalledApplications(apps: List<ApplicationInfo>, userId: Int) {
        whenever(
            packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(USER_ID))
            packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId))
        ).thenReturn(apps)
    }

    @Test
    fun loadApps_notShowInstantApps() = runTest {
        mockInstalledApplications(listOf(NORMAL_APP, INSTANT_APP))
        mockInstalledApplications(listOf(NORMAL_APP, INSTANT_APP), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = false)
        val appList = repository.loadApps(
            userId = ADMIN_USER_ID,
            showInstantApps = false,
        )

        assertThat(appListFlow).containsExactly(NORMAL_APP)
        assertThat(appList).containsExactly(NORMAL_APP)
    }

    @Test
    fun loadApps_showInstantApps() = runTest {
        mockInstalledApplications(listOf(NORMAL_APP, INSTANT_APP))
        mockInstalledApplications(listOf(NORMAL_APP, INSTANT_APP), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = true)
        val appList = repository.loadApps(
            userId = ADMIN_USER_ID,
            showInstantApps = true,
        )

        assertThat(appListFlow).containsExactly(NORMAL_APP, INSTANT_APP)
        assertThat(appList).containsExactly(NORMAL_APP, INSTANT_APP)
    }

    @Test
    fun loadApps_notMatchAnyUserForAdmin_withRegularFlags() = runTest {
        mockInstalledApplications(listOf(NORMAL_APP), ADMIN_USER_ID)

        val appList = repository.loadApps(
            userId = ADMIN_USER_ID,
            matchAnyUserForAdmin = false,
        )

        assertThat(appList).containsExactly(NORMAL_APP)
        val flags = ArgumentCaptor.forClass(ApplicationInfoFlags::class.java)
        verify(packageManager).getInstalledApplicationsAsUser(flags.capture(), eq(ADMIN_USER_ID))
        assertThat(flags.value.value).isEqualTo(
            PackageManager.MATCH_DISABLED_COMPONENTS or
                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
        )
    }

    @Test
    fun loadApps_matchAnyUserForAdmin_withMatchAnyUserFlag() = runTest {
        mockInstalledApplications(listOf(NORMAL_APP), ADMIN_USER_ID)

        val appList = repository.loadApps(
            userId = ADMIN_USER_ID,
            matchAnyUserForAdmin = true,
        )

        assertThat(appList).containsExactly(NORMAL_APP)
        val flags = ArgumentCaptor.forClass(ApplicationInfoFlags::class.java)
        verify(packageManager).getInstalledApplicationsAsUser(flags.capture(), eq(ADMIN_USER_ID))
        assertThat(flags.value.value and PackageManager.MATCH_ANY_USER.toLong()).isGreaterThan(0L)
    }

    @Test
    fun loadApps_matchAnyUserForAdminAndInstalledOnManagedProfileOnly_notDisplayed() = runTest {
        val managedProfileOnlyPackageName = "installed.on.managed.profile.only"
        mockInstalledApplications(listOf(ApplicationInfo().apply {
            packageName = managedProfileOnlyPackageName
        }), ADMIN_USER_ID)
        mockInstalledApplications(listOf(ApplicationInfo().apply {
            packageName = managedProfileOnlyPackageName
            flags = ApplicationInfo.FLAG_INSTALLED
        }), MANAGED_PROFILE_USER_ID)

        val appList = repository.loadApps(
            userId = ADMIN_USER_ID,
            matchAnyUserForAdmin = true,
        )

        assertThat(appList).isEmpty()
    }

    @Test
    fun loadApps_matchAnyUserForAdminAndInstalledOnSecondaryUserOnly_displayed() = runTest {
        val secondaryUserOnlyApp = ApplicationInfo().apply {
            packageName = "installed.on.secondary.user.only"
        }
        mockInstalledApplications(listOf(secondaryUserOnlyApp), ADMIN_USER_ID)
        mockInstalledApplications(emptyList(), MANAGED_PROFILE_USER_ID)

        val appList = repository.loadApps(
            userId = ADMIN_USER_ID,
            matchAnyUserForAdmin = true,
        )

        assertThat(appList).containsExactly(secondaryUserOnlyApp)
    }

    @Test
@@ -106,11 +196,11 @@ class AppListRepositoryTest {
        }
        whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames))
            .thenReturn(arrayOf(app.packageName))
        mockInstalledApplications(listOf(app))
        mockInstalledApplications(listOf(app), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = false)
        val appList = repository.loadApps(userId = ADMIN_USER_ID)

        assertThat(appListFlow).isEmpty()
        assertThat(appList).isEmpty()
    }

    @Test
@@ -122,11 +212,11 @@ class AppListRepositoryTest {
        }
        whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames))
            .thenReturn(arrayOf(app.packageName))
        mockInstalledApplications(listOf(app))
        mockInstalledApplications(listOf(app), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = false)
        val appList = repository.loadApps(userId = ADMIN_USER_ID)

        assertThat(appListFlow).isEmpty()
        assertThat(appList).isEmpty()
    }

    @Test
@@ -137,11 +227,11 @@ class AppListRepositoryTest {
        }
        whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames))
            .thenReturn(arrayOf(app.packageName))
        mockInstalledApplications(listOf(app))
        mockInstalledApplications(listOf(app), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = false)
        val appList = repository.loadApps(userId = ADMIN_USER_ID)

        assertThat(appListFlow).containsExactly(app)
        assertThat(appList).containsExactly(app)
    }

    @Test
@@ -151,11 +241,11 @@ class AppListRepositoryTest {
            enabled = false
            enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
        }
        mockInstalledApplications(listOf(app))
        mockInstalledApplications(listOf(app), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = false)
        val appList = repository.loadApps(userId = ADMIN_USER_ID)

        assertThat(appListFlow).containsExactly(app)
        assertThat(appList).containsExactly(app)
    }

    @Test
@@ -164,11 +254,11 @@ class AppListRepositoryTest {
            packageName = "disabled"
            enabled = false
        }
        mockInstalledApplications(listOf(app))
        mockInstalledApplications(listOf(app), ADMIN_USER_ID)

        val appListFlow = repository.loadApps(userId = USER_ID, showInstantApps = false)
        val appList = repository.loadApps(userId = ADMIN_USER_ID)

        assertThat(appListFlow).isEmpty()
        assertThat(appList).isEmpty()
    }

    @Test
@@ -219,7 +309,11 @@ class AppListRepositoryTest {
        val app = IN_LAUNCHER_APP

        whenever(
            packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
            packageManager.queryIntentActivitiesAsUser(
                any(),
                any<ResolveInfoFlags>(),
                eq(ADMIN_USER_ID)
            )
        ).thenReturn(listOf(resolveInfoOf(packageName = app.packageName)))

        val showSystemPredicate = getShowSystemPredicate(showSystem = false)
@@ -229,12 +323,16 @@ class AppListRepositoryTest {

    @Test
    fun getSystemPackageNames_returnExpectedValues() = runTest {
        mockInstalledApplications(listOf(
                NORMAL_APP, INSTANT_APP, SYSTEM_APP, UPDATED_SYSTEM_APP, HOME_APP, IN_LAUNCHER_APP))
        mockInstalledApplications(
            apps = listOf(
                NORMAL_APP, INSTANT_APP, SYSTEM_APP, UPDATED_SYSTEM_APP, HOME_APP, IN_LAUNCHER_APP
            ),
            userId = ADMIN_USER_ID,
        )

        val systemPackageNames = AppListRepositoryUtil.getSystemPackageNames(
            context = context,
            userId = USER_ID,
            userId = ADMIN_USER_ID,
            showInstantApps = false,
        )

@@ -243,12 +341,13 @@ class AppListRepositoryTest {

    private suspend fun getShowSystemPredicate(showSystem: Boolean) =
        repository.showSystemPredicate(
            userIdFlow = flowOf(USER_ID),
            userIdFlow = flowOf(ADMIN_USER_ID),
            showSystemFlow = flowOf(showSystem),
        ).first()

    private companion object {
        const val USER_ID = 0
        const val ADMIN_USER_ID = 0
        const val MANAGED_PROFILE_USER_ID = 11

        val NORMAL_APP = ApplicationInfo().apply {
            packageName = "normal"
Loading