Core-Telecom

La bibliothèque Core-Telecom simplifie le processus d'intégration de votre application d'appel à la plate-forme Android en fournissant un ensemble d'API robustes et cohérentes.

Si vous souhaitez explorer des implémentations pratiques, vous trouverez des exemples d'applications sur GitHub :

Configurer Core-Telecom

Ajoutez la dépendance androidx.core:core-telecom au fichier build.gradle de votre application :

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

Déclarez l'autorisation MANAGE_OWN_CALLS dans votre fichier AndroidManifest.xml :

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

Enregistrer votre application

Enregistrez votre application d'appel auprès d'Android à l'aide de CallsManager pour commencer à ajouter des appels au système. Lors de l'enregistrement, spécifiez les fonctionnalités de votre application (par exemple, la prise en charge audio et vidéo) :

val callsManager = CallsManager(context)

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

callsManager.registerAppWithTelecom(capabilities)

Gestion des appels

Utilisez les API Core-Telecom pour créer et gérer le cycle de vie d'un appel.

Créer un appel

L'objet CallAttributesCompat définit les propriétés d'un appel unique, qui peut présenter les caractéristiques suivantes :

  • displayName : nom de l'appelant.
  • address : adresse de l'appel (par exemple, numéro de téléphone ou lien de réunion).
  • direction : entrant ou sortant.
  • callType : audio ou vidéo.
  • callCapabilities : accepte le transfert et la mise en attente.

Voici un exemple de création d'un appel entrant :

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

Ajouter un appel

Utilisez callsManager.addCall avec CallAttributesCompat et des rappels pour ajouter un nouvel appel au système et gérer les mises à jour de la surface à distance. Le callControlScope dans le bloc addCall permet principalement à votre application de faire passer l'état de l'appel et de recevoir des mises à jour 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.
}

Répondre à un appel

Répondre à un appel entrant dans CallControlScope :

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

Refuser un appel

Refusez un appel à l'aide de disconnect() avec DisconnectCause.REJECTED dans CallControlScope :

disconnect(DisconnectCause(DisconnectCause.REJECTED))

Activer un appel sortant

Définissez un appel sortant sur "actif" une fois que le correspondant distant répond :

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

Mettre un appel en attente

Utilisez setInactive() pour mettre un appel en attente :

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

Mettre fin à un appel

Déconnectez un appel à l'aide de disconnect() avec un DisconnectCause :

disconnect(DisconnectCause(DisconnectCause.LOCAL))

Gérer les points de terminaison audio des appels

Observer et gérer les points de terminaison audio à l'aide des currentCallEndpoint, availableEndpoints et isMuted Flow dans CallControlScope

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

Modifiez l'appareil audio actif à l'aide de requestEndpointChange() :

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

Assistance de premier plan

La bibliothèque utilise ConnectionService (Android 13, niveau d'API 33 et versions antérieures) ou foregroundtypes (Android 14, niveau d'API 34 et versions ultérieures) pour la prise en charge du premier plan.

Conformément aux exigences concernant le premier plan, l'application doit publier une notification pour informer les utilisateurs qu'elle s'exécute au premier plan.

Pour vous assurer que votre application bénéficie d'une priorité d'exécution au premier plan, créez une notification une fois que vous avez ajouté l'appel avec la plate-forme. La priorité au premier plan est supprimée lorsque votre application met fin à l'appel ou que votre notification n'est plus valide.

En savoir plus sur les services de premier plan

Assistance Surface à distance

Les appareils à distance (smartwatches, casques Bluetooth, Android Auto) peuvent gérer les appels sans interaction directe avec le téléphone. Votre application doit implémenter des lambdas de rappel (onAnswerCall, onSetCallDisconnected, onSetCallActive, onSetCallInactive) fournis à CallsManager.addCall pour gérer les actions initiées par ces appareils.

Lorsqu'une action à distance se produit, le lambda correspondant est appelé.

La réussite de la fonction Lambda indique que la commande a été traitée. Si la commande ne peut pas être respectée, le lambda doit générer une exception.

Une implémentation correcte assure un contrôle fluide des appels sur différents appareils. Testez-les minutieusement avec différentes surfaces distantes.

Extensions d'appel

En plus de gérer l'état des appels et le routage audio, la bibliothèque prend également en charge les extensions d'appel, qui sont des fonctionnalités facultatives que votre application peut implémenter pour une expérience d'appel plus riche sur les surfaces distantes, telles qu'Android Auto. Ces fonctionnalités incluent les salles de réunion, la mise en silence des appels et des icônes d'appel supplémentaires. Lorsque votre application implémente une extension, les informations qu'elle fournit sont synchronisées avec tous les appareils connectés qui prennent également en charge l'affichage de ces extensions dans leur UI. Cela signifie que ces fonctionnalités seront également disponibles sur les appareils à distance pour que les utilisateurs puissent interagir avec.

Créer un appel avec des extensions

Lorsque vous créez un appel, au lieu d'utiliser CallManager#addCall, vous pouvez utiliser CallManager#addCallWithExtensions, qui donne à l'application l'accès à un champ d'application différent appelé ExtensionInitializationScope. Ce champ d'application permet à l'application d'initialiser l'ensemble des extensions facultatives qu'elle prend en charge. De plus, ce champ d'application fournit une méthode supplémentaire, onCall, qui fournit un CallControlScope à l'application une fois l'échange et l'initialisation des fonctionnalités d'extension terminés.

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

Participants à l'appel d'assistance

Si votre application prend en charge les participants aux réunions ou aux appels de groupe, utilisez addParticipantExtension pour déclarer la compatibilité avec cette extension et utilisez les API associées pour mettre à jour les surfaces distantes lorsque les participants changent.

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

En plus d'informer les surfaces distantes des participants à l'appel, le participant actif peut également être mis à jour à l'aide de ParticipantExtension#updateActiveParticipant.

Les actions facultatives liées aux participants à l'appel sont également prises en charge. L'application peut utiliser ParticipantExtension#addRaiseHandSupport pour permettre aux participants de lever la main pendant l'appel et voir quels autres participants ont également levé la main.

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

Couper le son des appels d'assistance

La mise en sourdine des appels permet à un utilisateur de demander à l'application de couper le son sortant d'un appel sans couper physiquement le micro de l'appareil. Cette fonctionnalité est gérée par appel. Jetpack gère donc la complexité de la gestion de l'état de mise en sourdine globale des appels mobiles en cours lorsqu'un appel VoIP est actif. Cela permet de réduire les risques d'erreur lors de la désactivation du son sortant dans les scénarios à appels multiples, tout en offrant des fonctionnalités utiles telles que les indications "Parlez-vous ?" lorsque l'utilisateur parle sans se rendre compte que le son est désactivé.

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

Icônes d'appel de l'assistance

Une icône d'appel permet à l'application de spécifier une icône personnalisée représentant l'appel à afficher sur les surfaces distantes pendant l'appel. Cette icône peut également être modifiée pendant la durée de l'appel.

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

Intégrer au journal des appels système

Core-Telecom permet à votre application de s'intégrer au journal d'appels du système. Cette fonctionnalité permet aux appels émis ou reçus à l'aide de votre application VoIP d'apparaître dans l'historique des appels du clavier de numérotation du système, ce qui permet aux utilisateurs d'utiliser le clavier de numérotation pour demander à être rappelés.

Activer l'intégration du journal d'appels

Pour activer cette fonctionnalité, votre application doit s'enregistrer pour gérer l'intention TelecomManager.ACTION_CALL_BACK dans votre AndroidManifest.xml et fournir un Activity pour traiter le rappel. Par exemple, si VoipCallbackActivity est l'activité qui gère le rappel, votre fichier manifeste doit se présenter comme suit :

Enregistrer l'intent dans le fichier manifeste

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

Gérer l'intent de rappel

Lorsque l'utilisateur lance un rappel depuis le clavier du système, votre activité enregistrée est lancée avec l'action TelecomManager.ACTION_CALL_BACK. L'intent contient TelecomManager.EXTRA_UUID fourni à votre application lorsque vous avez ajouté l'appel à l'aide de CallsManager.

La méthode CallControlScope getCallId renvoie cet ID unique pour l'appel.

Vous devez enregistrer toutes les informations nécessaires pour démarrer l'appel identifié par l'UUID dans votre application (par exemple, une liste de numéros de téléphone pour un appel de groupe) afin de pouvoir reconstruire et passer l'appel à nouveau lorsque le rappel est déclenché.

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

Désactiver l'intégration du journal d'appels

Par défaut, les appels sont enregistrés dans le journal d'appels du système. Vous pouvez désactiver cette fonctionnalité pour chaque appel en définissant le paramètre isLogged sur false lors de la création de CallAttributesCompat :

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