diff options
3 files changed, 160 insertions, 21 deletions
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<Boolean>, + val option: State<Int>, + val searchQuery: State<String>, +) /** * The template to render an App List. @@ -49,23 +56,26 @@ private const val TAG = "AppList" */ @Composable internal fun <T : AppRecord> AppList( - appListConfig: AppListConfig, + config: AppListConfig, listModel: AppListModel<T>, - showSystem: State<Boolean>, - option: State<Int>, - searchQuery: State<String>, + state: AppListState, + header: @Composable () -> Unit, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, bottomPadding: Dp, + appListDataSupplier: @Composable () -> State<AppListData<T>?> = { + 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 <T : AppRecord> AppListWidget( appListData: State<AppListData<T>?>, listModel: AppListModel<T>, + header: @Composable () -> Unit, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, bottomPadding: Dp, ) { @@ -81,6 +91,10 @@ private fun <T : AppRecord> 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 <T : AppRecord> AppListWidget( } @Composable -private fun <T : AppRecord> loadAppEntries( - appListConfig: AppListConfig, +private fun <T : AppRecord> loadAppListData( + config: AppListConfig, listModel: AppListModel<T>, - showSystem: State<Boolean>, - option: State<Int>, - searchQuery: State<String>, + state: AppListState, ): State<AppListData<T>?> { - val viewModel: AppListViewModel<T> = viewModel(key = appListConfig.userId.toString()) - viewModel.appListConfig.setIfAbsent(appListConfig) + val viewModel: AppListViewModel<T> = 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 <T : AppRecord> AppListPage( @@ -44,6 +46,7 @@ fun <T : AppRecord> AppListPage( listModel: AppListModel<T>, showInstantApps: Boolean = false, primaryUserOnly: Boolean = false, + header: @Composable () -> Unit = {}, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, ) { val showSystem = rememberSaveable { mutableStateOf(false) } @@ -59,14 +62,17 @@ fun <T : AppRecord> 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<AppEntry<TestAppRecord>>, + ) { + 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<TestAppRecord> { + override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = + appListFlow.asyncMapItem { TestAppRecord(it) } + + @Composable + override fun getSummary(option: Int, record: TestAppRecord) = null + + override fun filter( + userIdFlow: Flow<Int>, + option: Int, + recordListFlow: Flow<List<TestAppRecord>>, + ) = recordListFlow +} |