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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user