blob: 35494546c0510aacb324f274e1f5ef8c5ac2b330 [file] [log] [blame]
/*
* Copyright (C) 2021 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.google.android.connecteddevice.api
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.ParcelUuid
import android.os.RemoteException
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import com.google.android.companionprotos.Query
import com.google.android.companionprotos.QueryResponse
import com.google.android.companionprotos.SystemQuery
import com.google.android.companionprotos.SystemQueryType
import com.google.android.connecteddevice.api.Connector.AppNameCallback
import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR
import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR_FG
import com.google.android.connecteddevice.api.Connector.Companion.SYSTEM_FEATURE_ID
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_ALL
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_DRIVER
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_PASSENGER
import com.google.android.connecteddevice.api.Connector.Companion.UserType
import com.google.android.connecteddevice.api.Connector.QueryCallback
import com.google.android.connecteddevice.model.AssociatedDevice
import com.google.android.connecteddevice.model.ConnectedDevice
import com.google.android.connecteddevice.model.DeviceMessage
import com.google.android.connecteddevice.util.ByteUtils
import com.google.android.connecteddevice.util.Logger
import com.google.android.connecteddevice.util.SafeLog
import com.google.android.connecteddevice.util.aliveOrNull
import com.google.protobuf.ByteString
import com.google.protobuf.ExtensionRegistryLite
import com.google.protobuf.InvalidProtocolBufferException
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Class for establishing and maintaining a connection to the companion device platform.
*
* This connector will return different binders based on binding actions. Only foreground process
* should register listeners for feature coordinator initialization.
*
* @param context [Context] of the hosting process.
* @param isForegroundProcess Set to `true` if running from outside of the companion application.
* @param userType Filter devices to the matching [UserType].
*/
class CompanionConnector
@JvmOverloads
constructor(
private val context: Context,
private val isForegroundProcess: Boolean = false,
private val userType: @UserType Int = USER_TYPE_DRIVER,
) : Connector {
private val lock = ReentrantLock()
private val retryHandler = Handler(Looper.getMainLooper())
private val loggerId = Logger.getLogger().loggerId
private val isPlatformInitialized = AtomicBoolean(false)
private val waitingForConnection = AtomicBoolean(false)
private val featureCoordinatorAction =
if (isForegroundProcess) {
ACTION_BIND_FEATURE_COORDINATOR_FG
} else {
ACTION_BIND_FEATURE_COORDINATOR
}
private val featureCoordinatorConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (isForegroundProcess) {
handleForegroundUserConnection(service)
} else {
handleFeatureCoordinatorConnection(service)
}
}
override fun onServiceDisconnected(name: ComponentName) {
this@CompanionConnector.onServiceDisconnected()
}
override fun onNullBinding(name: ComponentName) {
this@CompanionConnector.onNullBinding()
}
override fun onBindingDied(name: ComponentName?) {
this@CompanionConnector.onBindingDied()
}
}
private val connectionCallback =
object : IConnectionCallback.Stub() {
override fun onDeviceConnected(device: ConnectedDevice) {
logd("Device ${device.deviceId} has connected. Notifying callback.")
callback?.onDeviceConnected(device)
if (device.hasSecureChannel()) {
callback?.onSecureChannelEstablished(device)
}
val featureId = featureId ?: return
logd("Registering device callback for $featureId on device ${device.deviceId}.")
// Only call method once. If featureCoordinator is not null, connectedDeviceManager is a
// wrapper and will result in a double call on featureCoordinator.
aliveFeatureCoordinator?.registerDeviceCallback(device, featureId, deviceCallback)
}
override fun onDeviceDisconnected(device: ConnectedDevice) {
logd("Device ${device.deviceId} has disconnected. Notifying callback.")
callback?.onDeviceDisconnected(device)
val featureId = featureId ?: return
logd("Unregistering device callback for $featureId on device ${device.deviceId}.")
// Only call method once. If featureCoordinator is not null, connectedDeviceManager is a
// wrapper and will result in a double call on featureCoordinator.
aliveFeatureCoordinator?.unregisterDeviceCallback(device, featureId, deviceCallback)
}
}
private val deviceCallback =
object : IDeviceCallback.Stub() {
override fun onSecureChannelEstablished(device: ConnectedDevice) {
logd("Secure channel has been established on ${device.deviceId}. Notifying callback.")
callback?.onSecureChannelEstablished(device)
}
override fun onMessageReceived(device: ConnectedDevice, message: DeviceMessage) {
processIncomingMessage(device, message)
}
override fun onDeviceError(device: ConnectedDevice, error: Int) {
logw("Received a device error of $error from ${device.deviceId}.")
callback?.onDeviceError(device, error)
}
}
private val deviceAssociationCallback =
object : IDeviceAssociationCallback.Stub() {
override fun onAssociatedDeviceAdded(device: AssociatedDevice) {
logd("New device ${device.deviceId} associated. Notifying callback.")
callback?.onAssociatedDeviceAdded(device)
}
override fun onAssociatedDeviceRemoved(device: AssociatedDevice) {
logd("Associated device ${device.deviceId} removed. Notifying callback.")
callback?.onAssociatedDeviceRemoved(device)
}
override fun onAssociatedDeviceUpdated(device: AssociatedDevice) {
logd("Associated device ${device.deviceId} updated. Notifying callback.")
callback?.onAssociatedDeviceUpdated(device)
}
}
private val logRequestedListener =
object : IOnLogRequestedListener.Stub() {
override fun onLogRecordsRequested() {
val loggerBytes = Logger.getLogger().toByteArray()
try {
featureCoordinator?.processLogRecords(loggerId, loggerBytes)
} catch (e: RemoteException) {
loge("Failed to send log records for logger $loggerId.", e)
}
}
}
private val queryIdGenerator = QueryIdGenerator()
// queryId -> callback
private val queryCallbacks: MutableMap<Int, QueryCallback> = ConcurrentHashMap()
// queryId -> original sender for response
private val queryResponseRecipients: MutableMap<Int, ParcelUuid> = ConcurrentHashMap()
private var bindAttempts = 0
// Binder returned for foreground users. Allow binding services/activities to register listeners
// and get notified when feature coordinator is initialized.
@VisibleForTesting
internal val foregroundUserBinder =
object : IFeatureCoordinatorStatusNotifier.Stub() {
override fun registerFeatureCoordinatorListener(listener: IFeatureCoordinatorListener) {
logd("Register feature coordinator listener.")
lock.withLock { registeredListeners.add(listener) }
if (isConnected) {
logd("Feature coordinator has already been initialized, notifying listeners.")
notifyFeatureCoordinatorListeners()
}
}
override fun unregisterFeatureCoordinatorListener(listener: IFeatureCoordinatorListener) {
logd("Unregister feature coordinator listener.")
lock.withLock { registeredListeners.removeIf { it.asBinder() == listener.asBinder() } }
}
}
@VisibleForTesting
internal var featureCoordinatorListener =
object : IFeatureCoordinatorListener.Stub() {
override fun onFeatureCoordinatorInitialized(featureCoordinator: IFeatureCoordinator) {
if (!isConnected) {
this@CompanionConnector.featureCoordinator = featureCoordinator
onServiceConnected()
}
}
}
@VisibleForTesting
@GuardedBy("lock")
internal var registeredListeners = mutableListOf<IFeatureCoordinatorListener>()
// Binder returned for system users. Allow binding services to access ConnectedDeviceService's
// feature coordinator.
@VisibleForTesting internal var featureCoordinator: IFeatureCoordinator? = null
@VisibleForTesting
internal var featureCoordinatorStatusNotifier: IFeatureCoordinatorStatusNotifier? = null
override var featureId: ParcelUuid? = null
override var callback: Connector.Callback? = null
override val connectedDevices: List<ConnectedDevice>
get() {
if (!isConnected) {
logw(
"Attempted to get connected devices before the platform was connected. Returning empty " +
"list."
)
return emptyList()
}
return when (userType) {
USER_TYPE_DRIVER -> aliveFeatureCoordinator?.connectedDevicesForDriver
USER_TYPE_PASSENGER -> aliveFeatureCoordinator?.connectedDevicesForPassengers
USER_TYPE_ALL -> aliveFeatureCoordinator?.allConnectedDevices
else -> null
}
?: emptyList()
}
override val isConnected: Boolean
get() = aliveFeatureCoordinator != null
private val isBound: Boolean
get() = featureCoordinator != null
private val aliveFeatureCoordinator
get() = featureCoordinator?.aliveOrNull()
private val aliveFeatureCoordinatorStatusNotifier
get() = featureCoordinatorStatusNotifier?.aliveOrNull()
override fun connect() {
logd("Initiating connection to companion platform.")
if (isConnected) {
logd("Platform is already connected. Skipping binding.")
initializePlatform()
return
}
if (waitingForConnection.compareAndSet(false, true)) {
bindAttempts = 0
bindToService()
}
}
private fun bindToService() {
logd("Platform is not currently connected. Initiating binding.")
val intent = resolveIntent(featureCoordinatorAction)
if (intent == null) {
loge("No services found supporting companion device. Aborting.")
callback?.onFailedToConnect()
return
}
val success = context.bindService(intent, featureCoordinatorConnection, /* flag= */ 0)
if (success) {
logd("Successfully started binding with ${intent.action}.")
return
}
bindAttempts++
if (bindAttempts > MAX_BIND_ATTEMPTS) {
loge("Failed to bind to service after $bindAttempts attempts. Aborting.")
waitingForConnection.set(false)
callback?.onFailedToConnect()
return
}
logw("Unable to bind to service with action ${intent.action}. Trying again.")
retryHandler.postDelayed(::bindToService, BIND_RETRY_DURATION.toMillis())
}
override fun disconnect() {
logd("Disconnecting from the companion platform.")
val wasConnected = isBound
logd("FeatureCoordinator is null: ${featureCoordinator == null} isBound: $isBound")
unbindFromService()
if (wasConnected) {
callback?.onDisconnected()
}
}
private fun unbindFromService() {
cleanUpFeatureCoordinator()
cleanUpFeatureCoordinatorStatusNotifier()
retryHandler.removeCallbacksAndMessages(/* token= */ null)
try {
context.unbindService(featureCoordinatorConnection)
} catch (e: IllegalArgumentException) {
logw("Attempted to unbind an already unbound service.")
}
waitingForConnection.set(false)
}
private fun cleanUpFeatureCoordinator() {
logd("Cleaning up FeatureCoordinator.")
aliveFeatureCoordinator?.let {
try {
it.unregisterConnectionCallback(connectionCallback)
val featureId = featureId
if (featureId != null) {
for (device in it.allConnectedDevices) {
it.unregisterDeviceCallback(device, featureId, deviceCallback)
}
}
it.unregisterDeviceAssociationCallback(deviceAssociationCallback)
it.unregisterOnLogRequestedListener(loggerId, logRequestedListener)
} catch (e: RemoteException) {
loge("Error while cleaning up FeatureCoordinator.", e)
}
}
// Only set to null if already non-null. Otherwise, this will also inadvertently null out the
// live connectedDeviceManager as well leaving us in a state where callbacks cannot be properly
// unregistered.
if (featureCoordinator != null) {
featureCoordinator = null
}
isPlatformInitialized.set(false)
}
private fun cleanUpFeatureCoordinatorStatusNotifier() {
logd("Clean up feature coordinator status notifier")
aliveFeatureCoordinatorStatusNotifier?.unregisterFeatureCoordinatorListener(
featureCoordinatorListener
)
}
override fun binderForAction(action: String): IBinder? {
logd("Binder for action: $action.")
return when (action) {
ACTION_BIND_FEATURE_COORDINATOR -> aliveFeatureCoordinator?.asBinder()
ACTION_BIND_FEATURE_COORDINATOR_FG -> foregroundUserBinder.asBinder()
else -> {
loge("Binder for unexpected action, returning null binder.")
null
}
}
}
override fun sendMessageSecurely(deviceId: String, message: ByteArray) {
if (!isConnected) {
loge("Unable to send message, the platform is not actively connected.")
callback?.onMessageFailedToSend(deviceId, message, isTransient = true)
return
}
val device = getConnectedDeviceById(deviceId)
if (device == null) {
loge("No matching device found with id $deviceId when trying to send secure message.")
callback?.onMessageFailedToSend(deviceId, message, isTransient = false)
return
}
sendMessageSecurely(device, message)
}
override fun sendMessageSecurely(device: ConnectedDevice, message: ByteArray) {
if (!isConnected) {
loge("Unable to send message, the platform is not actively connected.")
callback?.onMessageFailedToSend(device.deviceId, message, isTransient = true)
return
}
val deviceMessage =
DeviceMessage.createOutgoingMessage(
featureId?.uuid,
/* isMessageEncrypted= */ true,
DeviceMessage.OperationType.CLIENT_MESSAGE,
message
)
try {
sendMessageInternal(device, deviceMessage)
} catch (e: RemoteException) {
loge("Error while sending secure message.", e)
callback?.onMessageFailedToSend(device.deviceId, message, isTransient = false)
}
}
override fun sendQuerySecurely(
deviceId: String,
request: ByteArray,
parameters: ByteArray?,
callback: QueryCallback,
) {
if (!isConnected) {
loge("Unable to send query, the platform is not actively connected.")
callback.onQueryFailedToSend(isTransient = true)
return
}
val device = getConnectedDeviceById(deviceId)
if (device == null) {
loge("No matching device found with id $deviceId when trying to send a secure query.")
callback.onQueryFailedToSend(isTransient = false)
return
}
sendQuerySecurely(device, request, parameters, callback)
}
override fun sendQuerySecurely(
device: ConnectedDevice,
request: ByteArray,
parameters: ByteArray?,
callback: QueryCallback,
) {
val featureId = featureId
if (featureId == null) {
loge("Attempted to send a query with no feature id.")
callback.onQueryFailedToSend(isTransient = false)
return
}
sendQuerySecurelyInternal(device, featureId, request, parameters, callback)
}
private fun sendQuerySecurelyInternal(
device: ConnectedDevice,
recipient: ParcelUuid,
request: ByteArray,
parameters: ByteArray?,
callback: QueryCallback,
) {
if (!isConnected) {
loge("Unable to send message, the platform is not actively connected.")
callback.onQueryFailedToSend(isTransient = true)
return
}
val featureId = featureId
if (featureId == null) {
loge("Attempted to send a query with no feature id.")
callback.onQueryFailedToSend(isTransient = false)
return
}
val id = queryIdGenerator.next()
val builder =
Query.newBuilder()
.setId(id)
.setSender(ByteString.copyFrom(ByteUtils.uuidToBytes(featureId.uuid)))
.setRequest(ByteString.copyFrom(request))
if (parameters != null) {
builder.parameters = ByteString.copyFrom(parameters)
}
logd("Sending secure query with id $id.")
val deviceMessage =
DeviceMessage.createOutgoingMessage(
recipient.uuid,
/* isMessageEncrypted= */ true,
DeviceMessage.OperationType.QUERY,
builder.build().toByteArray()
)
try {
sendMessageInternal(device, deviceMessage)
} catch (e: RemoteException) {
loge("Error while sending secure query.", e)
callback.onQueryFailedToSend(isTransient = false)
return
}
queryCallbacks[id] = callback
}
override fun respondToQuerySecurely(
device: ConnectedDevice,
queryId: Int,
success: Boolean,
response: ByteArray?,
) {
if (!isConnected) {
loge("Unable to send query response, the platform is not actively connected.")
return
}
val recipientId: ParcelUuid? = queryResponseRecipients.remove(queryId)
if (recipientId == null) {
loge("Unable to send response to unrecognized query $queryId.")
return
}
val builder = QueryResponse.newBuilder().setQueryId(queryId).setSuccess(success)
if (response != null) {
builder.response = ByteString.copyFrom(response)
}
val queryResponse = builder.build()
logd("Sending response to query $queryId to $recipientId.")
val deviceMessage =
DeviceMessage.createOutgoingMessage(
recipientId.uuid,
/* isMessageEncrypted= */ true,
DeviceMessage.OperationType.QUERY_RESPONSE,
queryResponse.toByteArray()
)
try {
sendMessageInternal(device, deviceMessage)
} catch (e: RemoteException) {
loge("Error while sending query response.", e)
}
}
override fun getConnectedDeviceById(deviceId: String): ConnectedDevice? {
if (!isConnected) {
loge("Unable to get connected device by id. The platform is not actively connected.")
return null
}
val connectedDevices =
try {
aliveFeatureCoordinator?.allConnectedDevices
} catch (e: RemoteException) {
loge("Exception while retrieving connected devices.", e)
null
}
return connectedDevices?.find { it.deviceId == deviceId }
}
override fun retrieveCompanionApplicationName(
device: ConnectedDevice,
callback: AppNameCallback,
) {
val systemQuery = SystemQuery.newBuilder().setType(SystemQueryType.APP_NAME).build()
sendQuerySecurelyInternal(
device,
SYSTEM_FEATURE_ID,
systemQuery.toByteArray(),
parameters = null,
object : QueryCallback {
override fun onSuccess(response: ByteArray?) {
if (response == null || response.isEmpty()) {
loge("Received a null or empty response for the application name.")
callback.onError()
return
}
val appName = String(response, StandardCharsets.UTF_8)
logd("Received successful app name query response of $appName.")
callback.onNameReceived(appName)
}
override fun onError(response: ByteArray?) {
loge("Received an error response when querying for application name.")
callback.onError()
}
override fun onQueryFailedToSend(isTransient: Boolean) {
loge("Failed to send the query for the application name.")
callback.onError()
}
}
)
}
override fun startAssociation(callback: IAssociationCallback) {
aliveFeatureCoordinator?.startAssociation(callback)
}
override fun startAssociation(identifier: ParcelUuid, callback: IAssociationCallback) {
aliveFeatureCoordinator?.startAssociationWithIdentifier(callback, identifier)
}
override fun stopAssociation() {
aliveFeatureCoordinator?.stopAssociation()
}
override fun acceptVerification() {
aliveFeatureCoordinator?.acceptVerification()
}
override fun removeAssociatedDevice(deviceId: String) {
aliveFeatureCoordinator?.removeAssociatedDevice(deviceId)
}
override fun enableAssociatedDeviceConnection(deviceId: String) {
aliveFeatureCoordinator?.enableAssociatedDeviceConnection(deviceId)
}
override fun disableAssociatedDeviceConnection(deviceId: String) {
aliveFeatureCoordinator?.disableAssociatedDeviceConnection(deviceId)
}
override fun retrieveAssociatedDevices(listener: IOnAssociatedDevicesRetrievedListener) {
aliveFeatureCoordinator?.retrieveAssociatedDevices(listener)
}
override fun retrieveAssociatedDevicesForDriver(listener: IOnAssociatedDevicesRetrievedListener) {
aliveFeatureCoordinator?.retrieveAssociatedDevicesForDriver(listener)
}
override fun retrieveAssociatedDevicesForPassengers(
listener: IOnAssociatedDevicesRetrievedListener,
) {
aliveFeatureCoordinator?.retrieveAssociatedDevicesForPassengers(listener)
?: listener.onAssociatedDevicesRetrieved(emptyList())
}
override fun claimAssociatedDevice(deviceId: String) {
aliveFeatureCoordinator?.claimAssociatedDevice(deviceId)
}
override fun removeAssociatedDeviceClaim(deviceId: String) {
aliveFeatureCoordinator?.removeAssociatedDeviceClaim(deviceId)
}
private fun sendMessageInternal(device: ConnectedDevice, message: DeviceMessage) {
aliveFeatureCoordinator?.sendMessage(device, message)
}
private fun onServiceConnected() {
if (!isConnected) {
logd(
"Initialization criteria have not been met yet. Waiting for further service connections " +
"before starting."
)
return
}
initializePlatform()
}
private fun handleFeatureCoordinatorConnection(service: IBinder) {
logd("Feature coordinator binder connected.")
featureCoordinator =
checkNotNull(IFeatureCoordinator.Stub.asInterface(service)) {
"Cannot create wrapper of a null feature coordinator."
}
logd("Feature coordinator is alive: $isConnected")
waitingForConnection.set(false)
onServiceConnected()
notifyFeatureCoordinatorListeners()
}
private fun handleForegroundUserConnection(service: IBinder) {
logd("Foreground user service connection connected.")
try {
val featureCoordinatorStatusNotifier =
IFeatureCoordinatorStatusNotifier.Stub.asInterface(service)
featureCoordinatorStatusNotifier.registerFeatureCoordinatorListener(
featureCoordinatorListener
)
this@CompanionConnector.featureCoordinatorStatusNotifier = featureCoordinatorStatusNotifier
} catch (e: SecurityException) {
loge("Incompatible binder, invoking failure callback.", e)
callback?.onFailedToConnect()
unbindFromService()
return
}
waitingForConnection.set(false)
onServiceConnected()
}
private fun initializePlatform() {
if (!isPlatformInitialized.compareAndSet(false, true)) {
logw("Platform is already initialized. Ignoring.")
return
}
logd("Initializing FeatureCoordinator.")
val featureCoordinator = featureCoordinator
if (featureCoordinator == null) {
logd("Unable to initialize null FeatureCoordinator. Ignoring.")
return
}
try {
val connectedDevices: List<ConnectedDevice>
when (userType) {
USER_TYPE_DRIVER -> {
featureCoordinator.registerDriverConnectionCallback(connectionCallback)
connectedDevices = featureCoordinator.connectedDevicesForDriver
}
USER_TYPE_PASSENGER -> {
featureCoordinator.registerPassengerConnectionCallback(connectionCallback)
connectedDevices = featureCoordinator.connectedDevicesForPassengers
}
USER_TYPE_ALL -> {
featureCoordinator.registerAllConnectionCallback(connectionCallback)
connectedDevices = featureCoordinator.allConnectedDevices
}
else -> {
loge("Unknown user type $userType detected.")
connectedDevices = emptyList()
}
}
featureCoordinator.registerDeviceAssociationCallback(deviceAssociationCallback)
featureCoordinator.registerOnLogRequestedListener(loggerId, logRequestedListener)
val featureId = featureId
for (device in connectedDevices) {
callback?.onDeviceConnected(device)
if (device.hasSecureChannel()) {
callback?.onSecureChannelEstablished(device)
}
if (featureId == null) {
logd("There is currently no feature id set. Skipping device registration.")
continue
}
featureCoordinator.registerDeviceCallback(device, featureId, deviceCallback)
}
} catch (e: RemoteException) {
loge("Error while initializing FeatureCoordinator.", e)
}
callback?.onConnected()
}
private fun onServiceDisconnected() {
logd("Service has disconnected. Cleaning up.")
disconnect()
}
private fun onNullBinding() {
loge("Received a null binding for FeatureCoordinator. Unbinding service.")
unbindFromService()
callback?.onFailedToConnect()
}
private fun onBindingDied() {
logw("FeatureCoordinator binding died. Unbinding service.")
unbindFromService()
callback?.onDisconnected()
}
private fun resolveIntent(action: String): Intent? {
val packageManager = context.packageManager
val intent = Intent(action)
val services = packageManager.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY)
if (services.isEmpty()) {
logw("There are no services supporting the $action action installed on this device.")
return null
}
logd("Found ${services.size} service(s) supporting $action. Choosing the first one.")
val service = services[0]
return intent.apply {
component = ComponentName(service.serviceInfo.packageName, service.serviceInfo.name)
}
}
private fun processIncomingMessage(device: ConnectedDevice, deviceMessage: DeviceMessage) {
val operationType = deviceMessage.operationType
val message = deviceMessage.message
when (operationType) {
DeviceMessage.OperationType.CLIENT_MESSAGE -> {
logd("Received client message. Passing on to feature.")
callback?.onMessageReceived(device, message)
return
}
DeviceMessage.OperationType.QUERY -> {
try {
val query = Query.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry())
processQuery(device, query)
} catch (e: InvalidProtocolBufferException) {
loge("Unable to parse query.", e)
}
return
}
DeviceMessage.OperationType.QUERY_RESPONSE -> {
try {
val response = QueryResponse.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry())
processQueryResponse(response)
} catch (e: InvalidProtocolBufferException) {
loge("Unable to parse query response.", e)
}
return
}
else -> loge("Received unknown type of message: $operationType. Ignoring.")
}
}
private fun processQuery(device: ConnectedDevice, query: Query) {
logd("Received a new query with id ${query.id}. Passing on to feature.")
val sender = ParcelUuid(ByteUtils.bytesToUUID(query.sender.toByteArray()))
queryResponseRecipients[query.id] = sender
callback?.onQueryReceived(
device,
query.id,
query.request.toByteArray(),
query.parameters.toByteArray()
)
}
private fun processQueryResponse(response: QueryResponse) {
logd("Received a query response. Issuing registered callback.")
val callback = queryCallbacks.remove(response.queryId)
if (callback == null) {
loge("Unable to locate callback for query ${response.queryId}.")
return
}
if (response.success) {
callback.onSuccess(response.response.toByteArray())
} else {
callback.onError(response.response.toByteArray())
}
}
private fun notifyFeatureCoordinatorListeners() {
logd("Feature coordinator has been initiated, notifying listeners.")
scrubDeadListeners()
lock.withLock {
for (listener in registeredListeners) {
listener.onFeatureCoordinatorInitialized(aliveFeatureCoordinator)
}
}
}
private fun scrubDeadListeners() {
logd("Remove disconnected registered feature coordinator listeners")
lock.withLock { registeredListeners.removeIf { !it.asBinder().isBinderAlive } }
}
private fun logd(message: String) {
SafeLog.logd(TAG, "$message [Feature ID: $featureId]")
}
private fun logw(message: String) {
SafeLog.logw(TAG, "$message [Feature ID: $featureId]")
}
private fun loge(message: String, e: Exception? = null) {
SafeLog.loge(TAG, "$message [Feature ID: $featureId]", e)
}
companion object {
private const val TAG = "CompanionConnector"
private val BIND_RETRY_DURATION = Duration.ofSeconds(1)
@VisibleForTesting internal const val MAX_BIND_ATTEMPTS = 3
@JvmStatic
/** Create a [CompanionConnector] instance with a [featureCoordinator] already populated. */
fun createLocalConnector(
context: Context,
userType: @UserType Int,
featureCoordinator: IFeatureCoordinator,
): CompanionConnector =
CompanionConnector(context, userType = userType).apply {
this.featureCoordinator = featureCoordinator
}
/** A generator of unique IDs for queries. */
private class QueryIdGenerator {
private val messageId = AtomicInteger(0)
fun next(): Int {
val current = messageId.getAndIncrement()
messageId.compareAndSet(Int.MAX_VALUE, 0)
return current
}
}
}
}