SDK headless para dispositivos móveis para Android: ações inteligentes, anexos e redução

Este documento mostra como integrar e personalizar o SDK Headless Mobile no seu app Android. Ele aborda as ações inteligentes, os anexos e o desvio.

Configurar ações e anexos inteligentes

Com as ações inteligentes, os agentes e consumidores podem trocar informações avançadas e estruturadas, como fotos, vídeos, trechos de texto e verificação biométrica.

Na arquitetura sem comando, o SDK atua estritamente como a camada de transporte segura. Ele informa quando uma ação é necessária ou solicitada e transporta os dados com segurança para a plataforma CCAI Platform. Como você é proprietário da interface do usuário, seu app é totalmente responsável por criar as telas da câmera, os seletores de fotos, as caixas de texto e por acionar os comandos biométricos integrados do Android.

Ações inteligentes pré-sessão

As ações pré-sessão coletam contexto (como um número da conta ou um vídeo de um produto quebrado) antes que o usuário entre na fila, reduzindo significativamente o tempo de atendimento do agente.

O que o SDK oferece: as regras e os requisitos de uma fila específica (por exemplo, se a verificação biométrica, o envio de fotos ou a entrada de texto são opcionais ou obrigatórios antes de entrar).

O que você cria: as telas da UI pré-sessão (câmera, entrada de texto e solicitações biométricas) que aparecem entre o momento em que o usuário toca em uma fila e entra na sala de espera, além da lógica de cache em disco para armazenar mídia com segurança até que a sessão seja conectada.

Exemplo de implementação

Esse código verifica a configuração do SDK para saber se a fila escolhida tem requisitos de pré-sessão. Ele demonstra como lidar com um requisito de vídeo mantendo uma referência de arquivo Uri no disco em vez de manter o vídeo na memória.

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

Ler configuração de ação inteligente pré-sessão

O SDK expõe a configuração de ações inteligentes pré-sessão por dois caminhos complementares. Verifique os dois para determinar se a captura pré-sessão é necessária para uma determinada fila e canal.

Caminho A: nível da fila (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
}

Caminho B: no nível do canal (VoiceCallChannel.preSessionSmartAction)

// Usage - "does the voice channel specifically require pre-session capture?"
val requiresPreSessionOnVoice =
    menu.channels.voiceCall?.preSessionSmartAction == true

Use o Caminho A para decidir se uma restrição pré-sessão será mostrada antes de o usuário escolher um canal. Use o Caminho B para refinamentos específicos do canal depois que um canal é selecionado (por exemplo, apenas solicitando antes de uma chamada de voz instantânea).

Ações inteligentes na sessão (iniciadas pelo agente)

Durante um chat ativo ou uma ligação, um representante pode clicar em um botão no painel dele para pedir dados ao usuário (por exemplo, Envie uma foto do seu documento de identidade ou Autentique-se com biometria).

O que o SDK oferece: eventos em tempo real alertando o app de que o agente solicitou um tipo específico de ação inteligente.

O que você vai criar: um alerta no chat ou na chamada notificando o usuário da solicitação, iniciando a ferramenta integrada apropriada (câmera, biblioteca de fotos, campo de texto ou BiometricPrompt do Android) e transmitindo o resultado de volta ao SDK.

Exemplo de implementação

Aqui, o app detecta eventos do SDK. Quando ele detecta uma solicitação de agente, mostra um alerta e abre a ferramenta Android apropriada.

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

As ações inteligentes iniciadas pelo agente chegam pelos eventos do SDK da sessão ativa. Inscreva-se nos eventos de chat ou chamada relevantes antes de iniciar a sessão para que seu app possa apresentar o comando adequado ao cliente assim que um pedido chegar.

Notificar e acompanhar ações inteligentes

Use smartActionService para reconhecer e atualizar as ações inteligentes à medida que o usuário avança nelas.

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

Se a ação pedir entrada de texto, colete o texto na UI e envie-o pelo serviço de ação inteligente antes de marcar a ação como concluída.

Formulários da Web

O que o SDK oferece: APIs para buscar, mostrar e validar formulários da Web acionados por agentes durante uma sessão de chat.

O que você vai criar: a UI de renderização do formulário da Web e o fluxo de envio que empacota as respostas do usuário.

Exemplo de implementação

Esse código busca um formulário da Web pelo ID externo e pelo ID da ação inteligente e valida a resposta antes do envio.

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

Formulários personalizados

O que o SDK oferece: APIs para recuperar definições de formulários configuráveis (com vários tipos de perguntas) e enviar as respostas do usuário.

O que você vai criar: uma UI dinâmica de formulário que renderiza cada tipo de pergunta (campos de texto, menus suspensos etc.), valida a entrada e coleta respostas para envio.

Exemplo de implementação

Esse código recupera as definições de perguntas de um formulário personalizado, empacota e envia as respostas do usuário.

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

Para conferir as definições completas de enumeração CustomFormDetailsResponse, CustomFormQuestion, CustomFormQuestionOption e CustomFormQuestionType, consulte a Referência da API do SDK móvel sem interface gráfica — Android.

Anexos iniciados pelo consumidor (chat)

Os usuários não precisam esperar que um representante faça perguntas. Eles podem enviar imagens e vídeos de forma proativa durante um chat. O SDK impõe limites estritos de tamanho de arquivo e tenta fazer a compactação automaticamente antes da transmissão.

Limites de tamanho dos anexos

  • Imagens: ≤ 2 MB

  • Vídeos: ≤ 20 MB

O que o SDK oferece: um pipeline de upload seguro que processa a transmissão de rede e compacta automaticamente os arquivos para atender aos limites da CCAI Platform.

O que você vai criar: o botão de clipe de papel / anexo na UI do chat, o seletor de fotos / vídeos e o balão de chat visual mostrando o estado do upload (carregando, enviado ou falha).

Exemplo de implementação

Quando um usuário selecionar uma foto ou um vídeo na galeria, transmita o arquivo local Uri para o SDK usando a mesma API sendMessage usada para todas as mensagens enviadas. Para mais informações, consulte Enviar mensagens.

O SDK processa a compactação e o upload.

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

Anexos iniciados pelo agente (chat)

Os agentes podem enviar arquivos de volta para o usuário, incluindo imagens, vídeos, clipes de áudio e documentos (PDFs, CSVs etc.).

Tipos de arquivos compatíveis

A plataforma CCAI aceita 19 tipos de arquivos em quatro categorias. Sua UI personalizada precisa processar cada categoria de maneira adequada (prévia de imagem, player de vídeo, player de áudio e download de documentos).

Categoria Tipos compatíveis
Imagens JPEG, PNG, GIF e WebP.
Vídeo MP4, MOV, AVI, WMV e WebM.
Áudio MP3, WAV, M4A e WEBA.
Documentos PDF, DOC, XLS, PPT, CSV e TXT.

O que o SDK oferece: payloads de mensagens de chat recebidas entregues usando o Flow de mensagens do serviço de chat que incluem metadados de anexos. Consulte SDK móvel sem interface gráfica para Android: tópicos avançados, localização e fluxos pós-sessão. O serviço de chat também oferece downloadMedia para recuperar mídia enviada pelo agente de forma segura.

O que você vai criar: a interface para renderizar esses anexos no feed de chat. Seu app decide se vai mostrar prévias de imagens, miniaturas de vídeos, controles de reprodução de áudio ou botões de download de documentos.

Exemplo de implementação

Observe as mensagens recebidas usando o Flow de mensagem do serviço de chat. Quando uma nova mensagem incluir metadados de mídia, chame downloadMedia antes de renderizar ou abrir o arquivo.

downloadMedia retorna DownloadMediaResponse { data: ByteArray, fileName: String? }: os bytes totalmente baixados mais um nome de arquivo de exibição opcional. O SDK não armazena downloads em cache. Se a interface rolar de volta para uma bolha mais antiga ou entrar novamente na visualização de chat, seu app será responsável por armazenar em cache a imagem decodificada ou manter os bytes. Chamar downloadMedia novamente aciona outra busca de rede.

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

Desvio (após o horário de expediente e excesso de capacidade)

O desvio redireciona os usuários quando sua equipe de suporte não está disponível. Isso geralmente acontece em dois cenários:

  • Fora do horário de expediente: o usuário tenta entrar em contato com o suporte fora do horário de funcionamento configurado.

  • Excesso de capacidade: a fila está aberta, mas o tempo de espera excede o limite configurado pelo administrador.

Em uma arquitetura sem interface, o SDK não bloqueia automaticamente o usuário nem mostra uma tela de erro. Em vez disso, ele transmite silenciosamente o status de rejeição e as opções de substituição configuradas pelo administrador para seu app, que precisa verificar esse status antes de tentar iniciar uma sessão.

Processar estados de evasão

O que o SDK oferece: um deflectionFrom: List<QueueMenuDeflection>? em QueueMenu que lista as regras de redirecionamento de entrada, roteando outras filas para esta. Cada QueueMenuDeflection tem apenas dois campos:

  • deflectionType: DeflectionType?: a categoria de rejeição (por exemplo, fora do horário comercial ou acima da capacidade).

  • menuId: Int?: o ID da fila de origem que faz o desvio para esta.

O QueueMenuDeflection não tem uma mensagem de exibição nem um rótulo legível por humanos. Para a cópia voltada ao usuário (por exemplo, Nosso escritório está fechado),use afterHoursMessage da fila (derivado de QueueMenu.settings[].afterHours) ou suas próprias strings configuradas pelo administrador. Para o nome de exibição da fila de destino resolvida, procure na árvore de menus já buscada por menuId.

O que você cria: a lógica para verificar esses estados de evasão antes de colocar um usuário em uma fila e a UI personalizada (como um alerta em tela cheia ou um banner) que mostra a mensagem apropriada e os botões de encaminhamento alternativo.

Exemplo de implementação

O código a seguir intercepta a tentativa de um usuário de iniciar uma conversa. Ele verifica os metadados de evasão do SDK no QueueMenu e, se uma regra estiver ativa, apresenta uma tela de evasão usando o afterHoursMessage da fila (ou um substituto).

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

Renderizar opções de evasão

Quando uma regra de prevenção está ativa, os administradores provavelmente configuram outras maneiras de o usuário receber ajuda, geralmente indicando outra fila (que pode ser resolvida usando menuId) ou mostrando o afterHoursMessage configurado da fila atual.

O que o SDK oferece: a lista de entradas QueueMenuDeflection em deflectionFrom, cada uma com um deflectionType e um menuId.

O que você vai criar: um menu dinâmico que faz um loop nessas entradas, resolve cada menuId na árvore de menus já buscada para receber um nome de exibição e gera os botões apropriados.

Exemplo de implementação

Esse código usa as entradas de deflexão e cria uma tela de forma dinâmica. Ele usa o nome da fila de origem (resolvido de menuId) como o rótulo do botão, voltando para deflectionType quando um nome não está disponível.

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 é a lista de regras de desvio que encaminham para esse menu. O menuId de cada entrada identifica uma fila de origem que desvia para a fila atual. O app decide como expor esses dados. Algumas integrações apresentam a fila de destino como aqui é onde vamos enviar você, enquanto outras usam apenas a presença de qualquer regra como um portão para mostrar uma tela genérica de tente de novo mais tarde.