diff options
20 files changed, 762 insertions, 364 deletions
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt index 317346d26f89..cfcd75f86d1f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt @@ -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", +) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt index 783fef7e91cf..773e5f3929fb 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt @@ -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) ) ) } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index a16ee6c85409..c8a4650d3f01 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -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() diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt index e32de7a67d9d..cc0a79adaa88 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt @@ -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() diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt index 6e86fd785a0e..e75d09b7d17c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt @@ -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)) } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt index cbd028d17cba..9fc736c911af 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt @@ -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) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = "Preference" - override val summary = produceState(initialValue = " ") { - delay(1000L) - value = "Async summary" - } - }) - }.build() + .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( - SettingsEntryBuilder.create("Click me", owner) + createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE) .setIsAllowSearch(true) + .setSearchDataFn { + EntrySearchData(title = ASYNC_PREFERENCE_TITLE) + } .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 asyncSummary = remember { model.getAsyncSummary() } + Preference( + object : PreferenceModel { + override val title = ASYNC_PREFERENCE_TITLE + override val summary = asyncSummary } - }) + ) }.build() ) entryList.add( - SettingsEntryBuilder.create("Ticker", owner) + createEntry(EntryEnum.MANUAL_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() } - }) + 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("Disabled", owner) + createEntry(EntryEnum.AUTO_UPDATE_PREFERENCE) .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() } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt new file mode 100644 index 000000000000..1188e1e006b2 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt @@ -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 + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt index e5a1862599b6..c1bed07b0385 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt @@ -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) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt index 8aef2c6aa068..b196300abb63 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt @@ -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()}") }) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt index 90ce182cd2bd..5e4419a60ef0 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt @@ -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) - ) +fun EntryProvider.QueryEnum.getColumns(): Array<String> { + return columnNames.map { it.id }.toTypedArray() +} - 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.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 } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMarco.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMarco.kt new file mode 100644 index 000000000000..8399d125b24e --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMarco.kt @@ -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 +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt new file mode 100644 index 000000000000..9b262afc9c53 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt @@ -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") + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/PageModel.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/PageModel.kt index ee12f02e5a7a..edcca18017c1 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/PageModel.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/PageModel.kt @@ -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 diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt index 445c4ebfcee2..2239066201bf 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt @@ -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() } - @Composable - fun UiLayout(runtimeArguments: Bundle? = null) { + private fun fullArgument(runtimeArguments: Bundle? = null): Bundle { val arguments = Bundle() if (owner.arguments != null) arguments.putAll(owner.arguments) if (runtimeArguments != null) arguments.putAll(runtimeArguments) - uiLayoutImpl(arguments) + return 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 getSearchData(runtimeArguments: Bundle? = null): EntrySearchData? { + return searchDataImpl(fullArgument(runtimeArguments)) } - fun setArguments(arguments: Bundle?): SettingsPageBuilder { - this.arguments = arguments - return this + @Composable + fun UiLayout(runtimeArguments: Bundle? = null) { + 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) } - } -} - -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) - } + fun createRoot(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder { + val name = displayName ?: "${ROOT_ENTRY_NAME}_${owner.displayName}" + return createLinkTo(ROOT_ENTRY_NAME, owner).setDisplayName(name) } } - 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 } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt index a4e5a58246ff..77f064d35eb7 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt @@ -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] } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt new file mode 100644 index 000000000000..124743a23274 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt @@ -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) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepository.kt index 6adda6b2aa01..77a157fe815b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepository.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepository.kt @@ -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> { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavControllerWrapper.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavControllerWrapper.kt index 32ef0bb3d19b..13a2cc9d252c 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavControllerWrapper.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavControllerWrapper.kt @@ -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) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt index 0e6f53abca40..39b8d57d9da4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt @@ -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, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt index 4f28e378e510..25a4e7088a1a 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt @@ -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) } +} |