1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.core.telecom.extensions 18 19 import android.content.Context 20 import android.net.Uri 21 import android.os.Build 22 import android.os.Build.VERSION 23 import android.os.Bundle 24 import android.os.Handler 25 import android.os.IBinder 26 import android.os.Looper 27 import android.telecom.Call 28 import android.telecom.Call.Callback 29 import android.telecom.InCallService 30 import android.telecom.PhoneAccount 31 import android.telecom.PhoneAccountHandle 32 import android.telecom.TelecomManager 33 import android.util.Log 34 import androidx.annotation.IntDef 35 import androidx.annotation.RequiresApi 36 import androidx.annotation.VisibleForTesting 37 import androidx.core.telecom.CallsManager 38 import androidx.core.telecom.extensions.CallExtensionScopeImpl.Companion.CAPABILITY_EXCHANGE 39 import androidx.core.telecom.extensions.CallExtensionScopeImpl.Companion.CAPABILITY_EXCHANGE_TIMEOUT_MS 40 import androidx.core.telecom.extensions.CallExtensionScopeImpl.Companion.EXTRAS 41 import androidx.core.telecom.extensions.CallExtensionScopeImpl.Companion.NONE 42 import androidx.core.telecom.extensions.CallExtensionScopeImpl.Companion.UNKNOWN 43 import androidx.core.telecom.extensions.ExtrasCallExtensionProcessor.Companion.EXTRA_VOIP_API_VERSION 44 import androidx.core.telecom.internal.CapabilityExchangeListenerRemote 45 import androidx.core.telecom.internal.Compatibility 46 import androidx.core.telecom.internal.utils.Utils 47 import androidx.core.telecom.util.ExperimentalAppActions 48 import java.util.Collections 49 import kotlin.coroutines.resume 50 import kotlin.math.min 51 import kotlinx.coroutines.CoroutineScope 52 import kotlinx.coroutines.Dispatchers 53 import kotlinx.coroutines.async 54 import kotlinx.coroutines.cancel 55 import kotlinx.coroutines.channels.awaitClose 56 import kotlinx.coroutines.channels.trySendBlocking 57 import kotlinx.coroutines.coroutineScope 58 import kotlinx.coroutines.flow.Flow 59 import kotlinx.coroutines.flow.callbackFlow 60 import kotlinx.coroutines.flow.first 61 import kotlinx.coroutines.suspendCancellableCoroutine 62 import kotlinx.coroutines.withTimeoutOrNull 63 64 /** 65 * Encapsulates the [extensionCapability] associated with a call extension and the 66 * [onExchangeComplete], which is called when capability exchange has completed and the extension 67 * should be initialized. 68 */ 69 @OptIn(ExperimentalAppActions::class) 70 internal data class CallExtensionCreator( 71 val extensionCapability: Capability, 72 val onExchangeComplete: suspend (Capability?, CapabilityExchangeListenerRemote?) -> Unit 73 ) 74 75 /** 76 * Represents the result of performing capability exchange with the underlying VOIP application. 77 * Contains the capabilities that the VOIP app supports and the remote binder implementation used to 78 * communicate with the remote process. 79 */ 80 @OptIn(ExperimentalAppActions::class) 81 internal data class CapabilityExchangeResult( 82 val voipCapabilities: Set<Capability>, 83 val extensionInitializationBinder: CapabilityExchangeListenerRemote 84 ) 85 86 /** 87 * The scope enclosing an ongoing call that allows the user to set up optional extensions to the 88 * call. 89 * 90 * Once the call is connected to the remote, [onConnected] will be called and the [Call] is ready to 91 * be used with extensions: 92 * ``` 93 * connectExtensions(context, call) { 94 * // initialize extensions 95 * onConnected { call -> 96 * // call extensions are ready to be used along with the traditional call APIs 97 * } 98 * } 99 * ``` 100 */ 101 @OptIn(ExperimentalAppActions::class) 102 @RequiresApi(Build.VERSION_CODES.O) 103 internal class CallExtensionScopeImpl( 104 private val applicationContext: Context, 105 private val callScope: CoroutineScope, 106 private val call: Call 107 ) : CallExtensionScope { 108 companion object { 109 internal const val TAG = "CallExtensions" 110 111 internal const val CAPABILITY_EXCHANGE_VERSION = 1 112 internal const val RESOLVE_EXTENSIONS_TYPE_TIMEOUT_MS = 1000L 113 internal const val CALL_READY_TIMEOUT_MS = 500L 114 internal const val CAPABILITY_EXCHANGE_TIMEOUT_MS = 1000L 115 116 /** Constants used to denote the extension level supported by the VOIP app. */ 117 @Retention(AnnotationRetention.SOURCE) 118 @IntDef(NONE, EXTRAS, CAPABILITY_EXCHANGE, UNKNOWN) 119 internal annotation class CapabilityExchangeType 120 121 internal const val NONE = 0 122 internal const val EXTRAS = 1 123 internal const val CAPABILITY_EXCHANGE = 2 124 internal const val UNKNOWN = 3 125 } 126 127 private var delegate: (suspend (Call) -> Unit)? = null 128 // Creates a Set of creators that will be used to create and maintain the extension connection 129 // with the remote VOIP application. 130 // This has to be done this way because actions are set AFTER the extension is registered, so we 131 // need to query the Capability after CallExtensionScope initialization has completed. 132 private val callExtensionCreators = HashSet<() -> CallExtensionCreator>() 133 134 override fun onConnected(block: suspend (Call) -> Unit) { 135 delegate = block 136 } 137 138 override fun addParticipantExtension( 139 onActiveParticipantChanged: suspend (Participant?) -> Unit, 140 onParticipantsUpdated: suspend (Set<Participant>) -> Unit 141 ): ParticipantExtensionRemoteImpl { 142 val extension = 143 ParticipantExtensionRemoteImpl( 144 callScope, 145 onActiveParticipantChanged, 146 onParticipantsUpdated 147 ) 148 registerExtension { 149 CallExtensionCreator( 150 extensionCapability = 151 Capability().apply { 152 featureId = Extensions.PARTICIPANT 153 featureVersion = ParticipantExtensionImpl.VERSION 154 supportedActions = extension.actions 155 }, 156 onExchangeComplete = extension::onExchangeComplete 157 ) 158 } 159 return extension 160 } 161 162 override fun addMeetingSummaryExtension( 163 onCurrentSpeakerChanged: suspend (CharSequence?) -> Unit, 164 onParticipantCountChanged: suspend (Int) -> Unit 165 ): MeetingSummaryRemote { 166 val extension = 167 MeetingSummaryRemoteImpl(callScope, onCurrentSpeakerChanged, onParticipantCountChanged) 168 registerExtension { 169 CallExtensionCreator( 170 extensionCapability = 171 Capability().apply { 172 featureId = Extensions.MEETING_SUMMARY 173 featureVersion = ParticipantExtensionImpl.MEETING_SUMMARY_VERSION 174 supportedActions = extension.actions 175 }, 176 onExchangeComplete = extension::onExchangeComplete 177 ) 178 } 179 return extension 180 } 181 182 override fun addLocalCallSilenceExtension( 183 onIsLocallySilencedUpdated: suspend (Boolean) -> Unit 184 ): LocalCallSilenceExtensionRemoteImpl { 185 val extension = LocalCallSilenceExtensionRemoteImpl(callScope, onIsLocallySilencedUpdated) 186 registerExtension { 187 CallExtensionCreator( 188 extensionCapability = 189 Capability().apply { 190 featureId = Extensions.LOCAL_CALL_SILENCE 191 featureVersion = LocalCallSilenceExtensionImpl.VERSION 192 supportedActions = extension.actions 193 }, 194 onExchangeComplete = extension::onExchangeComplete 195 ) 196 } 197 return extension 198 } 199 200 /** 201 * Adds support for a call icon extension, allowing custom UI and actions to be integrated into 202 * the call screen. 203 * 204 * This function creates and registers a [CallIconExtensionRemoteImpl] instance, configuring its 205 * capabilities and setting up a callback for when the initial communication exchange with the 206 * system is complete. The extension's capabilities are defined, including its feature ID, 207 * version, and supported actions. 208 * 209 * @return The configured [CallIconExtensionRemoteImpl] instance, representing the call icon 210 * extension. 211 */ 212 override fun addCallIconSupport( 213 onCallIconChanged: suspend (Uri) -> Unit 214 ): CallIconExtensionRemote { 215 val extension = 216 CallIconExtensionRemoteImpl(applicationContext, callScope, onCallIconChanged) 217 registerExtension { 218 CallExtensionCreator( 219 extensionCapability = 220 Capability().apply { 221 featureId = Extensions.CALL_ICON 222 featureVersion = CallIconExtensionImpl.VERSION 223 supportedActions = extension.actions 224 }, 225 onExchangeComplete = extension::onExchangeComplete 226 ) 227 } 228 return extension 229 } 230 231 /** 232 * Register an extension with this call, whose capability will be negotiated with the VOIP 233 * application. 234 * 235 * Once capability exchange completes, the shared [Capability.featureId] will be used to map the 236 * negotiated capability with this extension and [receiver] will be called with a valid 237 * negotiated [Capability] and interface to use to create/manage this extension with the remote. 238 * 239 * @param receiver The receiver that will be called once capability exchange completes and we 240 * either have a valid negotiated capability or a `null` Capability if the remote side does 241 * not support this capability. 242 */ 243 internal fun registerExtension(receiver: () -> CallExtensionCreator) { 244 callExtensionCreators.add(receiver) 245 } 246 247 /** 248 * Invoke the stored [onConnected] block once capability exchange has completed and the 249 * associated extensions have been set up. 250 */ 251 private suspend fun invokeDelegate() { 252 Log.i(TAG, "invokeDelegate") 253 delegate?.invoke(call) 254 } 255 256 /** 257 * Internal helper used to help resolve the call extension type. This is invoked before 258 * capability exchange between the [InCallService] and VOIP app starts to ensure the necessary 259 * features are enabled to support it. 260 * 261 * If the call is placed using the V1.5 ConnectionService + Extensions Library (Auto Case), the 262 * call will have the [EXTRA_VOIP_API_VERSION] defined in the extras. The call extension would 263 * be resolved as [EXTRAS]. 264 * 265 * If the call is using the v2 APIs and the phone account associated with the call supports 266 * transactional ops (U+) or the call has the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property 267 * defined (on V devices), then the extension type is [CAPABILITY_EXCHANGE]. 268 * 269 * If the call is added via [CallsManager.addCall] on pre-U devices and the 270 * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] is present in the call extras, 271 * the extension type also resolves to [CAPABILITY_EXCHANGE]. 272 * 273 * In the case that none of the cases above apply and the phone account is found not to support 274 * transactional ops (assumes that caller has [android.Manifest.permission.READ_PHONE_NUMBERS] 275 * permission), then the extension type is [NONE]. 276 * 277 * If the caller does not have the required permission to retrieve the phone account, then the 278 * extension type will be [UNKNOWN], until it can be resolved. 279 * 280 * @return the extension type [CapabilityExchangeType] resolved for the call. 281 */ 282 @VisibleForTesting 283 internal suspend fun resolveCallExtensionsType(): Int { 284 var details = call.details 285 var type = NONE 286 if (Utils.hasPlatformV2Apis()) { 287 // Android CallsManager V+ check 288 if (details.hasProperty(CallsManager.PROPERTY_IS_TRANSACTIONAL)) { 289 Log.d(TAG, "resolveCallExtensionsType: PROPERTY_IS_TRANSACTIONAL present") 290 return CAPABILITY_EXCHANGE 291 } 292 // Android CallsManager U check 293 val acct = getPhoneAccountIfAllowed(details.accountHandle) 294 if (acct == null) { 295 Log.d(TAG, "resolveCallExtensionsType: Unable to resolve PA") 296 type = UNKNOWN 297 } else if ( 298 acct.hasCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS) 299 ) { 300 Log.d(TAG, "resolveCallExtensionsType: PA supports transactional API") 301 return CAPABILITY_EXCHANGE 302 } 303 } 304 // The extras may come in after the call is first signalled to InCallService - wait for the 305 // details to be populated with extras. 306 details = 307 withTimeoutOrNull(RESOLVE_EXTENSIONS_TYPE_TIMEOUT_MS) { 308 detailsFlow().first { details -> 309 details.extras != null && 310 !details.extras.isEmpty() && 311 // We do not want to get extras in the CONNECTING state because the remote 312 // extras from the VOIP app have not been populated yet. 313 Compatibility.getCallState(call) != Call.STATE_CONNECTING 314 } 315 // return initial details if no updates come in before the timeout 316 } ?: call.details 317 val callExtras = details.extras ?: Bundle() 318 // Extras based impl check 319 if (callExtras.containsKey(EXTRA_VOIP_API_VERSION)) { 320 return EXTRAS 321 } 322 // CS based impl check 323 if (callExtras.containsKey(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED)) { 324 return CAPABILITY_EXCHANGE 325 } 326 Log.i( 327 TAG, 328 "resolveCallExtensionsType: Unable to resolve call extension type. " + 329 "Returning $type." 330 ) 331 return type 332 } 333 334 private suspend fun getPhoneAccountIfAllowed(handle: PhoneAccountHandle): PhoneAccount? = 335 coroutineScope { 336 val telecomManager = 337 applicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager 338 async(Dispatchers.IO) { 339 try { 340 telecomManager.getPhoneAccount(handle) 341 } catch (e: SecurityException) { 342 Log.i( 343 TAG, 344 "getPhoneAccountIfAllowed: Unable to resolve call extension " + 345 "type due to lack of permission." 346 ) 347 null 348 } 349 } 350 .await() 351 } 352 353 /** Perform the operation to connect the extensions to the call. */ 354 internal suspend fun connectExtensionSession() { 355 val type = resolveCallExtensionsType() 356 Log.d(TAG, "connectExtensionsSession: type=$type") 357 358 val extensions: CapabilityExchangeResult? = 359 runCatching { 360 when (type) { 361 EXTRAS -> { 362 val extrasProcessor = ExtrasCallExtensionProcessor(callScope, call) 363 extrasProcessor.handleExtrasExtensionsFromVoipApp(detailsFlow()) 364 } 365 CAPABILITY_EXCHANGE, 366 UNKNOWN -> performExchangeWithRemote() 367 else -> { 368 Log.w( 369 TAG, 370 "connectExtensions: unexpected type: $type." + 371 " Proceeding with no extension support" 372 ) 373 null 374 } 375 } 376 } 377 .getOrNull() 378 379 try { 380 extensions?.let { 381 initializeExtensions(it) 382 invokeDelegate() 383 waitForDestroy(it) 384 } 385 } finally { 386 Log.i(TAG, "setupExtensionSession: scope closing, calling onRemoveExtensions") 387 callScope.cancel() 388 extensions?.extensionInitializationBinder?.onRemoveExtensions() 389 } 390 } 391 392 /** 393 * Register the extensions interface with the remote application and get the result. 394 * 395 * If negotiation takes longer than [CAPABILITY_EXCHANGE_TIMEOUT_MS], we will assume the remote 396 * does not support extensions at all. 397 */ 398 private suspend fun performExchangeWithRemote(): CapabilityExchangeResult? { 399 if (Utils.hasPlatformV2Apis()) { 400 Log.d(TAG, "performExchangeWithRemote: waiting for call ready signal...") 401 withTimeoutOrNull(CALL_READY_TIMEOUT_MS) { 402 // On Android U/V, we must wait for the jetpack lib to send a call ready event to 403 // prevent a race between telecom setting the TransactionalServiceWrapper and 404 // sending the CAPABILITY_EXCHANGE event 405 waitForCallReady() 406 } 407 } 408 Log.d(TAG, "performExchangeWithRemote: requesting extensions from remote") 409 val extensions = 410 withTimeoutOrNull(CAPABILITY_EXCHANGE_TIMEOUT_MS) { registerWithRemoteService() } 411 if (extensions == null) { 412 Log.w(TAG, "performExchangeWithRemote: never received response") 413 } 414 return extensions 415 } 416 417 /** Wait for the Call to receive [CallsManager.EVENT_CALL_READY] from the call producer. */ 418 private suspend fun waitForCallReady() = suspendCancellableCoroutine { continuation -> 419 val callback = 420 object : Callback() { 421 override fun onConnectionEvent(call: Call?, event: String?, extras: Bundle?) { 422 if (call == null || event == null) return 423 if (event == CallsManager.EVENT_CALL_READY) { 424 continuation.resume(Unit) 425 } 426 } 427 } 428 call.registerCallback(callback, Handler(Looper.getMainLooper())) 429 continuation.invokeOnCancellation { call.unregisterCallback(callback) } 430 } 431 432 /** 433 * Initialize all extensions that were registered with [registerExtension] and provide the 434 * negotiated capability or null if the remote doesn't support this extension. 435 */ 436 private suspend fun initializeExtensions(extensions: CapabilityExchangeResult?) { 437 Log.i(TAG, "initializeExtensions: Initializing extensions...") 438 val allRegisteredRemoteExtensions = callExtensionCreators.map { it() } 439 440 if (extensions == null) { 441 allRegisteredRemoteExtensions.forEach { it.onExchangeComplete(null, null) } 442 return // Early return 443 } 444 445 allRegisteredRemoteExtensions.forEach { remoteExtensionImpl -> 446 Log.d( 447 TAG, 448 "initializeExtensions: capability=${remoteExtensionImpl.extensionCapability}" 449 ) 450 val capability = 451 extensions.voipCapabilities.firstOrNull { 452 it.featureId == remoteExtensionImpl.extensionCapability.featureId 453 } 454 if (capability == null) { 455 Log.d(TAG, "initializeExtensions: no VOIP capability, skipping...") 456 remoteExtensionImpl.onExchangeComplete.invoke(null, null) 457 return@forEach // Continue to the next iteration 458 } 459 460 val negotiatedCap = 461 calculateNegotiatedCapability(remoteExtensionImpl.extensionCapability, capability) 462 Log.d(TAG, "initializeExtensions: negotiated cap=$negotiatedCap") 463 remoteExtensionImpl.onExchangeComplete.invoke( 464 negotiatedCap, 465 extensions.extensionInitializationBinder 466 ) 467 } 468 } 469 470 /** 471 * Initiate capability exchange via a call event and wait for the response from the calling 472 * application using the Binder passed to the remote service. 473 * 474 * @return the remote capabilities and Binder interface used to communicate with the remote 475 */ 476 private suspend fun registerWithRemoteService(): CapabilityExchangeResult? = 477 suspendCancellableCoroutine { continuation -> 478 val binder = 479 object : ICapabilityExchange.Stub() { 480 override fun beginExchange( 481 capabilities: MutableList<Capability>?, 482 l: ICapabilityExchangeListener? 483 ) { 484 Log.d( 485 TAG, 486 "registerWithRemoteService: received remote result," + 487 " caps=$capabilities, listener is null=${l == null}" 488 ) 489 continuation.resume( 490 l?.let { 491 CapabilityExchangeResult( 492 capabilities?.toSet() ?: Collections.emptySet(), 493 CapabilityExchangeListenerRemote(l) 494 ) 495 } 496 ) 497 } 498 } 499 Log.d(TAG, "registerWithRemoteService: sending event:") 500 val extras = setExtras(binder) 501 call.sendCallEvent(Extensions.EVENT_JETPACK_CAPABILITY_EXCHANGE, extras) 502 } 503 504 /** 505 * @return the negotiated capability by finding the highest version and actions supported by 506 * both the local and remote interfaces. 507 */ 508 @ExperimentalAppActions 509 private fun calculateNegotiatedCapability( 510 localCapability: Capability, 511 remoteCapability: Capability 512 ): Capability { 513 return Capability().apply { 514 featureId = localCapability.featureId 515 featureVersion = min(localCapability.featureVersion, remoteCapability.featureVersion) 516 supportedActions = 517 localCapability.supportedActions 518 .intersect(remoteCapability.supportedActions.toSet()) 519 .toIntArray() 520 } 521 } 522 523 /** 524 * @return a [Bundle] that contains the binder and version used by the remote to respond to a 525 * capability exchange request. 526 */ 527 @RequiresApi(Build.VERSION_CODES.O) 528 private fun setExtras(binder: IBinder): Bundle { 529 return Bundle().apply { 530 putBinder(Extensions.EXTRA_CAPABILITY_EXCHANGE_BINDER, binder) 531 putInt(Extensions.EXTRA_CAPABILITY_EXCHANGE_VERSION, CAPABILITY_EXCHANGE_VERSION) 532 } 533 } 534 535 /** Create a flow that reports changes to [Call.Details] provided by the [Call.Callback]. */ 536 private fun detailsFlow(): Flow<Call.Details> = callbackFlow { 537 val callback = 538 object : Callback() { 539 override fun onDetailsChanged(call: Call?, details: Call.Details?) { 540 details?.also { trySendBlocking(it) } 541 } 542 } 543 // send the current state first since registering for the callback doesn't deliver the 544 // current value. 545 trySendBlocking(call.details) 546 call.registerCallback(callback, Handler(Looper.getMainLooper())) 547 awaitClose { call.unregisterCallback(callback) } 548 } 549 550 /** Wait for the call to be destroyed or the remote process to be killed. */ 551 private suspend fun waitForDestroy(cer: CapabilityExchangeResult?) = 552 suspendCancellableCoroutine { continuation -> 553 val callback = 554 object : Callback() { 555 override fun onCallDestroyed(targetCall: Call?) { 556 if (targetCall == null || call != targetCall || continuation.isCompleted) 557 return 558 continuation.resume(Unit) 559 } 560 } 561 cer?.extensionInitializationBinder 562 ?.asBinder() 563 ?.linkToDeath( 564 { 565 Log.w(TAG, "waitForDestroy: binderDied called, cleaning up") 566 continuation.resume(Unit) 567 }, 568 0 /* flags */ 569 ) 570 if (Api26Impl.getCallState(call) != Call.STATE_DISCONNECTED) { 571 call.registerCallback(callback, Handler(Looper.getMainLooper())) 572 continuation.invokeOnCancellation { call.unregisterCallback(callback) } 573 } else { 574 continuation.resume(Unit) 575 } 576 } 577 } 578 579 /** Ensure compatibility for [Call] APIs back to API level 26 */ 580 @RequiresApi(Build.VERSION_CODES.O) 581 private object Api26Impl { 582 @Suppress("DEPRECATION") 583 @JvmStatic getCallStatenull584 fun getCallState(call: Call): Int { 585 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 586 Api31Impl.getCallState(call) 587 } else { 588 call.state 589 } 590 } 591 } 592 593 /** Ensure compatibility for [Call] APIs for API level 31+ */ 594 @RequiresApi(Build.VERSION_CODES.S) 595 private object Api31Impl { 596 @JvmStatic getCallStatenull597 fun getCallState(call: Call): Int { 598 return call.details.state 599 } 600 } 601