Core-Telecom

La libreria Core-Telecom semplifica il processo di integrazione dell'applicazione di chiamata con la piattaforma Android fornendo un insieme di API robusto e coerente

Se vuoi esplorare implementazioni pratiche, puoi trovare applicazioni di esempio su GitHub:

Configurare Core-Telecom

Aggiungi la dipendenza androidx.core:core-telecom al file build.gradle dell'app:

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

Dichiara l'autorizzazione MANAGE_OWN_CALLS in AndroidManifest.xml:

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

Registrazione dell'our app

Registra la tua app di chiamata con Android utilizzando CallsManager per iniziare ad aggiungere chiamate al sistema. Durante la registrazione, specifica le funzionalità dell'app (ad esempio, supporto audio, video):

val callsManager = CallsManager(context)

val capabilities: @CallsManager.Companion.Capability Int =
    (CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)

callsManager.registerAppWithTelecom(capabilities)

Gestione chiamate

Utilizza le API Core-Telecom per creare e gestire il ciclo di vita di una chiamata.

Creare una chiamata

L'oggetto CallAttributesCompat definisce le proprietà di una chiamata univoca, che può avere le seguenti caratteristiche:

  • displayName: nome del chiamante.
  • address: indirizzo della chiamata (ad esempio, numero di telefono, link della riunione).
  • direction: in arrivo o in uscita.
  • callType: audio o video.
  • callCapabilities: supporta il trasferimento e la messa in attesa.

Di seguito è riportato un esempio di come creare una chiamata in arrivo:

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

Aggiungere una chiamata

Utilizza callsManager.addCall con CallAttributesCompat e i callback per aggiungere una nuova chiamata al sistema e gestire gli aggiornamenti della superficie remota. callControlScope all'interno del blocco addCall consente principalmente all'app di modificare lo stato della chiamata e ricevere aggiornamenti 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.
}

Rispondere a una chiamata

Rispondi a una chiamata in arrivo all'interno di CallControlScope:

when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> { /* Call answered */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Rifiutare una chiamata

Rifiuta una chiamata utilizzando disconnect() con DisconnectCause.REJECTED all'interno di CallControlScope:

disconnect(DisconnectCause(DisconnectCause.REJECTED))

Rendere attiva una chiamata in uscita

Imposta una chiamata in uscita su attiva una volta che la parte remota risponde:

when (val result = setActive()) {
    is CallControlResult.Success -> { /* Call active */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Mettere in attesa una chiamata

Utilizza setInactive() per mettere in attesa una chiamata:

when (val result = setInactive()) {
    is CallControlResult.Success -> { /* Call on hold */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Terminare una chiamata

Termina una chiamata utilizzando disconnect() con un DisconnectCause:

disconnect(DisconnectCause(DisconnectCause.LOCAL))

Gestire gli endpoint audio delle chiamate

Osserva e gestisci gli endpoint audio utilizzando i Flow di currentCallEndpoint, availableEndpoints e isMuted all'interno di CallControlScope

fun observeAudioStateChanges(callControlScope: CallControlScope) {
    with(callControlScope) {
        launch { currentCallEndpoint.collect { /* Update UI */ } }
        launch { availableEndpoints.collect { /* Update UI */ } }
        launch { isMuted.collect { /* Handle mute state */ } }
    }
}

Modifica il dispositivo audio attivo utilizzando requestEndpointChange():

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

Supporto in primo piano

La libreria utilizza ConnectionService (livello API 33 di Android 13 e versioni precedenti) o foregroundtypes (livello API 34 di Android 14 e versioni successive) per il supporto in primo piano.

Nell'ambito dei requisiti in primo piano, l'applicazione deve pubblicare una notifica per informare gli utenti che l'applicazione è in esecuzione in primo piano.

Per assicurarti che la tua app abbia la priorità di esecuzione in primo piano, crea una notifica dopo aver aggiunto la chiamata con la piattaforma. La priorità in primo piano viene rimossa quando l'app termina la chiamata o la notifica non è più valida.

Scopri di più sui servizi in primo piano.

Supporto per la superficie remota

I dispositivi remoti (smartwatch, cuffie Bluetooth, Android Auto) sono in grado di gestire le chiamate senza interazione diretta con lo smartphone. La tua app deve implementare le lambda di callback (onAnswerCall, onSetCallDisconnected, onSetCallActive, onSetCallInactive) fornite a CallsManager.addCall per gestire le azioni avviate da questi dispositivi.

Quando si verifica un'azione remota, viene richiamata la lambda corrispondente.

Il completamento della lambda indica che il comando è stato elaborato. Se il comando non può essere eseguito, la lambda deve generare un'eccezione.

L'implementazione corretta garantisce un controllo delle chiamate senza interruzioni su diversi dispositivi. Esegui test approfonditi con varie superfici remote.

Estensioni di chiamata

Oltre a gestire lo stato della chiamata e il percorso audio delle chiamate, la libreria supporta anche le estensioni di chiamata, che sono funzionalità facoltative che la tua app può implementare per un'esperienza di chiamata più ricca su superfici remote, come Android Auto. Queste funzionalità includono sale riunioni, silenziamento delle chiamate e icone di chiamata aggiuntive. Quando la tua app implementa un'estensione, le informazioni fornite dall'app vengono sincronizzate con tutti i dispositivi connessi che supportano anche la visualizzazione di queste estensioni nella loro UI. Ciò significa che queste funzionalità saranno disponibili anche sui dispositivi remoti per l'interazione degli utenti.

Creare una chiamata con le estensioni

Quando crei una chiamata, anziché utilizzare CallManager#addCall, puoi utilizzare CallManager#addCallWithExtensions, che consente all' app di accedere a un ambito diverso denominato ExtensionInitializationScope. Questo ambito consente all'applicazione di inizializzare l'insieme di estensioni facoltative che supporta. Inoltre, questo ambito fornisce un metodo aggiuntivo, onCall, che fornisce un CallControlScope all'app dopo il completamento dello scambio e dell'inizializzazione delle funzionalità dell'estensione.

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

Supportare i partecipanti alla chiamata

Se la tua app supporta i partecipanti alla chiamata per riunioni o chiamate di gruppo, utilizza addParticipantExtension per dichiarare il supporto per questa estensione e utilizza le API correlate per aggiornare le superfici remote quando i partecipanti cambiano.

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

Oltre a notificare alle superfici remote quali partecipanti sono nella chiamata, è possibile aggiornare anche il partecipante attivo utilizzando ParticipantExtension#updateActiveParticipant.

È disponibile anche il supporto per le azioni facoltative relative ai partecipanti alla chiamata. L'app può utilizzare ParticipantExtension#addRaiseHandSupport per supportare la nozione di partecipanti che alzano la mano durante la chiamata e vedere quali altri partecipanti hanno alzato la mano.

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

Supportare il silenziamento delle chiamate

Il silenziamento delle chiamate consente a un utente di richiedere all'app di silenziare l'audio in uscita di una chiamata senza disattivare fisicamente il microfono del dispositivo. Questa funzionalità viene gestita per ogni chiamata, quindi Jetpack gestisce la complessità della gestione dello stato di silenziamento globale delle chiamate cellulari in corso mentre è attiva una chiamata VoIP. In questo modo, il silenziamento dell'audio in uscita è meno soggetto a errori negli scenari con più chiamate e consente anche funzionalità utili come le indicazioni "stai parlando" quando l'utente sta parlando senza rendersi conto che il silenziamento delle chiamate è attivo.

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

Supportare le icone di chiamata

Un'icona di chiamata consente all'app di specificare un'icona personalizzata che rappresenta la chiamata da visualizzare sulle superfici remote durante la chiamata. Questa icona può anche essere aggiornata durante la durata della chiamata.

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

Integrare con il registro chiamate di sistema

Core-Telecom consente alla tua applicazione di integrarsi con il registro chiamate di sistema. Questa funzionalità consente di visualizzare le chiamate effettuate o ricevute utilizzando l'applicazione VoIP nella cronologia chiamate del dialer di sistema, consentendo agli utenti di utilizzare il dialer per avviare un callback.

Attivare l'integrazione del registro chiamate

Per attivare questa funzionalità, l'applicazione deve registrarsi per gestire l'intent TelecomManager.ACTION_CALL_BACK in AndroidManifest.xml e fornire un'attività Activity per elaborare il callback. Ad esempio, se VoipCallbackActivity è l'attività che gestisce il callback, il manifest dovrebbe essere simile al seguente:

Registrare l'intent nel manifest

<activity
    android:name=".VoipCallbackActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.telecom.action.CALL_BACK" />
    </intent-filter>
</activity>

Gestire l'intent di callback

Quando l'utente avvia un callback dal dialer di sistema, l'attività registrata viene avviata con l'azione TelecomManager.ACTION_CALL_BACK. L'intent contiene TelecomManager.EXTRA_UUID fornito alla tua app quando hai aggiunto la chiamata utilizzando CallsManager.

Il metodo CallControlScope getCallId restituisce questo ID univoco per la chiamata.

Devi salvare tutte le informazioni necessarie per avviare la chiamata identificata dall'UUID all'interno dell'app (ad esempio, un elenco di numeri di telefono per una chiamata di gruppo) in modo da poter ricostruire ed effettuare nuovamente la chiamata quando viene attivato il callback.

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

Disattivare l'integrazione del registro chiamate

Per impostazione predefinita, le chiamate vengono registrate nel registro chiamate di sistema. Puoi disattivare questa funzionalità per ogni chiamata impostando il isLogged parametro su false quando crei CallAttributesCompat:

val callAttributes = CallAttributesCompat(
    displayName = callerName,
    address = addressUri,
    direction = CallAttributesCompat.DIRECTION_OUTGOING,
    isLogExcluded = true // Opt-out of system call logging
)