Merge "Add showInstantApps to AppListPage"

This commit is contained in:
Chaohui Wang
2022-09-28 02:29:15 +00:00
committed by Android (Google) Code Review
10 changed files with 255 additions and 35 deletions

View File

@@ -2,6 +2,9 @@
"presubmit": [
{
"name": "SpaLibTests"
},
{
"name": "SpaPrivilegedLibTests"
}
]
}

View File

@@ -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"],
}

View File

@@ -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
}
}
}

View File

@@ -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 ->

View File

@@ -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"

View File

@@ -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)

View File

@@ -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,

View 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",
],
}

View 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>

View File

@@ -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)
}
}
}