Este documento explica como integrar e personalizar o SDK Headless Mobile no seu app Android. Ela abrange voz, compartilhamento de tela, chamadas programadas e e-mail.
Ligação
O SDK Headless Mobile para Android oferece suporte a chamadas de voz pelos módulos
CCAICall e CCAICallRed. Seu app é proprietário da UI de chamada personalizada, enquanto
o SDK processa a criação de chamadas, o tratamento de chamadas recebidas, a integração de provedores e
o estado do ciclo de vida da chamada.
O que o SDK oferece: APIs de serviço de chamadas para iniciar chamadas instantâneas, iniciar correio de voz, atender chamadas recebidas e observar o estado da chamada.
O que você cria: a experiência de chamada visual, incluindo o ponto de entrada antes da chamada, o aviso de consentimento de gravação, os estados de toque ou espera, os controles na chamada, UI serviço em primeiro plano e qualquer navegação após a chamada.
Inicializar o módulo de chamada
Inicialize o módulo de chamada depois de inicializar o SDK principal:
import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaicall.CallOptions
import com.ccaiplatform.ccaicall.initializeCall
CCAI.initialize(context = applicationContext, options = initOptions)
CCAI.initializeCall(
context = applicationContext,
options = CallOptions()
)
Inspecionar recursos do VoiceCallChannel
Antes de oferecer uma ligação instantânea, inspecione o VoiceCallChannel da fila para decidir quais pontos de entrada serão expostos (ligação instantânea, fallback de caixa postal, redução no nível do canal) e se é necessário consentimento para gravação.
Campos VoiceCallChannel principais
instantEnabled:Boolean— se uma ligação ao vivo (instantânea) é permitida para essa fila. Tratefalsecomo "não oferecer um botão de chamada instantânea". Diferente devoiceCall != null, que apenas informa que a fila tem um canal de voz.preSessionSmartAction:Boolean— se as ações inteligentes pré-sessão são necessárias nesse canal específico antes de iniciar uma chamada. Consulte Ações inteligentes pré-sessão em SDK headless para dispositivos móveis para Android: ações inteligentes, anexos e desvio para o caminho complementar no nível da fila.scheduleEnabled:Boolean— indica se as chamadas programadas são compatíveis. Consulte a seção Chamadas programadas.recordingOption:RecordingOption?— o modelo de consentimento para gravação de chamada. Consulte Consentimento para gravação. Diferente deRecordingPermission.NOT_ASKED, que é usado apenas com chamadas programadas.voicemailReason:String?— preenchido quando o canal é configurado para encaminhar para o correio de voz.phoneNumber:String?— número alternativo da RPTC configurado para desvio ou correio de voz.scheduleDeflectionType:String?/deflected:Boolean?/deflectedReason:String?— metadados de evasão no nível do canal. Quandodeflected == true, mostre a mensagemdeflectedReasonconfigurada pelo administrador e ofereça o substituto configurado (por exemplo, um número de telefone para discar ou um ponto de entrada de chamada agendada) em vez de tentar uma chamada instantânea. Consulte a seção Redução do tempo de espera no SDK headless para dispositivos móveis do Android: ações inteligentes, anexos e redução para processamento de redução no nível da fila.
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)
}
}
Consentimento para gravação
O que o SDK oferece: um campo recordingOption: RecordingOption? em
VoiceCallChannel que descreve o modelo de consentimento para gravação de chamadas dos administradores
configurado no portal.
O que você cria: a solicitação de consentimento (quando ASK_USER) e a lógica de ramificação
que respeita a escolha do administrador.
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)
}
}
}
Iniciar uma chamada instantânea
Antes de iniciar uma chamada instantânea:
Inspecione o
VoiceCallChannelda fila selecionada.Verifique se as chamadas instantâneas estão ativadas e não foram desviadas.
Gerencie o consentimento de gravação de acordo com o
recordingOptionda fila.Solicite permissão de acesso ao microfone.
Ligue para
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)
}
}
Observar o estado da chamada
Colete eventos de serviço de chamada antes de iniciar ou aceitar uma ligação:
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)
}
}
Estado do provedor x status do servidor
stateChanged e callReceived.status são indicadores independentes. Leia-os como colunas, não linhas. O stateChanged do provedor informa o handshake VoIP local, e o callReceived.status do lado do servidor informa o ciclo de vida da chamada na plataforma. Faça a inversão "Conectado" e o início do timer de chamada de
callReceived.status, não de stateChanged atingindo o estado conectado.
| Sinal | O que ele informa | O que extrair disso |
|---|---|---|
stateChanged (provedor) |
Handshake VoIP local — Connecting, Connected (local), Disconnected, Error(...). |
Spinner de pré-status opcional antes da chegada do status do servidor, da exibição de erros e da limpeza de fim de chamada. |
callReceived.status (servidor) |
Ciclo de vida do lado do servidor: WAITING, CONNECTING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED. |
Inversão conectada, início do cronômetro de chamada, UI atribuída ao agente, sobreposições de transferência ou encaminhamento e o fluxo de encerramento de chamada. |
Chamadas recebidas
As chamadas de voz recebidas são entregues ao dispositivo usando o FCM. Como as chamadas recebidas podem chegar enquanto o app está em segundo plano ou totalmente encerrado, sua integração precisa estar pronta para recebê-las de qualquer estado de processo.
O que o SDK oferece: um fluxo IncomingCallEvent em callService que
emite o ciclo de vida de uma chamada recebida: Arrived, Accepted, Rejected. O serviço de
chamada também expõe acceptIncomingCall() e rejectIncomingCall() para
agir na oferta.
O que você vai criar: UI de chamadas recebidas (tela de toque em tela cheia, apresentação na tela de bloqueio ou banner no app) e os manipuladores que aceitam ou rejeitam a oferta.
Exemplo de implementação
Colete incomingCallEvent e renderize ou dispense sua UI em cada evento de ciclo de vida. Depois, aceite ou rejeite de um escopo de longa duração.
Defina applicationScope uma vez na sua classe Application para que as corrotinas de aceitação ou rejeição sobrevivam a qualquer encerramento de Activity de curta duração durante o 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()
}
}
Como fazer a ponte entre o FCM e a interface de chamadas recebidas
As chamadas recebidas entregues pelo FCM chegam em uma linha de execução em segundo plano, geralmente antes de qualquer
Activity. Use um tempo limite curto para fazer a ponte da carga útil do FCM com a primeira emissão de
IncomingCallEvent.Arrived e inicie a UI.
Exemplo de implementação
Faça uma pausa no próximo IncomingCallEvent.Arrived com
withTimeoutOrNull e entregue o dispositivo ao navegador.
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)
}
Integração de framework de telecomunicações (recomendada)
Para uma experiência de chamada recebida do Android integrada (campainha em tela cheia, apresentação na tela de bloqueio e integração com o registro de chamadas do sistema), registre um PhoneAccount e deixe o framework Telecom cuidar da UX de toque.
O que o SDK oferece: um auxiliar TelecomCallService que registra um
PhoneAccount para seu app (registerPhoneAccount(context)), um
TelecomConnectionService para fazer a ponte entre as chamadas da CCAI e o Android Telecom
e uma interface TelecomConnectionCallback para callbacks de estado de conexão.
O que você vai criar: o registro único PhoneAccount durante a inicialização
do app e uma declaração de manifesto para o serviço de conexão.
Exemplo de implementação
Registre a conta de telefone na inicialização do app e declare o serviço de conexão e a permissão no manifesto.
// 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: atividade de toque em tela cheia
Se você preferir renderizar sua própria tela de toque em vez de usar o framework
Telecom, o SDK vai expor IncomingCallService junto com a interface
IncomingCallUiNavigator. Implemente a interface e entregue sua
implementação a IncomingCallService durante a inicialização do app.
Exemplo de implementação
Implemente IncomingCallUiNavigator e transmita para
IncomingCallService uma vez 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)
}
Serviço em primeiro plano para toque ativo.
Enquanto uma chamada está tocando, o Android pode encerrar processos em segundo plano. O SDK envia um
IncomingRingingService que mantém o fluxo de toque ativo como um serviço
em primeiro plano. Declare no manifesto, e o SDK vai iniciar e parar
automaticamente quando as chamadas chegarem.
<service
android:name="com.ccaiplatform.android.call.IncomingRingingService"
android:foregroundServiceType="phoneCall"
android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
Status da chamada e estados do lado do servidor
O que o SDK oferece: um enumerador CallStatus controlado pelo servidor em cada payload Call
que descreve em que ponto do ciclo de vida a chamada está (por exemplo,
CONNECTING, WAITING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED,
FAILED).
O que você cria: o mapeamento de CallStatus para os estados voltados ao cliente
na tela de chamada.
Exemplo de implementação
Ramificação when em call.status de cada
emissão callReceived e renderização do estado da UI correspondente:
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)
}
}
Controles durante a chamada (silenciar, alto-falante, espera)
O que o SDK oferece: propriedades somente leitura em callService — isMuted, isSpeakerEnabled, isOnHold — junto com as ações para alternar cada estado (setMuted, setSpeakerEnabled, setOnHold).
O que você vai criar: os botões de mudo, alto-falante e espera na UI de chamada, conectados às ações do SDK e refletindo o estado atual.
Exemplo de implementação
Leia o estado atual de callService para
inicializar os botões. Depois, alterne e atualize ao tocar.
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
}
Retomar uma chamada em andamento após uma interrupção
Quando uma interrupção da PSTN (como uma chamada de celular recebida) pausa a chamada ativa da
CCAI, o SDK mantém a sessão ativa e emite interruptionDetected.
Depois que a interrupção terminar, o app poderá retomar a chamada.
O que o SDK oferece: getLastCallInProgress() para pesquisar uma
chamada em andamento (inclusive em reinicializações do app) e resumeCall(call) para trazê-la
para o primeiro plano.
O que você cria: a opção de retomar na sua UI (normalmente um banner ou uma retomada automática após o fim da interrupção) e a navegação de volta para a tela de chamada.
Exemplo de implementação
Procure a chamada em andamento com
getLastCallInProgress, retome e volte para a tela de chamada.
lifecycleScope.launch {
val call = CCAI.callService?.getLastCallInProgress() ?: return@launch
try {
CCAI.callService?.resumeCall(call)
navigateToCallScreen(call)
} catch (e: Exception) {
showCallResumeError(e)
}
}
Encaminhamento de um agente virtual
O que o SDK oferece: canEscalate(allowSkipVirtualAgent: Boolean) é um método em CallResponse (o modelo emitido por callService.callReceived), não em callService. Ele informa se a chamada atual está qualificada para encaminhamento a um agente humano com base no status atual do agente virtual.
O que você vai criar: mantenha a emissão callReceived mais recente como sua "chamada atual" e vincule a visibilidade do botão "Falar com uma pessoa" a currentCall?.canEscalate(...). O argumento allowSkipVirtualAgent é um valor de configuração do menu que você busca separadamente da resposta da chamada. Não codifique true na produção.
Exemplo de implementação
Mantenha o Call mais recente em uma propriedade e invoque
canEscalate nela. Resolva allowSkipVirtualAgent na configuração do 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
Correio de voz
O que o SDK oferece: startVoicemail(VoicemailRequest) em callService, além de uma enumeração VoicemailReason que descreve por que a plataforma ofereceu correio de voz (por exemplo, fallback após o horário de expediente, fallback por excesso de capacidade ou redirecionamento temporário).
O que você cria: o ponto de entrada de correio de voz na sua UI (normalmente mostrado quando
VoiceCallChannel.voicemailReason é preenchido ou quando o ciclo de vida da chamada
resulta em um correio de voz) e a própria UI de gravação.
Exemplo de implementação
Mostre o botão de correio de voz quando voicemailReason
estiver definido e chame startVoicemail com um 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)
}
}
Redução do tempo de espera
Quando a fila está aberta, mas o tempo de espera excede o limite configurado pelo administrador, a plataforma pode oferecer um caminho alternativo, geralmente um retorno de chamada programado ou um desvio de PSTN.
O que o SDK oferece: um payload CallDeflection que descreve a alternativa oferecida, recuperável usando getCallDeflection(callId) em callService. A
plataforma também emite o mesmo payload pelo fluxo deflectionOffered quando uma
deflexão fica disponível no meio da espera. Uma flag CompanyResponse.preventDirectPstnCall separada indica se a discagem direta da PSTN deve ser ocultada na UI alternativa.
O que você vai criar: o aviso de redirecionamento (por exemplo, "A espera estimada é de 12 minutos. Prefere agendar uma ligação?") e a lógica de ramificação que respeita a escolha do usuário.
Exemplo de implementação
Extraia a sugestão de evasão com o ID da chamada ativa, combine com CompanyResponse.preventDirectPstnCall e apresente o aviso.
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)
}
Avançado CallOptions
O CallOptions expõe um pequeno conjunto de alternâncias para apps que precisam ajustar a
experiência de chamada padrão.
connectingPollIntervalMs:Long— a frequência com que o SDK pesquisa a plataforma enquanto a chamada está no estado de conexão.connectedPollIntervalMs:Long— a frequência com que o SDK pesquisa a plataforma enquanto a chamada está conectada.endCallMaxRetries:Int— quantas vezes o SDK tenta novamente a solicitação de encerramento da chamada antes de mostrar um erro.endCallRetryDelayMs:Long— atraso entre as novas tentativas de encerrar a chamada.
Exemplo de implementação
Transmita os valores ajustados para CallOptions ao inicializar o módulo de chamada:
CCAI.initializeCall(
context = this,
options = CallOptions(
connectingPollIntervalMs = 2_000L,
connectedPollIntervalMs = 5_000L,
endCallMaxRetries = 3,
endCallRetryDelayMs = 1_000L
)
)
Compartilhamento de tela
Com o compartilhamento de tela, os representantes de suporte podem visualizar a tela do usuário para ajudar a resolver problemas. A plataforma CCAI oferece suporte a dois modos de compartilhamento de tela:
Compartilhamento no app: o agente só pode ver o que está acontecendo no seu app específico.
Compartilhamento completo do dispositivo (opcional): o agente pode acessar todo o dispositivo do usuário, incluindo a tela inicial do Android e outros apps. Isso exige configuração avançada do Android usando
MediaProjectione um serviço em primeiro plano.
O que o SDK oferece: o módulo CCAIScreenShare que cria e gerencia
sessões, processa mudanças de estado e solicitações de consentimento.
O que você vai criar: a configuração de inicialização, os comandos da UI para consentimento do usuário e (se houver suporte para o dispositivo completo) a integração MediaProjection integrada do Android com um serviço em primeiro plano.
O serviço de compartilhamento de tela oferece métodos para o ciclo de vida completo da sessão: startSession, activateSession e stopSession, além de métodos de configuração para controle remoto e compartilhamento de dispositivo completo. As próximas subseções explicam o fluxo principal de integração.
Para ver assinaturas de métodos completas e detalhes de parâmetros de todos os métodos de compartilhamento de tela, consulte a Referência da API do SDK móvel sem interface gráfica - Android.
Inicializar e configurar o compartilhamento de tela
Inicialize o serviço de compartilhamento de tela durante a configuração do app fornecendo sua chave de compartilhamento de tela:
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"
// )
// )
Verificar a qualificação para compartilhamento de tela
Antes de ativar o compartilhamento de tela, verifique se ele está disponível para o chat atual:
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
fun isScreenShareEnabled(chat: ChatResponse): Boolean {
return CCAI.screenShareService != null
&& chat.supportScreenShare == true
}
Gerenciar solicitações de compartilhamento de tela
Implemente o processamento de solicitações de compartilhamento de tela usando ScreenShareManager. Você cria uma
sessão e processa a resposta assíncrona usando 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
)
}
Processar mudanças de estado do compartilhamento de tela
Detecte mudanças no estado da sessão de compartilhamento de tela usando o callback onSessionStateChanged
fornecido para 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()
}
}
}
Referência de ScreenShareCallbacks
| Callback | Descrição |
|---|---|
onSessionStateChanged |
Chamado quando o estado da sessão de compartilhamento de tela muda (INACTIVE, PENDING, ACTIVE). |
onSessionCreationError |
Chamado quando a criação da sessão falha (problema de rede, serviço indisponível etc.). |
onSessionActivationRequest |
Chamado quando a sessão está pronta para ser ativada. Chame ScreenShareManager.activateSession() para começar a compartilhar. Para sessões iniciadas pelo usuário, mostre primeiro uma solicitação de consentimento. Para sessões iniciadas pelo agente, ative imediatamente. |
onSessionRemoteControlRequest |
Chamado quando o agente solicita o controle remoto da tela do usuário. Mostrar uma solicitação de consentimento antes de conceder o controle remoto. |
onSessionFullDeviceRequest | Chamado quando o agente
solicita o compartilhamento completo do dispositivo (consulte Compartilhamento de tela do dispositivo completo
abaixo). Acione o fluxo de consentimento MediaProjection em
resposta. |
onSessionDidSucceed |
Chamado depois que uma sessão é ativada e começa a transmitir. Use isso para atualizar a UI do usuário para o estado "compartilhamento de tela ativo". |
Para conferir as definições completas de enumeração de estado e todos os tipos associados, consulte a Referência da API do SDK Headless Mobile para Android.
Compartilhamento de tela em todo o dispositivo (avançado)
Com o compartilhamento de tela do dispositivo completo, os atendentes podem ver telas de aplicativos
fora do seu, incluindo configurações do sistema e navegação entre aplicativos.
Isso exige a integração da API MediaProjection do Android e a execução de um
serviço em primeiro plano.
Declarar permissões e serviço em primeiro plano
Adicione as permissões necessárias e a declaração de serviço em primeiro plano ao seu
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>
Solicitar consentimento do MediaProjection
Antes de capturar a tela, peça o consentimento do usuário usando
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")
}
}
Implementar o serviço em primeiro plano
Crie um serviço em primeiro plano que mantenha a sessão 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
}
}
Ativar o compartilhamento de dispositivos completos
Depois que o MediaProjection estiver ativo, ative o compartilhamento de todo o dispositivo pelo serviço de compartilhamento de tela:
// After MediaProjection consent is granted and foreground service is started
ScreenShareManager.enableFullDeviceSharing(true)
Considerações sobre o Jetpack Compose para compartilhamento de tela
Abordagens recomendadas para interação remota no Jetpack Compose
Encapsule os principais elementos interativos com visualizações do Android: para áreas que precisam ser clicáveis remotamente, encapsule-as como controles de visualização padrão do Android (por exemplo,
android.widget.Button) usando o elemento combinávelAndroidView. Isso garante que os cliques remotos funcionem de maneira confiável pelo caminho baseado em visualização do Android.Encaminhar toques remotos com processamento de toque personalizado: para áreas do Compose que não podem ser substituídas por Views, use callbacks de toque personalizados para encaminhar eventos de toque remoto à sua lógica do Compose.
Forneça orientação ou substituto no nível do produto: indique claramente nas telas do Compose que "Os agentes só podem visualizar; o usuário precisa tocar localmente" ou restrinja automaticamente as permissões de controle remoto nessas páginas, mantendo o acesso de visualização.
Exemplo de implementação de wrapper de visualização do 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")
}
}
}
Chamadas programadas
Com as chamadas programadas, os usuários podem agendar uma ligação por voz para o futuro em vez de esperar na fila por uma chamada instantânea. No Android, o SDK expõe métodos de chamada de serviço para verificar a disponibilidade, buscar horários, agendar, reagendar e cancelar ligações, além de processar erros de agendamento.
Detecção de recursos
O que o SDK oferece: uma flag scheduleEnabled em VoiceCallChannel que
informa se a fila selecionada aceita agendamento de chamadas futuras.
O que você vai criar: a lógica para verificar essa flag antes de mostrar o ponto de entrada de agendamento.
Exemplo de implementação
Verifique scheduleEnabled na ligação de voz da fila. Não use voiceCall != null como um proxy. Uma fila pode oferecer suporte a
chamadas de voz instantâneas sem oferecer suporte a chamadas programadas e vice-versa.
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
}
Verificação de disponibilidade opcional
Antes de mostrar o seletor de horários, verifique se a fila selecionada tem horários disponíveis.
Exemplo de implementação
Use hasScheduledTimeSlots(menuId:) como uma pré-verificação leve. Se ele retornar false, mostre um estado vazio ou uma opção de substituição em vez de abrir o seletor.
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()
}
}
Buscar horários
Recupera os horários disponíveis para a fila selecionada. O SDK retorna valores de data que seu app pode mostrar no fuso horário local do usuário.
O que o SDK oferece: carimbos de data/hora de slots disponíveis para a fila selecionada, com suporte opcional para reagendar uma chamada programada.
O que você cria: a agenda, o seletor de hora, a apresentação do fuso horário, o processamento de estado vazio e o comportamento de nova tentativa.
Exemplo de implementação
Chame getScheduledTimeSlots com o ID do menu da fila.
Ao reagendar, transmita o ID da chamada programada para que o serviço possa retornar
a disponibilidade adequada.
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}")
}
}
Ao reagendar, transmita o ID da chamada agendada atual:
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
Criar uma chamada programada
Depois que o usuário selecionar um horário, crie a chamada programada usando
callService.
O que o SDK oferece: uma API createScheduledCall que agenda o horário selecionado e retorna o registro da chamada agendada.
O que você vai criar: validação de número de telefone E.164, UI de confirmação, persistência local do ID da chamada programada e recuperação para conflitos de horários ou respostas fora do horário comercial.
Exemplo de implementação
Transmita o slot selecionado, o número de telefone, o ID da fila e
RecordingPermission.NOT_ASKED em um 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}")
}
}
O número de telefone precisa estar no formato E.164, como +14155551234.
Remarcar uma chamada
Para reagendar, busque horários e crie a nova chamada programada com o ID scheduled-call atual.
O que o SDK oferece: as mesmas APIs de criação e intervalo de tempo, com
callIdForRescheduling usado para identificar a reserva atual.
O que você vai criar: a interface "mudar horário", a pesquisa local do ID da chamada programada armazenada e o estado de confirmação atualizado após a criação da nova reserva.
Exemplo de implementação
Transmita o ID da chamada atual para getScheduledTimeSlots e 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}")
}
}
Cancelar uma ligação programada
Quando o usuário cancelar uma chamada futura, chame cancelScheduledCall e limpe o
ID da chamada armazenado localmente.
O que o SDK oferece: uma API de cancelamento para um ID de chamada programada.
O que você vai criar: o comando de confirmação, a UI de cancelamento bem-sucedido ou de erro e a revisão dos dados de qualquer estado de próxima chamada armazenado localmente.
Exemplo de implementação
Chame cancelScheduledCall(callId:) com o ID armazenado e remova o ID do armazenamento do app.
lifecycleScope.launch {
try {
CCAI.callService?.cancelScheduledCall(callId = existingCallId)
clearScheduledCallId()
showCancellationConfirmation()
} catch (e: Exception) {
Log.e("CCAI", "Failed to cancel call: ${e.message}")
}
}
O que o SDK fornece: o endereço de e-mail de destino configurado para essa fila específica, além de instruções que o administrador quer que o usuário veja (por exemplo, inclua o número da sua conta).
O que você vai criar: a tela do compositor de e-mail e a lógica para rastrear a
conclusão dela. Você pode criar seu próprio formulário personalizado ou usar o
Intent.ACTION_SENDTO integrado do Android para iniciar o cliente de e-mail preferido do usuário.
Exemplo de implementação
Esse código extrai o endereço de e-mail de suporte do objeto Channels do SDK no QueueMenu. Ele abre o cliente de e-mail
Android integrado e processa o caso em que nenhum cliente de e-mail está instalado.
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")
}
}
Links de redirecionamento externos
O que o SDK oferece: uma lista de URLs (como a Central de Ajuda da sua empresa, a política de reembolso ou um app parceiro) e os títulos de exibição deles, exatamente como configurados no Portal do administrador.
O que você cria: a lógica para executar esses links quando um usuário toca neles, seja abrindo em um navegador no app (usando as abas personalizadas do Android) ou iniciando o navegador padrão do sistema.
Exemplo de implementação (guias personalizadas — recomendado)
Quando o usuário toca em um link externo no seu menu, esse código extrai a string de URL fornecida pelo SDK e usa as guias personalizadas do Chrome para mostrar a página da Web em uma experiência de navegador no app. As guias personalizadas oferecem uma experiência mais rápida e integrada do que iniciar o app de navegador completo, mas ainda mostram a barra de endereço para que o usuário saiba que está visualizando conteúdo externo.
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)
}
Implementação de substituição (navegador do sistema): se as guias personalizadas não estiverem disponíveis ou você preferir uma abordagem mais simples, abra o link no navegador padrão do usuário:
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.")
}
}
Exemplo de integração completa
Esse código lê o link de rejeição do objeto Channels do SDK e o apresenta ao usuário como um botão clicável no menu do canal.
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 expõe os campos de nível superior url / displayName
e uma matriz links: List<ExternalDeflection>? interna. O exemplo anterior
renderiza apenas a entrada de nível superior, o que é suficiente para filas de link único. Se
os administradores configurarem vários links externos em uma única fila, itere
deflectionLink.links e renderize um botão por entrada ativada. Cada
ExternalDeflection tem seu próprio 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)
}