Dokumen ini menjelaskan cara mengintegrasikan dan menyesuaikan Headless Mobile SDK di aplikasi Android Anda. Fitur ini mencakup suara, Berbagi Layar, panggilan terjadwal, dan email.
Panggilan suara
Headless Mobile SDK untuk Android menyediakan dukungan panggilan suara melalui
modul CCAICall dan CCAICallRed. Aplikasi Anda memiliki UI panggilan kustom, sementara
SDK menangani pembuatan panggilan, penanganan panggilan masuk, integrasi penyedia, dan
status siklus proses panggilan.
Yang disediakan SDK: API layanan panggilan untuk memulai panggilan instan, memulai pesan suara, menangani panggilan masuk, dan mengamati status panggilan.
Apa yang Anda bangun: Pengalaman panggilan visual, termasuk titik masuk pra-panggilan, dialog izin perekaman, status berdering atau menunggu, kontrol dalam panggilan, UI layanan latar depan, dan navigasi pasca-panggilan.
Melakukan inisialisasi modul panggilan
Lakukan inisialisasi modul panggilan setelah melakukan inisialisasi SDK inti:
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()
)
Memeriksa kemampuan VoiceCallChannel
Sebelum menawarkan panggilan instan, periksa VoiceCallChannel antrean untuk memutuskan titik entri mana yang diekspos (panggilan instan, penggantian pesan suara, pengalihan tingkat saluran) dan apakah izin perekaman diperlukan.
Kolom VoiceCallChannel utama
instantEnabled:Boolean— apakah panggilan suara live (instan) diizinkan untuk antrean ini. Perlakukanfalsesebagai "jangan menawarkan tombol panggilan instan". Berbeda denganvoiceCall != null, yang hanya memberi tahu Anda bahwa antrean memiliki channel suara.preSessionSmartAction:Boolean— apakah tindakan smart pra-sesi diperlukan di saluran tertentu ini sebelum memulai panggilan. Lihat Tindakan smart pra-sesi di Headless Mobile SDK for Android: Smart Actions, Attachments, and Deflection untuk jalur tingkat antrean yang saling melengkapi.scheduleEnabled:Boolean— apakah panggilan terjadwal didukung. Lihat bagian Panggilan terjadwal.recordingOption:RecordingOption?— model izin perekaman panggilan. Lihat Merekam izin. Berbeda denganRecordingPermission.NOT_ASKED, yang hanya digunakan dengan panggilan terjadwal.voicemailReason:String?— diisi saat saluran dikonfigurasi untuk merutekan ke pesan suara.phoneNumber:String?— Nomor pengganti PSTN yang dikonfigurasi untuk pengalihan atau pesan suara.scheduleDeflectionType:String?/deflected:Boolean?/deflectedReason:String?— metadata pengalihan tingkat channel. Jikadeflected == true, tampilkan pesandeflectedReasonyang dikonfigurasi admin dan tawarkan penggantian yang dikonfigurasi (misalnya, nomor telepon yang akan dihubungi atau titik entri panggilan terjadwal) daripada mencoba panggilan instan. Lihat bagian Pengalihan waktu tunggu. di Headless Mobile SDK for Android: Smart Actions, Attachments, and Deflection untuk penanganan pengalihan tingkat antrean.
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)
}
}
Izin perekaman
Yang disediakan SDK: Kolom recordingOption: RecordingOption? di
VoiceCallChannel yang menjelaskan model izin perekaman panggilan yang dikonfigurasi admin
di portal.
Yang Anda bangun: Dialog izin (saat ASK_USER) dan logika percabangan
yang mematuhi pilihan admin.
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)
}
}
}
Memulai panggilan instan
Sebelum memulai panggilan instan:
Periksa
VoiceCallChannelantrean yang dipilih.Pastikan panggilan instan diaktifkan dan tidak dialihkan.
Tangani izin perekaman sesuai dengan
recordingOptionantrean.Minta izin mikrofon.
Panggil
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)
}
}
Mengamati status panggilan
Kumpulkan peristiwa layanan panggilan sebelum memulai atau menerima panggilan:
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)
}
}
Status penyedia vs. status server
stateChanged dan callReceived.status adalah sinyal independen — baca sebagai kolom, bukan baris. stateChanged penyedia melaporkan handshake VoIP lokal; callReceived.status sisi server melaporkan siklus proses panggilan di platform. Mendorong flip "Terhubung" dan memulai timer panggilan dari
callReceived.status — bukan dari stateChanged yang mencapai status terhubung.
| Sinyal | Yang dilaporkan | Yang dapat disimpulkan dari hal tersebut |
|---|---|---|
stateChanged (penyedia) |
Handshake VoIP lokal — Connecting, Connected (local), Disconnected, Error(...). |
Spinner pra-status opsional sebelum status server tiba, error muncul, dan pembersihan akhir panggilan. |
callReceived.status (server) |
Siklus proses sisi server — WAITING, CONNECTING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED. |
Flip terhubung, mulai timer panggilan, UI yang ditetapkan agen, overlay transfer atau eskalasi, dan alur mengakhiri panggilan. |
Panggilan masuk
Panggilan suara masuk dikirimkan ke perangkat menggunakan FCM. Karena panggilan masuk dapat tiba saat aplikasi Anda berada di latar belakang atau dihentikan sepenuhnya, integrasi Anda harus siap menerimanya dari status proses apa pun.
Yang disediakan SDK: Alur IncomingCallEvent di callService yang
memancarkan siklus proses panggilan masuk: Arrived, Accepted, Rejected. Layanan
panggilan juga mengekspos acceptIncomingCall() dan rejectIncomingCall() untuk
menindaklanjuti penawaran.
Yang Anda buat: UI panggilan masuk Anda (layar dering layar penuh, presentasi layar kunci, atau banner dalam aplikasi) dan handler yang menerima atau menolak penawaran.
Contoh penerapan
Kumpulkan incomingCallEvent dan render atau tutup
UI Anda di setiap peristiwa siklus proses, lalu terima atau tolak dari cakupan yang berjalan lama.
Tentukan applicationScope sekali di class Application Anda sehingga terima atau tolak coroutine bertahan dari penghentian Activity singkat selama 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()
}
}
Menghubungkan FCM ke UI panggilan masuk Anda
Panggilan masuk yang dikirim FCM tiba di thread latar belakang, sering kali sebelum ada Activity. Gunakan waktu tunggu singkat untuk menjembatani payload FCM ke emisi
IncomingCallEvent.Arrived pertama, lalu mulai UI Anda.
Contoh penerapan
Tunda di IncomingCallEvent.Arrived berikutnya dengan
withTimeoutOrNull, lalu serahkan ke navigator Anda.
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)
}
Integrasi framework telekomunikasi (direkomendasikan)
Untuk pengalaman panggilan masuk Android bawaan — dering layar penuh, presentasi layar kunci, dan integrasi dengan log panggilan sistem — daftarkan PhoneAccount dan biarkan framework Telecom memiliki UX dering.
Yang disediakan SDK: Helper TelecomCallService yang mendaftarkan
PhoneAccount untuk aplikasi Anda (registerPhoneAccount(context)), TelecomConnectionService untuk menjembatani panggilan CCAI masuk ke Android Telecom,
dan antarmuka TelecomConnectionCallback untuk callback status koneksi.
Yang Anda bangun: Pendaftaran PhoneAccount satu kali selama inisialisasi aplikasi dan deklarasi manifes untuk layanan koneksi.
Contoh penerapan
Daftarkan akun ponsel saat aplikasi dimulai, lalu deklarasikan layanan dan izin koneksi dalam manifes Anda.
// 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>
Alternatif: aktivitas dering layar penuh
Jika Anda lebih suka merender layar dering sendiri daripada menggunakan framework Telecom, SDK akan mengekspos IncomingCallService bersama dengan antarmuka IncomingCallUiNavigator. Terapkan antarmuka, lalu serahkan
penerapan Anda ke IncomingCallService selama inisialisasi aplikasi.
Contoh penerapan
Terapkan IncomingCallUiNavigator dan teruskan ke
IncomingCallService sekali selama 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)
}
Layanan latar depan untuk dering aktif
Saat panggilan berdering, Android dapat menghentikan proses latar belakang. SDK mengirimkan
IncomingRingingService yang membuat alur dering tetap aktif sebagai layanan
latar depan. Deklarasikan di manifes Anda dan SDK akan memulai dan menghentikannya
secara otomatis saat panggilan masuk tiba.
<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 panggilan dan status sisi server
Yang disediakan SDK: Enum CallStatus yang didorong server di setiap payload Call
yang menjelaskan posisi panggilan dalam siklus prosesnya (misalnya,
CONNECTING, WAITING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED,
FAILED).
Apa yang Anda buat: Pemetaan dari CallStatus ke status yang ditampilkan kepada pelanggan
di layar panggilan Anda.
Contoh penerapan
Cabang when pada call.status dari setiap
emisi callReceived dan merender status UI yang cocok:
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)
}
}
Kontrol dalam panggilan (bisukan audio, speaker, tunggu)
Yang disediakan SDK: Properti hanya baca di callService — isMuted,
isSpeakerEnabled, isOnHold — beserta tindakan untuk mengubah setiap status
(setMuted, setSpeakerEnabled, setOnHold).
Yang Anda bangun: Tombol bisu, speaker, dan tunggu di UI dalam panggilan Anda, terhubung ke tindakan SDK dan mencerminkan status saat ini.
Contoh penerapan
Baca status saat ini dari callService untuk menginisialisasi tombol, lalu alihkan dan perbarui saat diketuk.
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
}
Melanjutkan panggilan yang sedang berlangsung setelah terganggu
Saat gangguan PSTN (seperti panggilan seluler masuk) menjeda panggilan CCAI aktif, SDK akan mempertahankan sesi tetap aktif dan memancarkan interruptionDetected.
Setelah gangguan berakhir, aplikasi Anda dapat melanjutkan panggilan.
Yang disediakan SDK: getLastCallInProgress() untuk mencari panggilan yang sedang berlangsung (termasuk di seluruh proses mulai ulang aplikasi) dan resumeCall(call) untuk membawanya kembali ke latar depan.
Yang Anda bangun: Kemampuan melanjutkan di UI Anda (biasanya banner atau melanjutkan otomatis setelah gangguan berakhir) dan navigasi kembali ke layar panggilan Anda.
Contoh penerapan
Cari panggilan yang sedang berlangsung dengan
getLastCallInProgress, lanjutkan, dan kembali ke layar panggilan Anda.
lifecycleScope.launch {
val call = CCAI.callService?.getLastCallInProgress() ?: return@launch
try {
CCAI.callService?.resumeCall(call)
navigateToCallScreen(call)
} catch (e: Exception) {
showCallResumeError(e)
}
}
Eskalasi dari agen virtual
Yang disediakan SDK: canEscalate(allowSkipVirtualAgent: Boolean) adalah
metode pada CallResponse (model yang dikeluarkan oleh callService.callReceived), bukan
pada callService itu sendiri. API ini melaporkan apakah panggilan saat ini memenuhi syarat untuk
eskalasi ke agen manusia berdasarkan status agen virtual panggilan saat ini.
Yang Anda bangun: Pertahankan emisi callReceived terbaru sebagai
"panggilan saat ini", lalu ikat visibilitas tombol "Bicara dengan petugas" ke
currentCall?.canEscalate(...). Argumen allowSkipVirtualAgent adalah nilai konfigurasi menu yang Anda ambil secara terpisah dari respons panggilan itu sendiri
— jangan hardcode true dalam produksi.
Contoh penerapan
Simpan Call terbaru dalam properti, lalu panggil
canEscalate di dalamnya. Atasi allowSkipVirtualAgent dari konfigurasi menu Anda.
// 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
Pesan suara
Yang disediakan SDK: startVoicemail(VoicemailRequest) di callService,
serta enum VoicemailReason yang menjelaskan alasan platform menawarkan pesan suara
(misalnya, penggantian setelah jam kerja, penggantian karena kapasitas berlebih, atau pengalihan
sementara).
Yang Anda bangun: Titik entri pesan suara di UI Anda (biasanya ditampilkan saat
VoiceCallChannel.voicemailReason diisi, atau saat siklus proses panggilan
diselesaikan ke hasil pesan suara) dan UI perekaman itu sendiri.
Contoh penerapan
Tampilkan tombol pesan suara saat voicemailReason
disetel, lalu panggil startVoicemail dengan 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)
}
}
Pengalihan waktu tunggu
Jika antrean terbuka, tetapi waktu tunggu melebihi batas yang dikonfigurasi admin, platform dapat menawarkan jalur alternatif, biasanya berupa panggilan balik terjadwal atau pengalihan PSTN.
Yang disediakan SDK: Payload CallDeflection yang menjelaskan alternatif yang ditawarkan, yang dapat diambil menggunakan getCallDeflection(callId) di callService. Platform
juga memancarkan payload yang sama melalui Alur deflectionOffered saat
pengalihan tersedia di tengah penantian. Tanda
CompanyResponse.preventDirectPstnCall terpisah menunjukkan apakah panggilan PSTN
langsung harus disembunyikan di UI penggantian Anda.
Yang Anda buat: Perintah pengalihan (misalnya, "Perkiraan waktu tunggu adalah 12 menit — apakah Anda ingin menjadwalkan panggilan balik?") dan logika percabangan yang menghormati pilihan pengguna.
Contoh penerapan
Ambil pengalihan yang ditawarkan dengan ID
panggilan aktif, gabungkan dengan CompanyResponse.preventDirectPstnCall, dan tampilkan
perintah.
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)
}
Lanjutan CallOptions
CallOptions mengekspos sekumpulan kecil tombol untuk aplikasi yang perlu menyesuaikan
pengalaman panggilan default.
connectingPollIntervalMs:Long— seberapa sering SDK melakukan polling pada platform saat panggilan berada dalam status menghubungkan.connectedPollIntervalMs:Long— seberapa sering SDK melakukan polling platform saat panggilan terhubung.endCallMaxRetries:Int— berapa kali SDK mencoba ulang permintaan akhiri panggilan sebelum menampilkan error.endCallRetryDelayMs:Long— penundaan antara percobaan ulang panggilan yang diakhiri.
Contoh penerapan
Teruskan nilai yang disesuaikan ke CallOptions saat menginisialisasi modul panggilan:
CCAI.initializeCall(
context = this,
options = CallOptions(
connectingPollIntervalMs = 2_000L,
connectedPollIntervalMs = 5_000L,
endCallMaxRetries = 3,
endCallRetryDelayMs = 1_000L
)
)
Berbagi layar
Berbagi layar memungkinkan agen dukungan melihat layar pengguna untuk membantu memecahkan masalah. Platform CCAI mendukung dua mode berbagi layar:
Berbagi dalam aplikasi: agen hanya dapat melihat apa yang terjadi di dalam aplikasi spesifik Anda.
Berbagi seluruh perangkat (opsional): agen dapat melihat seluruh perangkat pengguna, termasuk layar utama Android dan aplikasi lainnya. Tindakan ini memerlukan konfigurasi Android lanjutan menggunakan
MediaProjectiondan layanan latar depan.
Yang disediakan SDK: Modul CCAIScreenShare yang membuat dan mengelola
sesi, menangani perubahan status, dan memproses permintaan izin.
Yang Anda bangun: Konfigurasi inisialisasi, perintah UI untuk izin pengguna, dan (jika mendukung perangkat lengkap) integrasi MediaProjection Android bawaan dengan layanan latar depan.
Layanan berbagi layar menyediakan metode untuk siklus proses sesi penuh —
startSession, activateSession, stopSession — serta metode konfigurasi
untuk kontrol jarak jauh dan berbagi perangkat penuh. Subbagian berikutnya akan membahas
alur integrasi inti.
Untuk mengetahui tanda tangan metode dan detail parameter lengkap untuk semua metode berbagi layar, lihat Headless Mobile SDK - Android API Reference.
Melakukan inisialisasi dan mengonfigurasi berbagi layar
Inisialisasi layanan berbagi layar selama penyiapan aplikasi dengan memberikan kunci berbagi layar Anda:
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"
// )
// )
Memeriksa kelayakan berbagi layar
Sebelum mengaktifkan fungsi berbagi layar, periksa apakah fungsi tersebut tersedia untuk chat saat ini:
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
fun isScreenShareEnabled(chat: ChatResponse): Boolean {
return CCAI.screenShareService != null
&& chat.supportScreenShare == true
}
Menangani permintaan berbagi layar
Terapkan penanganan permintaan berbagi layar menggunakan ScreenShareManager. Anda membuat
sesi dan menangani respons asinkron menggunakan 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
)
}
Menangani perubahan status berbagi layar
Mendengarkan perubahan status sesi berbagi layar melalui callback onSessionStateChanged
yang diberikan ke 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()
}
}
}
Referensi ScreenShareCallbacks
| Callback | Deskripsi |
|---|---|
onSessionStateChanged |
Dipanggil saat status sesi berbagi layar berubah (INACTIVE, PENDING, ACTIVE). |
onSessionCreationError |
Dipanggil saat pembuatan sesi gagal (masalah jaringan, layanan tidak tersedia, dll.). |
onSessionActivationRequest |
Dipanggil saat sesi siap diaktifkan — panggil ScreenShareManager.activateSession() untuk mulai berbagi. Untuk sesi yang dimulai pengguna, tampilkan dialog izin terlebih dahulu; untuk sesi yang dimulai agen, aktifkan segera. |
onSessionRemoteControlRequest |
Dipanggil saat agen meminta kontrol jarak jauh layar pengguna. Menampilkan dialog izin sebelum memberikan kontrol jarak jauh. |
onSessionFullDeviceRequest | Dipanggil saat agen
meminta berbagi perangkat penuh (lihat Berbagi layar perangkat penuh
di bawah). Aktifkan alur izin MediaProjection sebagai respons. |
onSessionDidSucceed |
Dipanggil setelah sesi berhasil diaktifkan dan mulai melakukan streaming. Gunakan ini untuk mengupdate UI Anda ke status "berbagi layar aktif". |
Untuk definisi enum status lengkap dan semua jenis terkait, lihat Headless Mobile SDK - Android API Reference.
Berbagi layar seluruh perangkat (lanjutan)
Berbagi layar seluruh perangkat memungkinkan agen melihat layar dari aplikasi di luar aplikasi Anda sendiri, termasuk setelan sistem dan navigasi antar-aplikasi.
Hal ini memerlukan integrasi API MediaProjection Android dan menjalankan layanan latar depan.
Mendeklarasikan izin dan layanan latar depan
Tambahkan izin yang diperlukan dan pernyataan layanan latar depan ke
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>
Meminta izin MediaProjection
Sebelum merekam layar, Anda harus meminta izin pengguna menggunakan
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")
}
}
Menerapkan layanan latar depan
Buat layanan latar depan yang menyimpan sesi 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
}
}
Mengaktifkan berbagi perangkat penuh
Setelah MediaProjection aktif, aktifkan berbagi seluruh perangkat melalui layanan berbagi layar:
// After MediaProjection consent is granted and foreground service is started
ScreenShareManager.enableFullDeviceSharing(true)
Pertimbangan Jetpack Compose untuk berbagi layar
Pendekatan yang direkomendasikan untuk interaksi jarak jauh di Jetpack Compose
Menggabungkan elemen interaktif utama dengan tampilan Android — untuk area yang perlu dapat diklik dari jarak jauh, gabungkan sebagai kontrol tampilan Android standar (misalnya,
android.widget.Button) menggunakan composableAndroidView. Hal ini memastikan klik jarak jauh berfungsi dengan andal melalui jalur berbasis tampilan Android.Meneruskan sentuhan jarak jauh dengan penanganan sentuhan kustom — untuk area Compose yang tidak dapat diganti dengan View, gunakan callback sentuhan kustom untuk meneruskan peristiwa sentuhan jarak jauh ke logika Compose Anda.
Memberikan panduan atau penggantian tingkat produk — tunjukkan dengan jelas di layar Compose bahwa "Agen hanya dapat melihat; pengguna harus mengetuk secara lokal", atau batasi izin kontrol jarak jauh secara otomatis di halaman ini sambil mempertahankan akses hanya lihat.
Contoh penerapan wrapper tampilan 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")
}
}
}
Panggilan terjadwal
Panggilan terjadwal memungkinkan pengguna memesan panggilan suara di masa mendatang, bukan menunggu dalam antrean untuk panggilan instan. Di Android, SDK mengekspos metode layanan panggilan untuk memeriksa ketersediaan, mengambil slot waktu, memesan panggilan, menjadwalkan ulang panggilan, membatalkan panggilan, dan menangani error penjadwalan.
Deteksi kemampuan
Yang disediakan SDK: Flag scheduleEnabled di VoiceCallChannel yang
memberi tahu Anda apakah antrean yang dipilih mendukung pemesanan panggilan mendatang.
Yang Anda buat: Logika untuk memeriksa tanda ini sebelum menampilkan titik entri penjadwalan Anda.
Contoh penerapan
Periksa scheduleEnabled di saluran panggilan suara antrean. Jangan gunakan voiceCall != null sebagai proxy — antrean dapat mendukung panggilan suara instan tanpa mendukung panggilan terjadwal, dan sebaliknya.
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
}
Pemeriksaan ketersediaan opsional
Sebelum menampilkan pemilih slot waktu, Anda dapat memeriksa apakah antrean yang dipilih memiliki slot penjadwalan yang tersedia.
Contoh penerapan
Gunakan hasScheduledTimeSlots(menuId:) sebagai
pemeriksaan awal yang ringan. Jika menampilkan false, tampilkan opsi status kosong atau penggantian
daripada membuka pemilih.
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()
}
}
Mengambil slot waktu
Mengambil waktu janji temu yang tersedia untuk antrean yang dipilih. SDK menampilkan nilai Tanggal yang dapat ditampilkan aplikasi Anda di zona waktu lokal pengguna.
Yang disediakan SDK: Stempel waktu slot yang tersedia untuk antrean yang dipilih, dengan dukungan opsional untuk menjadwalkan ulang panggilan terjadwal yang ada.
Yang akan Anda bangun: Kalender, pemilih waktu, presentasi zona waktu, penanganan status kosong, dan perilaku percobaan ulang.
Contoh penerapan
Panggil getScheduledTimeSlots dengan ID menu antrean.
Saat menjadwalkan ulang, teruskan ID panggilan terjadwal yang ada sehingga layanan dapat menampilkan
ketersediaan yang sesuai.
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}")
}
}
Saat menjadwalkan ulang, teruskan ID panggilan terjadwal yang ada:
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
Membuat panggilan terjadwal
Setelah pengguna memilih slot waktu, buat panggilan terjadwal melalui
callService.
Yang disediakan SDK: API createScheduledCall yang memesan slot
yang dipilih dan menampilkan catatan panggilan terjadwal.
Yang Anda buat: Validasi nomor telepon E.164, UI konfirmasi, persistensi lokal ID panggilan terjadwal, dan pemulihan untuk konflik slot atau respons di luar jam kerja.
Contoh penerapan
Teruskan slot yang dipilih, nomor telepon, ID antrean, dan
RecordingPermission.NOT_ASKED dalam 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}")
}
}
Nomor telepon harus dalam format E.164, seperti +14155551234.
Menjadwalkan ulang panggilan yang ada
Untuk menjadwalkan ulang, ambil slot dan buat panggilan terjadwal baru dengan ID panggilan terjadwal yang ada.
Yang disediakan SDK: API slot waktu dan pembuatan yang sama, dengan
callIdForRescheduling digunakan untuk mengidentifikasi pemesanan yang ada.
Yang Anda buat: UI "ubah waktu", penelusuran lokal ID panggilan terjadwal yang disimpan, dan status konfirmasi yang diperbarui setelah pemesanan baru dibuat.
Contoh penerapan
Teruskan ID panggilan yang ada ke
getScheduledTimeSlots dan 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}")
}
}
Membatalkan panggilan terjadwal
Saat pengguna membatalkan panggilan mendatang, panggil cancelScheduledCall dan hapus ID panggilan yang disimpan secara lokal.
Yang disediakan SDK: API pembatalan untuk ID panggilan terjadwal.
Yang Anda bangun: Prompt konfirmasi, UI keberhasilan atau error pembatalan, dan pembersihan status panggilan mendatang yang disimpan secara lokal.
Contoh penerapan
Panggil cancelScheduledCall(callId:) dengan ID yang disimpan, lalu hapus ID dari penyimpanan aplikasi Anda.
lifecycleScope.launch {
try {
CCAI.callService?.cancelScheduledCall(callId = existingCallId)
clearScheduledCallId()
showCancellationConfirmation()
} catch (e: Exception) {
Log.e("CCAI", "Failed to cancel call: ${e.message}")
}
}
Yang disediakan SDK: Alamat email tujuan yang dikonfigurasi untuk antrean tertentu tersebut, beserta petunjuk apa pun yang ingin ditampilkan administrator kepada pengguna (misalnya, sertakan nomor akun Anda).
Yang Anda buat: Layar pembuat email dan logika untuk melacak penyelesaiannya. Anda dapat membuat formulir kustom sendiri, atau menggunakan
Intent.ACTION_SENDTO bawaan Android untuk meluncurkan program email pilihan pengguna.
Contoh penerapan
Kode ini menarik alamat email dukungan dari
objek Channels SDK di QueueMenu. Aplikasi ini akan membuka program email Android bawaan dan menangani kasus saat tidak ada program email yang diinstal.
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 pengalihan eksternal
Yang disediakan SDK: Daftar URL (seperti Pusat Bantuan perusahaan Anda, kebijakan pengembalian dana, atau aplikasi partner) dan judul tampilannya, persis seperti yang dikonfigurasi di Portal Admin.
Yang Anda bangun: Logika untuk mengeksekusi link tersebut saat pengguna mengetuknya - baik dengan membukanya di browser dalam aplikasi (menggunakan tab kustomAndroid) atau meluncurkan browser sistem default.
Contoh penerapan (tab kustom — direkomendasikan)
Saat pengguna mengetuk link eksternal dari menu Anda, kode ini akan mengekstrak string URL yang disediakan oleh SDK dan menggunakan Tab Kustom Chrome untuk menampilkan halaman web dalam pengalaman browser dalam aplikasi. Tab kustom memberikan pengalaman yang lebih cepat dan lancar daripada meluncurkan aplikasi browser lengkap, sekaligus tetap menampilkan kolom URL sehingga pengguna tahu bahwa mereka sedang melihat konten eksternal.
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)
}
Implementasi penggantian (browser sistem): Jika Tab Khusus tidak tersedia atau Anda lebih memilih pendekatan yang lebih sederhana, Anda dapat membuka link di browser default pengguna:
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.")
}
}
Contoh integrasi penuh
Kode ini membaca link pengalihan dari objek
Channels SDK dan menampilkannya kepada pengguna sebagai tombol yang dapat diketuk di menu channel Anda.
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 mengekspos kolom url / displayName tingkat teratas
dan array links: List<ExternalDeflection>? dalam. Contoh sebelumnya
hanya merender entri tingkat teratas, yang cukup untuk antrean link tunggal. Jika
admin Anda mengonfigurasi beberapa link eksternal dalam satu antrean, lakukan iterasi
deflectionLink.links dan render satu tombol per entri yang diaktifkan - setiap
ExternalDeflection memiliki url dan displayName-nya sendiri:
deflectionLink.links
?.filter { it.enabled == true }
?.forEach { link ->
val button = MaterialButton(this).apply {
text = link.displayName
setOnClickListener { openHelpCenterLink(link.url) }
}
helpCenterContainer.addView(button)
}