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