Ce document vous explique comment intégrer et personnaliser le SDK Headless Mobile dans votre application Android. Il couvre les actions intelligentes, les pièces jointes et la redirection.
Configurer les actions intelligentes et les pièces jointes
Les actions intelligentes permettent aux agents et aux clients d'échanger des informations riches et structurées, comme des photos, des vidéos, des extraits de texte et des informations de validation biométrique.
Dans l'architecture headless, le SDK agit strictement en tant que couche de transport sécurisée. Il vous indique quand une action est requise ou demandée, et il transfère les données de manière sécurisée vers la plate-forme CCAI Platform. Comme vous possédez l'interface utilisateur, votre application est entièrement responsable de la création des écrans de l'appareil photo, des sélecteurs de photos, des zones de texte et du déclenchement des invites biométriques intégrées d'Android.
Actions intelligentes de présession
Les actions avant session permettent de recueillir du contexte (comme un numéro de compte ou une vidéo d'un produit défectueux) avant que l'utilisateur ne rejoigne la file d'attente, ce qui réduit considérablement le temps de traitement des agents.
Ce que fournit le SDK : les règles et les exigences pour une file d'attente spécifique (par exemple, si la validation biométrique, l'importation de photos ou la saisie de texte sont facultatives ou obligatoires avant de rejoindre la file d'attente).
Ce que vous allez créer : les écrans de l'UI de pré-session (invite de caméra, saisie de texte et invite biométrique) qui s'affichent entre le moment où l'utilisateur appuie sur une file d'attente et celui où il entre dans la salle d'attente, ainsi que la logique de mise en cache sur disque pour conserver les contenus multimédias en toute sécurité jusqu'à ce que la session se connecte.
Exemple de mise en œuvre
Ce code vérifie la configuration du SDK pour voir si la file d'attente choisie présente des exigences de pré-session. Il montre comment gérer une exigence vidéo en conservant une référence de fichier Uri sur le disque au lieu de conserver la vidéo en mémoire.
// 1. Check requirements and collect data BEFORE joining the queue
fun checkPreSessionRequirements(queueMenu: QueueMenu) {
val smartActions = queueMenu.settings
if (smartActions.isNullOrEmpty()) {
startChat(queueMenu)
return
}
// Example: handle a video requirement
presentVideoPicker { localFileUri ->
// IMPORTANT: localFileUri should point to context.cacheDir
MySessionStateManager.pendingPreSessionVideo = localFileUri
startChat(queueMenu)
}
}
// 2. Upload and clean up AFTER the session officially connects
fun handleSessionConnected() {
val pendingVideoUri = MySessionStateManager.pendingPreSessionVideo ?: return
// Upload using the standard in-session pipeline
// then clean up the temporary file
val file = File(pendingVideoUri.path ?: return)
file.delete()
MySessionStateManager.pendingPreSessionVideo = null
}
Lire la configuration des actions intelligentes de présession
Le SDK expose la configuration des actions intelligentes de présession via deux chemins complémentaires. Vérifiez les deux pour déterminer si la capture avant session est requise pour une file d'attente et un canal donnés.
Chemin A : au niveau de la file d'attente (QueueMenu.settings[].sdk.preSessionSmartAction)
// `QueueMenuSetting` and `QueueMenuSDKSetting` (simplified)
data class QueueMenuSetting(
val sdk: QueueMenuSDKSetting?
// ... other fields ...
)
data class QueueMenuSDKSetting(
val preSessionSmartAction: Boolean?
// ... other fields ...
)
// Usage - "does any queue-level setting require a pre-session smart action?"
fun queueRequiresPreSession(menu: QueueMenu): Boolean {
return menu.settings?.any { it.sdk?.preSessionSmartAction == true } ?: false
}
Parcours B : au niveau du canal (VoiceCallChannel.preSessionSmartAction)
// Usage - "does the voice channel specifically require pre-session capture?"
val requiresPreSessionOnVoice =
menu.channels.voiceCall?.preSessionSmartAction == true
Utilisez le chemin A pour déterminer s'il faut afficher une porte de pré-session avant que l'utilisateur choisisse une chaîne. Utilisez le chemin B pour affiner les canaux après en avoir sélectionné un (par exemple, en n'invitant l'utilisateur qu'avant un appel vocal instantané).
Actions intelligentes en cours de session (initiées par l'agent)
Lors d'une conversation active par chat ou appel vocal, un agent peut cliquer sur un bouton de son tableau de bord pour demander des données à l'utilisateur (par exemple, Envoyez une photo de votre pièce d'identité ou Authentifiez-vous avec vos données biométriques).
Ce que fournit le SDK : des événements en temps réel qui alertent votre application lorsque l'agent a demandé un type spécifique d'action intelligente.
Ce que vous allez créer : une alerte dans le chat ou lors d'un appel informant l'utilisateur de la demande, lançant l'outil intégré approprié (appareil photo, bibliothèque de photos, champ de texte ou BiometricPrompt Android) et renvoyant le résultat au SDK.
Exemple de mise en œuvre
Ici, votre application écoute les événements du SDK. Lorsqu'il détecte une demande d'agent, il affiche une alerte, puis ouvre l'outil Android approprié.
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
// Listen for Smart Action requests from the active SDK session.
//
// `SmartActionRequest` exposes a nested `smartAction: SmartAction`, and the
// switchable type lives at `request.smartAction.type`. The `SmartActionType`
// enum is `PascalCase: Unknown`, `Verification`, `Screenshot`, `Photo`, `Video`,
// `TextInput`, `ScreenShare`. Use `Verification` for Face ID / fingerprint flows.
fun handleSmartActionRequest(request: SmartActionRequest) {
when (request.smartAction.type) {
SmartActionType.Photo -> {
promptUserForPhoto(message = "The agent requested a photo.") { capturedBitmap ->
// Send the image back to the agent using the SDK.
sendAttachment(image = capturedBitmap)
}
}
SmartActionType.Verification -> {
val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
sendBiometricResult(success = true)
}
override fun onAuthenticationFailed() {
sendBiometricResult(success = false)
}
override fun onAuthenticationError(
errorCode: Int, errString: CharSequence
) {
sendBiometricResult(success = false)
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Identity Verification")
.setSubtitle("Verify your identity to continue")
.setNegativeBtnText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo)
}
SmartActionType.TextInput -> {
presentTextInputPrompt(message = "The agent requested text input.") { text ->
sendSmartActionText(text)
}
}
SmartActionType.Video -> {
// Agent-requested video capture. Reuse the same disk-cached file
// pattern recommended for pre-session media keep the recording on
// disk and pass the local Uri to the SDK rather than holding a
// large `ByteArray` in memory.
promptUserForVideo(message = "The agent requested a video.") { capturedVideoUri ->
sendAttachment(videoUri = capturedVideoUri)
}
}
SmartActionType.Screenshot -> {
// The SDK exposes `Screenshot` as a distinct request type the
// user captures (or selects), a single still image of the current
// screen. This is **not** the same as `ScreenShare`, which
// starts a live co-browse session.
promptUserForScreenshot(message = "The agent requested a screenshot.") { capturedBitmap ->
sendAttachment(image = capturedBitmap)
}
}
SmartActionType.ScreenShare -> {
// Hand off to the full screen share flow (see Headless Mobile SDK for Android: Voice, screen share, scheduled call, and email).
// Don't confuse with `Screenshot`; `ScreenShare` starts a
// live co-browse session using `CCAIScreenShare`, while `Screenshot`
// captures a single still frame.
startScreenShareFlow()
}
SmartActionType.Unknown -> {
// Future-proof: a smart action type the SDK does not recognize.
// Log and continue; don't block the user.
Log.w("CCAI", "Received unknown smart action type")
}
}
}
Les actions intelligentes initiées par l'agent arrivent via les événements SDK de la session active. Abonnez-vous aux événements de chat ou d'appel pertinents avant de démarrer la session afin que votre application puisse présenter l'invite appropriée aux clients dès qu'une demande arrive.
Notifier et suivre les actions intelligentes
Utilisez smartActionService pour confirmer et mettre à jour les actions intelligentes au fur et à mesure que l'utilisateur les effectue.
val router = CommunicationRouter(
id = chatId,
type = CommunicationType.Chat
)
// Notify the server that a smart action has been received.
lifecycleScope.launch {
try {
CCAI.smartActionService?.notifySmartActionReceived(
router = router,
smartActionId = smartActionId
)
Log.d("CCAI", "Smart action notification sent successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to notify smart action: ${e.message}")
}
}
// Update the smart action status as the user completes it.
lifecycleScope.launch {
try {
CCAI.smartActionService?.updateSmartActionStatus(
router = router,
smartActionId = smartActionId,
status = SmartActionStatus.Finished
)
Log.d("CCAI", "Smart action status updated successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to update smart action status: ${e.message}")
}
}
Si l'action demande une saisie de texte, collectez le texte dans votre UI et envoyez-le via le service d'actions intelligentes avant de marquer l'action comme terminée.
Formulaires Web
Ce que fournit le SDK : des API permettant de récupérer, d'afficher et de valider les formulaires Web déclenchés par les agents lors d'une session de chat.
Ce que vous allez créer : l'UI de rendu du formulaire Web et le flux d'envoi qui regroupe les réponses de l'utilisateur.
Exemple de mise en œuvre
Ce code récupère un formulaire Web par son ID externe et son ID d'action intelligente, puis valide la réponse avant l'envoi.
// Fetch a web form - `fetchWebForm` takes two positional parameters on Android.
lifecycleScope.launch {
try {
val webFormResponse = CCAI.chatService?.fetchWebForm(
externalFormId = "form123",
smartActionId = 123
)
if (webFormResponse != null) {
Log.d("CCAI", "Web form received: $webFormResponse")
// Render the web form in your custom UI.
renderWebForm(webFormResponse)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to fetch web form: ${e.message}")
}
}
// Validate a web form response.
lifecycleScope.launch {
try {
val isValid = CCAI.chatService?.validateWebForm(webFormResponse)
if (isValid == true) {
Log.d("CCAI", "Web form is valid")
} else {
Log.d("CCAI", "Web form validation failed")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to validate web form: ${e.message}")
}
}
Formulaires personnalisés
Ce que fournit le SDK : des API permettant de récupérer des définitions de formulaires configurables (avec plusieurs types de questions) et d'envoyer les réponses des utilisateurs.
Ce que vous allez créer : une UI de formulaire dynamique qui affiche chaque type de question (champs de texte, menus déroulants, etc.), valide les entrées et collecte les réponses à envoyer.
Exemple de mise en œuvre
Ce code récupère les définitions des questions d'un formulaire personnalisé, puis regroupe et envoie les réponses de l'utilisateur.
// Retrieve custom form details
lifecycleScope.launch {
try {
val formDetails = CCAI.chatService?.getCustomFormDetails(formId = 123)
if (formDetails != null) {
Log.d("CCAI", "Form Title: ${formDetails.title ?: "No title"}")
Log.d("CCAI", "Questions: ${formDetails.questions.size}")
// Render the form dynamically based on question types
renderCustomForm(formDetails)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to get custom form details: ${e.message}")
}
}
// Submit a custom form
val request = SubmitCustomFormRequest(
smartActionId = 123,
formResponse = listOf(
SubmitCustomFormRequest.Answer(questionId = 1, value = "John Doe"),
SubmitCustomFormRequest.Answer(questionId = 2, value = "john@example.com")
)
)
lifecycleScope.launch {
try {
CCAI.chatService?.submitCustomForm(request = request)
Log.d("CCAI", "Form submitted successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to submit form: ${e.message}")
}
}
Pour obtenir la définition complète des énumérations CustomFormDetailsResponse, CustomFormQuestion,
CustomFormQuestionOption et CustomFormQuestionType, consultez la documentation de référence de l'API Headless Mobile SDK pour Android.
Pièces jointes initiées par le consommateur (chat)
Les utilisateurs n'ont pas besoin d'attendre qu'un agent le leur demande. Ils peuvent envoyer des images et des vidéos de manière proactive pendant une discussion. Le SDK applique des limites strictes à la taille des fichiers et tente automatiquement de les compresser avant de les transmettre.
Limites de taille des pièces jointes
Images : ≤ 2 Mo
Vidéos : ≤ 20 Mo
Ce que fournit le SDK : un pipeline d'importation sécurisé qui gère la transmission réseau et compresse automatiquement les fichiers pour respecter les limites de CCAI Platform.
Ce que vous allez créer : le bouton en forme de trombone / pièce jointe dans l'UI de votre chat, le sélecteur de photos/vidéos et la bulle de chat visuelle indiquant l'état de l'importation (en cours, envoyée ou échec).
Exemple de mise en œuvre
Lorsqu'un utilisateur sélectionne une photo ou une vidéo dans sa galerie, transmettez le fichier local Uri au SDK à l'aide de la même API sendMessage que celle utilisée pour tous les messages sortants. Pour en savoir plus, consultez Envoyer des messages.
Le SDK gère la compression et l'importation.
// Called when the user picks an image from their photo gallery
fun userDidSelectImage(imageUri: Uri) {
val messageBubble = myChatAdapter.addLoadingImageBubble()
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessage(
OutgoingMessageContent.Photos(
images = listOf(imageUri),
smartAction = null
)
)
messageBubble.markAsSent()
} catch (e: Exception) {
messageBubble.showError("Failed to send: ${e.message}")
}
}
}
Pièces jointes initiées par l'agent (chat)
Les agents peuvent renvoyer des fichiers à l'utilisateur, y compris des images, des vidéos, des extraits audio et des documents (PDF, CSV, etc.).
Types de fichiers compatibles
La plate-forme CCAI Platform accepte 19 types de fichiers répartis dans quatre catégories. Votre UI personnalisée doit gérer chaque catégorie de manière appropriée (aperçu d'image, lecteur vidéo, lecteur audio, téléchargement de document).
| Catégorie | Types compatibles |
|---|---|
| Images | JPEG, PNG, GIF et WebP |
| Vidéo | MP4, MOV, AVI, WMV et WebM. |
| Audio | MP3, WAV, M4A et WEBA. |
| Documents | PDF, DOC, XLS, PPT, CSV et TXT. |
Ce que fournit le SDK : charges utiles des messages de chat entrants fournies à l'aide du message Flow du service de chat, qui incluent les métadonnées des pièces jointes. Consultez SDK mobile sans interface utilisateur pour Android : sujets avancés, localisation et flux post-session. Le service de chat fournit également downloadMedia pour récupérer les contenus multimédias envoyés par les agents de manière sécurisée.
Ce que vous allez créer : l'UI pour afficher ces pièces jointes dans le flux de chat. Votre application décide d'afficher ou non des aperçus d'images, des miniatures de vidéos, des commandes de lecture audio ou des boutons de téléchargement de documents.
Exemple de mise en œuvre
Observez les messages entrants à l'aide de la Flow de message du service de chat. Lorsqu'un nouveau message inclut des métadonnées multimédias, appelez downloadMedia avant d'afficher ou d'ouvrir le fichier.
downloadMedia renvoie DownloadMediaResponse { data: ByteArray, fileName:
String? }, c'est-à-dire les octets entièrement téléchargés, ainsi qu'un nom de fichier à afficher (facultatif). Le SDK ne met pas en cache les téléchargements. Si votre UI revient à une ancienne bulle ou réintègre la vue de chat, votre application est responsable de la mise en cache de l'image décodée ou de la persistance des octets. L'appel de downloadMedia déclenche une autre récupération réseau.
import android.graphics.BitmapFactory
import java.io.File
lifecycleScope.launch {
CCAI.chatService?.messagesReceived?.collect { messages ->
for (message in messages) {
renderIncomingMessage(message)
}
}
}
fun renderIncomingMessage(message: ChatMessage) {
val media = message.media
if (media == null) {
myChatAdapter.insertTextBubble(text = message.text)
return
}
lifecycleScope.launch {
try {
// `downloadMedia` returns the raw bytes plus an optional filename.
val response = CCAI.chatService?.downloadMedia(
mediaId = media.id,
mediaType = media.type
) ?: return@launch
when (media.type) {
DownloadableMediaType.Photo -> {
// Decode in-memory; no disk write needed.
val bitmap = BitmapFactory.decodeByteArray(
response.data, 0, response.data.size
)
myChatAdapter.insertImageBubble(
bitmap = bitmap,
fileName = response.fileName
)
}
DownloadableMediaType.Video,
DownloadableMediaType.Audio,
DownloadableMediaType.Document -> {
// Larger media usually needs an on-disk file for a player
// or document viewer. Write to the app's cache directory
// yourself and hand the resulting File/Uri to your UI.
val file = File(
applicationContext.cacheDir,
response.fileName ?: "attachment"
)
file.writeBytes(response.data)
myChatAdapter.insertAttachmentBubble(
file = file,
mediaType = media.type
)
}
}
} catch (e: Exception) {
myChatAdapter.insertAttachmentErrorBubble(fileName = message.media?.name)
}
}
}
Déviation (hors horaires d'ouverture et pour dépassement de capacité)
La redirection de la demande permet de rediriger les utilisateurs lorsque votre équipe d'assistance n'est pas disponible. Cela se produit généralement dans deux cas :
En dehors des heures d'ouverture : l'utilisateur tente de contacter l'assistance en dehors des heures d'ouverture configurées.
Capacité dépassée : la file d'attente est ouverte, mais le temps d'attente dépasse le seuil configuré par l'administrateur.
Dans une architecture headless, le SDK ne bloque pas automatiquement l'utilisateur ni n'affiche un écran d'erreur. Au lieu de cela, il transmet discrètement l'état de la redirection et les options de remplacement configurées par l'administrateur à votre application. Votre application doit vérifier cet état avant de tenter de démarrer une session.
Gérer les états de déviation
Ce que fournit le SDK : un deflectionFrom: List<QueueMenuDeflection>? sur QueueMenu qui liste les règles de déviation entrantes qui redirigent d'autres files d'attente vers celle-ci. Chaque QueueMenuDeflection ne comporte que deux champs :
deflectionType: DeflectionType?: catégorie de déviation (par exemple, hors horaires d'ouverture ou capacité dépassée).menuId: Int?: ID de la file d'attente source qui est redirigée vers celle-ci.
QueueMenuDeflection ne comporte pas de message à afficher ni de libellé lisible par l'utilisateur. Pour le texte destiné aux utilisateurs (par exemple, Nos bureaux sont fermés),utilisez afterHoursMessage de la file d'attente (dérivé de QueueMenu.settings[].afterHours) ou vos propres chaînes configurées par l'administrateur. Pour le nom à afficher de la file d'attente de destination résolue, recherchez-le dans l'arborescence de menu que vous avez déjà récupérée à l'aide de menuId.
Ce que vous allez créer : la logique permettant de vérifier ces états d'évitement avant de placer un utilisateur dans une file d'attente, ainsi que l'UI personnalisée (par exemple, une alerte en plein écran ou une bannière) qui affiche le message approprié et les boutons de redirection alternatifs.
Exemple de mise en œuvre
Le code suivant intercepte la tentative d'un utilisateur de démarrer une discussion. Il vérifie les métadonnées de déviation du SDK sur QueueMenu et, si une règle est active, affiche un écran de déviation à l'aide de afterHoursMessage de la file d'attente (ou d'une solution de repli).
fun onSupportMenuTapped(queueMenu: QueueMenu) {
// 1. Check if any deflection rules route into this queue.
val deflections = queueMenu.deflectionFrom
if (!deflections.isNullOrEmpty()) {
// Deflection is active - stop routing. Show the custom deflection screen instead.
val message = queueMenu.afterHoursMessage?.text
?: "Support is currently unavailable."
showDeflectionScreen(
message = message,
deflections = deflections
)
} else {
// No deflection rules apply. Safe to proceed to channel selection or directly to chat/call.
startSupportFlow(queueMenu)
}
}
Options de redirection du rendu
Lorsqu'une règle de redirection est active, vos administrateurs configurent probablement d'autres moyens pour l'utilisateur d'obtenir de l'aide. Ils indiquent généralement une autre file d'attente (que vous pouvez résoudre à l'aide de menuId) ou affichent le afterHoursMessage configuré pour la file d'attente actuelle.
Ce que fournit le SDK : liste des entrées QueueMenuDeflection sur deflectionFrom, chacune avec un deflectionType et un menuId.
Ce que vous allez créer : un menu dynamique qui parcourt ces entrées, résout chaque menuId par rapport à l'arborescence de menu déjà récupérée pour obtenir un nom à afficher et génère les boutons appropriés.
Exemple de mise en œuvre
Ce code prend les entrées de déviation et crée un écran de manière dynamique. Il utilise le nom de la file d'attente source (résolu à partir de menuId) comme libellé du bouton, en revenant à deflectionType lorsqu'aucun nom n'est disponible.
fun showDeflectionScreen(message: String, deflections: List<QueueMenuDeflection>) {
// 1. Update your UI with the admin's configured message (or your fallback).
deflectionTitleTextView.text = message
// 2. Loop through the incoming deflection rules and generate buttons.
for (deflection in deflections) {
val sourceMenu = deflection.menuId?.let { findMenuById(it) }
val label = sourceMenu?.name
?: deflection.deflectionType?.name
?: "Alternative option"
val button = MaterialButton(this).apply {
text = label
setOnClickListener {
handleDeflectionAction(deflection)
}
}
menuLinearLayout.addView(button)
}
// 3. Present the compiled screen to the user.
deflectionContainer.isVisible = true
}
// Walk your previously fetched menu tree (for example, by using `QueueMenu.flatten()`) to resolve a `menuId`.
private fun findMenuById(id: Int): QueueMenu? =
cachedRootMenus.flatMap { it.flatten() }.firstOrNull { it.id == id }
deflectionFrom correspond à la liste des règles de déviation qui acheminent les demandes vers ce menu. Le menuId de chaque entrée identifie une file d'attente source qui dévie les demandes vers la file d'attente actuelle. Votre application décide de la manière de les exposer. Certaines intégrations présentent la file d'attente de destination comme c'est ici que nous vous enverrons à la place, tandis que d'autres utilisent simplement la présence d'une règle comme porte d'entrée pour afficher un écran générique réessayez plus tard.