[go: nahoru, domu]

blob: c9ed978002284448d069e58608136e845e223023 [file] [log] [blame]
/*
* Copyright 2023 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 androidx.core.telecom.internal
import android.os.Build
import android.os.Bundle
import android.telecom.Call
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.telecom.CallsManager
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.CapabilityExchange
import androidx.core.telecom.extensions.ParticipantClientActions
import androidx.core.telecom.extensions.ParticipantClientActionsImpl
import androidx.core.telecom.internal.utils.CapabilityExchangeUtils
import androidx.core.telecom.util.ExperimentalAppActions
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.withTimeout
@ExperimentalAppActions
@RequiresApi(Build.VERSION_CODES.O)
internal class CallCompat(
private val call: Call,
val scope: CoroutineScope,
) {
internal val icsCapabilities = mutableListOf<Capability>()
@VisibleForTesting
internal var capExchangeSetupComplete = false
internal lateinit var onParticipantInitializationComplete: (ParticipantClientActions) -> Unit
internal lateinit var participantStateListener: ParticipantClientActionsImpl
companion object {
/**
* Current capability exchange version
*/
internal const val CAPABILITY_EXCHANGE_VERSION = 1
private val TAG = CallCompat::class.simpleName
fun toCallCompat(call: Call, scope: CoroutineScope, init: CallCompat.() -> Unit):
CallCompat {
Log.i(TAG, "toCallCompat; call = $call")
val callCompat = CallCompat(call, scope)
callCompat.init()
return callCompat
}
}
fun toCall(): Call {
return call
}
internal fun getParticipantClientActions(): Result<ParticipantClientActions> {
Log.i(TAG, "getParticipantClientActions")
return if (this::participantStateListener.isInitialized) {
if (participantStateListener.mIsInitializationComplete) {
Result.success(participantStateListener)
} else {
Result.failure(IllegalAccessException("ParticipantClientActions not setup yet."))
}
} else {
Result.failure(IllegalAccessException("The participantStateListener field in " +
"CallCompat was not initialized."))
}
}
internal fun addCapability(capability: Capability) {
Log.i(TAG, "addCapability capability = $capability")
// This is called by extensions to include their capabilities to the call.
icsCapabilities.add(capability)
}
internal fun addExtension(onInitializationComplete: (pca: ParticipantClientActions) -> Unit) {
Log.i(TAG, "addExtension")
onParticipantInitializationComplete = onInitializationComplete
}
/**
* Initiate capability exchange negotiation between ICS and VOIP app. The acknowledgement begins
* when the ICS sends a call event with [CallsManager.EVENT_JETPACK_CAPABILITY_EXCHANGE] to
* notify the VOIP app to begin capability exchange negotiation. At that point, 3 stages of
* acknowledgement are required between the two parties in order for negotiation to succeed.
*
* This entails the ICS side waiting for the VOIP app to communicate its supported capabilities,
* the VOIP side waiting for the ICS side to communicate its supported capabilities, and the
* VOIP side signaling the ICS side that feature setup (negotiation) is complete. If any one of
* the aforementioned stages of ACK fails (i.e. timeout), the negotiation will fail.
*
* Note: Negotiation is only supported by InCallServices that support capability exchange
* ([InCallServiceCompat.CAPABILITY_EXCHANGE]).
*
* @return the capability negotiation status.
* between the ICS and VOIP app.
*/
internal suspend fun startCapabilityExchange() {
Log.i(TAG, "startCapabilityExchange: Starting capability negotiation with VOIP app...")
// Initialize binder for facilitating IPC (capability exchange) between ICS and VOIP app
// and notify VOIP app via a call event.
val capExchange = CapabilityExchange()
val extras = Bundle()
extras.putBinder(CallsManager.EXTRA_CAPABILITY_EXCHANGE_BINDER, capExchange)
extras.putInt(
CallsManager.EXTRA_CAPABILITY_EXCHANGE_VERSION,
CAPABILITY_EXCHANGE_VERSION
)
call.sendCallEvent(CallsManager.EVENT_JETPACK_CAPABILITY_EXCHANGE, extras)
// Launch a new coroutine from the context of the current coroutine and wait for task to
// complete.
scope.async {
beginCapabilityNegotiationAck(capExchange)
}.await()
}
/**
* Helper to start acknowledgement process for capability negotiation between the ICS and VOIP
* app.
*/
private suspend fun beginCapabilityNegotiationAck(capExchange: CapabilityExchange) {
Log.i(TAG, "beginCapabilityNegotiationAck")
try {
withTimeout(CapabilityExchangeUtils.CAPABILITY_NEGOTIATION_COROUTINE_TIMEOUT) {
// Wait for VOIP app to return its supported capabilities.
if (capExchange.beginExchangeLatch.await(
CapabilityExchangeUtils.CAPABILITY_EXCHANGE_TIMEOUT,
TimeUnit.MILLISECONDS)) {
Log.i(TAG, "beginCapabilityNegotiationAck beginExchange returned from " +
"the VOIP side.")
setupSupportedCapabilities(capExchange)
Log.i(TAG, "beginCapabilityNegotiationAck: " +
"Completed capability exchange feature set up.")
capExchangeSetupComplete = true
}
}
} catch (e: Exception) {
when (e) {
is CancellationException -> {
Log.i(
TAG, "beginCapabilityNegotiationAck: Capability negotiation job " +
"timed out in ICS side."
)
completeParticipantCapExchangeUnsupported()
// Todo: complete other extensions exceptionally
}
else -> {
// Handle the case where the VOIP app dies:
Log.i(
TAG, "beginCapabilityNegotiationAck: Remote party threw exception = $e"
)
completeParticipantCapExchangeUnsupported()
// Todo: complete other extensions exceptionally
}
}
}
}
/***********************************************************************************************
* Helpers
*********************************************************************************************/
internal fun setupSupportedCapabilities(capExchange: CapabilityExchange) {
val voipCaps: Set<Capability> = capExchange.voipCapabilities.toSet()
for (icsCap in icsCapabilities) {
// Check if the VoIP app supports this capability:
val voipCap: Capability? = voipCaps.find {
it.featureId == icsCap.featureId
}
// If so, then initialize the listener and send the relevant callback:
if (voipCap != null) {
val negotiatedActions = icsCap.supportedActions
.intersect(voipCap.supportedActions.toSet())
val minExtVersion = min(icsCap.featureVersion,
voipCap.featureVersion)
when (icsCap.featureId) {
CallsManager.PARTICIPANT -> initializeParticipantListenerAndInformVoipApp(
negotiatedActions, minExtVersion, capExchange)
CallsManager.CALL_ICON -> initializeCallIconListenerAndInformVoipApp(
negotiatedActions, minExtVersion, capExchange)
}
} else {
when (icsCap.featureId) {
CallsManager.PARTICIPANT -> completeParticipantCapExchangeUnsupported()
CallsManager.CALL_ICON -> completeCallIconCapExchangeUnsupported()
}
}
}
}
private fun initializeParticipantListenerAndInformVoipApp(
negotiatedParticipantActions: Set<Int>,
minVersion: Int,
capExchange: CapabilityExchange
) {
participantStateListener = ParticipantClientActionsImpl(scope, negotiatedParticipantActions,
onParticipantInitializationComplete)
capExchange.capabilityExchangeListener.onCreateParticipantExtension(
minVersion,
negotiatedParticipantActions.toIntArray(),
participantStateListener)
}
private fun initializeCallIconListenerAndInformVoipApp(
negotiatedCallIconActions: Set<Int>,
minVersion: Int,
capExchange: CapabilityExchange
) {
Log.i(TAG, "initializeCallIconListenerAndInformVoipApp: size of negotiatedActions" +
" = ${negotiatedCallIconActions.size}, version = $minVersion, " +
"capExchange = $capExchange")
// Todo: initialize ICallDetailsListener and send onCreateCallDetailsExtension.
}
private fun completeParticipantCapExchangeUnsupported() {
// complete the call cap exchange exceptionally and let Telecom take care of the cleanup:
participantStateListener = ParticipantClientActionsImpl(scope, emptySet()) {}
participantStateListener.mIsParticipantExtensionSupported = false
onParticipantInitializationComplete(participantStateListener)
}
private fun completeCallIconCapExchangeUnsupported() {
Log.i(TAG, "completeCallIconCapExchangeUnsupported")
// Todo: inform the ICS app that voip doesn't support CallDetails extension
}
}