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
This commit is contained in:
Chaohui Wang
2022-11-02 19:19:29 +08:00
parent 93c598059b
commit 61311da7f2
3 changed files with 160 additions and 21 deletions

View File

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

View File

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

View File

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