From d4ea1cc454a8b23aa2017f862205d5fc6e2e49b3 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 10 Apr 2023 01:36:43 +0800 Subject: [PATCH] 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 --- .../model/app/AppListRepository.kt | 57 +++++- .../model/app/AppListViewModel.kt | 8 +- .../spaprivileged/template/app/AppList.kt | 1 + .../spaprivileged/template/app/AppListPage.kt | 2 + .../model/app/AppListRepositoryTest.kt | 163 ++++++++++++++---- .../model/app/AppListViewModelTest.kt | 12 +- .../spaprivileged/template/app/AppListTest.kt | 6 +- 7 files changed, 206 insertions(+), 43 deletions(-) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index 5342def0003d4..2c3e58ece8e0e 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -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 + suspend fun loadApps( + userId: Int, + showInstantApps: Boolean = false, + matchAnyUserForAdmin: Boolean = false, + ): List /** 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 = 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 { + 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, showSystemFlow: Flow, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt index 889604209b08b..bd99ebdfa3e59 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt @@ -80,12 +80,15 @@ internal open class AppListViewModelImpl( 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( fun reloadApps() { scope.launch { - appsStateFlow.value = appListRepository.loadApps(userId, showInstantApps) + appsStateFlow.value = + appListRepository.loadApps(userId, showInstantApps, matchAnyUserForAdmin) } } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index f4a6b59d4c4b8..066db3475b364 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -62,6 +62,7 @@ private const val CONTENT_TYPE_HEADER = "header" data class AppListConfig( val userIds: List, val showInstantApps: Boolean, + val matchAnyUserForAdmin: Boolean, ) data class AppListState( diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 2ebbe8aab809e..89bfa0eb646b9 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -38,6 +38,7 @@ fun AppListPage( title: String, listModel: AppListModel, showInstantApps: Boolean = false, + matchAnyUserForAdmin: Boolean = false, primaryUserOnly: Boolean = false, noItemMessage: String? = null, moreOptions: @Composable MoreOptionsScope.() -> Unit = {}, @@ -59,6 +60,7 @@ fun AppListPage( config = AppListConfig( userIds = userGroup.userInfos.map { it.id }, showInstantApps = showInstantApps, + matchAnyUserForAdmin = matchAnyUserForAdmin, ), listModel = listModel, state = AppListState( diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index 57972edc6d3a5..b732a6a4495e0 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -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(), eq(USER_ID)) + packageManager.queryIntentActivitiesAsUser(any(), any(), 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) { + private fun mockInstalledApplications(apps: List, userId: Int) { whenever( - packageManager.getInstalledApplicationsAsUser(any(), eq(USER_ID)) + packageManager.getInstalledApplicationsAsUser(any(), 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(), eq(USER_ID)) + packageManager.queryIntentActivitiesAsUser( + any(), + any(), + 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" diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt index 4f0cddef078bf..36d9db5ef251f 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt @@ -85,7 +85,11 @@ class AppListViewModelTest { } private object FakeAppListRepository : AppListRepository { - override suspend fun loadApps(userId: Int, showInstantApps: Boolean) = listOf(APP) + override suspend fun loadApps( + userId: Int, + showInstantApps: Boolean, + matchAnyUserForAdmin: Boolean, + ) = listOf(APP) override fun showSystemPredicate( userIdFlow: Flow, @@ -112,7 +116,11 @@ class AppListViewModelTest { const val USER_ID = 0 const val PACKAGE_NAME = "package.name" const val LABEL = "Label" - val CONFIG = AppListConfig(userIds = listOf(USER_ID), showInstantApps = false) + val CONFIG = AppListConfig( + userIds = listOf(USER_ID), + showInstantApps = false, + matchAnyUserForAdmin = false, + ) val APP = ApplicationInfo().apply { packageName = PACKAGE_NAME } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt index a99d02de0df06..241a134b6a766 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt @@ -114,7 +114,11 @@ class AppListTest { ) { composeTestRule.setContent { AppListInput( - config = AppListConfig(userIds = listOf(USER_ID), showInstantApps = false), + config = AppListConfig( + userIds = listOf(USER_ID), + showInstantApps = false, + matchAnyUserForAdmin = false, + ), listModel = TestAppListModel(enableGrouping = enableGrouping), state = AppListState( showSystem = false.toState(),