Add debug page in gallery to show page & entry debug information.

Bug: 244122804
Test: manual - build Spa gallery
Change-Id: I4853e17028dfe0eed4342b4691962b508bc87357
This commit is contained in:
Zekan Qian
2022-09-08 16:13:53 +08:00
parent 834c811cbe
commit d3a94cf497
9 changed files with 277 additions and 57 deletions

View File

@@ -29,6 +29,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<activity
android:name=".GalleryDebugActivity"
android:exported="true">
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
/*
* 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
import com.android.settingslib.spa.framework.DebugActivity
class GalleryDebugActivity : DebugActivity(SpaEnvironment.EntryRepository)

View File

@@ -51,7 +51,7 @@ object SpaEnvironment {
IllustrationPageProvider,
),
rootPages = listOf(
SettingsPage(HomePageProvider.name)
SettingsPage.create(HomePageProvider.name)
) + ArgumentPageProvider.buildRootPages()
)
}

View File

@@ -17,15 +17,12 @@
package com.android.settingslib.spa.gallery.home
import android.os.Bundle
import androidx.compose.material3.Button
import androidx.compose.material3.Text
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.SettingsPageProvider
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.gallery.R
import com.android.settingslib.spa.gallery.SpaEnvironment
import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
import com.android.settingslib.spa.gallery.page.FooterPageProvider
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
@@ -55,17 +52,6 @@ private fun HomePage() {
SettingsPagerPageProvider.EntryItem()
FooterPageProvider.EntryItem()
IllustrationPageProvider.EntryItem()
/**
* A test button to generate hierarchy.
* TODO: remove it once the content provider is ready.
*/
Button(onClick = {
SpaEnvironment.EntryRepository.printAllPages()
SpaEnvironment.EntryRepository.printAllEntries()
}) {
Text(text = "Generate Entry")
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
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.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryRepository
import com.android.settingslib.spa.framework.compose.localNavController
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.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
private const val ROUTE_ROOT = "root"
private const val ROUTE_All_PAGES = "pages"
private const val ROUTE_All_ENTRIES = "entries"
private const val ROUTE_PAGE = "page"
private const val ROUTE_ENTRY = "entry"
private const val PARAM_NAME_PAGE_ID = "pid"
private const val PARAM_NAME_ENTRY_ID = "eid"
open class DebugActivity(
private val entryRepository: SettingsEntryRepository
) : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.Theme_SpaLib_DayNight)
super.onCreate(savedInstanceState)
setContent {
SettingsTheme {
MainContent()
}
}
}
@Composable
private fun MainContent() {
val navController = rememberNavController()
CompositionLocalProvider(navController.localNavController()) {
NavHost(navController, ROUTE_ROOT) {
composable(route = ROUTE_ROOT) { RootPage() }
composable(route = ROUTE_All_PAGES) { AllPages() }
composable(route = ROUTE_All_ENTRIES) { AllEntries() }
composable(
route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
arguments = listOf(
navArgument(PARAM_NAME_PAGE_ID) { type = NavType.IntType },
)
) { navBackStackEntry -> OnePage(navBackStackEntry.arguments) }
composable(
route = "$ROUTE_ENTRY/{$PARAM_NAME_ENTRY_ID}",
arguments = listOf(
navArgument(PARAM_NAME_ENTRY_ID) { type = NavType.IntType },
)
) { navBackStackEntry -> OneEntry(navBackStackEntry.arguments) }
}
}
}
@Composable
fun RootPage() {
HomeScaffold(title = "Entry Debug") {
Preference(object : PreferenceModel {
override val title = "List All Pages"
override val onClick = navigator(route = ROUTE_All_PAGES)
})
Preference(object : PreferenceModel {
override val title = "List All Entries"
override val onClick = navigator(route = ROUTE_All_ENTRIES)
})
}
}
@Composable
fun AllPages() {
RegularScaffold(title = "All Pages") {
for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
Preference(object : PreferenceModel {
override val title =
"${pageWithEntry.page.displayName} (${pageWithEntry.entries.size})"
override val summary = pageWithEntry.page.formatArguments().toState()
override val onClick =
navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id}")
})
}
}
}
@Composable
fun AllEntries() {
RegularScaffold(title = "All Entries") {
EntryList(entryRepository.getAllEntries())
}
}
@Composable
fun OnePage(arguments: Bundle?) {
val id = arguments!!.getInt(PARAM_NAME_PAGE_ID)
val pageWithEntry = entryRepository.getPageWithEntry(id)!!
RegularScaffold(title = "Page ${pageWithEntry.page.displayName}") {
Text(text = pageWithEntry.page.formatArguments())
Text(text = "Entry size: ${pageWithEntry.entries.size}")
EntryList(pageWithEntry.entries)
}
}
@Composable
fun OneEntry(arguments: Bundle?) {
val id = arguments!!.getInt(PARAM_NAME_ENTRY_ID)
val entry = entryRepository.getEntry(id)!!
RegularScaffold(title = "Entry ${entry.displayName}") {
Text (text = entry.formatAll())
}
}
@Composable
private fun EntryList(entries: Collection<SettingsEntry>) {
for (entry in entries) {
Preference(object : PreferenceModel {
override val title = entry.displayName
override val summary =
"${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState()
override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}")
})
}
}
}

View File

@@ -37,38 +37,57 @@ data class UiData(val title: String = "")
/**
* Defines data to identify a Settings page.
*/
data class SettingsPage(val name: String = "", val arguments: Bundle? = null) {
override fun toString(): String {
val argsStr = arguments?.toString()?.removeRange(0, 6) ?: ""
return name + argsStr
}
data class SettingsPage(
// The unique id of this page, which is computed by name + 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,
// The display name of the page, for better readability.
// By default, it is the same as name.
val displayName: String,
// 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, parameter.normalize(arguments))
return SettingsPageBuilder(name, parameter).setArguments(arguments).build()
}
}
fun formatArguments(): String {
if (arguments == null || arguments.isEmpty) return "[No arguments]"
return arguments.toString().removeRange(0, 6)
}
fun formatAll(): String {
return "$displayName ${formatArguments()}"
}
}
/**
* Defines data of a Settings entry.
*/
data class SettingsEntry(
// The unique id of this entry.
// By default, it is computed by name + owner + fromPage + toPage
val id: String,
// The display name of this entry, which is used to be shown in hierarchy.
// By default, it is computed by name + owner
val displayName: String,
// 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,
// The owner page of this entry.
val owner: SettingsPage,
// The display name of the entry, for better readability.
// By default, it is $owner:$name
val displayName: String,
// Defines linking of Settings entries
val fromPage: SettingsPage? = null,
val toPage: SettingsPage? = null,
@@ -106,8 +125,41 @@ data class SettingsEntry(
*/
val uiLayout: (@Composable () -> Unit) = {},
) {
override fun toString(): String {
return displayName + "(${fromPage?.toString()}->${toPage?.toString()})"
fun formatAll(): String {
val content = listOf<String>(
"owner = ${owner.formatAll()}",
"linkFrom = ${fromPage?.formatAll()}",
"linkTo = ${toPage?.formatAll()}",
)
return content.joinToString("\n")
}
}
data class SettingsPageWithEntry(
val page: SettingsPage,
val entries: List<SettingsEntry>,
)
class SettingsPageBuilder(
private val name: String,
private val parameter: List<NamedNavArgument> = emptyList()
) {
private var displayName: String? = null
private var arguments: Bundle? = null
fun build(): SettingsPage {
val normArguments = parameter.normalize(arguments)
return SettingsPage(
id = "$name:${normArguments?.toString()}".toUniqueId(),
name = name,
displayName = displayName ?: name,
arguments = normArguments,
)
}
fun setArguments(arguments: Bundle?): SettingsPageBuilder {
this.arguments = arguments
return this
}
}
@@ -115,7 +167,6 @@ data class SettingsEntry(
* The helper to build a Settings Entry instance.
*/
class SettingsEntryBuilder(private val name: String, private val owner: SettingsPage) {
private var uniqueId: String? = null
private var displayName: String? = null
private var fromPage: SettingsPage? = null
private var toPage: SettingsPage? = null
@@ -126,8 +177,8 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
fun build(): SettingsEntry {
return SettingsEntry(
id = computeUniqueId(),
displayName = computeDisplayName(),
id = "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toUniqueId(),
displayName = displayName ?: "${owner.displayName}:$name",
name = name,
owner = owner,
@@ -136,7 +187,7 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
toPage = toPage,
// attributes
isAllowSearch = computeSearchable(),
isAllowSearch = getIsSearchable(),
// functions
searchData = searchDataFn,
@@ -168,12 +219,7 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
return this
}
private fun computeUniqueId(): String =
uniqueId ?: "$owner:$name" + fromPage?.toString() + toPage?.toString()
private fun computeDisplayName(): String = displayName ?: "$owner:$name"
private fun computeSearchable(): Boolean = isAllowSearch ?: false
private fun getIsSearchable(): Boolean = isAllowSearch ?: false
companion object {
fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
@@ -197,3 +243,7 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
}
}
}
private fun String.toUniqueId(): Int {
return this.hashCode()
}

View File

@@ -26,15 +26,15 @@ private const val MAX_ENTRY_SIZE = 5000
*/
class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
// Map of entry unique Id to entry
private val entryMap: Map<String, SettingsEntry>
private val entryMap: Map<Int, SettingsEntry>
// Map of Settings page to its contained entries.
private val pageToEntryListMap: Map<String, List<SettingsEntry>>
private val pageWithEntryMap: Map<Int, SettingsPageWithEntry>
init {
logMsg("Initialize")
entryMap = mutableMapOf()
pageToEntryListMap = mutableMapOf()
pageWithEntryMap = mutableMapOf()
val entryQueue = LinkedList<SettingsEntry>()
for (page in sppRepository.getAllRootPages()) {
@@ -48,10 +48,10 @@ class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
while (entryQueue.isNotEmpty() && entryMap.size < MAX_ENTRY_SIZE) {
val entry = entryQueue.pop()
val page = entry.toPage
if (page == null || pageToEntryListMap.containsKey(page.toString())) continue
if (page == null || pageWithEntryMap.containsKey(page.id)) continue
val spp = sppRepository.getProviderOrNull(page.name) ?: continue
val newEntries = spp.buildEntry(page.arguments)
pageToEntryListMap[page.toString()] = newEntries
pageWithEntryMap[page.id] = SettingsPageWithEntry(page, newEntries)
for (newEntry in newEntries) {
if (!entryMap.containsKey(newEntry.id)) {
entryQueue.push(newEntry)
@@ -60,19 +60,23 @@ class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
}
}
logMsg("Initialize Completed: ${entryMap.size} entries in ${pageToEntryListMap.size} pages")
logMsg("Initialize Completed: ${entryMap.size} entries in ${pageWithEntryMap.size} pages")
}
fun printAllPages() {
for (entry in pageToEntryListMap.entries) {
logMsg("page: ${entry.key} with ${entry.value.size} entries")
}
fun getAllPageWithEntry(): Collection<SettingsPageWithEntry> {
return pageWithEntryMap.values
}
fun printAllEntries() {
for (entry in entryMap.values) {
logMsg("entry: $entry")
}
fun getPageWithEntry(pageId: Int): SettingsPageWithEntry? {
return pageWithEntryMap[pageId]
}
fun getAllEntries(): Collection<SettingsEntry> {
return entryMap.values
}
fun getEntry(entryId: Int): SettingsEntry? {
return entryMap[entryId]
}
}

View File

@@ -82,7 +82,7 @@ internal class TogglePermissionAppInfoPageProvider(
internal fun navigator(permissionType: String, app: ApplicationInfo) =
navigator(route = "$PAGE_NAME/$permissionType/${app.toRoute()}")
internal fun buildPageId(permissionType: String): SettingsPage {
internal fun buildPageData(permissionType: String): SettingsPage {
return SettingsPage.create(
PAGE_NAME, PAGE_PARAMETER, bundleOf(PERMISSION to permissionType))
}

View File

@@ -56,7 +56,7 @@ internal class TogglePermissionAppListPageProvider(
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val permissionType = parameter.getStringArg(PERMISSION, arguments)!!
val appListPage = SettingsPage.create(name, parameter, arguments)
val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageId(permissionType)
val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageData(permissionType)
val entryList = mutableListOf<SettingsEntry>()
// TODO: add more categories, such as personal, work, cloned, etc.
for (category in listOf("personal")) {