Questo documento spiega come integrare e personalizzare l'SDK Headless Mobile nella tua app per Android. Include la voce, la condivisione dello schermo, le chiamate programmate e l'email.
Chiamata vocale
L'SDK Headless Mobile per Android fornisce il supporto per le chiamate vocali tramite i moduli
CCAICall e CCAICallRed. La tua app è proprietaria della UI di chiamata personalizzata, mentre
l'SDK gestisce la creazione di chiamate, la gestione delle chiamate in arrivo, l'integrazione del provider e
lo stato del ciclo di vita delle chiamate.
Cosa fornisce l'SDK: API di servizi di chiamata per avviare chiamate istantanee, avviare la segreteria, gestire le chiamate in arrivo e osservare lo stato delle chiamate.
Cosa crei: l'esperienza di chiamata visiva, inclusi il punto di ingresso pre-chiamata, la richiesta di consenso alla registrazione, gli stati di chiamata o attesa, i controlli durante la chiamata, l'interfaccia utente del servizio in primo piano e qualsiasi navigazione post-chiamata.
Inizializza il modulo di chiamata
Inizializza il modulo di chiamata dopo aver inizializzato l'SDK principale:
import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaicall.CallOptions
import com.ccaiplatform.ccaicall.initializeCall
CCAI.initialize(context = applicationContext, options = initOptions)
CCAI.initializeCall(
context = applicationContext,
options = CallOptions()
)
Ispeziona le funzionalità di VoiceCallChannel
Prima di offrire una chiamata immediata, esamina VoiceCallChannel della coda per
decidere quali punti di ingresso sono esposti (chiamata immediata, fallback della segreteria, deviazione a livello di canale) e se è necessario il consenso alla registrazione.
Campi chiave VoiceCallChannel
instantEnabled:Boolean: indica se per questa coda è consentita una chiamata vocale live (istantanea). Considerafalsecome "non offrire un pulsante di chiamata istantanea". A differenza divoiceCall != null, che ti indica solo se la coda ha un canale vocale.preSessionSmartAction:Boolean: indica se sono richieste azioni intelligenti pre-sessione su questo canale specifico prima di iniziare una chiamata. Consulta Azioni intelligenti pre-sessione in SDK Headless Mobile per Android: azioni intelligenti, allegati e deflezione per il percorso complementare a livello di coda.scheduleEnabled:Boolean: indica se le chiamate pianificate sono supportate. Consulta la sezione Chiamate programmate.recordingOption:RecordingOption?, il modello di consenso alla registrazione delle chiamate. Vedi Registrare il consenso. A differenza diRecordingPermission.NOT_ASKED, che viene utilizzato solo con le chiamate pianificate.voicemailReason:String?. Questo campo viene compilato quando il canale è configurato per il routing alla segreteria.phoneNumber:String?: numero di fallback PSTN configurato per il trasferimento o la segreteria.scheduleDeflectionType:String?/deflected:Boolean?/deflectedReason:String?: metadati di deviazione a livello di canale. Quandodeflected == true, mostra il messaggiodeflectedReasonconfigurato dall'amministratore e offri il fallback configurato (ad esempio, un numero di telefono da chiamare o un punto di accesso per le chiamate pianificate) anziché tentare una chiamata istantanea. Consulta la sezione Riduzione del tempo di attesa in Headless Mobile SDK per Android: Azioni intelligenti, allegati e riduzione per la gestione della riduzione a livello di coda.
fun voiceEntryPoints(menu: QueueMenu) {
val voice = menu.channels.voiceCall
if (voice == null) {
callNowButton.isVisible = false
voicemailButton.isVisible = false
return
}
// Instant call - only when explicitly enabled and not deflected
callNowButton.isVisible = voice.instantEnabled && voice.deflected != true
// Voicemail fallback
val number = voice.phoneNumber
if (voice.voicemailReason != null && number != null) {
voicemailButton.isVisible = true
voicemailButton.text = "Leave a voicemail"
voicemailButton.setOnClickListener {
dialVoicemail(number, reason = voice.voicemailReason)
}
}
// Channel-level deflection
if (voice.deflected == true) {
showChannelDeflection(reason = voice.deflectedReason)
}
}
Consenso alla registrazione
Cosa fornisce l'SDK: un campo recordingOption: RecordingOption? su
VoiceCallChannel che descrive il modello di consenso alla registrazione delle chiamate configurato
dagli amministratori nel portale.
Cosa crei: la richiesta di consenso (quando ASK_USER) e la logica di ramificazione
che rispetta la scelta dell'amministratore.
enum class RecordingOption {
ALWAYS, // Calls are always recorded - surface a disclosure before connecting
NEVER, // Calls are never recorded - no disclosure needed
ASK_USER // Prompt the user for consent before the call connects
}
fun handleRecordingConsent(voice: VoiceCallChannel, onResult: (Boolean) -> Unit) {
when (voice.recordingOption) {
RecordingOption.ALWAYS -> {
showRecordingDisclosure("This call will be recorded.")
onResult(true)
}
RecordingOption.NEVER, null -> onResult(true)
RecordingOption.ASK_USER -> presentConsentPrompt { granted ->
onResult(granted)
}
}
}
Avviare una chiamata istantanea
Prima di avviare una chiamata istantanea:
Esamina
VoiceCallChanneldella coda selezionata.Verifica che le chiamate istantanee siano attive e non deviate.
Gestisci il consenso alla registrazione in base ai
recordingOptiondella coda.Richiedi l'autorizzazione microfono.
Chiama il
startInstantCall.
suspend fun initiateInstantCall(queueMenu: QueueMenu) {
val voice = queueMenu.channels.voiceCall ?: return
if (!voice.instantEnabled || voice.deflected == true) {
return
}
val recordingPermission = resolveRecordingPermission(voice)
requestMicrophonePermissionIfNeeded()
try {
CCAI.callService?.startInstantCall(
menuId = queueMenu.id,
recordingPermission = recordingPermission
)
} catch (e: Exception) {
showCallStartError(e)
}
}
Osservare lo stato della chiamata
Raccogli gli eventi del servizio di chiamata prima di avviare o accettare una chiamata:
val service = CCAI.callService ?: return
lifecycleScope.launch {
service.stateChanged.collect { state ->
updateCallConnectionState(state)
}
}
lifecycleScope.launch {
service.callReceived.collect { call ->
updateCallDetails(call)
}
}
lifecycleScope.launch {
service.incomingCallEvent.collect { event ->
handleIncomingCallEvent(event)
}
}
lifecycleScope.launch {
service.waitTimeUpdated.collect { waitTime ->
updateEstimatedWaitTime(waitTime)
}
}
lifecycleScope.launch {
service.participantUpdated.collect { participant ->
updateParticipantInfo(participant)
}
}
lifecycleScope.launch {
service.interruptionDetected.collect { interruption ->
// A PSTN interruption (for example, an incoming cellular call)
// has paused the active CCAI call. Surface a "call interrupted"
// banner and offer a Resume action after the interruption ends.
showInterruptionBanner(interruption)
}
}
lifecycleScope.launch {
service.deflectionOffered.collect { deflection ->
// The platform offered a wait-time deflection (for example, a
// scheduled callback or PSTN fallback). Render the prompt and
// honor the user's choice.
presentWaitTimeDeflection(deflection)
}
}
Stato del provider e stato del server
stateChanged e callReceived.status sono indicatori indipendenti: leggili come
colonne, non righe. stateChanged del provider segnala l'handshake VoIP locale; callReceived.status lato server segnala il ciclo di vita della chiamata sulla piattaforma. Attiva l'opzione "Connesso" e avvia il timer della chiamata da
callReceived.status, non da stateChanged quando raggiunge lo stato connesso.
| Indicatore | Cosa viene segnalato | Cosa dedurre |
|---|---|---|
stateChanged (fornitore) |
Handshake VoIP locale - Connecting, Connected (local), Disconnected, Error(...). |
(Facoltativo) Indicatore di caricamento pre-stato prima dell'arrivo dello stato del server, della visualizzazione dell'errore e della pulizia di fine chiamata. |
callReceived.status (server) |
Ciclo di vita lato server: WAITING, CONNECTING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED. |
Chiamata con flip, avvio del timer di chiamata, UI assegnata dall'agente, overlay di trasferimento o riassegnazione e flusso di fine chiamata. |
Chiamate in arrivo
Le chiamate vocali in arrivo vengono recapitate al dispositivo tramite FCM. Poiché le chiamate in arrivo possono arrivare mentre la tua app è in background o completamente chiusa, la tua integrazione deve essere pronta a riceverle da qualsiasi stato del processo.
Cosa fornisce l'SDK: un flusso IncomingCallEvent su callService che
emette il ciclo di vita di una chiamata in arrivo: Arrived, Accepted, Rejected. Il
servizio di chiamata espone anche acceptIncomingCall() e rejectIncomingCall() per
agire sull'offerta.
Cosa crei: la UI delle chiamate in arrivo (schermata di squillo a schermo intero, presentazione nella schermata di blocco o banner in-app) e i gestori che accettano o rifiutano l'offerta.
Esempio di implementazione
Raccogli incomingCallEvent e visualizza o chiudi
la tua UI in ogni evento del ciclo di vita, quindi accetta o rifiuta da un ambito di lunga durata.
Definisci applicationScope una volta nella classe Application in modo che le coroutine di accettazione o rifiuto
sopravvivano a qualsiasi smontaggio Activity di breve durata durante l'handshake.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
// Define this in your Application class (or another long-lived singleton).
// Expose it however your app prefers as top-level property, dependency
// injection, or a static field on the Application subclass.
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
lifecycleScope.launch {
CCAI.callService?.incomingCallEvent?.collect { event ->
when (event) {
is IncomingCallEvent.Arrived -> showIncomingCallUi(event.call)
is IncomingCallEvent.Accepted -> navigateToCallScreen(event.call)
is IncomingCallEvent.Rejected -> dismissIncomingCallUi()
}
}
}
// User taps "Accept" in your custom UI.
fun onAcceptTapped() {
// Important: accept/reject helpers suspend internally. Launch them on
// `applicationScope` (defined above) so the call survives if the
// ringing Activity is dismissed during the handshake.
applicationScope.launch {
CCAI.callService?.acceptIncomingCall()
}
}
fun onRejectTapped() {
applicationScope.launch {
CCAI.callService?.rejectIncomingCall()
}
}
Collegamento di FCM all'interfaccia utente delle chiamate in arrivo
Le chiamate in arrivo inviate da FCM arrivano su un thread in background, spesso prima che esista un Activity. Utilizza un timeout breve per collegare il payload FCM alla prima
emissione di IncomingCallEvent.Arrived, quindi avvia la tua UI.
Esempio di implementazione
Sospendi la navigazione al prossimo IncomingCallEvent.Arrived con
withTimeoutOrNull, poi passa il turno al navigatore.
suspend fun handleIncomingCallPush() {
val arrived = withTimeoutOrNull(60_000L) {
CCAI.callService?.incomingCallEvent
?.filterIsInstance<IncomingCallEvent.Arrived>()
?.first()
} ?: return
// `myNavigator` is the `IncomingCallUiNavigator` implementation you wire
// up during app initialization (see "Alternative: full-screen ringing
// Activity" below). Hold the reference somewhere long-lived (for
// example, on your `Application` subclass) so it can be reused here.
myNavigator.showRingingUi(applicationContext, arrived.call.id)
}
Integrazione del framework per le telecomunicazioni (consigliata)
Per un'esperienza di chiamata in arrivo Android integrata, con suoneria a schermo intero, presentazione nella schermata di blocco e integrazione con il registro chiamate di sistema, registra un PhoneAccount e lascia che il framework Telecom gestisca l'esperienza utente della suoneria.
Cosa fornisce l'SDK: un helper TelecomCallService che registra un
PhoneAccount per la tua app (registerPhoneAccount(context)), un
TelecomConnectionService per collegare le chiamate CCAI in entrata ad Android Telecom e un'interfaccia TelecomConnectionCallback per i callback dello stato della connessione.
Cosa crei: la registrazione PhoneAccount una tantum durante l'inizializzazione dell'app e una dichiarazione del manifest per il servizio di connessione.
Esempio di implementazione
Registra l'account del telefono all'avvio dell'app, quindi dichiara il servizio di connessione e l'autorizzazione nel manifest.
// In Application.onCreate(), after CCAI.initializeCall(...)
TelecomCallService.registerPhoneAccount(context = this)
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<service
android:name="com.ccaiplatform.android.call.TelecomConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
Alternativa: attività di chiamata a schermo intero
Se preferisci eseguire il rendering della tua schermata di chiamata anziché utilizzare il framework Telecom, l'SDK espone IncomingCallService insieme all'interfaccia IncomingCallUiNavigator. Implementa l'interfaccia, poi passa l'implementazione a IncomingCallService durante l'inizializzazione dell'app.
Esempio di implementazione
Implementa IncomingCallUiNavigator e passalo a
IncomingCallService una volta durante Application.onCreate().
// 1. Implement the navigator. Override the three interface methods to
// declare the ringing Activity intent and to show/hide your UI as the
// call lifecycle changes.
class MyNavigator : IncomingCallUiNavigator {
override fun ringingActivityIntent(
context: Context,
callId: Long,
callerName: String?
): Intent = Intent(context, RingingActivity::class.java).apply {
putExtra("callId", callId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
override fun showRingingUi(context: Context, callId: Long) {
// Optional: surface an in-app banner or push your ringing UI.
}
override fun hideRingingUi(context: Context, callId: Long) {
// Optional: dismiss whatever you surfaced in showRingingUi.
}
}
// 2. Wire it up during app initialization (typically in Application.onCreate()).
val incomingCallService = IncomingCallService(
applicationContext,
MyNavigator()
)
if (!CCAI.canShowIncomingCallFullScreen(context)) {
CCAI.openFullScreenIntentSettings(context)
}
Servizio in primo piano per la suoneria attiva
Mentre una chiamata squilla, Android potrebbe terminare i processi in background. L'SDK include un
IncomingRingingService che mantiene attivo il flusso di chiamata come servizio in primo piano. Dichiaralo nel manifest e l'SDK lo avvia e lo arresta
automaticamente all'arrivo delle chiamate in entrata.
<service
android:name="com.ccaiplatform.android.call.IncomingRingingService"
android:foregroundServiceType="phoneCall"
android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
Stato della chiamata e stati lato server
Cosa fornisce l'SDK: un enum CallStatus basato sul server in ogni payload Call che descrive la fase del ciclo di vita della chiamata (ad esempio, CONNECTING, WAITING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED, FAILED).
Cosa crei: la mappatura da CallStatus agli stati visibili ai clienti
nella schermata di chiamata.
Esempio di implementazione
when-branch on call.status from each
callReceived emission and render the matching UI state:
fun updateCallDetails(call: Call) {
when (call.status) {
CallStatus.CONNECTING -> renderConnectingState()
CallStatus.WAITING -> renderWaitingInQueueState(call.estimatedWait)
CallStatus.CONNECTED -> renderConnectedState(call.agent)
CallStatus.ON_HOLD -> renderOnHoldState()
CallStatus.INTERRUPTED -> renderInterruptedState()
CallStatus.ENDED, CallStatus.FAILED -> renderEndedState(call.endReason)
}
}
Controlli durante la chiamata (disattivazione audio, altoparlante, attesa)
Cosa fornisce l'SDK: proprietà di sola lettura su callService: isMuted,
isSpeakerEnabled, isOnHold, insieme alle azioni per attivare/disattivare ogni stato
(setMuted, setSpeakerEnabled, setOnHold).
Cosa crei: i pulsanti Muto, Altoparlante e Attesa nell'interfaccia utente durante la chiamata, collegati alle azioni dell'SDK e che riflettono lo stato attuale.
Esempio di implementazione
Leggi lo stato attuale da callService per
inizializzare i pulsanti, quindi attiva/disattiva e aggiorna con un tocco.
muteButton.isSelected = CCAI.callService?.isMuted == true
speakerButton.isSelected = CCAI.callService?.isSpeakerEnabled == true
holdButton.isSelected = CCAI.callService?.isOnHold == true
fun onMuteTapped() {
val next = CCAI.callService?.isMuted != true
CCAI.callService?.setMuted(next)
muteButton.isSelected = next
}
fun onSpeakerTapped() {
val next = CCAI.callService?.isSpeakerEnabled != true
CCAI.callService?.setSpeakerEnabled(next)
speakerButton.isSelected = next
}
fun onHoldTapped() {
val next = CCAI.callService?.isOnHold != true
CCAI.callService?.setOnHold(next)
holdButton.isSelected = next
}
Riprendere una chiamata in corso dopo un'interruzione
Quando un'interruzione della rete PSTN (ad esempio una chiamata cellulare in arrivo) mette in pausa la chiamata
CCAI attiva, l'SDK mantiene attiva la sessione ed emette interruptionDetected.
Al termine dell'interruzione, l'app può riprendere la chiamata.
Cosa fornisce l'SDK: getLastCallInProgress() per cercare una chiamata in corso (anche dopo il riavvio dell'app) e resumeCall(call) per riportarla in primo piano.
Cosa crei: la funzionalità di ripresa nell'interfaccia utente (in genere un banner o una ripresa automatica al termine dell'interruzione) e la navigazione per tornare alla schermata della chiamata.
Esempio di implementazione
Cerca la chiamata in corso con
getLastCallInProgress, riprendila e torna alla schermata della chiamata.
lifecycleScope.launch {
val call = CCAI.callService?.getLastCallInProgress() ?: return@launch
try {
CCAI.callService?.resumeCall(call)
navigateToCallScreen(call)
} catch (e: Exception) {
showCallResumeError(e)
}
}
Riassegnazione da un agente virtuale
Cosa fornisce l'SDK: canEscalate(allowSkipVirtualAgent: Boolean) è un
metodo su CallResponse (il modello emesso da callService.callReceived), non
su callService stesso. Indica se la chiamata attuale è idonea per
il trasferimento a un operatore umano in base allo stato attuale dell'agente virtuale della chiamata.
Cosa crei: mantieni l'emissione callReceived più recente come
"chiamata corrente", quindi associa la visibilità del pulsante "Parla con una persona" a
currentCall?.canEscalate(...). L'argomento allowSkipVirtualAgent è un valore di configurazione del menu che recuperi separatamente dalla risposta alla chiamata stessa. Non codificare in modo permanente true in produzione.
Esempio di implementazione
Mantieni l'ultima Call in una proprietà, quindi richiama
canEscalate. Risolvi allowSkipVirtualAgent dalla configurazione del menu.
// Held from the most recent `callReceived` emission
private var currentCall: Call? = null
// `allowSkipVirtualAgent` is fetched separately from menu configuration
// it is not derived from the CallResponse.
val allowSkip = menuConfiguration.allowSkipVirtualAgent
val allowed = currentCall?.canEscalate(allowSkipVirtualAgent = allowSkip) == true
talkToAgentButton.isVisible = allowed
Segreteria
Cosa fornisce l'SDK: startVoicemail(VoicemailRequest) su callService,
più un'enumerazione VoicemailReason che descrive il motivo per cui la piattaforma ha offerto la segreteria
telefonica (ad esempio, fallback dopo l'orario di lavoro, fallback per capacità eccessiva o reindirizzamento
temporaneo).
Cosa crei: il punto di accesso alla segreteria nella tua UI (in genere mostrato quando
VoiceCallChannel.voicemailReason è compilato o quando il ciclo di vita della chiamata
si risolve in un risultato di segreteria) e l'UI di registrazione stessa.
Esempio di implementazione
Mostra il pulsante della segreteria quando voicemailReason
è impostato, poi chiama startVoicemail con un VoicemailRequest.
enum class VoicemailReason {
AFTER_HOUR_DEFLECTION,
OVER_CAPACITY_DEFLECTION,
TEMPORARY_REDIRECTION
}
suspend fun offerVoicemail(menu: QueueMenu) {
val voice = menu.channels.voiceCall ?: return
if (voice.voicemailReason == null) return
try {
CCAI.callService?.startVoicemail(
VoicemailRequest(menuId = menu.id)
)
} catch (e: Exception) {
showVoicemailError(e)
}
}
Riduzione dei tempi di attesa
Quando la coda è aperta, ma il tempo di attesa supera la soglia configurata dall'amministratore, la piattaforma può offrire un percorso alternativo, in genere un richiamo programmato o un trasferimento alla rete PSTN.
Cosa fornisce l'SDK: un payload CallDeflection che descrive l'alternativa offerta, recuperabile utilizzando getCallDeflection(callId) su callService. La
piattaforma emette lo stesso payload tramite il flusso deflectionOffered quando
una deviazione diventa disponibile a metà attesa. Un flag CompanyResponse.preventDirectPstnCall
separato indica se la composizione
PSTN diretta deve essere nascosta nell'interfaccia utente di fallback.
Cosa crei: il prompt di deviazione (ad esempio "L'attesa stimata è di 12 minuti. Vuoi programmare una richiamata?") e la logica di ramificazione che rispetta la scelta dell'utente.
Esempio di implementazione
Recupera la deviazione offerta con l'ID della chiamata attiva, combinala con CompanyResponse.preventDirectPstnCall e presenta la richiesta.
lifecycleScope.launch {
val currentCall = CCAI.callService?.getLastCallInProgress()
?: return@launch
val deflection = CCAI.callService?.getCallDeflection(callId = currentCall.id)
?: return@launch
val company = CCAI.companyService?.get()
val allowDirectPstn = company?.preventDirectPstnCall != true
presentWaitTimeDeflection(deflection, allowDirectPstn)
}
Avanzata CallOptions
CallOptions espone un piccolo insieme di pulsanti di attivazione/disattivazione per le app che devono ottimizzare l'esperienza di chiamata predefinita.
connectingPollIntervalMs:Long: la frequenza con cui l'SDK esegue il polling della piattaforma mentre la chiamata è in stato di connessione.connectedPollIntervalMs:Long: la frequenza con cui l'SDK esegue il polling della piattaforma mentre la chiamata è connessa.endCallMaxRetries:Int: il numero di volte in cui l'SDK riprova la richiesta di terminazione della chiamata prima di mostrare un errore.endCallRetryDelayMs:Long: ritardo tra i tentativi di chiamata.
Esempio di implementazione
Passa i valori ottimizzati in CallOptions durante l'inizializzazione del modulo di chiamata:
CCAI.initializeCall(
context = this,
options = CallOptions(
connectingPollIntervalMs = 2_000L,
connectedPollIntervalMs = 5_000L,
endCallMaxRetries = 3,
endCallRetryDelayMs = 1_000L
)
)
Condivisione schermo
La condivisione dello schermo consente agli agenti dell'assistenza di visualizzare lo schermo dell'utente per aiutarlo a risolvere i problemi. La piattaforma CCAI supporta due modalità di condivisione schermo:
Condivisione in-app: l'agente può vedere solo cosa succede all'interno della tua app specifica.
Condivisione completa del dispositivo (facoltativa): l'agente può vedere l'intero dispositivo dell'utente, inclusa la schermata Home di Android e altre app. Questa operazione richiede una configurazione avanzata di Android che utilizza
MediaProjectione un servizio in primo piano.
Cosa fornisce l'SDK: il modulo CCAIScreenShare che crea e gestisce
le sessioni, gestisce le modifiche dello stato ed elabora le richieste di consenso.
Cosa crei: la configurazione di inizializzazione, le richieste della UI per il consenso dell'utente e (se supporti l'intero dispositivo) l'integrazione MediaProjection integrata di Android con un servizio in primo piano.
Il servizio di condivisione schermo fornisce metodi per l'intero ciclo di vita della sessione: startSession, activateSession, stopSession, oltre a metodi di configurazione per il controllo remoto e la condivisione dell'intero dispositivo. Le sottosezioni successive illustrano
il flusso di integrazione principale.
Per le firme complete dei metodi e i dettagli dei parametri per tutti i metodi di condivisione dello schermo, consulta il Riferimento API dell'SDK mobile headless per Android.
Inizializzare e configurare la condivisione schermo
Inizializza il servizio di condivisione schermo durante la configurazione dell'app fornendo la chiave di condivisione schermo:
import com.ccaiplatform.android.CCAI
import com.ccaiplatform.android.ScreenShareOptions
// Minimal - domain defaults to your instance's configured provider
CCAI.initializeScreenShare(
context = this,
screenShareOptions = ScreenShareOptions(
key = "YOUR_SCREEN_SHARE_KEY"
)
)
// If your instance or provider requires an explicit domain:
// CCAI.initializeScreenShare(
// context = this,
// screenShareOptions = ScreenShareOptions(
// key = "YOUR_SCREEN_SHARE_KEY",
// domain = "your_subdomain.ccaiplatform.com"
// )
// )
Verifica l'idoneità alla condivisione dello schermo
Prima di attivare la funzionalità di condivisione schermo, verifica se è disponibile per la chat corrente:
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
fun isScreenShareEnabled(chat: ChatResponse): Boolean {
return CCAI.screenShareService != null
&& chat.supportScreenShare == true
}
Gestire le richieste di condivisione schermo
Implementa la gestione delle richieste di condivisione schermo utilizzando ScreenShareManager. Crea una
sessione e gestisci la risposta asincrona utilizzando ScreenShareCallbacks:
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
import com.ccaiplatform.ccaiscreenshare.ScreenShareCallbacks
fun requestScreenShare(chatId: String, isFromRemote: Boolean) {
val screenShareService = CCAI.screenShareService
if (screenShareService == null) {
Log.e("ScreenShare", "Screen share service not available")
return
}
// Track whether this session is agent-initiated
screenShareInitiatedFrom = if (isFromRemote) "agent" else "endUser"
if (!isFromRemote) {
sendScreenShareMessage(event = "screenShareRequestedFromEndUser")
}
val callbacks = ScreenShareCallbacks(
onSessionStateChanged = { state ->
Log.d("ScreenShare", "State changed: $state")
handleScreenShareStateChange(state)
},
onSessionCreationError = { error ->
Log.e("ScreenShare", "Session creation failed: ${error.message}")
sendScreenShareMessage(event = "screenShareFailed")
showErrorToast("Screen share failed to start.")
},
onSessionActivationRequest = {
Log.d("ScreenShare", "Activation request received")
// For agent-initiated sessions, activate immediately
// For user-initiated sessions, show a confirmation dialog first
if (screenShareInitiatedFrom == "agent") {
ScreenShareManager.activateSession()
} else {
showScreenShareConsentDialog {
ScreenShareManager.activateSession()
}
}
}
)
ScreenShareManager.startSession(
request = ScreenShareRequest(
communicationId = chatId,
communicationType = CommunicationType.Chat,
initiatedFrom = if (isFromRemote) ScreenShareFrom.AGENT else ScreenShareFrom.END_USER
),
callbacks = callbacks
)
}
Gestire le modifiche dello stato di condivisione schermo
Ascolta le modifiche dello stato della sessione di condivisione dello schermo tramite il callback onSessionStateChanged
fornito a ScreenShareCallbacks:
fun handleScreenShareStateChange(state: ScreenShareSessionState) {
currentScreenShareSessionState = state
when (state) {
ScreenShareSessionState.INACTIVE -> {
Log.d("ScreenShare", "No active session")
}
ScreenShareSessionState.PENDING -> {
Log.d("ScreenShare", "Session pending - waiting for activation")
// For agent-initiated sessions, activate immediately
if (screenShareInitiatedFrom == "agent") {
ScreenShareManager.activateSession()
}
}
ScreenShareSessionState.ACTIVE -> {
Log.d("ScreenShare", "Screen share active")
sendScreenShareMessage(event = "screenShareStarted")
updateUiForActiveScreenShare()
}
}
}
Riferimento ScreenShareCallbacks
| Callback | Descrizione |
|---|---|
onSessionStateChanged |
Chiamato quando lo stato della sessione di condivisione dello schermo cambia (INACTIVE, PENDING, ACTIVE). |
onSessionCreationError |
Chiamato quando la creazione della sessione non va a buon fine (problema di rete, servizio non disponibile e così via). |
onSessionActivationRequest |
Chiamato quando la sessione è pronta per l'attivazione. Chiama ScreenShareManager.activateSession() per iniziare la condivisione. Per le sessioni avviate dall'utente, mostra prima un prompt di consenso; per le sessioni avviate dall'agente, attiva immediatamente. |
onSessionRemoteControlRequest |
Chiamato quando l'agente richiede il controllo remoto dello schermo dell'utente. Mostra una richiesta di consenso prima di concedere il controllo remoto. |
onSessionFullDeviceRequest | Chiamato quando l'agente
richiede la condivisione dell'intero dispositivo (vedi Condivisione dello schermo dell'intero dispositivo
di seguito). Attiva il flusso di consenso per MediaProjection nella
risposta. |
onSessionDidSucceed |
Chiamato dopo che una sessione viene attivata correttamente e inizia lo streaming. Utilizza questo valore per aggiornare l'UI allo stato "Condivisione schermo attiva". |
Per le definizioni complete dell'enumerazione dello stato e tutti i tipi associati, consulta la documentazione di riferimento dell'API Headless Mobile SDK per Android.
Condivisione schermo dell'intero dispositivo (avanzata)
La condivisione dello schermo dell'intero dispositivo consente agli agenti di visualizzare le schermate delle applicazioni
al di fuori della tua, incluse le impostazioni di sistema e la navigazione tra le applicazioni.
Per farlo, è necessario integrare l'API MediaProjection di Android ed eseguire un
servizio in primo piano.
Dichiarare le autorizzazioni e il servizio in primo piano
Aggiungi le autorizzazioni richieste e la dichiarazione del servizio in primo piano al tuo
AndroidManifest.xml:
<manifest>
<!-- Required for screen capture -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<application>
<service
android:name=".screenshare.ScreenShareForegroundService"
android:foregroundServiceType="mediaProjection"
android:exported="false" />
</application>
</manifest>
Richiedere il consenso per MediaProjection
Prima di acquisire lo schermo, devi chiedere il consenso all'utente utilizzando
MediaProjectionManager:
import android.media.projection.MediaProjectionManager
import android.content.Context
private val mediaProjectionManager by lazy {
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
}
fun requestScreenCapturePermission() {
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
screenCaptureResultLauncher.launch(captureIntent)
}
// Handle the result in your Activity
private val screenCaptureResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
// User granted screen capture permission
startScreenShareForegroundService(result.resultCode, result.data!!)
} else {
Log.d("ScreenShare", "User denied screen capture permission")
}
}
Implementare il servizio in primo piano
Crea un servizio in primo piano che contenga la sessione MediaProjection:
import android.app.Service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.IBinder
class ScreenShareForegroundService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
startForeground(NOTIFICATION_ID, notification)
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Screen Share",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Active screen share session"
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
private fun buildNotification(): Notification {
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("Screen Sharing")
.setContentText("An agent is viewing your screen.")
.setSmallIcon(R.drawable.ic_screen_share)
.build()
}
companion object {
private const val CHANNEL_ID = "screen_share_channel"
private const val NOTIFICATION_ID = 1001
}
}
Attivare la condivisione dell'intero dispositivo
Una volta attivato MediaProjection, abilita la condivisione dell'intero dispositivo tramite il servizio di condivisione
dello schermo:
// After MediaProjection consent is granted and foreground service is started
ScreenShareManager.enableFullDeviceSharing(true)
Considerazioni su Jetpack Compose per la condivisione schermo
Approcci consigliati per l'interazione remota in Jetpack Compose
Racchiudi gli elementi interattivi chiave con le visualizzazioni Android: per le aree che devono essere cliccabili da remoto, racchiudile come controlli di visualizzazione Android standard (ad esempio
android.widget.Button) utilizzando il composableAndroidView. In questo modo, i clic remoti funzionano in modo affidabile tramite il percorso basato sulla visualizzazione Android.Inoltra tocchi remoti con gestione personalizzata dei tocchi: per le aree di composizione che non possono essere sostituite con le visualizzazioni, utilizza i callback touch personalizzati per inoltrare gli eventi touch remoti alla logica di Compose.
Fornisci indicazioni o fallback a livello di prodotto: indica chiaramente nelle schermate di composizione che "Gli agenti possono solo visualizzare; l'utente deve toccare localmente" o limita automaticamente le autorizzazioni di controllo remoto su queste pagine mantenendo l'accesso di sola visualizzazione.
Esempio di implementazione del wrapper della visualizzazione Android
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
import android.widget.Button
@Composable
fun RemoteControlButton(
title: String,
onClick: () -> Unit
) {
AndroidView(
factory = { context ->
Button(context).apply {
text = title
setOnClickListener { onClick() }
}
},
update = { button ->
button.text = title
}
)
}
// Usage in a Composable
@Composable
fun ScreenShareSupportScreen() {
Column {
Text("This screen supports remote control")
RemoteControlButton(title = "Clickable Button") {
println("Button tapped remotely or locally")
}
}
}
Chiamate pianificate
Le chiamate pianificate consentono agli utenti di prenotare una chiamata vocale futura anziché attendere in coda una chiamata immediata. Su Android, l'SDK espone i metodi di servizio di chiamata per controllare la disponibilità, recuperare le fasce orarie, prenotare chiamate, riprogrammare chiamate, annullare chiamate e gestire gli errori di pianificazione.
Rilevamento delle funzionalità
Cosa fornisce l'SDK: un flag scheduleEnabled su VoiceCallChannel che
indica se la coda selezionata supporta la prenotazione di chiamate future.
Cosa crei: la logica per controllare questo flag prima di mostrare il punto di ingresso della pianificazione.
Esempio di implementazione
Controlla scheduleEnabled sul canale di chiamata vocale della coda. Non utilizzare voiceCall != null come proxy: una coda può supportare
chiamate vocali istantanee senza supportare le chiamate pianificate e viceversa.
fun setupScheduledCallEntryPoint(queueMenu: QueueMenu) {
val voiceChannel = queueMenu.channels.voiceCall
if (voiceChannel == null) {
callNowButton.isVisible = false
scheduleCallButton.isVisible = false
return
}
callNowButton.isVisible = voiceChannel.instantEnabled && voiceChannel.deflected != true
scheduleCallButton.isVisible = voiceChannel.scheduleEnabled == true
}
Controllo facoltativo della disponibilità
Prima di mostrare il selettore di fasce orarie, puoi verificare se la coda selezionata dispone di slot di pianificazione disponibili.
Esempio di implementazione
Utilizza hasScheduledTimeSlots(menuId:) come
controllo preliminare leggero. Se restituisce false, mostra un'opzione di stato vuoto o di riserva
anziché aprire il selettore.
lifecycleScope.launch {
try {
val available = CCAI.callService?.hasScheduledTimeSlots(menuId = menu.id) == true
if (available) {
showScheduleCallUI()
} else {
showNoSlotsMessage()
}
} catch (e: Exception) {
Log.e("CCAI", "Availability check failed: ${e.message}")
showNoSlotsMessage()
}
}
Recupera fasce orarie
Recupera gli orari disponibili per gli appuntamenti per la coda selezionata. L'SDK restituisce valori di data che la tua app può visualizzare nel fuso orario locale dell'utente.
Cosa fornisce l'SDK: timestamp degli slot disponibili per la coda selezionata, con supporto facoltativo per la riprogrammazione di una chiamata pianificata esistente.
Cosa crei: il calendario, il selettore dell'ora, la presentazione del fuso orario, la gestione dello stato vuoto e il comportamento di riprova.
Esempio di implementazione
Chiama getScheduledTimeSlots con l'ID menu della coda.
Quando riprogrammi, passa l'ID chiamata pianificata esistente in modo che il servizio possa restituire
la disponibilità appropriata.
lifecycleScope.launch {
try {
val slots: List<Date> = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = null
) ?: emptyList()
if (slots.isEmpty()) {
showNoSlotsMessage()
} else {
displayTimeSlotPicker(slots)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to fetch time slots: ${e.message}")
}
}
Quando riprogrammi, trasmetti l'ID della chiamata pianificata esistente:
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
Creare una chiamata pianificata
Dopo che l'utente ha selezionato una fascia oraria, crea la chiamata pianificata tramite
callService.
Cosa fornisce l'SDK: un'API createScheduledCall che prenota lo slot selezionato e restituisce il record della chiamata pianificata.
Cosa crei: convalida del numero di telefono E.164, UI di conferma, persistenza locale dell'ID della chiamata pianificata e recupero per conflitti di slot o risposte al di fuori dell'orario di lavoro.
Esempio di implementazione
Passa lo slot selezionato, il numero di telefono, l'ID coda e
RecordingPermission.NOT_ASKED in un ScheduledCallRequest.
lifecycleScope.launch {
try {
val response = CCAI.callService?.createScheduledCall(
ScheduledCallRequest(
menuId = menu.id,
phoneNumber = userPhoneNumber,
scheduleTime = selectedSlot,
recordingPermission = RecordingPermission.NOT_ASKED,
callIdForRescheduling = null,
customData = null,
ticketId = null
)
)
saveScheduledCallId(response?.id)
showConfirmation(scheduledAt = response?.scheduledAt)
} catch (e: CallCreationError.AfterHours) {
showMessage(e.displayMessage)
refreshTimeSlots()
} catch (e: Exception) {
Log.e("CCAI", "Failed to schedule call: ${e.message}")
}
}
Il numero di telefono deve essere nel formato E.164, ad esempio +14155551234.
Ripianificare una chiamata esistente
Per riprogrammare, recupera gli slot e crea la nuova chiamata pianificata con l'ID chiamata pianificata esistente.
Cosa fornisce l'SDK: le stesse API per la creazione e gli slot orari, con
callIdForRescheduling utilizzato per identificare la prenotazione esistente.
Cosa crei: l'interfaccia utente "Modifica orario", la ricerca locale dell'ID chiamata pianificata memorizzato e lo stato di conferma aggiornato dopo la creazione della nuova prenotazione.
Esempio di implementazione
Trasferisci l'ID chiamata esistente sia a
getScheduledTimeSlots che a createScheduledCall.
lifecycleScope.launch {
try {
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
val response = CCAI.callService?.createScheduledCall(
ScheduledCallRequest(
menuId = menu.id,
phoneNumber = userPhoneNumber,
scheduleTime = newSelectedSlot,
recordingPermission = RecordingPermission.NOT_ASKED,
callIdForRescheduling = existingCallId,
customData = null,
ticketId = null
)
)
saveScheduledCallId(response?.id)
showConfirmation(scheduledAt = response?.scheduledAt)
} catch (e: CallCreationError.AfterHours) {
showMessage(e.displayMessage)
refreshTimeSlots()
} catch (e: Exception) {
Log.e("CCAI", "Failed to reschedule: ${e.message}")
}
}
Annullare una chiamata pianificata
Quando l'utente annulla una chiamata imminente, chiama cancelScheduledCall e cancella l'ID chiamata
memorizzato localmente.
Cosa fornisce l'SDK: un'API di annullamento per un ID chiamata pianificata.
Cosa crei: il prompt di conferma, l'interfaccia utente di annullamento riuscito o di errore e la pulizia di qualsiasi stato di chiamata imminente memorizzato localmente.
Esempio di implementazione
Chiama cancelScheduledCall(callId:) con l'ID memorizzato, poi rimuovi l'ID dallo spazio di archiviazione dell'app.
lifecycleScope.launch {
try {
CCAI.callService?.cancelScheduledCall(callId = existingCallId)
clearScheduledCallId()
showCancellationConfirmation()
} catch (e: Exception) {
Log.e("CCAI", "Failed to cancel call: ${e.message}")
}
}
Cosa fornisce l'SDK: l'indirizzo email di destinazione configurato per quella coda specifica, insieme a eventuali istruzioni che l'amministratore vuole che l'utente visualizzi (ad esempio, includi il numero di conto bancario).
Cosa crei: la schermata di composizione delle email e la logica per monitorare il completamento. Puoi creare un modulo personalizzato o utilizzare l'intent
Intent.ACTION_SENDTO integrato di Android per avviare il client di posta preferito dell'utente.
Esempio di implementazione
Questo codice estrae l'indirizzo email di assistenza dall'oggetto Channels dell'SDK su QueueMenu. Apre il client di posta Android integrato e gestisce il caso in cui non è installato alcun client di posta.
import android.content.Intent
import android.net.Uri
import android.widget.Toast
class SupportActivity : AppCompatActivity() {
// 1. Trigger the email flow
fun openEmailComposer(queueMenu: QueueMenu) {
val emailChannel = queueMenu.channels.email ?: return
val emailAddress = emailChannel.email ?: return
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, arrayOf(emailAddress))
putExtra(Intent.EXTRA_SUBJECT, "Support Request: ${queueMenu.name ?: ""}")
// If the queue provides an instruction message, include it in the body
// putExtra(Intent.EXTRA_TEXT, emailChannel.instructionMessage ?: "")
}
if (emailIntent.resolveActivity(packageManager) != null) {
emailResultLauncher.launch(emailIntent)
} else {
Toast.makeText(
this,
"No email client installed on this device.",
Toast.LENGTH_LONG
).show()
}
}
// 2. Track the outcome using ActivityResultContract
private val emailResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
// Note: Most Android email clients don't return a meaningful
// result code. Unlike iOS's MFMailComposeViewControllerDelegate,
// Android's email intent doesn't reliably report whether the
// email was sent, saved as draft, or cancelled.
// Log this event to your analytics and assume the user
// interacted with the email composer.
Log.d("CCAI", "Email composer closed. Result code: ${result.resultCode}")
MyAnalytics.logEvent("email_composer_closed")
}
}
Link di deviazione esterni
Cosa fornisce l'SDK: un elenco di URL (ad esempio il Centro assistenza della tua azienda, le norme sui rimborsi o un'app partner) e i relativi titoli visualizzati, esattamente come configurati nel portale amministratore.
Cosa crei: la logica per eseguire questi link quando un utente li tocca, aprendoli in un browser in-app (utilizzando le schede personalizzate di Android) o avviando il browser di sistema predefinito.
Esempio di implementazione (schede personalizzate, consigliato)
Quando l'utente tocca un link esterno dal menu, questo codice estrae la stringa URL fornita dall'SDK e utilizza le schede personalizzate di Chrome per visualizzare la pagina web in un'esperienza di browser in-app. Le schede personalizzate offrono un'esperienza più rapida e fluida rispetto all'avvio dell'app browser completa, mostrando comunque la barra degli indirizzi in modo che l'utente sappia di visualizzare contenuti esterni.
import androidx.browser.customtabs.CustomTabsIntent
import android.net.Uri
fun openHelpCenterLink(urlString: String) {
val uri = Uri.parse(urlString) ?: return
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(this, uri)
}
Implementazione di riserva (browser di sistema): se le schede personalizzate non sono disponibili o preferisci un approccio più semplice, puoi aprire il link nel browser predefinito dell'utente:
import android.content.Intent
import android.net.Uri
fun openHelpCenterLink(urlString: String) {
val uri = Uri.parse(urlString) ?: return
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
showErrorToast("No browser available to open this link.")
}
}
Esempio di integrazione completa
Questo codice legge il link di deviazione dall'oggetto
Channels dell'SDK e lo presenta all'utente come pulsante toccabile nel menu
del canale.
fun setupDeflectionLinks(queueMenu: QueueMenu) {
val deflectionLink = queueMenu.channels.externalDeflectionLink
if (deflectionLink != null) {
helpCenterButton.isVisible = true
helpCenterButton.setOnClickListener {
// Read the URL from the structured ExternalDeflectionLink type
val url = deflectionLink.url ?: return@setOnClickListener
openHelpCenterLink(url)
}
} else {
helpCenterButton.isVisible = false
}
}
ExternalDeflectionLink espone sia i campi url / displayName di primo livello
sia un array links: List<ExternalDeflection>? interno. L'esempio precedente
restituisce solo la voce di primo livello, che è sufficiente per le code a un solo link. Se
gli amministratori configurano più link esterni in una singola coda, itera
deflectionLink.links e visualizza un pulsante per ogni voce abilitata. Ogni
ExternalDeflection ha il proprio url e displayName:
deflectionLink.links
?.filter { it.enabled == true }
?.forEach { link ->
val button = MaterialButton(this).apply {
text = link.displayName
setOnClickListener { openHelpCenterLink(link.url) }
}
helpCenterContainer.addView(button)
}