blob: 28d6f9bcc18e1c7218a7081214927f4b6dec58b6 [file]
/*
* Copyright (C) 2024 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.car.appcard
import android.content.ContentProvider
import android.content.ContentResolver
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Bundle
import android.os.CancellationSignal
import android.util.Log
import androidx.annotation.CallSuper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.android.car.appcard.AppCardContext.Companion.fromBundle
import com.android.car.appcard.AppCardMessageConstants.InteractionMessageConstants
import com.android.car.appcard.annotations.EnforceFastUpdateRate
import com.android.car.appcard.component.Button
import com.android.car.appcard.component.Component
import com.android.car.appcard.internal.AppCardTransport
import com.android.car.appcard.util.ParcelableUtils
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
/** A [ContentProvider] that provides the system access to an application's app cards. */
abstract class AppCardContentProvider : ContentProvider(), LifecycleOwner {
private val activeAppCardCountMap: ConcurrentMap<String, Int> = ConcurrentHashMap()
private val appCardIdComponentMap: ConcurrentMap<String, ConcurrentMap<String, Component>> =
ConcurrentHashMap()
private val latestAppCard: ConcurrentMap<String, AppCard?> = ConcurrentHashMap()
private val lock = Any()
private val dispatcher = ProviderLifecycleDispatcher(provider = this)
override val lifecycle: Lifecycle = dispatcher.lifecycle
/**
* @return authority of provider in manifest
*/
abstract val authority: String
@CallSuper
override fun onCreate(): Boolean {
dispatcher.queueOnCreate()
return true
}
/**
* Setup app card for given ID.
*
* @param id id of app card being requested
* @param ctx context providing information such as dimensions of app card, etc.
* @return app card being requested
*/
protected abstract fun onAppCardAdded(id: String, ctx: AppCardContext): AppCard
/**
* Update app card for given ID when system is ready to receive an update
*
* @param appCard app card that should be updated
*/
fun sendAppCardUpdate(appCard: AppCard) {
synchronized(lock) {
if (!isAppCardActive(appCard.id)) {
Log.e(TAG, "For app card update, app card must be active")
return
}
latestAppCard[appCard.id] = appCard
appCardIdComponentMap.put(appCard.id, getComponentMapFromAppCard(appCard))
}
}
/**
* Update app card component
* - Component's tagged with [EnforceFastUpdateRate] will be updated before system is
* ready for a full app card update
* - Otherwise, the component will be updated when the system is ready for a full update
*
* @param id id of app card being requested
* @param component app card component that should be updated
*/
fun sendAppCardComponentUpdate(id: String, component: Component) {
synchronized(lock) {
if (!isAppCardActive(id)) {
Log.e(TAG, "For component updates, app card must be active")
return
}
val latestAppCard = latestAppCard[id]
val componentMap = appCardIdComponentMap[id] ?: return
if (!componentMap.containsKey(component.componentId)) {
Log.e(
TAG,
"For component updates, component must already exist inside app card"
)
return
}
var supportedAppCard = true
var updated: AppCard? = null
if (latestAppCard is ImageAppCard) {
latestAppCard.updateComponent(component)
updated = latestAppCard
} else {
supportedAppCard = false
}
updated ?: run {
if (supportedAppCard) {
Log.e(TAG, "No matching component found")
} else {
Log.e(TAG, "Unsupported app card")
}
return
}
this.latestAppCard[updated.id] = updated
appCardIdComponentMap[updated.id] = getComponentMapFromAppCard(updated)
if (component.javaClass.isAnnotationPresent(EnforceFastUpdateRate::class.java)) {
requestUpdate(updated.id, component.componentId)
}
}
}
protected abstract val appCardIds: List<String>
/**
* Handle cleanup for when an app card is removed
*
* @param id id of app card being removed
*/
protected abstract fun onAppCardRemoved(id: String)
/**
* Handle updates to App Card's [AppCardContext]
*
* @param id app card ID whose context is being update
* @param appCardContext updated context
*/
protected abstract fun onAppCardContextChanged(id: String, appCardContext: AppCardContext)
/** Request system to ask for an update for app card or app card component with given IDs */
private fun requestUpdate(id: String, componentId: String) {
val context = context ?: run {
Log.e(TAG, "Unable to get content resolver since context is null")
return
}
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.appendPath(id)
.appendPath(componentId)
.build()
val observer = null
context.contentResolver.notifyChange(uri, observer)
}
final override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor? = null
final override fun getType(uri: Uri): String? = TYPE
final override fun insert(uri: Uri, contentValues: ContentValues?): Uri? = null
final override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int = 0
final override fun update(
uri: Uri,
contentValues: ContentValues?,
s: String?,
strings: Array<String>?,
): Int = 0
final override fun query(
uri: Uri,
projection: Array<String>?,
queryArgs: Bundle?,
cancellationSignal: CancellationSignal?,
): Cursor? {
synchronized(lock) {
var id: String? = null
var appCardContext: AppCardContext? = null
queryArgs?.let {
val defaultValue = null
id = it.getString(BUNDLE_KEY_APP_CARD_ID, defaultValue)
val appCardContextBundle = it.getBundle(BUNDLE_KEY_APP_CARD_CONTEXT)
appCardContext = fromBundle(appCardContextBundle)
}
return when (val method = getMethod(uri)) {
AppCardMessageConstants.MSG_SEND_ALL_APP_CARDS -> handleSendAllAppCards(appCardContext)
AppCardMessageConstants.MSG_APP_CARD_UPDATE -> handleAppCardUpdate(id)
AppCardMessageConstants.MSG_APP_CARD_ADDED -> handleAppCardAdded(id, appCardContext)
else -> {
Log.e(TAG, "Unrecognized method: $method")
null
}
}
}
}
private fun handleSendAllAppCards(appCardContext: AppCardContext?): Cursor? {
appCardContext ?: run {
Log.e(TAG, "App Card Context must exist")
return null
}
val cursor = MatrixCursor(arrayOf(CURSOR_COLUMN_APP_CARD_TRANSPORT))
for (appCardId in appCardIds) {
cursor.addRow(
arrayOf(
ParcelableUtils.parcelableToBytes(AppCardTransport(getAppCard(appCardId, appCardContext)))
)
)
}
return cursor
}
private fun handleAppCardUpdate(id: String?): Cursor? {
id ?: run {
Log.e(TAG, "App Card ID must exist")
return null
}
val appCard = latestAppCard[id] ?: run {
Log.e(TAG, "App Card ID must be active")
return null
}
val cursor = MatrixCursor(arrayOf(CURSOR_COLUMN_APP_CARD_TRANSPORT))
cursor.addRow(
arrayOf(
ParcelableUtils.parcelableToBytes(AppCardTransport(appCard))
)
)
return cursor
}
private fun handleAppCardAdded(id: String?, appCardContext: AppCardContext?): Cursor? {
id ?: run {
Log.e(TAG, "App Card ID must exist")
return null
}
appCardContext ?: run {
Log.e(TAG, "App Card Context must exist")
return null
}
val appCard = getAppCard(id, appCardContext)
val cursor = MatrixCursor(arrayOf(CURSOR_COLUMN_APP_CARD_TRANSPORT))
cursor.addRow(
arrayOf(
ParcelableUtils.parcelableToBytes(AppCardTransport(appCard))
)
)
return cursor
}
private fun getMethod(uri: Uri) = uri.pathSegments[0]
final override fun call(
method: String,
arg: String?,
extras: Bundle?,
): Bundle? {
synchronized(lock) {
var id: String? = null
var componentId: String? = null
var interactionId: String? = null
var appCardContext: AppCardContext? = null
extras?.let {
val defaultValue = null
id = it.getString(BUNDLE_KEY_APP_CARD_ID, defaultValue)
componentId = it.getString(BUNDLE_KEY_APP_CARD_COMPONENT_ID, defaultValue)
interactionId = it.getString(BUNDLE_KEY_APP_CARD_INTERACTION_ID, defaultValue)
val appCardContextBundle = it.getBundle(BUNDLE_KEY_APP_CARD_CONTEXT)
appCardContext = fromBundle(appCardContextBundle)
}
var bundle: Bundle? = null
when (method) {
AppCardMessageConstants.MSG_APP_CARD_COMPONENT_UPDATE ->
bundle = handleAppCardComponentUpdate(id, componentId)
AppCardMessageConstants.MSG_APP_CARD_REMOVED -> removeAppCard(id)
AppCardMessageConstants.MSG_APP_CARD_INTERACTION ->
handleInteraction(id, componentId, interactionId)
AppCardMessageConstants.MSG_APP_CARD_CONTEXT_UPDATE ->
handleAppCardContextUpdate(id, appCardContext)
AppCardMessageConstants.MSG_CLOSE_PROVIDER -> handleClose()
else -> Log.e(TAG, "Unrecognized method: $method")
}
return bundle
}
}
private fun handleAppCardComponentUpdate(id: String?, componentId: String?): Bundle? {
id ?: run {
Log.e(TAG, "App Card ID must exist")
return null
}
componentId ?: run {
Log.e(TAG, "App Card Component ID must exist")
return null
}
val componentMap = appCardIdComponentMap[id] ?: run {
Log.e(TAG, "App Card ID must exist in component map")
return null
}
val component = componentMap[componentId] ?: run {
Log.e(TAG, "App Card Component ID must exist in component map")
return null
}
val bundle = Bundle()
bundle.putParcelable(BUNDLE_KEY_APP_CARD_COMPONENT, AppCardTransport(component))
return bundle
}
private fun handleInteraction(id: String?, componentId: String?, interactionId: String?) {
id ?: run {
Log.e(TAG, "App Card ID must exist")
return
}
componentId ?: run {
Log.e(TAG, "App Card Component ID must exist")
return
}
interactionId ?: run {
Log.e(TAG, "App Card Component's interaction ID must exist")
return
}
val componentInteractionMap = appCardIdComponentMap[id] ?: run {
Log.e(TAG, "App Card ID is not active: $id")
return
}
val component = componentInteractionMap[componentId] ?: run {
Log.e(TAG, "Component ID ($componentId) does not exist in app card with ID: $id")
return
}
if (component !is Button) {
Log.e(
TAG,
"Component ID (" + componentId + ") in app card with ID (" + id +
") is not interactable: " + interactionId
)
return
}
if (InteractionMessageConstants.MSG_INTERACTION_ON_CLICK == interactionId) {
component.onClickListener?.onClick()
} else {
Log.e(
TAG,
"Component ID (" + componentId + ") in app card with ID (" + id +
") does not contain interaction: " + interactionId
)
}
}
private fun handleClose() {
if (!activeAppCardCountMap.isEmpty()) return
if (dispatcher.getDesiredState() == Lifecycle.Event.ON_CREATE) dispatcher.queueOnStart()
if (dispatcher.getDesiredState() == Lifecycle.Event.ON_START) dispatcher.queueOnResume()
if (dispatcher.getDesiredState() == Lifecycle.Event.ON_RESUME) dispatcher.queueOnPause()
dispatcher.queueOnStop()
dispatcher.queueOnDestroy()
}
private fun handleAppCardContextUpdate(id: String?, appCardContext: AppCardContext?) {
id ?: run {
Log.e(TAG, "App Card ID must exist")
return
}
appCardContext ?: run {
Log.e(TAG, "App Card Context must exist")
return
}
onAppCardContextChanged(id, appCardContext)
}
private fun getAppCard(id: String, appCardContext: AppCardContext): AppCard {
val appCard = latestAppCard[id] ?: run {
val temp = onAppCardAdded(id, appCardContext)
check(value = temp.id == id) { "Requested app card did not contain required ID" }
temp
}
if (!appCard.verifyUniquenessOfComponentIds()) {
Log.e(TAG, "App cards must contain unique component IDs")
// Do not throw error here since this will be verified and dropped on system side
// and a call to {@link AppCardContentProvider#onAppCardRemoved(String)} will be called
return appCard
}
latestAppCard[appCard.id] = appCard
activeAppCardCountMap[id] = (activeAppCardCountMap[id]?.let { it + 1 }) ?: run {
if (dispatcher.getDesiredState() != Lifecycle.Event.ON_RESUME) {
if (dispatcher.getDesiredState() != Lifecycle.Event.ON_START) dispatcher.queueOnStart()
dispatcher.queueOnResume()
}
1 // run's return value
}
appCardIdComponentMap[id] = getComponentMapFromAppCard(appCard)
return appCard
}
private fun removeAppCard(id: String?) {
id ?: run {
Log.e(TAG, "App Card ID must exist")
return
}
val defaultValue = 0
var count = activeAppCardCountMap.getOrDefault(id, defaultValue)
when (count) {
0 -> Log.e(TAG, "App card remove requested for an inactive app card")
1 -> {
activeAppCardCountMap.remove(id)
appCardIdComponentMap.remove(id)
latestAppCard.remove(id)
onAppCardRemoved(id)
if (activeAppCardCountMap.isEmpty() &&
dispatcher.getDesiredState() != Lifecycle.Event.ON_STOP) {
if (dispatcher.getDesiredState() != Lifecycle.Event.ON_PAUSE) dispatcher.queueOnPause()
dispatcher.queueOnStop()
}
}
else -> activeAppCardCountMap[id] = --count
}
}
private fun getComponentMapFromAppCard(appCard: AppCard?): ConcurrentMap<String, Component> {
val result: ConcurrentMap<String, Component> = ConcurrentHashMap()
if (appCard is ImageAppCard) {
appCard.image?.let {
result[it.componentId] = it
}
for (button in appCard.buttons) {
result[button.componentId] = button
}
appCard.header?.let {
result[it.componentId] = it
}
appCard.progressBar?.let {
result[it.componentId] = it
}
}
return result
}
private fun isAppCardActive(id: String): Boolean {
return appCardIdComponentMap.containsKey(id) && latestAppCard.containsKey(id)
}
companion object {
/** Bundle key for app card ID */
const val BUNDLE_KEY_APP_CARD_ID = "appCardId"
/** Bundle key for app card component ID */
const val BUNDLE_KEY_APP_CARD_COMPONENT_ID = "appCardComponentId"
/** Bundle key for app card component's interaction ID */
const val BUNDLE_KEY_APP_CARD_INTERACTION_ID = "appCardInteractionId"
/** Bundle key for app card context bundle */
const val BUNDLE_KEY_APP_CARD_CONTEXT = "appCardContext"
/** Bundle key for app card component */
const val BUNDLE_KEY_APP_CARD_COMPONENT = "appCardComponent"
/** Cursor column name for app card transport */
const val CURSOR_COLUMN_APP_CARD_TRANSPORT = "appCardTransport"
private const val TAG = "AppCardContentProvider"
private const val TYPE = "android.car.appcard"
}
}