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.

Call Extensions

In addition to managing the call state and audio route of your calls, the library also supports call extensions, which are optional features that your app can implement for a richer calling experience on remote surfaces, such as Android Auto. These features include meeting rooms, call silence, and additional call icons. When your app implements an extension, information that the app provides will be synchronized with all of the connected devices that also support displaying these extensions in their UI. This means that these features will be also be available on remote devices for users to interact with.

Create a Call with Extensions

When creating a call, instead of using CallManager#addCall to create the call, you can instead use CallManager#addCallWithExtensions, which gives the app access to a different scope called ExtensionInitializationScope. This scope allows the application to initialize the set of optional extensions that it supports. Additionally, this scope provides an extra method, onCall, which provides a CallControlScope back to the app after extension capability exchange and initialization completes.

scope.launch {
    mCallsManager.addCallWithExtensions(
        attributes,
        onAnswer,
        onDisconnect,
        onSetActive,
        onSetInactive
    ) {
        // Initialize extension-specific code...

        // After the call has been initialized, perform in-call actions
        onCall {
            // Example: process call state updates
            callStateFlow.onEach { newState ->
                // handle call state updates and notify telecom
            }.launchIn(this)

            // Use initialized extensions...
        }
    }
}

Support Call Participants

If your app supports call participants for meetings or group calls, use addParticipantExtension to declare support for this extension and use the related APIs to update remote surfaces when the participants change.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial participants state in the call.
        val participantExtension = addParticipantExtension(
            initialParticipants,
            initialActiveParticipant
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
        }
    }

Along with notifying remote surfaces of what participants are in the call, the active participant can also be updated using ParticipantExtension#updateActiveParticipant.

There's also support for optional actions related to the call participants. The app can use ParticipantExtension#addRaiseHandSupport to support the notion of participants raising their hand in the call and see which other participants also have their hands raised.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial list of participants in the call.
        val participantExtension = addParticipantExtension(initialParticipants)
        // Notifies Jetpack that this app supports the notion of participants
        // being able to raise and lower their hands.
        val raiseHandState = participantExtension.addRaiseHandSupport(
                initialRaisedHands
            ) { onHandRaisedStateChanged ->
                // handle this user's raised hand state changed updates from
                // remote surfaces.
            }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
            // notify remote surfaces of which of the participants have their
            // hands raised
            raisedHandsFlow.onEach { newRaisedHands ->
                raiseHandState.updateRaisedHands(newRaisedHands)
            }.launchIn(this)
        }
    }

Support Call Silence

Call silence allows a user to request that the app silence a call's outgoing audio without physically muting the device's microphone. This feature is managed per call, so Jetpack handles the complexity of managing the global mute state of ongoing cellular calls while a VOIP call is active. This makes silencing outgoing audio less error prone in multi-call scenarios while also allowing for helpful features such as "are you speaking" indications when the user is speaking while not realizing that call silence is enabled.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for locally silencing the call's outgoing audio and
        // register a handler for when the user changes the call silence state
        // from a remote surface.
        val callSilenceExtension = addLocalCallSilenceExtension(
            initialCallSilenceState = false
        ) { newCallSilenceStateRequest ->
            // handle the user's request to enable/disable call silence from
            // a remote surface
        }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's call silence state changes, update remote
            // surfaces of the new state.
            callSilenceState.onEach { isSilenced ->
                callSilenceExtension.updateIsLocallySilenced(isSilenced)
            }.launchIn(this)
        }
    }

Support Call Icons

A call icon allows the app to specify a custom icon representing the call to be displayed on remote surfaces during the call. This icon can also be updated over the lifetime of the call.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for a custom call icon to be displayed during the
        // lifetime of the call.
        val callIconExtension = addCallIconExtension(
            initialCallIconUri = initialUri
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's icon changes, update remote surfaces by providing
            // the new URI.
            callIconUri.onEach { newIconUri ->
                callIconExtension.updateCallIconUri(newIconUri)
            }.launchIn(this)
        }
    }