Ce document explique comment intégrer et personnaliser le SDK Headless Mobile dans votre application Android. Il couvre la voix, le partage d'écran, les appels planifiés et l'e-mail.
Appel vocal
Le SDK Headless Mobile pour Android permet de passer des appels vocaux grâce aux modules CCAICall et CCAICallRed. Votre application possède l'UI d'appel personnalisée, tandis que le SDK gère la création d'appels, la gestion des appels entrants, l'intégration du fournisseur et l'état du cycle de vie des appels.
Ce que fournit le SDK : des API de service d'appel pour lancer des appels instantanés, démarrer la messagerie vocale, gérer les appels entrants et observer l'état des appels.
Ce que vous allez créer : l'expérience d'appel visuelle, y compris le point d'entrée avant l'appel, l'invite de consentement à l'enregistrement, les états de sonnerie ou d'attente, les commandes en cours d'appel, l'UI du service de premier plan et toute navigation après l'appel.
Initialiser le module d'appel
Initialisez le module d'appel après avoir initialisé le SDK principal :
import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaicall.CallOptions
import com.ccaiplatform.ccaicall.initializeCall
CCAI.initialize(context = applicationContext, options = initOptions)
CCAI.initializeCall(
context = applicationContext,
options = CallOptions()
)
Inspecter les fonctionnalités de VoiceCallChannel
Avant de proposer un appel instantané, inspectez le VoiceCallChannel de la file d'attente pour décider quels points d'entrée sont exposés (appel instantané, renvoi vers la messagerie vocale, redirection au niveau du canal) et si le consentement à l'enregistrement est requis.
Champs VoiceCallChannel clés
instantEnabled:Booleanindique si un appel vocal en direct (instantané) est autorisé pour cette file d'attente. Traitefalsecomme "ne pas proposer de bouton d'appel instantané". Contrairement àvoiceCall != null, qui vous indique uniquement que la file d'attente dispose d'un canal vocal.preSessionSmartAction:Booleanindique si des actions intelligentes avant session sont requises sur ce canal spécifique avant de commencer un appel. Consultez Actions intelligentes avant la session dans SDK mobile sans interface utilisateur pour Android : actions intelligentes, pièces jointes et évitement pour le chemin complémentaire au niveau de la file d'attente.scheduleEnabled:Boolean(indique si les appels programmés sont acceptés). Consultez la section Appels programmés.recordingOption:RecordingOption?: modèle de consentement pour l'enregistrement des appels. Consultez Enregistrer le consentement. Différent deRecordingPermission.NOT_ASKED, qui n'est utilisé qu'avec les appels planifiés.voicemailReason:String?— renseigné lorsque le canal est configuré pour être redirigé vers la messagerie vocale.phoneNumber:String?— Numéro de secours RTCP configuré pour la déviation ou la messagerie vocale.scheduleDeflectionType:String?/deflected:Boolean?/deflectedReason:String?: métadonnées de déviation au niveau du canal. Lorsquedeflected == true, affichez le messagedeflectedReasonconfiguré par l'administrateur et proposez la solution de repli configurée (par exemple, un numéro de téléphone à composer ou un point d'entrée d'appel planifié) au lieu de tenter un appel instantané. Consultez la section Déflection du temps d'attente dans le SDK Mobile headless pour Android : actions intelligentes, pièces jointes et déflection pour la gestion de la déflection au niveau de la file d'attente.
fun voiceEntryPoints(menu: QueueMenu) {
val voice = menu.channels.voiceCall
if (voice == null) {
callNowButton.isVisible = false
voicemailButton.isVisible = false
return
}
// Instant call - only when explicitly enabled and not deflected
callNowButton.isVisible = voice.instantEnabled && voice.deflected != true
// Voicemail fallback
val number = voice.phoneNumber
if (voice.voicemailReason != null && number != null) {
voicemailButton.isVisible = true
voicemailButton.text = "Leave a voicemail"
voicemailButton.setOnClickListener {
dialVoicemail(number, reason = voice.voicemailReason)
}
}
// Channel-level deflection
if (voice.deflected == true) {
showChannelDeflection(reason = voice.deflectedReason)
}
}
Autorisation d'enregistrement
Ce que fournit le SDK : un champ recordingOption : RecordingOption? sur VoiceCallChannel qui décrit le modèle de consentement pour l'enregistrement des appels configuré par les administrateurs dans le portail.
Ce que vous allez créer : l'invite de consentement (lorsque ASK_USER) et la logique de branchement qui respecte le choix de l'administrateur.
enum class RecordingOption {
ALWAYS, // Calls are always recorded - surface a disclosure before connecting
NEVER, // Calls are never recorded - no disclosure needed
ASK_USER // Prompt the user for consent before the call connects
}
fun handleRecordingConsent(voice: VoiceCallChannel, onResult: (Boolean) -> Unit) {
when (voice.recordingOption) {
RecordingOption.ALWAYS -> {
showRecordingDisclosure("This call will be recorded.")
onResult(true)
}
RecordingOption.NEVER, null -> onResult(true)
RecordingOption.ASK_USER -> presentConsentPrompt { granted ->
onResult(granted)
}
}
}
Démarrer un appel instantané
Avant de démarrer un appel instantané :
Inspectez le
VoiceCallChannelde la file d'attente sélectionnée.Vérifiez que les appels instantanés sont activés et qu'ils ne sont pas redirigés.
Gérez l'autorisation d'enregistrement en fonction des
recordingOptionde la file d'attente.Demandez l'autorisation d'accéder au micro.
Appelez le
startInstantCall.
suspend fun initiateInstantCall(queueMenu: QueueMenu) {
val voice = queueMenu.channels.voiceCall ?: return
if (!voice.instantEnabled || voice.deflected == true) {
return
}
val recordingPermission = resolveRecordingPermission(voice)
requestMicrophonePermissionIfNeeded()
try {
CCAI.callService?.startInstantCall(
menuId = queueMenu.id,
recordingPermission = recordingPermission
)
} catch (e: Exception) {
showCallStartError(e)
}
}
Observer l'état de l'appel
Collectez les événements du service d'appel avant de démarrer ou d'accepter un appel :
val service = CCAI.callService ?: return
lifecycleScope.launch {
service.stateChanged.collect { state ->
updateCallConnectionState(state)
}
}
lifecycleScope.launch {
service.callReceived.collect { call ->
updateCallDetails(call)
}
}
lifecycleScope.launch {
service.incomingCallEvent.collect { event ->
handleIncomingCallEvent(event)
}
}
lifecycleScope.launch {
service.waitTimeUpdated.collect { waitTime ->
updateEstimatedWaitTime(waitTime)
}
}
lifecycleScope.launch {
service.participantUpdated.collect { participant ->
updateParticipantInfo(participant)
}
}
lifecycleScope.launch {
service.interruptionDetected.collect { interruption ->
// A PSTN interruption (for example, an incoming cellular call)
// has paused the active CCAI call. Surface a "call interrupted"
// banner and offer a Resume action after the interruption ends.
showInterruptionBanner(interruption)
}
}
lifecycleScope.launch {
service.deflectionOffered.collect { deflection ->
// The platform offered a wait-time deflection (for example, a
// scheduled callback or PSTN fallback). Render the prompt and
// honor the user's choice.
presentWaitTimeDeflection(deflection)
}
}
État du fournisseur et état du serveur
stateChanged et callReceived.status sont des signaux indépendants. Lisez-les comme des colonnes, et non comme des lignes. Le stateChanged du fournisseur indique le handshake VoIP local, tandis que le callReceived.status côté serveur indique le cycle de vie de l'appel sur la plate-forme. Le flip "Connecté" et le minuteur d'appel sont déclenchés par callReceived.status, et non par stateChanged lorsque l'état "Connecté" est atteint.
| Signal | Informations fournies | Ce que vous pouvez en tirer |
|---|---|---|
stateChanged (fournisseur) |
Échange local VoIP : Connecting, Connected (local), Disconnected, Error(...). |
Spinner d'état facultatif avant l'arrivée de l'état du serveur, l'affichage de l'erreur et le nettoyage de fin d'appel. |
callReceived.status (serveur) |
Cycle de vie côté serveur : WAITING, CONNECTING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED. |
le flip connecté, le début du minuteur d'appel, l'UI attribuée à l'agent, les calques de transfert ou d'escalade, et le flux de fin d'appel. |
Appels entrants
Les appels vocaux entrants sont transmis à l'appareil à l'aide de FCM. Étant donné que les appels entrants peuvent arriver lorsque votre application est en arrière-plan ou complètement arrêtée, votre intégration doit être prête à les recevoir depuis n'importe quel état de processus.
Ce que fournit le SDK : un flux IncomingCallEvent sur callService qui émet le cycle de vie d'un appel entrant : Arrived, Accepted, Rejected. Le service d'appel expose également acceptIncomingCall() et rejectIncomingCall() pour agir sur l'offre.
Ce que vous allez créer : l'UI des appels entrants (écran de sonnerie en plein écran, présentation sur l'écran de verrouillage ou bannière dans l'application) et les gestionnaires qui acceptent ou refusent l'offre.
Exemple de mise en œuvre
Collectez incomingCallEvent et affichez ou fermez votre UI à chaque événement de cycle de vie, puis acceptez ou refusez à partir d'un champ d'application de longue durée.
Définissez applicationScope une seule fois dans votre classe Application afin que les coroutines d'acceptation ou de refus survivent à toute suppression Activity de courte durée pendant l'établissement de la connexion.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
// Define this in your Application class (or another long-lived singleton).
// Expose it however your app prefers as top-level property, dependency
// injection, or a static field on the Application subclass.
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
lifecycleScope.launch {
CCAI.callService?.incomingCallEvent?.collect { event ->
when (event) {
is IncomingCallEvent.Arrived -> showIncomingCallUi(event.call)
is IncomingCallEvent.Accepted -> navigateToCallScreen(event.call)
is IncomingCallEvent.Rejected -> dismissIncomingCallUi()
}
}
}
// User taps "Accept" in your custom UI.
fun onAcceptTapped() {
// Important: accept/reject helpers suspend internally. Launch them on
// `applicationScope` (defined above) so the call survives if the
// ringing Activity is dismissed during the handshake.
applicationScope.launch {
CCAI.callService?.acceptIncomingCall()
}
}
fun onRejectTapped() {
applicationScope.launch {
CCAI.callService?.rejectIncomingCall()
}
}
Faire le pont entre FCM et l'UI de vos appels entrants
Les appels entrants transmis par FCM arrivent sur un thread en arrière-plan, souvent avant qu'un Activity existe. Utilisez un délai d'expiration court pour relier la charge utile FCM à la première émission IncomingCallEvent.Arrived, puis démarrez votre UI.
Exemple de mise en œuvre
Suspendez l'exécution sur le prochain IncomingCallEvent.Arrived avec withTimeoutOrNull, puis passez la main à votre navigateur.
suspend fun handleIncomingCallPush() {
val arrived = withTimeoutOrNull(60_000L) {
CCAI.callService?.incomingCallEvent
?.filterIsInstance<IncomingCallEvent.Arrived>()
?.first()
} ?: return
// `myNavigator` is the `IncomingCallUiNavigator` implementation you wire
// up during app initialization (see "Alternative: full-screen ringing
// Activity" below). Hold the reference somewhere long-lived (for
// example, on your `Application` subclass) so it can be reused here.
myNavigator.showRingingUi(applicationContext, arrived.call.id)
}
Intégration du framework Telecom (recommandée)
Pour une expérience d'appel entrant Android intégrée (sonnerie en plein écran, affichage sur l'écran de verrouillage et intégration au journal d'appels système), enregistrez un PhoneAccount et laissez le framework Telecom gérer l'expérience utilisateur de la sonnerie.
Ce que fournit le SDK : un assistant TelecomCallService qui enregistre un PhoneAccount pour votre application (registerPhoneAccount(context)), un TelecomConnectionService pour relier les appels CCAI entrants à Android Telecom et une interface TelecomConnectionCallback pour les rappels d'état de connexion.
Ce que vous allez créer : l'enregistrement PhoneAccount unique lors de l'initialisation de l'application et une déclaration de fichier manifeste pour le service de connexion.
Exemple de mise en œuvre
Enregistrez le compte de téléphone au démarrage de l'application, puis déclarez le service de connexion et l'autorisation dans votre fichier manifeste.
// In Application.onCreate(), after CCAI.initializeCall(...)
TelecomCallService.registerPhoneAccount(context = this)
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<service
android:name="com.ccaiplatform.android.call.TelecomConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
Alternative : activité d'appel en plein écran
Si vous préférez afficher votre propre écran de sonnerie au lieu d'utiliser le framework Telecom, le SDK expose IncomingCallService avec l'interface IncomingCallUiNavigator. Implémentez l'interface, puis transmettez votre implémentation à IncomingCallService lors de l'initialisation de l'application.
Exemple de mise en œuvre
Implémentez IncomingCallUiNavigator et transmettez-le à IncomingCallService une fois pendant Application.onCreate().
// 1. Implement the navigator. Override the three interface methods to
// declare the ringing Activity intent and to show/hide your UI as the
// call lifecycle changes.
class MyNavigator : IncomingCallUiNavigator {
override fun ringingActivityIntent(
context: Context,
callId: Long,
callerName: String?
): Intent = Intent(context, RingingActivity::class.java).apply {
putExtra("callId", callId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
override fun showRingingUi(context: Context, callId: Long) {
// Optional: surface an in-app banner or push your ringing UI.
}
override fun hideRingingUi(context: Context, callId: Long) {
// Optional: dismiss whatever you surfaced in showRingingUi.
}
}
// 2. Wire it up during app initialization (typically in Application.onCreate()).
val incomingCallService = IncomingCallService(
applicationContext,
MyNavigator()
)
if (!CCAI.canShowIncomingCallFullScreen(context)) {
CCAI.openFullScreenIntentSettings(context)
}
Service de premier plan pour la sonnerie active
Lorsqu'un appel sonne, Android peut fermer les processus en arrière-plan. Le SDK fournit un IncomingRingingService qui maintient le flux de sonnerie actif en tant que service de premier plan. Déclarez-le dans votre fichier manifeste. Le SDK le démarre et l'arrête automatiquement à l'arrivée d'appels.
<service
android:name="com.ccaiplatform.android.call.IncomingRingingService"
android:foregroundServiceType="phoneCall"
android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
État de l'appel et états côté serveur
Ce que fournit le SDK : une énumération CallStatus pilotée par le serveur sur chaque charge utile Call qui décrit l'état de l'appel dans son cycle de vie (par exemple, CONNECTING, WAITING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED, FAILED).
Ce que vous allez créer : le mappage de CallStatus aux états visibles par le client sur l'écran d'appel.
Exemple de mise en œuvre
Branchez when sur call.status à partir de chaque émission callReceived et affichez l'état de l'UI correspondant :
fun updateCallDetails(call: Call) {
when (call.status) {
CallStatus.CONNECTING -> renderConnectingState()
CallStatus.WAITING -> renderWaitingInQueueState(call.estimatedWait)
CallStatus.CONNECTED -> renderConnectedState(call.agent)
CallStatus.ON_HOLD -> renderOnHoldState()
CallStatus.INTERRUPTED -> renderInterruptedState()
CallStatus.ENDED, CallStatus.FAILED -> renderEndedState(call.endReason)
}
}
Commandes en cours d'appel (couper le son, haut-parleur, mise en attente)
Ce que fournit le SDK : propriétés en lecture seule sur callService – isMuted, isSpeakerEnabled, isOnHold – ainsi que les actions permettant d'activer/désactiver chaque état (setMuted, setSpeakerEnabled, setOnHold).
Ce que vous allez créer : les boutons de désactivation du micro, du haut-parleur et de mise en attente dans votre UI d'appel, connectés aux actions du SDK et reflétant l'état actuel.
Exemple de mise en œuvre
Lisez l'état actuel à partir de callService pour initialiser vos boutons, puis activez et mettez à jour l'état au moment de l'appui.
muteButton.isSelected = CCAI.callService?.isMuted == true
speakerButton.isSelected = CCAI.callService?.isSpeakerEnabled == true
holdButton.isSelected = CCAI.callService?.isOnHold == true
fun onMuteTapped() {
val next = CCAI.callService?.isMuted != true
CCAI.callService?.setMuted(next)
muteButton.isSelected = next
}
fun onSpeakerTapped() {
val next = CCAI.callService?.isSpeakerEnabled != true
CCAI.callService?.setSpeakerEnabled(next)
speakerButton.isSelected = next
}
fun onHoldTapped() {
val next = CCAI.callService?.isOnHold != true
CCAI.callService?.setOnHold(next)
holdButton.isSelected = next
}
Reprendre un appel en cours après une interruption
Lorsqu'une interruption du réseau téléphonique public commuté (comme un appel mobile entrant) met en pause l'appel CCAI actif, le SDK maintient la session active et émet interruptionDetected.
Une fois l'interruption terminée, votre application peut reprendre l'appel.
Ce que fournit le SDK : getLastCallInProgress() pour rechercher un appel en cours (y compris lors des redémarrages de l'application) et resumeCall(call) pour le ramener au premier plan.
Ce que vous allez créer : l'affordance de reprise dans votre UI (généralement une bannière ou une reprise automatique après la fin de l'interruption) et la navigation vers l'écran d'appel.
Exemple de mise en œuvre
Recherchez l'appel en cours avec getLastCallInProgress, reprenez-le et revenez à l'écran d'appel.
lifecycleScope.launch {
val call = CCAI.callService?.getLastCallInProgress() ?: return@launch
try {
CCAI.callService?.resumeCall(call)
navigateToCallScreen(call)
} catch (e: Exception) {
showCallResumeError(e)
}
}
Escalade depuis un agent virtuel
Ce que fournit le SDK : canEscalate(allowSkipVirtualAgent: Boolean) est une méthode sur CallResponse (le modèle émis par callService.callReceived), et non sur callService lui-même. Il indique si l'appel en cours peut être transféré à un agent humain en fonction de l'état actuel de l'agent virtuel de l'appel.
Ce que vous allez créer : conservez la dernière émission callReceived comme "appel en cours", puis associez la visibilité de votre bouton "Parler à une personne" à currentCall?.canEscalate(...). L'argument allowSkipVirtualAgent est une valeur de configuration de menu que vous récupérez séparément de la réponse à l'appel lui-même. Ne codez pas en dur true en production.
Exemple de mise en œuvre
Conservez le dernier Call dans une propriété, puis appelez canEscalate dessus. Résolvez allowSkipVirtualAgent à partir de la configuration de votre menu.
// Held from the most recent `callReceived` emission
private var currentCall: Call? = null
// `allowSkipVirtualAgent` is fetched separately from menu configuration
// it is not derived from the CallResponse.
val allowSkip = menuConfiguration.allowSkipVirtualAgent
val allowed = currentCall?.canEscalate(allowSkipVirtualAgent = allowSkip) == true
talkToAgentButton.isVisible = allowed
Messagerie vocale
Ce que fournit le SDK : startVoicemail(VoicemailRequest) sur callService, ainsi qu'une énumération VoicemailReason qui décrit pourquoi la plate-forme a proposé la messagerie vocale (par exemple, en cas de redirection en dehors des heures d'ouverture, en cas de redirection en raison d'une capacité dépassée ou en cas de redirection temporaire).
Ce que vous allez créer : le point d'entrée de la messagerie vocale dans votre UI (généralement affiché lorsque VoiceCallChannel.voicemailReason est renseigné ou lorsque le cycle de vie de l'appel aboutit à un message vocal) et l'UI d'enregistrement elle-même.
Exemple de mise en œuvre
Affichez le bouton de messagerie vocale lorsque voicemailReason
est défini, puis appelez startVoicemail avec un VoicemailRequest.
enum class VoicemailReason {
AFTER_HOUR_DEFLECTION,
OVER_CAPACITY_DEFLECTION,
TEMPORARY_REDIRECTION
}
suspend fun offerVoicemail(menu: QueueMenu) {
val voice = menu.channels.voiceCall ?: return
if (voice.voicemailReason == null) return
try {
CCAI.callService?.startVoicemail(
VoicemailRequest(menuId = menu.id)
)
} catch (e: Exception) {
showVoicemailError(e)
}
}
Déflection du temps d'attente
Lorsque la file d'attente est ouverte, mais que le temps d'attente dépasse le seuil configuré par l'administrateur, la plate-forme peut proposer une autre solution (généralement un rappel programmé ou une redirection vers le réseau téléphonique public commuté).
Ce que fournit le SDK : une charge utile CallDeflection décrivant l'alternative proposée, récupérable à l'aide de getCallDeflection(callId) sur callService. La plate-forme émet également la même charge utile via le flux deflectionOffered lorsqu'une redirection devient disponible en cours d'attente. Un indicateur CompanyResponse.preventDirectPstnCall distinct indique si la numérotation PSTN directe doit être masquée dans votre UI de secours.
Ce que vous allez créer : l'invite de redirection (par exemple, "Le temps d'attente est estimé à 12 minutes. Souhaitez-vous plutôt planifier un rappel ?") et la logique de branchement qui respecte le choix de l'utilisateur.
Exemple de mise en œuvre
Récupérez la déviation proposée avec l'ID de l'appel actif, combinez-la avec CompanyResponse.preventDirectPstnCall et présentez l'invite.
lifecycleScope.launch {
val currentCall = CCAI.callService?.getLastCallInProgress()
?: return@launch
val deflection = CCAI.callService?.getCallDeflection(callId = currentCall.id)
?: return@launch
val company = CCAI.companyService?.get()
val allowDirectPstn = company?.preventDirectPstnCall != true
presentWaitTimeDeflection(deflection, allowDirectPstn)
}
Avancée CallOptions
CallOptions expose un petit ensemble de boutons à bascule pour les applications qui doivent ajuster l'expérience d'appel par défaut.
connectingPollIntervalMs:Long— fréquence à laquelle le SDK interroge la plate-forme lorsque l'appel est à l'état "Connexion".connectedPollIntervalMs:Long— fréquence à laquelle le SDK interroge la plate-forme lorsque l'appel est connecté.endCallMaxRetries:Int— nombre de fois où le SDK tente d'envoyer la requête de fin d'appel avant d'afficher une erreur.endCallRetryDelayMs:Long– délai entre les nouvelles tentatives de fin d'appel.
Exemple de mise en œuvre
Transmettez les valeurs ajustées à CallOptions lors de l'initialisation du module d'appel :
CCAI.initializeCall(
context = this,
options = CallOptions(
connectingPollIntervalMs = 2_000L,
connectedPollIntervalMs = 5_000L,
endCallMaxRetries = 3,
endCallRetryDelayMs = 1_000L
)
)
Partage d'écran
Le partage d'écran permet aux agents d'assistance de voir l'écran de l'utilisateur pour l'aider à résoudre les problèmes. CCAI Platform est compatible avec deux modes de partage d'écran :
Partage dans l'application : l'agent ne peut voir que ce qui se passe dans votre application spécifique.
Partage complet de l'appareil (facultatif) : l'agent peut voir l'intégralité de l'appareil de l'utilisateur, y compris l'écran d'accueil Android et d'autres applications. Cela nécessite une configuration Android avancée à l'aide de
MediaProjectionet d'un service de premier plan.
Ce que fournit le SDK : le module CCAIScreenShare qui crée et gère les sessions, gère les changements d'état et traite les demandes de consentement.
Ce que vous allez créer : la configuration de l'initialisation, les invites de l'UI pour le consentement de l'utilisateur et (si vous prenez en charge l'appareil complet) l'intégration MediaProjection Android intégrée avec un service de premier plan.
Le service de partage d'écran fournit des méthodes pour l'ensemble du cycle de vie de la session : startSession, activateSession, stopSession, ainsi que des méthodes de configuration pour le contrôle à distance et le partage de l'appareil complet. Les sous-sections suivantes présentent le flux d'intégration principal.
Pour obtenir les signatures de méthode complètes et les détails des paramètres de toutes les méthodes de partage d'écran, consultez la documentation de référence de l'API Headless Mobile SDK pour Android.
Initialiser et configurer le partage d'écran
Initialisez le service de partage d'écran lors de la configuration de l'application en fournissant votre clé de partage d'écran :
import com.ccaiplatform.android.CCAI
import com.ccaiplatform.android.ScreenShareOptions
// Minimal - domain defaults to your instance's configured provider
CCAI.initializeScreenShare(
context = this,
screenShareOptions = ScreenShareOptions(
key = "YOUR_SCREEN_SHARE_KEY"
)
)
// If your instance or provider requires an explicit domain:
// CCAI.initializeScreenShare(
// context = this,
// screenShareOptions = ScreenShareOptions(
// key = "YOUR_SCREEN_SHARE_KEY",
// domain = "your_subdomain.ccaiplatform.com"
// )
// )
Vérifier l'éligibilité au partage d'écran
Avant d'activer la fonctionnalité de partage d'écran, vérifiez si elle est disponible pour la discussion en cours :
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
fun isScreenShareEnabled(chat: ChatResponse): Boolean {
return CCAI.screenShareService != null
&& chat.supportScreenShare == true
}
Traiter les demandes de partage d'écran
Implémentez la gestion des demandes de partage d'écran à l'aide de ScreenShareManager. Créez une session et gérez la réponse asynchrone à l'aide de ScreenShareCallbacks :
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
import com.ccaiplatform.ccaiscreenshare.ScreenShareCallbacks
fun requestScreenShare(chatId: String, isFromRemote: Boolean) {
val screenShareService = CCAI.screenShareService
if (screenShareService == null) {
Log.e("ScreenShare", "Screen share service not available")
return
}
// Track whether this session is agent-initiated
screenShareInitiatedFrom = if (isFromRemote) "agent" else "endUser"
if (!isFromRemote) {
sendScreenShareMessage(event = "screenShareRequestedFromEndUser")
}
val callbacks = ScreenShareCallbacks(
onSessionStateChanged = { state ->
Log.d("ScreenShare", "State changed: $state")
handleScreenShareStateChange(state)
},
onSessionCreationError = { error ->
Log.e("ScreenShare", "Session creation failed: ${error.message}")
sendScreenShareMessage(event = "screenShareFailed")
showErrorToast("Screen share failed to start.")
},
onSessionActivationRequest = {
Log.d("ScreenShare", "Activation request received")
// For agent-initiated sessions, activate immediately
// For user-initiated sessions, show a confirmation dialog first
if (screenShareInitiatedFrom == "agent") {
ScreenShareManager.activateSession()
} else {
showScreenShareConsentDialog {
ScreenShareManager.activateSession()
}
}
}
)
ScreenShareManager.startSession(
request = ScreenShareRequest(
communicationId = chatId,
communicationType = CommunicationType.Chat,
initiatedFrom = if (isFromRemote) ScreenShareFrom.AGENT else ScreenShareFrom.END_USER
),
callbacks = callbacks
)
}
Gérer les changements d'état du partage d'écran
Écoutez les changements d'état de la session de partage d'écran via le rappel onSessionStateChanged fourni à ScreenShareCallbacks :
fun handleScreenShareStateChange(state: ScreenShareSessionState) {
currentScreenShareSessionState = state
when (state) {
ScreenShareSessionState.INACTIVE -> {
Log.d("ScreenShare", "No active session")
}
ScreenShareSessionState.PENDING -> {
Log.d("ScreenShare", "Session pending - waiting for activation")
// For agent-initiated sessions, activate immediately
if (screenShareInitiatedFrom == "agent") {
ScreenShareManager.activateSession()
}
}
ScreenShareSessionState.ACTIVE -> {
Log.d("ScreenShare", "Screen share active")
sendScreenShareMessage(event = "screenShareStarted")
updateUiForActiveScreenShare()
}
}
}
Référence ScreenShareCallbacks
| Rappel | Description |
|---|---|
onSessionStateChanged |
Appelé lorsque l'état de la session de partage d'écran change (INACTIVE, PENDING, ACTIVE). |
onSessionCreationError |
Appelé en cas d'échec de la création de la session (problème de réseau, service indisponible, etc.). |
onSessionActivationRequest |
Appelé lorsque la session est prête à être activée. Appelez ScreenShareManager.activateSession() pour commencer le partage. Pour les sessions initiées par l'utilisateur, affichez d'abord une invite de consentement. Pour les sessions initiées par l'agent, activez-les immédiatement. |
onSessionRemoteControlRequest |
Appelé lorsque l'agent demande le contrôle à distance de l'écran de l'utilisateur. Afficher une invite de consentement avant d'accorder le contrôle à distance |
onSessionFullDeviceRequest | Appelée lorsque l'agent demande le partage de l'intégralité de l'appareil (voir Partage d'écran de l'intégralité de l'appareil ci-dessous). Déclenchez votre flux de consentement MediaProjection en réponse. |
onSessionDidSucceed |
Appelé après l'activation et le début du streaming d'une session. Utilisez-le pour mettre à jour votre UI et la faire passer à l'état "partage d'écran actif". |
Pour obtenir la définition complète de l'énumération d'état et de tous les types associés, consultez la documentation de référence de l'API Headless Mobile SDK pour Android.
Partage d'écran complet (avancé)
Le partage d'écran complet de l'appareil permet aux agents d'afficher les écrans des applications autres que les vôtres, y compris les paramètres système et la navigation entre les applications.
Cela nécessite d'intégrer l'API MediaProjection d'Android et d'exécuter un service de premier plan.
Déclarer les autorisations et le service de premier plan
Ajoutez les autorisations requises et la déclaration de service de premier plan à votre AndroidManifest.xml :
<manifest>
<!-- Required for screen capture -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<application>
<service
android:name=".screenshare.ScreenShareForegroundService"
android:foregroundServiceType="mediaProjection"
android:exported="false" />
</application>
</manifest>
Demander l'autorisation MediaProjection
Avant de capturer l'écran, vous devez demander le consentement de l'utilisateur à l'aide de MediaProjectionManager :
import android.media.projection.MediaProjectionManager
import android.content.Context
private val mediaProjectionManager by lazy {
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
}
fun requestScreenCapturePermission() {
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
screenCaptureResultLauncher.launch(captureIntent)
}
// Handle the result in your Activity
private val screenCaptureResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
// User granted screen capture permission
startScreenShareForegroundService(result.resultCode, result.data!!)
} else {
Log.d("ScreenShare", "User denied screen capture permission")
}
}
Implémenter le service de premier plan
Créez un service de premier plan qui contient la session MediaProjection :
import android.app.Service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.IBinder
class ScreenShareForegroundService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
startForeground(NOTIFICATION_ID, notification)
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Screen Share",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Active screen share session"
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
private fun buildNotification(): Notification {
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("Screen Sharing")
.setContentText("An agent is viewing your screen.")
.setSmallIcon(R.drawable.ic_screen_share)
.build()
}
companion object {
private const val CHANNEL_ID = "screen_share_channel"
private const val NOTIFICATION_ID = 1001
}
}
Activer le partage complet de l'appareil
Une fois MediaProjection activé, activez le partage de l'intégralité de l'appareil via le service de partage d'écran :
// After MediaProjection consent is granted and foreground service is started
ScreenShareManager.enableFullDeviceSharing(true)
Considérations concernant Jetpack Compose pour le partage d'écran
Approches recommandées pour l'interaction à distance dans Jetpack Compose
Encapsulez les principaux éléments interactifs avec des vues Android : pour les zones qui doivent être cliquables à distance, encapsulez-les en tant que commandes de vue Android standards (par exemple,
android.widget.Button) à l'aide du composableAndroidView. Cela garantit que les clics à distance fonctionnent de manière fiable via le chemin basé sur les vues Android.Transférer les événements tactiles à distance avec une gestion tactile personnalisée : pour les zones Compose qui ne peuvent pas être remplacées par des vues, utilisez des rappels tactiles personnalisés pour transférer les événements tactiles à distance vers votre logique Compose.
Fournir des conseils ou des solutions de secours au niveau du produit : indiquez clairement sur les écrans Compose que "les agents ne peuvent que consulter les informations ; l'utilisateur doit appuyer localement", ou limitez automatiquement les autorisations de contrôle à distance sur ces pages tout en conservant l'accès en lecture seule.
Exemple d'implémentation d'un wrapper de vue Android
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
import android.widget.Button
@Composable
fun RemoteControlButton(
title: String,
onClick: () -> Unit
) {
AndroidView(
factory = { context ->
Button(context).apply {
text = title
setOnClickListener { onClick() }
}
},
update = { button ->
button.text = title
}
)
}
// Usage in a Composable
@Composable
fun ScreenShareSupportScreen() {
Column {
Text("This screen supports remote control")
RemoteControlButton(title = "Clickable Button") {
println("Button tapped remotely or locally")
}
}
}
Appels programmés
Les appels planifiés permettent aux utilisateurs de réserver un appel vocal à une date ultérieure au lieu d'attendre dans la file d'attente pour un appel instantané. Sur Android, le SDK expose des méthodes d'appel de service pour vérifier la disponibilité, récupérer des créneaux horaires, réserver des appels, les reprogrammer, les annuler et gérer les erreurs de planification.
Détection des fonctionnalités
Ce que fournit le SDK : un indicateur scheduleEnabled sur VoiceCallChannel qui vous indique si la file d'attente sélectionnée permet de planifier des appels.
Ce que vous allez créer : la logique permettant de vérifier ce flag avant d'afficher votre point d'entrée de planification.
Exemple de mise en œuvre
Consultez scheduleEnabled sur le canal d'appel vocal de la file d'attente. N'utilisez pas voiceCall != null comme proxy. Une file d'attente peut prendre en charge les appels vocaux instantanés sans prendre en charge les appels planifiés, et inversement.
fun setupScheduledCallEntryPoint(queueMenu: QueueMenu) {
val voiceChannel = queueMenu.channels.voiceCall
if (voiceChannel == null) {
callNowButton.isVisible = false
scheduleCallButton.isVisible = false
return
}
callNowButton.isVisible = voiceChannel.instantEnabled && voiceChannel.deflected != true
scheduleCallButton.isVisible = voiceChannel.scheduleEnabled == true
}
Vérification de la disponibilité facultative
Avant d'afficher le sélecteur de créneaux horaires, vous pouvez vérifier si la file d'attente sélectionnée dispose de créneaux horaires disponibles.
Exemple de mise en œuvre
Utilisez hasScheduledTimeSlots(menuId:) comme prévérification légère. Si elle renvoie false, affichez un état vide ou une option de remplacement au lieu d'ouvrir le sélecteur.
lifecycleScope.launch {
try {
val available = CCAI.callService?.hasScheduledTimeSlots(menuId = menu.id) == true
if (available) {
showScheduleCallUI()
} else {
showNoSlotsMessage()
}
} catch (e: Exception) {
Log.e("CCAI", "Availability check failed: ${e.message}")
showNoSlotsMessage()
}
}
Récupérer les créneaux horaires
Récupérez les créneaux disponibles pour la file d'attente sélectionnée. Le SDK renvoie des valeurs de date que votre application peut afficher dans le fuseau horaire local de l'utilisateur.
Ce que fournit le SDK : les codes temporels des créneaux disponibles pour la file d'attente sélectionnée, avec une option permettant de reprogrammer un appel déjà planifié.
Ce que vous allez créer : le calendrier, le sélecteur d'heure, la présentation du fuseau horaire, la gestion de l'état vide et le comportement de nouvelle tentative.
Exemple de mise en œuvre
Appelez getScheduledTimeSlots avec l'ID du menu de file d'attente.
Lorsque vous reprogrammez un appel, transmettez l'ID de l'appel planifié existant afin que le service puisse renvoyer les disponibilités appropriées.
lifecycleScope.launch {
try {
val slots: List<Date> = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = null
) ?: emptyList()
if (slots.isEmpty()) {
showNoSlotsMessage()
} else {
displayTimeSlotPicker(slots)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to fetch time slots: ${e.message}")
}
}
Lorsque vous reprogrammez un appel, transmettez l'ID de l'appel planifié existant :
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
Planifier un appel
Une fois que l'utilisateur a sélectionné un créneau horaire, créez l'appel programmé via callService.
Ce que fournit le SDK : une API createScheduledCall qui réserve le créneau sélectionné et renvoie l'enregistrement de l'appel programmé.
Ce que vous allez créer : validation du numéro de téléphone au format E.164, UI de confirmation, persistance locale de l'ID de l'appel programmé et récupération en cas de conflits de créneaux ou de réponses en dehors des heures d'ouverture.
Exemple de mise en œuvre
Transmettez le créneau sélectionné, le numéro de téléphone, l'ID de file d'attente et RecordingPermission.NOT_ASKED dans un ScheduledCallRequest.
lifecycleScope.launch {
try {
val response = CCAI.callService?.createScheduledCall(
ScheduledCallRequest(
menuId = menu.id,
phoneNumber = userPhoneNumber,
scheduleTime = selectedSlot,
recordingPermission = RecordingPermission.NOT_ASKED,
callIdForRescheduling = null,
customData = null,
ticketId = null
)
)
saveScheduledCallId(response?.id)
showConfirmation(scheduledAt = response?.scheduledAt)
} catch (e: CallCreationError.AfterHours) {
showMessage(e.displayMessage)
refreshTimeSlots()
} catch (e: Exception) {
Log.e("CCAI", "Failed to schedule call: ${e.message}")
}
}
Le numéro de téléphone doit être au format E.164, par exemple +14155551234.
Reprogrammer un appel existant
Pour reprogrammer un appel, récupérez les créneaux horaires et créez le nouvel appel programmé avec l'ID d'appel programmé existant.
Ce que fournit le SDK : les mêmes API de création et de créneaux horaires, avec callIdForRescheduling utilisé pour identifier la réservation existante.
Ce que vous allez créer : l'UI "change time" (modifier l'heure), la recherche locale de l'ID de l'appel programmé stocké et l'état de confirmation mis à jour après la création de la nouvelle réservation.
Exemple de mise en œuvre
Transmettez l'ID d'appel existant à getScheduledTimeSlots et createScheduledCall.
lifecycleScope.launch {
try {
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
val response = CCAI.callService?.createScheduledCall(
ScheduledCallRequest(
menuId = menu.id,
phoneNumber = userPhoneNumber,
scheduleTime = newSelectedSlot,
recordingPermission = RecordingPermission.NOT_ASKED,
callIdForRescheduling = existingCallId,
customData = null,
ticketId = null
)
)
saveScheduledCallId(response?.id)
showConfirmation(scheduledAt = response?.scheduledAt)
} catch (e: CallCreationError.AfterHours) {
showMessage(e.displayMessage)
refreshTimeSlots()
} catch (e: Exception) {
Log.e("CCAI", "Failed to reschedule: ${e.message}")
}
}
Annuler un appel programmé
Lorsque l'utilisateur annule un appel à venir, appelez cancelScheduledCall et effacez l'ID d'appel stocké localement.
Ce que fournit le SDK : une API d'annulation pour un ID d'appel programmé.
Ce que vous allez créer : l'invite de confirmation, l'UI de réussite ou d'erreur d'annulation, et le nettoyage de tout état d'appel à venir stocké localement.
Exemple de mise en œuvre
Appelez cancelScheduledCall(callId:) avec l'ID stocké, puis supprimez l'ID du stockage de votre application.
lifecycleScope.launch {
try {
CCAI.callService?.cancelScheduledCall(callId = existingCallId)
clearScheduledCallId()
showCancellationConfirmation()
} catch (e: Exception) {
Log.e("CCAI", "Failed to cancel call: ${e.message}")
}
}
Ce que fournit le SDK : l'adresse e-mail de destination configurée pour cette file d'attente spécifique, ainsi que toutes les instructions que l'administrateur souhaite que l'utilisateur voie (par exemple, indiquez votre numéro de compte).
Ce que vous allez créer : l'écran de rédaction d'e-mails et la logique permettant de suivre son achèvement. Vous pouvez créer votre propre formulaire personnalisé ou utiliser Intent.ACTION_SENDTO intégré à Android pour lancer le client de messagerie préféré de l'utilisateur.
Exemple de mise en œuvre
Ce code extrait l'adresse e-mail d'assistance de l'objet Channels du SDK sur QueueMenu. Il ouvre le client de messagerie Android intégré et gère le cas où aucun client de messagerie n'est installé.
import android.content.Intent
import android.net.Uri
import android.widget.Toast
class SupportActivity : AppCompatActivity() {
// 1. Trigger the email flow
fun openEmailComposer(queueMenu: QueueMenu) {
val emailChannel = queueMenu.channels.email ?: return
val emailAddress = emailChannel.email ?: return
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, arrayOf(emailAddress))
putExtra(Intent.EXTRA_SUBJECT, "Support Request: ${queueMenu.name ?: ""}")
// If the queue provides an instruction message, include it in the body
// putExtra(Intent.EXTRA_TEXT, emailChannel.instructionMessage ?: "")
}
if (emailIntent.resolveActivity(packageManager) != null) {
emailResultLauncher.launch(emailIntent)
} else {
Toast.makeText(
this,
"No email client installed on this device.",
Toast.LENGTH_LONG
).show()
}
}
// 2. Track the outcome using ActivityResultContract
private val emailResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
// Note: Most Android email clients don't return a meaningful
// result code. Unlike iOS's MFMailComposeViewControllerDelegate,
// Android's email intent doesn't reliably report whether the
// email was sent, saved as draft, or cancelled.
// Log this event to your analytics and assume the user
// interacted with the email composer.
Log.d("CCAI", "Email composer closed. Result code: ${result.resultCode}")
MyAnalytics.logEvent("email_composer_closed")
}
}
Liens de déviation externe
Ce que fournit le SDK : une liste d'URL (par exemple, le centre d'aide de votre entreprise, les conditions de remboursement ou une application partenaire) et leurs titres à afficher, exactement tels qu'ils sont configurés dans le portail d'administration.
Ce que vous allez créer : la logique permettant d'exécuter ces liens lorsqu'un utilisateur appuie dessus, soit en les ouvrant dans un navigateur intégré dans l'application (à l'aide des onglets personnalisés d'Android), soit en lançant le navigateur système par défaut.
Exemple d'implémentation (onglets personnalisés : recommandé)
Lorsque l'utilisateur appuie sur un lien externe de votre menu, ce code extrait la chaîne d'URL fournie par le SDK et utilise les onglets personnalisés Chrome pour afficher la page Web dans un navigateur intégré à l'application. Les onglets personnalisés offrent une expérience plus rapide et plus fluide que le lancement de l'application de navigateur complète, tout en affichant la barre d'adresse pour que l'utilisateur sache qu'il consulte du contenu externe.
import androidx.browser.customtabs.CustomTabsIntent
import android.net.Uri
fun openHelpCenterLink(urlString: String) {
val uri = Uri.parse(urlString) ?: return
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(this, uri)
}
Implémentation de secours (navigateur système) : si les onglets personnalisés ne sont pas disponibles ou si vous préférez une approche plus simple, vous pouvez ouvrir le lien dans le navigateur par défaut de l'utilisateur :
import android.content.Intent
import android.net.Uri
fun openHelpCenterLink(urlString: String) {
val uri = Uri.parse(urlString) ?: return
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
showErrorToast("No browser available to open this link.")
}
}
Exemple d'intégration complète
Ce code lit le lien de redirection à partir de l'objet Channels du SDK et le présente à l'utilisateur sous la forme d'un bouton cliquable dans le menu de votre chaîne.
fun setupDeflectionLinks(queueMenu: QueueMenu) {
val deflectionLink = queueMenu.channels.externalDeflectionLink
if (deflectionLink != null) {
helpCenterButton.isVisible = true
helpCenterButton.setOnClickListener {
// Read the URL from the structured ExternalDeflectionLink type
val url = deflectionLink.url ?: return@setOnClickListener
openHelpCenterLink(url)
}
} else {
helpCenterButton.isVisible = false
}
}
ExternalDeflectionLink expose à la fois les champs url / displayName de premier niveau et un tableau links: List<ExternalDeflection>? interne. L'exemple précédent n'affiche que l'entrée de premier niveau, ce qui est suffisant pour les files d'attente à un seul lien. Si vos administrateurs configurent plusieurs liens externes dans une même file d'attente, itérez deflectionLink.links et affichez un bouton par entrée activée. Chaque ExternalDeflection comporte ses propres url et displayName :
deflectionLink.links
?.filter { it.enabled == true }
?.forEach { link ->
val button = MaterialButton(this).apply {
text = link.displayName
setOnClickListener { openHelpCenterLink(link.url) }
}
helpCenterContainer.addView(button)
}