Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6f45b490 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "Add header for AppList"

parents 90e9de18 61311da7
Loading
Loading
Loading
Loading
+29 −17
Original line number Diff line number Diff line
@@ -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)
}
+10 −4
Original line number Diff line number Diff line
@@ -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,
                    state = AppListState(
                        showSystem = showSystem,
                        option = selectedOption,
                        searchQuery = searchQuery,
                    ),
                    header = header,
                    appItem = appItem,
                    bottomPadding = bottomPadding,
                )
+121 −0
Original line number Diff line number Diff line
/*
 * 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
}