blob: fb42f01b14f88d6828eb0e1d647b6d52eba1379a [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.framework.common
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import com.android.settingslib.spa.framework.compose.LocalNavController
const val INJECT_ENTRY_NAME = "INJECT"
const val ROOT_ENTRY_NAME = "ROOT"
interface EntryData {
val pageId: String?
get() = null
val entryId: String?
get() = null
val isHighlighted: Boolean
get() = false
}
val LocalEntryDataProvider =
compositionLocalOf<EntryData> { object : EntryData {} }
/**
* 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: String,
// The name of the page, which is used to compute the unique id, and need to be stable.
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,
// Defines linking of Settings entries
val fromPage: SettingsPage? = null,
val toPage: SettingsPage? = null,
/**
* ========================================
* Defines entry attributes here.
* ========================================
*/
val isAllowSearch: Boolean = false,
val isSearchDataDynamic: Boolean = false,
/**
* ========================================
* Defines entry APIs to get data here.
* ========================================
*/
/**
* API to get Search related data for this entry.
* Returns null if this entry is not available for the search at the moment.
*/
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
* support the case that the injection page owner wants to maintain both data and UI of the
* injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
* use each entries' UI rendering function in the page instead.
*/
private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
) {
fun formatContent(): String {
val content = listOf(
"id = $id",
"owner = ${owner.formatDisplayTitle()}",
"linkFrom = ${fromPage?.formatDisplayTitle()}",
"linkTo = ${toPage?.formatDisplayTitle()}",
"${getSearchData()?.format()}",
)
return content.joinToString("\n")
}
fun displayTitle(): String {
return "${owner.displayName}:$displayName"
}
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
}
private fun fullArgument(runtimeArguments: Bundle? = null): Bundle {
val arguments = Bundle()
if (owner.arguments != null) arguments.putAll(owner.arguments)
// Put runtime args later, which can override page args.
if (runtimeArguments != null) arguments.putAll(runtimeArguments)
return arguments
}
fun getSearchData(runtimeArguments: Bundle? = null): EntrySearchData? {
return searchDataImpl(fullArgument(runtimeArguments))
}
@Composable
fun UiLayout(runtimeArguments: Bundle? = null) {
CompositionLocalProvider(provideLocalEntryData()) {
uiLayoutImpl(fullArgument(runtimeArguments))
}
}
@Composable
fun provideLocalEntryData(): ProvidedValue<EntryData> {
val controller = LocalNavController.current
return LocalEntryDataProvider provides remember {
object : EntryData {
override val pageId = containerPage().id
override val entryId = id
override val isHighlighted = controller.highlightEntryId == id
}
}
}
}
/**
* 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
// Attributes
private var isAllowSearch: Boolean = false
private var isSearchDataDynamic: Boolean = false
// Functions
private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
fun build(): SettingsEntry {
return SettingsEntry(
id = id(),
name = name,
owner = owner,
displayName = displayName,
// linking data
fromPage = fromPage,
toPage = toPage,
// attributes
isAllowSearch = isAllowSearch,
isSearchDataDynamic = isSearchDataDynamic,
// functions
searchDataImpl = searchDataFn,
uiLayoutImpl = uiLayoutFn,
)
}
fun setDisplayName(displayName: String): SettingsEntryBuilder {
this.displayName = displayName
return this
}
fun setLink(
fromPage: SettingsPage? = null,
toPage: SettingsPage? = null
): SettingsEntryBuilder {
if (fromPage != null) this.fromPage = fromPage
if (toPage != null) this.toPage = toPage
return this
}
fun setIsAllowSearch(isAllowSearch: Boolean): SettingsEntryBuilder {
this.isAllowSearch = isAllowSearch
return this
}
fun setIsSearchDataDynamic(isDynamic: Boolean): SettingsEntryBuilder {
this.isSearchDataDynamic = isDynamic
return this
}
fun setMacro(fn: (arguments: Bundle?) -> EntryMacro): SettingsEntryBuilder {
setSearchDataFn { fn(it).getSearchData() }
setUiLayoutFn {
val macro = remember { fn(it) }
macro.UiLayout()
}
return this
}
fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
this.searchDataFn = fn
return this
}
fun setUiLayoutFn(fn: @Composable (arguments: Bundle?) -> Unit): SettingsEntryBuilder {
this.uiLayoutFn = fn
return this
}
// The unique id of this entry, which is computed by name + owner + fromPage + toPage.
private fun id(): String {
return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId()
}
companion object {
fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
return SettingsEntryBuilder(entryName, owner)
}
fun createLinkFrom(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
return create(entryName, owner).setLink(fromPage = owner)
}
fun createLinkTo(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
return create(entryName, owner).setLink(toPage = owner)
}
fun create(owner: SettingsPage, entryName: String, displayName: String? = null):
SettingsEntryBuilder {
return SettingsEntryBuilder(entryName, owner).setDisplayName(displayName ?: entryName)
}
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)
}
}
}