Android 向けヘッドレス モバイル SDK: 高度なトピック、ローカライズ、セッション後のフロー

このドキュメントでは、Android アプリケーションで Headless Mobile SDK を統合してカスタマイズする方法について説明します。ローカライズ、セッション後のフロー、その他の高度なトピックについて説明します。

セッション後のフロー(セッション後の VA、アンケート、CSAT)

チャットや音声通話が終了すると、コンタクト センターはフィードバックを収集したり、自動フォローアップを提供したりすることがよくあります。CCAI プラットフォームは、次の 3 種類のアフター セッション エクスペリエンスをサポートしています。

  1. セッション後の仮想エージェント(VA): 人間のエージェントが切断した後も会話を続ける自動 chatbot。

  2. アンケート: 特定のキューに合わせて調整された複数質問形式。

  3. CSAT(顧客満足度): 評価(1 ~ 5 つ星など)と、任意でテキストによるフィードバック。

階層の適用

管理者がポータルで重複するルールを設定することはよくあります(たとえば、CSAT をグローバルにオンにしながら、請求キューのセッション後の VA もオンにするなど)。

SDK が提供するもの: 厳密な競合解決エンジン。SDK はポータルルールを評価し、次の階層を厳守して、セッションごとに 1 つのセッション後のフローのみがトリガーされるようにします。

  • セッション後の優先度: セッション後の VA > アンケート > CSAT

    • 例: キューで CSAT とセッション後の VA の両方が有効になっている場合、ユーザーにはセッション後の VA のみが表示されます。

作成するもの: セッションの終了を待機し、SDK に階層評価でどのフローが勝ったかを問い合わせ、対応する画面を描画するリスナー。どのエクスペリエンスが勝つかを判断するために複雑な if-else ロジックを記述する必要はありません。SDK がその計算を行います。

実装例

このスニペットは、アプリがセッションの終了をリッスンし、SDK に次の動作を問い合わせる方法を示しています。

// Called when the active chat or call ends
fun handleSessionEnded(postSessionFlow: PostSessionFlow?) {
    // 1. Check if there is a required post-session flow.
    //    The SDK has already calculated the hierarchy (VA > Survey > CSAT).
    val flow = postSessionFlow
    if (flow == null) {
        // No feedback required. Safely close the support UI.
        closeSupportExperience()
        return
    }

    // 2. Route to the winning flow returned by the SDK.
    when (flow.type) {
        PostSessionType.VIRTUAL_AGENT -> transitionToPostSessionVA()
        PostSessionType.SURVEY -> showSurveyScreen(flow)
        PostSessionType.CSAT -> showCSATScreen(flow)
    }
}

セッション後のフェーズに移行する

階層でどのフローが優先されるかにかかわらず、chatService で 2 つの共有ライフサイクル メソッドを呼び出して、セッション後のフェーズが開始されたことを CCAI プラットフォームに通知する必要があります。

SDK が提供するもの: セッションをセッション後のフェーズに移行させるライフサイクル メソッド(startPostSession、readyPostSession)。

作成するもの: セッション後の UI フローの適切なポイントでこれらのメソッドを呼び出すオーケストレーション ロジック。

実装例

このコードは、セッションをセッション後の 2 つのライフサイクル ステージに移行します。

lifecycleScope.launch {
    // Step 1: Start the post-session process (updates status to "in_progress")
    try {
        val postSessionResponse = CCAI.chatService?.startPostSession()
        if (postSessionResponse != null) {
            Log.d("CCAI", "Post-session started successfully: ${postSessionResponse.id}")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to start post-session: ${e.message}")
    }

    // Step 2: Signal that post-session is ready (updates status to "ready")
    try {
        val readyResponse = CCAI.chatService?.readyPostSession()
        if (readyResponse != null) {
            Log.d("CCAI", "Post-session ready: ${readyResponse.id}")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to prepare post-session: ${e.message}")
    }
}

プラットフォームが移行を認識したら、この特定のフローに進みます。

ChatResponse のセッション後の状態フィールド

ChatResponse の 2 つのセッション後のオーケストレーション フィールドが、移行ロジックを制御します。

  • postSessionTransferStatus: String? - セッション後の転送の現在の状態("in_progress"、"ready"、"completed" など)。プラットフォームはこのフィールドに基づいてオーケストレーション状態マシンをキー設定します。startPostSession() は "in_progress" に移動し、readyPostSession() は "ready" に移動します。この値は、プラットフォーム側の移行を確認するために、後続の ChatResponse エミッション(チャット サービス チャットフローから)で確認できます。

  • postSessionOptInRequired: Boolean? - セッション後のフローを実行する前に、ユーザーが明示的にオプトインする必要があるかどうか。true の場合は、UI にオプトイン プロンプトを表示し、ユーザーが同意した後にのみ startPostSession() を呼び出します。false または null の場合、オプトインの確認は不要で、セッション後のフローに直接移行できます。

suspend fun orchestratePostSession(chat: ChatResponse) {
    // 1. Honor opt-in gating if required
    if (chat.postSessionOptInRequired == true) {
        val consented = presentPostSessionOptIn()
        if (!consented) {
            closeSupportExperience()
            return
        }
    }

    // 2. Drive the state machine
    CCAI.chatService?.startPostSession()   // → "in_progress"
    CCAI.chatService?.readyPostSession()   // → "ready"

    // 3. Observe postSessionTransferStatus on subsequent chat updates
    //    to confirm the transfer completed before rendering the winning flow.
}
  • postSessionRequired は、セッションにいずれかのセッション後のフローが構成されているかどうかを示します。

  • postSessionOptInRequired は、実行前にユーザーの明示的な同意が必要かどうかを示します。

  • postSessionTransferStatus は、移行ライフサイクルにおけるプラットフォームのフェーズを示します。

3 つはすべて独立しているため、必ずそれぞれを確認してください。

セッション後の仮想エージェント(最優先)

SDK が提供するもの: セッションを人間のエージェントから自動 bot に戻すための遷移状態と、bot のメッセージのルーティング。

構築する内容: まったく新しいものはありません。セッション後の VA はチャットとして動作するため、既存のカスタム チャット UI を開いたままにして、人間のエージェントのメッセージを処理したときと同じように、受信した自動メッセージを処理させます。

実装例

SDK がセッション後の VA への移行を通知した場合は、チャット UI を更新して変更を反映します。

fun transitionToPostSessionVA() {
    // Keep your chat UI open, but update the visual context
    myChatAdapter.addSystemMessage(
        "You are now speaking with our Virtual Assistant"
    )
    agentNameTextView.text = "Virtual Assistant"
    agentAvatarImageView.setImageResource(R.drawable.ic_bot_avatar)

    // The SDK will automatically start piping the VA's messages into
    // your standard chat message observer (messagesReceived).
}

アンケート(優先度: 中)

SDK で提供されるもの: rateService は、コミュニケーション セッションのアンケートの質問を取得して回答を送信するためのメソッドを公開します。アンケートの質問は、キューごとに管理者が構成します。複数の質問タイプ(CSAT、星評価、自由形式のテキスト、列挙、数値スケール)を含めることができます。

構築するもの: アンケートを取得し、質問をループ処理して、それぞれに適切な UI(ラジオボタン、星評価、テキスト フィールドなど)をレンダリングし、必須フィールドが入力されていることを検証し、回答をパッケージ化して返送する動的フォーム。

鍵のタイプ

// Survey response from getSurvey()
data class Survey(
    val id: Int?,
    val lang: String?,
    val questions: List<SurveyQuestion>?,
    val signOffText: String?  // "Thank you" message to show after submission
)

// Request payload for sendSurvey()
data class SurveyRequest(
    val answers: Map<Int, String>  // Maps question IDs to answer values
)

実装例

このコードは、rateService からアンケートの質問を取得し、カスタム フラグメントでレンダリングして、回答を送信します。

fun showSurveyScreen(communicationId: Long) {
    val rateService = CCAI.rateService ?: return

    lifecycleScope.launch {
        try {
            // 1. Fetch the survey questions for this session
            val survey = rateService.getSurvey(
                communicationId = communicationId,
                communicationType = "chats"
            ).getOrThrow()

            // 2. Present your custom survey UI
            val surveyFragment = MyCustomSurveyFragment.newInstance(
                survey.questions.orEmpty()
            )
            surveyFragment.onSubmit = { answersMap ->
                lifecycleScope.launch {
                    try {
                        // 3. Submit the answers
                        val request = SurveyRequest(answers = answersMap)
                        rateService.sendSurvey(
                            communicationId = communicationId,
                            communicationType = "chats",
                            answers = request
                        ).getOrThrow()
                        rateService.clear()
                        closeSupportExperience()
                    } catch (e: Exception) {
                        Log.e("CCAI", "Failed to submit survey: ${e.message}")
                    }
                }
            }
            supportFragmentManager.beginTransaction()
                .replace(R.id.container, surveyFragment)
                .addToBackStack(null)
                .commit()
        } catch (e: Exception) {
            Log.e("CCAI", "Failed to load survey: ${e.message}")
            closeSupportExperience()
        }
    }
}

CSAT - 顧客満足度(優先度が最も低い)

SDK が提供するもの: rateService は、コミュニケーション セッションの CSAT スコアとオプションのフィードバックを送信する rate() メソッドを公開します。通常、評価は 1 ~ 5 の整数値です。

作成するもの: 視覚的な評価画面(タップ可能な星の行や 1 ~ 5 の数値スケールなど)、フィードバックが許可されている場合はテキスト フィールド、[送信] ボタン。

鍵のタイプ

// Request payload for rate()
data class RatingRequest(
    val rating: Int? = null,      // Typically 1-5
    val feedback: String? = null  // Optional qualitative feedback
)

実装例

ユーザーが星の評価をタップしてフィードバックを書き込んだら、それらの値を rateService に渡します。

fun submitCSATFeedback(communicationId: Long, rating: Int, comment: String?) {
    val rateService = CCAI.rateService ?: return
    submitButton.isEnabled = false
    progressBar.isVisible = true

    lifecycleScope.launch {
        try {
            val request = RatingRequest(
                rating = rating,
                feedback = comment?.trim()?.takeIf { it.isNotEmpty() }
            )
            rateService.rate(
                communicationId = communicationId,
                communicationType = "chats",
                request = request
            ).getOrThrow()
            rateService.clear()
            showThankYouScreen()
        } catch (e: Exception) {
            showErrorToast("Failed to submit feedback. Please try again.")
        } finally {
            submitButton.isEnabled = true
            progressBar.isVisible = false
        }
    }
}

チャットの文字起こしをダウンロードする

SDK で提供されるもの: チャットの文字起こしを PDF ファイルとしてダウンロードする方法。

構築するもの: UI のダウンロード ボタンとファイル処理ロジック(保存、共有、プレビュー)。

実装例

このコードは、Android の文字起こしダウンロード アプローチを示しています。PDF をファイルとして取得し、アプリで保存、共有、プレビューします。

// Download to a file destination
val documentsDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
val transcriptFile = File(documentsDir, "chat_transcript.pdf")

lifecycleScope.launch {
    try {
        val transcriptFile = CCAI.chatService?.downloadChatTranscript()
        if (transcriptFile != null) {
            Log.d("CCAI", "Transcript downloaded to: ${transcriptFile.absolutePath}")
        }
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to download transcript: ${e.message}")
    }
}

Android の downloadChatTranscript() メソッドはパラメータを受け取らず、ダウンロードした PDF を指す File? を返します。downloadChatTranscriptData() メソッド(ディスクに保存せずにメモリに直接 PDF 生データを返す)は iOS でのみ使用できます。共有ストレージに保存する場合は、アプリに適切なファイル ストレージ権限があることを確認します。

言語とローカライズ

Headless Mobile SDK を使用してカスタムの組み込み UI を構築しているため、アプリのボタン、メニュー、カスタム アラートの翻訳はユーザーが行う必要があります。

では、SDK がユーザーの言語を把握する必要があるのはなぜでしょうか?

  1. キューの転送: CCAI プラットフォームは言語コードを使用して、その言語を話すエージェントにユーザーを転送します。

  2. システム メッセージ: CCAI プラットフォームのバックエンドによって生成された自動メッセージ(「エージェントがまもなく対応いたします」など)は、アクティブな言語で配信されます。

言語サービス API

SDK が提供するもの: アクティブな SDK 言語の読み取り / 書き込み languageCode プロパティ。

作成するもの: 言語選択ツール UI(ユーザーに手動で選択させたい場合)と、セッション開始前に言語を設定するロジック。

// Read the active language
val currentLanguage = CCAI.languageService?.languageCode

// Set the active language before the user starts a support session
CCAI.languageService?.languageCode = "es"

SDK は、SDK でサポートされている言語の固定リストを公開していません。アプリに言語選択ツールが表示される場合は、アプリの表示リストを自分で管理するか、SDK から返されるインスタンスまたはエンタープライズ構成を使用して、提供する言語を決定します。

SDK の言語を設定する(セッション前のルール)

SDK が提供するもの: CCAI Platform ポータルで管理者が実際に有効にした言語と、リクエストした言語を比較する LanguageService。

構築するもの: ユーザーが言語を手動でオーバーライドできるようにする場合(デバイスのデフォルト設定に依存するだけではなく)、選択 UI(プルダウン メニューなど)を構築し、サポート チャネルをタップする前にのみアクセスできるようにする必要があります。

実装例

このコードは、標準の ISO 639 言語コード(スペイン語の場合は "es"、フランス語の場合は "fr" など)を使用して SDK の言語を明示的にオーバーライドする方法を示しています。優先言語は、InitOptions データクラスの SDK 初期化時に languageCode パラメータを使用して設定されます。SDK を再初期化するときに設定することもできます。

// Set language during initialization
val initOptions = InitOptions(
    key = "YOUR_COMPANY_KEY",
    urlHost = "your_subdomain.ccaiplatform.com",
    languageCode = "es"  // for example, "es" for Spanish, "fr" for French
)

try {
    CCAI.initialize(context = this, options = initOptions)
} catch (e: Exception) {
    Log.e("CCAI", "Failed to initialize CCAI: ${e.message}")
}

チャット モジュールの初期化(Android)

言語コードを受け取るオーバーロードを使用してチャット モジュールを初期化する場合は、言語も指定できます。

val chatOptions = ChatOptions(
    webFormInterface = WebFormManager(),
    downloadTranscriptVisibility = DownloadTranscriptVisibility.SHOW_ALL,
    greeting = "Hello! How can I help you?"
)
CCAI.initializeChat(context = this, options = chatOptions)

キューメニューを取得するときに言語を渡すこともできます。

val menuResult = CCAI.queueMenuService?.get(key = null)

ChatRequest を使用してセッションごとにオーバーライドします。

val chatRequest = ChatRequest(
    chat = Chat(
        menuId = menuId,
        languageCode = "fr"  // This session will use French
    )
)

CCAI プラットフォームが最終的な言語を解決する方法

言語を明示的に設定しない場合でも、SDK はルーティングとシステム メッセージの言語コードを必要とします。動作を予測可能にするには、ユーザーがサポート セッションを開始する前に自分で設定することをおすすめします。

次の優先順位を使用します。

  1. 明示的な言語(推奨): InitOptions.languageCode で言語を設定すると、SDK はその言語を使用します。

  2. セッション前の言語の更新(明示的): ユーザーがキューに入る前に CCAI.languageService?.languageCode = "es" を設定すると、SDK は更新された言語を使用します。

  3. セッションごとのオーバーライド(チャットのみ): ChatRequest(chat = Chat(..., languageCode = "fr")) を指定すると、そのチャット セッションではリクエストされた言語コードが使用されます。

  4. 何も設定しない場合: 動作は、SDK がデバイスとインスタンスの構成で利用可能な最良のオプションと判断した内容にフォールバックします。結果の一貫性を保つため、これに依存せず、常に言語を明示的に設定してください(手順 1 ~ 3)。

エラー処理、フォールバック、セッション管理

最も堅牢なアプリでも、接続の切断、依存関係の欠落、認証の不具合が発生することがあります。Headless Mobile SDK は、基盤となるプラットフォームの問題を検出し、アプリに通知するように設計されています。これにより、カスタム UI 内で適切に処理したり、CCAI Platform の自動フォールバックに依存したりできます。

SDK エラーの処理

SDK が提供するもの: 標準の Kotlin 例外とともに、2 つのカテゴリの型付き例外。

  • HttpExceptionWithCode - CCAI プラットフォームへの HTTP リクエストが失敗した場合に、非同期サービス メソッドによってスローされます。Int code プロパティを公開して、ステータス コード(認証の期限切れの場合は 401、レート制限の場合は 429、サーバーエラーの場合は 5xx など)で分岐できるようにします。

  • CallCreationError.AfterHours - スロットの競合が発生した場合や、予約受付期間が終了した場合に、スケジュール設定呼び出し API(callService.createScheduledCall)によってスローされる封印されたエラー。ユーザーに直接表示することを目的とした、サーバーでローカライズされた displayMessage を保持します。

  • 標準の Kotlin または Java の例外 - ネットワーク障害は java.io.IOException、コルーチンのキャンセルは kotlinx.coroutines.CancellationException、予期しないものはベースの Exception クラスとして表面化します。

構築するもの: すべての SDK サービス呼び出しの周りにレイヤードされた try または catch。最初に最も具体的な例外(該当する場合は CallCreationError.AfterHours などのドメイン固有のシールド エラー、次に HTTP ステータス分岐用の HttpExceptionWithCode)をキャッチし、ネットワーク、システム、予期しない障害については汎用的な Exception ハンドラにフォールバックします。

実装例

このコードは、SDK 呼び出しをレイヤード try または catch パターンでラップし、最初に HttpExceptionWithCode をキャッチして HTTP ステータス コードで分岐し、それ以外のすべての処理に対して汎用ハンドラにフォールバックします。

import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope

lifecycleScope.launch {
    try {
        CCAI.chatService?.start(chatRequest)
        // Handle success - observe chat state using chatReceived
    } catch (e: HttpExceptionWithCode) {
        // Handle HTTP-level failures from CCAI endpoints - inspect e.code for status code
        Log.e("CCAI", "HTTP ${e.code}: ${e.message}")
        showErrorToast("Something went wrong. Please try again.")
    } catch (e: Exception) {
        // Handle other errors (examples: network and system)
        Log.e("CCAI", "Error: ${e.message}")
        showErrorToast("An unexpected error occurred.")
    }
}

ベスト プラクティス

  • すべての SDK サービス呼び出しをレイヤード try/catch でラップします。HttpExceptionWithCode を使用して HTTP ステータス コードで分岐します。たとえば、401(認証トークンの有効期限切れ)で ccaiShouldAuthenticate() を再トリガーしたり、429(レート制限)でバックオフを適用したり、5xx レスポンスで一般的なエラーを表示したりします。

  • スケジュール設定された呼び出し API の場合、HttpExceptionWithCode の前に CallCreationError.AfterHours をキャッチして、409 スロットの競合がサーバーローカライズされたメッセージで処理されるようにします。

  • 最後に、IOException、CancellationException、予期しない障害のセーフティ ネットとして、一般的な Exception をキャッチします。開発中に例外の詳細をロギングすると、根本原因をすばやく特定できます。

セッションを確認してクリアする

アプリのライフサイクルは CCAI Platform のサポート ライフサイクルとは別であるため、セッションの状態(特にユーザーのログアウト)を管理する必要があります。

SDK が提供するもの: サポート セッションがまだ進行中かどうかを確認するユーティリティ API(アプリの再起動後や強制終了後を含む)、特定のエンドユーザーに関連付けられたキャッシュに保存された SDK 認証状態をクリアするメソッド。これにより、次にログインするユーザーが前のユーザーのセッションを引き継ぐことを防ぎます。

構築するもの: 新しいセッションの開始を試みる前のプリフライト チェックと、ユーザーがメインアプリからログアウトしたときに起動するクリーンアップ ルーティン。

実装例(ログアウトのクリーンアップ)

アクティブなセッションを終了し、SDK からキャッシュに保存された認証トークンをクリアし、ローカル アプリデータをワイプして、ログイン画面に移動します。これは、ユーザーがログアウトしたときにセッションが継承されないようにするために必要な最小限のクリーンアップです。

fun handleUserLogout() {
    lifecycleScope.launch {
        // 1. Check for an in-progress session and end it cleanly
        try {
            val activeChat = CCAI.chatService?.getLastChatInProgress()
            if (activeChat != null) {
                CCAI.chatService?.endChat()
                Log.d("CCAI", "Ended active chat before logout.")
            }
        } catch (e: Exception) {
            // Session may not exist - that's fine
            Log.w("CCAI", "No active session to end: ${e.message}")
        }

        // 2. Clear the cached auth token
        //    (prevents the next user from inheriting the previous session)
        CCAI.authService?.updateAuthToken(null)

        // 3. Wipe your own app's user data
        MyAuthManager.clearLocalData()

        // 4. Navigate back to the login screen
        navigateToLoginScreen()
    }
}

Kotlin コルーチンと Flow の統合

Android 向け Headless Mobile SDK は、主なリアクティブ パターンとして Kotlin コルーチンと Flow を中心に設計されています。これは、iOS SDK の Combine 統合に相当する Android の機能です。

SDK が提供するもの: リアルタイムの SDK イベント(着信メッセージ、画面共有の状態の変化)をオブザーバブルなリアクティブ ストリームとして出力する Kotlin Flow ストリームと一時停止関数。

作成するもの: これらの Flow を収集し、それに応じて Android UI を更新するコルーチン ベースの統合。

関数を一時停止する

多くの SDK サービス メソッドは Kotlin の一時停止関数です。つまり、コルーチン スコープ内から呼び出すことができ、結果を待機している間は一時停止(ブロックではない)します。

import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope

// In an Activity or Fragment
lifecycleScope.launch {
    try {
        // These are suspend functions - they suspend, not block
        val menuResult = CCAI.queueMenuService?.get()
        CCAI.chatService?.start(chatRequest)
        val company = CCAI.companyService?.get()
    } catch (e: Exception) {
        handleError(e)
    }
}

チャット サービスのフロー

chatService は、リアルタイムのチャット イベントをモニタリングするための Kotlin SharedFlow プロパティを公開します。ライフサイクル対応コルーチン スコープ(lifecycleScope または viewModelScope)内で収集します。

フロー Emits
messagesReceived チャット受信メッセージのリスト(エージェント、VA、システムからのメッセージ)。
typingEvent ユーザーまたはエージェントが入力しているとき。
memberEvent ユーザーまたはエージェントがチャットに参加または退出したとき(エージェントの転送や切断を含む)。
stateChanged チャット プロバイダの状態の変化(接続中、接続済み、切断済み、エラー)。
chatReceived チャットの受信時または更新時のチャット応答データ(ステータスの変更、エージェントの割り当て、セッション後の状態など)。

chatService は、転送イベント(transferEventSubject)、ステータス チェックの結果(checkStatusSubject)、メッセージ送信の結果(messageSendResultSubject)の追加のフローを公開します。

実装例

import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged

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

// 2. Typing indicators
lifecycleScope.launch {
    CCAI.chatService?.typingEvent?.collect { event ->
        when (event) {
            is ChatTypingEvent.Started -> showTypingIndicator()
            is ChatTypingEvent.Ended -> hideTypingIndicator()
        }
    }
}

// 3. Member join/leave
lifecycleScope.launch {
    CCAI.chatService?.memberEvent?.collect { event ->
        when (event) {
            is MemberEvent.Joined -> Log.d("Chat", "Member joined")
            is MemberEvent.Left -> Log.d("Chat", "Member left")
        }
        // Refresh chat status after member changes
        try { CCAI.chatService?.checkStatus() } catch (_: Exception) {}
    }
}

// 4. Chat provider state changes
lifecycleScope.launch {
    CCAI.chatService?.stateChanged?.distinctUntilChanged()?.collect { state ->
        // Update UI based on connection state (examples: connecting, connected)
        updateConnectionStatus(state)
    }
}

// 5. Chat metadata updates (agent assignment, status changes)
lifecycleScope.launch {
    CCAI.chatService?.chatReceived?.collect { chat ->
        Log.d("Chat", "Received chat update: ${chat.id}")
        updateAgentInfo(chat)
        handlePostSessionIfNeeded(chat)
    }
}

画面共有の状態

SDK が提供するもの: 画面共有セッションの状態は、ScreenShareCallbacks の onSessionStateChanged コールバックを通じて配信されます。これは ScreenShareManager.startSession() に渡されます。Android 向けヘッドレス モバイル SDK: 音声、画面共有、スケジュール設定された通話、メールをご覧ください。このコールバックは、セッションが INACTIVE、PENDING、ACTIVE の間で移行するたびに起動します。

作成するもの: ScreenShareCallbacks.onSessionStateChanged に接続されたハンドラ。セッションが状態間を移行する際に UI を更新します。また、コールバックの外部で現在の状態を読み取る必要がある場合は、ローカル プロパティ(currentScreenShareSessionState など)も作成します。

実装例

ScreenShareCallbacks パスに加えて、ScreenShareManager はスタンドアロンの addStateChangeListener / removeStateChangeListener API と同期 getSessionState() アクセサも公開します。これらは、startSession() を呼び出したコンポーネント以外のコンポーネントから状態を監視する必要がある場合や、1 回限りの非リアクティブな読み取りを行う場合に使用します。ScreenShareCallbacks パスとは異なり(セッションが終了するとコールバックが自動的に解放される)、スタンドアロン リスナーは自動的に解放されません。リークを回避するため、ライフサイクル オーナーの teardown で removeStateChangeListener(...) を呼び出します。

// Register a state change listener
val listener: (ScreenShareSessionState) -> Unit = { state ->
    when (state) {
        ScreenShareSessionState.INACTIVE -> Log.d("ScreenShare", "No active session")
        ScreenShareSessionState.PENDING -> Log.d("ScreenShare", "Session pending")
        ScreenShareSessionState.ACTIVE -> Log.d("ScreenShare", "Session active")
    }
}
ScreenShareManager.addStateChangeListener(listener)

// Get the current state directly (not reactive)
val currentState = ScreenShareManager.getSessionState()

// Clean up when done
ScreenShareManager.removeStateChangeListener(listener)

カスタムデータ

SDK が提供するもの: 構造化されたメタデータをチャット セッションに関連付けるメカニズム。署名付き JWT(バックエンドで検証)または署名なしペイロード(クライアントで直接設定)として関連付けることができます。

構築するもの: アプリのデータの収集ロジックと、署名付きデータの場合はバックエンド サーバーの JWT 署名エンドポイント(完全な認証と JWT ペイロード スキーマについては、Headless Mobile SDK for Android: スタートガイドをご覧ください)。

署名付きカスタムデータ

署名付きカスタムデータは、バックエンド サーバーで会社のシークレットを使用して署名された JWT トークンを使用します。これは、機密データや信頼できるデータを渡すための安全なアプローチです。

実装例

バックエンドで生成された JWT から署名付きカスタム データ オブジェクトを作成します。

// JWT signed with company secret (signing must happen on your backend)
val customData = CustomData(signed = "header.payload.signature")

署名なしのカスタムデータ

署名なしのカスタムデータは、CustomDataPayload と CustomDataItem を使用してクライアントに直接設定されます。

実装例

表示可能なデータ項目とエージェント非表示のデータ項目を含む無署名のペイロードを構築します。CustomDataPayload は MutableMap<String, CustomDataItem?> を受け取るため、添字代入(または mutableMapOf)を使用して、キーごとに各アイテムを追加します。選択したキー("user_name" や "account_tier" など)は、エージェントとルーティング/分析に表示されるフィールド名になります。

val items = mutableMapOf<String, CustomDataItem?>()
items["user_name"] = CustomDataItem(
    label = "Name",
    value = "John"
)
items["user_age"] = CustomDataItem(
    label = "Age",
    value = 30,
    type = CustomDataType.NUMBER,
    invisibleToAgent = true
)
val payload = CustomDataPayload(items = items)
val customData = CustomData(unsigned = payload)

カスタムデータ アイテムは、次のプロパティをサポートしています。

  • label: エージェントに表示されるラベル。

  • value: データ値(例: String、Int、Bool)。

  • type: データ型(STRING、NUMBER、DATE、URL、BOOLEAN)。

  • invisibleToAgent: true の場合、データはエージェントには表示されませんが、ルーティングと分析には使用できます。

チャット リクエストでカスタムデータを使用する

チャット セッションの開始時にカスタムデータを添付します。

実装例

val chatRequest = ChatRequest(
    chat = Chat(
        menuId = 123,
        languageCode = "en",
        customData = customData
    )
)

lifecycleScope.launch {
    try {
        CCAI.chatService?.start(chatRequest)
        // Chat started - observe chat state using chatReceived
    } catch (e: Exception) {
        Log.e("CCAI", "Failed to start chat: ${e.message}")
    }
}

外部チャットの転送

SDK が提供するもの: 以前のエージェントの名前と会話の文字起こしを新しい CCAI プラットフォーム セッションに渡すためのデータ構造(ExternalChatTransfer)。

構築するもの: 外部チャット システムからコンテキストを抽出し、SDK 用にパッケージ化する移行フロー。

実装例

このコードは、前のエージェントの名前を使用して外部チャット転送オブジェクトを作成し、セッションのカスタムデータに関連付けます。

// ExternalChatAgent also accepts an optional `avatar: String? = null` parameter
// for displaying the previous agent's avatar, for example:
//     ExternalChatAgent(name = "John Smith", avatar = "https://cdn.example.com/avatars/jsmith.png")
val transfer = ExternalChatTransfer(
    agent = ExternalChatAgent(name = "John Smith"),
    transcript = emptyList()
)

// Attach to your custom data payload
val payload = CustomDataPayload()
payload.externalChatTransfer = transfer

val customData = CustomData(unsigned = payload)

これは、ユーザーをサードパーティのチャット システムから CCAI Platform を利用したセッションに移行し、受信側のエージェントの会話コンテキストを保持する場合に便利です。

スレッド処理に関する考慮事項

Headless Mobile SDK for Android は、Kotlin コルーチンと連携するように設計されており、大部分がメインセーフです。ただし、ヘッドレス統合にはスレッド処理に関する重要な考慮事項があります。

メインスレッドの安全性

すべての SDK メソッドはスレッドセーフであり、どのスレッドからでも呼び出すことができます。ほとんどの SDK サービス呼び出し(chatService?.start()、queueMenuService?.get() など)は、内部で独自のスレッド処理を行う一時停止関数です。スレッドをブロックせずに呼び出し元のコルーチンを一時停止します。コルーチン スコープ内のメインスレッドから安全に呼び出すことができます。

// Safe to call from the main thread
lifecycleScope.launch {
    val menu = CCAI.queueMenuService?.get()  // Suspends, does not block
    updateUi(menu)  // Back on main thread
}

CCAIDelegate とバックグラウンド スレッド

CCAIDelegate の ccaiShouldAuthenticate() メソッドは suspend 関数であるため、その中でネットワーク呼び出しを安全に実行できます。

override suspend fun ccaiShouldAuthenticate(): String? {
    // This runs in a coroutine context - safe to call suspend functions
    return withContext(Dispatchers.IO) {
        MyBackendApi.getSignedJwt()  // Network call on IO dispatcher
    }
}

SDK コールバックからの UI の更新

Flow を使用して SDK イベントをモニタリングする場合は、UI の更新がメインスレッドで行われるようにしてください。lifecycleScope を使用している場合、これは自動的に処理されます。

// lifecycleScope dispatches to Main by default
lifecycleScope.launch {
    chatMessagesFlow.collect { messages ->
        // Already on Main - safe to update UI
        chatAdapter.submitList(messages)
    }
}

メイン以外のディスパッチャーから収集する場合は、明示的に切り替えます。

lifecycleScope.launch(Dispatchers.IO) {
    // Processing on IO thread
    val processed = heavyProcessing()
    withContext(Dispatchers.Main) {
        // Update UI on Main thread
        updateChatUi(processed)
    }
}

初期化スレッド

CCAI.initialize() と、initializeChat()、initializeCall()、initializeScreenShare() などの関連モジュールの初期化呼び出しは、Application.onCreate() のメインスレッドで呼び出す必要があります。これらは、SDK の内部状態を設定する同期呼び出しです。

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()  // Always on main thread

        // All initialization must happen on main thread
        CCAI.initialize(context = this, options = initOptions)
        CCAI.initializeChat(context = this, options = ChatOptions())
        CCAI.initializeCall(context = this)
    }
}

メモリ管理

メモリリークを防ぎ、SDK リソースのクリーンな破棄を確実に行うには、適切なメモリ管理が不可欠です。

コルーチンとフローのライフサイクル

Android ライフサイクル メソッド(アクティビティの場合は onDestroy()、ViewModel の場合は onCleared())で、コルーチン Job 参照と Flow コレクタを常にキャンセルします。そうしないと、参照が意図したよりも長く存続し、メモリリークが発生する可能性があります。

import kotlinx.coroutines.Job

class ChatViewModel : ViewModel() {
    private var chatJob: Job? = null

    fun observeChatMessages() {
        chatJob = viewModelScope.launch {
            CCAI.chatService?.messagesReceived?.collect { messages ->
                // Handle new messages
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        chatJob?.cancel()
    }
}

リスナーとコールバックのリファレンス

リスナーとコールバックにウィーク リファレンスを使用して、保持サイクルを防ぎます。Activity または ViewModel が破棄されたら、登録済みのコールバックをすべてクリアします。

class ChatActivity : AppCompatActivity() {
    private var chatJob: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        chatJob = lifecycleScope.launch {
            CCAI.chatService?.messagesReceived?.collect { messages ->
                updateChatUI(messages)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        chatJob?.cancel()
    }
}

ベスト プラクティス

  • コルーチン Job 参照を保存する: スコープを超えて存続するコルーチンの暴走を防ぐため、launch {} から返された Job 参照を常に保存します。

  • ライフサイクル メソッドでクリーンアップする: onDestroy()(アクティビティ)または onCleared()(ViewModel)ですべてのジョブをキャンセルし、コールバックをクリアします。

  • リスナーに弱参照を使用する: this(囲みアクティビティまたはフラグメントのコンテキスト)を暗黙的にキャプチャするラムダは、ガベージ コレクションを妨げる可能性があるため、注意が必要です。

  • サービスは SDK によって管理される: SDK サービスを手動でリリースする必要はありません。CCAI シングルトン オブジェクトによって管理されます。

トラブルシューティング

このセクションでは、一般的な統合の問題とその解決策について説明します。

ビルドと依存関係の問題

このセクションでは、一般的なビルドと依存関係の問題について説明します。

問題

Gradle が com.ccaiplatform.android:CCAIKit を解決できません。

  • 原因: CCAI Maven リポジトリが存在しないか、間違った URL を使用しています。

  • 修正: settings.gradle.kts に次のものが含まれていることを確認します。

    maven { url = uri("https://sdk.ujet.co/ccaip/android/") }
    

    /ccaip/ パス セグメントに注目してください。以前の URL(https://sdk.ujet.co/android/)ではアーティファクトは解決されません。

問題

Java 17 でコンパイル エラーが必要になりました。

  • 原因: この SDK には Java 17 との互換性が必要です。

  • 修正: build.gradle.kts を更新します。

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
    

問題

chatLibraryNotFound(エラーコード 1100)が返されます。

  • 原因: CCAIChatRed モジュールが依存関係にありません。

  • 修正: チャットには CCAIChat と CCAIChatRed の両方が必要です。追加:

    implementation("com.ccaiplatform.android:CCAIChat:ccaiVersion")
    implementation("com.ccaiplatform.android:CCAIChatRed:ccaiVersion")
    

初期化に関する問題

このセクションでは、一般的な初期化の問題について説明します。

問題

CCAI.chatService にアクセスする際に NullPointerException が表示されます。

  • 原因: CCAI.initializeChat() が呼び出されていないか、CCAI.initialize() の前に呼び出されています。

  • 修正: Application.onCreate() での初期化順序を保証:

    1. CCAI.initialize(context, options = initOptions)

    2. CCAI.initializeChat(context, options = chatOptions)

    3. CCAI.initializeCall(context)(音声通話またはスケジュールされた通話を使用している場合)

    4. CCAI.initializeScreenShare(context, options)(画面共有を使用している場合)

問題

SDK サービスは初期化後に null を返します。

  • 原因: CCAI.initialize() がメインスレッドで呼び出されなかったか、Application クラスがマニフェストに登録されていません。

  • 修正: AndroidManifest.xml ファイルに android:name=".MainApplication" 行が存在し、すべての init 呼び出しが onCreate() にあることを確認します。

認証の問題

このセクションでは、認証に関する一般的な問題について説明します。

問題

認証がサイレントに失敗するか、ccaiShouldAuthenticate() が呼び出されない。

  • 原因: InitOptions で CCAIDelegate が設定されていませんでした。

  • 修正: delegate が InitOptions に渡されるようにします。

    val initOptions = InitOptions(
        key = "YOUR_KEY",
        urlHost = "your_subdomain.ccaiplatform.com",
        delegate = MyCCAIDelegate()  // Must not be null
    )
    

問題

JWT 検証エラー(エラーコード 101)。

  • 原因: JWT の有効期限が切れている、形式が正しくない、または誤ったシークレットで署名されている。

  • 修正: バックエンドが次の条件を満たしていることを確認します。

    • 正しい会社の秘密コードを使用している。

    • 有効な iat と exp のタイムスタンプを設定します。

    • 適切な形式の JWT 文字列を返します。

プッシュ通知に関する問題

このセクションでは、プッシュ通知に関する一般的な問題について説明します。

問題

プッシュ通知が受信または処理されない。

  • 原因: FCM トークンが SDK に登録されていない、Firebase メッセージング サービスが登録されていない、受信データ ペイロードが SDK に転送されていない。

  • 修正:

    • onNewToken() と起動時の両方で CCAI.pushNotificationService?.updatePushToken(token) を呼び出します。

    • Firebase メッセージング サービスから remoteMessage.data を CCAI.pushNotificationService?.handlePushNotification(remoteMessage.data) に転送します。

    • google-services.json がアプリ モジュールにあることを確認します。

    • CCAI Platform 管理者ポータルで Firebase サービス アカウント キーが構成されていることを確認します。

    • 表示される通知や着信通話の UI は、アプリのコードでレンダリングします。

問題

SDK はプッシュ通知の着信に反応しません(メッセージは無視されます)。

  • 原因: handlePushNotification() メソッドに間違ったデータ型を渡している。

  • 修正: RemoteMessage オブジェクト全体ではなく、remoteMessage.data(Map<String, String>)を渡すようにしてください。

チャット セッションに関する問題

このセクションでは、チャット セッションに関する一般的な問題について説明します。

問題

chatService?.start() は例外をスローするか、null を返します。

  • 原因: menuId が無効、認証の失敗、ネットワークの問題。

  • 修正:

    • menuId が管理者ポータルの有効なキューと一致していることを確認します。

    • ccaiShouldAuthenticate() が CCAI.authService?.authenticate(jwt) を介して有効な署名付き JWT を交換し、結果の認証トークンを返すことを確認します。

    • デバイスがネットワークに接続されていることを確認します。

    • 呼び出しを try または catch でラップし、例外の詳細をログに記録します。

問題

チャット メッセージが UI に表示されない。

  • 原因: チャット サービスのメッセージ ストリームをモニタリングしていません。

  • 修正: ライフサイクルを認識するスコープでチャット サービスの Flow から収集していることを確認します(上記のチャット サービスのフローのセクションを参照)。

画面共有に関する問題

このセクションでは、画面共有に関する一般的な問題について説明します。

問題

ユーザーが同意しても画面共有が開始されない。

  • 原因: CCAIScreenShare モジュールが初期化されていないか、Android MediaProjection が起動していません。

  • 修正:

    • CCAI.initializeScreenShare() が有効な ScreenShareOptions で呼び出されるようにします。

    • 同意後に Android の MediaProjectionManager とフォアグラウンド サービスを正しく開始していることを確認します。

    • Android 14 以降では、フォアグラウンド サービス タイプに mediaProjection が含まれていることを確認してください。

一般的なデバッグのヒント

  • 開発中に詳細ログを有効にして、SDK の内部状態を確認します。

  • CCAI Platform 管理者ポータルで、キューの構成、チャネルの有効化、営業時間を確認します。

  • Charles Proxy や Android Studio のネットワーク インスペクタなどのツールを使用してネットワーク トラフィックをモニタリングし、*.ccaiplatform.com への API 呼び出しを確認します。

  • モジュールのバージョンが一致していることを確認する - 互換性の問題を回避するため、すべての com.ccaiplatform.android モジュールで同じバージョン番号を使用する必要があります。

シリーズのまとめ

おめでとうございます。Android 向け Headless Mobile SDK の 5 部構成のシリーズを完了しました。統合の対象範囲は次のとおりです。

継続的に参照できるように、Headless Mobile SDK - Android API リファレンスをブックマークし、CCAI Platform リリースノートで SDK の更新情報を確認してください。