La biblioteca Core-Telecom optimiza el proceso de integración de tu aplicación de llamadas con la plataforma de Android, ya que proporciona un conjunto sólido y coherente de APIs.
Si deseas explorar implementaciones prácticas, puedes encontrar aplicaciones de muestra en GitHub:
- Aplicación de muestra ligera: Es un ejemplo mínimo
que demuestra el uso de la API
Core-Telecom. Es ideal para comprender rápidamente los conceptos fundamentales. - Aplicación de muestra integral (desarrollada por el equipo de Core-Telecom Team): Es una aplicación con más funciones que muestra las prácticas recomendadas y las funcionalidades avanzadas de Telecom. Es un excelente recurso para comprender situaciones de integración complejas.
Configura Core-Telecom
Agrega la dependencia androidx.core:core-telecom al archivo build.gradle de tu app:
dependencies {
implementation ("androidx.core:core-telecom:1.0.0")
}
Declara el permiso MANAGE_OWN_CALLS en tu AndroidManifest.xml:
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
Cómo registrar la aplicación
Registra tu app de llamadas en Android con CallsManager para comenzar a agregar llamadas al sistema. Cuando te registres, especifica las capacidades de tu app (por ejemplo, compatibilidad con audio y video):
val callsManager = CallsManager(context)
val capabilities: @CallsManager.Companion.Capability Int =
(CallsManager.CAPABILITY_BASELINE or
CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)
callsManager.registerAppWithTelecom(capabilities)
Administración de llamadas
Usa las APIs de Core-Telecom para crear y administrar un ciclo de vida de llamadas.
Crea una llamada
El objeto CallAttributesCompat define las propiedades de una llamada única, que puede tener las siguientes características:
displayName: Nombre del emisoraddress: Dirección de la llamada (por ejemplo, número de teléfono o vínculo de reunión)direction: Entrante o salientecallType: Audio o videocallCapabilities: Admite transferencia y espera
El siguiente es un ejemplo de cómo crear una llamada entrante:
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
)
}
Agregar una llamada
Usa callsManager.addCall con CallAttributesCompat y devoluciones de llamada para agregar una llamada nueva al sistema y administrar las actualizaciones de la superficie remota. El callControlScope dentro del bloque addCall permite principalmente que tu app realice la transición del estado de la llamada y reciba actualizaciones de audio:
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.
}
Cómo responder una llamada
Responde una llamada entrante dentro de CallControlScope:
when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
is CallControlResult.Success -> { /* Call answered */ }
is CallControlResult.Error -> { /* Handle error */ }
}
Rechaza una llamada
Rechaza una llamada con disconnect() con DisconnectCause.REJECTED dentro de CallControlScope:
disconnect(DisconnectCause(DisconnectCause.REJECTED))
Activa una llamada saliente
Configura una llamada saliente como activa una vez que la parte remota responda:
when (val result = setActive()) {
is CallControlResult.Success -> { /* Call active */ }
is CallControlResult.Error -> { /* Handle error */ }
}
Coloca una llamada en espera
Usa setInactive() para poner una llamada en espera:
when (val result = setInactive()) {
is CallControlResult.Success -> { /* Call on hold */ }
is CallControlResult.Error -> { /* Handle error */ }
}
Desconecta una llamada
Desconecta una llamada con disconnect() con un DisconnectCause:
disconnect(DisconnectCause(DisconnectCause.LOCAL))
Administra extremos de audio de llamadas
Observa y administra los extremos de audio con currentCallEndpoint, availableEndpoints y isMuted Flows dentro de CallControlScope.
fun observeAudioStateChanges(callControlScope: CallControlScope) {
with(callControlScope) {
launch { currentCallEndpoint.collect { /* Update UI */ } }
launch { availableEndpoints.collect { /* Update UI */ } }
launch { isMuted.collect { /* Handle mute state */ } }
}
}
Cambia el dispositivo de audio activo con requestEndpointChange():
coroutineScope.launch {
callControlScope.requestEndpointChange(callEndpoint)
}
Compatibilidad con primer plano
La biblioteca usa ConnectionService (nivel de API 33 de Android 13 y versiones anteriores) o
foregroundtypes (nivel de API 34 de Android 14 y versiones posteriores) para la compatibilidad con primer plano.
Como parte de los requisitos de primer plano, la aplicación debe publicar una notificación para que los usuarios sepan que se está ejecutando en primer plano.
Para asegurarte de que tu app obtenga prioridad de ejecución en primer plano, crea una notificación una vez que agregues la llamada con la plataforma. La prioridad de primer plano se quita cuando la app finaliza la llamada o cuando la notificación ya no es válida.
Obtén más información sobre los servicios en primer plano.
Compatibilidad con superficies remotas
Los dispositivos remotos (relojes inteligentes, auriculares Bluetooth y Android Auto) pueden administrar llamadas sin interacción directa con el teléfono. Tu app debe implementar lambdas de devolución de llamada (onAnswerCall, onSetCallDisconnected, onSetCallActive, onSetCallInactive) proporcionadas a CallsManager.addCall para controlar las acciones iniciadas por estos dispositivos.
Cuando se produce una acción remota, se invoca la lambda correspondiente.
La finalización correcta de la lambda indica que se procesó el comando. Si no se puede cumplir con el comando, la lambda debe arrojar una excepción.
La implementación adecuada garantiza un control de llamadas fluido en diferentes dispositivos. Realiza pruebas exhaustivas con varias superficies remotas.
Extensiones de llamada
Además de administrar el estado de la llamada y la ruta de audio de tus llamadas, la biblioteca también admite extensiones de llamada, que son funciones opcionales que tu app puede implementar para una experiencia de llamadas más enriquecida en superficies remotas, como Android Auto. Estas funciones incluyen salas de reuniones, silencio de llamadas y íconos de llamadas adicionales. Cuando tu app implementa una extensión, la información que proporciona se sincronizará con todos los dispositivos conectados que también admitan mostrar estas extensiones en su IU. Esto significa que estas funciones también estarán disponibles en dispositivos remotos para que los usuarios interactúen con ellas.
Crea una llamada con extensiones
Cuando crees una llamada, en lugar de usar CallManager#addCall para crearla,
puedes usar CallManager#addCallWithExtensions, que le da a la
app acceso a un permiso diferente llamado ExtensionInitializationScope. Este permiso permite que la aplicación inicialice el conjunto de extensiones opcionales que admite. Además, este permiso proporciona un método adicional, onCall, que proporciona un CallControlScope a la app después de que se completa el intercambio y la inicialización de la capacidad de extensión.
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...
}
}
}
Admite participantes de llamadas
Si tu app admite participantes de llamadas para reuniones o llamadas grupales, usa addParticipantExtension para declarar la compatibilidad con esta extensión y usa las APIs relacionadas para actualizar las superficies remotas cuando cambien los participantes.
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)
}
}
Además de notificar a las superficies remotas qué participantes están en la llamada, también se puede actualizar el participante activo con ParticipantExtension#updateActiveParticipant.
También hay compatibilidad con acciones opcionales relacionadas con los participantes de la llamada.
La app puede usar ParticipantExtension#addRaiseHandSupport para admitir la noción de que los participantes levanten la mano en la llamada y ver qué otros participantes también tienen la mano levantada.
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)
}
}
Admite el silencio de llamadas
El silencio de llamadas permite que un usuario solicite que la app silencie el audio saliente de una llamada sin silenciar físicamente el micrófono del dispositivo. Esta función se administra por llamada, por lo que Jetpack controla la complejidad de administrar el estado de silencio global de las llamadas celulares en curso mientras una llamada VoIP está activa. Esto hace que el silencio del audio saliente sea menos propenso a errores en situaciones de varias llamadas y, al mismo tiempo, permite funciones útiles, como las indicaciones de "¿estás hablando?" cuando el usuario habla sin darse cuenta de que el silencio de llamadas está habilitado.
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)
}
}
Admite íconos de llamadas
Un ícono de llamada permite que la app especifique un ícono personalizado que represente la llamada que se mostrará en las superficies remotas durante la llamada. Este ícono también se puede actualizar durante el ciclo de vida de la llamada.
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)
}
}
Se integra en el registro de llamadas del sistema
Core-Telecom permite que tu aplicación se integre en el registro de llamadas del sistema. Esta función permite que las llamadas realizadas o recibidas con tu aplicación VoIP aparezcan en el historial de llamadas del marcador del sistema, lo que permite a los usuarios usar el marcador para iniciar una devolución de llamada.
Habilita la integración del registro de llamadas
Para habilitar esta función, tu aplicación debe registrarse para controlar el intent TelecomManager.ACTION_CALL_BACK en tu AndroidManifest.xml y proporcionar una Activity para procesar la devolución de llamada. Por ejemplo, si VoipCallbackActivity es la actividad que controla la devolución de llamada, tu manifiesto debería verse de la siguiente manera:
Registra el intent en el manifiesto
<activity
android:name=".VoipCallbackActivity"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.action.CALL_BACK" />
</intent-filter>
</activity>
Controla el intent de devolución de llamada
Cuando el usuario inicia una devolución de llamada desde el marcador del sistema, se inicia la actividad registrada
con la TelecomManager.ACTION_CALL_BACK acción.
El intent contiene TelecomManager.EXTRA_UUID que se proporcionó a tu app cuando
agregaste la llamada originalmente con CallsManager.
El método CallControlScope getCallId muestra este ID único para la
llamada.
Debes guardar toda la información necesaria para iniciar la llamada identificada por el UUID dentro de tu app (por ejemplo, una lista de números de teléfono para una llamada grupal) para que puedas reconstruir y volver a realizar la llamada cuando se active la devolución de llamada.
class VoipCallbackActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent?.action == TelecomManager.ACTION_CALL_BACK) {
val uuidString = intent.getStringExtra(TelecomManager.EXTRA_UUID)
if (uuidString != null) {
val uuid = UUID.fromString(uuidString)
// Retrieve your persisted call info using the UUID
val callData = CallRepository.getCallByUuid(uuid)
if (callData != null) {
// Place the call again using your app's logic
// ...
}
}
}
finish()
}
}
Inhabilita la integración del registro de llamadas
De forma predeterminada, las llamadas se registran en el registro de llamadas del sistema. Puedes inhabilitar esta
función por llamada configurando el isLogged parámetro en false
cuando crees CallAttributesCompat:
val callAttributes = CallAttributesCompat(
displayName = callerName,
address = addressUri,
direction = CallAttributesCompat.DIRECTION_OUTGOING,
isLogExcluded = true // Opt-out of system call logging
)