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 = {}