Defines SettingsEntry and use it in ArgumentPage

Add buildEntry API in SPP.
Implement buildEntry API in ArgumentPage.
Add helper class SettingsEntryBuilder to build SettingsEntry.
Add helper class to control the arguments of Settings pages.

Bug: 244122804
Test: manual - build Spa gallery

Change-Id: I6158d525c8aa92381d049d95c587ddd8764eda93
This commit is contained in:
Zekan Qian
2022-08-29 13:19:11 +08:00
parent a4e040444d
commit df0103f882
4 changed files with 365 additions and 31 deletions

View File

@@ -21,6 +21,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.android.settingslib.spa.framework.common.PageArguments
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.toState
@@ -41,41 +45,72 @@ object ArgumentPageProvider : SettingsPageProvider {
navArgument(INT_PARAM_NAME) { type = NavType.IntType },
)
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val pageArgs = PageArguments(ArgumentPageProvider.arguments, arguments)
if (!pageArgs.isValid()) return emptyList()
val owner = SettingsPageBuilder.create(name, pageArgs.normalize()).build()
val entryList = mutableListOf<SettingsEntry>()
val stringParamEntry = SettingsEntryBuilder.create("string_param", owner)
stringParamEntry.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "String param value"
override val summary = pageArgs.getStringArg(STRING_PARAM_NAME)!!.toState()
})
}
entryList.add(stringParamEntry.build())
val intParamEntry = SettingsEntryBuilder.create("int_param", owner)
intParamEntry.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Int param value"
override val summary = pageArgs.getIntArg(INT_PARAM_NAME)!!.toString().toState()
})
}
entryList.add(intParamEntry.build())
val entryFoo = buildInjectEntry(pageArgs.buildNextArg("foo"))
val entryBar = buildInjectEntry(pageArgs.buildNextArg("bar"))
if (entryFoo != null) entryList.add(entryFoo.setLink(fromPage = owner).build())
if (entryBar != null) entryList.add(entryBar.setLink(fromPage = owner).build())
return entryList
}
private fun buildInjectEntry(arguments: Bundle?): SettingsEntryBuilder? {
val pageArgs = PageArguments(ArgumentPageProvider.arguments, arguments)
if (!pageArgs.isValid()) return null
val seBuilder = SettingsEntryBuilder.createLinkTo("injection", name, pageArgs.normalize())
seBuilder.setIsAllowSearch(false)
seBuilder.setUiLayoutFn {
val summaryArray = listOf(
"$STRING_PARAM_NAME=" + pageArgs.getStringArg(STRING_PARAM_NAME)!!,
"$INT_PARAM_NAME=" + pageArgs.getIntArg(INT_PARAM_NAME)!!
)
Preference(object : PreferenceModel {
override val title = TITLE
override val summary = summaryArray.joinToString(", ").toState()
override val onClick = navigator(name + pageArgs.navLink())
})
}
return seBuilder
}
@Composable
override fun Page(arguments: Bundle?) {
ArgumentPage(
stringParam = arguments!!.getString(STRING_PARAM_NAME, "default"),
intParam = arguments.getInt(INT_PARAM_NAME),
)
RegularScaffold(title = TITLE) {
for (entry in buildEntry(arguments)) {
entry.uiLayout()
}
}
}
@Composable
fun EntryItem(stringParam: String, intParam: Int) {
Preference(object : PreferenceModel {
override val title = TITLE
override val summary =
"$STRING_PARAM_NAME=$stringParam, $INT_PARAM_NAME=$intParam".toState()
override val onClick = navigator("$name/$stringParam/$intParam")
})
}
}
@Composable
fun ArgumentPage(stringParam: String, intParam: Int) {
RegularScaffold(title = TITLE) {
Preference(object : PreferenceModel {
override val title = "String param value"
override val summary = stringParam.toState()
})
Preference(object : PreferenceModel {
override val title = "Int param value"
override val summary = intParam.toString().toState()
})
ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = intParam + 1)
ArgumentPageProvider.EntryItem(stringParam = "bar", intParam = intParam + 1)
buildInjectEntry(buildArgument(stringParam, intParam))?.build()?.uiLayout?.let { it() }
}
}
@@ -83,6 +118,26 @@ fun ArgumentPage(stringParam: String, intParam: Int) {
@Composable
private fun ArgumentPagePreview() {
SettingsTheme {
ArgumentPage(stringParam = "foo", intParam = 0)
ArgumentPageProvider.Page(buildArgument(stringParam = "foo", intParam = 0))
}
}
private fun PageArguments.isValid(): Boolean {
val stringParam = getStringArg(STRING_PARAM_NAME)
return (stringParam != null && listOf("foo", "bar").contains(stringParam))
}
private fun PageArguments.buildNextArg(stringParam: String): Bundle {
val intParam = getIntArg(INT_PARAM_NAME)
return if (intParam == null)
buildArgument(stringParam)
else
buildArgument(stringParam, intParam + 1)
}
private fun buildArgument(stringParam: String, intParam: Int? = null): Bundle {
val args = Bundle()
args.putString(STRING_PARAM_NAME, stringParam)
if (intParam != null) args.putInt(INT_PARAM_NAME, intParam)
return args
}

View File

@@ -0,0 +1,74 @@
/*
* 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.spa.framework.common
import android.os.Bundle
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavType
class PageArguments(
private val navArgsDef: List<NamedNavArgument>,
private val rawArgs: Bundle?
) {
fun normalize(): Bundle {
if (rawArgs == null) return Bundle.EMPTY
val normArgs = Bundle()
for (navArg in navArgsDef) {
when (navArg.argument.type) {
NavType.StringType -> {
val value = rawArgs.getString(navArg.name)
if (value != null) normArgs.putString(navArg.name, value)
}
NavType.IntType -> {
if (rawArgs.containsKey(navArg.name))
normArgs.putInt(navArg.name, rawArgs.getInt(navArg.name))
}
}
}
return normArgs
}
fun navLink(): String{
if (rawArgs == null) return ""
val argsArray = mutableListOf<String>()
for (navArg in navArgsDef) {
when (navArg.argument.type) {
NavType.StringType -> {
argsArray.add(rawArgs.getString(navArg.name, ""))
}
NavType.IntType -> {
argsArray.add(rawArgs.getInt(navArg.name).toString())
}
}
}
return argsArray.joinToString("") {arg -> "/$arg" }
}
fun getStringArg(key: String): String? {
if (rawArgs != null && rawArgs.containsKey(key)) {
return rawArgs.getString(key)
}
return null
}
fun getIntArg(key: String): Int? {
if (rawArgs != null && rawArgs.containsKey(key)) {
return rawArgs.getInt(key)
}
return null
}
}

View File

@@ -0,0 +1,205 @@
/*
* 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.spa.framework.common
import android.os.Bundle
import androidx.compose.runtime.Composable
/**
* Defines data of one Settings entry for Settings search.
*/
data class SearchData(val keyword: String = "")
/**
* Defines data of one Settings entry for UI rendering.
*/
data class UiData(val title: String = "")
/**
* Defines data to identify a Settings page.
*/
data class SettingsPage(val name: String = "", val args: Bundle = Bundle.EMPTY)
/**
* Defines data of a Settings entry.
*/
data class SettingsEntry(
val name: String,
val owner: SettingsPage,
// Generates the unique id of this entry
val uniqueId: String,
// Defines linking of Settings entries
val fromPage: SettingsPage? = null,
val toPage: SettingsPage? = null,
/**
* ========================================
* Defines entry attributes here.
* ========================================
*/
val isAllowSearch: Boolean,
/**
* ========================================
* Defines entry APIs to get data here.
* ========================================
*/
/**
* API to get Search related data for this entry.
* Returns null if this entry is not available for the search at the moment.
*/
val searchData: () -> SearchData? = { null },
/**
* API to get UI related data for this entry.
* Returns null if the entry is not render-able.
*/
val uiData: () -> UiData? = { null },
/**
* API to Render UI of this entry directly. For now, we use it in the internal injection, to
* support the case that the injection page owner wants to maintain both data and UI of the
* injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
* use each entries' UI rendering function in the page instead.
*/
val uiLayout: (@Composable () -> Unit) = {},
)
/**
* The helper to build a Settings Page instance.
*/
class SettingsPageBuilder(private val name: String) {
private val arguments = Bundle()
fun pushArgs(args: Bundle?): SettingsPageBuilder {
if (args != null) arguments.putAll(args)
return this
}
fun pushArg(argName: String, argValue: String?): SettingsPageBuilder {
if (argValue != null) this.arguments.putString(argName, argValue)
return this
}
fun pushArg(argName: String, argValue: Int?): SettingsPageBuilder {
if (argValue != null) this.arguments.putInt(argName, argValue)
return this
}
fun build(): SettingsPage {
return SettingsPage(name, arguments)
}
companion object {
fun create(name: String, args: Bundle? = null): SettingsPageBuilder {
return SettingsPageBuilder(name).apply {
pushArgs(args)
}
}
}
}
/**
* The helper to build a Settings Entry instance.
*/
class SettingsEntryBuilder(private val name: String, private val owner: SettingsPage) {
private var uniqueId: String? = null
private var fromPage: SettingsPage? = null
private var toPage: SettingsPage? = null
private var isAllowSearch: Boolean? = null
private var searchDataFn: () -> SearchData? = { null }
private var uiLayoutFn: (@Composable () -> Unit) = {}
fun build(): SettingsEntry {
return SettingsEntry(
name = name,
owner = owner,
uniqueId = computeUniqueId(),
// linking data
fromPage = fromPage,
toPage = toPage,
// attributes
isAllowSearch = computeSearchable(),
// functions
searchData = searchDataFn,
uiLayout = uiLayoutFn,
)
}
fun setLink(
fromPage: SettingsPage? = null,
toPage: SettingsPage? = null
): SettingsEntryBuilder {
if (fromPage != null) this.fromPage = fromPage
if (toPage != null) this.toPage = toPage
return this
}
fun setIsAllowSearch(isAllowSearch: Boolean): SettingsEntryBuilder {
this.isAllowSearch = isAllowSearch
return this
}
fun setSearchDataFn(fn: () -> SearchData?): SettingsEntryBuilder {
this.searchDataFn = fn
return this
}
fun setUiLayoutFn(fn: @Composable () -> Unit): SettingsEntryBuilder {
this.uiLayoutFn = fn
return this
}
private fun computeUniqueId(): String = uniqueId ?: (name + owner.toString())
private fun computeSearchable(): Boolean = isAllowSearch ?: false
companion object {
fun create(
entryName: String,
ownerPageName: String,
ownerPageArgs: Bundle? = null
): SettingsEntryBuilder {
val owner = SettingsPageBuilder.create(ownerPageName, ownerPageArgs)
return SettingsEntryBuilder(entryName, owner.build())
}
fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
return SettingsEntryBuilder(entryName, owner)
}
fun createLinkTo(
entryName: String,
ownerPageName: String,
ownerPageArgs: Bundle? = null
): SettingsEntryBuilder {
val owner = SettingsPageBuilder.create(ownerPageName, ownerPageArgs)
return SettingsEntryBuilder(entryName, owner.build()).setLink(toPage = owner.build())
}
fun createLinkTo(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
return SettingsEntryBuilder(entryName, owner).setLink(toPage = owner)
}
}
}

View File

@@ -36,5 +36,5 @@ interface SettingsPageProvider {
@Composable
fun Page(arguments: Bundle?)
// fun buildEntry( arguments: Bundle?) : List<entry>
fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
}