Merge "Support slice in Spa."

This commit is contained in:
Zekan Qian
2022-11-14 15:35:38 +00:00
committed by Android (Google) Code Review
16 changed files with 576 additions and 19 deletions

View File

@@ -24,6 +24,9 @@ android_library {
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",

View File

@@ -56,6 +56,9 @@ android {
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"

View File

@@ -119,7 +119,7 @@ class SpaSearchProvider : ContentProvider() {
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 @@ class SpaSearchProvider : ContentProvider() {
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

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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() }
}
}

View File

@@ -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 @@ interface EntryData {
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 @@ data class SettingsEntry(
// 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 @@ data class SettingsEntry(
* 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 @@ data class SettingsEntry(
* 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 @@ data class SettingsEntry(
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 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
// 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 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
// attributes
isAllowSearch = isAllowSearch,
isSearchDataDynamic = isSearchDataDynamic,
mutableStatus = mutableStatus,
hasMutableStatus = hasMutableStatus,
hasSliceSupport = hasSliceSupport,
// functions
statusDataImpl = statusDataFn,
searchDataImpl = searchDataFn,
sliceDataImpl = sliceDataFn,
uiLayoutImpl = uiLayoutFn,
)
}
@@ -207,7 +230,7 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
}
fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
this.mutableStatus = hasMutableStatus
this.hasMutableStatus = hasMutableStatus
return this
}
@@ -221,17 +244,23 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings
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
}

View File

@@ -113,6 +113,12 @@ data class SettingsPage(
)
}
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>?,

View File

@@ -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 @@ object SpaEnvironmentFactory {
Log.d(TAG, "resetForPreview")
}
fun isReady(): Boolean {
return spaEnvironment != null
}
val instance: SpaEnvironment
get() {
if (spaEnvironment == null)
@@ -59,13 +65,15 @@ abstract class SpaEnvironment(context: Context) {
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.

View File

@@ -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.localNavController
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.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 TAG = "DebugActivity"
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 @@ class DebugActivity : ComponentActivity() {
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 @@ class DebugActivity : ComponentActivity() {
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 @@ class DebugActivity : ComponentActivity() {
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)
})
}
}
@@ -139,6 +150,19 @@ class DebugActivity : ComponentActivity() {
}
}
@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
@@ -221,6 +245,18 @@ class DebugActivity : ComponentActivity() {
}
}
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.
*/

View File

@@ -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)
}
}

View File

@@ -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
)
}

View File

@@ -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) }
)
}

View File

@@ -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"
)
}

View File

@@ -63,7 +63,7 @@ class SettingsEntryTest {
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 @@ class SettingsEntryTest {
assertThat(entry.toPage).isNull()
assertThat(entry.isAllowSearch).isTrue()
assertThat(entry.isSearchDataDynamic).isFalse()
assertThat(entry.mutableStatus).isTrue()
assertThat(entry.hasMutableStatus).isTrue()
}
@Test

View File

@@ -118,6 +118,7 @@ class SettingsPageTest {
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))