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 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.job
/**
* 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
private 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_GROUP)
@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
/**
* 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."
}
/**
* VoIP applications should look at each [Capability] annotated above and call this API in
* order to start adding calls via [addCall].
*
* Note: Registering capabilities must be done before calling [addCall] or an exception will
* be thrown by [addCall].
*
* @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.
*
* @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
* @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
* @Throws CallException if [CallControlScope.setCallback] is not called first within the block
*/
@RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@Suppress("ClassVerificationFailure")
suspend fun addCall(
callAttributes: CallAttributesCompat,
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()
// 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)
/**
* 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)
)
openResult.await() /* wait for the platform to provide a CallControl object */
/* at this point in time we have CallControl object */
val scope =
CallSession.CallControlScopeImpl(openResult.getCompleted(), callChannels)
// 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)
mConnectionService.createConnectionRequest(
mTelecomManager,
JetpackConnectionService.PendingConnectionRequest(
callAttributes, callChannels, coroutineContext, openResult
)
)
openResult.await()
val scope =
CallSessionLegacy.CallControlScopeImpl(openResult.getCompleted(), callChannels)
// Run the clients code with the session active and exposed via the
// CallControlScope interface implementation declared above.
scope.block()
}
}
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
}
}