Android SDK(gtncontact-sdk)
GTN Contact の顧客向けチャットをネイティブAndroidアプリに組み込むための Jetpack Compose / Material3 SDK です。会話一覧・チャット・添付・CSATアンケートまでを含む画面一式を提供します。
概要
| 項目 | 内容 |
|---|---|
| minSdk / compileSdk | 29 / 35 |
| 言語 | Kotlin 2.1 |
| UI | Jetpack Compose(Compose BOM、Material3標準コンポーネントのみ)。文言は日本語 |
| 依存 | OkHttp(HTTP/WebSocket)、kotlinx-serialization-json、kotlinx-coroutines、Compose標準のみ |
| モジュール | :gtncontact-sdk(com.android.library) |
インストール(Gradle)
settings.gradle.kts に :gtncontact-sdk モジュールを include し、アプリモジュールから依存します。
// settings.gradle.kts
include(":gtncontact-sdk")
// app/build.gradle.kts
dependencies {
implementation(project(":gtncontact-sdk"))
}
クイックスタート
1. SDKを初期化(configure)
UIエントリポイントの起動前に必ず GTNContact.configure を呼び出します。
GTNContact.configure(
baseUrl = "https://api.example.com", // 開発時エミュレータ: http://10.0.2.2:3010
tokenProvider = { myAuthBackend.fetchContactToken() }, // suspend関数
language = "ja",
)
2. トークン供給(tokenProvider)について
- SDKは絶対に自分でトークンを発行しません。
tokenProvider(suspend () -> String)でホストアプリが顧客トークンを供給します。 - 取得したトークンはSDK内部でキャッシュされ、RESTとWebSocketの両方に使われます。
- devモードのトークン形式は
dev:<customerId>です(サンプルアプリではPOST /v1/guestで自前取得)。
3. サポート画面を起動
一番簡単な方法はSDK内蔵の GTNContactActivity です(会話一覧+チャット画面一式を内包)。
GTNContactActivity.start(
activityContext = this,
sourceScreen = "請求明細画面", // 起動元画面名でサポートフローが自動解決される
contextMap = mapOf("契約プラン" to "プレミアム"), // フローに渡す追加コンテキスト
)
認証
tokenProvider と 401 リトライ
- 全リクエストに
Authorization: Bearer <token>が付与されます。 - APIが 401 を返した場合、SDKはキャッシュを破棄して
tokenProviderを再呼び出しし、1回だけリトライします。再取得後も401ならGTNContactException.Unauthorizedを投げます。 - トークン失効時の再取得(リフレッシュ)はホストアプリの責務です。
tokenProviderは常に「現在有効なトークン」を返すよう実装してください。
ゲスト→ログイン統合(claimGuest)
ゲスト時代の会話をログイン後の顧客に統合します(POST /v1/identities/claim)。tokenProvider がログイン後のトークンを返すようになった後に呼び出してください。
val res = GTNContact.claimGuest(guestCustomerId) // res.mergedConversations = 統合された会話数
プッシュトークン登録
// FCMトークン取得後・更新時に登録(POST /v1/push-tokens、platform: "android")
GTNContact.registerPushToken(fcmToken)
// ログアウト時に解除(DELETE /v1/push-tokens)
GTNContact.unregisterPushToken(fcmToken)
画面の埋め込み方
| エントリポイント | 用途 |
|---|---|
GTNContactActivity.start(activityContext:, sourceScreen:, contextMap:) | 最短導線。SDK内蔵Activityで画面一式を表示 |
SupportHomeScreen(sourceScreen, context, onClose) | 会話一覧+新規問い合わせのコンポーザブル。自前のCompose画面に埋め込む(内部でチャットへ遷移) |
ChatScreen(conversationId, initialMessages, initialConversation, onBack) | 特定の会話のチャット画面を直接埋め込む(プッシュ通知タップからの遷移など) |
// 会話一覧+新規問い合わせ(内部でチャット画面へ遷移)
SupportHomeScreen(
sourceScreen = "ホーム画面",
context = mapOf("契約プラン" to "プレミアム"),
onClose = { /* 閉じる */ },
)
// 特定の会話のチャット画面
ChatScreen(conversationId = conversationId)
機能リファレンス
会話
- 会話一覧(
GET /v1/conversations): 新しい順表示、プルリフレッシュ - 会話開始(
POST /v1/conversations):sourceScreenでフローが自動解決され、チケットも自動作成されます - ステータスは5種類で、日本語バッジ表示されます
| status | 表示 |
|---|---|
bot | ボット対応中 |
queued | 接続待ち |
agent | 対応中 |
waiting_customer | 返信待ち |
closed | 終了 |
構造化メッセージ(7種)
MessageBubbles が contentType に応じてレンダリングします。
| contentType | 内容 | 挙動 |
|---|---|---|
text | テキスト | 通常バブル |
buttons | 選択肢ボタン | タップで value をtext送信。最新メッセージのみ有効、selected は押下不可 |
form | 入力フォーム | 最新のみ編集可、過去分は読み取り専用 |
carousel | カルーセル | 横スクロールカード |
file | ファイル | SAS取得→外部アプリで表示 |
link_card | リンクカード | 外部ブラウザで開く |
system | システム通知 | 中央寄せグレー小文字 |
差分同期とページング(tail / before / after)
メッセージの seq は会話内で単調増加するカーソルです。GET /v1/conversations/{id}/messages は3つのクエリを受け付けます。
| パラメータ | 用途 | レスポンス |
|---|---|---|
tail=1 | 初回表示: 最新50件(limit 指定可)を取得 | prevCursor(次の before に渡すseq)と hasMoreBefore が返る |
before=<prevCursor> | 上スクロール: 過去メッセージを遡って取得 | 同上(さらに古い分があれば hasMoreBefore: true) |
after=<maxSeq> | 差分同期: 保持している最大seqより新しい分だけ取得 | nextCursor(次の after に渡すseq)と hasMore |
推奨パターン: 初回は tail=1 で最新50件 → 上スクロールで before=prevCursor を繰り返して遡り → 新着・再接続時は after=maxSeq で差分同期。
内蔵の ChatScreen は3つのカーソルをすべて使い分けます: 初回表示は getLatestMessages(tail=1)で最新50件のみを取得して最下部を表示し、上スクロールで先頭に到達すると getMessagesBefore(before=prevCursor)で古いページを追加読み込みします(firstVisibleItemの補正でスクロール位置は維持)。新着・再接続時の取りこぼし回復は従来どおり after=maxSeq の差分同期+id による重複排除です。REST APIを直接呼ぶ場合も同じ3カーソルが利用できます。
WebSocket(リアルタイム)
SocketClient が会話単位で GET /v1/ws?conversationId=<id>&token=<bearer token> に接続し、SharedFlow<SocketEvent> でイベントを配信します。
| イベント | 内容 |
|---|---|
Connected | 接続確立(再接続含む)。受信側は after=maxSeq で差分同期し、readを再送すること |
Disconnected | 切断(自動再接続が走る) |
MessageCreated | 新着メッセージ。自分の送信もエコーされるため id で重複排除する |
ConversationUpdated | status変更・エージェント参加など |
Typing | 入力中通知(SDKは送信側500msデバウンス、受信時「入力中…」表示) |
Read | 既読通知(senderType: AGENT なら自分のメッセージに既読表示) |
Presence | 閲覧者一覧(顧客UIでは未使用) |
ReactionUpdated | 絵文字リアクションの追加/削除。対象メッセージの全リアクションのスナップショットが届くため丸ごと置き換える |
- 自動再接続: 指数バックオフ 1s → 最大10s。再接続時は差分取得+既読再送を行います。
- 画面破棄時は必ず
close()を呼びます(内蔵UIは自動で処理)。
送達・既読
- 送信は
clientMessageIdでべき等。ネットワークエラー時は同一IDで最大3回自動リトライし、失敗時は手動再送できます。 - 表示は 送信中 → 送達(チェック)→ 既読 の3段階。既読は会話詳細の
readsとWSのReadイベントで更新されます。
絵文字リアクション
- バブルの長押しでクイックリアクション(👍 🙏 ❤️ 😂 🎉 😢)を付けられます。バブル下のチップをタップすると自分のリアクションをトグルできます。
- メニュー末尾の「+」から全絵文字ピッカー(Google公式
androidx.emoji2:emoji2-emojipicker)を開き、任意の絵文字を選べます(カテゴリ・最近使った絵文字・肌色バリアント対応)。 - API:
addReaction(conversationId, messageId, emoji)/removeReaction(conversationId, messageId, emoji)(POST/DELETE /v1/conversations/{id}/messages/{messageId}/reactions)。追加は同一絵文字の再送に対してべき等です。 - どちらも対象メッセージの全リアクション(スナップショット)を返します。他の参加者の変更はWSの
ReactionUpdatedで届きます。 - ViewModelでは
toggleReaction(message, emoji)/hasMyReaction(message, emoji)を利用できます(会話は単一顧客のためreactorType == CUSTOMERが自分の判定)。
添付(SASフロー)
createAttachmentUpload(conversationId, fileName, contentType)—POST /v1/conversations/{id}/attachmentsでアップロード用SASを発行uploadAttachment(sas, data, mimeType)— 返却されたuploadUrlへheaders(x-ms-blob-type等)付きで Azure Blobへ直接PUTsendMessage(contentType = MessageContentType.FILE)—blobName/fileName/fileSizeを含むfileメッセージを送信- 閲覧時は
attachmentDownloadUrl(conversationId, blobName)でダウンロード用SAS(60分有効)を取得し、外部アプリで表示
内蔵UIはPhoto Picker / ドキュメント選択からこのフローを自動実行します。
CSAT(満足度アンケート)
- 会話クローズ後、
CsatFormDialogがアンケートを表示します(入力欄は無効化されCSAT導線を表示)。 - 設問は
csatQuestions()(GET /v1/csat/questions、有効設問のみorder順)で取得。設問タイプはstar5/binary/nps/textの4種で、必須チェック付き。 - 回答は
submitCsat(conversationId, score, comment, answers)(POST /v1/conversations/{id}/csat)で送信します(クローズ後のみ・1会話1回。409回答済みにも対応)。
エラーハンドリング
エラーは GTNContactException(sealed class)で投げられ、message はそのままユーザーに表示できる日本語です。内蔵UIはアラート表示します。
| 例外 | 発生条件 | 日本語メッセージ |
|---|---|---|
NotConfigured | configure 未呼び出し | SDKが初期化されていません。GTNContact.configure を呼び出してください。 |
Network | ネットワーク到達不可・タイムアウト | 通信に失敗しました。ネットワーク接続を確認して、もう一度お試しください。 |
Unauthorized | トークン再取得後も401 | 認証に失敗しました。しばらくしてからもう一度お試しください。 |
ConversationClosed | 409 conversation_closed | この会話は終了しているため、メッセージを送信できません。 |
CsatAlreadySubmitted | 409 already_submitted | このアンケートにはすでに回答済みです。 |
ConversationNotClosed | 409 conversation_not_closed | 会話が終了していないため、アンケートに回答できません。 |
Server(status, code, detail) | その他のサーバーエラー | エラーが発生しました(code): detail |
Decoding | レスポンスの解析失敗 | サーバー応答の解析に失敗しました。 |
409 コード一覧
| HTTP | error コード | 意味 |
|---|---|---|
| 409 | conversation_closed | クローズ済み会話へのメッセージ送信 |
| 409 | already_submitted | CSATにすでに回答済み |
| 409 | conversation_not_closed | 未クローズ会話へのCSAT回答 |
サンプルアプリの起動方法
- APIサーバーをホストPCで起動(
http://localhost:3010) - インストール:
./gradlew :sample-app:installDebug
- アプリを起動 → 「デモユーザーでログイン」(
POST /v1/guestでトークンを自前取得しSDKへ供給) - 「ホーム画面から起動」または「請求明細画面から起動」(請求明細画面では選択肢ボタン付きフローが動く)
ビルド・テスト:
./gradlew :gtncontact-sdk:assembleDebug :sample-app:assembleDebug # ビルド
./gradlew :gtncontact-sdk:testDebugUnitTest # ユニットテスト
FAQ
エミュレータからローカルAPI(localhost)に接続できない
エミュレータからホストPCの localhost へは http://10.0.2.2:3010 で接続します。平文HTTPを使う場合はマニフェストでcleartextを許可してください(サンプルアプリは設定済み)。
<application android:usesCleartextTraffic="true">
Auth0 接続時に変更は必要?
SDK側のコード変更は不要で、tokenProvider の実装だけが変わります。
- devモード(サーバーの
ALLOW_DEV_AUTHが有効)ではdev:<customerId>形式のトークンをそのまま渡せます。 - サーバーに
AUTH0_DOMAINが設定されている場合、tokenProviderは Auth0が発行した顧客用JWT(AUTH0_CUSTOMER_AUDIENCEのaudience)を返すようにします。エージェント用audienceとは分離されています。 - トークン失効時はSDKの401リトライで
tokenProviderが再呼び出しされるので、そこでリフレッシュ済みトークンを返してください。 - ゲスト運用から移行する場合は、ログイン完了後に
GTNContact.claimGuest(guestCustomerId)でゲスト時代の会話を統合できます。