Some code refactoring.

Move unique id generation logic into separate file.
Move createSettingsPage function to Spp.
Add getPageProvider API in Spp for ease use.
Add NullPageProvider instance.

Bug: 244122804
Test: unit-test & local build gallery
Change-Id: Ic99ab6a99db98d0666a4c5c59d8841e18d628eeb
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
index 838c0cf..494e3cc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
@@ -161,7 +161,7 @@
                 .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
                 .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
                 .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
-                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
+                .add(ColumnEnum.PAGE_BROWSABLE.id, if (page.isBrowsable()) 1 else 0)
         }
         return cursor
     }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
index bb9a134..fc6160e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
@@ -28,7 +28,7 @@
     PAGE_ROUTE("pageRoute"),
     PAGE_INTENT_URI("pageIntent"),
     PAGE_ENTRY_COUNT("entryCount"),
-    HAS_RUNTIME_PARAM("hasRuntimeParam"),
+    PAGE_BROWSABLE("pageBrowsable"),
     PAGE_START_ADB("pageStartAdb"),
 
     // Columns related to entry
@@ -67,7 +67,7 @@
             ColumnEnum.PAGE_ROUTE,
             ColumnEnum.PAGE_INTENT_URI,
             ColumnEnum.PAGE_ENTRY_COUNT,
-            ColumnEnum.HAS_RUNTIME_PARAM,
+            ColumnEnum.PAGE_BROWSABLE,
         )
     ),
 
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 bccd8aa..f1b1abd 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
@@ -39,7 +39,7 @@
 import androidx.navigation.NavGraph.Companion.findStartDestination
 import com.android.settingslib.spa.R
 import com.android.settingslib.spa.framework.common.LogCategory
-import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.NullPageProvider
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
@@ -106,14 +106,13 @@
 
 @Composable
 private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) {
-    val nullPage = SettingsPage.createNull()
     AnimatedNavHost(
         navController = navController,
-        startDestination = nullPage.sppName,
+        startDestination = NullPageProvider.name,
     ) {
         val slideEffect = tween<IntOffset>(durationMillis = 300)
         val fadeEffect = tween<Float>(durationMillis = 300)
-        composable(nullPage.sppName) {}
+        composable(NullPageProvider.name) {}
         for (spp in allProvider) {
             composable(
                 route = spp.name + spp.parameter.navRoute(),
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 44714ab..4b73e94 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
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.remember
 import com.android.settingslib.spa.framework.compose.LocalNavController
+import com.android.settingslib.spa.framework.util.genEntryId
 
 private const val INJECT_ENTRY_NAME = "INJECT"
 private const val ROOT_ENTRY_NAME = "ROOT"
@@ -188,7 +189,7 @@
         val page = fromPage ?: owner
         val isEnabled = page.isEnabled()
         return SettingsEntry(
-            id = id(),
+            id = genEntryId(name, owner, fromPage, toPage),
             name = name,
             owner = owner,
             displayName = displayName,
@@ -274,11 +275,6 @@
         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)
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 14b1629..14dc785 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
@@ -49,7 +49,7 @@
         entryMap = mutableMapOf()
         pageWithEntryMap = mutableMapOf()
 
-        val nullPage = SettingsPage.createNull()
+        val nullPage = NullPageProvider.createSettingsPage()
         val entryQueue = LinkedList<SettingsEntry>()
         for (page in sppRepository.getAllRootPages()) {
             val rootEntry =
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
index 94cfcc2..c810648 100644
--- 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
@@ -19,12 +19,9 @@
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.framework.util.isRuntimeParam
 import com.android.settingslib.spa.framework.util.navLink
-import com.android.settingslib.spa.framework.util.normalize
-import com.android.settingslib.spa.framework.util.normalizeArgList
-
-private const val NULL_PAGE_NAME = "NULL"
 
 /**
  * Defines data to identify a Settings page.
@@ -46,10 +43,7 @@
     val arguments: Bundle? = null,
 ) {
     companion object {
-        fun createNull(): SettingsPage {
-            return create(NULL_PAGE_NAME)
-        }
-
+        // TODO: cleanup it once all its usage in Settings are switched to Spp.createSettingsPage
         fun create(
             name: String,
             displayName: String? = null,
@@ -57,23 +51,13 @@
             arguments: Bundle? = null
         ): SettingsPage {
             return SettingsPage(
-                id = id(name, parameter, arguments),
+                id = genPageId(name, parameter, arguments),
                 sppName = name,
                 displayName = displayName ?: name,
                 parameter = parameter,
                 arguments = arguments
             )
         }
-
-        // The unique id of this page, which is computed by name + normalized(arguments)
-        private fun id(
-            name: String,
-            parameter: List<NamedNavArgument> = emptyList(),
-            arguments: Bundle? = null
-        ): String {
-            val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
-            return "$name:${normArguments?.toString()}".toHashId()
-        }
     }
 
     // Returns if this Settings Page is created by the given Spp.
@@ -85,43 +69,20 @@
         return sppName + parameter.navLink(arguments)
     }
 
-    fun hasRuntimeParam(): Boolean {
-        for (navArg in parameter) {
-            if (navArg.isRuntimeParam()) return true
-        }
-        return false
-    }
-
     fun isBrowsable(): Boolean {
-        return !isCreateBy(NULL_PAGE_NAME) && !hasRuntimeParam()
-    }
-
-    private fun getProvider(): SettingsPageProvider? {
-        if (!SpaEnvironmentFactory.isReady()) return null
-        val pageProviderRepository by SpaEnvironmentFactory.instance.pageProviderRepository
-        return pageProviderRepository.getProviderOrNull(sppName)
+        if (sppName == NullPageProvider.name) return false
+        for (navArg in parameter) {
+            if (navArg.isRuntimeParam()) return false
+        }
+        return true
     }
 
     fun isEnabled(): Boolean {
-        return getProvider()?.isEnabled(arguments) ?: false
+        return getPageProvider(sppName)?.isEnabled(arguments) ?: false
     }
 
     @Composable
     fun UiLayout() {
-        getProvider()?.Page(arguments)
+        getPageProvider(sppName)?.Page(arguments)
     }
 }
-
-fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
-    return SettingsPage.create(
-        name = name,
-        displayName = displayName + parameter.normalizeArgList(arguments, eraseRuntimeValues = true)
-            .joinToString("") { arg -> "/$arg" },
-        parameter = parameter,
-        arguments = arguments,
-    )
-}
-
-fun String.toHashId(): String {
-    return this.hashCode().toUInt().toString(36)
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index c564130..18f964e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -20,8 +20,12 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.genPageId
+import com.android.settingslib.spa.framework.util.normalizeArgList
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
+private const val NULL_PAGE_NAME = "NULL"
+
 /**
  * An SettingsPageProvider which is used to create Settings page instances.
  */
@@ -62,3 +66,24 @@
         }
     }
 }
+
+fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
+    return SettingsPage(
+        id = genPageId(name, parameter, arguments),
+        sppName = name,
+        displayName = displayName + parameter.normalizeArgList(arguments, eraseRuntimeValues = true)
+            .joinToString("") { arg -> "/$arg" },
+        parameter = parameter,
+        arguments = arguments,
+    )
+}
+
+object NullPageProvider : SettingsPageProvider {
+    override val name = NULL_PAGE_NAME
+}
+
+fun getPageProvider(sppName: String): SettingsPageProvider? {
+    if (!SpaEnvironmentFactory.isReady()) return null
+    val pageProviderRepository by SpaEnvironmentFactory.instance.pageProviderRepository
+    return pageProviderRepository.getProviderOrNull(sppName)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt
new file mode 100644
index 0000000..3b0ff7d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.util
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.common.SettingsPage
+
+fun genPageId(
+    sppName: String,
+    parameter: List<NamedNavArgument> = emptyList(),
+    arguments: Bundle? = null
+): String {
+    val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
+    return "$sppName:${normArguments?.toString()}".toHashId()
+}
+
+fun genEntryId(
+    name: String,
+    owner: SettingsPage,
+    fromPage: SettingsPage? = null,
+    toPage: SettingsPage? = null
+): String {
+    return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId()
+}
+
+// TODO: implement a better hash function
+private fun String.toHashId(): String {
+    return this.hashCode().toUInt().toString(36)
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
index c0b7464..730aa8f 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
@@ -19,12 +19,12 @@
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genEntryId
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.android.settingslib.spa.tests.testutils.SppHome
 import com.android.settingslib.spa.tests.testutils.SppLayer1
 import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,18 +41,18 @@
         val pageWithEntry = entryRepository.getAllPageWithEntry()
         assertThat(pageWithEntry.size).isEqualTo(3)
         assertThat(
-            entryRepository.getPageWithEntry(getUniquePageId("SppHome"))
+            entryRepository.getPageWithEntry(genPageId("SppHome"))
                 ?.entries?.size
         ).isEqualTo(1)
         assertThat(
-            entryRepository.getPageWithEntry(getUniquePageId("SppLayer1"))
+            entryRepository.getPageWithEntry(genPageId("SppLayer1"))
                 ?.entries?.size
         ).isEqualTo(3)
         assertThat(
-            entryRepository.getPageWithEntry(getUniquePageId("SppLayer2"))
+            entryRepository.getPageWithEntry(genPageId("SppLayer2"))
                 ?.entries?.size
         ).isEqualTo(2)
-        assertThat(entryRepository.getPageWithEntry(getUniquePageId("SppWithParam"))).isNull()
+        assertThat(entryRepository.getPageWithEntry(genPageId("SppWithParam"))).isNull()
     }
 
     @Test
@@ -61,17 +61,17 @@
         assertThat(entry.size).isEqualTo(7)
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId(
+                genEntryId(
                     "ROOT",
                     SppHome.createSettingsPage(),
-                    SettingsPage.createNull(),
+                    NullPageProvider.createSettingsPage(),
                     SppHome.createSettingsPage(),
                 )
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer1.createSettingsPage(),
                     SppHome.createSettingsPage(),
@@ -81,7 +81,7 @@
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer2.createSettingsPage(),
                     SppLayer1.createSettingsPage(),
@@ -91,22 +91,22 @@
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
+                genEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
+                genEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+                genEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
+                genEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
             )
         ).isNotNull()
     }
@@ -115,21 +115,21 @@
     fun testGetEntryPath() {
         assertThat(
             entryRepository.getEntryPathWithDisplayName(
-                getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+                genEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
             )
         ).containsExactly("Layer2Entry1", "INJECT_SppLayer2", "INJECT_SppLayer1", "ROOT_SppHome")
             .inOrder()
 
         assertThat(
             entryRepository.getEntryPathWithTitle(
-                getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
+                genEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
                 "entryTitle"
             )
         ).containsExactly("entryTitle", "SppLayer2", "TitleLayer1", "TitleHome").inOrder()
 
         assertThat(
             entryRepository.getEntryPathWithDisplayName(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer1.createSettingsPage(),
                     SppHome.createSettingsPage(),
@@ -140,7 +140,7 @@
 
         assertThat(
             entryRepository.getEntryPathWithTitle(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer2.createSettingsPage(),
                     SppLayer1.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index 6de1ae5..5754c9b 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -23,11 +23,12 @@
 import androidx.core.os.bundleOf
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genEntryId
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.slice.appendSpaParams
 import com.android.settingslib.spa.slice.getEntryId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -64,9 +65,9 @@
 
     @Test
     fun testBuildBasic() {
-        val owner = SettingsPage.create("mySpp")
+        val owner = createSettingsPage("mySpp")
         val entry = SettingsEntryBuilder.create(owner, "myEntry").build()
-        assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry.displayName).isEqualTo("myEntry")
         assertThat(entry.owner.sppName).isEqualTo("mySpp")
         assertThat(entry.owner.displayName).isEqualTo("mySpp")
@@ -80,19 +81,19 @@
 
     @Test
     fun testBuildWithLink() {
-        val owner = SettingsPage.create("mySpp")
-        val fromPage = SettingsPage.create("fromSpp")
-        val toPage = SettingsPage.create("toSpp")
+        val owner = createSettingsPage("mySpp")
+        val fromPage = createSettingsPage("fromSpp")
+        val toPage = createSettingsPage("toSpp")
         val entryFrom =
             SettingsEntryBuilder.createLinkFrom("myEntry", owner).setLink(toPage = toPage).build()
-        assertThat(entryFrom.id).isEqualTo(getUniqueEntryId("myEntry", owner, owner, toPage))
+        assertThat(entryFrom.id).isEqualTo(genEntryId("myEntry", owner, owner, toPage))
         assertThat(entryFrom.displayName).isEqualTo("myEntry")
         assertThat(entryFrom.fromPage!!.sppName).isEqualTo("mySpp")
         assertThat(entryFrom.toPage!!.sppName).isEqualTo("toSpp")
 
         val entryTo =
             SettingsEntryBuilder.createLinkTo("myEntry", owner).setLink(fromPage = fromPage).build()
-        assertThat(entryTo.id).isEqualTo(getUniqueEntryId("myEntry", owner, fromPage, owner))
+        assertThat(entryTo.id).isEqualTo(genEntryId("myEntry", owner, fromPage, owner))
         assertThat(entryTo.displayName).isEqualTo("myEntry")
         assertThat(entryTo.fromPage!!.sppName).isEqualTo("fromSpp")
         assertThat(entryTo.toPage!!.sppName).isEqualTo("mySpp")
@@ -100,10 +101,10 @@
 
     @Test
     fun testBuildInject() {
-        val owner = SettingsPage.create("mySpp")
+        val owner = createSettingsPage("mySpp")
         val entryInject = SettingsEntryBuilder.createInject(owner).build()
         assertThat(entryInject.id).isEqualTo(
-            getUniqueEntryId(
+            genEntryId(
                 INJECT_ENTRY_NAME_TEST, owner, toPage = owner
             )
         )
@@ -114,10 +115,10 @@
 
     @Test
     fun testBuildRoot() {
-        val owner = SettingsPage.create("mySpp")
+        val owner = createSettingsPage("mySpp")
         val entryInject = SettingsEntryBuilder.createRoot(owner, "myRootEntry").build()
         assertThat(entryInject.id).isEqualTo(
-            getUniqueEntryId(
+            genEntryId(
                 ROOT_ENTRY_NAME_TEST, owner, toPage = owner
             )
         )
@@ -129,7 +130,7 @@
     @Test
     fun testSetAttributes() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = SettingsPage.create("SppHome")
+        val owner = createSettingsPage("SppHome")
         val entryBuilder =
             SettingsEntryBuilder.create(owner, "myEntry")
                 .setDisplayName("myEntryDisplay")
@@ -138,7 +139,7 @@
                 .setSearchDataFn { null }
                 .setSliceDataFn { _, _ -> null }
         val entry = entryBuilder.build()
-        assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry.displayName).isEqualTo("myEntryDisplay")
         assertThat(entry.fromPage).isNull()
         assertThat(entry.toPage).isNull()
@@ -148,7 +149,7 @@
         assertThat(entry.hasSliceSupport).isTrue()
 
         // Test disabled Spp
-        val ownerDisabled = SettingsPage.create("SppDisabled")
+        val ownerDisabled = createSettingsPage("SppDisabled")
         val entryBuilderDisabled =
             SettingsEntryBuilder.create(ownerDisabled, "myEntry")
                 .setDisplayName("myEntryDisplay")
@@ -157,7 +158,7 @@
                 .setSearchDataFn { null }
                 .setSliceDataFn { _, _ -> null }
         val entryDisabled = entryBuilderDisabled.build()
-        assertThat(entryDisabled.id).isEqualTo(getUniqueEntryId("myEntry", ownerDisabled))
+        assertThat(entryDisabled.id).isEqualTo(genEntryId("myEntry", ownerDisabled))
         assertThat(entryDisabled.displayName).isEqualTo("myEntryDisplay")
         assertThat(entryDisabled.fromPage).isNull()
         assertThat(entryDisabled.toPage).isNull()
@@ -173,7 +174,7 @@
         // Clear SppHome in spa environment
         SpaEnvironmentFactory.reset()
         val entry3 = entryBuilder.build()
-        assertThat(entry3.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry3.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry3.displayName).isEqualTo("myEntryDisplay")
         assertThat(entry3.fromPage).isNull()
         assertThat(entry3.toPage).isNull()
@@ -186,12 +187,12 @@
     @Test
     fun testSetMarco() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = SettingsPage.create("SppHome", arguments = bundleOf("param" to "v1"))
+        val owner = createSettingsPage("SppHome", arguments = bundleOf("param" to "v1"))
         val entry = SettingsEntryBuilder.create(owner, "myEntry").setMacro {
             assertThat(it?.getString("param")).isEqualTo("v1")
             assertThat(it?.getString("rtParam")).isEqualTo("v2")
             assertThat(it?.getString("unknown")).isNull()
-            MacroForTest(getUniquePageId("SppHome"), getUniqueEntryId("myEntry", owner))
+            MacroForTest(genPageId("SppHome"), genEntryId("myEntry", owner))
         }.build()
 
         val rtArguments = bundleOf("rtParam" to "v2")
@@ -211,8 +212,8 @@
     @Test
     fun testSetSliceDataFn() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = SettingsPage.create("SppHome")
-        val entryId = getUniqueEntryId("myEntry", owner)
+        val owner = createSettingsPage("SppHome")
+        val entryId = genEntryId("myEntry", owner)
         val emptySliceData = EntrySliceData()
 
         val entryBuilder = SettingsEntryBuilder.create(owner, "myEntry").setSliceDataFn { uri, _ ->
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
index 6c0c652..8576573 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.spa.framework.common
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,13 +30,14 @@
         assertThat(sppRepoEmpty.getDefaultStartPage()).isEqualTo("")
         assertThat(sppRepoEmpty.getAllRootPages()).isEmpty()
 
+        val nullPage = NullPageProvider.createSettingsPage()
         val sppRepoNull =
-            SettingsPageProviderRepository(emptyList(), listOf(SettingsPage.createNull()))
+            SettingsPageProviderRepository(emptyList(), listOf(nullPage))
         assertThat(sppRepoNull.getDefaultStartPage()).isEqualTo("NULL")
-        assertThat(sppRepoNull.getAllRootPages()).contains(SettingsPage.createNull())
+        assertThat(sppRepoNull.getAllRootPages()).contains(nullPage)
 
-        val rootPage1 = SettingsPage.create(name = "Spp1", displayName = "Spp1")
-        val rootPage2 = SettingsPage.create(name = "Spp2", displayName = "Spp2")
+        val rootPage1 = createSettingsPage(sppName = "Spp1", displayName = "Spp1")
+        val rootPage2 = createSettingsPage(sppName = "Spp2", displayName = "Spp2")
         val sppRepo = SettingsPageProviderRepository(emptyList(), listOf(rootPage1, rootPage2))
         val allRoots = sppRepo.getAllRootPages()
         assertThat(sppRepo.getDefaultStartPage()).isEqualTo("Spp1")
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
index 7fa1e26..dc74a10 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
@@ -22,8 +22,9 @@
 import androidx.navigation.navArgument
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,27 +36,25 @@
 
     @Test
     fun testNullPage() {
-        val page = SettingsPage.createNull()
-        assertThat(page.id).isEqualTo(getUniquePageId("NULL"))
+        val page = NullPageProvider.createSettingsPage()
+        assertThat(page.id).isEqualTo(genPageId("NULL"))
         assertThat(page.sppName).isEqualTo("NULL")
         assertThat(page.displayName).isEqualTo("NULL")
         assertThat(page.buildRoute()).isEqualTo("NULL")
         assertThat(page.isCreateBy("NULL")).isTrue()
         assertThat(page.isCreateBy("Spp")).isFalse()
-        assertThat(page.hasRuntimeParam()).isFalse()
         assertThat(page.isBrowsable()).isFalse()
     }
 
     @Test
     fun testRegularPage() {
-        val page = SettingsPage.create("mySpp", "SppDisplayName")
-        assertThat(page.id).isEqualTo(getUniquePageId("mySpp"))
+        val page = createSettingsPage("mySpp", "SppDisplayName")
+        assertThat(page.id).isEqualTo(genPageId("mySpp"))
         assertThat(page.sppName).isEqualTo("mySpp")
         assertThat(page.displayName).isEqualTo("SppDisplayName")
         assertThat(page.buildRoute()).isEqualTo("mySpp")
         assertThat(page.isCreateBy("NULL")).isFalse()
         assertThat(page.isCreateBy("mySpp")).isTrue()
-        assertThat(page.hasRuntimeParam()).isFalse()
         assertThat(page.isBrowsable()).isTrue()
     }
 
@@ -67,7 +66,7 @@
         )
         val page = spaEnvironment.createPage("SppWithParam", arguments)
         assertThat(page.id).isEqualTo(
-            getUniquePageId(
+            genPageId(
                 "SppWithParam", listOf(
                     navArgument("string_param") { type = NavType.StringType },
                     navArgument("int_param") { type = NavType.IntType },
@@ -78,7 +77,6 @@
         assertThat(page.displayName).isEqualTo("SppWithParam/myStr/10")
         assertThat(page.buildRoute()).isEqualTo("SppWithParam/myStr/10")
         assertThat(page.isCreateBy("SppWithParam")).isTrue()
-        assertThat(page.hasRuntimeParam()).isFalse()
         assertThat(page.isBrowsable()).isTrue()
     }
 
@@ -91,7 +89,7 @@
         )
         val page = spaEnvironment.createPage("SppWithRtParam", arguments)
         assertThat(page.id).isEqualTo(
-            getUniquePageId(
+            genPageId(
                 "SppWithRtParam", listOf(
                     navArgument("string_param") { type = NavType.StringType },
                     navArgument("int_param") { type = NavType.IntType },
@@ -103,7 +101,6 @@
         assertThat(page.displayName).isEqualTo("SppWithRtParam/myStr/10")
         assertThat(page.buildRoute()).isEqualTo("SppWithRtParam/myStr/10/rtStr")
         assertThat(page.isCreateBy("SppWithRtParam")).isTrue()
-        assertThat(page.hasRuntimeParam()).isTrue()
         assertThat(page.isBrowsable()).isFalse()
     }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
index 1854728..f974cca 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
@@ -19,9 +19,10 @@
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.common.NullPageProvider
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.google.common.truth.Truth
 import org.junit.Before
@@ -40,7 +41,7 @@
 
     @Test
     fun testCreateIntent() {
-        val nullPage = SettingsPage.createNull()
+        val nullPage = NullPageProvider.createSettingsPage()
         Truth.assertThat(nullPage.createIntent()).isNull()
         Truth.assertThat(SettingsEntryBuilder.createInject(nullPage).build().createIntent())
             .isNull()
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt
new file mode 100644
index 0000000..c17f83b
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 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.util
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UniqueIdTest {
+    @Test
+    fun testUniquePageId() {
+        Truth.assertThat(genPageId("mySpp")).isEqualTo("1byojwa")
+
+        val parameter = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+        )
+        val arguments = bundleOf(
+            "string_param" to "myStr",
+            "int_param" to 10,
+        )
+        Truth.assertThat(genPageId("mySppWithParam", parameter, arguments)).isEqualTo("1sz4pbq")
+
+        val parameter2 = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+            navArgument("rt_param") { type = NavType.StringType },
+        )
+        val arguments2 = bundleOf(
+            "string_param" to "myStr",
+            "int_param" to 10,
+            "rt_param" to "myRtStr",
+        )
+        Truth.assertThat(genPageId("mySppWithRtParam", parameter2, arguments2)).isEqualTo("ts6d8k")
+    }
+
+    @Test
+    fun testUniqueEntryId() {
+        val owner = createSettingsPage("mySpp")
+        val fromPage = createSettingsPage("fromSpp")
+        val toPage = createSettingsPage("toSpp")
+
+        Truth.assertThat(genEntryId("myEntry", owner)).isEqualTo("145pppn")
+        Truth.assertThat(genEntryId("myEntry", owner, fromPage = fromPage, toPage = toPage))
+            .isEqualTo("1m7jzew")
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
index 530d2ed..341a4a5 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
@@ -25,10 +25,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.util.genEntryId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.android.settingslib.spa.tests.testutils.SppHome
 import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -52,7 +52,7 @@
 
         // Slice supported
         val page = SppLayer2.createSettingsPage()
-        val entryId = getUniqueEntryId("Layer2Entry1", page)
+        val entryId = genEntryId("Layer2Entry1", page)
         val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
         assertThat(sliceUri.getDestination()).isEqualTo("SppLayer2")
         assertThat(sliceUri.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
@@ -61,7 +61,7 @@
         assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri)).isSameInstanceAs(sliceData)
 
         // Slice unsupported
-        val entryId2 = getUniqueEntryId("Layer2Entry2", page)
+        val entryId2 = genEntryId("Layer2Entry2", page)
         val sliceUri2 = Uri.Builder().appendSpaParams(page.buildRoute(), entryId2).build()
         assertThat(sliceUri2.getDestination()).isEqualTo("SppLayer2")
         assertThat(sliceUri2.getSliceId()).isEqualTo("${entryId2}_Bundle[{}]")
@@ -73,7 +73,7 @@
         SpaEnvironmentFactory.reset(spaEnvironment)
 
         val page = SppLayer2.createSettingsPage()
-        val entryId = getUniqueEntryId("Layer2Entry1", page)
+        val entryId = genEntryId("Layer2Entry1", page)
         val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
 
         // build slice data first
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt
new file mode 100644
index 0000000..caf4136
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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.tests.testutils
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.util.genPageId
+
+fun createSettingsPage(
+    sppName: String,
+    displayName: String? = null,
+    parameter: List<NamedNavArgument> = emptyList(),
+    arguments: Bundle? = null
+): SettingsPage {
+    return SettingsPage(
+        id = genPageId(sppName, parameter, arguments),
+        sppName = sppName,
+        displayName = displayName ?: sppName,
+        parameter = parameter,
+        arguments = arguments
+    )
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
deleted file mode 100644
index ce9b791..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.tests.testutils
-
-import android.os.Bundle
-import androidx.navigation.NamedNavArgument
-import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.toHashId
-import com.android.settingslib.spa.framework.util.normalize
-
-fun getUniquePageId(
-    name: String,
-    parameter: List<NamedNavArgument> = emptyList(),
-    arguments: Bundle? = null
-): String {
-    val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
-    return "$name:${normArguments?.toString()}".toHashId()
-}
-
-fun getUniquePageId(page: SettingsPage): String {
-    return getUniquePageId(page.sppName, page.parameter, page.arguments)
-}
-
-fun getUniqueEntryId(
-    name: String,
-    owner: SettingsPage,
-    fromPage: SettingsPage? = null,
-    toPage: SettingsPage? = null
-): String {
-    val ownerId = getUniquePageId(owner)
-    val fromId = if (fromPage == null) "null" else getUniquePageId(fromPage)
-    val toId = if (toPage == null) "null" else getUniquePageId(toPage)
-    return "$name:$ownerId($fromId-$toId)".toHashId()
-}