CallsManager.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

import android.content.ComponentName
import android.content.Context
import android.os.Build.VERSION_CODES
import android.os.OutcomeReceiver
import android.os.Process
import android.telecom.CallControl
import android.telecom.CallException
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.util.Log
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.annotation.RestrictTo
import androidx.core.telecom.CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL
import androidx.core.telecom.internal.CallChannels
import androidx.core.telecom.internal.CallSession
import androidx.core.telecom.internal.CallSessionLegacy
import androidx.core.telecom.internal.JetpackConnectionService
import androidx.core.telecom.internal.utils.Utils
import java.util.concurrent.CancellationException
import java.util.concurrent.Executor
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.job
import kotlinx.coroutines.withTimeout

/**
 * CallsManager allows VoIP applications to add their calls to the Android system service Telecom.
 * By doing this, other services are aware of your VoIP application calls which leads to a more
 * stable environment. For example, a wearable may be able to answer an incoming call from your
 * application if the call is added to the Telecom system.  VoIP applications that manage calls and
 * do not inform the Telecom system may experience issues with resources (ex. microphone access).
 *
 * Note that access to some telecom information is permission-protected. Your app cannot access the
 * protected information or gain access to protected functionality unless it has the appropriate
 * permissions declared in its manifest file. Where permissions apply, they are noted in the method
 * descriptions.
 */
@RequiresApi(VERSION_CODES.O)
class CallsManager constructor(context: Context) {
    private val mContext: Context = context
    private var mPhoneAccount: PhoneAccount? = null
    private val mTelecomManager: TelecomManager =
        mContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
    internal val mConnectionService: JetpackConnectionService = JetpackConnectionService()

    // A single declared constant for a direct [Executor], since the coroutines primitives we invoke
    // from the associated callbacks will perform their own dispatch as needed.
    private val mDirectExecutor = Executor { it.run() }

    companion object {
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
        @IntDef(
            CAPABILITY_BASELINE,
            CAPABILITY_SUPPORTS_VIDEO_CALLING,
            CAPABILITY_SUPPORTS_CALL_STREAMING,
            flag = true
        )
        @Retention(AnnotationRetention.SOURCE)
        annotation class Capability

        /**
         * Set on Connections that are using ConnectionService+AUTO specific extension layer.
         */
        internal const val EXTRA_VOIP_API_VERSION = "android.telecom.extra.VOIP_API_VERSION"

        /**
         * Set on Jetpack Connections that are emulating the transactional APIs using
         * ConnectionService.
         */
        internal const val EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED =
            "android.telecom.extra.VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED"

        /**
         * EVENT used by InCallService as part of sendCallEvent to notify the VOIP Application that
         * this InCallService supports jetpack extensions
         */
        internal const val EVENT_JETPACK_CAPABILITY_EXCHANGE =
            "android.telecom.event.CAPABILITY_EXCHANGE";

        /**
         * VERSION used for handling future compatibility in capability exchange.
         */
        internal const val EXTRA_CAPABILITY_EXCHANGE_VERSION = "CAPABILITY_EXCHANGE_VERSION"

        /**
         * BINDER used for handling capability exchange between the ICS and VOIP app sides, sent
         * as part of sendCallEvent in the included extras.
         */
        internal const val EXTRA_CAPABILITY_EXCHANGE_BINDER = "CAPABILITY_EXCHANGE_BINDER"

        /**
         * The connection is using transactional call APIs.
         *
         *
         * The underlying connection was added as a transactional call via the
         * [TelecomManager.addCall] API.
         */
        internal const val PROPERTY_IS_TRANSACTIONAL = 0x00008000

        /**
         * If your VoIP application does not want support any of the capabilities below, then your
         * application can register with [CAPABILITY_BASELINE].
         *
         * Note: Calls can still be added and to the Telecom system but if other services request to
         * perform a capability that is not supported by your application, Telecom will notify the
         * service of the inability to perform the action instead of hitting an error.
         */
        const val CAPABILITY_BASELINE = 1 shl 0

        /**
         * Flag indicating that your VoIP application supports video calling.
         * This is not an indication that your application is currently able to make a video
         * call, but rather that it has the ability to make video calls (but not necessarily at this
         * time).
         *
         * Whether a call can make a video call is ultimately controlled by
         * [androidx.core.telecom.CallAttributesCompat]s capability
         * [androidx.core.telecom.CallAttributesCompat.CallType]#[CALL_TYPE_VIDEO_CALL],
         * which indicates that particular call is currently capable of making a video call.
         */
        const val CAPABILITY_SUPPORTS_VIDEO_CALLING = 1 shl 1

        /**
         * Flag indicating that this VoIP application supports call streaming. Call streaming means
         * a call can be streamed from a root device to another device to continue the call
         * without completely transferring it. The call continues to take place on the source
         * device, however media and control are streamed to another device.
         * [androidx.core.telecom.CallAttributesCompat.CallType]#[CAPABILITY_SUPPORTS_CALL_STREAMING]
         * must also be set on per call basis in the event an application wants to gate this
         * capability on a stricter basis.
         */
        const val CAPABILITY_SUPPORTS_CALL_STREAMING = 1 shl 2

        // identifiers that indicate the call was established with core-telecom
        internal const val PACKAGE_HANDLE_ID: String = "Jetpack"
        internal const val PACKAGE_LABEL: String = "Telecom-Jetpack"
        internal const val CONNECTION_SERVICE_CLASS =
            "androidx.core.telecom.internal.JetpackConnectionService"

        // fail messages specific to addCall
        internal const val CALL_CREATION_FAILURE_MSG =
            "The call failed to be added."
        internal const val ADD_CALL_TIMEOUT = 5000L
        private val TAG: String = CallsManager::class.java.simpleName.toString()
    }

    /**
     * VoIP applications should look at each [Capability] annotated above and call this API in
     * order to start adding calls via [addCall].  Registering capabilities must be done before
     * calling [addCall] or an exception will be thrown by [addCall]. The capabilities can be
     * updated by re-registering.
     *
     * Note: There is no need to unregister at any point. Telecom will handle unregistering once
     * the application using core-telecom has been removed from the device.
     *
     * @throws UnsupportedOperationException if the device is on an invalid build
     */
    @RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
    fun registerAppWithTelecom(@Capability capabilities: Int) {
        // verify the build version supports this API and throw an exception if not
        Utils.verifyBuildVersion()
        // start to build the PhoneAccount that will be registered via the platform API
        var platformCapabilities: Int = PhoneAccount.CAPABILITY_SELF_MANAGED
        val phoneAccountBuilder = PhoneAccount.builder(
            getPhoneAccountHandleForPackage(),
            PACKAGE_LABEL
        )
        // append additional capabilities if the device is on a U build or above
        if (Utils.hasPlatformV2Apis()) {
            platformCapabilities = PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS or
                Utils.remapJetpackCapabilitiesToPlatformCapabilities(capabilities)
        }
        // remap and set capabilities
        phoneAccountBuilder.setCapabilities(platformCapabilities)
        // build and register the PhoneAccount via the Platform API
        mPhoneAccount = phoneAccountBuilder.build()
        mTelecomManager.registerPhoneAccount(mPhoneAccount)
    }

    /**
     * Adds a new call with the specified [CallAttributesCompat] to the telecom service. This method
     * can be used to add both incoming and outgoing calls. Once the call is ready to be
     * disconnected, use the [CallControlScope.disconnect].
     *
     * <b>Call Lifecycle</b>: Your app is given foreground execution priority as long as you have an
     * ongoing call and are posting a [android.app.Notification.CallStyle] notification within 5
     * seconds of adding the call via this method. When your application is given foreground
     * execution priority, your app is treated as a foreground service. Foreground execution
     * priority will prevent the [android.app.ActivityManager] from killing your application when
     * it is placed the background. Foreground execution priority is removed from your app when all
     * of your app's calls terminate or your app no longer posts a valid notification.
     *
     * - Other things that should be noted:
     *     - For outgoing calls, your application should either immediately post a
     *       [android.app.Notification.CallStyle] notification or delay adding the call via this
     *       addCall method until the remote side is ready.
     *     - Each lambda function (onAnswer, onDisconnect, onSetActive, onSetInactive) has a
     *       timeout of 5000 milliseconds. Failing to complete the suspend fun before the timeout
     *       will result in a failed transaction.
     *     - Telecom assumes each callback (onAnswer, onDisconnect, onSetActive, onSetInactive)
     *       is handled successfully on the client side. If the callback cannot be completed,
     *       an Exception should be thrown. Telecom will rethrow the Exception and tear down
     *       the call session.
     *     - Each lambda function (onAnswer, onDisconnect, onSetActive, onSetInactive) has a
     *       timeout of 5000 milliseconds. Failing to complete the suspend fun before the
     *       timeout will result in a failed transaction.
     *
     * @param callAttributes     attributes of the new call (incoming or outgoing, address, etc. )
     *
     * @param onAnswer           where callType is the audio/video state the call should be
     *                           answered as.  Telecom is informing your VoIP application to answer
     *                           an incoming call and  set it to active. Telecom is requesting this
     *                           on behalf of an system service (e.g. Automotive service) or a
     *                           device (e.g. Wearable).
     *
     * @param onDisconnect       where disconnectCause represents the cause for disconnecting the
     *                           call. Telecom is informing your VoIP application to disconnect the
     *                           incoming call. Telecom is requesting this on behalf of an system
     *                           service (e.g. Automotive service) or a device (e.g. Wearable).
     *
     * @param onSetActive        Telecom is informing your VoIP application to set the call active.
     *                           Telecom is requesting this on behalf of an system service (e.g.
     *                           Automotive service) or a device (e.g. Wearable).
     *
     * @param onSetInactive      Telecom is informing your VoIP application to set the call
     *                           inactive. This is the same as holding a call for two endpoints but
     *                           can be extended to setting a meeting inactive. Telecom is
     *                           requesting this on behalf of an system service (e.g. Automotive
     *                           service) or a device (e.g.Wearable). Note: Your app must stop
     *                           using the microphone and playing incoming media when returning.
     * @param block              DSL interface block that will run when the call is ready
     *
     * @throws UnsupportedOperationException if the device is on an invalid build
     * @throws CancellationException if the call failed to be added within 5000 milliseconds
     */
    @RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
    @Suppress("ClassVerificationFailure")
    suspend fun addCall(
        callAttributes: CallAttributesCompat,
        onAnswer: suspend (callType: @CallAttributesCompat.Companion.CallType Int) -> Unit,
        onDisconnect: suspend (disconnectCause: android.telecom.DisconnectCause) -> Unit,
        onSetActive: suspend () -> Unit,
        onSetInactive: suspend () -> Unit,
        block: CallControlScope.() -> Unit
    ) {
        // This API is not supported for device running anything below Android O (26)
        Utils.verifyBuildVersion()
        // Setup channels for the CallEventCallbacks that only provide info updates
        val callChannels = CallChannels()
        callAttributes.mHandle = getPhoneAccountHandleForPackage()
        // This variable controls the addCall execution in the calling activity. AddCall will block
        // for the duration of the session.  When the session is terminated via a disconnect or
        // exception, addCall will unblock.
        val blockingSessionExecution = CompletableDeferred<Unit>(parent = coroutineContext.job)

        // create a call session based off the build version
        @RequiresApi(34)
        if (Utils.hasPlatformV2Apis()) {
            // CompletableDeferred pauses the execution of this method until the CallControl is
            // returned by the Platform.
            val openResult = CompletableDeferred<CallSession>(parent = coroutineContext.job)
            // CallSession is responsible for handling both CallControl responses from the Platform
            // and propagates CallControlCallbacks that originate in the Platform out to the client.
            val callSession = CallSession(
                coroutineContext,
                onAnswer,
                onDisconnect,
                onSetActive,
                onSetInactive,
                blockingSessionExecution)

            /**
             * The Platform [android.telecom.TelecomManager.addCall] requires a
             * [OutcomeReceiver]#<[CallControl], [CallException]> that will receive the async
             * response of whether the call can be added.
             */
            val callControlOutcomeReceiver =
                object : OutcomeReceiver<CallControl, CallException> {
                    override fun onResult(control: CallControl) {
                        callSession.setCallControl(control)
                        openResult.complete(callSession)
                    }

                    override fun onError(reason: CallException) {
                        // close all channels
                        callChannels.closeAllChannels()
                        // fail if we were still waiting for a CallControl
                        openResult.cancel(CancellationException(CALL_CREATION_FAILURE_MSG))
                    }
                }
            // leverage the platform API
            mTelecomManager.addCall(
                callAttributes.toCallAttributes(getPhoneAccountHandleForPackage()),
                mDirectExecutor,
                callControlOutcomeReceiver,
                CallSession.CallControlCallbackImpl(callSession),
                CallSession.CallEventCallbackImpl(callChannels, coroutineContext)
            )

            pauseExecutionUntilCallIsReady_orTimeout(openResult)

            /* at this point in time we have CallControl object */
            val scope =
                CallSession.CallControlScopeImpl(
                    openResult.getCompleted(),
                    callChannels,
                    blockingSessionExecution,
                    coroutineContext
                )

            // Run the clients code with the session active and exposed via the CallControlScope
            // interface implementation declared above.
            scope.block()
        } else {
            // CompletableDeferred pauses the execution of this method until the Connection
            // is created in JetpackConnectionService
            val openResult =
                CompletableDeferred<CallSessionLegacy>(parent = coroutineContext.job)

            val request = JetpackConnectionService.PendingConnectionRequest(
                callAttributes,
                callChannels,
                coroutineContext,
                openResult,
                onAnswer,
                onDisconnect,
                onSetActive,
                onSetInactive,
                blockingSessionExecution
            )

            mConnectionService.createConnectionRequest(mTelecomManager, request)

            pauseExecutionUntilCallIsReady_orTimeout(openResult, request)

            val scope = CallSessionLegacy.CallControlScopeImpl(
                openResult.getCompleted(),
                callChannels,
                blockingSessionExecution,
                coroutineContext
            )

            // Run the clients code with the session active and exposed via the
            // CallControlScope interface implementation declared above.
            scope.block()
        }
        blockingSessionExecution.await()
    }

    private suspend fun pauseExecutionUntilCallIsReady_orTimeout(
        openResult: CompletableDeferred<*>,
        request: JetpackConnectionService.PendingConnectionRequest? = null
    ) {
        try {
            withTimeout(ADD_CALL_TIMEOUT) {
                Log.i(TAG, "addCall: pausing [$coroutineContext] execution" +
                    " until the CallControl or Connection is ready")
                openResult.await()
            }
        } catch (timeout: TimeoutCancellationException) {
            Log.i(TAG, "addCall: timeout hit; canceling call in context=[$coroutineContext]")
            if (request != null) {
                JetpackConnectionService.mPendingConnectionRequests.remove(request)
            }
            openResult.cancel(CancellationException(CALL_CREATION_FAILURE_MSG))
        }
        Log.i(TAG, "addCall: creating call session and running the clients scope")
    }

    internal fun getPhoneAccountHandleForPackage(): PhoneAccountHandle {
        // This API is not supported for device running anything below Android O (26)
        Utils.verifyBuildVersion()

        val className = if (Utils.hasPlatformV2Apis()) {
            mContext.packageName
        } else {
            CONNECTION_SERVICE_CLASS
        }
        return PhoneAccountHandle(
            ComponentName(mContext.packageName, className),
            PACKAGE_HANDLE_ID,
            Process.myUserHandle()
        )
    }

    internal fun getBuiltPhoneAccount(): PhoneAccount? {
        return mPhoneAccount
    }
}