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

Commit 9b603e65 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[expressive design] Migrate AppList Page." into main

parents 4bf8a0ae 926a5282
Loading
Loading
Loading
Loading
+13 −1
Original line number Diff line number Diff line
@@ -17,8 +17,12 @@
package com.android.settingslib.spa.gallery.ui

import android.os.Bundle
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -30,6 +34,7 @@ import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category
import com.android.settingslib.spa.widget.ui.LazyCategory

private const val TITLE = "Sample Category"

@@ -65,7 +70,7 @@ object CategoryPageProvider : SettingsPageProvider {
        )
        entryList.add(
            SettingsEntryBuilder.create("Preference 3", owner)
                .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 3") }
                .setMacro { SimplePreferenceMacro(title = "Preference 3", summary = "Summary 3") }
                .build()
        )
        entryList.add(
@@ -88,6 +93,13 @@ object CategoryPageProvider : SettingsPageProvider {
                entries[2].UiLayout()
                entries[3].UiLayout()
            }
            Column(Modifier.height(200.dp)) {
                LazyCategory(
                    list = entries,
                    entry = { index: Int -> @Composable { entries[index].UiLayout() } },
                    title = { index: Int -> if (index == 0 || index == 2) "LazyCategory" else null },
                ) {}
            }
        }
    }
}
+57 −0
Original line number Diff line number Diff line
@@ -19,8 +19,13 @@ package com.android.settingslib.spa.widget.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material3.MaterialTheme
@@ -34,6 +39,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsShape
@@ -98,6 +104,57 @@ fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit)
    }
}

/**
 * A container that is used to group items with lazy loading.
 *
 * @param list The list of items to display.
 * @param entry The entry for each list item according to its index in list.
 * @param key Optional. The key for each item in list to provide unique item identifiers, making
 * the list more efficient.
 * @param title Optional. Category title for each item or each group of items in the list. It
 * should be decided by the index.
 * @param bottomPadding Optional. Bottom outside padding of the category.
 * @param state Optional. State of LazyList.
 * @param content Optional. Content to be shown at the top of the category.
 */

@Composable
fun LazyCategory(
    list: List<Any>,
    entry: (Int) -> @Composable () -> Unit,
    key: ((Int) -> Any)? = null,
    title: ((Int) -> String?)? = null,
    bottomPadding: Dp = SettingsDimension.paddingSmall,
    state: LazyListState = rememberLazyListState(),
    content: @Composable () -> Unit,
) {
    Column(
        Modifier.padding(
                PaddingValues(
                    start = SettingsDimension.paddingLarge,
                    end = SettingsDimension.paddingLarge,
                    top = SettingsDimension.paddingSmall,
                    bottom = bottomPadding,
                )
            )
            .clip(SettingsShape.CornerMedium2)
    ) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny),
            state = state,
        ) {
            item { content() }

            items(count = list.size, key = key) {
                title?.invoke(it)?.let { title -> CategoryTitle(title) }
                val entryPreference = entry(it)
                entryPreference()
            }
        }
    }
}

@Preview
@Composable
private fun CategoryPreview() {
+53 −13
Original line number Diff line number Diff line
@@ -16,10 +16,16 @@

package com.android.settingslib.spa.widget.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -30,14 +36,11 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CategoryTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    @get:Rule val composeTestRule = createComposeRule()

    @Test
    fun categoryTitle() {
        composeTestRule.setContent {
            CategoryTitle(title = "CategoryTitle")
        }
        composeTestRule.setContent { CategoryTitle(title = "CategoryTitle") }

        composeTestRule.onNodeWithText("CategoryTitle").assertIsDisplayed()
    }
@@ -46,12 +49,14 @@ class CategoryTest {
    fun category_hasContent_titleDisplayed() {
        composeTestRule.setContent {
            Category(title = "CategoryTitle") {
                Preference(remember {
                Preference(
                    remember {
                        object : PreferenceModel {
                            override val title = "Some Preference"
                            override val summary = { "Some summary" }
                        }
                })
                    }
                )
            }
        }

@@ -60,10 +65,45 @@ class CategoryTest {

    @Test
    fun category_noContent_titleNotDisplayed() {
        composeTestRule.setContent {
            Category(title = "CategoryTitle") {}
        }
        composeTestRule.setContent { Category(title = "CategoryTitle") {} }

        composeTestRule.onNodeWithText("CategoryTitle").assertDoesNotExist()
    }

    @Test
    fun lazyCategory_content_displayed() {
        composeTestRule.setContent { TestLazyCategory() }

        composeTestRule.onNodeWithText("text").assertExists()
    }

    @Test
    fun lazyCategory_title_displayed() {
        composeTestRule.setContent { TestLazyCategory() }

        composeTestRule.onNodeWithText("LazyCategory 0").assertExists()
        composeTestRule.onNodeWithText("LazyCategory 1").assertDoesNotExist()
    }
}

@Composable
private fun TestLazyCategory() {
    val list: List<PreferenceModel> =
        listOf(
            object : PreferenceModel {
                override val title = "title"
            },
            object : PreferenceModel {
                override val title = "title"
            },
        )
    Column(Modifier.height(200.dp)) {
        LazyCategory(
            list = list,
            entry = { index: Int -> @Composable { Preference(list[index]) } },
            title = { index: Int -> if (index == 0) "LazyCategory $index" else null },
        ) {
            Text("text")
        }
    }
}
+57 −36
Original line number Diff line number Diff line
@@ -37,7 +37,9 @@ import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.framework.compose.LogCompositions
import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
import com.android.settingslib.spa.widget.ui.CategoryTitle
import com.android.settingslib.spa.widget.ui.LazyCategory
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
import com.android.settingslib.spa.widget.ui.Spinner
import com.android.settingslib.spa.widget.ui.SpinnerOption
@@ -55,19 +57,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
private const val TAG = "AppList"
private const val CONTENT_TYPE_HEADER = "header"

/**
 * The config used to load the App List.
 */
/** The config used to load the App List. */
data class AppListConfig(
    val userIds: List<Int>,
    val showInstantApps: Boolean,
    val matchAnyUserForAdmin: Boolean,
)

data class AppListState(
    val showSystem: () -> Boolean,
    val searchQuery: () -> String,
)
data class AppListState(val showSystem: () -> Boolean, val searchQuery: () -> String)

data class AppListInput<T : AppRecord>(
    val config: AppListConfig,
@@ -90,7 +87,7 @@ fun <T : AppRecord> AppListInput<T>.AppList() {

@Composable
internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
    viewModelSupplier: @Composable () -> IAppListViewModel<T>,
    viewModelSupplier: @Composable () -> IAppListViewModel<T>
) {
    LogCompositions(TAG, config.userIds.toString())
    val viewModel = viewModelSupplier()
@@ -125,7 +122,7 @@ private fun <T : AppRecord> AppListModel<T>.AppListWidget(
    appListData: State<AppListData<T>?>,
    header: @Composable () -> Unit,
    bottomPadding: Dp,
    noItemMessage: String?
    noItemMessage: String?,
) {
    val timeMeasurer = rememberTimeMeasurer(TAG)
    appListData.value?.let { (list, option) ->
@@ -135,14 +132,33 @@ private fun <T : AppRecord> AppListModel<T>.AppListWidget(
            PlaceholderTitle(noItemMessage ?: stringResource(R.string.no_applications))
            return
        }
        if (isSpaExpressiveEnabled) {
            LazyCategory(
                list = list,
                entry = { index: Int ->
                    @Composable {
                        val appEntry = list[index]
                        val summary = getSummary(option, appEntry.record) ?: { "" }
                        remember(appEntry) {
                                AppListItemModel(appEntry.record, appEntry.label, summary)
                            }
                            .AppItem()
                    }
                },
                key = { index: Int -> list[index].record.itemKey(option) },
                title = { index: Int -> getGroupTitle(option, list[index].record) },
                bottomPadding = bottomPadding,
                state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
            ) {
                header()
            }
        } else {
            LazyColumn(
                modifier = Modifier.fillMaxSize(),
                state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
                contentPadding = PaddingValues(bottom = bottomPadding),
            ) {
            item(contentType = CONTENT_TYPE_HEADER) {
                header()
            }
                item(contentType = CONTENT_TYPE_HEADER) { header() }

                items(count = list.size, key = { list[it].record.itemKey(option) }) {
                    remember(list) { getGroupTitleIfFirst(option, list, it) }
@@ -152,21 +168,23 @@ private fun <T : AppRecord> AppListModel<T>.AppListWidget(
                    val summary = getSummary(option, appEntry.record) ?: { "" }
                    remember(appEntry) {
                            AppListItemModel(appEntry.record, appEntry.label, summary)
                }.AppItem()
                        }
                        .AppItem()
                }
            }
        }
    }
}

private fun <T : AppRecord> T.itemKey(option: Int) =
    listOf(option, app.packageName, app.userId)
private fun <T : AppRecord> T.itemKey(option: Int) = listOf(option, app.packageName, app.userId)

/** Returns group title if this is the first item of the group. */
private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst(
    option: Int,
    list: List<AppEntry<T>>,
    index: Int,
): String? = getGroupTitle(option, list[index].record)?.takeIf {
): String? =
    getGroupTitle(option, list[index].record)?.takeIf {
        index == 0 || it != getGroupTitle(option, list[index - 1].record)
    }

@@ -183,7 +201,8 @@ private fun <T : AppRecord> rememberViewModel(
    viewModel.searchQuery.Sync(state.searchQuery)

    LifecycleEffect(onStart = { viewModel.reloadApps() })
    val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
    val intentFilter =
        IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
            addAction(Intent.ACTION_PACKAGE_REMOVED)
            addAction(Intent.ACTION_PACKAGE_CHANGED)
            addDataScheme("package")
@@ -192,7 +211,9 @@ private fun <T : AppRecord> rememberViewModel(
        DisposableBroadcastReceiverAsUser(
            intentFilter = intentFilter,
            userHandle = UserHandle.of(userId),
        ) { viewModel.reloadApps() }
        ) {
            viewModel.reloadApps()
        }
    }
    return viewModel
}