CallSession.kt

/*
 * 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.Bundle
import android.os.OutcomeReceiver
import android.os.ParcelUuid
import android.telecom.CallException
import android.telecom.DisconnectCause
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallControlResult
import androidx.core.telecom.CallControlScope
import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.CallsManager
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.voip.VoipExtensionManager
import androidx.core.telecom.internal.utils.EndpointUtils
import androidx.core.telecom.util.ExperimentalAppActions
import java.util.function.Consumer
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

@RequiresApi(34)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@Suppress("ClassVerificationFailure")
internal class CallSession(
    coroutineContext: CoroutineContext,
    val onAnswerCallback: suspend (callType: Int) -> Unit,
    val onDisconnectCallback: suspend (disconnectCause: DisconnectCause) -> Unit,
    val onSetActiveCallback: suspend () -> Unit,
    val onSetInactiveCallback: suspend () -> Unit,
    private val blockingSessionExecution: CompletableDeferred<Unit>
) {
    private val mCoroutineContext = coroutineContext
    private var mPlatformInterface: android.telecom.CallControl? = null

    class CallControlCallbackImpl(private val callSession: CallSession) :
        android.telecom.CallControlCallback {
        override fun onSetActive(wasCompleted: Consumer<Boolean>) {
            callSession.onSetActive(wasCompleted)
        }

        override fun onSetInactive(wasCompleted: Consumer<Boolean>) {
            callSession.onSetInactive(wasCompleted)
        }

        override fun onAnswer(videoState: Int, wasCompleted: Consumer<Boolean>) {
            callSession.onAnswer(videoState, wasCompleted)
        }

        override fun onDisconnect(
            disconnectCause: DisconnectCause,
            wasCompleted: Consumer<Boolean>
        ) {
            callSession.onDisconnect(disconnectCause, wasCompleted)
        }

        override fun onCallStreamingStarted(wasCompleted: Consumer<Boolean>) {
            TODO("Implement with the CallStreaming code")
        }
    }

    class CallEventCallbackImpl(
        private val callChannels: CallChannels,
        private val coroutineContext: CoroutineContext,
        private val voipExtensionManager: VoipExtensionManager
    ) :
        android.telecom.CallEventCallback {
        private val CALL_EVENT_CALLBACK_TAG = CallEventCallbackImpl::class.simpleName
        /**
         * Stubbed supported capabilities for v2 connections.
         */
        @ExperimentalAppActions
        private val supportedCapabilities = mutableListOf(Capability())
        override fun onCallEndpointChanged(
            endpoint: android.telecom.CallEndpoint
        ) {
            callChannels.currentEndpointChannel.trySend(
                EndpointUtils.Api34PlusImpl.toCallEndpointCompat(endpoint)
            ).getOrThrow()
        }

        override fun onAvailableCallEndpointsChanged(
            endpoints: List<android.telecom.CallEndpoint>
        ) {
            callChannels.availableEndpointChannel.trySend(
                EndpointUtils.Api34PlusImpl.toCallEndpointsCompat(endpoints)
            ).getOrThrow()
        }

        override fun onMuteStateChanged(isMuted: Boolean) {
            callChannels.isMutedChannel.trySend(isMuted).getOrThrow()
        }

        override fun onCallStreamingFailed(reason: Int) {
            TODO("Implement with the CallStreaming code")
        }

        @ExperimentalAppActions
        override fun onEvent(event: String, extras: Bundle) {
            // Call events are sent via Call#sendCallEvent(event, extras). Begin initial capability
            // exchange procedure once we know that the ICS supports it.
            if (event == CallsManager.EVENT_JETPACK_CAPABILITY_EXCHANGE) {
                Log.i(CALL_EVENT_CALLBACK_TAG, "onEvent: EVENT_JETPACK_CAPABILITY_EXCHANGE: " +
                        "beginning capability exchange.")
                // Launch a new coroutine from the context of the current coroutine
                CoroutineScope(coroutineContext).launch {
                        voipExtensionManager.initiateVoipAppCapabilityExchange(
                            extras, supportedCapabilities, CALL_EVENT_CALLBACK_TAG)
                }
            }
        }
    }

    /**
     * CallControl is set by CallsManager#addCall when the CallControl object is returned by the
     * platform
     */
    fun setCallControl(control: android.telecom.CallControl) {
        mPlatformInterface = control
    }

    /**
     * Custom OutcomeReceiver that handles the Platform responses to a CallControl API call
     */
    inner class CallControlReceiver(deferred: CompletableDeferred<CallControlResult>) :
        OutcomeReceiver<Void, CallException> {
        private val mResultDeferred: CompletableDeferred<CallControlResult> = deferred

        override fun onResult(r: Void?) {
            mResultDeferred.complete(CallControlResult.Success())
        }

        override fun onError(error: CallException) {
            mResultDeferred.complete(CallControlResult.Error(
                androidx.core.telecom.CallException.fromTelecomCode(error.code)))
        }
    }

    fun getCallId(): ParcelUuid {
        return mPlatformInterface!!.callId
    }

    suspend fun setActive(): CallControlResult {
        val result: CompletableDeferred<CallControlResult> = CompletableDeferred()
        mPlatformInterface?.setActive(Runnable::run, CallControlReceiver(result))
        result.await()
        return result.getCompleted()
    }

    suspend fun setInactive(): CallControlResult {
        val result: CompletableDeferred<CallControlResult> = CompletableDeferred()
        mPlatformInterface?.setInactive(Runnable::run, CallControlReceiver(result))
        result.await()
        return result.getCompleted()
    }

    suspend fun answer(videoState: Int): CallControlResult {
        val result: CompletableDeferred<CallControlResult> = CompletableDeferred()
        mPlatformInterface?.answer(videoState, Runnable::run, CallControlReceiver(result))
        result.await()
        return result.getCompleted()
    }

    suspend fun requestEndpointChange(endpoint: android.telecom.CallEndpoint): CallControlResult {
        val result: CompletableDeferred<CallControlResult> = CompletableDeferred()
        mPlatformInterface?.requestCallEndpointChange(
            endpoint,
            Runnable::run, CallControlReceiver(result)
        )
        result.await()
        return result.getCompleted()
    }

    suspend fun disconnect(disconnectCause: DisconnectCause): CallControlResult {
        val result: CompletableDeferred<CallControlResult> = CompletableDeferred()
        mPlatformInterface?.disconnect(
            disconnectCause,
            Runnable::run,
            CallControlReceiver(result)
        )
        result.await()
        return result.getCompleted()
    }

    /**
     * CallControlCallback
     */
    fun onSetActive(wasCompleted: Consumer<Boolean>) {
        CoroutineScope(mCoroutineContext).launch {
            try {
                onSetActiveCallback()
                wasCompleted.accept(true)
            } catch (e: Exception) {
                handleCallbackFailure(wasCompleted, e)
            }
        }
    }

    fun onSetInactive(wasCompleted: Consumer<Boolean>) {
        CoroutineScope(mCoroutineContext).launch {
            try {
                onSetInactiveCallback()
                wasCompleted.accept(true)
            } catch (e: Exception) {
                handleCallbackFailure(wasCompleted, e)
            }
        }
    }

    fun onAnswer(videoState: Int, wasCompleted: Consumer<Boolean>) {
        CoroutineScope(mCoroutineContext).launch {
            try {
                onAnswerCallback(videoState)
                wasCompleted.accept(true)
            } catch (e: Exception) {
                handleCallbackFailure(wasCompleted, e)
            }
        }
    }

    fun onDisconnect(cause: DisconnectCause, wasCompleted: Consumer<Boolean>) {
        CoroutineScope(mCoroutineContext).launch {
            try {
                onDisconnectCallback(cause)
                wasCompleted.accept(true)
            } catch (e: Exception) {
                wasCompleted.accept(false)
                throw e
            } finally {
                blockingSessionExecution.complete(Unit)
            }
        }
    }

    private fun handleCallbackFailure(wasCompleted: Consumer<Boolean>, e: Exception) {
        wasCompleted.accept(false)
        blockingSessionExecution.complete(Unit)
        throw e
    }

    /**
     * =========================================================================================
     *  Simple implementation of [CallControlScope] with a [CallSession] as the session.
     * =========================================================================================
     */
    class CallControlScopeImpl(
        private val session: CallSession,
        callChannels: CallChannels,
        private val blockingSessionExecution: CompletableDeferred<Unit>,
        override val coroutineContext: CoroutineContext
    ) : CallControlScope {
        // handle requests that originate from the client and propagate into platform
        //  return the platforms response which indicates success of the request.
        override fun getCallId(): ParcelUuid {
            CoroutineScope(session.mCoroutineContext).launch {
            }
            return session.getCallId()
        }

        override suspend fun setActive(): CallControlResult {
            return session.setActive()
        }

        override suspend fun setInactive(): CallControlResult {
            return session.setInactive()
        }

        override suspend fun answer(callType: Int): CallControlResult {
            return session.answer(callType)
        }

        override suspend fun disconnect(disconnectCause: DisconnectCause): CallControlResult {
            val response = session.disconnect(disconnectCause)
            blockingSessionExecution.complete(Unit)
            return response
        }

        override suspend fun requestEndpointChange(endpoint: CallEndpointCompat):
            CallControlResult {
            return session.requestEndpointChange(
                EndpointUtils.Api34PlusImpl.toCallEndpoint(endpoint)
            )
        }

        // Send these events out to the client to collect
        override val currentCallEndpoint: Flow<CallEndpointCompat> =
            callChannels.currentEndpointChannel.receiveAsFlow()

        override val availableEndpoints: Flow<List<CallEndpointCompat>> =
            callChannels.availableEndpointChannel.receiveAsFlow()

        override val isMuted: Flow<Boolean> =
            callChannels.isMutedChannel.receiveAsFlow()
    }
}