Headless Mobile SDK untuk Android: Tindakan cerdas, lampiran, dan pengalihan

Dokumen ini menunjukkan cara mengintegrasikan dan menyesuaikan Headless Mobile SDK di aplikasi Android Anda. Bagian ini mencakup tindakan smart, lampiran, dan pengalihan.

Mengonfigurasi tindakan dan lampiran smart

Tindakan smart memungkinkan agen dan konsumen bertukar informasi terstruktur yang kaya, seperti foto, video, cuplikan teks, dan verifikasi biometrik.

Dalam arsitektur tanpa antarmuka, SDK bertindak secara ketat sebagai lapisan transportasi yang aman. Hal ini memberi tahu Anda kapan tindakan diperlukan atau diminta, dan secara aman mentransfer data ke platform CCAI. Karena Anda memiliki antarmuka pengguna, aplikasi Anda sepenuhnya bertanggung jawab untuk membuat layar kamera, pemilih foto, kotak teks, dan memicu perintah biometrik bawaan Android.

Tindakan cerdas pra-sesi

Tindakan pra-sesi mengumpulkan konteks (seperti nomor akun atau video produk yang rusak) sebelum pengguna bergabung ke antrean, sehingga mengurangi waktu penanganan agen secara signifikan.

Yang disediakan SDK: Aturan dan persyaratan untuk antrean tertentu (misalnya, apakah verifikasi biometrik, upload foto, atau input teks bersifat opsional atau wajib sebelum bergabung).

Yang Anda bangun: Layar UI pra-sesi (kamera, input teks, dan perintah biometrik) yang muncul antara pengguna mengetuk antrean dan memasuki ruang tunggu, serta logika penyiapan cache disk untuk menyimpan media dengan aman hingga sesi terhubung.

Contoh penerapan

Kode ini memeriksa konfigurasi SDK untuk melihat apakah antrean yang dipilih memiliki persyaratan pra-sesi. Contoh ini menunjukkan cara menangani persyaratan video dengan menyimpan referensi file Uri di disk, bukan menyimpan video dalam memori.

// 1. Check requirements and collect data BEFORE joining the queue
fun checkPreSessionRequirements(queueMenu: QueueMenu) {
    val smartActions = queueMenu.settings
    if (smartActions.isNullOrEmpty()) {
        startChat(queueMenu)
        return
    }

    // Example: handle a video requirement
    presentVideoPicker { localFileUri ->
        // IMPORTANT: localFileUri should point to context.cacheDir
        MySessionStateManager.pendingPreSessionVideo = localFileUri
        startChat(queueMenu)
    }
}

// 2. Upload and clean up AFTER the session officially connects
fun handleSessionConnected() {
    val pendingVideoUri = MySessionStateManager.pendingPreSessionVideo ?: return

    // Upload using the standard in-session pipeline
    // then clean up the temporary file
    val file = File(pendingVideoUri.path ?: return)
    file.delete()
    MySessionStateManager.pendingPreSessionVideo = null
}

Membaca konfigurasi tindakan smart pra-sesi

SDK mengekspos konfigurasi tindakan smart pra-sesi melalui dua jalur pelengkap. Periksa keduanya untuk menentukan apakah pengambilan pra-sesi diperlukan untuk antrean dan saluran tertentu.

Jalur A - Tingkat antrean (QueueMenu.settings[].sdk.preSessionSmartAction)

// `QueueMenuSetting` and `QueueMenuSDKSetting` (simplified)
data class QueueMenuSetting(
    val sdk: QueueMenuSDKSetting?
    // ... other fields ...
)

data class QueueMenuSDKSetting(
    val preSessionSmartAction: Boolean?
    // ... other fields ...
)

// Usage - "does any queue-level setting require a pre-session smart action?"
fun queueRequiresPreSession(menu: QueueMenu): Boolean {
    return menu.settings?.any { it.sdk?.preSessionSmartAction == true } ?: false
}

Jalur B - Tingkat channel (VoiceCallChannel.preSessionSmartAction)

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

Gunakan Jalur A untuk memutuskan apakah akan menampilkan penghalang pra-sesi sebelum pengguna memilih channel. Gunakan Jalur B untuk penyempurnaan khusus saluran setelah saluran dipilih (misalnya, hanya meminta sebelum panggilan suara instan).

Tindakan cerdas dalam sesi (diprakarsai agen)

Selama panggilan suara atau chat aktif, agen dapat mengklik tombol di dasbornya untuk meminta data dari pengguna (misalnya, Kirim foto tanda pengenal Anda atau Lakukan autentikasi dengan biometrik).

Yang disediakan SDK: Peristiwa real-time yang memberi tahu aplikasi Anda bahwa agen telah meminta jenis tindakan smart tertentu.

Yang Anda buat: Pemberitahuan dalam chat atau dalam panggilan yang memberi tahu pengguna tentang permintaan, meluncurkan alat bawaan yang sesuai (kamera, galeri foto, kolom teks, atau BiometricPrompt Android), dan meneruskan hasilnya kembali ke SDK.

Contoh penerapan

Di sini, aplikasi Anda memproses peristiwa SDK. Saat mendeteksi permintaan agen, alat ini akan menampilkan peringatan, lalu membuka alat Android yang sesuai.

import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat

// Listen for Smart Action requests from the active SDK session.
//
// `SmartActionRequest` exposes a nested `smartAction: SmartAction`, and the
// switchable type lives at `request.smartAction.type`. The `SmartActionType`
// enum is `PascalCase: Unknown`, `Verification`, `Screenshot`, `Photo`, `Video`,
// `TextInput`, `ScreenShare`. Use `Verification` for Face ID / fingerprint flows.
fun handleSmartActionRequest(request: SmartActionRequest) {
    when (request.smartAction.type) {
        SmartActionType.Photo -> {
            promptUserForPhoto(message = "The agent requested a photo.") { capturedBitmap ->
                // Send the image back to the agent using the SDK.
                sendAttachment(image = capturedBitmap)
            }
        }

        SmartActionType.Verification -> {
            val executor = ContextCompat.getMainExecutor(this)
            val biometricPrompt = BiometricPrompt(this, executor,
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(
                        result: BiometricPrompt.AuthenticationResult
                    ) {
                        sendBiometricResult(success = true)
                    }

                    override fun onAuthenticationFailed() {
                        sendBiometricResult(success = false)
                    }

                    override fun onAuthenticationError(
                        errorCode: Int, errString: CharSequence
                    ) {
                        sendBiometricResult(success = false)
                    }
                }
            )

            val promptInfo = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Identity Verification")
                .setSubtitle("Verify your identity to continue")
                .setNegativeBtnText("Cancel")
                .build()

            biometricPrompt.authenticate(promptInfo)
        }

        SmartActionType.TextInput -> {
            presentTextInputPrompt(message = "The agent requested text input.") { text ->
                sendSmartActionText(text)
            }
        }

        SmartActionType.Video -> {
            // Agent-requested video capture. Reuse the same disk-cached file
            // pattern recommended for pre-session media keep the recording on
            // disk and pass the local Uri to the SDK rather than holding a
            // large `ByteArray` in memory.
            promptUserForVideo(message = "The agent requested a video.") { capturedVideoUri ->
                sendAttachment(videoUri = capturedVideoUri)
            }
        }

        SmartActionType.Screenshot -> {
            // The SDK exposes `Screenshot` as a distinct request type the
            // user captures (or selects), a single still image of the current
            // screen. This is **not** the same as `ScreenShare`, which
            // starts a live co-browse session.
            promptUserForScreenshot(message = "The agent requested a screenshot.") { capturedBitmap ->
                sendAttachment(image = capturedBitmap)
            }
        }

        SmartActionType.ScreenShare -> {
            // Hand off to the full screen share flow (see Headless Mobile SDK for Android: Voice, screen share, scheduled call, and email).
            // Don't confuse with `Screenshot`;  `ScreenShare` starts a
            // live co-browse session using `CCAIScreenShare`, while `Screenshot`
            // captures a single still frame.
            startScreenShareFlow()
        }

        SmartActionType.Unknown -> {
            // Future-proof: a smart action type the SDK does not recognize.
            // Log and continue; don't block the user.
            Log.w("CCAI", "Received unknown smart action type")
        }
    }
}

Tindakan smart yang dimulai agen tiba melalui peristiwa SDK sesi aktif. Berlangganan ke peristiwa panggilan atau chat yang relevan sebelum memulai sesi agar aplikasi Anda dapat menampilkan perintah yang sesuai untuk pelanggan segera setelah permintaan tiba.

Memberi tahu dan melacak tindakan smart

Gunakan smartActionService untuk mengonfirmasi dan memperbarui tindakan smart saat pengguna melakukan tindakan tersebut.

val router = CommunicationRouter(
    id = chatId,
    type = CommunicationType.Chat
)

// Notify the server that a smart action has been received.
lifecycleScope.launch {
    try {
        CCAI.smartActionService?.notifySmartActionReceived(
            router = router,
            smartActionId = smartActionId
        )
        Log.d("CCAI", "Smart action notification sent successfully")
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to notify smart action: ${e.message}")
    }
}

// Update the smart action status as the user completes it.
lifecycleScope.launch {
    try {
        CCAI.smartActionService?.updateSmartActionStatus(
            router = router,
            smartActionId = smartActionId,
            status = SmartActionStatus.Finished
        )
        Log.d("CCAI", "Smart action status updated successfully")
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to update smart action status: ${e.message}")
    }
}

Jika tindakan meminta input teks, kumpulkan teks di UI Anda dan kirimkan melalui layanan tindakan smart sebelum menandai tindakan sebagai selesai.

Formulir web

Yang disediakan SDK: API untuk mengambil, menampilkan, dan memvalidasi formulir web yang dipicu oleh agen selama sesi chat.

Yang Anda buat: UI rendering formulir web dan alur pengiriman yang mengemas respons pengguna.

Contoh penerapan

Kode ini mengambil formulir web berdasarkan ID eksternal dan ID tindakan smart-nya, lalu memvalidasi respons sebelum pengiriman.

// Fetch a web form - `fetchWebForm` takes two positional parameters on Android.
lifecycleScope.launch {
    try {
        val webFormResponse = CCAI.chatService?.fetchWebForm(
            externalFormId = "form123",
            smartActionId = 123
        )
        if (webFormResponse != null) {
            Log.d("CCAI", "Web form received: $webFormResponse")
            // Render the web form in your custom UI.
            renderWebForm(webFormResponse)
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to fetch web form: ${e.message}")
    }
}

// Validate a web form response.
lifecycleScope.launch {
    try {
        val isValid = CCAI.chatService?.validateWebForm(webFormResponse)
        if (isValid == true) {
            Log.d("CCAI", "Web form is valid")
        } else {
            Log.d("CCAI", "Web form validation failed")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to validate web form: ${e.message}")
    }
}

Formulir kustom

Yang disediakan SDK: API untuk mengambil definisi formulir yang dapat dikonfigurasi (dengan beberapa jenis pertanyaan) dan mengirimkan jawaban pengguna.

Yang akan Anda bangun: UI formulir dinamis yang merender setiap jenis pertanyaan (kolom teks, dropdown, dll.), memvalidasi input, dan mengumpulkan respons untuk pengiriman.

Contoh penerapan

Kode ini mengambil definisi pertanyaan formulir kustom, lalu mengemas dan mengirimkan jawaban pengguna.

// Retrieve custom form details
lifecycleScope.launch {
    try {
        val formDetails = CCAI.chatService?.getCustomFormDetails(formId = 123)
        if (formDetails != null) {
            Log.d("CCAI", "Form Title: ${formDetails.title ?: "No title"}")
            Log.d("CCAI", "Questions: ${formDetails.questions.size}")
            // Render the form dynamically based on question types
            renderCustomForm(formDetails)
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to get custom form details: ${e.message}")
    }
}

// Submit a custom form
val request = SubmitCustomFormRequest(
    smartActionId = 123,
    formResponse = listOf(
        SubmitCustomFormRequest.Answer(questionId = 1, value = "John Doe"),
        SubmitCustomFormRequest.Answer(questionId = 2, value = "john@example.com")
    )
)

lifecycleScope.launch {
    try {
        CCAI.chatService?.submitCustomForm(request = request)
        Log.d("CCAI", "Form submitted successfully")
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to submit form: ${e.message}")
    }
}

Untuk definisi enum CustomFormDetailsResponse, CustomFormQuestion, CustomFormQuestionOption, dan CustomFormQuestionType lengkap, lihat Headless Mobile SDK — Referensi API Android.

Lampiran yang dimulai konsumen (chat)

Pengguna tidak perlu menunggu agen bertanya; mereka dapat secara proaktif mengirim gambar dan video selama percakapan. SDK menerapkan batas ukuran file yang ketat dan secara otomatis mencoba kompresi sebelum transmisi.

Batas ukuran lampiran

  • Gambar: ≤ 2 MB

  • Video: ≤ 20 MB

Yang disediakan SDK: Pipeline upload aman yang menangani transmisi jaringan dan otomatis mengompresi file untuk memenuhi batas Platform CCAI.

Yang Anda bangun: Tombol klip kertas / lampiran di UI chat Anda, pemilih foto/video, dan balon chat visual yang menampilkan status upload (memuat, terkirim, atau gagal).

Contoh penerapan

Saat pengguna memilih foto atau video dari galeri mereka, teruskan file lokal Uri ke SDK menggunakan sendMessage API yang sama yang digunakan untuk semua pesan keluar. Untuk mengetahui informasi selengkapnya, lihat Mengirim pesan.

SDK menangani kompresi dan upload.

// Called when the user picks an image from their photo gallery
fun userDidSelectImage(imageUri: Uri) {
    val messageBubble = myChatAdapter.addLoadingImageBubble()

    lifecycleScope.launch {
        try {
            CCAI.chatService?.sendMessage(
                OutgoingMessageContent.Photos(
                    images = listOf(imageUri),
                    smartAction = null
                )
            )
            messageBubble.markAsSent()
        } catch (e: Exception) {
            messageBubble.showError("Failed to send: ${e.message}")
        }
    }
}

Lampiran yang dimulai agen (chat)

Agen dapat mengirim file kembali kepada pengguna, termasuk gambar, video, klip audio, dan dokumen (PDF, CSV, dll.).

Jenis file yang didukung

Platform CCAI Platform menerima 19 jenis file dalam empat kategori. UI kustom Anda harus menangani setiap kategori dengan baik (pratinjau gambar, pemutar video, pemutar audio, download dokumen).

Kategori Jenis yang didukung
Gambar JPEG, PNG, GIF, dan WebP.
Video MP4, MOV, AVI, WMV, dan WebM.
Audio MP3, WAV, M4A, dan WEBA.
Dokumen PDF, DOC, XLS, PPT, CSV, dan TXT.

Yang disediakan SDK: Payload pesan chat masuk yang dikirim menggunakan Flow pesan layanan chat yang menyertakan metadata lampiran — lihat Headless Mobile SDK for Android: Topik lanjutan, pelokalan, dan alur pasca-sesi. Layanan chat juga menyediakan downloadMedia untuk mengambil media yang dikirim agen secara aman.

Yang Anda bangun: UI untuk merender lampiran ini di feed chat. Aplikasi Anda memutuskan apakah akan menampilkan pratinjau gambar, thumbnail video, kontrol pemutaran audio, atau tombol download dokumen.

Contoh penerapan

Amati pesan masuk menggunakan Flow pesan layanan chat. Saat pesan baru menyertakan metadata media, panggil downloadMedia sebelum merender atau membuka file.

downloadMedia menampilkan DownloadMediaResponse { data: ByteArray, fileName: String? } — byte yang didownload sepenuhnya ditambah nama file tampilan opsional. SDK tidak menyimpan download dalam cache. Jika UI Anda men-scroll kembali ke balon lama atau memasuki kembali tampilan chat, aplikasi Anda bertanggung jawab untuk meng-cache gambar yang didekode atau mempertahankan byte; memanggil downloadMedia lagi akan memicu pengambilan jaringan lain.

import android.graphics.BitmapFactory
import java.io.File

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

fun renderIncomingMessage(message: ChatMessage) {
    val media = message.media

    if (media == null) {
        myChatAdapter.insertTextBubble(text = message.text)
        return
    }

    lifecycleScope.launch {
        try {
            // `downloadMedia` returns the raw bytes plus an optional filename.
            val response = CCAI.chatService?.downloadMedia(
                mediaId = media.id,
                mediaType = media.type
            ) ?: return@launch

            when (media.type) {
                DownloadableMediaType.Photo -> {
                    // Decode in-memory; no disk write needed.
                    val bitmap = BitmapFactory.decodeByteArray(
                        response.data, 0, response.data.size
                    )
                    myChatAdapter.insertImageBubble(
                        bitmap = bitmap,
                        fileName = response.fileName
                    )
                }
                DownloadableMediaType.Video,
                DownloadableMediaType.Audio,
                DownloadableMediaType.Document -> {
                    // Larger media usually needs an on-disk file for a player
                    // or document viewer. Write to the app's cache directory
                    // yourself and hand the resulting File/Uri to your UI.
                    val file = File(
                        applicationContext.cacheDir,
                        response.fileName ?: "attachment"
                    )
                    file.writeBytes(response.data)
                    myChatAdapter.insertAttachmentBubble(
                        file = file,
                        mediaType = media.type
                    )
                }
            }
        } catch (e: Exception) {
            myChatAdapter.insertAttachmentErrorBubble(fileName = message.media?.name)
        }
    }
}

Pengalihan (di luar jam kerja dan kelebihan kapasitas)

Pengalihan mengalihkan pengguna saat tim dukungan Anda tidak tersedia. Hal ini biasanya terjadi dalam dua skenario:

  • Di luar jam kerja: pengguna mencoba menghubungi dukungan di luar jam operasional yang dikonfigurasi.

  • Kelebihan kapasitas: antrean terbuka, tetapi waktu tunggu melebihi batas yang dikonfigurasi admin.

Dalam arsitektur headless, SDK tidak otomatis memblokir pengguna atau memunculkan layar error. Sebagai gantinya, aplikasi ini secara diam-diam meneruskan status pengalihan dan opsi penggantian yang dikonfigurasi admin ke aplikasi Anda. Aplikasi Anda harus memeriksa status ini sebelum mencoba memulai sesi.

Menangani status pengalihan

Yang disediakan SDK: deflectionFrom: List<QueueMenuDeflection>? pada QueueMenu yang mencantumkan aturan pengalihan masuk yang merutekan antrean lain ke antrean ini. Setiap QueueMenuDeflection hanya memiliki dua kolom:

  • deflectionType: DeflectionType?: kategori pengalihan (misalnya, di luar jam kerja atau kelebihan kapasitas).

  • menuId: Int?: ID antrean sumber yang dialihkan ke antrean ini.

QueueMenuDeflection tidak membawa pesan tampilan atau label yang dapat dibaca manusia. Untuk salinan yang ditampilkan kepada pengguna (misalnya, Kantor kami tutup), gunakan afterHoursMessage antrean (berasal dari QueueMenu.settings[].afterHours) atau string yang dikonfigurasi admin Anda sendiri. Untuk nama tampilan antrean tujuan yang telah diselesaikan, cari dari hierarki menu yang sudah diambil dengan menuId.

Yang Anda bangun: Logika untuk memeriksa status pengalihan ini sebelum menempatkan pengguna dalam antrean, dan UI kustom (seperti pemberitahuan layar penuh atau banner) yang menampilkan pesan yang sesuai dan tombol perutean alternatif.

Contoh penerapan

Kode berikut mencegat upaya pengguna untuk memulai chat. Hal ini memeriksa metadata pengalihan SDK di QueueMenu dan, jika aturan aktif, menampilkan layar pengalihan menggunakan afterHoursMessage antrean (atau penggantian).

fun onSupportMenuTapped(queueMenu: QueueMenu) {
    // 1. Check if any deflection rules route into this queue.
    val deflections = queueMenu.deflectionFrom
    if (!deflections.isNullOrEmpty()) {
        // Deflection is active - stop routing. Show the custom deflection screen instead.
        val message = queueMenu.afterHoursMessage?.text
            ?: "Support is currently unavailable."
        showDeflectionScreen(
            message = message,
            deflections = deflections
        )
    } else {
        // No deflection rules apply. Safe to proceed to channel selection or directly to chat/call.
        startSupportFlow(queueMenu)
    }
}

Opsi pembelokan render

Saat aturan pengalihan aktif, admin Anda kemungkinan akan mengonfigurasi cara alternatif bagi pengguna untuk mendapatkan bantuan — biasanya dengan mengarahkan ke antrean lain (yang dapat Anda selesaikan menggunakan menuId) atau dengan menampilkan afterHoursMessage yang dikonfigurasi di antrean saat ini.

Yang disediakan SDK: Daftar entri QueueMenuDeflection di deflectionFrom, masing-masing dengan deflectionType dan menuId.

Yang Anda bangun: Menu dinamis yang mengulang entri ini, menyelesaikan setiap menuId terhadap struktur menu yang sudah diambil untuk mendapatkan nama tampilan, dan membuat tombol yang sesuai.

Contoh penerapan

Kode ini mengambil entri pembelokan dan membangun layar secara dinamis. Tombol ini menggunakan nama antrean sumber (diperoleh dari menuId) sebagai label tombol, dan kembali ke deflectionType jika nama tidak tersedia.

fun showDeflectionScreen(message: String, deflections: List<QueueMenuDeflection>) {
    // 1. Update your UI with the admin's configured message (or your fallback).
    deflectionTitleTextView.text = message

    // 2. Loop through the incoming deflection rules and generate buttons.
    for (deflection in deflections) {
        val sourceMenu = deflection.menuId?.let { findMenuById(it) }
        val label = sourceMenu?.name
            ?: deflection.deflectionType?.name
            ?: "Alternative option"

        val button = MaterialButton(this).apply {
            text = label
            setOnClickListener {
                handleDeflectionAction(deflection)
            }
        }
        menuLinearLayout.addView(button)
    }

    // 3. Present the compiled screen to the user.
    deflectionContainer.isVisible = true
}

// Walk your previously fetched menu tree (for example, by using `QueueMenu.flatten()`) to resolve a `menuId`.
private fun findMenuById(id: Int): QueueMenu? =
    cachedRootMenus.flatMap { it.flatten() }.firstOrNull { it.id == id }

deflectionFrom adalah daftar aturan pengalihan yang dialihkan ke menu ini — menuId setiap entri mengidentifikasi antrean sumber yang dialihkan ke antrean saat ini. Aplikasi Anda akan memutuskan cara mengeksposnya. Beberapa integrasi menampilkan tujuan antrean sebagai di sinilah kami akan mengirimkan Anda, sementara yang lain hanya menggunakan keberadaan aturan apa pun sebagai gerbang untuk menampilkan layar coba lagi nanti generik.