Support StaticData / SearchData in SettingsEntry.

Add StaticData / SearchData API in SettingsEntry.
Introduce SppEnum & EntryEnum, which includes name (for logging) and displayName.
Refactor ArgumentPage & PreferencePage to use above APIs.

Bug: 244122804
Test: manual - build Spa gallery
Change-Id: I172ee08ee7b0e1e09337f289406f57b2caf9937e
This commit is contained in:
Zekan Qian
2022-09-19 11:38:36 +08:00
parent c78e7fc612
commit f839df8ca2
20 changed files with 773 additions and 375 deletions

View File

@@ -18,4 +18,8 @@ package com.android.settingslib.spa.gallery
import com.android.settingslib.spa.framework.DebugActivity
class GalleryDebugActivity : DebugActivity(SpaEnvironment.EntryRepository, MainActivity::class.java)
class GalleryDebugActivity : DebugActivity(
SpaEnvironment.EntryRepository,
MainActivity::class.java,
"com.android.spa.gallery.provider",
)

View File

@@ -16,6 +16,8 @@
package com.android.settingslib.spa.gallery
import android.os.Bundle
import androidx.navigation.NamedNavArgument
import com.android.settingslib.spa.framework.common.SettingsEntryRepository
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
@@ -34,6 +36,32 @@ import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferenceP
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
/**
* Enum to define all SPP name here.
* Since the SPP name would be used in log, DO NOT change it once it is set. One can still change
* the display name for better readability if necessary.
*/
enum class SettingsPageProviderEnum(val displayName: String) {
HOME("home"),
PREFERENCE("preference"),
ARGUMENT("argument"),
// Add your SPPs
}
fun createSettingsPage(
SppName: SettingsPageProviderEnum,
parameter: List<NamedNavArgument> = emptyList(),
arguments: Bundle? = null
): SettingsPage {
return SettingsPage(
name = SppName.name,
displayName = SppName.displayName,
parameter = parameter,
arguments = arguments,
)
}
object SpaEnvironment {
val PageProviderRepository: SettingsPageProviderRepository by
lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
@@ -55,7 +83,7 @@ object SpaEnvironment {
ActionButtonPageProvider,
),
rootPages = listOf(
SettingsPage.create(HomePageProvider.name)
createSettingsPage(SettingsPageProviderEnum.HOME)
)
)
}

View File

@@ -21,10 +21,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.gallery.R
import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
import com.android.settingslib.spa.gallery.page.ArgumentPageModel
import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
@@ -38,20 +38,19 @@ import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
object HomePageProvider : SettingsPageProvider {
override val name = "Home"
override val name = SettingsPageProviderEnum.HOME.name
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val owner = SettingsPage.create(name)
return listOf(
PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(),
SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
SettingsPagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
PreferenceMainPageProvider.buildInjectEntry().build(),
ArgumentPageProvider.buildInjectEntry("foo")!!.build(),
SliderPageProvider.buildInjectEntry().build(),
SpinnerPageProvider.buildInjectEntry().build(),
SettingsPagerPageProvider.buildInjectEntry().build(),
FooterPageProvider.buildInjectEntry().build(),
IllustrationPageProvider.buildInjectEntry().build(),
CategoryPageProvider.buildInjectEntry().build(),
ActionButtonPageProvider.buildInjectEntry().build(),
)
}
@@ -59,7 +58,7 @@ object HomePageProvider : SettingsPageProvider {
override fun Page(arguments: Bundle?) {
HomeScaffold(title = stringResource(R.string.app_name)) {
for (entry in buildEntry(arguments)) {
if (entry.name.startsWith(ArgumentPageModel.name)) {
if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) {
entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0))
} else {
entry.UiLayout()

View File

@@ -24,23 +24,38 @@ import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.gallery.createSettingsPage
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
object ArgumentPageProvider : SettingsPageProvider {
override val name = ArgumentPageModel.name
// Defines all entry name in this page.
// Note that entry name would be used in log. DO NOT change it once it is set.
// One can still change the display name for better readability if necessary.
private enum class EntryEnum(val displayName: String) {
STRING_PARAM("string_param"),
INT_PARAM("int_param"),
}
private fun createEntry(owner: SettingsPage, entry: EntryEnum): SettingsEntryBuilder {
return SettingsEntryBuilder.create(owner, entry.name, entry.displayName)
}
override val name = SettingsPageProviderEnum.ARGUMENT.name
override val parameter = ArgumentPageModel.parameter
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
if (!ArgumentPageModel.isValidArgument(arguments)) return emptyList()
val owner = SettingsPage.create(name, parameter, arguments)
val owner = createSettingsPage(SettingsPageProviderEnum.ARGUMENT, parameter, arguments)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create("string_param", owner)
createEntry(owner, EntryEnum.STRING_PARAM)
// Set attributes
.setIsAllowSearch(true)
.setSearchDataFn { ArgumentPageModel.genStringParamSearchData() }
.setUiLayoutFn {
// Set ui rendering
Preference(ArgumentPageModel.create(it).genStringParamPreferenceModel())
@@ -48,9 +63,10 @@ object ArgumentPageProvider : SettingsPageProvider {
)
entryList.add(
SettingsEntryBuilder.create("int_param", owner)
createEntry(owner, EntryEnum.INT_PARAM)
// Set attributes
.setIsAllowSearch(true)
.setSearchDataFn { ArgumentPageModel.genIntParamSearchData() }
.setUiLayoutFn {
// Set ui rendering
Preference(ArgumentPageModel.create(it).genIntParamPreferenceModel())
@@ -68,11 +84,12 @@ object ArgumentPageProvider : SettingsPageProvider {
if (!ArgumentPageModel.isValidArgument(arguments)) return null
return SettingsEntryBuilder.createInject(
entryName = "${name}_$stringParam",
owner = SettingsPage.create(name, parameter, arguments)
owner = createSettingsPage(SettingsPageProviderEnum.ARGUMENT, parameter, arguments),
displayName = "${name}_$stringParam",
)
// Set attributes
.setIsAllowSearch(false)
.setSearchDataFn { ArgumentPageModel.genInjectSearchData() }
.setUiLayoutFn {
// Set ui rendering
Preference(ArgumentPageModel.create(it).genInjectPreferenceModel())
@@ -83,7 +100,7 @@ object ArgumentPageProvider : SettingsPageProvider {
override fun Page(arguments: Bundle?) {
RegularScaffold(title = ArgumentPageModel.create(arguments).genPageTitle()) {
for (entry in buildEntry(arguments)) {
if (entry.name.startsWith(name)) {
if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) {
entry.UiLayout(ArgumentPageModel.buildNextArgument(arguments))
} else {
entry.UiLayout()

View File

@@ -23,22 +23,28 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.android.settingslib.spa.framework.BrowseActivity
import com.android.settingslib.spa.framework.common.EntrySearchData
import com.android.settingslib.spa.framework.common.PageModel
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.util.getIntArg
import com.android.settingslib.spa.framework.util.getStringArg
import com.android.settingslib.spa.framework.util.navLink
import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.widget.preference.PreferenceModel
private const val TITLE = "Sample page with arguments"
// Defines all the resources for this page.
// In real Settings App, resources data is defined in xml, rather than SPP.
private const val PAGE_TITLE = "Sample page with arguments"
private const val STRING_PARAM_TITLE = "String param value"
private const val INT_PARAM_TITLE = "Int param value"
private const val STRING_PARAM_NAME = "stringParam"
private const val INT_PARAM_NAME = "intParam"
private val ARGUMENT_PAGE_KEYWORDS = listOf("argument keyword1", "argument keyword2")
class ArgumentPageModel : PageModel() {
companion object {
const val name = "Argument"
val parameter = listOf(
navArgument(STRING_PARAM_NAME) { type = NavType.StringType },
navArgument(INT_PARAM_NAME) { type = NavType.IntType },
@@ -62,6 +68,18 @@ class ArgumentPageModel : PageModel() {
return (stringParam != null && listOf("foo", "bar").contains(stringParam))
}
fun genStringParamSearchData(): EntrySearchData {
return EntrySearchData(title = STRING_PARAM_TITLE)
}
fun genIntParamSearchData(): EntrySearchData {
return EntrySearchData(title = INT_PARAM_TITLE)
}
fun genInjectSearchData(): EntrySearchData {
return EntrySearchData(title = PAGE_TITLE, keyword = ARGUMENT_PAGE_KEYWORDS)
}
@Composable
fun create(arguments: Bundle?): ArgumentPageModel {
val pageModel: ArgumentPageModel = viewModel(key = arguments.toString())
@@ -70,7 +88,7 @@ class ArgumentPageModel : PageModel() {
}
}
private val title = TITLE
private val title = PAGE_TITLE
private var arguments: Bundle? = null
private var stringParam: String? = null
private var intParam: Int? = null
@@ -92,7 +110,7 @@ class ArgumentPageModel : PageModel() {
@Composable
fun genStringParamPreferenceModel(): PreferenceModel {
return object : PreferenceModel {
override val title = "String param value"
override val title = STRING_PARAM_TITLE
override val summary = stateOf(stringParam!!)
}
}
@@ -100,7 +118,7 @@ class ArgumentPageModel : PageModel() {
@Composable
fun genIntParamPreferenceModel(): PreferenceModel {
return object : PreferenceModel {
override val title = "Int param value"
override val title = INT_PARAM_TITLE
override val summary = stateOf(intParam!!.toString())
}
}
@@ -114,7 +132,8 @@ class ArgumentPageModel : PageModel() {
return object : PreferenceModel {
override val title = genPageTitle()
override val summary = stateOf(summaryArray.joinToString(", "))
override val onClick = navigator(name + parameter.navLink(arguments))
override val onClick = navigator(
SettingsPageProviderEnum.ARGUMENT.displayName + parameter.navLink(arguments))
}
}
}

View File

@@ -18,132 +18,166 @@ package com.android.settingslib.spa.gallery.preference
import android.os.Bundle
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Autorenew
import androidx.compose.material.icons.outlined.DisabledByDefault
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.EntrySearchData
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.gallery.createSettingsPage
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_TITLE
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.MANUAL_UPDATE_PREFERENCE_TITLE
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.PAGE_TITLE
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE
import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.logMsg
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SimplePreferenceMarco
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.SettingsIcon
import kotlinx.coroutines.delay
private const val TITLE = "Sample Preference"
object PreferencePageProvider : SettingsPageProvider {
override val name = "Preference"
// Defines all entry name in this page.
// Note that entry name would be used in log. DO NOT change it once it is set.
// One can still change the display name for better readability if necessary.
enum class EntryEnum(val displayName: String) {
SIMPLE_PREFERENCE("preference"),
SUMMARY_PREFERENCE("preference_with_summary"),
DISABLED_PREFERENCE("preference_disable"),
ASYNC_SUMMARY_PREFERENCE("preference_with_async_summary"),
MANUAL_UPDATE_PREFERENCE("preference_actionable"),
AUTO_UPDATE_PREFERENCE("preference_auto_update"),
}
override val name = SettingsPageProviderEnum.PREFERENCE.name
private val owner = createSettingsPage(SettingsPageProviderEnum.PREFERENCE)
private fun createEntry(entry: EntryEnum): SettingsEntryBuilder {
return SettingsEntryBuilder.create(owner, entry.name, entry.displayName)
}
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create("Preference", owner)
createEntry(EntryEnum.SIMPLE_PREFERENCE)
.setIsAllowSearch(true)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Preference"
})
}.build()
.setMarco {
logMsg("create marco for ${EntryEnum.SIMPLE_PREFERENCE}")
SimplePreferenceMarco(title = SIMPLE_PREFERENCE_TITLE)
}
.build()
)
entryList.add(
SettingsEntryBuilder.create("Preference with summary", owner)
createEntry(EntryEnum.SUMMARY_PREFERENCE)
.setIsAllowSearch(true)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Preference"
override val summary = "With summary".toState()
})
}.build()
.setMarco {
logMsg("create marco for ${EntryEnum.SUMMARY_PREFERENCE}")
SimplePreferenceMarco(
title = SIMPLE_PREFERENCE_TITLE,
summary = SIMPLE_PREFERENCE_SUMMARY,
searchKeywords = SIMPLE_PREFERENCE_KEYWORDS,
)
}
.build()
)
entryList.add(
SettingsEntryBuilder.create("Preference with async summary", owner)
createEntry(EntryEnum.DISABLED_PREFERENCE)
.setIsAllowSearch(true)
.setMarco {
logMsg("create marco for ${EntryEnum.DISABLED_PREFERENCE}")
SimplePreferenceMarco(
title = DISABLE_PREFERENCE_TITLE,
summary = DISABLE_PREFERENCE_SUMMARY,
disabled = true,
icon = Icons.Outlined.DisabledByDefault,
)
}
.build()
)
entryList.add(
createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE)
.setIsAllowSearch(true)
.setSearchDataFn {
EntrySearchData(title = ASYNC_PREFERENCE_TITLE)
}
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Preference"
override val summary = produceState(initialValue = " ") {
delay(1000L)
value = "Async summary"
val model = PreferencePageModel.create()
val asyncSummary = remember { model.getAsyncSummary() }
Preference(
object : PreferenceModel {
override val title = ASYNC_PREFERENCE_TITLE
override val summary = asyncSummary
}
})
)
}.build()
)
entryList.add(
SettingsEntryBuilder.create("Click me", owner)
createEntry(EntryEnum.MANUAL_UPDATE_PREFERENCE)
.setIsAllowSearch(true)
.setUiLayoutFn {
var count by rememberSaveable { mutableStateOf(0) }
Preference(object : PreferenceModel {
override val title = "Click me"
override val summary = derivedStateOf { count.toString() }
override val onClick: (() -> Unit) = { count++ }
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.TouchApp)
val model = PreferencePageModel.create()
val manualUpdaterSummary = remember { model.getManualUpdaterSummary() }
Preference(
object : PreferenceModel {
override val title = MANUAL_UPDATE_PREFERENCE_TITLE
override val summary = manualUpdaterSummary
override val onClick = { model.manualUpdaterOnClick() }
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.TouchApp)
}
}
})
)
}.build()
)
entryList.add(
SettingsEntryBuilder.create("Ticker", owner)
createEntry(EntryEnum.AUTO_UPDATE_PREFERENCE)
.setIsAllowSearch(true)
.setUiLayoutFn {
var ticks by rememberSaveable { mutableStateOf(0) }
LaunchedEffect(ticks) {
delay(1000L)
ticks++
}
Preference(object : PreferenceModel {
override val title = "Ticker"
override val summary = derivedStateOf { ticks.toString() }
})
}.build()
)
entryList.add(
SettingsEntryBuilder.create("Disabled", owner)
.setIsAllowSearch(true)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Disabled"
override val summary = "Disabled".toState()
override val enabled = false.toState()
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault)
val model = PreferencePageModel.create()
val autoUpdaterSummary = remember { model.getAutoUpdaterSummary() }
Preference(
object : PreferenceModel {
override val title = AUTO_UPDATE_PREFERENCE_TITLE
override val summary = autoUpdaterSummary.observeAsState(" ")
override val icon = @Composable {
SettingsIcon(imageVector = Icons.Outlined.Autorenew)
}
}
})
}.build()
)
}
.build()
)
return entryList
}
fun buildInjectEntry(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
return SettingsEntryBuilder.createInject(owner = owner)
.setIsAllowSearch(true)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
override val onClick = navigator(name)
})
.setMarco {
logMsg("create marco for INJECT entry")
SimplePreferenceMarco(
title = PAGE_TITLE,
clickRoute = SettingsPageProviderEnum.PREFERENCE.name
)
}
}
@Composable
override fun Page(arguments: Bundle?) {
RegularScaffold(title = TITLE) {
RegularScaffold(title = PAGE_TITLE) {
for (entry in buildEntry(arguments)) {
entry.UiLayout()
}

View File

@@ -0,0 +1,115 @@
/*
* 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.gallery.preference
import android.os.Bundle
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settingslib.spa.framework.common.PageModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class PreferencePageModel : PageModel() {
companion object {
// Defines all the resources for this page.
// In real Settings App, resources data is defined in xml, rather than SPP.
const val PAGE_TITLE = "Sample Preference"
const val SIMPLE_PREFERENCE_TITLE = "Preference"
const val SIMPLE_PREFERENCE_SUMMARY = "Simple summary"
const val DISABLE_PREFERENCE_TITLE = "Disabled"
const val DISABLE_PREFERENCE_SUMMARY = "Disabled summary"
const val ASYNC_PREFERENCE_TITLE = "Async Preference"
private const val ASYNC_PREFERENCE_SUMMARY = "Async summary"
const val MANUAL_UPDATE_PREFERENCE_TITLE = "Manual Updater"
const val AUTO_UPDATE_PREFERENCE_TITLE = "Auto Updater"
val SIMPLE_PREFERENCE_KEYWORDS = listOf("simple keyword1", "simple keyword2")
@Composable
fun create(): PreferencePageModel {
val pageModel: PreferencePageModel = viewModel()
pageModel.initOnce()
return pageModel
}
fun logMsg(message: String) {
Log.d("PreferencePageModel", message)
}
}
private val asyncSummary = mutableStateOf(" ")
private val manualUpdater = mutableStateOf(0)
private val autoUpdater = object : MutableLiveData<String>(" ") {
private var tick = 0
private var updateJob: Job? = null
override fun onActive() {
logMsg("autoUpdater.active")
updateJob = viewModelScope.launch(Dispatchers.IO) {
while (true) {
delay(1000L)
tick++
logMsg("autoUpdater.value $tick")
postValue(tick.toString())
}
}
}
override fun onInactive() {
logMsg("autoUpdater.inactive")
updateJob?.cancel()
}
}
override fun initialize(arguments: Bundle?) {
logMsg("init with args " + arguments.toString())
viewModelScope.launch(Dispatchers.IO) {
delay(2000L)
asyncSummary.value = ASYNC_PREFERENCE_SUMMARY
}
}
fun getAsyncSummary(): State<String> {
logMsg("getAsyncSummary")
return asyncSummary
}
fun getManualUpdaterSummary(): State<String> {
logMsg("getManualUpdaterSummary")
return derivedStateOf { manualUpdater.value.toString() }
}
fun manualUpdaterOnClick() {
logMsg("manualUpdaterOnClick")
manualUpdater.value = manualUpdater.value + 1
}
fun getAutoUpdaterSummary(): LiveData<String> {
logMsg("getAutoUpdaterSummary")
return autoUpdater
}
}

View File

@@ -31,12 +31,22 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.android.settingslib.spa.R
import com.android.settingslib.spa.framework.common.ROOT_PAGE_NAME
import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
import com.android.settingslib.spa.framework.compose.localNavController
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.framework.util.navRoute
const val NULL_PAGE_NAME = "NULL"
/**
* The Activity to render ALL SPA pages, and handles jumps between SPA pages.
* One can open any SPA page by:
* $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination <SpaPageRoute>
* For gallery, BrowseActivityComponent = com.android.settingslib.spa.gallery/.MainActivity
* Some examples:
* $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination HOME
* $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination ARGUMENT/bar/5
*/
open class BrowseActivity(
private val sppRepository: SettingsPageProviderRepository,
) : ComponentActivity() {
@@ -55,8 +65,8 @@ open class BrowseActivity(
private fun MainContent() {
val navController = rememberNavController()
CompositionLocalProvider(navController.localNavController()) {
NavHost(navController, ROOT_PAGE_NAME) {
composable(ROOT_PAGE_NAME) {}
NavHost(navController, NULL_PAGE_NAME) {
composable(NULL_PAGE_NAME) {}
for (page in sppRepository.getAllProviders()) {
composable(
route = page.name + page.parameter.navRoute() +
@@ -82,7 +92,7 @@ open class BrowseActivity(
destinationNavigated.value = true
LaunchedEffect(Unit) {
val destination =
intent?.getStringExtra(KEY_DESTINATION) ?: sppRepository.getDefaultStartPageName()
intent?.getStringExtra(KEY_DESTINATION) ?: sppRepository.getDefaultStartPage()
if (destination.isNotEmpty()) {
navController.navigate(destination) {
popUpTo(navController.graph.findStartDestination().id) {

View File

@@ -25,6 +25,7 @@ import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
@@ -33,7 +34,6 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.android.settingslib.spa.R
import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
import com.android.settingslib.spa.framework.EntryProvider.Companion.PAGE_INFO_QUERY
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryRepository
import com.android.settingslib.spa.framework.common.SettingsPage
@@ -54,6 +54,12 @@ private const val ROUTE_ENTRY = "entry"
private const val PARAM_NAME_PAGE_ID = "pid"
private const val PARAM_NAME_ENTRY_ID = "eid"
/**
* The Debug Activity to display all Spa Pages & Entries.
* One can open the debug activity by:
* $ adb shell am start -n <Activity>
* For gallery, Activity = com.android.settingslib.spa.gallery/.GalleryDebugActivity
*/
open class DebugActivity(
private val entryRepository: SettingsEntryRepository,
private val browseActivityClass: Class<*>,
@@ -75,15 +81,16 @@ open class DebugActivity(
if (entryProviderAuthorities == null) return
try {
val query = EntryProvider.QueryEnum.PAGE_INFO_QUERY
contentResolver.query(
Uri.parse("content://$entryProviderAuthorities/${PAGE_INFO_QUERY.queryPath}"),
Uri.parse("content://$entryProviderAuthorities/${query.queryPath}"),
null, null, null
).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val route = cursor.getString(PAGE_INFO_QUERY.getIndex(ColumnName.PAGE_ROUTE))
val entryCount = cursor.getInt(PAGE_INFO_QUERY.getIndex(ColumnName.ENTRY_COUNT))
val route = cursor.getString(query, EntryProvider.ColumnEnum.PAGE_ROUTE)
val entryCount = cursor.getInt(query, EntryProvider.ColumnEnum.ENTRY_COUNT)
val hasRuntimeParam =
cursor.getInt(PAGE_INFO_QUERY.getIndex(ColumnName.HAS_RUNTIME_PARAM)) == 1
cursor.getBoolean(query, EntryProvider.ColumnEnum.HAS_RUNTIME_PARAM)
Log.d(
"DEBUG ACTIVITY", "Page Info: $route ($entryCount) " +
(if (hasRuntimeParam) "with" else "no") + "-runtime-params"
@@ -106,13 +113,13 @@ open class DebugActivity(
composable(
route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
arguments = listOf(
navArgument(PARAM_NAME_PAGE_ID) { type = NavType.IntType },
navArgument(PARAM_NAME_PAGE_ID) { type = NavType.StringType },
)
) { navBackStackEntry -> OnePage(navBackStackEntry.arguments) }
composable(
route = "$ROUTE_ENTRY/{$PARAM_NAME_ENTRY_ID}",
arguments = listOf(
navArgument(PARAM_NAME_ENTRY_ID) { type = NavType.IntType },
navArgument(PARAM_NAME_ENTRY_ID) { type = NavType.StringType },
)
) { navBackStackEntry -> OneEntry(navBackStackEntry.arguments) }
}
@@ -121,13 +128,15 @@ open class DebugActivity(
@Composable
fun RootPage() {
val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
val allEntry = remember { entryRepository.getAllEntries() }
HomeScaffold(title = "Settings Debug") {
Preference(object : PreferenceModel {
override val title = "List All Pages"
override val title = "List All Pages (${allPageWithEntry.size})"
override val onClick = navigator(route = ROUTE_All_PAGES)
})
Preference(object : PreferenceModel {
override val title = "List All Entries"
override val title = "List All Entries (${allEntry.size})"
override val onClick = navigator(route = ROUTE_All_ENTRIES)
})
}
@@ -135,14 +144,15 @@ open class DebugActivity(
@Composable
fun AllPages() {
RegularScaffold(title = "All Pages") {
for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
RegularScaffold(title = "All Pages (${allPageWithEntry.size})") {
for (pageWithEntry in allPageWithEntry) {
Preference(object : PreferenceModel {
override val title =
"${pageWithEntry.page.name} (${pageWithEntry.entries.size})"
"${pageWithEntry.page.displayName} (${pageWithEntry.entries.size})"
override val summary = pageWithEntry.page.formatArguments().toState()
override val onClick =
navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id}")
navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id()}")
})
}
}
@@ -150,16 +160,18 @@ open class DebugActivity(
@Composable
fun AllEntries() {
RegularScaffold(title = "All Entries") {
EntryList(entryRepository.getAllEntries())
val allEntry = remember { entryRepository.getAllEntries() }
RegularScaffold(title = "All Entries (${allEntry.size})") {
EntryList(allEntry)
}
}
@Composable
fun OnePage(arguments: Bundle?) {
val id = arguments!!.getInt(PARAM_NAME_PAGE_ID)
val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
val pageWithEntry = entryRepository.getPageWithEntry(id)!!
RegularScaffold(title = "Page ${pageWithEntry.page.name}") {
RegularScaffold(title = "Page - ${pageWithEntry.page.displayName}") {
Text(text = pageWithEntry.page.id().toString())
Text(text = pageWithEntry.page.formatArguments())
Text(text = "Entry size: ${pageWithEntry.entries.size}")
Preference(model = object : PreferenceModel {
@@ -173,15 +185,16 @@ open class DebugActivity(
@Composable
fun OneEntry(arguments: Bundle?) {
val id = arguments!!.getInt(PARAM_NAME_ENTRY_ID)
val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
val entry = entryRepository.getEntry(id)!!
RegularScaffold(title = "Entry ${entry.displayName()}") {
val entryContent = remember { entry.formatContent() }
RegularScaffold(title = "Entry - ${entry.displayTitle()}") {
Preference(model = object : PreferenceModel {
override val title = "open entry"
override val enabled = (!entry.hasRuntimeParam()).toState()
override val onClick = openEntry(entry)
})
Text(text = entry.formatAll())
Text(text = entryContent)
}
}
@@ -189,10 +202,10 @@ open class DebugActivity(
private fun EntryList(entries: Collection<SettingsEntry>) {
for (entry in entries) {
Preference(object : PreferenceModel {
override val title = entry.displayName()
override val title = entry.displayTitle()
override val summary =
"${entry.fromPage?.name} -> ${entry.toPage?.name}".toState()
override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}")
"${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState()
override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id()}")
})
}
}

View File

@@ -28,35 +28,55 @@ import android.util.Log
import com.android.settingslib.spa.framework.common.SettingsEntryRepository
/**
* Enum to define all column names in provider.
* The content provider to return entry related data, which can be used for search and hierarchy.
* One can query the provider result by:
* $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
* For gallery, AuthorityPath = com.android.spa.gallery.provider
* Some examples:
* $ adb shell content query --uri content://<AuthorityPath>/page_start
* $ adb shell content query --uri content://<AuthorityPath>/page_info
*/
enum class ColumnName(val id: String) {
PAGE_NAME("pageName"),
PAGE_ROUTE("pageRoute"),
ENTRY_COUNT("entryCount"),
HAS_RUNTIME_PARAM("hasRuntimeParam"),
PAGE_START_ADB("pageStartAdb"),
}
data class QueryDefinition(
val queryPath: String,
val queryMatchCode: Int,
val columnNames: List<ColumnName>,
) {
fun getColumns(): Array<String> {
return columnNames.map { it.id }.toTypedArray()
}
fun getIndex(name: ColumnName): Int {
return columnNames.indexOf(name)
}
}
open class EntryProvider(
private val entryRepository: SettingsEntryRepository,
private val browseActivityComponentName: String? = null,
) : ContentProvider() {
/**
* Enum to define all column names in provider.
*/
enum class ColumnEnum(val id: String) {
PAGE_ID("pageId"),
PAGE_NAME("pageName"),
PAGE_ROUTE("pageRoute"),
ENTRY_COUNT("entryCount"),
HAS_RUNTIME_PARAM("hasRuntimeParam"),
PAGE_START_ADB("pageStartAdb"),
}
/**
* Enum to define all queries supported in the provider.
*/
enum class QueryEnum(
val queryPath: String,
val queryMatchCode: Int,
val columnNames: List<ColumnEnum>
) {
PAGE_START_COMMAND(
"page_start", 1,
listOf(ColumnEnum.PAGE_START_ADB)
),
PAGE_INFO_QUERY(
"page_info", 2,
listOf(
ColumnEnum.PAGE_ID,
ColumnEnum.PAGE_NAME,
ColumnEnum.PAGE_ROUTE,
ColumnEnum.ENTRY_COUNT,
ColumnEnum.HAS_RUNTIME_PARAM,
)
),
}
private var mMatcher: UriMatcher? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
@@ -92,13 +112,13 @@ open class EntryProvider(
if (info != null) {
mMatcher!!.addURI(
info.authority,
PAGE_START_COMMAND_QUERY.queryPath,
PAGE_START_COMMAND_QUERY.queryMatchCode
QueryEnum.PAGE_START_COMMAND.queryPath,
QueryEnum.PAGE_START_COMMAND.queryMatchCode
)
mMatcher!!.addURI(
info.authority,
PAGE_INFO_QUERY.queryPath,
PAGE_INFO_QUERY.queryMatchCode
QueryEnum.PAGE_INFO_QUERY.queryPath,
QueryEnum.PAGE_INFO_QUERY.queryMatchCode
)
}
super.attachInfo(context, info)
@@ -113,8 +133,8 @@ open class EntryProvider(
): Cursor? {
return try {
when (mMatcher!!.match(uri)) {
PAGE_START_COMMAND_QUERY.queryMatchCode -> queryPageStartCommand()
PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
QueryEnum.PAGE_START_COMMAND.queryMatchCode -> queryPageStartCommand()
QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
else -> throw UnsupportedOperationException("Unknown Uri $uri")
}
} catch (e: UnsupportedOperationException) {
@@ -126,13 +146,13 @@ open class EntryProvider(
}
private fun queryPageStartCommand(): Cursor {
val componentName = browseActivityComponentName ?: "[component-name]"
val cursor = MatrixCursor(PAGE_START_COMMAND_QUERY.getColumns())
val componentName = browseActivityComponentName ?: "<activity-component-name>"
val cursor = MatrixCursor(QueryEnum.PAGE_START_COMMAND.getColumns())
for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
val page = pageWithEntry.page
if (!page.hasRuntimeParam()) {
cursor.newRow().add(
ColumnName.PAGE_START_ADB.id,
ColumnEnum.PAGE_START_ADB.id,
"adb shell am start -n $componentName" +
" -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}"
)
@@ -142,31 +162,39 @@ open class EntryProvider(
}
private fun queryPageInfo(): Cursor {
val cursor = MatrixCursor(PAGE_INFO_QUERY.getColumns())
val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
val page = pageWithEntry.page
cursor.newRow().add(ColumnName.PAGE_NAME.id, page.name)
.add(ColumnName.PAGE_ROUTE.id, page.buildRoute())
.add(ColumnName.ENTRY_COUNT.id, pageWithEntry.entries.size)
.add(ColumnName.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
cursor.newRow()
.add(ColumnEnum.PAGE_ID.id, page.id())
.add(ColumnEnum.PAGE_NAME.id, page.displayName)
.add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
.add(ColumnEnum.ENTRY_COUNT.id, pageWithEntry.entries.size)
.add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
}
return cursor
}
companion object {
val PAGE_START_COMMAND_QUERY = QueryDefinition(
"page_start", 1,
listOf(ColumnName.PAGE_START_ADB)
)
val PAGE_INFO_QUERY = QueryDefinition(
"page_info", 2,
listOf(
ColumnName.PAGE_NAME,
ColumnName.PAGE_ROUTE,
ColumnName.ENTRY_COUNT,
ColumnName.HAS_RUNTIME_PARAM,
)
)
}
}
fun EntryProvider.QueryEnum.getColumns(): Array<String> {
return columnNames.map { it.id }.toTypedArray()
}
fun EntryProvider.QueryEnum.getIndex(name: EntryProvider.ColumnEnum): Int {
return columnNames.indexOf(name)
}
fun Cursor.getString(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): String {
return this.getString(query.getIndex(columnName))
}
fun Cursor.getInt(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): Int {
return this.getInt(query.getIndex(columnName))
}
fun Cursor.getBoolean(
query: EntryProvider.QueryEnum,
columnName: EntryProvider.ColumnEnum
): Boolean {
return this.getInt(query.getIndex(columnName)) == 1
}

View File

@@ -0,0 +1,30 @@
/*
* 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 androidx.compose.runtime.Composable
/**
* Defines interface of a entry marco, which contains all entry functions to support different
* scenarios, such as browsing (UiLayout), search, etc.
* SPA team will rebuild some entry marcos, in order to make the entry creation easier.
*/
interface EntryMarco {
@Composable
fun UiLayout() {}
fun getSearchData(): EntrySearchData? = null
}

View File

@@ -0,0 +1,33 @@
/*
* 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
/**
* Defines Search data of one Settings entry.
*/
data class EntrySearchData(
val title: String = "",
val keyword: List<String> = emptyList(),
) {
fun format(): String {
val content = listOf(
"search_title = $title",
"search_keyword = $keyword",
)
return content.joinToString("\n")
}
}

View File

@@ -22,7 +22,7 @@ import androidx.lifecycle.ViewModel
open class PageModel : ViewModel() {
var initialized = false
fun initOnce(arguments: Bundle?) {
fun initOnce(arguments: Bundle? = null) {
// Initialize only once
if (initialized) return
initialized = true

View File

@@ -18,84 +18,20 @@ package com.android.settingslib.spa.framework.common
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavType
import com.android.settingslib.spa.framework.BrowseActivity
import com.android.settingslib.spa.framework.util.navLink
import androidx.compose.runtime.remember
const val INJECT_ENTRY_NAME = "INJECT"
const val ROOT_ENTRY_NAME = "ROOT"
const val ROOT_PAGE_NAME = "Root"
/**
* 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(
// The unique id of this page, which is computed by name + normalized(arguments)
val id: Int,
// The name of the page, which is used to compute the unique id, and need to be stable.
val name: String,
// Defined parameters of this page.
val parameter: List<NamedNavArgument> = emptyList(),
// The arguments of this page.
val arguments: Bundle? = null,
) {
companion object {
fun create(
name: String,
parameter: List<NamedNavArgument> = emptyList(),
arguments: Bundle? = null
): SettingsPage {
return SettingsPageBuilder(name, parameter).setArguments(arguments).build()
}
}
fun formatArguments(): String {
val normalizedArguments = parameter.normalize(arguments)
if (normalizedArguments == null || normalizedArguments.isEmpty) return "[No arguments]"
return normalizedArguments.toString().removeRange(0, 6)
}
fun formatAll(): String {
return "$name ${formatArguments()}"
}
fun buildRoute(highlightEntryName: String? = null): String {
val highlightParam =
if (highlightEntryName == null)
""
else
"?${BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME}=$highlightEntryName"
return name + parameter.navLink(arguments) + highlightParam
}
fun hasRuntimeParam(): Boolean {
return parameter.hasRuntimeParam(arguments)
}
}
/**
* Defines data of a Settings entry.
*/
data class SettingsEntry(
// The unique id of this entry, which is computed by name + owner + fromPage + toPage.
val id: Int,
// The name of the page, which is used to compute the unique id, and need to be stable.
val name: String,
private val name: String,
// The display name of the page, for better readability.
val displayName: String,
// The owner page of this entry.
val owner: SettingsPage,
@@ -109,7 +45,7 @@ data class SettingsEntry(
* Defines entry attributes here.
* ========================================
*/
val isAllowSearch: Boolean,
val isAllowSearch: Boolean = false,
/**
* ========================================
@@ -121,13 +57,7 @@ data class SettingsEntry(
* 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 },
private val searchDataImpl: (arguments: Bundle?) -> EntrySearchData? = { null },
/**
* API to Render UI of this entry directly. For now, we use it in the internal injection, to
@@ -135,68 +65,56 @@ data class SettingsEntry(
* 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 uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
) {
fun formatAll(): String {
// The unique id of this entry, which is computed by name + owner + fromPage + toPage.
fun id(): String {
return "$name:${owner.id()}(${fromPage?.id()}-${toPage?.id()})".toHashId()
}
fun formatContent(): String {
val content = listOf(
"owner = ${owner.formatAll()}",
"linkFrom = ${fromPage?.formatAll()}",
"linkTo = ${toPage?.formatAll()}",
"id = ${id()}",
"owner = ${owner.formatDisplayTitle()}",
"linkFrom = ${fromPage?.formatDisplayTitle()}",
"linkTo = ${toPage?.formatDisplayTitle()}",
"${getSearchData()?.format()}",
)
return content.joinToString("\n")
}
// The display name of the entry, for better readability.
fun displayName(): String {
return "${owner.name}:$name"
fun displayTitle(): String {
return "${owner.displayName}:$displayName"
}
private fun getDisplayPage(): SettingsPage {
// Display the entry on its from-page, or on its owner page if the from-page is unset.
private fun containerPage(): SettingsPage {
// The Container page of the entry, which is the from-page or
// the owner-page if from-page is unset.
return fromPage ?: owner
}
fun buildRoute(): String {
return getDisplayPage().buildRoute(name)
return containerPage().buildRoute(id())
}
fun hasRuntimeParam(): Boolean {
return getDisplayPage().hasRuntimeParam()
return containerPage().hasRuntimeParam()
}
private fun fullArgument(runtimeArguments: Bundle? = null): Bundle {
val arguments = Bundle()
if (owner.arguments != null) arguments.putAll(owner.arguments)
if (runtimeArguments != null) arguments.putAll(runtimeArguments)
return arguments
}
fun getSearchData(runtimeArguments: Bundle? = null): EntrySearchData? {
return searchDataImpl(fullArgument(runtimeArguments))
}
@Composable
fun UiLayout(runtimeArguments: Bundle? = null) {
val arguments = Bundle()
if (owner.arguments != null) arguments.putAll(owner.arguments)
if (runtimeArguments != null) arguments.putAll(runtimeArguments)
uiLayoutImpl(arguments)
}
}
data class SettingsPageWithEntry(
val page: SettingsPage,
val entries: List<SettingsEntry>,
)
class SettingsPageBuilder(
private val name: String,
private val parameter: List<NamedNavArgument> = emptyList()
) {
private var arguments: Bundle? = null
fun build(): SettingsPage {
val normArguments = parameter.normalize(arguments)
return SettingsPage(
id = "$name:${normArguments?.toString()}".toUniqueId(),
name = name,
parameter = parameter,
arguments = arguments,
)
}
fun setArguments(arguments: Bundle?): SettingsPageBuilder {
this.arguments = arguments
return this
uiLayoutImpl(fullArgument(runtimeArguments))
}
}
@@ -204,32 +122,41 @@ class SettingsPageBuilder(
* The helper to build a Settings Entry instance.
*/
class SettingsEntryBuilder(private val name: String, private val owner: SettingsPage) {
private var displayName = name
private var fromPage: SettingsPage? = null
private var toPage: SettingsPage? = null
private var isAllowSearch: Boolean? = null
private var searchDataFn: () -> SearchData? = { null }
private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = {}
// Attributes
private var isAllowSearch: Boolean = false
// Functions
private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
fun build(): SettingsEntry {
return SettingsEntry(
id = "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toUniqueId(),
name = name,
owner = owner,
displayName = displayName,
// linking data
fromPage = fromPage,
toPage = toPage,
// attributes
isAllowSearch = getIsSearchable(),
isAllowSearch = isAllowSearch,
// functions
searchData = searchDataFn,
searchDataImpl = searchDataFn,
uiLayoutImpl = uiLayoutFn,
)
}
fun setDisplayName(displayName: String): SettingsEntryBuilder {
this.displayName = displayName
return this
}
fun setLink(
fromPage: SettingsPage? = null,
toPage: SettingsPage? = null
@@ -244,7 +171,16 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
return this
}
fun setSearchDataFn(fn: () -> SearchData?): SettingsEntryBuilder {
fun setMarco(fn: (arguments: Bundle?) -> EntryMarco): SettingsEntryBuilder {
setSearchDataFn { fn(it).getSearchData() }
setUiLayoutFn {
val marco = remember { fn(it) }
marco.UiLayout()
}
return this
}
fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
this.searchDataFn = fn
return this
}
@@ -254,8 +190,6 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
return this
}
private fun getIsSearchable(): Boolean = isAllowSearch ?: false
companion object {
fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
return SettingsEntryBuilder(entryName, owner)
@@ -269,48 +203,19 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
return create(entryName, owner).setLink(toPage = owner)
}
fun createInject(owner: SettingsPage, entryName: String? = null): SettingsEntryBuilder {
val name = entryName ?: "${INJECT_ENTRY_NAME}_${owner.name}"
return createLinkTo(name, owner)
fun create(owner: SettingsPage, entryName: String, displayName: String? = null):
SettingsEntryBuilder {
return SettingsEntryBuilder(entryName, owner).setDisplayName(displayName ?: entryName)
}
fun createRoot(owner: SettingsPage, entryName: String? = null): SettingsEntryBuilder {
val name = entryName ?: "${ROOT_ENTRY_NAME}_${owner.name}"
return createLinkTo(name, owner)
fun createInject(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder {
val name = displayName ?: "${INJECT_ENTRY_NAME}_${owner.displayName}"
return createLinkTo(INJECT_ENTRY_NAME, owner).setDisplayName(name)
}
fun createRoot(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder {
val name = displayName ?: "${ROOT_ENTRY_NAME}_${owner.displayName}"
return createLinkTo(ROOT_ENTRY_NAME, owner).setDisplayName(name)
}
}
}
private fun String.toUniqueId(): Int {
return this.hashCode()
}
private fun List<NamedNavArgument>.normalize(arguments: Bundle? = null): Bundle? {
if (this.isEmpty()) return null
val normArgs = Bundle()
for (navArg in this) {
when (navArg.argument.type) {
NavType.StringType -> {
val value = arguments?.getString(navArg.name)
if (value != null)
normArgs.putString(navArg.name, value)
else
normArgs.putString("unset_" + navArg.name, null)
}
NavType.IntType -> {
if (arguments != null && arguments.containsKey(navArg.name))
normArgs.putInt(navArg.name, arguments.getInt(navArg.name))
else
normArgs.putString("unset_" + navArg.name, null)
}
}
}
return normArgs
}
private fun List<NamedNavArgument>.hasRuntimeParam(arguments: Bundle? = null): Boolean {
for (navArg in this) {
if (arguments == null || !arguments.containsKey(navArg.name)) return true
}
return false
}

View File

@@ -21,15 +21,20 @@ import java.util.LinkedList
private const val MAX_ENTRY_SIZE = 5000
data class SettingsPageWithEntry(
val page: SettingsPage,
val entries: List<SettingsEntry>,
)
/**
* The repository to maintain all Settings entries
*/
class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
// Map of entry unique Id to entry
private val entryMap: Map<Int, SettingsEntry>
private val entryMap: Map<String, SettingsEntry>
// Map of Settings page to its contained entries.
private val pageWithEntryMap: Map<Int, SettingsPageWithEntry>
private val pageWithEntryMap: Map<String, SettingsPageWithEntry>
init {
logMsg("Initialize")
@@ -39,23 +44,29 @@ class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
val entryQueue = LinkedList<SettingsEntry>()
for (page in sppRepository.getAllRootPages()) {
val rootEntry = SettingsEntryBuilder.createRoot(owner = page).build()
if (!entryMap.containsKey(rootEntry.id)) {
val rootEntryId = rootEntry.id()
if (!entryMap.containsKey(rootEntryId)) {
entryQueue.push(rootEntry)
entryMap.put(rootEntry.id, rootEntry)
entryMap.put(rootEntryId, rootEntry)
}
}
while (entryQueue.isNotEmpty() && entryMap.size < MAX_ENTRY_SIZE) {
val entry = entryQueue.pop()
val page = entry.toPage
if (page == null || pageWithEntryMap.containsKey(page.id)) continue
val pageId = page?.id()
if (pageId == null || pageWithEntryMap.containsKey(pageId)) continue
val spp = sppRepository.getProviderOrNull(page.name) ?: continue
val newEntries = spp.buildEntry(page.arguments)
pageWithEntryMap[page.id] = SettingsPageWithEntry(page, newEntries)
val newEntries = spp.buildEntry(page.arguments).map {
// Set from-page if it is missing.
if (it.fromPage == null) it.copy(fromPage = page) else it
}
pageWithEntryMap[pageId] = SettingsPageWithEntry(page, newEntries)
for (newEntry in newEntries) {
if (!entryMap.containsKey(newEntry.id)) {
val newEntryId = newEntry.id()
if (!entryMap.containsKey(newEntryId)) {
entryQueue.push(newEntry)
entryMap.put(newEntry.id, newEntry)
entryMap.put(newEntryId, newEntry)
}
}
}
@@ -67,7 +78,7 @@ class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
return pageWithEntryMap.values
}
fun getPageWithEntry(pageId: Int): SettingsPageWithEntry? {
fun getPageWithEntry(pageId: String): SettingsPageWithEntry? {
return pageWithEntryMap[pageId]
}
@@ -75,7 +86,7 @@ class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
return entryMap.values
}
fun getEntry(entryId: Int): SettingsEntry? {
fun getEntry(entryId: String): SettingsEntry? {
return entryMap[entryId]
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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
import com.android.settingslib.spa.framework.BrowseActivity
import com.android.settingslib.spa.framework.util.navLink
/**
* Defines data to identify a Settings page.
*/
data class SettingsPage(
// The name of the page, which is used to compute the unique id, and need to be stable.
val name: String,
// The display name of the page, for better readability.
val displayName: String,
// Defined parameters of this page.
val parameter: List<NamedNavArgument> = emptyList(),
// The arguments of this page.
val arguments: Bundle? = null,
) {
companion object {
fun create(
name: String,
parameter: List<NamedNavArgument> = emptyList(),
arguments: Bundle? = null
): SettingsPage {
return SettingsPage(name, name, parameter, arguments)
}
}
// The unique id of this page, which is computed by name + normalized(arguments)
fun id(): String {
val normArguments = parameter.normalize(arguments)
return "$name:${normArguments?.toString()}".toHashId()
}
// Returns if this Settings Page is created by the given Spp.
fun isCreateBy(SppName: String): Boolean {
return name == SppName
}
fun formatArguments(): String {
val normalizedArguments = parameter.normalize(arguments)
if (normalizedArguments == null || normalizedArguments.isEmpty) return "[No arguments]"
return normalizedArguments.toString().removeRange(0, 6)
}
fun formatDisplayTitle(): String {
return "$displayName ${formatArguments()}"
}
fun buildRoute(highlightEntryName: String? = null): String {
val highlightParam =
if (highlightEntryName == null)
""
else
"?${BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME}=$highlightEntryName"
return name + parameter.navLink(arguments) + highlightParam
}
fun hasRuntimeParam(): Boolean {
return parameter.hasRuntimeParam(arguments)
}
}
private fun List<NamedNavArgument>.normalize(arguments: Bundle? = null): Bundle? {
if (this.isEmpty()) return null
val normArgs = Bundle()
for (navArg in this) {
when (navArg.argument.type) {
NavType.StringType -> {
val value = arguments?.getString(navArg.name)
if (value != null)
normArgs.putString(navArg.name, value)
else
normArgs.putString("unset_" + navArg.name, null)
}
NavType.IntType -> {
if (arguments != null && arguments.containsKey(navArg.name))
normArgs.putInt(navArg.name, arguments.getInt(navArg.name))
else
normArgs.putString("unset_" + navArg.name, null)
}
}
}
return normArgs
}
private fun List<NamedNavArgument>.hasRuntimeParam(arguments: Bundle? = null): Boolean {
for (navArg in this) {
if (arguments == null || !arguments.containsKey(navArg.name)) return true
}
return false
}
fun String.toHashId(): String {
return this.hashCode().toString(36)
}

View File

@@ -30,12 +30,8 @@ class SettingsPageProviderRepository(
logMsg("Initialize Completed: ${pageProviderMap.size} spp")
}
fun getDefaultStartPageName(): String {
return if (rootPages.isNotEmpty()) {
rootPages[0].name
} else {
""
}
fun getDefaultStartPage(): String {
return if (rootPages.isEmpty()) "" else rootPages[0].buildRoute()
}
fun getAllRootPages(): Collection<SettingsPage> {

View File

@@ -49,7 +49,8 @@ val LocalNavController = compositionLocalOf<NavControllerWrapper> {
}
@Composable
fun navigator(route: String): () -> Unit {
fun navigator(route: String?): () -> Unit {
if (route == null) return {}
val navController = LocalNavController.current
return { navController.navigate(route) }
}

View File

@@ -21,7 +21,39 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import com.android.settingslib.spa.framework.common.EntryMarco
import com.android.settingslib.spa.framework.common.EntrySearchData
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.ui.createSettingsIcon
data class SimplePreferenceMarco(
val title: String,
val summary: String? = null,
val icon: ImageVector? = null,
val disabled: Boolean = false,
val clickRoute: String? = null,
val searchKeywords: List<String> = emptyList(),
) : EntryMarco {
@Composable
override fun UiLayout() {
Preference(model = object : PreferenceModel {
override val title: String = this@SimplePreferenceMarco.title
override val summary = stateOf(this@SimplePreferenceMarco.summary ?: "")
override val icon = createSettingsIcon(this@SimplePreferenceMarco.icon)
override val enabled = stateOf(!this@SimplePreferenceMarco.disabled)
override val onClick = navigator(clickRoute)
})
}
override fun getSearchData(): EntrySearchData {
return EntrySearchData(
title = this@SimplePreferenceMarco.title,
keyword = searchKeywords
)
}
}
/**
* The widget model for [Preference] widget.
@@ -71,9 +103,9 @@ interface PreferenceModel {
@Composable
fun Preference(model: PreferenceModel) {
val modifier = remember(model.enabled.value, model.onClick) {
model.onClick?.let { onClick ->
Modifier.clickable(enabled = model.enabled.value, onClick = onClick)
} ?: Modifier
model.onClick?.let { onClick ->
Modifier.clickable(enabled = model.enabled.value, onClick = onClick)
} ?: Modifier
}
BasePreference(
title = model.title,

View File

@@ -33,3 +33,8 @@ fun SettingsIcon(imageVector: ImageVector) {
tint = MaterialTheme.colorScheme.onSurface,
)
}
fun createSettingsIcon(imageVector: ImageVector?): (@Composable () -> Unit)? {
if (imageVector == null) return null
return { SettingsIcon(imageVector = imageVector) }
}