From 61311da7f2c618ef772da452dcc31c1a0a25753d Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Wed, 2 Nov 2022 19:19:29 +0800 Subject: Add header for AppList Default no header, when added, displayed before all the apps. Also add unit tests for AppList. Bug: 235727273 Test: Unit test Change-Id: Ie4f45c4334d110a5ccd5d7d6cedc95d90bb10134 --- .../spaprivileged/template/app/AppList.kt | 46 +++++--- .../spaprivileged/template/app/AppListPage.kt | 14 ++- .../spaprivileged/template/app/AppListTest.kt | 121 +++++++++++++++++++++ 3 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt 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 3cd8378b8960..9d6b311a33cb 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 @@ -41,6 +41,13 @@ import com.android.settingslib.spaprivileged.model.app.AppRecord import kotlinx.coroutines.Dispatchers private const val TAG = "AppList" +private const val CONTENT_TYPE_HEADER = "header" + +internal data class AppListState( + val showSystem: State, + val option: State, + val searchQuery: State, +) /** * The template to render an App List. @@ -49,23 +56,26 @@ private const val TAG = "AppList" */ @Composable internal fun AppList( - appListConfig: AppListConfig, + config: AppListConfig, listModel: AppListModel, - showSystem: State, - option: State, - searchQuery: State, + state: AppListState, + header: @Composable () -> Unit, appItem: @Composable (itemState: AppListItemModel) -> Unit, bottomPadding: Dp, + appListDataSupplier: @Composable () -> State?> = { + loadAppListData(config, listModel, state) + }, ) { - LogCompositions(TAG, appListConfig.userId.toString()) - val appListData = loadAppEntries(appListConfig, listModel, showSystem, option, searchQuery) - AppListWidget(appListData, listModel, appItem, bottomPadding) + LogCompositions(TAG, config.userId.toString()) + val appListData = appListDataSupplier() + AppListWidget(appListData, listModel, header, appItem, bottomPadding) } @Composable private fun AppListWidget( appListData: State?>, listModel: AppListModel, + header: @Composable () -> Unit, appItem: @Composable (itemState: AppListItemModel) -> Unit, bottomPadding: Dp, ) { @@ -81,6 +91,10 @@ private fun AppListWidget( state = rememberLazyListStateAndHideKeyboardWhenStartScroll(), contentPadding = PaddingValues(bottom = bottomPadding), ) { + item(contentType = CONTENT_TYPE_HEADER) { + header() + } + items(count = list.size, key = { option to list[it].record.app.packageName }) { val appEntry = list[it] val summary = listModel.getSummary(option, appEntry.record) ?: "".toState() @@ -94,19 +108,17 @@ private fun AppListWidget( } @Composable -private fun loadAppEntries( - appListConfig: AppListConfig, +private fun loadAppListData( + config: AppListConfig, listModel: AppListModel, - showSystem: State, - option: State, - searchQuery: State, + state: AppListState, ): State?> { - val viewModel: AppListViewModel = viewModel(key = appListConfig.userId.toString()) - viewModel.appListConfig.setIfAbsent(appListConfig) + val viewModel: AppListViewModel = viewModel(key = config.userId.toString()) + viewModel.appListConfig.setIfAbsent(config) viewModel.listModel.setIfAbsent(listModel) - viewModel.showSystem.Sync(showSystem) - viewModel.option.Sync(option) - viewModel.searchQuery.Sync(searchQuery) + viewModel.showSystem.Sync(state.showSystem) + viewModel.option.Sync(state.option) + viewModel.searchQuery.Sync(state.searchQuery) return viewModel.appListDataFlow.collectAsState(null, Dispatchers.Default) } 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 29533679d9c1..388a7d87a1bd 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 @@ -37,6 +37,8 @@ import com.android.settingslib.spaprivileged.template.common.WorkProfilePager /** * The full screen template for an App List page. + * + * @param header the description header appears before all the applications. */ @Composable fun AppListPage( @@ -44,6 +46,7 @@ fun AppListPage( listModel: AppListModel, showInstantApps: Boolean = false, primaryUserOnly: Boolean = false, + header: @Composable () -> Unit = {}, appItem: @Composable (itemState: AppListItemModel) -> Unit, ) { val showSystem = rememberSaveable { mutableStateOf(false) } @@ -59,14 +62,17 @@ fun AppListPage( val selectedOption = rememberSaveable { mutableStateOf(0) } Spinner(options, selectedOption.value) { selectedOption.value = it } AppList( - appListConfig = AppListConfig( + config = AppListConfig( userId = userInfo.id, showInstantApps = showInstantApps, ), listModel = listModel, - showSystem = showSystem, - option = selectedOption, - searchQuery = searchQuery, + state = AppListState( + showSystem = showSystem, + option = selectedOption, + searchQuery = searchQuery, + ), + header = header, appItem = appItem, bottomPadding = bottomPadding, ) 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 new file mode 100644 index 000000000000..80c4eac9b98f --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt @@ -0,0 +1,121 @@ +/* + * 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.template.app + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.icu.text.CollationKey +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.dp +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.framework.compose.toState +import com.android.settingslib.spa.framework.util.asyncMapItem +import com.android.settingslib.spaprivileged.R +import com.android.settingslib.spaprivileged.model.app.AppEntry +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.AppRecord +import kotlinx.coroutines.flow.Flow +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AppListTest { + @get:Rule + val composeTestRule = createComposeRule() + + private var context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun whenNoApps() { + setContent(appEntries = emptyList()) + + composeTestRule.onNodeWithText(context.getString(R.string.no_applications)) + .assertIsDisplayed() + } + + @Test + fun couldShowAppItem() { + setContent(appEntries = listOf(APP_ENTRY)) + + composeTestRule.onNodeWithText(APP_ENTRY.label).assertIsDisplayed() + } + + @Test + fun couldShowHeader() { + setContent(header = { Text(HEADER) }, appEntries = listOf(APP_ENTRY)) + + composeTestRule.onNodeWithText(HEADER).assertIsDisplayed() + } + + private fun setContent( + header: @Composable () -> Unit = {}, + appEntries: List>, + ) { + composeTestRule.setContent { + AppList( + config = AppListConfig(userId = USER_ID, showInstantApps = false), + listModel = TestAppListModel(), + state = AppListState( + showSystem = false.toState(), + option = 0.toState(), + searchQuery = "".toState(), + ), + header = header, + appItem = { AppListItem(it) {} }, + bottomPadding = 0.dp, + appListDataSupplier = { + stateOf(AppListData(appEntries, option = 0)) + } + ) + } + } + + private companion object { + const val USER_ID = 0 + const val HEADER = "Header" + val APP_ENTRY = AppEntry( + record = TestAppRecord(ApplicationInfo()), + label = "AAA", + labelCollationKey = CollationKey("", byteArrayOf()), + ) + } +} + +private data class TestAppRecord(override val app: ApplicationInfo) : AppRecord + +private class TestAppListModel : AppListModel { + override fun transform(userIdFlow: Flow, appListFlow: Flow>) = + appListFlow.asyncMapItem { TestAppRecord(it) } + + @Composable + override fun getSummary(option: Int, record: TestAppRecord) = null + + override fun filter( + userIdFlow: Flow, + option: Int, + recordListFlow: Flow>, + ) = recordListFlow +} -- cgit v1.2.3-59-g8ed1b