Android 向けヘッドレス モバイル SDK: スマート アクション、添付ファイル、回避

このドキュメントでは、Android アプリケーションで Headless Mobile SDK を統合してカスタマイズする方法について説明します。スマート アクション、添付ファイル、回避について説明します。

スマート アクションと添付ファイルを構成する

スマート アクションを使用すると、エージェントとお客様は、写真、動画、テキスト スニペット、生体認証などのリッチな構造化情報を交換できます。

ヘッドレス アーキテクチャでは、SDK は厳密に安全なトランスポート レイヤとして機能します。アクションが必要な場合やリクエストされた場合を通知し、データを CCAI Platform プラットフォームに安全に転送します。ユーザー インターフェースはアプリが所有しているため、カメラ画面、写真選択ツール、テキスト ボックスの作成、Android の組み込みバイオメトリクス プロンプトのトリガーは、アプリが完全に担当します。

セッション前のスマート アクション

セッション前の操作では、ユーザーがキューに参加する前にコンテキスト(口座番号や破損した製品の動画など)を収集するため、エージェントの処理時間を大幅に短縮できます。

SDK が提供するもの: 特定のキューのルールと要件(参加前に生体認証、写真のアップロード、テキスト入力が任意か必須かなど)。

構築するもの: ユーザーがキューをタップしてから待合室に入るまでの間に表示されるセッション前の UI 画面(カメラ、テキスト入力、生体認証プロンプト)と、セッションが接続されるまでメディアを安全に保持するためのディスク キャッシュ ロジック。

実装例

このコードは、SDK の構成をチェックして、選択したキューにセッション前の要件があるかどうかを確認します。このサンプルでは、動画をメモリに保持するのではなく、ディスクに Uri ファイル参照を保持することで、動画の要件を処理する方法を示します。

// 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
}

プリセッション スマート アクション構成を読み取る

SDK は、2 つの補完的なパスを介してセッション前のスマート アクション構成を公開します。両方を確認して、特定のキューとチャネルでセッション前のキャプチャが必要かどうかを判断します。

パス A - キューレベル(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
}

パス B - チャンネル レベル(VoiceCallChannel.preSessionSmartAction)

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

パス A を使用して、ユーザーがチャンネルを選択する前にセッション前のゲートを表示するかどうかを決定します。チャネルが選択された後のチャネル固有の調整(たとえば、インスタント音声通話の前にのみプロンプトを表示するなど)には、パス B を使用します。

セッション内のスマート アクション(エージェントが開始)

チャットや音声通話中に、エージェントはダッシュボードのボタンをクリックして、ユーザーにデータの提供をリクエストできます(例: 身分証明書の写真を送信してください、生体認証で認証してください)。

SDK が提供するもの: エージェントが特定タイプのスマート アクションをリクエストしたことをアプリに通知するリアルタイム イベント。

構築するもの: リクエストをユーザーに通知し、適切な組み込みツール(カメラ、フォト ライブラリ、テキスト フィールド、Android BiometricPrompt)を起動して、結果を SDK に渡す、チャット内または通話内のアラート。

実装例

ここでは、アプリが SDK イベントをリッスンします。エージェント リクエストを検出すると、アラートを表示してから、適切な Android ツールを開きます。

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")
        }
    }
}

エージェントが開始したスマート アクションは、アクティブなセッションの SDK イベントを通じて届きます。セッションを開始する前に、関連するチャット イベントまたは通話イベントをサブスクライブして、リクエストが届いたらすぐに適切な顧客向けプロンプトを表示できるようにします。

スマート アクションの通知と追跡

smartActionService を使用して、ユーザーがスマート アクションを進めるにつれて、スマート アクションを認識して更新します。

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}")
    }
}

アクションでテキスト入力が求められる場合は、UI でテキストを収集し、スマート アクション サービスを介して送信してから、アクションを完了済みとしてマークします。

ウェブフォーム

SDK が提供するもの: チャット セッション中にエージェントによってトリガーされたウェブフォームを取得、表示、検証するための API。

構築するもの: ウェブフォームのレンダリング UI と、ユーザーの回答をパッケージ化する送信フロー。

実装例

このコードは、外部 ID とスマート アクション ID でウェブフォームを取得し、送信前にレスポンスを検証します。

// 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}")
    }
}

カスタム フォーム

SDK が提供するもの: 構成可能なフォーム定義(複数の質問タイプを含む)を取得し、ユーザーの回答を送信するための API。

構築するもの: 各質問タイプ(テキスト フィールド、プルダウンなど)をレンダリングし、入力を検証して、送信用の回答を収集する動的フォーム UI。

実装例

このコードは、カスタム フォームの質問の定義を取得し、ユーザーの回答をパッケージ化して送信します。

// 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}")
    }
}

CustomFormDetailsResponse、CustomFormQuestion、CustomFormQuestionOption、CustomFormQuestionType 列挙型の定義の完全版については、ヘッドレス モバイル SDK - Android API リファレンスをご覧ください。

お客様が開始した添付ファイル(チャット)

エージェントからの質問を待つ必要はなく、チャット中に画像や動画を積極的に送信できます。SDK は厳格なファイルサイズ制限を適用し、送信前に自動的に圧縮を試みます。

添付ファイルのサイズ制限

  • 画像: 2 MB 以下

  • 動画: ≤ 20 MB

SDK が提供するもの: ネットワーク送信を処理し、CCAI Platform の上限を満たすようにファイルを自動的に圧縮する安全なアップロード パイプライン。

構築するもの: チャット UI のクリップ / 添付ファイル ボタン、写真 / 動画選択ツール、アップロード状態(読み込み中、送信済み、失敗)を示す視覚的なチャット バブル。

実装例

ユーザーがギャラリーから写真や動画を選択したら、すべての送信メッセージに使用される同じ sendMessage API を使用して、ローカル ファイル Uri を SDK に渡します。詳細については、メッセージの送信をご覧ください。

圧縮とアップロードは SDK が処理します。

// 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}")
        }
    }
}

エージェントが開始した添付ファイル(チャット)

エージェントは、画像、動画、音声クリップ、ドキュメント(PDF、CSV など)などのファイルをユーザーに送信できます。

サポートされているファイル形式

CCAI Platform プラットフォームでは、4 つのカテゴリにわたる 19 種類のファイル形式がサポートされています。カスタム UI は、各カテゴリ(画像プレビュー、動画プレーヤー、音声プレーヤー、ドキュメントのダウンロード)を適切に処理する必要があります。

カテゴリ サポートされる形式
画像 JPEG、PNG、GIF、WebP。
動画 MP4、MOV、AVI、WMV、WebM。
音声 MP3、WAV、M4A、WEBA。
ドキュメント PDF、DOC、XLS、PPT、CSV、TXT。

SDK が提供するもの: チャット サービスのメッセージ Flow を使用して配信される、添付ファイルのメタデータを含む受信チャット メッセージ ペイロード - Headless Mobile SDK for Android: 高度なトピック、ローカライズ、セッション後のフローを参照してください。チャット サービスは、エージェントが送信したメディアを安全に取得するための downloadMedia も提供します。

作成するもの: チャット フィードでこれらの添付ファイルをレンダリングする UI。アプリは、画像プレビュー、動画サムネイル、音声再生コントロール、ドキュメント ダウンロード ボタンを表示するかどうかを決定します。

実装例

チャット サービスのメッセージ Flow を使用して、受信メッセージを監視します。新しいメッセージにメディア メタデータが含まれている場合は、ファイルをレンダリングまたは開く前に downloadMedia を呼び出します。

downloadMedia は、完全にダウンロードされたバイト数と、必要に応じて表示されるファイル名を返します。DownloadMediaResponse { data: ByteArray, fileName: String? }SDK はダウンロードをキャッシュに保存しません。UI が古いバブルまでスクロール バックするか、チャットビューに再度入ると、アプリはデコードされた画像をキャッシュに保存するか、バイトを永続化する必要があります。downloadMedia を再度呼び出すと、別のネットワーク フェッチがトリガーされます。

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)
        }
    }
}

自動対応チャネルへの誘導(営業時間外とキャパシティ超過)

サポートチームが対応できない場合、ユーザーは Deflection によってリダイレクトされます。通常、これは次の 2 つのシナリオで発生します。

  • 営業時間外: ユーザーが構成された営業時間外にサポートに連絡しようとしています。

  • 容量超過: キューは開いていますが、待機時間が管理者が設定したしきい値を超えています。

ヘッドレス アーキテクチャでは、SDK はユーザーを自動的にブロックしたり、エラー画面をポップアップしたりしません。代わりに、リダイレクト ステータスと管理者が構成したフォールバック オプションがアプリに渡されます。アプリは、セッションを開始する前にこのステータスを確認する必要があります。

たわみ状態を処理する

SDK が提供するもの: QueueMenu の deflectionFrom: List<QueueMenuDeflection>?。他のキューをこのキューに転送する着信転送ルールを一覧表示します。各 QueueMenuDeflection には次の 2 つのフィールドのみが含まれます。

  • deflectionType: DeflectionType?: たらい回しのカテゴリ(営業時間外、容量超過など)。

  • menuId: Int?: このキューに転送されるソースキューの ID。

QueueMenuDeflection には、表示メッセージや人間が読めるラベルは含まれません。ユーザー向けのコピー(「オフィスは閉鎖されています」など)には、キューの afterHoursMessage(QueueMenu.settings[].afterHours から派生)または管理者設定の文字列を使用します。解決された宛先キューの表示名については、menuId で、すでに取得済みのメニューツリーから検索します。

構築するもの: ユーザーをキューに入れる前にこれらの回避状態を確認するロジックと、適切なメッセージと代替ルーティング ボタンを表示するカスタム UI(全画面アラートやバナーなど)。

実装例

次のコードは、ユーザーがチャットを開始しようとしたときにインターセプトします。QueueMenu で SDK の回避メタデータをチェックし、ルールが有効な場合は、キューの afterHoursMessage(またはフォールバック)を使用して回避画面を表示します。

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)
    }
}

レンダリングの偏向オプション

転送ルールが有効になっている場合、管理者はユーザーがサポートを受けるための代替手段を設定します。通常は、別のキューを指定する(menuId を使用して解決できます)か、現在のキューに設定されている afterHoursMessage を表示します。

SDK が提供するもの: deflectionFrom の QueueMenuDeflection エントリのリスト。各エントリには deflectionType と menuId が含まれます。

作成するもの: これらのエントリをループ処理し、各 menuId をすでに取得済みのメニューツリーと照合して表示名を取得し、適切なボタンを生成する動的メニュー。

実装例

このコードは、たわみエントリを取得して画面を動的に構築します。ボタンラベルには、ソースキューの名前(menuId から解決)が使用されます。名前が使用できない場合は、deflectionType にフォールバックします。

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 は、このメニューに転送する転送ルールのリストです。各エントリの menuId は、現在のキューに転送する送信元キューを識別します。アプリは、これらの公開方法を決定します。統合によっては、宛先キューが「こちらに送信します」と表示されることもあれば、ルールが存在することだけをゲートとして使用して、一般的な「後でやり直してください」画面を表示することもあります。