Add entry provider in SPA framework, which is going to provide entry data for search & hierarchy generation.

Bug: 244122804
Test: manual - build Spa gallery
Change-Id: I57bb80e0749ca62c70abb09510ae8c9b95007c56
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index e34fedd..e583138 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -35,5 +35,13 @@
             android:name=".GalleryDebugActivity"
             android:exported="true">
         </activity>
+
+        <provider
+            android:name=".GalleryEntryProvider"
+            android:authorities="com.android.spa.gallery.provider"
+            android:enabled="true"
+            android:exported="false">
+        </provider>
+
     </application>
 </manifest>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
new file mode 100644
index 0000000..3210eb5
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.EntryProvider
+
+class GalleryEntryProvider : EntryProvider(
+    SpaEnvironment.EntryRepository,
+    "com.android.settingslib.spa.gallery/.MainActivity",
+)
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 bd5aaa7..8aef2c6 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
@@ -17,6 +17,7 @@
 package com.android.settingslib.spa.framework
 
 import android.content.Intent
+import android.net.Uri
 import android.os.Bundle
 import android.util.Log
 import androidx.activity.ComponentActivity
@@ -32,6 +33,7 @@
 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
@@ -55,21 +57,12 @@
 open class DebugActivity(
     private val entryRepository: SettingsEntryRepository,
     private val browseActivityClass: Class<*>,
+    private val entryProviderAuthorities: String? = null,
 ) : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         setTheme(R.style.Theme_SpaLib_DayNight)
         super.onCreate(savedInstanceState)
-
-        val packageName = browseActivityClass.packageName
-        val className = browseActivityClass.toString().removePrefix("class $packageName")
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            if (pageWithEntry.page.hasRuntimeParam()) continue
-            val route = pageWithEntry.page.buildRoute()
-            Log.d(
-                "DEBUG ACTIVITY",
-                "adb shell am start -n $packageName/$className -e $KEY_DESTINATION $route"
-            )
-        }
+        displayDebugMessage()
 
         setContent {
             SettingsTheme {
@@ -78,6 +71,30 @@
         }
     }
 
+    private fun displayDebugMessage() {
+        if (entryProviderAuthorities == null) return
+
+        try {
+            contentResolver.query(
+                Uri.parse("content://$entryProviderAuthorities/${PAGE_INFO_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 hasRuntimeParam =
+                        cursor.getInt(PAGE_INFO_QUERY.getIndex(ColumnName.HAS_RUNTIME_PARAM)) == 1
+                    Log.d(
+                        "DEBUG ACTIVITY", "Page Info: $route ($entryCount) " +
+                            (if (hasRuntimeParam) "with" else "no") + "-runtime-params"
+                    )
+                }
+            }
+        } catch (e: Exception) {
+            Log.e("DEBUG ACTIVITY", "Provider querying exception:", e)
+        }
+    }
+
     @Composable
     private fun MainContent() {
         val navController = rememberNavController()
@@ -122,7 +139,7 @@
             for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
                 Preference(object : PreferenceModel {
                     override val title =
-                        "${pageWithEntry.page.displayName} (${pageWithEntry.entries.size})"
+                        "${pageWithEntry.page.name} (${pageWithEntry.entries.size})"
                     override val summary = pageWithEntry.page.formatArguments().toState()
                     override val onClick =
                         navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id}")
@@ -142,7 +159,7 @@
     fun OnePage(arguments: Bundle?) {
         val id = arguments!!.getInt(PARAM_NAME_PAGE_ID)
         val pageWithEntry = entryRepository.getPageWithEntry(id)!!
-        RegularScaffold(title = "Page ${pageWithEntry.page.displayName}") {
+        RegularScaffold(title = "Page ${pageWithEntry.page.name}") {
             Text(text = pageWithEntry.page.formatArguments())
             Text(text = "Entry size: ${pageWithEntry.entries.size}")
             Preference(model = object : PreferenceModel {
@@ -158,7 +175,7 @@
     fun OneEntry(arguments: Bundle?) {
         val id = arguments!!.getInt(PARAM_NAME_ENTRY_ID)
         val entry = entryRepository.getEntry(id)!!
-        RegularScaffold(title = "Entry ${entry.displayName}") {
+        RegularScaffold(title = "Entry ${entry.displayName()}") {
             Preference(model = object : PreferenceModel {
                 override val title = "open entry"
                 override val enabled = (!entry.hasRuntimeParam()).toState()
@@ -172,9 +189,9 @@
     private fun EntryList(entries: Collection<SettingsEntry>) {
         for (entry in entries) {
             Preference(object : PreferenceModel {
-                override val title = entry.displayName
+                override val title = entry.displayName()
                 override val summary =
-                    "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState()
+                    "${entry.fromPage?.name} -> ${entry.toPage?.name}".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
new file mode 100644
index 0000000..90ce182
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.SettingsEntryRepository
+
+/**
+ * Enum to define all column names in provider.
+ */
+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() {
+
+    private var mMatcher: UriMatcher? = null
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+        TODO("Implement this to handle requests to delete one or more rows")
+    }
+
+    override fun getType(uri: Uri): String? {
+        TODO(
+            "Implement this to handle requests for the MIME type of the data" +
+                "at the given URI"
+        )
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? {
+        TODO("Implement this to handle requests to insert a new row.")
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int {
+        TODO("Implement this to handle requests to update one or more rows.")
+    }
+
+    override fun onCreate(): Boolean {
+        return true
+    }
+
+    override fun attachInfo(context: Context?, info: ProviderInfo?) {
+        mMatcher = UriMatcher(UriMatcher.NO_MATCH)
+        if (info != null) {
+            mMatcher!!.addURI(
+                info.authority,
+                PAGE_START_COMMAND_QUERY.queryPath,
+                PAGE_START_COMMAND_QUERY.queryMatchCode
+            )
+            mMatcher!!.addURI(
+                info.authority,
+                PAGE_INFO_QUERY.queryPath,
+                PAGE_INFO_QUERY.queryMatchCode
+            )
+        }
+        super.attachInfo(context, info)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? {
+        return try {
+            when (mMatcher!!.match(uri)) {
+                PAGE_START_COMMAND_QUERY.queryMatchCode -> queryPageStartCommand()
+                PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
+                else -> throw UnsupportedOperationException("Unknown Uri $uri")
+            }
+        } catch (e: UnsupportedOperationException) {
+            throw e
+        } catch (e: Exception) {
+            Log.e("EntryProvider", "Provider querying exception:", e)
+            null
+        }
+    }
+
+    private fun queryPageStartCommand(): Cursor {
+        val componentName = browseActivityComponentName ?: "[component-name]"
+        val cursor = MatrixCursor(PAGE_START_COMMAND_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val page = pageWithEntry.page
+            if (!page.hasRuntimeParam()) {
+                cursor.newRow().add(
+                    ColumnName.PAGE_START_ADB.id,
+                    "adb shell am start -n $componentName" +
+                        " -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}"
+                )
+            }
+        }
+        return cursor
+    }
+
+    private fun queryPageInfo(): Cursor {
+        val cursor = MatrixCursor(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)
+        }
+        return cursor
+    }
+
+    companion object {
+        val PAGE_START_COMMAND_QUERY = QueryDefinition(
+            "page_start", 1,
+            listOf(ColumnName.PAGE_START_ADB)
+        )
+
+        val PAGE_INFO_QUERY = QueryDefinition(
+            "page_info", 2,
+            listOf(
+                ColumnName.PAGE_NAME,
+                ColumnName.PAGE_ROUTE,
+                ColumnName.ENTRY_COUNT,
+                ColumnName.HAS_RUNTIME_PARAM,
+            )
+        )
+    }
+}
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 b0a1cbe..445c4eb 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
@@ -47,10 +47,6 @@
     // 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,
-
     // Defined parameters of this page.
     val parameter: List<NamedNavArgument> = emptyList(),
 
@@ -74,7 +70,7 @@
     }
 
     fun formatAll(): String {
-        return "$displayName ${formatArguments()}"
+        return "$name ${formatArguments()}"
     }
 
     fun buildRoute(highlightEntryName: String? = null): String {
@@ -104,10 +100,6 @@
     // 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,
@@ -146,7 +138,7 @@
     val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
 ) {
     fun formatAll(): String {
-        val content = listOf<String>(
+        val content = listOf(
             "owner = ${owner.formatAll()}",
             "linkFrom = ${fromPage?.formatAll()}",
             "linkTo = ${toPage?.formatAll()}",
@@ -154,6 +146,11 @@
         return content.joinToString("\n")
     }
 
+    // The display name of the entry, for better readability.
+    fun displayName(): String {
+        return "${owner.name}:$name"
+    }
+
     private fun getDisplayPage(): SettingsPage {
         // Display the entry on its from-page, or on its owner page if the from-page is unset.
         return fromPage ?: owner
@@ -185,7 +182,6 @@
     private val name: String,
     private val parameter: List<NamedNavArgument> = emptyList()
 ) {
-    private var displayName: String? = null
     private var arguments: Bundle? = null
 
     fun build(): SettingsPage {
@@ -193,7 +189,6 @@
         return SettingsPage(
             id = "$name:${normArguments?.toString()}".toUniqueId(),
             name = name,
-            displayName = displayName ?: name,
             parameter = parameter,
             arguments = arguments,
         )
@@ -209,7 +204,6 @@
  * The helper to build a Settings Entry instance.
  */
 class SettingsEntryBuilder(private val name: String, private val owner: SettingsPage) {
-    private var displayName: String? = null
     private var fromPage: SettingsPage? = null
     private var toPage: SettingsPage? = null
     private var isAllowSearch: Boolean? = null
@@ -220,7 +214,6 @@
     fun build(): SettingsEntry {
         return SettingsEntry(
             id = "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toUniqueId(),
-            displayName = displayName ?: "${owner.displayName}:$name",
             name = name,
             owner = owner,
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
index aaf8107..d7d7750 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
@@ -25,9 +25,12 @@
 }
 
 fun List<NamedNavArgument>.navLink(arguments: Bundle? = null): String {
-    if (arguments == null) return ""
     val argsArray = mutableListOf<String>()
     for (navArg in this) {
+        if (arguments == null || !arguments.containsKey(navArg.name)) {
+            argsArray.add("[rt]")
+            continue
+        }
         when (navArg.argument.type) {
             NavType.StringType -> {
                 argsArray.add(arguments.getString(navArg.name, ""))