From dcfda4093926223dcfae19d4ffd9da5bdd4016ed Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 27 Sep 2022 11:40:33 +0800 Subject: [PATCH] Add showInstantApps to AppListPage Default false, which hide the instant apps from App List. Also setup the SpaPrivilegedLibTests. Bug: 235727273 Test: Manual with Settings App Change-Id: I0d3dafc5c61b7b8d69cee972bd5f985d8b99ca10 --- packages/SettingsLib/Spa/TEST_MAPPING | 3 + packages/SettingsLib/SpaPrivileged/Android.bp | 6 + ...AppsRepository.kt => AppListRepository.kt} | 43 ++++--- .../model/app/AppListViewModel.kt | 18 +-- .../app/{Apps.kt => ApplicationInfos.kt} | 4 +- .../spaprivileged/template/app/AppList.kt | 19 +-- .../spaprivileged/template/app/AppListPage.kt | 10 +- .../SpaPrivileged/tests/Android.bp | 46 +++++++ .../SpaPrivileged/tests/AndroidManifest.xml | 28 +++++ .../model/app/AppListRepositoryTest.kt | 113 ++++++++++++++++++ 10 files changed, 255 insertions(+), 35 deletions(-) rename packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/{AppsRepository.kt => AppListRepository.kt} (80%) rename packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/{Apps.kt => ApplicationInfos.kt} (90%) create mode 100644 packages/SettingsLib/SpaPrivileged/tests/Android.bp create mode 100644 packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml create mode 100644 packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt diff --git a/packages/SettingsLib/Spa/TEST_MAPPING b/packages/SettingsLib/Spa/TEST_MAPPING index ef3db4aef66d3..b4b65d4c0ddca 100644 --- a/packages/SettingsLib/Spa/TEST_MAPPING +++ b/packages/SettingsLib/Spa/TEST_MAPPING @@ -2,6 +2,9 @@ "presubmit": [ { "name": "SpaLibTests" + }, + { + "name": "SpaPrivilegedLibTests" } ] } diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp index 082ce97e3273d..e7e37e4181854 100644 --- a/packages/SettingsLib/SpaPrivileged/Android.bp +++ b/packages/SettingsLib/SpaPrivileged/Android.bp @@ -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"], +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppsRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt similarity index 80% rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppsRepository.kt rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index bb94b33bfa28a..ee8900352cf20 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppsRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.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): Flow> = userInfoFlow + fun loadApps(configFlow: Flow): Flow> = configFlow .map { loadApps(it) } .flowOn(Dispatchers.Default) - private suspend fun loadApps(userInfo: UserInfo): List { + private suspend fun loadApps(config: AppListConfig): List { 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, showSystemFlow: Flow, ): 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) = - 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, + ) = when { + !showInstantApps && isInstantApp -> false + packageName in hiddenSystemModules -> false + enabled -> true + isDisabledUntilUsed -> true + else -> false + } } } 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 9265158b3b4a6..1e487daa36fb7 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 @@ -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( internal class AppListViewModel( application: Application, ) : AndroidViewModel(application) { - val userInfo = StateFlowBridge() + val appListConfig = StateFlowBridge() val listModel = StateFlowBridge>() val showSystem = StateFlowBridge() val option = StateFlowBridge() val searchQuery = StateFlowBridge() - 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() 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 -> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt similarity index 90% rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt index 96e3e77e93223..f9f75fb55d398 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt @@ -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" 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 315dc5d6ff700..6318b4e9c1862 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 @@ -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 AppList( - userInfo: UserInfo, + appListConfig: AppListConfig, listModel: AppListModel, showSystem: State, option: State, searchQuery: State, appItem: @Composable (itemState: AppListItemModel) -> 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 AppListWidget( @Composable private fun loadAppEntries( - userInfo: UserInfo, + appListConfig: AppListConfig, listModel: AppListModel, showSystem: State, option: State, searchQuery: State, ): State?> { - val viewModel: AppListViewModel = viewModel(key = userInfo.id.toString()) - viewModel.userInfo.setIfAbsent(userInfo) + val viewModel: AppListViewModel = viewModel(key = appListConfig.userId.toString()) + viewModel.appListConfig.setIfAbsent(appListConfig) viewModel.listModel.setIfAbsent(listModel) viewModel.showSystem.Sync(showSystem) viewModel.option.Sync(option) 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 d537ec258cade..fb03d2c2e2cbe 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 @@ -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 AppListPage( title: String, listModel: AppListModel, + showInstantApps: Boolean = false, primaryUserOnly: Boolean = false, appItem: @Composable (itemState: AppListItemModel) -> Unit, ) { @@ -62,7 +67,10 @@ fun 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, diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp new file mode 100644 index 0000000000000..940a1fed817be --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp @@ -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", + ], +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml new file mode 100644 index 0000000000000..c4f490ed398b0 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + 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 new file mode 100644 index 0000000000000..c010c68f37a45 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -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(), eq(USER_ID)) + ).thenReturn(listOf(normalApp, instantApp)) + whenever( + packageManager.queryIntentActivitiesAsUser(any(), any(), 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>() + 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>() + appListFlow.toList(flowValues) + assertThat(flowValues).hasSize(1) + + assertThat(flowValues[0]).containsExactly(normalApp, instantApp) + } + } +}