blob: a97a981dff4ff80472a9709c72dcf47be96055a6 [file]
/*
* Copyright 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.host
import android.app.ActivityManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ProviderInfo
import android.os.Build
import android.os.Bundle
import android.os.DeadObjectException
import android.os.UserHandle
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.car.appcard.AppCard
import com.android.car.appcard.AppCardContentProvider
import com.android.car.appcard.AppCardContext
import com.android.car.appcard.AppCardMessageConstants
import com.android.car.appcard.host.AppCardObserver.AppCardObserverCallback
import com.android.car.appcard.host.AppCardTimer.UpdateReadyListener
import com.android.car.appcard.internal.AppCardTransport
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.MoreExecutors
import com.google.protobuf.InvalidProtocolBufferException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.function.Consumer
import kotlin.math.max
class AppCardHost internal constructor(
c: Context,
updateRate: Int,
fastUpdateRate: Int,
private val responseExecutor: Executor,
private val timerProvider: AppCardTimerProvider,
private val userProvider: UserProvider,
) : UpdateReadyListener, AppCardObserverCallback {
constructor(
c: Context,
updateRate: Int,
fastUpdateRate: Int,
responseExecutor: Executor,
) : this(
c,
updateRate,
fastUpdateRate,
responseExecutor,
object : AppCardTimerProvider {
override fun getNewTimer(
listener: UpdateReadyListener,
updateRateMs: Int,
fastUpdateRateMs: Int,
) = AppCardTimer(listener, updateRateMs, fastUpdateRateMs)
},
object : UserProvider {
override fun getCurrentUser() = ActivityManager.getCurrentUser()
}
)
private val listeners: MutableSet<AppCardListener>
private val idBrokerMap: ConcurrentMap<ApplicationIdentifier, BrokerWrapper>
private val executorService = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool())
private val packageManager: PackageManager
private val contentResolver: ContentResolver
private val updateRateMs: Int
private val fastUpdateRateMs: Int
private val brokerFactory: BrokerFactory
private var context: Context
private var currentUser = userProvider.getCurrentUser()
@VisibleForTesting
internal var appChangeReceiver: AppChangeReceiver? = null
@VisibleForTesting
internal var userChangeReceiver: UserChangeReceiver? = null
init {
context = createUserContext(c, currentUser)
packageManager = context.packageManager
contentResolver = context.contentResolver
listeners = HashSet()
idBrokerMap = ConcurrentHashMap()
updateRateMs = max(updateRate.toDouble(), MINIMUM_UPDATE_RATE_MS.toDouble()).toInt()
fastUpdateRateMs = max(fastUpdateRate.toDouble(), MINIMUM_FAST_UPDATE_RATE_MS.toDouble())
.toInt()
val expectedBrokerPermission =
context.resources.getString(com.android.car.appcard.R.string.host_permission)
brokerFactory = ContentProviderBrokerFactory(
context,
contentResolver,
packageManager,
expectedBrokerPermission,
callback = this
)
brokerFactory.connectToAllCompatibleApplications().forEach {
idBrokerMap[it.key] = BrokerWrapper(it.value, it.key)
}
setupAppChangeReceiver()
setupUserChangeReceiver()
}
private fun setupAppChangeReceiver() {
val filter = IntentFilter()
filter.apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme(DATA_SCHEME_PACKAGE)
}
appChangeReceiver = AppChangeReceiver(currentUser)
val broadcastPermission = null
val scheduler = null
context.registerReceiverForAllUsers(
appChangeReceiver,
filter,
broadcastPermission,
scheduler
)
}
private fun setupUserChangeReceiver() {
val filter = IntentFilter()
filter.addAction(Intent.ACTION_USER_FOREGROUND)
userChangeReceiver = UserChangeReceiver()
val broadcastPermission = null
val scheduler = null
context.registerReceiverForAllUsers(
userChangeReceiver,
filter,
broadcastPermission,
scheduler
)
}
/** Add any new added [AppCardBroker] */
fun refreshCompatibleApplication() {
synchronized(idBrokerMap) {
brokerFactory.refreshCompatibleApplication(idBrokerMap.keys).forEach {
idBrokerMap[it.key] = BrokerWrapper(it.value, it.key)
}
}
}
private fun closeAllApplicationConnections() {
synchronized(idBrokerMap) {
idBrokerMap.values.forEach(
Consumer { brokerWrapper: BrokerWrapper ->
val appCardIds = brokerWrapper.activeAppCardMap.values.map {
it.appCard.id
}
appCardIds.forEach {
notifyAppCardRemovedInternal(
it,
brokerWrapper.identifier,
brokerWrapper
)
}
brokerWrapper.broker.close()
}
)
idBrokerMap.clear()
}
}
/** Cleanup all connections and internal members */
fun destroy() {
synchronized(idBrokerMap) {
context.unregisterReceiver(appChangeReceiver)
context.unregisterReceiver(userChangeReceiver)
synchronized(listeners) {
listeners.clear()
}
closeAllApplicationConnections()
}
}
/** Register an app card listener */
fun registerListener(listener: AppCardListener) {
synchronized(listeners) { listeners.add(listener) }
}
/** Unregister an app card listener */
fun unregisterListener(listener: AppCardListener) {
synchronized(listeners) { listeners.remove(listener) }
}
/**
* Request all available app cards from all bound
* [com.android.car.appcard.AppCardContentProvider]s
*/
fun getAllAppCards(context: AppCardContext) {
synchronized(idBrokerMap) {
idBrokerMap.values.forEach(
Consumer { brokerWrapper: BrokerWrapper ->
val broker = brokerWrapper.broker
val identifier = brokerWrapper.identifier
val activeAppCardMap = brokerWrapper.activeAppCardMap
val listenableFuture = executorService.submit<List<AppCardContainer>> {
val bundle = Bundle()
bundle.putBundle(
AppCardContentProvider.BUNDLE_KEY_APP_CARD_CONTEXT,
context.toBundle()
)
val appCardTransports = try {
broker.getAppCardTransports(
identifier,
bundle,
AppCardMessageConstants.MSG_SEND_ALL_APP_CARDS
)
} catch (exception: Exception) {
if (exception is ContentProviderBroker.ContentProviderBrokerException) {
if (exception.cause is DeadObjectException) {
handleInvalidBroker(identifier)
}
}
throw exception
}
getHostAppCardsFromTransport(appCardTransports, identifier)
}
Futures.addCallback(
listenableFuture,
getAllAppCardsFutureCallBack(activeAppCardMap, identifier),
responseExecutor
)
}
)
}
}
private fun getAllAppCardsFutureCallBack(
activeAppCardMap: ConcurrentMap<String, ActiveAppCard>,
identifier: ApplicationIdentifier,
): FutureCallback<List<AppCardContainer>> {
return object : FutureCallback<List<AppCardContainer>> {
override fun onSuccess(appCards: List<AppCardContainer>) {
synchronized(listeners) {
listeners.forEach(
Consumer { appCardListener: AppCardListener ->
appCards.forEach { appCardContainer ->
appCardListener.onAppCardReceived(appCardContainer)
val appCard = appCardContainer.appCard
val appCardId = appCard.id
val activeCard = activeAppCardMap[appCardId]
activeCard?.appCardTimer?.updateAppCard(
AppCardContainer(identifier, appCard)
) ?: setupCacheTimer(
activeAppCardMap,
identifier,
appCard
)
}
}
)
}
}
override fun onFailure(throwable: Throwable) {
Log.e(TAG, throwable.message, throwable)
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener ->
appCardListener.onPackageCommunicationError(
identifier,
throwable
)
})
}
}
}
}
private fun handleInvalidBroker(identifier: ApplicationIdentifier) {
synchronized(idBrokerMap) {
val brokerWrapper = idBrokerMap[identifier] ?: run {
Log.d(TAG, "Inactive content provider is invalid")
return
}
val activeAppCards = mutableListOf<String>()
brokerWrapper.broker.close()
brokerWrapper.activeAppCardMap.values.forEach {
activeAppCards.add(it.appCard.id)
it.appCardTimer.destroy()
}
if (activeAppCards.isEmpty()) {
Log.d(TAG, "Content provider with no active app cards has become invalid")
} else {
Log.d(
TAG,
"Content provider has become invalid, affected app card IDs: $activeAppCards"
)
}
brokerWrapper.activeAppCardMap.clear()
idBrokerMap.remove(identifier)
}
}
/** Request an app card */
fun requestAppCard(
context: AppCardContext,
identifier: ApplicationIdentifier,
appCardId: String,
) = retrieveAppCard(AppCardMessageConstants.MSG_APP_CARD_ADDED, context, identifier, appCardId)
/** Notify [com.android.car.appcard.AppCardContentProvider] that its app card has been removed */
fun notifyAppCardRemoved(
identifier: ApplicationIdentifier,
appCardId: String,
) {
synchronized(idBrokerMap) {
val brokerWrapper = idBrokerMap[identifier] ?: return
notifyAppCardRemovedInternal(appCardId, identifier, brokerWrapper)
}
}
private fun notifyAppCardRemovedInternal(
appCardId: String,
identifier: ApplicationIdentifier,
brokerWrapper: BrokerWrapper,
) {
synchronized(idBrokerMap) {
val broker = brokerWrapper.broker
val listenableFuture = executorService.submit<Boolean> {
val bundle = Bundle()
bundle.putString(AppCardContentProvider.BUNDLE_KEY_APP_CARD_ID, appCardId)
try {
broker.sendMessage(AppCardMessageConstants.MSG_APP_CARD_REMOVED, bundle, appCardId)
} catch (exception: Exception) {
if (exception is ContentProviderBroker.ContentProviderBrokerException) {
if (exception.cause is DeadObjectException) {
handleInvalidBroker(identifier)
}
}
throw exception
}
true
}
Futures.addCallback(
listenableFuture,
object : FutureCallback<Boolean> {
override fun onSuccess(aBoolean: Boolean) {
// no-op
}
override fun onFailure(throwable: Throwable) {
Log.e(TAG, throwable.message, throwable)
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onPackageCommunicationError(identifier, throwable)
})
}
}
},
responseExecutor
)
val cache = brokerWrapper.activeAppCardMap[appCardId]
cache?.appCardTimer?.destroy()
brokerWrapper.activeAppCardMap.remove(appCardId)
}
}
/**
* Notify [com.android.car.appcard.AppCardContentProvider] that a component inside
* an app card has been interacted with
*/
fun notifyAppCardInteraction(
identifier: ApplicationIdentifier,
appCardId: String,
componentId: String,
interactionType: String,
) {
synchronized(idBrokerMap) {
val brokerWrapper = idBrokerMap[identifier] ?: return
val broker = brokerWrapper.broker
val timer = brokerWrapper.activeAppCardMap[appCardId]?.appCardTimer
val listenableFuture = executorService.submit<Boolean> {
val bundle = Bundle()
bundle.apply {
putString(AppCardContentProvider.BUNDLE_KEY_APP_CARD_ID, appCardId)
putString(
AppCardContentProvider.BUNDLE_KEY_APP_CARD_COMPONENT_ID,
componentId
)
putString(
AppCardContentProvider.BUNDLE_KEY_APP_CARD_INTERACTION_ID,
interactionType
)
}
try {
broker.sendMessage(
AppCardMessageConstants.MSG_APP_CARD_INTERACTION,
bundle,
errorId = "$appCardId#$componentId#$interactionType"
)
} catch (exception: Exception) {
if (exception is ContentProviderBroker.ContentProviderBrokerException) {
if (exception.cause is DeadObjectException) {
handleInvalidBroker(identifier)
}
}
throw exception
}
true
}
Futures.addCallback(
listenableFuture,
object : FutureCallback<Boolean> {
override fun onSuccess(result: Boolean) {
timer?.resetAppCardTimerAndRequestUpdate()
}
override fun onFailure(throwable: Throwable) {
Log.e(TAG, throwable.message, throwable)
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onPackageCommunicationError(identifier, throwable)
})
}
}
},
responseExecutor
)
}
}
/** Send an [AppCardContext] update for a specific app card */
fun sendAppCardContextUpdate(
appCardContext: AppCardContext,
identifier: ApplicationIdentifier,
appCardId: String,
) {
synchronized(idBrokerMap) {
val brokerWrapper = idBrokerMap[identifier] ?: return
val broker = brokerWrapper.broker
val timer = brokerWrapper.activeAppCardMap[appCardId]?.appCardTimer
val listenableFuture = executorService.submit<Boolean> {
val bundle = Bundle()
bundle.apply {
putString(AppCardContentProvider.BUNDLE_KEY_APP_CARD_ID, appCardId)
putBundle(
AppCardContentProvider.BUNDLE_KEY_APP_CARD_CONTEXT,
appCardContext.toBundle()
)
}
try {
broker.sendMessage(AppCardMessageConstants.MSG_APP_CARD_CONTEXT_UPDATE, bundle, appCardId)
} catch (exception: Exception) {
if (exception is ContentProviderBroker.ContentProviderBrokerException) {
if (exception.cause is DeadObjectException) {
handleInvalidBroker(identifier)
}
}
throw exception
}
true
}
Futures.addCallback(
listenableFuture,
object : FutureCallback<Boolean> {
override fun onSuccess(result: Boolean) {
timer?.resetAppCardTimerAndRequestUpdate()
}
override fun onFailure(throwable: Throwable) {
Log.e(TAG, throwable.message, throwable)
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onPackageCommunicationError(identifier, throwable)
})
}
}
},
responseExecutor
)
}
}
private fun retrieveAppCard(
eventName: String,
context: AppCardContext?,
identifier: ApplicationIdentifier,
appCardId: String,
) {
synchronized(idBrokerMap) {
val brokerWrapper = idBrokerMap[identifier] ?: return
val broker = brokerWrapper.broker
val activeAppCardMap = brokerWrapper.activeAppCardMap
val listenableFuture = executorService.submit<AppCardContainer> {
val bundle = Bundle()
bundle.putString(AppCardContentProvider.BUNDLE_KEY_APP_CARD_ID, appCardId)
if (AppCardMessageConstants.MSG_APP_CARD_ADDED == eventName) {
context?.also {
bundle.putBundle(
AppCardContentProvider.BUNDLE_KEY_APP_CARD_CONTEXT,
context.toBundle()
)
} ?: throw AppCardHostException(
eventName,
appCardId,
IllegalStateException(
"Null AppCardContext for " + AppCardMessageConstants.MSG_APP_CARD_ADDED
)
)
}
val appCardTransport = try {
broker.getAppCardTransport(
identifier,
appCardId,
bundle,
eventName
)
} catch (exception: Exception) {
if (exception is ContentProviderBroker.ContentProviderBrokerException) {
if (exception.cause is DeadObjectException) {
handleInvalidBroker(identifier)
}
}
throw exception
}
val appCard = appCardTransport.appCard ?: throw AppCardHostException(
eventName,
appCardId,
IllegalStateException("App card missing from transport")
)
AppCardContainer(identifier, appCard)
}
Futures.addCallback(
listenableFuture,
getRetrieveAppCardFutureCallback(
eventName,
activeAppCardMap,
identifier,
appCardId
),
responseExecutor
)
}
}
private fun getRetrieveAppCardFutureCallback(
eventName: String,
activeAppCardMap: ConcurrentMap<String, ActiveAppCard>,
identifier: ApplicationIdentifier,
appCardId: String,
): FutureCallback<AppCardContainer> {
return object : FutureCallback<AppCardContainer> {
override fun onSuccess(appCardContainer: AppCardContainer) {
val appCard = appCardContainer.appCard
val validAppCard = appCard.verifyUniquenessOfComponentIds()
if (!validAppCard) {
if (AppCardMessageConstants.MSG_APP_CARD_ADDED == eventName) {
notifyAppCardRemoved(identifier, appCardId)
}
return
}
synchronized(idBrokerMap) {
activeAppCardMap[appCardId]?.appCardTimer?.updateAppCard(
AppCardContainer(
identifier,
appCard
)
) ?: setupCacheTimer(activeAppCardMap, identifier, appCard)
}
synchronized(listeners) {
listeners.forEach(
Consumer { appCardListener: AppCardListener ->
appCardListener.onAppCardReceived(appCardContainer)
}
)
}
}
override fun onFailure(throwable: Throwable) {
Log.e(TAG, throwable.message, throwable)
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onPackageCommunicationError(identifier, throwable)
})
}
}
}
}
private fun setupCacheTimer(
activeAppCardMap: ConcurrentMap<String, ActiveAppCard>,
identifier: ApplicationIdentifier,
appCard: AppCard,
) {
synchronized(idBrokerMap) {
val timer = timerProvider.getNewTimer(
listener = this,
updateRateMs = updateRateMs,
fastUpdateRateMs = fastUpdateRateMs
)
timer.updateAppCard(AppCardContainer(identifier, appCard))
activeAppCardMap.put(appCard.id, ActiveAppCard(appCard, timer))
}
}
private fun retrieveAppCardComponentUpdate(
brokerWrapper: BrokerWrapper,
activeAppCard: ActiveAppCard,
identifier: ApplicationIdentifier,
appCardId: String,
componentId: String,
) {
synchronized(idBrokerMap) {
val broker = brokerWrapper.broker
val timer = activeAppCard.appCardTimer
val listenableFuture = executorService.submit<AppCardComponentContainer> {
val bundle = Bundle()
bundle.apply {
putString(AppCardContentProvider.BUNDLE_KEY_APP_CARD_ID, appCardId)
putString(
AppCardContentProvider.BUNDLE_KEY_APP_CARD_COMPONENT_ID,
componentId
)
}
val appCardTransport = try {
broker.getAppCardTransport(
identifier,
errorId = "$appCardId#$componentId",
bundle,
AppCardMessageConstants.MSG_APP_CARD_COMPONENT_UPDATE
)
} catch (exception: Exception) {
if (exception is ContentProviderBroker.ContentProviderBrokerException) {
if (exception.cause is DeadObjectException) {
handleInvalidBroker(identifier)
}
}
throw exception
}
val component = appCardTransport.component ?: throw AppCardHostException(
AppCardMessageConstants.MSG_APP_CARD_COMPONENT_UPDATE,
id = "$appCardId#$componentId",
IllegalStateException("App card component missing from transport")
)
timer.componentUpdate(component.componentId)
AppCardComponentContainer(identifier, appCardId, component)
}
Futures.addCallback(
listenableFuture,
object : FutureCallback<AppCardComponentContainer> {
override fun onSuccess(appCardComponentContainer: AppCardComponentContainer) {
synchronized(listeners) {
listeners.forEach(
Consumer { appCardListener: AppCardListener ->
appCardListener.onComponentReceived(
appCardComponentContainer
)
}
)
}
}
override fun onFailure(throwable: Throwable) {
Log.e(TAG, throwable.message, throwable)
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onPackageCommunicationError(
identifier,
throwable
)
})
}
}
},
responseExecutor
)
}
}
private fun createUserContext(context: Context, userId: Int): Context {
val flags = 0
return context.createContextAsUser(UserHandle.of(userId), flags)
}
@VisibleForTesting
internal fun handlePackageAddedOrEnabled(packageName: String, authority: String?) {
synchronized(idBrokerMap) {
brokerFactory.refreshCompatibleApplication(idBrokerMap.keys).forEach {
idBrokerMap[it.key] = BrokerWrapper(it.value, it.key)
}
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onProviderAdded(packageName, authority)
})
}
}
}
@VisibleForTesting
internal fun handlePackageRemovedOrDisabled(packageName: String, authority: String?) {
synchronized(idBrokerMap) {
var isRemoved = false
idBrokerMap.entries.removeIf { (key, value) ->
authority?.let {
if (!key.containsAuthority(it)) return@removeIf false
}
if (!key.containsPackage(packageName)) return@removeIf false
value.broker.close()
value.activeAppCardMap.clear()
isRemoved = true
true
}
if (!isRemoved) return
synchronized(listeners) {
listeners.forEach(Consumer { appCardListener: AppCardListener ->
appCardListener.onProviderRemoved(packageName, authority)
})
}
}
}
@VisibleForTesting
internal fun handlePackageChanged(changedPackageName: String, componentNames: Array<String>) {
for (name in componentNames) {
if (name == changedPackageName) {
val enabled = isPackageEnabled(name)
handlePackageStateChange(enabled, changedPackageName)
} else {
getApplicationIdentifier(changedPackageName, name)?.let {
val enabled = isComponentEnabled(changedPackageName, name)
handlePackageStateChange(enabled, changedPackageName, it.authority)
}
}
}
}
private fun handlePackageStateChange(
isEnabled: Boolean,
packageName: String,
authority: String? = null,
) {
if (isEnabled) {
handlePackageAddedOrEnabled(packageName, authority)
} else {
handlePackageRemovedOrDisabled(packageName, authority)
}
}
private fun getApplicationIdentifier(pkgName: String, clsName: String): ApplicationIdentifier? {
val providers = getProviderInfos(pkgName)
for (componentInfo in providers) {
if (componentInfo.name == clsName) {
return ApplicationIdentifier(componentInfo.packageName, componentInfo.authority)
}
}
// The component is not declared in the app's manifest
return null
}
private fun isPackageEnabled(pkgName: String): Boolean {
return try {
val flags = 0
val appInfo = packageManager.getApplicationInfo(pkgName, flags)
appInfo.enabled
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
private fun isComponentEnabled(pkgName: String, clsName: String): Boolean {
val componentName = ComponentName(pkgName, clsName)
return when (packageManager.getComponentEnabledSetting(componentName)) {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false
PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> getComponentDefaultState(pkgName, clsName)
else -> getComponentDefaultState(pkgName, clsName)
}
}
private fun getComponentDefaultState(pkgName: String, clsName: String): Boolean {
val providers = getProviderInfos(pkgName)
for (componentInfo in providers) {
if (componentInfo.name == clsName) return componentInfo.isEnabled
}
// The component is not declared in the app's manifest
return false
}
private fun getProviderInfos(pkgName: String): Array<ProviderInfo> {
return try {
val flags = PackageManager.GET_PROVIDERS or PackageManager.MATCH_DISABLED_COMPONENTS
val packageInfo = packageManager.getPackageInfo(pkgName, flags)
packageInfo.providers ?: arrayOf()
} catch (e: PackageManager.NameNotFoundException) {
arrayOf()
}
}
@VisibleForTesting
internal fun handleUserChange() {
synchronized(idBrokerMap) {
closeAllApplicationConnections()
currentUser = userProvider.getCurrentUser()
idBrokerMap.clear()
brokerFactory.connectToAllCompatibleApplications().forEach {
idBrokerMap[it.key] = BrokerWrapper(it.value, it.key)
}
}
}
override fun appCardIsReadyForUpdate(
identifier: ApplicationIdentifier?,
appCardId: String?,
) {
appCardId ?: return
identifier ?: return
retrieveAppCard(
eventName = AppCardMessageConstants.MSG_APP_CARD_UPDATE,
context = null,
identifier = identifier,
appCardId = appCardId
)
}
private class BrokerWrapper(
var broker: AppCardBroker,
var identifier: ApplicationIdentifier,
) {
var activeAppCardMap: ConcurrentMap<String, ActiveAppCard> = ConcurrentHashMap()
}
private class ActiveAppCard(
var appCard: AppCard,
var appCardTimer: AppCardTimer,
)
internal inner class AppChangeReceiver(private val userId: Int) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
intent.data ?: return
intent.action ?: return
intent.extras ?: return
val data = intent.data
val changedPackageName = data?.schemeSpecificPart
changedPackageName ?: run {
Log.d(TAG, "App change broadcast received without package name")
return
}
val extras = intent.extras
val defaultValue = -1
val uid = extras?.getInt(Intent.EXTRA_UID, defaultValue)
if (uid == -1 || uid == null) {
Log.d(TAG, "App change broadcast received without UID")
return
}
val packageUserHandle = UserHandle.getUserHandleForUid(uid)
if (packageUserHandle != UserHandle.of(this.userId)) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED ->
handlePackageAddedOrEnabled(changedPackageName, authority = null)
Intent.ACTION_PACKAGE_CHANGED -> {
val componentNames =
extras.getStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST)
if (componentNames.isNullOrEmpty()) {
Log.d(TAG, "App change broadcast received without component name list")
return
}
handlePackageChanged(changedPackageName, componentNames)
}
Intent.ACTION_PACKAGE_REMOVED ->
handlePackageRemovedOrDisabled(changedPackageName, authority = null)
}
}
}
internal inner class UserChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == null || Intent.ACTION_USER_FOREGROUND != intent.action) return
handleUserChange()
}
}
override fun handleAppCardAppComponentRequest(
authority: String,
id: String,
componentId: String,
) {
synchronized(idBrokerMap) {
val identifier = idBrokerMap.keys
.stream()
.filter { i: ApplicationIdentifier -> i.containsAuthority(authority) }
.findFirst()
val isEmpty = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
identifier.isEmpty
} else {
!identifier.isPresent
}
if (isEmpty) return
val brokerWrapper = idBrokerMap[identifier.get()] ?: return
val cacheTimer = brokerWrapper.activeAppCardMap[id] ?: return
if (!cacheTimer.appCardTimer.isComponentReadyForUpdate(componentId)) return
retrieveAppCardComponentUpdate(
brokerWrapper,
cacheTimer,
identifier.get(),
id,
componentId
)
}
}
/** Internal interface to abstract out timers from tests */
internal interface AppCardTimerProvider {
fun getNewTimer(
listener: UpdateReadyListener,
updateRateMs: Int,
fastUpdateRateMs: Int,
): AppCardTimer
}
/** Internal interface to abstract out [ActivityManager.getCurrentUser] */
internal interface UserProvider {
fun getCurrentUser(): Int
}
class AppCardHostException(
val method: String,
val id: String?,
cause: Throwable,
) :
Exception(
id?.let {
"Exception occurred when calling (with $method) app card content provider (ID: $id)"
} ?: "Exception occurred when calling (with $method) app card content provider",
cause
)
companion object {
private const val TAG = "AppCardHost"
private const val MINIMUM_UPDATE_RATE_MS = 5000
private const val MINIMUM_FAST_UPDATE_RATE_MS = 500
private const val DATA_SCHEME_PACKAGE = "package"
@Throws(AppCardHostException::class)
private fun getHostAppCardsFromTransport(
appCardTransports: List<AppCardTransport>,
identifier: ApplicationIdentifier,
): List<AppCardContainer> {
val appCards: MutableList<AppCardContainer> = ArrayList()
for (appCardTransport in appCardTransports) {
val appCard: AppCard? = try {
appCardTransport.appCard
} catch (e: InvalidProtocolBufferException) {
throw AppCardHostException(
AppCardMessageConstants.MSG_SEND_ALL_APP_CARDS,
id = null,
e
)
}
appCard?.let {
appCards.add(AppCardContainer(identifier, it))
}
}
return appCards
}
}
}