Merge "Support App List match any user for admin" into udc-dev
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Int>,
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user