Core-Telecom

The Core-Telecom library streamlines the process of integrating your calling application with the Android platform by providing a robust and consistent set of APIs

If you want to explore practical implementations, you can find sample applications on GitHub:

Set up Core-Telecom

Add the androidx.core:core-telecom dependency to your app's build.gradle file:

dependencies {
    implementation ("androidx.core:core-telecom:1.0.0")
}

Declare the MANAGE_OWN_CALLS permission in your AndroidManifest.xml:

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

Register your app

Register your calling app with Android using CallsManager to begin adding calls to the system. When registering, specify your app's capabilities (for example, audio, video support):

val callsManager = CallsManager(context)

val capabilities: @CallsManager.Companion.Capability Int =
    (CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)

callsManager.registerAppWithTelecom(capabilities)

Call Management

Use Core-Telecom APIs to create and manage a call lifecycle.

Create a call

The CallAttributesCompat object defines the properties of a unique call, which can have the following characteristics:

  • displayName: caller name.
  • address: Call address (for example, phone number, meeting link).
  • direction: Incoming or outgoing.
  • callType: Audio or video.
  • callCapabilities: Supports transfer and hold.

Here's an example of how to create an incoming call:

fun createIncomingCallAttributes(
    callerName: String,
    callerNumber: String,
    isVideoCall: Boolean): CallAttributesCompat {
    val addressUri = Uri.parse("YourAppScheme:$callerNumber")

    // Define capabilities supported by your call.
    val callCapabilities = CallAttributesCompat.CallCapability(
        supportsSetInactive = CallAttributesCompat.SUPPORTS_SET_INACTIVE // Call can be made inactive (implies hold)
    )

    return CallAttributesCompat(
        displayName = callerName,
        address = addressUri,
        direction = CallAttributesCompat.DIRECTION_INCOMING,
        callType = if (isVideoCall) CallAttributesCompat.CALL_TYPE_VIDEO_CALL else CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
        callCapabilitiesCompat = callCapabilities
    )
}

Add a call

Use callsManager.addCall with CallAttributesCompat and callbacks to add a new call to the system and manage remote surface updates. The callControlScope within the addCall block primarily allows your app to transition the call state and receive audio updates:

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onAnswerCall, // Watch needs to know if it can answer the call.
        onSetCallDisconnected,
        onSetCallActive,
        onSetCallInactive
    ) {
        // The call was successfully added once this scope runs.
        callControlScope = this
    }
}
catch(addCallException: Exception){
   // Handle the addCall failure.
}

Answer a call

Answer an incoming call within the CallControlScope:

when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> { /* Call answered */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Reject a call

Reject a call using disconnect() with DisconnectCause.REJECTED within the CallControlScope:

disconnect(DisconnectCause(DisconnectCause.REJECTED))

Make an outgoing call active

Set an outgoing call to active once the remote party answers:

when (val result = setActive()) {
    is CallControlResult.Success -> { /* Call active */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Place a call on hold

Use setInactive() to put a call on hold:

when (val result = setInactive()) {
    is CallControlResult.Success -> { /* Call on hold */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Disconnect a call

Disconnect a call using disconnect() with a DisconnectCause:

disconnect(DisconnectCause(DisconnectCause.LOCAL))

Manage call audio endpoints

Observe and manage audio endpoints using currentCallEndpoint, availableEndpoints, and isMuted Flows within the CallControlScope

fun observeAudioStateChanges(callControlScope: CallControlScope) {
    with(callControlScope) {
        launch { currentCallEndpoint.collect { /* Update UI */ } }
        launch { availableEndpoints.collect { /* Update UI */ } }
        launch { isMuted.collect { /* Handle mute state */ } }
    }
}

Change the active audio device using requestEndpointChange():

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

Foreground support

The library uses ConnectionService (Android 13 API level 33 and lower) or foregroundtypes (Android 14 API level 34 and higher) for foreground support.

As part of the foreground requirements, the application must post a notification for users to know that the application is running in the foreground.

To ensure that your app gets foreground execution priority, create a notification once you add the call with the platform. Foreground priority is removed when your app terminates the call or your notification is no longer valid.

Learn more about foreground services.

Remote Surface support

Remote devices (smartwatches, Bluetooth headsets, Android Auto) are capable of call management without direct phone interaction. Your app must implement callback lambdas (onAnswerCall, onSetCallDisconnected, onSetCallActive, onSetCallInactive) provided to CallsManager.addCall to handle actions initiated by these devices.

When a remote action occurs, the corresponding lambda is invoked.

Successful completion of the lambda signals that the command was processed. If the command cannot be obeyed, the lambda should throw an exception.

Proper implementation ensures seamless call control across different devices. Test thoroughly with various remote surfaces.