VoipExtensionManager.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.extensions.voip

import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.RemoteException
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallControlScope
import androidx.core.telecom.CallsManager
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.CapabilityExchange
import androidx.core.telecom.internal.CallChannels
import kotlin.coroutines.CoroutineContext

@RequiresApi(Build.VERSION_CODES.O)
@androidx.annotation.OptIn(androidx.core.telecom.util.ExperimentalAppActions::class)
internal class VoipExtensionManager(
    private val context: Context,
    private val coroutineContext: CoroutineContext?,
    private val callChannels: CallChannels,
    // Capabilities to be included as specified on VOIP side.
    private val extensionsToAdd: List<Capability>
) {
    // The current call control session scope.
    private lateinit var session: CallControlScope
    // Handles participant extension updates from VOIP side to ICS.
    internal var participantExtensionManager: VoipParticipantExtensionManager? = null
    // Todo: re-enable once call icon impl. is complete.
    // Handles call detail extension updates from VOIP side to ICS.
//    internal var callDetailsExtensionManager: VoipCallDetailsExtensionManager? = null

    // Track each ICS with a unique id so that it can be used to distinguish the different
    // subscribers when sending updates from the VOIP side.
    private var currentIcsId = 0

    companion object {
        private val TAG = VoipExtensionManager::class.simpleName

        /**
         * Todo: VERSION HANDLING
         * List of all possible supported actions for the Participant extension. This will be
         * modified to include other actions for future versions.
         */
        internal val PARTICIPANT_SUPPORTED_ACTIONS = setOf(
            CallsManager.RAISE_HAND_ACTION, CallsManager.KICK_PARTICIPANT_ACTION)
        internal val CALL_DETAILS_SUPPORTED_ACTIONS:
            Set<@CallsManager.Companion.ExtensionSupportedActions Int> = setOf()
        internal val EXTENSION_SUPPORTED_ACTIONS_MAPPING:
            MutableMap<@CallsManager.Companion.ExtensionType Int,
                Set<@CallsManager.Companion.ExtensionSupportedActions Int>> = hashMapOf(
                    CallsManager.PARTICIPANT to PARTICIPANT_SUPPORTED_ACTIONS,
                    CallsManager.CALL_ICON to CALL_DETAILS_SUPPORTED_ACTIONS
                )

        /**
         * Static helper to determine if the ICS supports a given action.
         */
        internal fun isActionSupportedByIcs(
            actions: IntArray,
            actionToCheck: @CallsManager.Companion.ExtensionSupportedActions Int
        ): Boolean {
            for (action in actions) {
                if (action == actionToCheck) {
                    return true
                }
            }
            return false
        }
    }

    /**
     * Initialize the call session once it becomes available (CallSession / CallSessionLegacy).
     */
    internal fun initializeSession(currentSession: CallControlScope) {
        session = currentSession
    }

    /**
     * Initialize capabilities to be included as specified by the VOIP app.
     */
    internal fun initializeExtensions() {
        for (capability in extensionsToAdd) {
            addExtension(capability)
        }
    }

    /**
     * Internal helper to being capability negotiation between the VOIP app and ICS. This helper is
     * invoked on the VOIP side where negotiation begins when we are notified via a call event
     * (containing [CallsManager.EVENT_JETPACK_CAPABILITY_EXCHANGE]). The VOIP side is responsible
     * for informing the ICS of its supported capabilities and providing it with a listener with
     * which the ICS can leverage to set up extensions support with the VOIP app.
     *
     * @param extras received from call event.
     * @param supportedCapabilities for the VOIP app.
     * @param logTag
     */
    internal fun initiateVoipAppCapabilityExchange(
        extras: Bundle,
        supportedCapabilities: MutableList<Capability>,
        logTag: String? = TAG
    ) {
        Log.i(logTag, "initiateVoipAppCapabilityExchange: Begin capability exchange")
        // Retrieve binder from ICS.
        val capabilityExchange: CapabilityExchange? = extras.getBinder(
            CallsManager.EXTRA_CAPABILITY_EXCHANGE_BINDER) as CapabilityExchange?

        // Initialize capability exchange listener and set it on binder
        val capabilityExchangeListener = CapabilityExchangeListener(
            this@VoipExtensionManager, currentIcsId++)
        try {
            capabilityExchange?.let {
                capabilityExchange.beginExchange(supportedCapabilities, capabilityExchangeListener)
            }
        } catch (e: RemoteException) {
            Log.w(logTag, "initiateVoipAppCapabilityExchange: Remote exception occurred " +
                "while starting capability exchange with ICS.", e)
        } catch (e: Exception) {
            Log.w(logTag, "initiateVoipAppCapabilityExchange: Exception occurred", e)
        }
    }

    /**
     * Tear down all extensions and stop collecting updates when the call session is terminated.
     */
    internal fun tearDownExtensions() {
        participantExtensionManager?.tearDown()
//        callDetailsExtensionManager?.tearDown()
    }

    /***********************************************************************************************
     *                           Private Helpers
     *********************************************************************************************/

    /**
     * Private helper to register a specified extension on the VOIP side.
     *
     * @param capability (extension) to register.
     */
    private fun addExtension(capability: Capability) {
        when (capability.featureId) {
            CallsManager.PARTICIPANT -> {
                participantExtensionManager = VoipParticipantExtensionManager(session,
                    coroutineContext!!, callChannels, capability)
            }
            CallsManager.CALL_ICON -> {
                // Todo: Re-enable once call icon impl. is ready.
//                callDetailsExtensionManager = VoipCallDetailsExtensionManager(context, session,
//                    coroutineContext!!, capability)
            }
            CallsManager.CALL_SILENCE -> {
                // Todo
            }
            else -> Log.i(TAG, "Attempted to add incompatible extension")
        }
    }
}