Telecom

The new Android Telecom Jetpack library makes it easy to tell the platform what state your call is in. You can find the source code and a sample app on GitHub.

Dependencies and permissions

First, open your app module build.gradle file and add a dependency for the androidx Telecom module:

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

In your app manifest, declare that your app uses the MANAGE_OWN_CALLS` permission:

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

Register the application

To let Android know about your app, you must register it and its capabilities. This tells Android what features your app supports, like video calling, call streaming, and holding calls. This info is important so Android can configure itself to work with your app's features.

 private val callsManager = CallsManager(context)

var capabilities: @CallsManager.Companion.Capability Int =
    CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING

callsManager.registerAppWithTelecom(capabilities)

Platform integration

The two most common calling scenarios for any calling application are incoming and outgoing calls. To correctly register the direction of the call and appropriately notify the user with notifications, use the APIs below.

Register a call

This example demonstrates how to register an incoming call:

companion object {
  const val APP_SCHEME = "MyCustomScheme"
  const val ALL_CALL_CAPABILITIES = (CallAttributes.SUPPORTS_SET_INACTIVE
    or CallAttributes.SUPPORTS_STREAM or CallAttributes.SUPPORTS_TRANSFER)

  const val INCOMING_NAME = "Luke"
  val INCOMING_URI: Uri = Uri.fromParts(APP_SCHEME, "", "")
  // Define all possible properties for CallAttributes
  val INCOMING_CALL_ATTRIBUTES =
    CallAttributes(
      INCOMING_NAME,
      INCOMING_URI,
      DIRECTION_INCOMING,
      CALL_TYPE_VIDEO_CALL,
      ALL_CALL_CAPABILITIES)
}

The callAttributes object can have the following properties:

  • displayName: The name of the caller, meeting or session.
  • address: The Address of the call. Note, this can be extended to a meeting link.
  • direction: The direction of the call, such as incoming or outgoing.
  • callType: Information related to the data being transmitted, such as video and audio.
  • callCapabilities: An object that specifies the capabilities of the call.

The callCapabilities object can have the following properties:

  • streaming: Indicates whether the call supports streaming audio to another Android-powered device.
  • transfer: Indicates whether the call can be transferred.
  • hold: Indicates whether the call can be placed on hold.

Add a call

The addCall() method returns an exception if the device does not support telecom, or if an error has occurred when setting up the call.

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onIsCallAnswered, // Watch needs to know if it can answer the call
        onIsCallDisconnected,
        onIsCallActive,
        onIsCallInactive
    ) {
        callControlScope = this
    }
}

Answer a call

Once you have placed an incoming call you must answer or reject the call. This examle demonstrates how to answer a call:

when (answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> {

    }

    is CallControlResult.Error -> {

    }
}

If another call is in progress, answer() will return CallControlResult.Error, which informs why the call could not be answered. In this case, the user needs to place the other call on hold.

Reject a call

To reject a call, disconnect the call with DisconnectCause.Rejected.

fun onRejectCall(){
    coroutineScope.launch {
        callControlScope?.let {
            it.disconnect(DisconnectCause(DisconnectCause.REJECTED))
        }
    }
}

Outgoing call

When placing an outgoing call, once the remote party answers, you must set the call to active to make the platform aware that the call is in progress:

when (setActive()) {
    is CallControlResult.Success -> {
        onIsCallActive()
    }

    is CallControlResult.Error -> {
        updateCurrentCall {
            copy(errorCode = result.errorCode)
        }
    }
}

Place a call on hold

If your calling app supports holding calls, use setInActive to tell the platform that your call is not active and the microphone and camera are free to be used by other apps:

when (setInActive()) {
    is CallControlResult.Success -> {

    }

    is CallControlResult.Error -> {
        updateCurrentCall {
            copy(errorCode = result.errorCode)
        }
    }
}

Disconnect

To disconnect a call, inform the Telecom stack to disconnect by supplying a valid cause:

coroutineScope.launch {
    callControlScope?.disconnect(DisconnectCause(DisconnectCause.LOCAL))
}

Route audio

During a call, users sometimes switch between devices, such as a speaker, earpiece, or Bluetooth device. Use the availableEndpoints and currentCallEndpoint APIs to get a list of all the devices available to the user and which device is active.

This example combines both flows to create a UI object to show to the user a list of devices and which one is active:

availableEndpoint = combine(callControlScope.availableEndpoints,
    callControlScope.currentCallEndpoint) {
    availableDevices: List<CallEndpoint>, activeDevice : CallEndpoint ->
    availableDevices.map {
        EndPointUI(
            isActive = activeDevice.endpointName == it.endpointName, it
        )
    }
}

To change an active device, use the requestEndpointChange with the CallEndpoint you want to change to.

coroutineScope.launch {
     callControlScope?.requestEndpointChange(callEndpoint)
}

Foreground support

The Telecom library comes with foreground support. This library uses ConnectionService for devices running Android 13 and lower. For Android 14 and higher it uses the foregroundtypes microphone and camera to correctly support foreground services. Learn more about foreground services.

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 register the call with the platform. Foreground priority is removed when your app terminates the call or your notification is no longer valid.

is TelecomCall.Registered -> {
    val notification = createNotification(call)
    notificationManager.notify(TELECOM_NOTIFICATION_ID, notification)
}

Surface support

Watches have a generic endpoint receiver application. This application provides the user with a basic interface such as answering, rejecting and disconnecting calls. The application supports these actions by implementing lambda functions that inform the platform you have performed the action on the device.

Each lambda function times out after 5 seconds with a failed transaction if your application does not respond.

callsManager.addCall(
        attributes,
        onIsCallAnswered, // Watch/Auto need to know if they can answer the call
        onIsCallDisconnected,
        onIsCallActive,
        onIsCallInactive
    ) {
//Call Scope
}
/**
  *  Can the call be successfully answered??
  *  TIP: Check the connection/call state to see if you can answer a call
  *  Example you may need to wait for another call to hold.
  **/
val onIsCallAnswered: suspend(type: Int) -> Unit = {}

/**
  * Can the call perform a disconnect
  */
val onIsCallDisconnected: suspend (cause: DisconnectCause) -> Unit = {}

/**
  *  Check is see if you can make the call active.
  *  Other calls and state might stop us from activating the call
  */
val onIsCallActive: suspend () -> Unit = {
    updateCurrentCall {
    }
}

/**
  * Check to see if you can make the call inactivate
  */
val onIsCallInactive: suspend () -> Unit = {}