SDK mobile headless per Android: argomenti avanzati, localizzazione e flussi post-sessione

Questo documento mostra come integrare e personalizzare l'SDK mobile headless nella tua app per Android. Tratta argomenti avanzati come la localizzazione, i flussi post-sessione e altri.

Flussi post-sessione (VA post-sessione, sondaggi e CSAT)

Al termine di una chat live o di una chiamata vocale, i contact center spesso vogliono raccogliere feedback o offrire follow-up automatici. La piattaforma CCAI supporta tre tipi di esperienze post-sessione:

  1. Agente virtuale post-sessione: un chatbot automatizzato che continua la conversazione dopo che l'agente umano si è disconnesso.

  2. Sondaggi: un modulo con più domande personalizzato per la coda specifica.

  3. CSAT (soddisfazione del cliente): una valutazione (ad esempio, da 1 a 5 stelle) e un feedback testuale facoltativo.

Applicazione della gerarchia

È normale che gli amministratori configurino regole sovrapposte nel portale (ad esempio, attivando la CSAT a livello globale, ma anche attivando un VA post-sessione per la coda Fatturazione).

Cosa fornisce l'SDK: un motore di risoluzione dei conflitti rigoroso. L'SDK valuta le regole del portale e garantisce che venga attivato un solo flusso post-sessione per sessione, rispettando rigorosamente la seguente gerarchia:

  • Priorità post-sessione: VA post-sessione > Sondaggi > CSAT

    • Esempio: se sia il CSAT sia l'assistente virtuale post-sessione sono attivi per una coda, l'utente vedrà SOLO l'assistente virtuale post-sessione.

Cosa crei: un listener che attende la fine della sessione, chiede all'SDK quale flusso ha vinto la valutazione della gerarchia e disegna la schermata corrispondente. Non devi scrivere una logica if-else complessa per capire quale esperienza vince: l'SDK esegue i calcoli per te.

Esempio di implementazione

Questo snippet mostra come l'app deve ascoltare la fine di una sessione e chiedere all'SDK cosa fare dopo.

// Called when the active chat or call ends
fun handleSessionEnded(postSessionFlow: PostSessionFlow?) {
    // 1. Check if there is a required post-session flow.
    //    The SDK has already calculated the hierarchy (VA > Survey > CSAT).
    val flow = postSessionFlow
    if (flow == null) {
        // No feedback required. Safely close the support UI.
        closeSupportExperience()
        return
    }

    // 2. Route to the winning flow returned by the SDK.
    when (flow.type) {
        PostSessionType.VIRTUAL_AGENT -> transitionToPostSessionVA()
        PostSessionType.SURVEY -> showSurveyScreen(flow)
        PostSessionType.CSAT -> showCSATScreen(flow)
    }
}

Transizione alla fase post-sessione

Indipendentemente dal flusso che vince la gerarchia, devi chiamare due metodi del ciclo di vita condivisi su chatService per segnalare alla piattaforma CCAI Platform che è iniziata la fase post-sessione.

Cosa fornisce l'SDK: metodi del ciclo di vita per la transizione della sessione nella fase post-sessione e durante questa fase (startPostSession, readyPostSession).

Cosa crei: la logica di orchestrazione che chiama questi metodi nei punti corretti del flusso dell'interfaccia utente post-sessione.

Esempio di implementazione

Questo codice fa passare la sessione attraverso le due fasi del ciclo di vita post-sessione.

lifecycleScope.launch {
    // Step 1: Start the post-session process (updates status to "in_progress")
    try {
        val postSessionResponse = CCAI.chatService?.startPostSession()
        if (postSessionResponse != null) {
            Log.d("CCAI", "Post-session started successfully: ${postSessionResponse.id}")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to start post-session: ${e.message}")
    }

    // Step 2: Signal that post-session is ready (updates status to "ready")
    try {
        val readyResponse = CCAI.chatService?.readyPostSession()
        if (readyResponse != null) {
            Log.d("CCAI", "Post-session ready: ${readyResponse.id}")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to prepare post-session: ${e.message}")
    }
}

Dopo che la piattaforma ha riconosciuto la transizione, procedi con questo flusso specifico.

Campi dello stato post-sessione su ChatResponse

Due campi di orchestrazione post-sessione su ChatResponse guidano la logica di transizione:

  • postSessionTransferStatus: String?: lo stato attuale del trasferimento post-sessione (ad esempio "in_progress", "ready", "completed"). La piattaforma utilizza questo campo per la macchina a stati di orchestrazione: startPostSession() lo sposta su "in_progress", readyPostSession() lo sposta su "ready" e osservi questo valore nelle emissioni ChatResponse successive (dal flusso di chat del servizio di chat) per confermare la transizione lato piattaforma.

  • postSessionOptInRequired: Boolean?: indica se l'utente deve attivare esplicitamente il flusso post-sessione prima che venga eseguito. Quando true, mostra un prompt di attivazione nella tua UI e chiama startPostSession() solo dopo che l'utente ha dato il consenso. Quando false o null, il controllo di attivazione non è obbligatorio e puoi passare direttamente al flusso post-sessione.

suspend fun orchestratePostSession(chat: ChatResponse) {
    // 1. Honor opt-in gating if required
    if (chat.postSessionOptInRequired == true) {
        val consented = presentPostSessionOptIn()
        if (!consented) {
            closeSupportExperience()
            return
        }
    }

    // 2. Drive the state machine
    CCAI.chatService?.startPostSession()   // → "in_progress"
    CCAI.chatService?.readyPostSession()   // → "ready"

    // 3. Observe postSessionTransferStatus on subsequent chat updates
    //    to confirm the transfer completed before rendering the winning flow.
}
  • postSessionRequired indica se è configurato qualsiasi flusso post-sessione per la sessione.

  • postSessionOptInRequired indica se è necessario il consenso esplicito dell'utente prima di eseguirlo.

  • postSessionTransferStatus indica a che punto del ciclo di vita della transizione si trova la piattaforma.

Tutti e tre sono indipendenti, quindi controllali sempre.

Agente virtuale post-sessione (priorità più alta)

Cosa fornisce l'SDK: lo stato di transizione per spostare la sessione da un agente umano a un bot automatico e il routing dei messaggi del bot.

Cosa crei: niente di completamente nuovo. Poiché l'assistente virtuale post-sessione funziona come una chat, mantieni aperta l'interfaccia utente della chat personalizzata esistente e lascia che gestisca i messaggi automatizzati in arrivo proprio come ha gestito i messaggi dell'agente umano.

Esempio di implementazione

Se l'SDK segnala una transizione a un VA post-sessione, aggiorna la UI della chat per riflettere la modifica.

fun transitionToPostSessionVA() {
    // Keep your chat UI open, but update the visual context
    myChatAdapter.addSystemMessage(
        "You are now speaking with our Virtual Assistant"
    )
    agentNameTextView.text = "Virtual Assistant"
    agentAvatarImageView.setImageResource(R.drawable.ic_bot_avatar)

    // The SDK will automatically start piping the VA's messages into
    // your standard chat message observer (messagesReceived).
}

Sondaggi (priorità media)

Cosa fornisce l'SDK: rateService espone metodi per recuperare le domande del sondaggio e inviare le risposte per una sessione di comunicazione. Le domande del sondaggio vengono configurate dall'amministratore per ogni coda e possono includere più tipi di domande (CSAT, valutazione a stelle, testo in formato libero, enumerazione, scala numerica).

Cosa crei: un modulo dinamico che recupera il sondaggio, scorre le domande, esegue il rendering dell'interfaccia utente appropriata (ad esempio pulsanti di opzione, valutazioni a stelle o campi di testo) per ciascuna, verifica che i campi obbligatori siano compilati e raggruppa le risposte da inviare.

Tipi di chiave

// Survey response from getSurvey()
data class Survey(
    val id: Int?,
    val lang: String?,
    val questions: List<SurveyQuestion>?,
    val signOffText: String?  // "Thank you" message to show after submission
)

// Request payload for sendSurvey()
data class SurveyRequest(
    val answers: Map<Int, String>  // Maps question IDs to answer values
)

Esempio di implementazione

Questo codice recupera le domande del sondaggio da rateService, le visualizza in un frammento personalizzato e invia le risposte.

fun showSurveyScreen(communicationId: Long) {
    val rateService = CCAI.rateService ?: return

    lifecycleScope.launch {
        try {
            // 1. Fetch the survey questions for this session
            val survey = rateService.getSurvey(
                communicationId = communicationId,
                communicationType = "chats"
            ).getOrThrow()

            // 2. Present your custom survey UI
            val surveyFragment = MyCustomSurveyFragment.newInstance(
                survey.questions.orEmpty()
            )
            surveyFragment.onSubmit = { answersMap ->
                lifecycleScope.launch {
                    try {
                        // 3. Submit the answers
                        val request = SurveyRequest(answers = answersMap)
                        rateService.sendSurvey(
                            communicationId = communicationId,
                            communicationType = "chats",
                            answers = request
                        ).getOrThrow()
                        rateService.clear()
                        closeSupportExperience()
                    } catch (e: Exception) {
                        Log.e("CCAI", "Failed to submit survey: ${e.message}")
                    }
                }
            }
            supportFragmentManager.beginTransaction()
                .replace(R.id.container, surveyFragment)
                .addToBackStack(null)
                .commit()
        } catch (e: Exception) {
            Log.e("CCAI", "Failed to load survey: ${e.message}")
            closeSupportExperience()
        }
    }
}

CSAT - soddisfazione del cliente (priorità più bassa)

Cosa fornisce l'SDK: l'rateService espone un metodo rate() per inviare un punteggio CSAT e un feedback facoltativo per una sessione di comunicazione. La valutazione è in genere un valore intero compreso tra 1 e 5.

Cosa crei:la schermata di valutazione visiva (ad esempio una riga di stelle toccabili o una scala numerica da 1 a 5), un campo di testo se è consentito il feedback e un pulsante Invia.

Tipi di chiave

// Request payload for rate()
data class RatingRequest(
    val rating: Int? = null,      // Typically 1-5
    val feedback: String? = null  // Optional qualitative feedback
)

Esempio di implementazione

Dopo che l'utente tocca una valutazione a stelle e scrive il suo feedback, trasmetti questi valori a rateService.

fun submitCSATFeedback(communicationId: Long, rating: Int, comment: String?) {
    val rateService = CCAI.rateService ?: return
    submitButton.isEnabled = false
    progressBar.isVisible = true

    lifecycleScope.launch {
        try {
            val request = RatingRequest(
                rating = rating,
                feedback = comment?.trim()?.takeIf { it.isNotEmpty() }
            )
            rateService.rate(
                communicationId = communicationId,
                communicationType = "chats",
                request = request
            ).getOrThrow()
            rateService.clear()
            showThankYouScreen()
        } catch (e: Exception) {
            showErrorToast("Failed to submit feedback. Please try again.")
        } finally {
            submitButton.isEnabled = true
            progressBar.isVisible = false
        }
    }
}

Download delle trascrizioni delle chat

Cosa fornisce l'SDK:un metodo per scaricare la trascrizione della chat come file PDF.

Cosa crei: il pulsante di download nell'interfaccia utente e la logica di gestione dei file (salvataggio, condivisione o anteprima).

Esempio di implementazione

Questo codice mostra l'approccio di download della trascrizione di Android: recupera il PDF come file e poi lo salva, lo condivide o ne visualizza l'anteprima nella tua app.

// Download to a file destination
val documentsDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
val transcriptFile = File(documentsDir, "chat_transcript.pdf")

lifecycleScope.launch {
    try {
        val transcriptFile = CCAI.chatService?.downloadChatTranscript()
        if (transcriptFile != null) {
            Log.d("CCAI", "Transcript downloaded to: ${transcriptFile.absolutePath}")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to download transcript: ${e.message}")
    }
}

Il metodo Android downloadChatTranscript() non accetta parametri e restituisce un File? che punta al PDF scaricato. Il metodo downloadChatTranscriptData() (che restituisce i dati PDF non elaborati direttamente in memoria senza salvarli su disco) è disponibile solo su iOS. Se salvi i file nello spazio di archiviazione condiviso, assicurati che la tua app disponga delle autorizzazioni di archiviazione dei file appropriate.

Lingue e localizzazione

Poiché stai creando un'interfaccia utente integrata personalizzata utilizzando l'SDK Headless Mobile, la traduzione dei pulsanti, dei menu e degli avvisi personalizzati della tua app è di tua responsabilità.

Quindi, perché l'SDK deve conoscere la lingua dell'utente?

  1. Routing delle code: la piattaforma CCAI utilizza il codice lingua per indirizzare l'utente a un agente che parla quella lingua.

  2. Messaggi di sistema: i messaggi automatici generati dal backend della piattaforma CCAI (ad esempio "Un operatore sarà presto con te") vengono inviati nella lingua attiva.

API del servizio di traduzione

Cosa fornisce l'SDK: una proprietà languageCode di lettura e scrittura per la lingua SDK attiva.

Cosa crei: un'interfaccia utente per il selettore lingua (se vuoi che gli utenti scelgano manualmente) e la logica per impostare la lingua prima dell'inizio di una sessione.

// Read the active language
val currentLanguage = CCAI.languageService?.languageCode

// Set the active language before the user starts a support session
CCAI.languageService?.languageCode = "es"

L'SDK non espone un elenco fisso di lingue supportate. Se la tua app mostra un selettore lingua, gestisci autonomamente l'elenco di visualizzazione dell'app o utilizza la configurazione dell'istanza o dell'azienda restituita dall'SDK per determinare le lingue da offrire.

Impostazione della lingua dell'SDK (la regola pre-sessione)

Cosa fornisce l'SDK: un LanguageService che confronta la lingua che richiedi con le lingue effettivamente attivate dagli amministratori nel portale della piattaforma CCAI.

Cosa devi creare: se vuoi consentire agli utenti di eseguire l'override manuale della lingua (anziché fare affidamento solo sulle impostazioni predefinite del dispositivo), devi creare l'interfaccia utente di selezione (ad esempio un menu a discesa) e assicurarti che sia accessibile solo prima che tocchino un canale di assistenza.

Esempio di implementazione

Questo codice mostra come eseguire l'override esplicito della lingua dell'SDK utilizzando i codici lingua ISO 639 standard (ad esempio, "es" per lo spagnolo, "fr" per il francese). La lingua preferita viene impostata utilizzando il parametro languageCode durante l'inizializzazione dell'SDK nella classe di dati InitOptions. Puoi impostarlo anche durante la reinizializzazione dell'SDK:

// Set language during initialization
val initOptions = InitOptions(
    key = "YOUR_COMPANY_KEY",
    urlHost = "your_subdomain.ccaiplatform.com",
    languageCode = "es"  // for example, "es" for Spanish, "fr" for French
)

try {
    CCAI.initialize(context = this, options = initOptions)
} catch (e: Exception) {
    Log.e("CCAI", "Failed to initialize CCAI: ${e.message}")
}

Inizializzazione del modulo di chat (Android)

Se inizializzi il modulo di chat utilizzando l'overload che accetta un codice lingua, puoi fornire la lingua anche lì:

val chatOptions = ChatOptions(
    webFormInterface = WebFormManager(),
    downloadTranscriptVisibility = DownloadTranscriptVisibility.SHOW_ALL,
    greeting = "Hello! How can I help you?"
)
CCAI.initializeChat(context = this, options = chatOptions)

Puoi anche passare la lingua quando recuperi i menu della coda:

val menuResult = CCAI.queueMenuService?.get(key = null)

Esegui l'override per sessione utilizzando ChatRequest:

val chatRequest = ChatRequest(
    chat = Chat(
        menuId = menuId,
        languageCode = "fr"  // This session will use French
    )
)

In che modo CCAI Platform risolve la lingua finale

Se non imposti esplicitamente una lingua, l'SDK ha comunque bisogno di un codice lingua per il routing e i messaggi di sistema. Per mantenere un comportamento prevedibile, è consigliabile impostarlo personalmente prima che l'utente avvii una sessione di assistenza.

Utilizza questo ordine di priorità:

  1. Linguaggio esplicito (consigliato): se imposti una lingua in InitOptions.languageCode, l'SDK la utilizza.

  2. Aggiornamento della lingua prima della sessione (ancora esplicito): se imposti CCAI.languageService?.languageCode = "es" prima che l'utente entri in una coda, l'SDK utilizza la lingua aggiornata.

  3. Override per sessione (solo chat): se fornisci ChatRequest(chat = Chat(..., languageCode = "fr")), la sessione di chat utilizza il codice lingua richiesto.

  4. Se non imposti nulla: il comportamento torna all'opzione migliore disponibile sul dispositivo e nella configurazione dell'istanza, come determinato dall'SDK. Per risultati coerenti, evita di fare affidamento su questa impostazione e imposta sempre una lingua in modo esplicito (passaggi 1-3).

Gestione degli errori, fallback e gestione delle sessioni

Anche le app più robuste riscontrano interruzioni delle connessioni, dipendenze mancanti o problemi di autenticazione. L'SDK mobile headless è progettato per rilevare questi problemi della piattaforma sottostanti e mostrarli nella tua app, consentendoti di gestirli in modo controllato all'interno della tua UI personalizzata o di fare affidamento ai fallback automatici di CCAI Platform.

Gestione degli errori SDK

Cosa fornisce l'SDK: due categorie di eccezioni tipizzate, oltre alle eccezioni Kotlin standard:

  • HttpExceptionWithCode: generato da qualsiasi metodo di servizio asincrono quando una richiesta HTTP alla piattaforma CCAI non va a buon fine. Espone una proprietà Int code in modo da poter creare rami in base ai codici di stato (ad esempio, 401 per l'autenticazione scaduta, 429 per limitazione di frequenza, 5xx per gli errori del server).

  • CallCreationError.AfterHours: errore sigillato generato dall'API scheduled-call (callService.createScheduledCall) quando si verifica un conflitto di slot o la finestra di programmazione è chiusa. Contiene un displayMessage localizzato sul server e destinato alla visualizzazione diretta da parte dell'utente.

  • Eccezioni Kotlin o Java standard: gli errori di rete vengono visualizzati come java.io.IOException, l'annullamento della coroutine come kotlinx.coroutines.CancellationException e qualsiasi imprevisto come classe Exception di base.

Cosa crei: un blocco try o catch a più livelli intorno a ogni chiamata di servizio SDK. Rileva prima l'eccezione più specifica (errori sigillati specifici del dominio come CallCreationError.AfterHours, se applicabile, poi HttpExceptionWithCode per la ramificazione dello stato HTTP) e torna a un gestore Exception generico per errori di rete, di sistema e imprevisti.

Esempio di implementazione

Questo codice racchiude una chiamata SDK in un pattern try o catch a più livelli, rilevando prima HttpExceptionWithCode per ramificarsi in base al codice di stato HTTP, per poi ricorrere a un gestore generico per tutto il resto.

import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope

lifecycleScope.launch {
    try {
        CCAI.chatService?.start(chatRequest)
        // Handle success - observe chat state using chatReceived
    } catch (e: HttpExceptionWithCode) {
        // Handle HTTP-level failures from CCAI endpoints - inspect e.code for status code
        Log.e("CCAI", "HTTP ${e.code}: ${e.message}")
        showErrorToast("Something went wrong. Please try again.")
    } catch (e: Exception) {
        // Handle other errors (examples: network and system)
        Log.e("CCAI", "Error: ${e.message}")
        showErrorToast("An unexpected error occurred.")
    }
}

Best practice

  • Esegui il wrapping di ogni chiamata di servizio SDK in un blocco try/catch a più livelli. Utilizza HttpExceptionWithCode per creare rami in base ai codici di stato HTTP. Ad esempio, riattiva ccaiShouldAuthenticate() su 401 (token di autenticazione scaduto), applica un backoff su 429 (limitazione di frequenza) o mostra un errore generico sulle risposte 5xx.

  • Per le API di chiamata pianificata, intercetta CallCreationError.AfterHours prima HttpExceptionWithCode in modo che i conflitti di slot 409 vengano gestiti con il messaggio localizzato sul server.

  • Infine, utilizza l'errore generico Exception come rete di sicurezza per IOException, CancellationException e qualsiasi errore imprevisto. La registrazione dei dettagli dell'eccezione durante lo sviluppo aiuta a individuare rapidamente le cause principali.

Controllare e cancellare le sessioni

Poiché il ciclo di vita della tua app è separato da quello del supporto della piattaforma CCAI, devi gestire lo stato della sessione, in particolare per quanto riguarda i logout degli utenti.

Cosa fornisce l'SDK: API di utilità per verificare se una sessione di assistenza è ancora in corso (anche dopo riavvii o chiusure forzate dell'app) e metodi per cancellare lo stato di autenticazione dell'SDK memorizzato nella cache e associato a un utente finale specifico, impedendo all'utente che esegue l'accesso successivo di ereditare la sessione dell'utente precedente.

Cosa crei: controlli pre-volo prima di tentare di avviare nuove sessioni e routine di pulizia che vengono attivate quando un utente esce dall'app principale.

Esempio di implementazione (pulizia al momento della disconnessione)

Termina qualsiasi sessione attiva, cancella il token di autenticazione memorizzato nella cache dall'SDK, cancella i dati locali dell'app e passa alla schermata di accesso. Si tratta della pulizia minima richiesta per impedire l'ereditarietà della sessione al momento della disconnessione dell'utente.

fun handleUserLogout() {
    lifecycleScope.launch {
        // 1. Check for an in-progress session and end it cleanly
        try {
            val activeChat = CCAI.chatService?.getLastChatInProgress()
            if (activeChat != null) {
                CCAI.chatService?.endChat()
                Log.d("CCAI", "Ended active chat before logout.")
            }
        } catch (e: Exception) {
            // Session may not exist - that's fine
            Log.w("CCAI", "No active session to end: ${e.message}")
        }

        // 2. Clear the cached auth token
        //    (prevents the next user from inheriting the previous session)
        CCAI.authService?.updateAuthToken(null)

        // 3. Wipe your own app's user data
        MyAuthManager.clearLocalData()

        // 4. Navigate back to the login screen
        navigateToLoginScreen()
    }
}

Integrazione di coroutine e Flow di Kotlin

L'SDK Headless Mobile per Android è progettato intorno alle coroutine Kotlin e a Flow come pattern reattivi principali. Si tratta dell'equivalente Android dell'integrazione Combine dell'SDK per iOS.

Cosa fornisce l'SDK: flussi Kotlin Flow e funzioni di sospensione che emettono eventi SDK in tempo reale (messaggi in arrivo, modifiche dello stato di condivisione dello schermo) come flussi reattivi osservabili.

Cosa crei: integrazioni basate su coroutine che raccolgono questi flussi e aggiornano la tua UI Android di conseguenza.

Sospendi funzioni

Molti metodi di servizio SDK sono funzioni di sospensione Kotlin, il che significa che possono essere chiamati dall'interno di un ambito di coroutine e verranno sospesi (non bloccati) durante l'attesa di un risultato.

import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope

// In an Activity or Fragment
lifecycleScope.launch {
    try {
        // These are suspend functions - they suspend, not block
        val menuResult = CCAI.queueMenuService?.get()
        CCAI.chatService?.start(chatRequest)
        val company = CCAI.companyService?.get()
    } catch (e: Exception) {
        handleError(e)
    }
}

Flussi del servizio di chat

chatService espone le proprietà SharedFlow Kotlin per osservare gli eventi della chat in tempo reale. Raccoglili all'interno di un ambito di coroutine consapevole del ciclo di vita (lifecycleScope o viewModelScope).

Flow Emette
messagesReceived Elenchi di messaggi di chat in arrivo (da agenti, assistenti virtuali o dal sistema).
typingEvent Quando un utente o un agente sta digitando.
memberEvent Quando un utente o un agente entra o esce dalla chat (inclusi i trasferimenti e le disconnessioni degli agenti).
stateChanged Modifiche dello stato del fornitore di chat (connessione, connesso, disconnesso, errore).
chatReceived Dati delle risposte della chat quando una chat viene ricevuta o aggiornata (inclusi cambiamenti di stato, assegnazione dell'agente e stato post-sessione).

chatService espone flussi aggiuntivi per eventi di trasferimento (transferEventSubject), risultati del controllo dello stato (checkStatusSubject) e risultati dell'invio di messaggi (messageSendResultSubject).

Esempio di implementazione

import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged

// 1. Incoming messages
lifecycleScope.launch {
    CCAI.chatService?.messagesReceived?.collect { messages ->
        for (message in messages) {
            renderIncomingMessage(message)
        }
    }
}

// 2. Typing indicators
lifecycleScope.launch {
    CCAI.chatService?.typingEvent?.collect { event ->
        when (event) {
            is ChatTypingEvent.Started -> showTypingIndicator()
            is ChatTypingEvent.Ended -> hideTypingIndicator()
        }
    }
}

// 3. Member join/leave
lifecycleScope.launch {
    CCAI.chatService?.memberEvent?.collect { event ->
        when (event) {
            is MemberEvent.Joined -> Log.d("Chat", "Member joined")
            is MemberEvent.Left -> Log.d("Chat", "Member left")
        }
        // Refresh chat status after member changes
        try { CCAI.chatService?.checkStatus() } catch (_: Exception) {}
    }
}

// 4. Chat provider state changes
lifecycleScope.launch {
    CCAI.chatService?.stateChanged?.distinctUntilChanged()?.collect { state ->
        // Update UI based on connection state (examples: connecting, connected)
        updateConnectionStatus(state)
    }
}

// 5. Chat metadata updates (agent assignment, status changes)
lifecycleScope.launch {
    CCAI.chatService?.chatReceived?.collect { chat ->
        Log.d("Chat", "Received chat update: ${chat.id}")
        updateAgentInfo(chat)
        handlePostSessionIfNeeded(chat)
    }
}

Stato della condivisione schermo

Cosa fornisce l'SDK: lo stato della sessione di condivisione dello schermo viene fornito tramite il callback onSessionStateChanged su ScreenShareCallbacks, che passi a ScreenShareManager.startSession(). Consulta SDK mobile headless per Android: voce, condivisione dello schermo, chiamata pianificata ed email. Il callback viene attivato ogni volta che la sessione passa da INACTIVE, PENDING e ACTIVE.

Cosa crei: un gestore collegato a ScreenShareCallbacks.onSessionStateChanged che aggiorna la UI man mano che la sessione passa da uno stato all'altro, oltre a una proprietà locale (ad esempio currentScreenShareSessionState) se devi leggere lo stato attuale al di fuori del callback.

Esempio di implementazione

Oltre al percorso ScreenShareCallbacks, ScreenShareManager espone anche API addStateChangeListener / removeStateChangeListener autonome e un accessor getSessionState() sincrono. Utilizzali quando devi osservare lo stato di un componente diverso da quello che ha chiamato startSession() o per eseguire una lettura non reattiva una tantum. A differenza del percorso ScreenShareCallbacks, in cui il callback viene rilasciato automaticamente al termine della sessione, i listener autonomi non vengono rilasciati automaticamente. Chiama removeStateChangeListener(...) nello smontaggio del proprietario del ciclo di vita per evitare perdite.

// Register a state change listener
val listener: (ScreenShareSessionState) -> Unit = { state ->
    when (state) {
        ScreenShareSessionState.INACTIVE -> Log.d("ScreenShare", "No active session")
        ScreenShareSessionState.PENDING -> Log.d("ScreenShare", "Session pending")
        ScreenShareSessionState.ACTIVE -> Log.d("ScreenShare", "Session active")
    }
}
ScreenShareManager.addStateChangeListener(listener)

// Get the current state directly (not reactive)
val currentState = ScreenShareManager.getSessionState()

// Clean up when done
ScreenShareManager.removeStateChangeListener(listener)

Dati personalizzati

Cosa fornisce l'SDK: un meccanismo per allegare metadati strutturati alle sessioni di chat, come JWT firmati (verificati dal backend) o come payload non firmati (impostati direttamente nel client).

Cosa crei: la logica di raccolta dei dati nella tua app e, per i dati firmati, l'endpoint di firma JWT sul tuo server di backend (consulta la sezione SDK Headless Mobile per Android: Guida introduttiva per lo schema completo di autenticazione e payload JWT).

Dati personalizzati firmati

I dati personalizzati firmati utilizzano un token JWT firmato con il segreto della tua azienda sul server di backend. Questo è l'approccio sicuro per il trasferimento di dati sensibili o attendibili.

Esempio di implementazione

Crea un oggetto dati personalizzato firmato da un JWT generato nel backend.

// JWT signed with company secret (signing must happen on your backend)
val customData = CustomData(signed = "header.payload.signature")

Dati personalizzati non firmati

I dati personalizzati non firmati vengono impostati direttamente nel client utilizzando CustomDataPayload e CustomDataItem.

Esempio di implementazione

Crea un payload non firmato con elementi di dati visibili e nascosti all'agente. CustomDataPayload accetta un MutableMap<String, CustomDataItem?>, quindi utilizza l'assegnazione di pedice (o mutableMapOf) per aggiungere ogni elemento per chiave. La chiave che scegli (ad esempio, "user_name" o "account_tier") diventa il nome del campo visualizzato dall'agente e per il routing/Analytics.

val items = mutableMapOf<String, CustomDataItem?>()
items["user_name"] = CustomDataItem(
    label = "Name",
    value = "John"
)
items["user_age"] = CustomDataItem(
    label = "Age",
    value = 30,
    type = CustomDataType.NUMBER,
    invisibleToAgent = true
)
val payload = CustomDataPayload(items = items)
val customData = CustomData(unsigned = payload)

Gli elementi di dati personalizzati supportano le seguenti proprietà:

  • label: l'etichetta da visualizzare mostrata all'agente.

  • value: il valore dei dati (esempi: String, Int, Bool).

  • type: tipo di dati (STRING, NUMBER, DATE, URL, BOOLEAN).

  • invisibleToAgent: quando true, i dati sono nascosti agli agenti, ma disponibili per il routing e l'analisi.

Utilizzo di dati personalizzati nelle richieste di chat

Allega dati personalizzati all'avvio di una sessione di chat.

Esempio di implementazione

val chatRequest = ChatRequest(
    chat = Chat(
        menuId = 123,
        languageCode = "en",
        customData = customData
    )
)

lifecycleScope.launch {
    try {
        CCAI.chatService?.start(chatRequest)
        // Chat started - observe chat state using chatReceived
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to start chat: ${e.message}")
    }
}

Trasferimento della chat esterna

Cosa fornisce l'SDK: una struttura di dati (ExternalChatTransfer) per trasferire il nome di un agente precedente e la trascrizione della conversazione in una nuova sessione di CCAI Platform.

Cosa crei: il flusso di migrazione che estrae il contesto dal sistema di chat esterno e lo pacchettizza per l'SDK.

Esempio di implementazione

Questo codice crea un oggetto di trasferimento della chat esterna con il nome dell'agente precedente e lo associa ai dati personalizzati della sessione.

// ExternalChatAgent also accepts an optional `avatar: String? = null` parameter
// for displaying the previous agent's avatar, for example:
//     ExternalChatAgent(name = "John Smith", avatar = "https://cdn.example.com/avatars/jsmith.png")
val transfer = ExternalChatTransfer(
    agent = ExternalChatAgent(name = "John Smith"),
    transcript = emptyList()
)

// Attach to your custom data payload
val payload = CustomDataPayload()
payload.externalChatTransfer = transfer

val customData = CustomData(unsigned = payload)

È utile quando si esegue la migrazione di un utente da un sistema di chat di terze parti a una sessione basata su CCAI Platform, mantenendo il contesto della conversazione per l'agente ricevente.

Considerazioni sui thread

L'SDK mobile headless per Android è progettato per funzionare con le coroutine Kotlin ed è in gran parte sicuro per il thread principale. Tuttavia, esistono importanti considerazioni sul threading per le integrazioni headless.

Sicurezza del thread principale

Tutti i metodi SDK sono thread-safe e possono essere chiamati da qualsiasi thread. La maggior parte delle chiamate di servizio SDK (ad esempio chatService?.start(), queueMenuService?.get()) sono funzioni di sospensione che gestiscono internamente i propri thread: sospendono la coroutine chiamante senza bloccare il thread. Puoi chiamarli in sicurezza dal thread principale all'interno di un ambito di coroutine:

// Safe to call from the main thread
lifecycleScope.launch {
    val menu = CCAI.queueMenuService?.get()  // Suspends, does not block
    updateUi(menu)  // Back on main thread
}

CCAIDelegate e thread in background

Il metodo ccaiShouldAuthenticate() in CCAIDelegate è una funzione di sospensione, quindi puoi eseguire in modo sicuro chiamate di rete al suo interno:

override suspend fun ccaiShouldAuthenticate(): String? {
    // This runs in a coroutine context - safe to call suspend functions
    return withContext(Dispatchers.IO) {
        MyBackendApi.getSignedJwt()  // Network call on IO dispatcher
    }
}

Aggiornamenti della UI dai callback dell'SDK

Quando osservi gli eventi SDK utilizzando Flow, assicurati che gli aggiornamenti dell'interfaccia utente avvengano sul thread principale. Se utilizzi lifecycleScope, questa operazione viene gestita automaticamente:

// lifecycleScope dispatches to Main by default
lifecycleScope.launch {
    chatMessagesFlow.collect { messages ->
        // Already on Main - safe to update UI
        chatAdapter.submitList(messages)
    }
}

Se raccogli da un dispatcher non principale, torna indietro in modo esplicito:

lifecycleScope.launch(Dispatchers.IO) {
    // Processing on IO thread
    val processed = heavyProcessing()
    withContext(Dispatchers.Main) {
        // Update UI on Main thread
        updateChatUi(processed)
    }
}

Threading di inizializzazione

CCAI.initialize() e le chiamate di inizializzazione dei moduli correlati, come initializeChat(), initializeCall() e initializeScreenShare(), devono essere chiamate nel thread principale del tuo Application.onCreate(). Si tratta di chiamate sincrone che configurano lo stato interno dell'SDK.

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()  // Always on main thread

        // All initialization must happen on main thread
        CCAI.initialize(context = this, options = initOptions)
        CCAI.initializeChat(context = this, options = ChatOptions())
        CCAI.initializeCall(context = this)
    }
}

Gestione della memoria

Una corretta gestione della memoria è essenziale per evitare perdite e garantire la pulizia delle risorse dell'SDK.

Ciclo di vita di coroutine e Flow

Annulla sempre i riferimenti alle coroutine Job e i raccoglitori di flussi nei metodi del ciclo di vita di Android (onDestroy() per le attività, onCleared() per i ViewModel). In caso contrario, i riferimenti possono rimanere attivi più a lungo del previsto, causando perdite di memoria.

import kotlinx.coroutines.Job

class ChatViewModel : ViewModel() {
    private var chatJob: Job? = null

    fun observeChatMessages() {
        chatJob = viewModelScope.launch {
            CCAI.chatService?.messagesReceived?.collect { messages ->
                // Handle new messages
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        chatJob?.cancel()
    }
}

Riferimenti a listener e callback

Utilizza riferimenti deboli per i listener e i callback per evitare cicli di conservazione. Cancella tutti i callback registrati quando l'attività o il ViewModel viene eliminato.

class ChatActivity : AppCompatActivity() {
    private var chatJob: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        chatJob = lifecycleScope.launch {
            CCAI.chatService?.messagesReceived?.collect { messages ->
                updateChatUI(messages)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        chatJob?.cancel()
    }
}

Best practice

  • Memorizza i riferimenti alle coroutine Job: memorizza sempre i riferimenti Job restituiti da launch {} per evitare coroutine incontrollate che sopravvivono al loro ambito.

  • Liberare spazio nei metodi del ciclo di vita: annulla tutti i job e cancella i callback in onDestroy() (attività) o onCleared() (ViewModel).

  • Utilizza riferimenti deboli per i listener: fai attenzione alle espressioni lambda che acquisiscono implicitamente this (il contesto di Activity o Fragment contenitore), in quanto ciò può impedire la garbage collection.

  • I servizi sono gestiti dall'SDK: non devi rilasciare manualmente i servizi SDK, che vengono gestiti dall'oggetto singleton CCAI.

Risoluzione dei problemi

Questa sezione tratta i problemi di integrazione comuni e le relative soluzioni.

Problemi di build e dipendenze

Questa sezione tratta i problemi comuni di build e dipendenze.

Problema

Gradle non riesce a risolvere com.ccaiplatform.android:CCAIKit.

  • Causa: il repository Maven di CCAI non è presente o utilizza l'URL errato.

  • Correzione: assicurati che il tuo settings.gradle.kts includa:

    maven { url = uri("https://sdk.ujet.co/ccaip/android/") }
    

    Prendi nota del segmento di percorso /ccaip/. L'URL legacy (https://sdk.ujet.co/android/) non risolve gli artefatti.

Problema

Errori di compilazione richiesti da Java 17.

  • Causa: questo SDK richiede la compatibilità con Java 17.

  • Correzione: aggiorna il tuo build.gradle.kts:

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
    

Problema

chatLibraryNotFound (codice di errore 1100) durante il runtime.

  • Causa: il modulo CCAIChatRed non è presente nelle dipendenze.

  • Correzione: sia CCAIChat che CCAIChatRed sono necessari per la chat. Aggiungi:

    implementation("com.ccaiplatform.android:CCAIChat:ccaiVersion")
    implementation("com.ccaiplatform.android:CCAIChatRed:ccaiVersion")
    

Problemi di inizializzazione

Questa sezione tratta i problemi di inizializzazione più comuni.

Problema

NullPointerException quando accedi a CCAI.chatService.

  • Causa: CCAI.initializeChat() non è stato chiamato o è stato chiamato prima di CCAI.initialize().

  • Correzione: assicurati che l'ordine di inizializzazione in Application.onCreate() sia corretto:

    1. CCAI.initialize(context, options = initOptions)

    2. CCAI.initializeChat(context, options = chatOptions)

    3. CCAI.initializeCall(context) (se utilizzi le chiamate vocali o programmate)

    4. CCAI.initializeScreenShare(context, options) (se utilizzi la condivisione schermo)

Problema

I servizi SDK restituiscono null dopo l'inizializzazione.

  • Causa: CCAI.initialize() non è stato chiamato nel thread principale oppure la classe Application non è registrata nel manifest.

  • Correzione: verifica che la riga android:name=".MainApplication" sia presente nel file AndroidManifest.xml e che tutte le chiamate init siano in onCreate().

Problemi di autenticazione

Questa sezione tratta i problemi di autenticazione comuni.

Problema

L'autenticazione non riesce in modo silenzioso o ccaiShouldAuthenticate() non viene mai chiamato.

  • Causa: CCAIDelegate non è stato impostato in InitOptions.

  • Correzione: assicurati che delegate venga passato a InitOptions:

    val initOptions = InitOptions(
        key = "YOUR_KEY",
        urlHost = "your_subdomain.ccaiplatform.com",
        delegate = MyCCAIDelegate()  // Must not be null
    )
    

Problema

Errori di convalida JWT (codice di errore 101).

  • Causa: il JWT è scaduto, non è valido o è firmato con il secret errato.

  • Correzione: verifica che il backend:

    • Utilizza il codice segreto dell'azienda corretto.

    • Imposta timestamp iat e exp validi.

    • Restituisce una stringa JWT formattata correttamente.

Problemi relativi alle notifiche push

Questa sezione tratta i problemi comuni relativi alle notifiche push.

Problema

Le notifiche push non vengono ricevute o gestite.

  • Causa: il token FCM non è registrato con l'SDK, il servizio di messaggistica Firebase non è registrato o i payload di dati in entrata non vengono inoltrati all'SDK.

  • Correzione:

    • Chiama CCAI.pushNotificationService?.updatePushToken(token) sia in onNewToken() sia all'avvio.

    • Inoltra remoteMessage.data a CCAI.pushNotificationService?.handlePushNotification(remoteMessage.data) dal servizio di messaggistica Firebase.

    • Verifica che google-services.json sia nel modulo dell'app.

    • Verifica che la chiave del service account Firebase sia configurata nel portale di amministrazione della piattaforma CCAI.

    • Esegui il rendering di qualsiasi notifica visibile o UI di chiamata in arrivo nel codice della tua app.

Problema

L'SDK non reagisce alle notifiche push in arrivo (i messaggi vengono ignorati).

  • Causa: passaggio del tipo di dati errato al metodo handlePushNotification().

  • Correzione: assicurati di passare remoteMessage.data (che è un Map<String, String>), non l'intero oggetto RemoteMessage.

Problemi con la sessione di chat

Questa sezione tratta i problemi comuni relativi alle sessioni di chat.

Problema

chatService?.start() genera un'eccezione o restituisce null.

  • Causa: menuId non valido, autenticazione non riuscita o problema di rete.

  • Correzione:

    • Verifica che menuId corrisponda a una coda valida nel portale amministratore.

    • Verifica che ccaiShouldAuthenticate() scambi un JWT firmato valido tramite CCAI.authService?.authenticate(jwt) e restituisca il token di autenticazione risultante.

    • Assicurati che il dispositivo abbia una connettività di rete.

    • Racchiudi la chiamata in un blocco try o catch, poi registra i dettagli dell'eccezione.

Problema

I messaggi di chat non vengono visualizzati nella UI.

  • Causa: mancata osservanza del flusso di messaggi del servizio di chat.

  • Correzione: assicurati di raccogliere i dati dal flusso del servizio di chat in un ambito sensibile al ciclo di vita (vedi la sezione Flussi del servizio di chat sopra).

Problemi di condivisione schermo

Questa sezione tratta i problemi comuni di condivisione dello schermo.

Problema

La condivisione dello schermo non viene avviata dopo che l'utente ha dato il consenso.

  • Causa: modulo CCAIScreenShare non inizializzato o Android MediaProjection non avviato.

  • Correzione:

    • Assicurati che CCAI.initializeScreenShare() venga chiamato con ScreenShareOptions validi.

    • Verifica di avviare correttamente MediaProjectionManager e il servizio in primo piano di Android dopo il consenso.

    • Su Android 14 e versioni successive, assicurati che il tipo di servizio in primo piano includa mediaProjection.

Suggerimenti generali per il debug

  • Attiva il logging dettagliato durante lo sviluppo per visualizzare lo stato interno dell'SDK.

  • Controlla il portale amministratore della piattaforma CCAI per verificare la configurazione delle code, l'attivazione dei canali e l'orario di apertura.

  • Monitora il traffico di rete con strumenti come Charles Proxy o Network Inspector di Android Studio per verificare le chiamate API a *.ccaiplatform.com.

  • Verifica che le versioni dei moduli corrispondano: tutti i moduli com.ccaiplatform.android devono utilizzare lo stesso numero di versione per evitare problemi di compatibilità.

Tempo di wrap up della serie

Congratulazioni, hai completato la serie di cinque parti sull'SDK mobile headless per Android. La tua integrazione ora copre:

Per un riferimento continuo, aggiungi ai preferiti la documentazione di riferimento dell'API Headless Mobile SDK - Android e tieni d'occhio le note di rilascio della piattaforma CCAI per gli aggiornamenti dell'SDK.