Merge "Add showInstantApps to AppListPage"
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
"presubmit": [
|
||||
{
|
||||
"name": "SpaLibTests"
|
||||
},
|
||||
{
|
||||
"name": "SpaPrivilegedLibTests"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,3 +45,9 @@ java_defaults {
|
||||
"-J-Xmx4G",
|
||||
],
|
||||
}
|
||||
|
||||
// Expose the srcs to tests, so the tests can access the internal classes.
|
||||
filegroup {
|
||||
name: "SpaPrivilegedLib_srcs",
|
||||
srcs: ["src/**/*.kt"],
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.content.pm.UserInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@@ -30,14 +29,25 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AppsRepository(context: Context) {
|
||||
/**
|
||||
* The config used to load the App List.
|
||||
*/
|
||||
internal data class AppListConfig(
|
||||
val userId: Int,
|
||||
val showInstantApps: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* The repository to load the App List data.
|
||||
*/
|
||||
internal class AppListRepository(context: Context) {
|
||||
private val packageManager = context.packageManager
|
||||
|
||||
fun loadApps(userInfoFlow: Flow<UserInfo>): Flow<List<ApplicationInfo>> = userInfoFlow
|
||||
fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow
|
||||
.map { loadApps(it) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private suspend fun loadApps(userInfo: UserInfo): List<ApplicationInfo> {
|
||||
private suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> {
|
||||
return coroutineScope {
|
||||
val hiddenSystemModulesDeferred = async {
|
||||
packageManager.getInstalledModules(0)
|
||||
@@ -50,11 +60,11 @@ class AppsRepository(context: Context) {
|
||||
PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
|
||||
)
|
||||
val installedApplicationsAsUser =
|
||||
packageManager.getInstalledApplicationsAsUser(flags, userInfo.id)
|
||||
packageManager.getInstalledApplicationsAsUser(flags, config.userId)
|
||||
|
||||
val hiddenSystemModules = hiddenSystemModulesDeferred.await()
|
||||
installedApplicationsAsUser.filter { app ->
|
||||
app.isInAppList(hiddenSystemModules)
|
||||
app.isInAppList(config.showInstantApps, hiddenSystemModules)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,9 +73,7 @@ class AppsRepository(context: Context) {
|
||||
userIdFlow: Flow<Int>,
|
||||
showSystemFlow: Flow<Boolean>,
|
||||
): Flow<(app: ApplicationInfo) -> Boolean> =
|
||||
userIdFlow.combine(showSystemFlow) { userId, showSystem ->
|
||||
showSystemPredicate(userId, showSystem)
|
||||
}
|
||||
userIdFlow.combine(showSystemFlow, ::showSystemPredicate)
|
||||
|
||||
private suspend fun showSystemPredicate(
|
||||
userId: Int,
|
||||
@@ -102,12 +110,15 @@ class AppsRepository(context: Context) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun ApplicationInfo.isInAppList(hiddenSystemModules: Set<String>) =
|
||||
when {
|
||||
packageName in hiddenSystemModules -> false
|
||||
enabled -> true
|
||||
enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
|
||||
else -> false
|
||||
}
|
||||
private fun ApplicationInfo.isInAppList(
|
||||
showInstantApps: Boolean,
|
||||
hiddenSystemModules: Set<String>,
|
||||
) = when {
|
||||
!showInstantApps && isInstantApp -> false
|
||||
packageName in hiddenSystemModules -> false
|
||||
enabled -> true
|
||||
isDisabledUntilUsed -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ package com.android.settingslib.spaprivileged.model.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.UserInfo
|
||||
import android.icu.text.Collator
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -48,28 +47,29 @@ internal data class AppListData<T : AppRecord>(
|
||||
internal class AppListViewModel<T : AppRecord>(
|
||||
application: Application,
|
||||
) : AndroidViewModel(application) {
|
||||
val userInfo = StateFlowBridge<UserInfo>()
|
||||
val appListConfig = StateFlowBridge<AppListConfig>()
|
||||
val listModel = StateFlowBridge<AppListModel<T>>()
|
||||
val showSystem = StateFlowBridge<Boolean>()
|
||||
val option = StateFlowBridge<Int>()
|
||||
val searchQuery = StateFlowBridge<String>()
|
||||
|
||||
private val appsRepository = AppsRepository(application)
|
||||
private val appListRepository = AppListRepository(application)
|
||||
private val appRepository = AppRepositoryImpl(application)
|
||||
private val collator = Collator.getInstance().freeze()
|
||||
private val labelMap = ConcurrentHashMap<String, String>()
|
||||
private val scope = viewModelScope + Dispatchers.Default
|
||||
|
||||
private val userIdFlow = userInfo.flow.map { it.id }
|
||||
private val userIdFlow = appListConfig.flow.map { it.userId }
|
||||
|
||||
private val recordListFlow = listModel.flow
|
||||
.flatMapLatest { it.transform(userIdFlow, appsRepository.loadApps(userInfo.flow)) }
|
||||
.flatMapLatest { it.transform(userIdFlow, appListRepository.loadApps(appListConfig.flow)) }
|
||||
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
private val systemFilteredFlow = appsRepository.showSystemPredicate(userIdFlow, showSystem.flow)
|
||||
.combine(recordListFlow) { showAppPredicate, recordList ->
|
||||
recordList.filter { showAppPredicate(it.app) }
|
||||
}
|
||||
private val systemFilteredFlow =
|
||||
appListRepository.showSystemPredicate(userIdFlow, showSystem.flow)
|
||||
.combine(recordListFlow) { showAppPredicate, recordList ->
|
||||
recordList.filter { showAppPredicate(it.app) }
|
||||
}
|
||||
|
||||
val appListDataFlow = option.flow.flatMapLatest(::filterAndSort)
|
||||
.combine(searchQuery.flow) { appListData, searchQuery ->
|
||||
|
||||
@@ -32,8 +32,8 @@ val ApplicationInfo.userHandle: UserHandle
|
||||
fun ApplicationInfo.hasFlag(flag: Int): Boolean = (flags and flag) > 0
|
||||
|
||||
/** Checks whether the application is disabled until used. */
|
||||
fun ApplicationInfo.isDisabledUntilUsed(): Boolean =
|
||||
enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
|
||||
val ApplicationInfo.isDisabledUntilUsed: Boolean
|
||||
get() = enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
|
||||
|
||||
/** Converts to the route string which used in navigation. */
|
||||
fun ApplicationInfo.toRoute() = "$packageName/$userId"
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.pm.UserInfo
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -33,6 +32,7 @@ import com.android.settingslib.spa.framework.compose.toState
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListConfig
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListData
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListViewModel
|
||||
@@ -41,17 +41,22 @@ import kotlinx.coroutines.Dispatchers
|
||||
|
||||
private const val TAG = "AppList"
|
||||
|
||||
/**
|
||||
* The template to render an App List.
|
||||
*
|
||||
* This UI element will take the remaining space on the screen to show the App List.
|
||||
*/
|
||||
@Composable
|
||||
internal fun <T : AppRecord> AppList(
|
||||
userInfo: UserInfo,
|
||||
appListConfig: AppListConfig,
|
||||
listModel: AppListModel<T>,
|
||||
showSystem: State<Boolean>,
|
||||
option: State<Int>,
|
||||
searchQuery: State<String>,
|
||||
appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
|
||||
) {
|
||||
LogCompositions(TAG, userInfo.id.toString())
|
||||
val appListData = loadAppEntries(userInfo, listModel, showSystem, option, searchQuery)
|
||||
LogCompositions(TAG, appListConfig.userId.toString())
|
||||
val appListData = loadAppEntries(appListConfig, listModel, showSystem, option, searchQuery)
|
||||
AppListWidget(appListData, listModel, appItem)
|
||||
}
|
||||
|
||||
@@ -85,14 +90,14 @@ private fun <T : AppRecord> AppListWidget(
|
||||
|
||||
@Composable
|
||||
private fun <T : AppRecord> loadAppEntries(
|
||||
userInfo: UserInfo,
|
||||
appListConfig: AppListConfig,
|
||||
listModel: AppListModel<T>,
|
||||
showSystem: State<Boolean>,
|
||||
option: State<Int>,
|
||||
searchQuery: State<String>,
|
||||
): State<AppListData<T>?> {
|
||||
val viewModel: AppListViewModel<T> = viewModel(key = userInfo.id.toString())
|
||||
viewModel.userInfo.setIfAbsent(userInfo)
|
||||
val viewModel: AppListViewModel<T> = viewModel(key = appListConfig.userId.toString())
|
||||
viewModel.appListConfig.setIfAbsent(appListConfig)
|
||||
viewModel.listModel.setIfAbsent(listModel)
|
||||
viewModel.showSystem.Sync(showSystem)
|
||||
viewModel.option.Sync(option)
|
||||
|
||||
@@ -36,14 +36,19 @@ import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
|
||||
import com.android.settingslib.spa.widget.scaffold.SettingsScaffold
|
||||
import com.android.settingslib.spa.widget.ui.Spinner
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListConfig
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.template.common.WorkProfilePager
|
||||
|
||||
/**
|
||||
* The full screen template for an App List page.
|
||||
*/
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListPage(
|
||||
title: String,
|
||||
listModel: AppListModel<T>,
|
||||
showInstantApps: Boolean = false,
|
||||
primaryUserOnly: Boolean = false,
|
||||
appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
|
||||
) {
|
||||
@@ -62,7 +67,10 @@ fun <T : AppRecord> AppListPage(
|
||||
val selectedOption = rememberSaveable { mutableStateOf(0) }
|
||||
Spinner(options, selectedOption.value) { selectedOption.value = it }
|
||||
AppList(
|
||||
userInfo = userInfo,
|
||||
appListConfig = AppListConfig(
|
||||
userId = userInfo.id,
|
||||
showInstantApps = showInstantApps,
|
||||
),
|
||||
listModel = listModel,
|
||||
showSystem = showSystem,
|
||||
option = selectedOption,
|
||||
|
||||
46
packages/SettingsLib/SpaPrivileged/tests/Android.bp
Normal file
46
packages/SettingsLib/SpaPrivileged/tests/Android.bp
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright (C) 2022 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["frameworks_base_license"],
|
||||
}
|
||||
|
||||
android_test {
|
||||
name: "SpaPrivilegedLibTests",
|
||||
certificate: "platform",
|
||||
platform_apis: true,
|
||||
test_suites: ["device-tests"],
|
||||
|
||||
srcs: [
|
||||
":SpaPrivilegedLib_srcs",
|
||||
"src/**/*.kt",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SpaPrivilegedLib",
|
||||
"androidx.compose.runtime_runtime",
|
||||
"androidx.compose.ui_ui-test-junit4",
|
||||
"androidx.compose.ui_ui-test-manifest",
|
||||
"androidx.test.ext.junit",
|
||||
"androidx.test.runner",
|
||||
"mockito-target-minus-junit4",
|
||||
"truth-prebuilt",
|
||||
],
|
||||
kotlincflags: [
|
||||
"-Xjvm-default=all",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
],
|
||||
}
|
||||
28
packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml
Normal file
28
packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.settingslib.spaprivileged.tests">
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:label="Tests for SpaPrivilegedLib"
|
||||
android:targetPackage="com.android.settingslib.spaprivileged.tests" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.model.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.ApplicationInfoFlags
|
||||
import android.content.pm.PackageManager.ResolveInfoFlags
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
|
||||
private const val USER_ID = 0
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppListRepositoryTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val mockito: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock
|
||||
private lateinit var context: Context
|
||||
|
||||
@Mock
|
||||
private lateinit var packageManager: PackageManager
|
||||
|
||||
private lateinit var repository: AppListRepository
|
||||
|
||||
private val normalApp = ApplicationInfo().apply {
|
||||
packageName = "normal"
|
||||
enabled = true
|
||||
}
|
||||
|
||||
private val instantApp = ApplicationInfo().apply {
|
||||
packageName = "instant"
|
||||
enabled = true
|
||||
privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
whenever(context.packageManager).thenReturn(packageManager)
|
||||
whenever(packageManager.getInstalledModules(anyInt())).thenReturn(emptyList())
|
||||
whenever(
|
||||
packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(USER_ID))
|
||||
).thenReturn(listOf(normalApp, instantApp))
|
||||
whenever(
|
||||
packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
|
||||
).thenReturn(emptyList())
|
||||
|
||||
repository = AppListRepository(context)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notShowInstantApps(): Unit = runBlocking {
|
||||
val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
|
||||
|
||||
val appListFlow = repository.loadApps(flowOf(appListConfig))
|
||||
|
||||
launch {
|
||||
val flowValues = mutableListOf<List<ApplicationInfo>>()
|
||||
appListFlow.toList(flowValues)
|
||||
assertThat(flowValues).hasSize(1)
|
||||
|
||||
assertThat(flowValues[0]).containsExactly(normalApp)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showInstantApps(): Unit = runBlocking {
|
||||
val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = true)
|
||||
|
||||
val appListFlow = repository.loadApps(flowOf(appListConfig))
|
||||
|
||||
launch {
|
||||
val flowValues = mutableListOf<List<ApplicationInfo>>()
|
||||
appListFlow.toList(flowValues)
|
||||
assertThat(flowValues).hasSize(1)
|
||||
|
||||
assertThat(flowValues[0]).containsExactly(normalApp, instantApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user