Merge "Support slice in Spa."
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 037b45e..2eaa73e 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -24,6 +24,9 @@
     srcs: ["src/**/*.kt"],
 
     static_libs: [
+        "androidx.slice_slice-builders",
+        "androidx.slice_slice-core",
+        "androidx.slice_slice-view",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
         "androidx.compose.runtime_runtime",
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 84988b0..4944784 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -56,6 +56,9 @@
 
 dependencies {
     api "androidx.appcompat:appcompat:1.7.0-alpha01"
+    api "androidx.slice:slice-builders:1.1.0-alpha02"
+    api "androidx.slice:slice-core:1.1.0-alpha02"
+    api "androidx.slice:slice-view:1.1.0-alpha02"
     api "androidx.compose.material3:material3:1.1.0-alpha01"
     api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
     api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
index 35b9c0f..3689e4e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
@@ -119,7 +119,7 @@
         val entryRepository by spaEnvironment.entryRepository
         val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
         for (entry in entryRepository.getAllEntries()) {
-            if (!entry.isAllowSearch || entry.mutableStatus) continue
+            if (!entry.isAllowSearch || entry.hasMutableStatus) continue
             fetchStatusData(entry, cursor)
         }
         return cursor
@@ -129,7 +129,7 @@
         val entryRepository by spaEnvironment.entryRepository
         val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
         for (entry in entryRepository.getAllEntries()) {
-            if (!entry.isAllowSearch || !entry.mutableStatus) continue
+            if (!entry.isAllowSearch || !entry.hasMutableStatus) continue
             fetchStatusData(entry, cursor)
         }
         return cursor
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
new file mode 100644
index 0000000..8c038c8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+class SpaSliceBroadcastReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context?, intent: Intent?) {
+        val sliceRepository = SpaEnvironmentFactory.instance.sliceDataRepository
+        val sliceUri = intent?.data ?: return
+        val sliceData = sliceRepository.getActiveSliceData(sliceUri) ?: return
+        sliceData.doAction()
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
new file mode 100644
index 0000000..d800e50
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.net.Uri
+import android.util.Log
+import androidx.lifecycle.Observer
+import androidx.slice.Slice
+import androidx.slice.SliceProvider
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+private const val TAG = "SpaSliceProvider"
+
+class SpaSliceProvider : SliceProvider(), Observer<Slice?> {
+    private fun getOrPutSliceData(sliceUri: Uri): EntrySliceData? {
+        if (!SpaEnvironmentFactory.isReady()) return null
+        return SpaEnvironmentFactory.instance.sliceDataRepository.getOrBuildSliceData(sliceUri)
+    }
+
+    override fun onBindSlice(sliceUri: Uri): Slice? {
+        if (context == null) return null
+        Log.d(TAG, "onBindSlice: $sliceUri")
+        return getOrPutSliceData(sliceUri)?.value
+    }
+
+    override fun onSlicePinned(sliceUri: Uri) {
+        Log.d(TAG, "onSlicePinned: $sliceUri")
+        super.onSlicePinned(sliceUri)
+        val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+        runBlocking {
+            withContext(Dispatchers.Main) {
+                sliceLiveData.observeForever(this@SpaSliceProvider)
+            }
+        }
+    }
+
+    override fun onSliceUnpinned(sliceUri: Uri) {
+        Log.d(TAG, "onSliceUnpinned: $sliceUri")
+        super.onSliceUnpinned(sliceUri)
+        val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+        runBlocking {
+            withContext(Dispatchers.Main) {
+                sliceLiveData.removeObserver(this@SpaSliceProvider)
+            }
+        }
+    }
+
+    override fun onChanged(slice: Slice?) {
+        val uri = slice?.uri ?: return
+        Log.d(TAG, "onChanged: $uri")
+        context?.contentResolver?.notifyChange(uri, null)
+    }
+
+    override fun onCreateSliceProvider(): Boolean {
+        Log.d(TAG, "onCreateSliceProvider")
+        return true
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
new file mode 100644
index 0000000..fc551a8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import androidx.lifecycle.LiveData
+import androidx.slice.Slice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+open class EntrySliceData : LiveData<Slice?>() {
+    private val asyncRunnerScope = CoroutineScope(Dispatchers.IO)
+    private var asyncRunnerJob: Job? = null
+    private var asyncActionJob: Job? = null
+    private var isActive = false
+
+    open suspend fun asyncRunner() {}
+
+    open suspend fun asyncAction() {}
+
+    override fun onActive() {
+        asyncRunnerJob?.cancel()
+        asyncRunnerJob = asyncRunnerScope.launch { asyncRunner() }
+        isActive = true
+    }
+
+    override fun onInactive() {
+        asyncRunnerJob?.cancel()
+        asyncRunnerJob = null
+        asyncActionJob?.cancel()
+        asyncActionJob = null
+        isActive = false
+    }
+
+    fun isActive(): Boolean {
+        return isActive
+    }
+
+    fun doAction() {
+        asyncActionJob?.cancel()
+        asyncActionJob = asyncRunnerScope.launch { asyncAction() }
+    }
+}
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 224fe1d..9ee7f9e 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
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.spa.framework.common
 
+import android.net.Uri
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -39,6 +40,11 @@
 val LocalEntryDataProvider =
     compositionLocalOf<EntryData> { object : EntryData {} }
 
+typealias UiLayerRenderer = @Composable (arguments: Bundle?) -> Unit
+typealias StatusDataGetter = (arguments: Bundle?) -> EntryStatusData?
+typealias SearchDataGetter = (arguments: Bundle?) -> EntrySearchData?
+typealias SliceDataGetter = (sliceUri: Uri, arguments: Bundle?) -> EntrySliceData?
+
 /**
  * Defines data of a Settings entry.
  */
@@ -71,7 +77,10 @@
 
     // Indicate whether the status of entry is mutable.
     // If so, for instance, we'll reindex its status for search.
-    val mutableStatus: Boolean = false,
+    val hasMutableStatus: Boolean = false,
+
+    // Indicate whether the entry has SliceProvider support.
+    val hasSliceSupport: Boolean = false,
 
     /**
      * ========================================
@@ -83,13 +92,19 @@
      * API to get the status data of the entry, such as isDisabled / isSwitchOff.
      * Returns null if this entry do NOT have any status.
      */
-    private val statusDataImpl: (arguments: Bundle?) -> EntryStatusData? = { null },
+    private val statusDataImpl: StatusDataGetter = { null },
 
     /**
      * API to get Search indexing data for this entry, such as title / keyword.
      * Returns null if this entry do NOT support search.
      */
-    private val searchDataImpl: (arguments: Bundle?) -> EntrySearchData? = { null },
+    private val searchDataImpl: SearchDataGetter = { null },
+
+    /**
+     * API to get Slice data of this entry. The Slice data is implemented as a LiveData,
+     * and is associated with the Slice's lifecycle (pin / unpin) by the framework.
+     */
+    private val sliceDataImpl: SliceDataGetter = { _: Uri, _: Bundle? -> null },
 
     /**
      * API to Render UI of this entry directly. For now, we use it in the internal injection, to
@@ -97,7 +112,7 @@
      * injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
      * use each entries' UI rendering function in the page instead.
      */
-    private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
+    private val uiLayoutImpl: UiLayerRenderer = {},
 ) {
     fun containerPage(): SettingsPage {
         // The Container page of the entry, which is the from-page or
@@ -121,6 +136,10 @@
         return searchDataImpl(fullArgument(runtimeArguments))
     }
 
+    fun getSliceData(sliceUri: Uri, runtimeArguments: Bundle? = null): EntrySliceData? {
+        return sliceDataImpl(sliceUri, fullArgument(runtimeArguments))
+    }
+
     @Composable
     fun UiLayout(runtimeArguments: Bundle? = null) {
         CompositionLocalProvider(provideLocalEntryData()) {
@@ -152,12 +171,14 @@
     // Attributes
     private var isAllowSearch: Boolean = false
     private var isSearchDataDynamic: Boolean = false
-    private var mutableStatus: Boolean = false
+    private var hasMutableStatus: Boolean = false
+    private var hasSliceSupport: Boolean = false
 
     // Functions
-    private var statusDataFn: (arguments: Bundle?) -> EntryStatusData? = { null }
-    private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
-    private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
+    private var uiLayoutFn: UiLayerRenderer = { }
+    private var statusDataFn: StatusDataGetter = { null }
+    private var searchDataFn: SearchDataGetter = { null }
+    private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
 
     fun build(): SettingsEntry {
         return SettingsEntry(
@@ -173,11 +194,13 @@
             // attributes
             isAllowSearch = isAllowSearch,
             isSearchDataDynamic = isSearchDataDynamic,
-            mutableStatus = mutableStatus,
+            hasMutableStatus = hasMutableStatus,
+            hasSliceSupport = hasSliceSupport,
 
             // functions
             statusDataImpl = statusDataFn,
             searchDataImpl = searchDataFn,
+            sliceDataImpl = sliceDataFn,
             uiLayoutImpl = uiLayoutFn,
         )
     }
@@ -207,7 +230,7 @@
     }
 
     fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
-        this.mutableStatus = hasMutableStatus
+        this.hasMutableStatus = hasMutableStatus
         return this
     }
 
@@ -221,17 +244,23 @@
         return this
     }
 
-    fun setStatusDataFn(fn: (arguments: Bundle?) -> EntryStatusData?): SettingsEntryBuilder {
+    fun setStatusDataFn(fn: StatusDataGetter): SettingsEntryBuilder {
         this.statusDataFn = fn
         return this
     }
 
-    fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
+    fun setSearchDataFn(fn: SearchDataGetter): SettingsEntryBuilder {
         this.searchDataFn = fn
         return this
     }
 
-    fun setUiLayoutFn(fn: @Composable (arguments: Bundle?) -> Unit): SettingsEntryBuilder {
+    fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
+        this.sliceDataFn = fn
+        this.hasSliceSupport = true
+        return this
+    }
+
+    fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
         this.uiLayoutFn = fn
         return this
     }
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 bb287d1..a372bbd 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
@@ -113,6 +113,12 @@
         )
     }
 
+    fun createBrowseIntent(entryId: String? = null): Intent? {
+        val context = SpaEnvironmentFactory.instance.appContext
+        val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass
+        return createBrowseIntent(context, browseActivityClass, entryId)
+    }
+
     fun createBrowseIntent(
         context: Context?,
         browseActivityClass: Class<out Activity>?,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index a9cb041..9bb06f0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -17,10 +17,12 @@
 package com.android.settingslib.spa.framework.common
 
 import android.app.Activity
+import android.content.BroadcastReceiver
 import android.content.Context
 import android.util.Log
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.slice.SettingsSliceDataRepository
 
 private const val TAG = "SpaEnvironment"
 
@@ -46,6 +48,10 @@
         Log.d(TAG, "resetForPreview")
     }
 
+    fun isReady(): Boolean {
+        return spaEnvironment != null
+    }
+
     val instance: SpaEnvironment
         get() {
             if (spaEnvironment == null)
@@ -59,13 +65,15 @@
 
     val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) }
 
+    val sliceDataRepository = SettingsSliceDataRepository()
+
     // In Robolectric test, applicationContext is not available. Use context as fallback.
     val appContext: Context = context.applicationContext ?: context
 
     open val browseActivityClass: Class<out Activity>? = null
-
+    open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null
     open val searchProviderAuthorities: String? = null
-
+    open val sliceProviderAuthorities: String? = null
     open val logger: SpaLogger = object : SpaLogger {}
 
     // TODO: add other environment setup here.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 26491d5..760064a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.spa.framework.debug
 
+import android.net.Uri
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
@@ -38,6 +39,8 @@
 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.slice.appendSliceParams
+import com.android.settingslib.spa.slice.presenter.SliceDemo
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -47,6 +50,7 @@
 private const val ROUTE_ROOT = "root"
 private const val ROUTE_All_PAGES = "pages"
 private const val ROUTE_All_ENTRIES = "entries"
+private const val ROUTE_All_SLICES = "slices"
 private const val ROUTE_PAGE = "page"
 private const val ROUTE_ENTRY = "entry"
 private const val PARAM_NAME_PAGE_ID = "pid"
@@ -81,6 +85,7 @@
                 composable(route = ROUTE_ROOT) { RootPage() }
                 composable(route = ROUTE_All_PAGES) { AllPages() }
                 composable(route = ROUTE_All_ENTRIES) { AllEntries() }
+                composable(route = ROUTE_All_SLICES) { AllSlices() }
                 composable(
                     route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
                     arguments = listOf(
@@ -102,6 +107,8 @@
         val entryRepository by spaEnvironment.entryRepository
         val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
         val allEntry = remember { entryRepository.getAllEntries() }
+        val allSliceEntry =
+            remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
         HomeScaffold(title = "Settings Debug") {
             Preference(object : PreferenceModel {
                 override val title = "List All Pages (${allPageWithEntry.size})"
@@ -111,6 +118,10 @@
                 override val title = "List All Entries (${allEntry.size})"
                 override val onClick = navigator(route = ROUTE_All_ENTRIES)
             })
+            Preference(object : PreferenceModel {
+                override val title = "List All Slices (${allSliceEntry.size})"
+                override val onClick = navigator(route = ROUTE_All_SLICES)
+            })
         }
     }
 
@@ -140,6 +151,19 @@
     }
 
     @Composable
+    fun AllSlices() {
+        val entryRepository by spaEnvironment.entryRepository
+        val authority = spaEnvironment.sliceProviderAuthorities
+        val allSliceEntry =
+            remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
+        RegularScaffold(title = "All Slices (${allSliceEntry.size})") {
+            for (entry in allSliceEntry) {
+                SliceDemo(sliceUri = entry.createSliceUri(authority))
+            }
+        }
+    }
+
+    @Composable
     fun OnePage(arguments: Bundle?) {
         val context = LocalContext.current
         val entryRepository by spaEnvironment.entryRepository
@@ -221,6 +245,18 @@
     }
 }
 
+private fun SettingsEntry.createSliceUri(
+    authority: String?,
+    runtimeArguments: Bundle? = null
+): Uri {
+    if (authority == null) return Uri.EMPTY
+    return Uri.Builder().scheme("content").authority(authority).appendSliceParams(
+        route = this.containerPage().buildRoute(),
+        entryId = this.id,
+        runtimeArguments = runtimeArguments,
+    ).build()
+}
+
 /**
  * A blank activity without any page.
  */
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
new file mode 100644
index 0000000..d8d2378
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.slice
+
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+private const val TAG = "SliceDataRepository"
+
+class SettingsSliceDataRepository {
+    // The map of slice uri to its EntrySliceData, a.k.a. LiveData<Slice?>
+    private val sliceDataMap: MutableMap<String, EntrySliceData> = mutableMapOf()
+
+    // Note: mark this function synchronized, so that we can get the same livedata during the
+    // whole lifecycle of a Slice.
+    @Synchronized
+    fun getOrBuildSliceData(sliceUri: Uri): EntrySliceData? {
+        val sliceString = sliceUri.getSliceId() ?: return null
+        return sliceDataMap[sliceString] ?: buildLiveDataImpl(sliceUri)?.let {
+            sliceDataMap[sliceString] = it
+            it
+        }
+    }
+
+    fun getActiveSliceData(sliceUri: Uri): EntrySliceData? {
+        val sliceString = sliceUri.getSliceId() ?: return null
+        val sliceData = sliceDataMap[sliceString] ?: return null
+        return if (sliceData.isActive()) sliceData else null
+    }
+
+    private fun buildLiveDataImpl(sliceUri: Uri): EntrySliceData? {
+        Log.d(TAG, "buildLiveData: $sliceUri")
+        if (!SpaEnvironmentFactory.isReady()) return null
+
+        val entryRepository by SpaEnvironmentFactory.instance.entryRepository
+        val entryId = sliceUri.getEntryId() ?: return null
+        val entry = entryRepository.getEntry(entryId) ?: return null
+        if (!entry.hasSliceSupport) return null
+
+        val arguments = sliceUri.getRuntimeArguments()
+        return entry.getSliceData(runtimeArguments = arguments, sliceUri = sliceUri)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
new file mode 100644
index 0000000..ff143ed
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.slice
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+// Defines SliceUri, which contains special query parameters:
+//  -- KEY_DESTINATION: The route that this slice is navigated to.
+//  -- KEY_HIGHLIGHT_ENTRY: The entry id of this slice
+//  Other parameters can considered as runtime parameters.
+// Use {entryId, runtimeParams} as the unique Id of this Slice.
+typealias SliceUri = Uri
+
+val RESERVED_KEYS = listOf(
+    KEY_DESTINATION,
+    KEY_HIGHLIGHT_ENTRY
+)
+
+fun SliceUri.getEntryId(): String? {
+    return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
+}
+
+fun SliceUri.getDestination(): String? {
+    return getQueryParameter(KEY_DESTINATION)
+}
+
+fun SliceUri.getRuntimeArguments(): Bundle {
+    val params = Bundle()
+    for (queryName in queryParameterNames) {
+        if (RESERVED_KEYS.contains(queryName)) continue
+        params.putString(queryName, getQueryParameter(queryName))
+    }
+    return params
+}
+
+fun SliceUri.getSliceId(): String? {
+    val entryId = getEntryId() ?: return null
+    val params = getRuntimeArguments()
+    return "${entryId}_$params"
+}
+
+fun Uri.Builder.appendSliceParams(
+    route: String? = null,
+    entryId: String? = null,
+    runtimeArguments: Bundle? = null
+): Uri.Builder {
+    if (route != null) appendQueryParameter(KEY_DESTINATION, route)
+    if (entryId != null) appendQueryParameter(KEY_HIGHLIGHT_ENTRY, entryId)
+    if (runtimeArguments != null) {
+        for (key in runtimeArguments.keySet()) {
+            appendQueryParameter(key, runtimeArguments.getString(key, ""))
+        }
+    }
+    return this
+}
+
+fun SliceUri.createBroadcastPendingIntent(): PendingIntent? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    val sliceBroadcastClass =
+        SpaEnvironmentFactory.instance.sliceBroadcastReceiverClass ?: return null
+    val entryId = getEntryId() ?: return null
+    return createBroadcastPendingIntent(context, sliceBroadcastClass, entryId)
+}
+
+fun SliceUri.createBrowsePendingIntent(): PendingIntent? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+    val destination = getDestination() ?: return null
+    val entryId = getEntryId()
+    return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+fun Intent.createBrowsePendingIntent(): PendingIntent? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+    val destination = getStringExtra(KEY_DESTINATION) ?: return null
+    val entryId = getStringExtra(KEY_HIGHLIGHT_ENTRY)
+    return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+private fun createBrowsePendingIntent(
+    context: Context,
+    browseActivityClass: Class<out Activity>,
+    destination: String,
+    entryId: String?
+): PendingIntent {
+    val intent = Intent().setComponent(ComponentName(context, browseActivityClass))
+        .apply {
+            // Set both extra and data (which is a Uri) in Slice Intent:
+            // 1) extra is used in SPA navigation framework
+            // 2) data is used in Slice framework
+            putExtra(KEY_DESTINATION, destination)
+            if (entryId != null) {
+                putExtra(KEY_HIGHLIGHT_ENTRY, entryId)
+            }
+            data = Uri.Builder().appendSliceParams(destination, entryId).build()
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK
+        }
+
+    return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+}
+
+private fun createBroadcastPendingIntent(
+    context: Context,
+    sliceBroadcastClass: Class<out BroadcastReceiver>,
+    entryId: String
+): PendingIntent {
+    val intent = Intent().setComponent(ComponentName(context, sliceBroadcastClass))
+        .apply { data = Uri.Builder().appendSliceParams(entryId = entryId).build() }
+    return PendingIntent.getBroadcast(
+        context, 0 /* requestCode */, intent,
+        PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
new file mode 100644
index 0000000..cff1c0c
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.slice.presenter
+
+import android.net.Uri
+import androidx.compose.material3.Divider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.slice.widget.SliceLiveData
+import androidx.slice.widget.SliceView
+
+@Composable
+fun SliceDemo(sliceUri: Uri) {
+    val context = LocalContext.current
+    val lifecycleOwner = LocalLifecycleOwner.current
+    val sliceData = remember {
+        SliceLiveData.fromUri(context, sliceUri)
+    }
+
+    Divider()
+    AndroidView(
+        factory = { localContext ->
+            val view = SliceView(localContext)
+            view.setShowTitleItems(true)
+            view.isScrollable = false
+            view
+        },
+        update = { view -> sliceData.observe(lifecycleOwner, view) }
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
new file mode 100644
index 0000000..b65b91f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.slice.provider
+
+import android.app.PendingIntent
+import android.content.Context
+import android.net.Uri
+import androidx.core.graphics.drawable.IconCompat
+import androidx.slice.Slice
+import androidx.slice.SliceManager
+import androidx.slice.builders.ListBuilder
+import androidx.slice.builders.SliceAction
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.slice.createBroadcastPendingIntent
+import com.android.settingslib.spa.slice.createBrowsePendingIntent
+
+fun createDemoBrowseSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+    val intent = sliceUri.createBrowsePendingIntent() ?: return null
+    return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoActionSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+    val intent = sliceUri.createBroadcastPendingIntent() ?: return null
+    return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoSlice(sliceUri: Uri, title: String, summary: String, intent: PendingIntent): Slice? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    if (!SliceManager.getInstance(context).pinnedSlices.contains(sliceUri)) return null
+    return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
+        .addRow(ListBuilder.RowBuilder().apply {
+            setPrimaryAction(createSliceAction(context, intent))
+            setTitle(title)
+            setSubtitle(summary)
+        }).build()
+}
+
+private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction {
+    return SliceAction.create(
+        intent,
+        IconCompat.createWithResource(
+            context,
+            com.google.android.material.R.drawable.navigation_empty_icon
+        ),
+        ListBuilder.ICON_IMAGE,
+        "Enter app"
+    )
+}
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 31d2ae4..2017d53 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
@@ -63,7 +63,7 @@
         assertThat(entry.toPage).isNull()
         assertThat(entry.isAllowSearch).isFalse()
         assertThat(entry.isSearchDataDynamic).isFalse()
-        assertThat(entry.mutableStatus).isFalse()
+        assertThat(entry.hasMutableStatus).isFalse()
     }
 
     @Test
@@ -133,7 +133,7 @@
         assertThat(entry.toPage).isNull()
         assertThat(entry.isAllowSearch).isTrue()
         assertThat(entry.isSearchDataDynamic).isFalse()
-        assertThat(entry.mutableStatus).isTrue()
+        assertThat(entry.hasMutableStatus).isTrue()
     }
 
     @Test
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 539e56b..7097a5d 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
@@ -118,6 +118,7 @@
         page.enterPage()
         page.leavePage()
         page.enterPage()
+        assertThat(page.createBrowseIntent()).isNotNull()
         assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK))
             .isEqualTo(2)
         assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK))