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

Bug: 244122804
Test: manual - build Spa gallery
Change-Id: I4853e17028dfe0eed4342b4691962b508bc87357
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 2229986..e5bf8ca 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -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>
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
new file mode 100644
index 0000000..52bbf75
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
@@ -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)
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 e75e76c..6f675a3 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
@@ -51,7 +51,7 @@
                 IllustrationPageProvider,
             ),
             rootPages = listOf(
-                SettingsPage(HomePageProvider.name)
+                SettingsPage.create(HomePageProvider.name)
             ) + ArgumentPageProvider.buildRootPages()
         )
     }
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 2428bba..7c57e75 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
@@ -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 @@
         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")
-        }
     }
 }
 
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
new file mode 100644
index 0000000..d1ec7d0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
@@ -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}")
+            })
+        }
+    }
+}
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 98734da..4452b81 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
@@ -37,38 +37,57 @@
 /**
  * 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 unique id of this entry, which is computed by name + owner + fromPage + toPage.
+    val id: Int,
 
-    // 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 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 @@
      */
     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 @@
  * 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 @@
 
     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 @@
             toPage = toPage,
 
             // attributes
-            isAllowSearch = computeSearchable(),
+            isAllowSearch = getIsSearchable(),
 
             // functions
             searchData = searchDataFn,
@@ -168,12 +219,7 @@
         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 @@
         }
     }
 }
+
+private fun String.toUniqueId(): Int {
+    return this.hashCode()
+}
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 e3a55e5..c5f72c5 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
@@ -26,15 +26,15 @@
  */
 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 @@
         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 @@
             }
         }
 
-        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]
     }
 }
 
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index d4d8ea4..c031fe8 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -82,7 +82,7 @@
         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))
         }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
index f91a34a..de8a65e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
@@ -56,7 +56,7 @@
     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")) {