Core-Telecom

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 quieres explorar implementaciones prácticas, puedes encontrar aplicaciones de muestra en GitHub:

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 con Android usando 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.

Cómo crear una llamada

El objeto CallAttributesCompat define las propiedades de una llamada única, que puede tener las siguientes características:

  • displayName: Es el nombre del emisor.
  • address: Es la dirección de llamada (por ejemplo, número de teléfono o vínculo de reunión).
  • direction: Llamada entrante o saliente.
  • callType: Audio o video.
  • callCapabilities: Admite transferencia y retención.

Este 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
    )
}

Cómo 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 */ }
}

Cómo rechazar una llamada

Rechaza una llamada con disconnect() con DisconnectCause.REJECTED dentro de CallControlScope:

disconnect(DisconnectCause(DisconnectCause.REJECTED))

Cómo activar una llamada saliente

Cómo establecer una llamada saliente como activa una vez que la parte remota responde:

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 */ }
}

Cómo desconectar una llamada

Desconecta una llamada con disconnect() con un DisconnectCause:

disconnect(DisconnectCause(DisconnectCause.LOCAL))

Administra los extremos de audio de las llamadas

Observa y administra los extremos de audio con Flow de currentCallEndpoint, availableEndpoints y isMuted 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 (Android 13 nivel de API 33 y versiones anteriores) o foregroundtypes (Android 14 nivel de API 34 y versiones posteriores) para la compatibilidad en primer plano.

Como parte de los requisitos en 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 en primer plano se quita cuando tu app finaliza la llamada o cuando tu notificación ya no es válida.

Obtén más información sobre los servicios en primer plano.

Compatibilidad remota con Surface

Los dispositivos remotos (relojes inteligentes, auriculares Bluetooth y Android Auto) pueden administrar llamadas sin interactuar directamente con el teléfono. Tu app debe implementar las lambdas de devolución de llamada (onAnswerCall, onSetCallDisconnected, onSetCallActive, onSetCallInactive) que se proporcionan a CallsManager.addCall para controlar las acciones que inician estos dispositivos.

Cuando se produce una acción remota, se invoca la lambda correspondiente.

Completación correcta de los indicadores lambda que indican que se procesó el comando. Si no se puede obedecer el comando, la expresión lambda debe arrojar una excepción.

La implementación adecuada garantiza un control de llamadas sin problemas en diferentes dispositivos. Prueba exhaustivamente con varias plataformas remotas.

Extensiones de llamada

Además de administrar el estado de las llamadas y la ruta de audio, la biblioteca también admite extensiones de llamadas, que son funciones opcionales que tu app puede implementar para brindar una experiencia de llamadas más enriquecida en plataformas remotas, como Android Auto. Estas funciones incluyen salas de reuniones, silenciamiento de llamadas y íconos de llamadas adicionales. Cuando tu app implemente una extensión, la información que proporciona se sincronizará con todos los dispositivos conectados que también admitan la visualización de 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.

Cómo crear una llamada con extensiones

Cuando crees una llamada, en lugar de usar CallManager#addCall, puedes usar CallManager#addCallWithExtensions, que le otorga a la app acceso a un alcance diferente llamado ExtensionInitializationScope. Este alcance permite que la aplicación inicialice el conjunto de extensiones opcionales que admite. Además, este alcance proporciona un método adicional, onCall, que devuelve un CallControlScope a la app después de que se completa el intercambio y la inicialización de la capability 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...
        }
    }
}

Cómo brindar asistencia a los participantes de una llamada

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 plataformas 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 plataformas remotas qué participantes están en la llamada, el participante activo también se puede actualizar con ParticipantExtension#updateActiveParticipant.

También se admite la ejecución de acciones opcionales relacionadas con los participantes de la llamada. La app puede usar ParticipantExtension#addRaiseHandSupport para admitir la idea 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)
        }
    }

Cómo silenciar llamadas de asistencia

La función de silenciar 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 silenciamiento global de las llamadas celulares en curso mientras una llamada de VoIP está activa. Esto hace que silenciar el 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 está hablando sin darse cuenta de que la función de silenciar llamadas está habilitada.

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)
        }
    }

Íconos de llamada de asistencia

Un ícono de llamada permite que la app especifique un ícono personalizado que represente la llamada que se mostrará en las plataformas remotas durante la llamada. Este ícono también se puede actualizar durante la vida útil 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)
        }
    }