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(),