Initial commit: Voice controlled device application, specs and Gitea Actions workflow

This commit is contained in:
2026-08-10 08:26:00 +00:00
commit d449469116
69 changed files with 3209 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# プロジェクト構成定義書 (Project Configuration)
## 1. プロジェクト基本情報 (Basic Information)
- **プロジェクト名**: Android 音声操作端末アプリ (Voice-Controlled Android App)
- **対象パス**: `/root/.gemini/antigravity-cli/scratch/voice_controlled_device`
- **開発種別**: 新規開発 (New Development)
- **対象デバイス / OS**: Android OS 10.0+ (API Level 29 以上推奨)
- **ハードウェア設計の有無**: なし (市販/標準Android端末の上で動作するソフトウェア開発。内蔵マイク・スピーカー・Bluetoothオーディオ機器等を使用)
- **主なユースケース**: ハンズフリーによる音声認識・コマンド入力、対話システム、音声読み上げ (TTS)、端末状態管理
---
## 2. 技術スタック (Tech Stack)
- **プログラミング言語**: Kotlin (v1.9+)
- **UIフレームワーク**: Jetpack Compose (宣言型UI)
- **音声認識 (Speech Recognition)**: `SpeechRecognizer` API / Android Voice Interaction API
- **音声合成 (Text-to-Speech)**: `TextToSpeech` (TTS) Engine
- **非同期処理・状態管理**: Kotlin Coroutines, StateFlow / SharedFlow
- **アーキテクチャモデル**: Clean Architecture + MVVM (Model-View-ViewModel)
- **依存関係注入 (DI)**: Hilt / Koin
- **データ保持・キャッシュ**: Room Database / DataStore Preferences
---
## 3. テスト実行方式 & CI/CD (Testing & CI Execution)
- **テスト実行方式**:
- **ローカルテスト (Local Execution)**:
- 単体テスト: JUnit 5, MockK, Turbine (Flow/Coroutinesテスト)
- UI・結合テスト: Jetpack Compose Test Framework, Robolectric
- **CI実行環境 (CI Pipeline)**:
- Gitea Runner / GitHub Actions によるビルド自動化、静的解析 (ktlint/detekt)、単体テスト実行
- **テストスコープ**:
- Domain / Data レイヤーのユニットテスト完全自動化
- SpeechRecognizer / TTS ラッパーインターフェースのモックテスト
---
## 4. 推奨ディレクトリ構成 (Directory Structure)
```
voice_controlled_device/
├── docs/
│ ├── 00_project_config.md # 本構成定義ファイル
│ └── 02_specifications.md # 機能仕様書
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/voiceapp/
│ │ │ │ ├── data/ # リポジトリ・ローカルデータソース
│ │ │ │ ├── domain/ # ユースケース・モデル定義
│ │ │ │ ├── ui/ # Jetpack Compose UI / ViewModel
│ │ │ │ └── voice/ # SpeechRecognizer & TTS ラッパーモジュール
│ │ │ └── AndroidManifest.xml
│ │ └── test/ # 単体テストコード
└── build.gradle.kts
```
---
## 5. 次の工程への引継ぎ事項
- 機能仕様書 (`docs/02_specifications.md`) の作成に進みます。
+277
View File
@@ -0,0 +1,277 @@
# **システム機能・構成仕様書:Android Dockerビルド&Gitea CI/CD環境およびAI音声アシスタントアプリ (改訂完全統合版)**
## **1. システム概要および運用モデル**
### **1.1 背景と目的**
本仕様書は、Android OS 8.0+ (API Level 26 以上 / Target API Level 34) を対象とした「ハンズフリーAI音声ナビゲーション連携アシスタントアプリ」の設計要件、およびDockerコンテナとGitea Runner (Act Runner) を活用した自動ビルド・CI/CDパイプライン構築要件を統合定義する。
運転中や作業中の手動操作を排除し、Bluetoothヘッドセットや画面タップによる音声入力、AIサーバーとの連携(JSON API)、TTS音声応答(リアクション)、ならびに NaviCon / Google Maps 経由での車載ナビ目的地自動転送を安全かつ低コストで実現する。
### **1.2 システム運用・開発モデル**
1. **ローカル開発環境**: Docker Compose を用いて、ホスト環境に依存せずコンテナ内で Gradle ビルドおよび単体テストを実行可能とする。
2. **CI/CD環境**: Gitea への Push / Pull Request をトリガーとして Gitea Runner 上で同一コンテナ環境を起動し、自動テストおよび APK/AAB の生成を行う。
3. **エミュレータ・実機検証モデル**: Android Studio エミュレータ(Android 8.0〜14)での仮想動作確認を標準とし、最終動作を Bluetooth ヘッドセット(エレコム LBT-HSC41BK-EC 等)および車載ナビ連携環境にて検証する。
## **2. システム要件および技術スタック**
### **2.1 動作環境・開発基盤**
* **ターゲットプラットフォーム**: Linux (x86_64) コンテナ環境(ビルド用)
* **対象端末OS**: Android OS 8.0 (API Level 26) 以上推奨 / Target API Level 34 (Android 14)
* **開発言語・フレームワーク**: Kotlin 1.9+ / Jetpack Compose / Coroutines & Flow / Clean Architecture + MVVM + Hilt
* **コンテナ・CI/CD環境**: Docker Engine 20.10+ / Docker Compose V2 / Gitea Runner (Act Runner)
* **ビルドツール・JDK**: OpenJDK 17 (または AGP 互換 JDK 21) / Gradle (AGP 8.x 対応)
### **2.2 必須権限およびサービス宣言 (AndroidManifest.xml)**
* android.permission.RECORD_AUDIO (マイク音声入力)
* android.permission.INTERNET / ACCESS_NETWORK_STATE (AIサーバー通信・ネットワーク確認)
* android.permission.ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION (GPS現在地情報付与)
* android.permission.MODIFY_AUDIO_SETTINGS (オーディオフォーカス・音量制御)
* android.permission.POST_NOTIFICATIONS (通知出力)
* android.permission.BIND_NOTIFICATION_LISTENER_SERVICE (未読メッセージ・通知本文取得用バインド権限)
* android.permission.SCHEDULE_EXACT_ALARM (アラーム・タイマー設定権限)
* android.permission.FOREGROUND_SERVICE / android.permission.FOREGROUND_SERVICE_MICROPHONE / android.permission.FOREGROUND_SERVICE_LOCATION (Android 14 / API 34 必須フォアグラウンドサービス権限)
## **3. アプリ主要機能仕様**
### **3.1 画面仕様 (Jetpack Compose UI)**
シングルアクティビティ構成のレスポンシブなJetpack Composeレイアウトを採用する。
1. **ステータス表示エリア (Top / Hero Banner)**
* アプリの状態 (Idle, Listening, Processing, Speaking, Error) を色とアイコンで視覚表示。
* Listening 時はマイク入力音量レベル (RmsdB) に同期する波形アニメーション (WaveformIndicator) を描画 (60fps)。
2. **対話・コマンド履歴エリア (Center Scroll Area)**
* ユーザー発話テキストとシステム応答テキスト(目的地情報含む)をタイムスタンプ付きチャット風UI (LazyColumn) で表示。
3. **音声操作コントロールエリア (Bottom Floating Area)**
* マイクボタン (FAB): タップにより音声認識の開始/停止をトグル。
* クリアボタン: 会話履歴のリセット。
4. **パーミッション誘導ダイアログ**
* マイク権限未許可時に許可リクエストを表示。永久拒否時は端末設定画面 (ACTION_APPLICATION_DETAILS_SETTINGS) への移行ダイアログを提示。
### **3.2 音声処理・オーディオ制御仕様**
#### **3.2.1 起動トリガー仕様**
* 常時マイク監視(WakeWord検知)によるバッテリー消費を防ぐため、トリガーを以下に一本化する。
1. 画面上のマイクボタンタップ
2. Bluetoothヘッドセット (LBT-HSC41BK-EC 等) のボタン操作・メディアキーインテント (Intent.ACTION_MEDIA_BUTTON / ACTION_VOICE_COMMAND)
* **バックグラウンド起動制限 (Background Activity Launch restrictions) 対策**:
Android 10+ ではスリープ中・バックグラウンドからのダイレクト Activity 起動が制限されるため、常駐フォアグラウンドサービス (`VoiceAssistantService`) および `MediaSessionCompat` を併用する。Bluetooth メディアボタン押下時は `MediaSessionCompat` の Callback にてイベントを補捉し、フォアグラウンドサービス経由で音声認識および TTS 処理を開始する。
#### **3.2.2 音声認識モジュール (IVoiceRecognizer / VoiceRecognitionManager)**
* **技術方式**: Android標準 android.speech.SpeechRecognizer のラッパーモジュール。
* **インターフェース定義**:
interface IVoiceRecognizer {
val state: StateFlow<VoiceRecognitionState>
val rmsDb: StateFlow<Float>
fun startListening()
fun stopListening()
fun destroy()
}
* **排他制御・フィードバック防止**:
* ITextToSpeech の状態が Speaking の間は startListening() を呼び出し不可とし、スピーカー音声の誤認識ループを防止。
#### **3.2.3 音声合成 (TTS) モジュール (ITextToSpeech / TextToSpeechManager)**
* **技術方式**: Android標準 android.speech.tts.TextToSpeech のラッパーモジュール。
* **状態管理の単一化**:
* 状態変化は val status: StateFlow<TtsStatus> に集約。speak() は Unit を返す。
sealed interface TtsStatus {
object Idle : TtsStatus
object Speaking : TtsStatus
object Completed : TtsStatus
data class Error(val message: String) : TtsStatus
}
interface ITextToSpeech {
val status: StateFlow<TtsStatus>
fun speak(text: String, queueMode: Int = TextToSpeech.QUEUE_FLUSH)
fun stop()
fun destroy()
}
* **オーディオフォーカス制御**:
* speak() 実行時に AudioManager.requestAudioFocus (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) を呼び出し、他アプリの音量をミュート/減衰。発話完了 (UtteranceProgressListener.onDone) 時に abandonAudioFocus を呼出し解放する。
### **3.3 AIサーバー連携およびコマンドパース仕様**
#### **3.3.1 AIサーバー通信インターフェース仕様 (JSON API)**
* **エンドポイント**: POST /api/v1/navigate
* **認証ヘッダー**: `Authorization: Bearer <API_KEY>` または `X-API-Key: <API_KEY>`
* **通信タイムアウト & リトライ方針**:
* Connect Timeout: 5,000 ms / Read Timeout: 10,000 ms
* ネットワークエラー発生時は Exponential Backoff(初期遅延 1秒、倍率 2、最大 3 回試行)でリトライを実施。
* **リクエスト構造 (App -> Server)**:
```json
{
"user_id": "string",
"query_text": "仕事で行く〇〇会社に設定して",
"current_location": {
"latitude": 35.681236,
"longitude": 139.767125
},
"timestamp": 1700000000
}
```
* **成功レスポンス構造 (Server -> App / HTTP 200)**:
```json
{
"status": "success",
"tts_message": "〇〇会社を目的地にセットします。",
"destination": {
"name": "〇〇会社 本社",
"address": "東京都千代田区丸の内1-1-1",
"latitude": 35.681236,
"longitude": 139.767125
}
}
```
* **エラーレスポンス構造 (Server -> App / HTTP 400, 404, 500)**:
```json
{
"status": "error",
"error_code": "LOCATION_NOT_FOUND",
"message": "該当する目的地を見つけることができませんでした。"
}
```
#### **3.3.2 漢数字・表記揺れコンバータ (KanjiToNumberConverter)**
* 正規表現マッチングの前に、入力テキスト内の漢数字(「一」「二」「三分」「七時」等)および全角数字をアラビア数字(「1」「2」「3分」「7時」)へ事前変換する処理を実行する。
#### **3.3.3 コマンドパターンおよびパラメータ抽出定義**
| コマンド識別子 | 正規表現パターン (前処理後) | 抽出パラメータキー | 実行処理・連携内容 | 音声応答メッセージ例 |
| :---- | :---- | :---- | :---- | :---- |
| NAVIGATE_AI | `.*(ナビ \| 行きたい \| 向かう \| セット).*` | query_text | AIサーバー通信 & NaviCon転送 | tts_message (レスポンス依存) |
| GET_TIME_DATE | `.*(今何時 \| 何時ですか \| 日付 \| 今日は何日).*` | なし | システム日時取得 | 「現在は14時30分です」 |
| SET_ALARM_TIMER | `.*((\d+)\s*(分 \| 秒 \| 時間)タイマー \| (\d+)時にアラーム).*` | timer_val, unit / alarm_hour | アラーム・タイマーインテント | 「5分タイマーをセットしました」 |
| CHANGE_SETTINGS | `.*(音量を \| ダークモード \| ライトモード).*` | setting_type, value | 設定変更 | 「音量を変更しました」 |
| READ_MESSAGES | `.*(メッセージ \| 未読 \| 通知).*` | なし | AppNotificationListenerService 連携 | 「未読メッセージは〇件です」 |
| GET_SYSTEM_INFO | `.*(バッテリー \| 電池 \| Wi-Fi \| ネットワーク).*` | info_type | バッテリー/通信状態取得 | 「バッテリー残量は80%です」 |
| UNKNOWN_FALLBACK | 上記いずれにも未マッチ | なし | ガイダンス応答 | 「申し訳ありません。よく理解できませんでした。」 |
### **3.4 外部ナビ連携仕様 (NaviCon & Google Maps)**
1. **NaviCon連携 (メイン軸 / 完全無料)**:
* AIサーバーから目的地の緯度・経度・地点名を受信後、URLスキームを発行して起動。
* 地点名 (`name`) パラメータは必ず UTF-8 URL エンコード(`URLEncoder.encode(name, "UTF-8")`)を行う。
* フォーマット: `navicon://point?ll={latitude},{longitude}&title={encoded_name}`
2. **Google Maps連携 (フォールバック / 完全無料)**:
* NaviCon未インストール時またはユーザー選択時、標準Intent(`geo:{latitude},{longitude}?q={encoded_name}`)を発行して起動。
3. **未インストール時の自動フォールバック順序**:
* パッケージ管理 (`PackageManager.getPackageInfo("jp.co.denso.navicon.user", 0)`) により NaviCon インストール状態を確認する。
* (1) NaviCon が検出可能 -> NaviCon Intent を発行。
* (2) NaviCon 未検出 且つ Google Maps が利用可能 -> 音声で「NaviConが未検出のため、Google Mapsで表示します」とアナウンスし、Google Maps Intent を発行。
* (3) いずれも未検出時 -> Playストアの NaviCon ページ (`market://details?id=jp.co.denso.navicon.user`) を起動。
### **3.5 未読メッセージ・通知取得サービスおよびフォアグラウンドサービス仕様**
1. **AppNotificationListenerService (未読メッセージ読み上げ)**:
* 他アプリの通知本文を読み上げるため、NotificationListenerService を継承し AndroidManifest.xml に定義する。
2. **VoiceAssistantService (常駐フォアグラウンドサービス)**:
* Android 14 (API 34) 準拠のフォアグラウンドサービスとして定義し、`foregroundServiceType="microphone|location"` を付与する。
* `AndroidManifest.xml` 定義例:
```xml
<service
android:name=".service.VoiceAssistantService"
android:exported="false"
android:foregroundServiceType="microphone|location" />
<service
android:name=".service.AppNotificationListenerService"
android:label="メッセージ読み上げサービス"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
```
## **4. 状態遷移設計 (State Machine Design)**
### **4.1 アプリケーション状態モデル (VoiceAppState)**
sealed interface VoiceAppState {
object Idle : VoiceAppState
data class Listening(val rmsDb: Float = 0f) : VoiceAppState
object Processing : VoiceAppState
data class Speaking(val text: String) : VoiceAppState
data class Error(val message: String, val errorCode: Int? = null) : VoiceAppState
}
### **4.2 状態遷移およびエラー時 3秒自動復帰制御**
* MainViewModel において、VoiceAppState.Error へ遷移した場合は viewModelScope.launch 内で delay(3000) を実行し、ユーザーの手動操作なしで3秒後に自動的に VoiceAppState.Idle へ安全に復帰させる。
stateDiagram-v2
[*] --> Idle
Idle --> Listening : マイクタップ / Bluetoothボタン
Listening --> Processing : 音声入力完了 (onResults)
Listening --> Error : 認識エラー / タイムアウト (onError)
Listening --> Idle : キャンセル操作
Processing --> Speaking : AI応答 / コマンド解析成功 (TTS開始)
Processing --> Error : AI通信エラー / 解析失敗
Speaking --> Idle : TTS再生完了 (onDone)
Speaking --> Error : TTS出力エラー
Error --> Idle : 3秒経過による自動復帰 (delay 3000ms)
## **5. ディレクトリ構造および成果物定義**
### **5.1 プロジェクトディレクトリ構造**
.
├── .gitea/
│ └── workflows/
│ └── build.yaml # Gitea Runner 用 CI/CD ワークフロー定義ファイル
├── docker/
│ ├── Dockerfile # Android ビルド環境定義 Dockerfile (Ubuntu 22.04 + OpenJDK 17 + Android SDK)
│ └── entrypoint.sh # 権限・初期化処理スクリプト
├── app/ # Android アプリソースコード (Kotlin / Jetpack Compose)
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── java/com/example/voiceapp/
│ │ │ ├── VoiceApplication.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── di/ # Hilt DI モジュール (VoiceModule, DomainModule)
│ │ │ ├── domain/ # UseCases, CommandParser, KanjiToNumberConverter
│ │ │ │ ├── model/ # CommandId, CommandMatchResult, CommandResult
│ │ │ │ ├── executor/ # NavigateAiExecutor, GetTimeDateExecutor, etc.
│ │ │ │ └── converter/ # KanjiToNumberConverter
│ │ │ ├── voice/ # SpeechRecognizer / TTS ラッパー
│ │ │ ├── service/ # VoiceAssistantService, AppNotificationListenerService
│ │ │ └── ui/ # Jetpack Compose UI / ViewModels
│ │ └── AndroidManifest.xml
│ └── test/ # ユニットテスト (JUnit 5, MockK, Turbine)
├── docker-compose.yml # ローカル環境用 Docker 実行定義ファイル
└── README.md # 仕様書・環境構築/実行マニュアル
### **5.2 Gitea CI/CD 実行モデル仕様**
* **Runner 構成**: Gitea Act Runner の Docker Runner モデルを採用。
* **実行環境**: Push / Pull Request 発生時、Runner 上で `docker/Dockerfile` にて生成されるコンテナイメージを起動。
* **自動パイプライン**: コンテナ内で `./gradlew testDebugUnitTest` (単体テスト) および `./gradlew assembleDebug` (Debug APK 生成) を逐次実行し、ビルド成果物をアーティファクトとして保存する。
## **6. 受入・評価基準 (Acceptance Criteria)**
1. **CI/CDパイプラインビルドの成功**: Gitea Runner 上で ./gradlew testDebugUnitTest および ./gradlew assembleDebug が正常終了し、エラーなくAPKが出力されること。
2. **単体テスト網羅性**:
* KanjiToNumberConverterTest: 漢数字(「三分」「7時」「15分」等)の変換正確性を検証。
* CommandParserTest: アラーム、タイマー、NAVIGATE_AI 含む全コマンドの正規表現パラメータ抽出を検証。
* MainViewModelTest: Turbine を用い、正常系フローおよび Error 遷移から3秒後に自動 Idle 復帰する非同期状態遷移を検証。
3. **堅牢性とエラーハンドリング**: モック応答環境において、Listening -> Processing -> Speaking -> Idle への単方向フローが一切クラッシュなく完了し、NaviCon未インストール時もGoogle Mapsへ安全にフォールバックすること。
+957
View File
@@ -0,0 +1,957 @@
# ハードウェア・システム詳細設計書 (Voice-Controlled Android App)
## 1. 使用部品・コンポーネント一覧
### 1.1 ハードウェア・コンポーネント構成 (Hardware System Layer)
1. **メイン演算・制御ユニット (SoC / Core Processor)**
- 対象端末: Android OS 8.0+ (API Level 26 以上 / Target API Level 34) 搭載の標準 Android 端末 / モジュール (クアッドコア ARM64 2.0GHz 以上推奨)
- RAM: 3GB 以上
- ROM/ストレージ: 32GB 以上 (アプリ本体・対話ログ・ローカルモデル保持用)
2. **音声入力モジュール (Audio Input Device)**
- 内蔵マイクアレイ (Dual/Multi-Microphone Array) または Bluetooth 外部 HFP/A2DP マイク (エレコム LBT-HSC41BK-EC 等)
- 入力仕様: PCM 16kHz / 16-bit モノラル (Android 音声認識 `SpeechRecognizer` 前処理入力)
3. **音声出力モジュール (Audio Output Device)**
- 内蔵スピーカー / D級オーディオアンプ または Bluetooth 外部 A2DP スピーカー / 車載ナビゲーションシステム
- 出力仕様: PCM 44.1kHz / 48kHz 16-bit ステレオ (Android `TextToSpeech` 合成出力)
4. **表示・操作インターフェース (Display & Touch Controller)**
- ディスプレイ: 5インチ〜10インチ静電容量式タッチパネル (1080x1920 解像度等)
- フィードバック: GPU アクセラレーション対応 (Jetpack Compose 60fps 描画)
5. **通信モジュール (Network Communications)**
- Wi-Fi (802.11 a/b/g/n/ac) / LTE (4G/5G) セルラー通信モジュール (AI サーバー REST API 通信用)
6. **外部入力デバイス (Bluetooth Media Control Button)**
- Bluetooth ヘッドセットボタン / リモートメディアキー (AVRCP プロファイル / `Intent.ACTION_MEDIA_BUTTON` / `ACTION_VOICE_COMMAND`)
### 1.2 ソフトウェア・アーキテクチャ概要 (Clean Architecture + MVVM + Foreground Service)
本システムは Clean Architecture, MVVM (Model-View-ViewModel), および常駐 Foreground Service + `MediaSessionCompat` を組み合わせたアーキテクチャ構造を採用する。レイヤー間の依存関係を一方向 (UI/Service -> Domain <- Voice Engine/Network Layer) に制限し、単体テスト可能性およびバックグラウンド実行時の起動制御・堅牢性を保証する。
- **UI / Presentation Layer**: Jetpack Compose によるシングルアクティビティ型 UI、`MainViewModel` による状態管理と `VoiceAppState` (Sealed Interface) に基づく単方向データフロー (UDF)。エラー発生時は `delay(3000)` による 3秒後自動 Idle 復帰制御を実施。
- **Domain Layer**: 純粋 Kotlin によるコアビジネスロジック。`KanjiToNumberConverter` (前処理コンバータ)、`ExecuteCommandUseCase`、`CommandParser`、`CommandDefinition`、`CommandId` (`NAVIGATE_AI` 含む)、および各種 `ICommandExecutor`。
- **Voice Engine Layer**: Android プラットフォーム固有 API のラッパーモジュール。`IVoiceRecognizer` (`VoiceRecognitionManager`)、`ITextToSpeech` (`TextToSpeechManager`)。`ITextToSpeech.speak()` は `Unit` を返し `StateFlow<TtsStatus>` で一元状態管理。
- **Network Layer**: Retrofit 2 + OkHttp 4 による AI サーバー REST API クライアント (`AiApiService`)。認証ヘッダー、タイムアウト設定、Exponential Backoff 自動リトライロジックを保持。
- **Service / Background Layer**: Android 14 (API 34) 準拠の常駐フォアグラウンドサービス `VoiceAssistantService` (`foregroundServiceType="microphone|location"`) および `MediaSessionCompat` による Bluetooth メディアボタン捕捉・バックグラウンド起動制限 (BAL) 回避モジュール。および `AppNotificationListenerService` (未読メッセージ取得)。
### 1.3 ソフトウェアコンポーネント・モジュール詳細
#### 1.3.1 Voice Engine Layer
- **`IVoiceRecognizer`**: `android.speech.SpeechRecognizer` のラッパー抽象インターフェース。音声入力の開始/停止、認識結果 (Flow/Callback)、マイク音量 (RmsdB) のリアルタイム更新を管理。
- **`VoiceRecognitionManager`**: `IVoiceRecognizer` の実装クラス。`RecognitionListener` を内部リスナーとして保持。
- **`ITextToSpeech`**: `android.speech.tts.TextToSpeech` のラッパー抽象インターフェース。`speak(text: String, queueMode: Int = TextToSpeech.QUEUE_FLUSH): Unit` のシグネチャを持ち、戻り値型を `Unit` に統一。
- **`TextToSpeechManager`**: `ITextToSpeech` の実装クラス。`UtteranceProgressListener` を用いて `Speaking` から `Idle` への状態遷移を管理。
#### 1.3.2 Network Layer (AI API Communication)
- **`AiApiService`**: Retrofit 2 インターフェース。POST `/api/v1/navigate` を提供。
- **データモデル**:
- `AiApiRequest`: `user_id`, `query_text`, `current_location` (latitude, longitude), `timestamp`
- `AiApiResponse`: `status` ("success"), `tts_message`, `destination` (name, address, latitude, longitude)
- `AiApiErrorResponse`: `status` ("error"), `error_code`, `message`
- **通信信頼性設計**:
- 認証ヘッダー: `Authorization: Bearer <API_KEY>` または `X-API-Key: <API_KEY>`
- タイムアウト: Connect Timeout 5,000ms / Read Timeout 10,000ms
- 自動リトライ: Exponential Backoff (初期遅延 1,000ms, 倍率 2.0, 最大 3 回試行)
#### 1.3.3 Domain Layer
- **`KanjiToNumberConverter`**: `domain/converter/KanjiToNumberConverter` に配置。正規表現パース前の事前処理として「一」「二」「三分」「七時」等の漢数字および全角数字を「1」「2」「3分」「7時」等のアラビア数字に前処理変換。
- **`CommandId`**: コマンド識別子 Enum (`GET_TIME_DATE`, `SET_ALARM_TIMER`, `CHANGE_SETTINGS`, `READ_MESSAGES`, `GET_SYSTEM_INFO`, `NAVIGATE_AI`, `UNKNOWN_FALLBACK`)。
- **`CommandParser`**: 事前変換後のテキストを入力とし、正規表現マッチングおよびパラメータ抽出を実行。
- **`ExecuteCommandUseCase`**: コマンド解析と実行フローの制御。
- **`ICommandExecutor` & 実装クラス**:
- `GetTimeDateExecutor`: システム日時の取得。
- `SetAlarmTimerExecutor`: アラーム・タイマーのインテント発行。
- `ChangeSettingsExecutor`: 設定変更。
- `ReadMessagesExecutor`: NotificationListener 連携による未読メッセージ取得。
- `GetSystemInfoExecutor`: バッテリー/通信状態の取得。
- `NavigateAiExecutor`: AI サーバー API 通信を行い、目的地の取得後に NaviCon (`navicon://point...`) URL スキーム生成 (UTF-8 URLエンコード `URLEncoder.encode(name, "UTF-8")` 適用) および Google Maps / Play Store への段階的フォールバックを実行。
- `UnknownFallbackExecutor`: ガイダンス応答。
#### 1.3.4 Service Layer & Background Launch
- **`VoiceAssistantService`**: `foregroundServiceType="microphone|location"` 指定の常駐 Service。通知バーにインジケータを常駐表示し、バックグラウンドでの音声認識・TTS実行基盤となる。
- **`MediaSessionCompat` & Callback**: Bluetooth メディアボタン (`Intent.ACTION_MEDIA_BUTTON` / `ACTION_VOICE_COMMAND`) を `MediaSessionCompat` の KeyEvent リスナーで直接受託。Android 10+ の Background Activity Launch Restrictions (バックグラウンド起動制限) を回避し、常駐 Service 内から音声認識を即座に開始。
- **`AppNotificationListenerService`**: `android.permission.BIND_NOTIFICATION_LISTENER_SERVICE` を持つ NotificationListenerService。他アプリの通知本文から未読メッセージを取得。
#### 1.3.5 UI / Presentation Layer
- **`MainViewModel`**: アプリ状態の統合プロセスマネージャー。`VoiceAppState.Error` 遷移時には `viewModelScope` 内で `delay(3000)` を実行し、3秒後に自動で `VoiceAppState.Idle` へ無害に復帰させる。
- **`VoiceAppState`**: `Idle`, `Listening(val rmsDb: Float)`, `Processing`, `Speaking(val text: String)`, `Error(val message: String, val errorCode: Int?)`
- **Compose Components**: `MainScreen`, `StatusBanner`, `WaveformIndicator`, `ConversationHistory`, `ControlBar`, `PermissionDialog`
## 2. ピン配置・インターフェース定義 (GPIO, SPI, I2C, UART, 電圧レベル等)
### 2.1 物理・論理インターフェース & バス・信号レベル定義
| インターフェース名 | 物理/論理区分 | 信号規格・データ形式 | 物理電圧 / バスレベル | 制御・連携方式 |
|---|---|---|---|---|
| **マイク音声入力バス (Audio In)** | 物理 / 論理 | PCM 16kHz, 16-bit, Mono | 1.8V / 3.3V (ADC / I2S 内蔵バス) | `AudioRecord` / `SpeechRecognizer` API via Binder IPC |
| **スピーカー音声出力バス (Audio Out)** | 物理 / 論理 | PCM 44.1kHz / 48kHz, 16-bit, Stereo | D級アンプ駆動 / DAC 出力 | `AudioTrack` / `TextToSpeech` API via AudioFlinger |
| **タッチパネル / ディスプレイ (I/O)** | 物理 | MIPI DSI / SPI / I2C (Touch) | 1.8V / 3.3V GPIO | Android Input subsystem & SurfaceFlinger / Compose GPU |
| **AI サーバー REST API (Network IPC)** | 論理 | HTTPS / JSON (POST /api/v1/navigate) | TCP/IP Port 443 (Wi-Fi/Cellular) | Retrofit 2 + OkHttp 4 (Auth Header, 5s/10s Timeout, Exp. Backoff) |
| **NaviCon / Google Maps / Play Store IPC** | 論理 | Android Intent / URL Scheme (`navicon://point...`, `geo:...`, `market://...`) | OS Binder IPC / Intent Subsystem | `URLEncoder.encode(name, "UTF-8")` 適用 Intent 発行 & パッケージ検出 |
| **Bluetooth メディアボタン (AVRCP)** | 物理 / 論理 | Bluetooth HID/AVRCP (`ACTION_MEDIA_BUTTON`) | 2.4GHz RF (Bluetooth HFP/A2DP) | `MediaSessionCompat.Callback` キーハンドリング via `VoiceAssistantService` |
| **位置情報 API (GPS / Location)** | 論理 | Android Location Manager / FusedLocationProviderClient | OS Binder IPC | `ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION` |
| **アラーム・タイマー API (AlarmManager)** | 論理 | Android AlarmManager | OS Binder IPC | `SCHEDULE_EXACT_ALARM` |
| **通知受託 API (NotificationListener)** | 論理 | NotificationListenerService IPC | OS Binder IPC | `BIND_NOTIFICATION_LISTENER_SERVICE` |
### 2.2 クラス構造と抽象インターフェース設計 (Kotlin / Clean Architecture)
#### 2.2.1 クラス図 (Mermaid Diagram)
```mermaid
classDiagram
namespace UI_Layer {
class MainScreen {
+ComposableContent()
}
class MainViewModel {
-ExecuteCommandUseCase executeCommandUseCase
-IVoiceRecognizer voiceRecognizer
-ITextToSpeech textToSpeech
+StateFlow~VoiceAppState~ uiState
+StateFlow~List~ChatMessage~~ conversationHistory
+onMicButtonClicked()
+onPermissionGranted()
+clearHistory()
}
class VoiceAppState {
<<sealed interface>>
}
}
namespace Domain_Layer {
class ExecuteCommandUseCase {
-KanjiToNumberConverter kanjiConverter
-CommandParser commandParser
+invoke(String text) Flow~CommandResult~
}
class KanjiToNumberConverter {
+convert(String text) String
}
class CommandParser {
-List~CommandDefinition~ commands
+parse(String text) CommandMatchResult
}
class CommandDefinition {
+CommandId id
+List~Regex~ patterns
+ICommandExecutor executor
}
class CommandId {
<<enum>>
GET_TIME_DATE
SET_ALARM_TIMER
CHANGE_SETTINGS
READ_MESSAGES
GET_SYSTEM_INFO
NAVIGATE_AI
UNKNOWN_FALLBACK
}
class ICommandExecutor {
<<interface>>
+execute(CommandMatchResult match) CommandResult
}
class NavigateAiExecutor {
-AiApiService apiService
-Context context
+execute(CommandMatchResult match) CommandResult
}
}
namespace Voice_Engine_Layer {
class IVoiceRecognizer {
<<interface>>
+StateFlow~VoiceRecognitionState~ state
+StateFlow~Float~ rmsDb
+startListening()
+stopListening()
+destroy()
}
class VoiceRecognitionManager {
-SpeechRecognizer speechRecognizer
}
class ITextToSpeech {
<<interface>>
+StateFlow~TtsStatus~ status
+speak(String text, Int queueMode) Unit
+stop()
+destroy()
}
class TextToSpeechManager {
-TextToSpeech ttsEngine
}
}
namespace Network_Layer {
class AiApiService {
<<interface>>
+navigate(AiApiRequest request) AiApiResponse
}
class AiApiRequest {
+String userId
+String queryText
+LocationData currentLocation
+Long timestamp
}
class AiApiResponse {
+String status
+String ttsMessage
+DestinationData destination
}
}
namespace Service_Layer {
class VoiceAssistantService {
-MediaSessionCompat mediaSession
+onStartCommand()
}
class AppNotificationListenerService {
+onNotificationPosted()
}
}
MainScreen ..> MainViewModel : Observe / Event
MainViewModel --> ExecuteCommandUseCase : Invoke
MainViewModel --> IVoiceRecognizer : Control
MainViewModel --> ITextToSpeech : Control
ExecuteCommandUseCase --> KanjiToNumberConverter : Pre-process
ExecuteCommandUseCase --> CommandParser : Use
CommandParser --> CommandDefinition : Contains
CommandDefinition --> ICommandExecutor : Delegates
ICommandExecutor <|.. NavigateAiExecutor : Implements
NavigateAiExecutor --> AiApiService : Call API
IVoiceRecognizer <|.. VoiceRecognitionManager : Implements
ITextToSpeech <|.. TextToSpeechManager : Implements
VoiceAssistantService --> IVoiceRecognizer : Trigger
```
#### 2.2.2 抽象インターフェース&データモデル定義 (Kotlin コード詳細)
##### 1. Voice Engine Layer インターフェース (`speak()` 戻り値 Unit 統一)
```kotlin
package com.example.voiceapp.voice
import kotlinx.coroutines.flow.StateFlow
sealed interface VoiceRecognitionState {
object Idle : VoiceRecognitionState
object Ready : VoiceRecognitionState
object Listening : VoiceRecognitionState
data class Success(val recognizedText: String) : VoiceRecognitionState
data class Error(val errorCode: Int, val message: String) : VoiceRecognitionState
}
interface IVoiceRecognizer {
val state: StateFlow<VoiceRecognitionState>
val rmsDb: StateFlow<Float>
fun startListening()
fun stopListening()
fun destroy()
}
sealed interface TtsStatus {
object Idle : TtsStatus
object Speaking : TtsStatus
object Completed : TtsStatus
data class Error(val message: String) : TtsStatus
}
interface ITextToSpeech {
val status: StateFlow<TtsStatus>
fun speak(text: String, queueMode: Int = 0): Unit
fun stop()
fun destroy()
}
```
##### 2. Network Layer API 定義 & データ構造
```kotlin
package com.example.voiceapp.data.api
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
data class LocationData(
val latitude: Double,
val longitude: Double
)
data class AiApiRequest(
val user_id: String,
val query_text: String,
val current_location: LocationData?,
val timestamp: Long
)
data class DestinationData(
val name: String,
val address: String,
val latitude: Double,
val longitude: Double
)
data class AiApiResponse(
val status: String,
val tts_message: String,
val destination: DestinationData?
)
data class AiApiErrorResponse(
val status: String,
val error_code: String,
val message: String
)
interface AiApiService {
@POST("api/v1/navigate")
suspend fun navigate(
@Header("Authorization") authHeader: String,
@Body request: AiApiRequest
): AiApiResponse
}
```
##### 3. Domain Layer インターフェース、漢数字コンバータ、コマンド構造
```kotlin
package com.example.voiceapp.domain.converter
class KanjiToNumberConverter {
private val kanjiMap = mapOf(
'零' to '0', '一' to '1', '二' to '2', '三' to '3', '四' to '4',
'五' to '5', '六' to '6', '七' to '7', '八' to '8', '九' to '9',
'0' to '0', '1' to '1', '2' to '2', '3' to '3', '4' to '4',
'5' to '5', '6' to '6', '7' to '7', '8' to '8', '9' to '9'
)
fun convert(input: String): String {
val sb = StringBuilder()
for (char in input) {
val replaced = kanjiMap[char]
if (replaced != null) {
sb.append(replaced)
} else {
sb.append(char)
}
}
return sb.toString()
}
}
```
```kotlin
package com.example.voiceapp.domain.model
enum class CommandId {
GET_TIME_DATE,
SET_ALARM_TIMER,
CHANGE_SETTINGS,
READ_MESSAGES,
GET_SYSTEM_INFO,
NAVIGATE_AI,
UNKNOWN_FALLBACK
}
data class CommandMatchResult(
val commandId: CommandId,
val matchedPattern: String,
val extractedParameters: Map<String, String>,
val rawText: String
)
data class CommandResult(
val commandId: CommandId,
val isSuccess: Boolean,
val responseText: String,
val data: Map<String, Any>? = null
)
interface ICommandExecutor {
suspend fun execute(matchResult: CommandMatchResult): CommandResult
}
data class CommandDefinition(
val id: CommandId,
val patterns: List<Regex>,
val executor: ICommandExecutor
)
```
```kotlin
package com.example.voiceapp.domain
import com.example.voiceapp.domain.converter.KanjiToNumberConverter
import com.example.voiceapp.domain.model.*
class CommandParser(private val definitions: List<CommandDefinition>) {
fun parse(text: String): CommandMatchResult {
for (def in definitions) {
for (pattern in def.patterns) {
val match = pattern.find(text)
if (match != null) {
val params = match.groups.mapIndexedNotNull { index, group ->
if (index > 0 && group != null) "param_$index" to group.value else null
}.toMap()
return CommandMatchResult(def.id, pattern.pattern, params, text)
}
}
}
return CommandMatchResult(CommandId.UNKNOWN_FALLBACK, "", emptyMap(), text)
}
}
class ExecuteCommandUseCase(
private val kanjiConverter: KanjiToNumberConverter,
private val commandParser: CommandParser
) {
suspend operator fun invoke(inputText: String): CommandResult {
val normalizedText = kanjiConverter.convert(inputText)
val matchResult = commandParser.parse(normalizedText)
return matchResult.executor.execute(matchResult)
}
}
```
##### 4. Presentation Layer 状態・メッセージモデル
```kotlin
package com.example.voiceapp.ui
import com.example.voiceapp.domain.model.CommandId
sealed interface VoiceAppState {
object Idle : VoiceAppState
data class Listening(val rmsDb: Float = 0f) : VoiceAppState
object Processing : VoiceAppState
data class Speaking(val text: String) : VoiceAppState
data class Error(val message: String, val errorCode: Int? = null) : VoiceAppState
}
enum class MessageSender { USER, SYSTEM }
data class ChatMessage(
val id: String = java.util.UUID.randomUUID().toString(),
val sender: MessageSender,
val text: String,
val timestamp: Long = System.currentTimeMillis(),
val commandId: CommandId? = null
)
```
#### 2.2.3 依存性注入 (DI: Hilt Framework Design)
```kotlin
package com.example.voiceapp.di
import android.content.Context
import com.example.voiceapp.data.api.AiApiService
import com.example.voiceapp.domain.*
import com.example.voiceapp.domain.converter.KanjiToNumberConverter
import com.example.voiceapp.domain.executor.*
import com.example.voiceapp.domain.model.*
import com.example.voiceapp.voice.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object VoiceModule {
@Provides
@Singleton
fun provideVoiceRecognizer(
@ApplicationContext context: Context
): IVoiceRecognizer = VoiceRecognitionManager(context)
@Provides
@Singleton
fun provideTextToSpeech(
@ApplicationContext context: Context
): ITextToSpeech = TextToSpeechManager(context)
}
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(5000, TimeUnit.MILLISECONDS)
.readTimeout(10000, TimeUnit.MILLISECONDS)
.addInterceptor { chain ->
var request = chain.request()
val apiKey = "YOUR_API_KEY_HERE"
request = request.newBuilder()
.header("Authorization", "Bearer $apiKey")
.build()
// Exponential Backoff Retry (Max 3 Tries)
var response = chain.proceed(request)
var tryCount = 0
var backoffDelay = 1000L
while (!response.isSuccessful && tryCount < 3) {
tryCount++
Thread.sleep(backoffDelay)
backoffDelay *= 2
response.close()
response = chain.proceed(request)
}
response
}
.build()
}
@Provides
@Singleton
fun provideAiApiService(okHttpClient: OkHttpClient): AiApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(AiApiService::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
object DomainModule {
@Provides
@Singleton
fun provideKanjiToNumberConverter(): KanjiToNumberConverter {
return KanjiToNumberConverter()
}
@Provides
@Singleton
fun provideNavigateAiExecutor(
@ApplicationContext context: Context,
apiService: AiApiService
): NavigateAiExecutor {
return NavigateAiExecutor(context, apiService)
}
@Provides
@Singleton
fun provideCommandDefinitions(
@ApplicationContext context: Context,
navigateAiExecutor: NavigateAiExecutor
): List<CommandDefinition> {
return listOf(
CommandDefinition(
id = CommandId.NAVIGATE_AI,
patterns = listOf(Regex(".*(ナビ|行きたい|向かう|セット).*")),
executor = navigateAiExecutor
),
CommandDefinition(
id = CommandId.GET_TIME_DATE,
patterns = listOf(Regex(".*(今何時|何時ですか|日付|今日は何日).*")),
executor = GetTimeDateExecutor()
),
CommandDefinition(
id = CommandId.SET_ALARM_TIMER,
patterns = listOf(Regex(".*(\\d+)\\s*(分|秒|時間)タイマー.*|.*(\\d+)時にアラーム.*")),
executor = SetAlarmTimerExecutor(context)
),
CommandDefinition(
id = CommandId.CHANGE_SETTINGS,
patterns = listOf(Regex(".*(音量を|ダークモード|ライトモード).*")),
executor = ChangeSettingsExecutor(context)
),
CommandDefinition(
id = CommandId.READ_MESSAGES,
patterns = listOf(Regex(".*(メッセージ|未読|通知).*")),
executor = ReadMessagesExecutor(context)
),
CommandDefinition(
id = CommandId.GET_SYSTEM_INFO,
patterns = listOf(Regex(".*(バッテリー|電池|Wi-Fi|ネットワーク).*")),
executor = GetSystemInfoExecutor(context)
),
CommandDefinition(
id = CommandId.UNKNOWN_FALLBACK,
patterns = emptyList(),
executor = UnknownFallbackExecutor()
)
)
}
@Provides
@Singleton
fun provideCommandParser(definitions: List<CommandDefinition>): CommandParser {
return CommandParser(definitions)
}
@Provides
@Singleton
fun provideExecuteCommandUseCase(
kanjiConverter: KanjiToNumberConverter,
parser: CommandParser
): ExecuteCommandUseCase {
return ExecuteCommandUseCase(kanjiConverter, parser)
}
}
```
## 3. 回路・構造・システムの注意事項
### 3.1 物理・ハードウェア、電力、オーディオ、ネットワーク注意事項
1. **マイクフィードバック (AEC) & オーディオフォーカス制御**
- TTS 発話中のスピーカー音量をマイクが拾うループを完全防止するため、`ITextToSpeech` の `status` が `Speaking` の間は `IVoiceRecognizer` の `startListening()` 呼び出しをシステムレベルでロックする。
- `speak()` 呼出時に `AudioManager.requestAudioFocus(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)` を要求し、他アプリの音量を自動ミュート/減衰させる。発話完了 (`onDone`) 時に `abandonAudioFocus()` を実行する。
2. **バッテリー消費抑制 & Bluetooth バックグラウンド起動制御 (BAL 対策)**
- Android 10+ (API 29+) のスリープ中・バックグラウンドからの Activity 起動制限 (Background Activity Launch restrictions) に対応するため、`VoiceAssistantService` を常駐 Foreground Service (`foregroundServiceType="microphone|location"`) として動作させる。
- Bluetooth ヘッドセットボタン (`Intent.ACTION_MEDIA_BUTTON`) 押下時は `MediaSessionCompat` の KeyEventListener でイベントを直接受信し、Service 内でマイク録音・音声認識を開始する。不要な Activity ダイレクト起動を行わないことで制限を回避し、かつ常時マイク監視を行わないため CPU の省電力サスペンドを維持する。
3. **通信信頼性・エラーハンドリング (Exponential Backoff & タイムアウト)**
- モバイル回線接続の不安定性に備え、REST API 通信には Connect Timeout 5,000ms, Read Timeout 10,000ms を設定。
- API 通信失敗時は Exponential Backoff (初期遅延 1,000ms, 倍率 2, 最大 3 回) にて自動リトライ。失敗継続時は `AiApiErrorResponse` (`LOCATION_NOT_FOUND` 等) を受託し、ユーザーへ親切なガイド音声を読み上げる。
### 3.2 システム制御シーケンス (シーケンス図)
#### 3.2.1 AI ナビゲーション実行 & 段階的フォールバックシーケンス
```mermaid
sequenceDiagram
autonumber
actor User as ユーザー
participant UI as Compose UI (MainScreen)
participant VM as MainViewModel
participant VR as IVoiceRecognizer
participant UC as ExecuteCommandUseCase
participant KC as KanjiToNumberConverter
participant CP as CommandParser
participant EX as NavigateAiExecutor
participant API as AiApiService (REST API)
participant TTS as ITextToSpeech
User->>UI: マイクボタンタップ / Bluetoothボタン
UI->>VM: onMicButtonClicked()
VM->>VR: startListening()
VR->>VM: state = Listening(rmsDb)
VM->>UI: StateFlow Update: VoiceAppState.Listening
User->>VR: 発話 (例:「仕事で行く三分後の〇〇会社に向かう」)
VR->>VM: state = Success("仕事で行く三分後の〇〇会社に向かう")
VM->>UI: StateFlow Update: VoiceAppState.Processing
VM->>VM: 会話履歴追加 (USER)
VM->>UC: invoke("仕事で行く三分後の〇〇会社に向かう")
UC->>KC: convert("仕事で行く三分後の〇〇会社に向かう")
KC-->>UC: 前処理結果 ("仕事で行く3分後の〇〇会社に向かう")
UC->>CP: parse("仕事で行く3分後の〇〇会社に向かう")
CP-->>UC: CommandMatchResult (NAVIGATE_AI)
UC->>EX: NavigateAiExecutor.execute()
EX->>API: POST /api/v1/navigate (Auth Header, 5s/10s Timeout)
API-->>EX: HTTP 200 (AiApiResponse: tts_message, destination)
EX->>EX: UTF-8 URL Encode: URLEncoder.encode(name, "UTF-8")
alt NaviCon インストール検出時
EX->>EX: NaviCon Intent (navicon://point?ll=lat,lng&title=encoded_name)
else NaviCon 未検出 且つ Google Maps インストール時
EX->>EX: Google Maps Intent (geo:lat,lng?q=encoded_name)
else いずれも未検出時
EX->>EX: Play Store Intent (market://details?id=jp.co.denso.navicon.user)
end
EX-->>UC: CommandResult (responseText = tts_message)
UC-->>VM: CommandResult 返却
VM->>TTS: speak(tts_message)
TTS->>VM: status = Speaking
VM->>UI: StateFlow Update: VoiceAppState.Speaking
TTS->>User: 音声出力
TTS->>VM: status = Completed
VM->>UI: StateFlow Update: VoiceAppState.Idle
```
#### 3.2.2 エラー発生時 3秒自動復帰シーケンス (delay 3000ms)
```mermaid
sequenceDiagram
autonumber
actor User as ユーザー
participant UI as MainScreen
participant VM as MainViewModel
participant VR as IVoiceRecognizer
participant TTS as ITextToSpeech
User->>VR: 発話不能 / ノイズ入力
VR->>VM: state = Error(ERROR_SPEECH_TIMEOUT)
VM->>UI: StateFlow Update: VoiceAppState.Error("音声が認識できませんでした")
VM->>TTS: speak("音声が認識できませんでした")
note over VM: MainViewModel 内で Coroutine delay(3000) 起動
VM->>VM: viewModelScope.launch { delay(3000); updateState(VoiceAppState.Idle) }
note over VM,UI: 3秒間エラーメッセージ・ダイアログを表示
VM->>UI: StateFlow Update: VoiceAppState.Idle
UI->>User: Idle 状態表示 (自動安全復帰完了)
```
#### 3.2.3 Bluetooth メディアボタン バックグラウンド起動シーケンス
```mermaid
sequenceDiagram
autonumber
actor User as ユーザー (ヘッドセット)
participant BT as Bluetooth Headset
participant SVC as VoiceAssistantService (Foreground)
participant MS as MediaSessionCompat.Callback
participant VR as IVoiceRecognizer
participant VM as MainViewModel
User->>BT: メディアボタン押下
BT->>MS: Intent.ACTION_MEDIA_BUTTON
MS->>SVC: onMediaButtonEvent()
SVC->>SVC: チェック: 常駐 Foreground Service 動作中
SVC->>VR: startListening()
VR->>VM: state = Listening
VM->>User: 音声認識開始 (画面オフ状態でも制御継続)
```
### 3.3 ナビゲーション連携 (NaviCon & Google Maps) & URL エンコード仕様
1. **NaviCon 連携 (メイン軸 / 完全無料)**
- API 応答から受信した目的地名 (`name`) に必ず `URLEncoder.encode(name, "UTF-8")` 処理を行う。
- スキームフォーマット: `navicon://point?ll={latitude},{longitude}&title={encoded_name}`
2. **Google Maps 連携 (フォールバック 1)**
- スキームフォーマット: `geo:{latitude},{longitude}?q={encoded_name}`
3. **Google Play ストア誘導 (フォールバック 2)**
- スキームフォーマット: `market://details?id=jp.co.denso.navicon.user`
4. **パッケージ確認ロジック (フォールバック判定順序)**
- `PackageManager.getPackageInfo("jp.co.denso.navicon.user", 0)` を試行。
- 存在する場合 -> NaviCon Intent 発行。
- 存在しない場合 -> 「NaviConが未検出のため、Google Mapsで表示します」と TTS 発話後、Google Maps Intent 発行。
- Google Maps も未検出の場合 -> Playストア Intent 発行。
### 3.4 ディレクトリ構造とソースファイル配置設計
ルートディレクトリ構成からアプリ内部のパッケージ構成に至る完全なディレクトリ構造は以下の通り。
```
.
├── .gitea/
│ └── workflows/
│ └── build.yaml # Gitea Runner CI/CD ワークフロー
├── docker/
│ ├── Dockerfile # Android ビルド用 Dockerfile (Ubuntu 22.04 + JDK 17 + Android SDK)
│ └── entrypoint.sh # コンテナ起動初期化スクリプト
├── docker-compose.yml # ローカル Docker ビルド定義
├── app/
│ ├── build.gradle.kts # アプリ依存関係設定
│ └── src/
│ ├── main/
│ │ ├── java/com/example/voiceapp/
│ │ │ ├── VoiceApplication.kt # Application クラス (@HiltAndroidApp)
│ │ │ ├── MainActivity.kt # シングルアクティビティ
│ │ │ ├── data/
│ │ │ │ └── api/
│ │ │ │ ├── AiApiService.kt
│ │ │ │ └── models/ # AiApiRequest, AiApiResponse, AiApiErrorResponse
│ │ │ ├── di/
│ │ │ │ ├── VoiceModule.kt
│ │ │ │ ├── NetworkModule.kt
│ │ │ │ └── DomainModule.kt
│ │ │ ├── domain/
│ │ │ │ ├── converter/
│ │ │ │ │ └── KanjiToNumberConverter.kt
│ │ │ │ ├── executor/
│ │ │ │ │ ├── ICommandExecutor.kt
│ │ │ │ │ ├── GetTimeDateExecutor.kt
│ │ │ │ │ ├── SetAlarmTimerExecutor.kt
│ │ │ │ │ ├── ChangeSettingsExecutor.kt
│ │ │ │ │ ├── ReadMessagesExecutor.kt
│ │ │ │ │ ├── GetSystemInfoExecutor.kt
│ │ │ │ │ ├── NavigateAiExecutor.kt
│ │ │ │ │ └── UnknownFallbackExecutor.kt
│ │ │ │ ├── model/
│ │ │ │ │ ├── CommandId.kt
│ │ │ │ │ ├── CommandMatchResult.kt
│ │ │ │ │ └── CommandResult.kt
│ │ │ │ ├── CommandDefinition.kt
│ │ │ │ ├── CommandParser.kt
│ │ │ │ └── ExecuteCommandUseCase.kt
│ │ │ ├── voice/
│ │ │ │ ├── IVoiceRecognizer.kt
│ │ │ │ ├── VoiceRecognitionManager.kt
│ │ │ │ ├── ITextToSpeech.kt
│ │ │ │ └── TextToSpeechManager.kt
│ │ │ ├── service/
│ │ │ │ ├── VoiceAssistantService.kt
│ │ │ │ └── AppNotificationListenerService.kt
│ │ │ └── ui/
│ │ │ ├── VoiceAppState.kt
│ │ │ ├── ChatMessage.kt
│ │ │ ├── MainViewModel.kt
│ │ │ └── components/
│ │ │ ├── MainScreen.kt
│ │ │ ├── StatusBanner.kt
│ │ │ ├── WaveformIndicator.kt
│ │ │ ├── ConversationHistory.kt
│ │ │ ├── ControlBar.kt
│ │ │ └── PermissionDialog.kt
│ │ └── AndroidManifest.xml
│ └── test/
│ └── java/com/example/voiceapp/
│ ├── domain/
│ │ ├── KanjiToNumberConverterTest.kt
│ │ ├── CommandParserTest.kt
│ │ └── ExecuteCommandUseCaseTest.kt
│ └── ui/
│ └── MainViewModelTest.kt
└── README.md
```
### 3.5 AndroidManifest.xml 権限設定・サービス設定設計
Android OS 8.0+ (API Level 26) から Android 14 (API Level 34) までの互換性および必須パーミッション・フォアグラウンドサービス宣言を網羅する `AndroidManifest.xml` の完全記述。
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.voiceapp">
<!-- 必須パーミッション宣言 -->
<!-- 1. 音声入力権限 (危険権限) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 2. AI サーバー通信・ネットワーク確認権限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 3. 位置情報権限 (AI ナビゲーション現在地付与用) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 4. オーディオ設定(音量変更・フォーカス要求)権限 -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- 5. アラーム・タイマーインテント設定権限 (Android 12+ API 31+) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- 6. 通知出力権限 (Android 13+ API 33+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 7. 未読メッセージ通知受託権限 -->
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
<!-- 8. Android 14 (API 34) 必須フォアグラウンドサービス権限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- ハードウェア機能制限の設定 (マイク必須設定) -->
<uses-feature
android:name="android.hardware.microphone"
android:required="true" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<application
android:name=".VoiceApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="音声操作ナビ端末"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VoiceControlledDevice">
<!-- シングルアクティビティ構成 -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 音声アシスタント呼び出しインテント -->
<intent-filter>
<action android:name="android.intent.action.VOICE_COMMAND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- 常駐フォアグラウンドサービス (Android 14 API 34 対応) -->
<service
android:name=".service.VoiceAssistantService"
android:exported="false"
android:foregroundServiceType="microphone|location" />
<!-- 未読メッセージ読み上げ通知受託サービス -->
<service
android:name=".service.AppNotificationListenerService"
android:label="メッセージ読み上げサービス"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<!-- SpeechRecognizer, NaviCon, Google Maps パッケージクエリ設定 (Android 11+ API 30+) -->
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
<package android:name="jp.co.denso.navicon.user" />
<package android:name="com.google.android.apps.maps" />
</queries>
</application>
</manifest>
```
### 3.6 Gitea CI/CD Docker Runner 環境・ビルドパイプライン設計
1. **Docker Runner ビルドモデル**
- CI/CD パイプラインは Gitea Act Runner の Docker Runner モードにて動的コンテナ環境を生成して実行する。
- ホストOSに依存せず、コンテナイメージ (`docker/Dockerfile`) 内で JDK 17, Android SDK (API 34 Build-tools), Gradle 8.x 環境を完全にカプセル化する。
2. **Gitea ワークフロー定義 (`.gitea/workflows/build.yaml`)**
```yaml
name: Android CI Build & Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
build-and-test:
runs-on: ubuntu-latest
container:
image: voice-app-build:latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest --stacktrace
- name: Assemble Debug APK
run: ./gradlew assembleDebug --stacktrace
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: debug-apk
path: app/build/outputs/apk/debug/app-debug.apk
```
3. **ローカル検証モデル (`docker-compose.yml`)**
- ローカル環境でも `docker-compose run --rm build-env ./gradlew testDebugUnitTest` で CI と同一のテスト・ビルド環境を実行可能とする。
+104
View File
@@ -0,0 +1,104 @@
# 整合性・仕様検証レポート
## 1. 検証結果サマリ (PASS / FAIL / WARNING)
**判定**: **FAIL**
### 検証サマリ概要
改訂された仕様書 (`docs/02_specifications.md`) と詳細設計書 (`docs/03_hardware_design.md`)、および前回のレビューレポート (`docs/04_review_report.md`) の指摘事項に対する包括的な比較・検証を実施いたしました。
前回の指摘事項のうち「参照ドキュメント `docs/03_hardware_design.md` の存在」については設計書が作成されたことで解消されましたが、**Android 14における `FOREGROUND_SERVICE` のタイプおよび個別パーミッションの未宣言、Bluetoothメディアボタンによるバックグラウンド起動制限(Background Activity Launch restrictions)に対する回避設計の欠落、Gitea CI/CDのコンテナ実行モデルの未定義、APIエラーレスポンス・認証ヘッダー・通信タイムアウト等の未定義、URLエンコード処理の規定不足** などの重要課題が仕様書・設計書の両面で未解決のまま存続しています。
さらに、仕様書と設計書の詳細比較検証において、以下の致命的な技術的齟齬・設計欠落が新たに確認されました:
1. **核心機能である AI サーバー連携 (`NAVIGATE_AI`) コマンドおよびナビ連携モジュールの設計書における完全欠落**
2. **`ITextToSpeech.speak()` の戻り値シグネチャの直接的矛盾 (`Unit` vs `StateFlow<TtsStatus>`)**
3. **前処理コンバータ (`KanjiToNumberConverter`) の設計書における完全欠落**
4. **位置情報パーミッション (`ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION`) および未読メッセージ読み上げサービス (`AppNotificationListenerService`) の `AndroidManifest.xml` からの脱落**
5. **対象 OS バージョンの不一致 (Min API Level 26 vs API Level 29)**
6. **エラー発生時 3秒自動復帰制御の設計書シーケンスにおける欠落**
以上の理由により、現時点での検証判定は **FAIL** といたします。開発(Coder)およびテスト(Tester)フェーズへの移行前に、仕様書・設計書の双方向における修正および技術整合性の確保が必須です。
---
## 2. 齟齬・不一致項目
### ① AI サーバー連携 (`NAVIGATE_AI`) コマンドおよびナビゲーション連携モジュールの設計書における完全欠落
- **仕様書**: 3.3.1 節で AI サーバー通信 API `POST /api/v1/navigate`(リクエスト/レスポンス)、3.3.3 表で `NAVIGATE_AI` コマンド (`.*(ナビ|行きたい|向かう|セット).*`)、3.4 節で NaviCon (`navicon://point...`) および Google Maps (`geo:...`) 連携を主軸機能として明記。
- **設計書**: `CommandId` Enum (2.2.2-2) に `NAVIGATE_AI` が存在せず、`DomainModule` (2.2.3)、`executor/` パッケージ、クラス図、API 通信クライアント (Retrofit/OkHttp 等) が**設計書から完全に欠落**している。
### ② `ITextToSpeech.speak()` メソッドシグネチャ・戻り値の不一致
- **仕様書**: 3.2.3 節にて「状態変化は `val status: StateFlow<TtsStatus>` に集約。`speak()` は `Unit` を返す」と明記。
- **設計書**: 2.2.2-1 節のコード定義で `fun speak(text: String, queueMode: Int = 0): StateFlow<TtsStatus>` と定義されており、戻り値型が仕様書と直接衝突している。
### ③ 漢数字コンバータ (`KanjiToNumberConverter`) の設計書における完全欠落
- **仕様書**: 3.3.2 節で正規表現パース前の事前変換処理として `KanjiToNumberConverter`(「三分」「7時」等の漢数字・全角数字をアラビア数字へ変換)を必須定義し、受入基準 (6.2) にテスト項目を指定。
- **設計書**: クラス図、Domain Layer インターフェース、`CommandParser.parse()` 実装例、`DomainModule` DI 構成、ディレクトリ構成のいずれにも `KanjiToNumberConverter` が存在しない。
### ④ 必須パーミッションおよび Service 宣言の `AndroidManifest.xml` における不一致・脱落
- **仕様書**: 2.2 節で `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`, `BIND_NOTIFICATION_LISTENER_SERVICE` を必須指定し、3.5 節で `AppNotificationListenerService` のマニフェスト定義を指定。
- **設計書**: 3.4 節 `AndroidManifest.xml` に位置情報パーミッション (`ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION`) および `AppNotificationListenerService` / `BIND_NOTIFICATION_LISTENER_SERVICE` の宣言が脱落。一方で仕様書一覧にない `SCHEDULE_EXACT_ALARM` が設計書マニフェストに追加されている。
### ⑤ 対象 OS バージョン (Min SDK API Level) の不一致
- **仕様書**: 1.1 / 2.1 節で `Android OS 8.0+ (API Level 26 以上 / Target API Level 34)` と定義。
- **設計書**: 1.1-1 節で `Android 10.0+ (API Level 29 以上)` と記述されており、最小サポート OS バージョンの仕様が相違している。
### ⑥ エラー時 3秒自動復帰制御の設計書における欠落
- **仕様書**: 4.2 節で `VoiceAppState.Error` 遷移時に `delay(3000)` により 3秒後に自動 `Idle` 復帰すると明確に定義。
- **設計書**: 3.2 節のシーケンス図および動作説明で正常系フローのみが記載され、エラー時 3秒自動復帰タイマー制御の記述が欠落している。
### ⑦ ディレクトリ構造・CI/CD 構成定義の乖離
- **仕様書**: 5.1 節で `.gitea/workflows/build.yaml`, `docker/Dockerfile`, `docker/entrypoint.sh`, `docker-compose.yml` などの CI/CD・コンテナ構成をプロジェクトルート下に記載。
- **設計書**: 3.3 節で `voice_controlled_device/` ルート下のアプリコード構造のみを記載し、CI/CD や Docker 関連構成が完全に除外されている。
---
## 3. 仕様の未定義・検討不足項目
### ① Target SDK Version 34 (Android 14) における `FOREGROUND_SERVICE` タイプ・権限の未定義
- Android 14 (API Level 34) 必須の `android.permission.FOREGROUND_SERVICE` およびサービスタイプ (`foregroundServiceType="microphone|location"` 等)、個別の `FOREGROUND_SERVICE_MICROPHONE`, `FOREGROUND_SERVICE_LOCATION` 権限などの宣言が仕様書・設計書双方のマニフェストから脱落している。
### ② Bluetooth メディアボタンによるバックグラウンド起動制限 (Background Activity Launch restrictions) 対策の未設計
- 画面オフ・スリープ時に `ACTION_MEDIA_BUTTON` や `ACTION_VOICE_COMMAND` から直接 Activity を起動することは Android 10+ OS 制約上不可能である。バックグラウンド常駐 Service + `MediaSessionCompat` による音響受信およびフォアグラウンドサービス制御設計が仕様書・設計書ともに未具体化。
### ③ AI サーバー API 仕様における未定義事項
1. **エラーレスポンス JSON フォーマットの未定義**: 正常系 (`status: "success"`) のみの定義であり、エラー発生時 (`status: "error"`) の JSON 構造(エラーコード、理由メッセージ)が未定義。
2. **認証・セキュリティヘッダーの欠落**: `POST /api/v1/navigate` リクエストに API キーや Authorization トークンが規定されていない。
3. **通信タイムアウト & 再試行方針の未定義**: モバイル回線接続時の Connect/Read タイムアウト値および Exponential Backoff リトライ方針が規定されていない。
### ④ NaviCon URL スキームの URL エンコードおよびフォールバック条件の未定義
- `navicon://point?ll={latitude},{longitude}&title={name}` の `title` に対する `UTF-8` URLエンコード規定が未記載。
- NaviCon 未インストール時に Google Maps アプリ Intent を優先するのか Google Play への誘導を優先するかの判定順序・ロジックが未決定。
### ⑤ Gitea CI/CD 実行モデルの詳細定義不足
- `.gitea/workflows/build.yaml` の Runner タイプ (Docker Runner / Host Runner) やコンテナ環境の紐付け記述が不足しており、ローカル `docker-compose` 環境との完全一致性が担保されていない。
---
## 4. Coder / tester への引き継ぎ注意事項
### 4.1 Coder (開発担当者) への注意事項
1. **`ITextToSpeech.speak()` のインターフェース実装**:
- 仕様書の原則に従い、`speak()` の戻り値は `Unit` とし、状態管理は `StateFlow<TtsStatus>` のみで一元化すること。
2. **`NAVIGATE_AI` コマンド & ナビゲーション連携の実装**:
- `CommandId` に `NAVIGATE_AI` を追加し、`NavigateAiExecutor` および Retrofit/OkHttp による `POST /api/v1/navigate` 通信クライアント、NaviCon/Google Maps 連携 Intent ロジックを構築すること。
3. **漢数字コンバータ (`KanjiToNumberConverter`) の組み込み**:
- 正規表現パース処理の前に必ず `KanjiToNumberConverter` を割り当て、漢数字(「一」「二」「三分」「七時」等)および全角数字をアラビア数字へ変換してから `CommandParser` へ入力すること。
4. **`AndroidManifest.xml` 権限・サービス宣言の網羅**:
- 位置情報権限 (`ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`)、`BIND_NOTIFICATION_LISTENER_SERVICE` (および `AppNotificationListenerService`) を追加すること。
- Android 14 (API 34) 対策として `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_MICROPHONE`, `FOREGROUND_SERVICE_LOCATION` を宣言し、適切なサービスタイプを設定すること。
5. **エラー時 3秒自動復帰タイマーの実装**:
- `VoiceAppState.Error` 遷移時は `MainViewModel` 内で Coroutine `delay(3000)` を起動し、3秒後に `VoiceAppState.Idle` へ自動復帰させる制御を組み込むこと。
6. **URL エンコード処理**:
- NaviCon URL スキーム生成時、パラメータ `title` は必ず `URLEncoder.encode(title, "UTF-8")` 処理を行うこと。
### 4.2 Tester (テスト担当者) への注意事項
1. **漢数字前処理変換単体テスト (`KanjiToNumberConverterTest`)**:
- 「三分」「7時」「十五分」「百二十秒」等の漢数字・全角・アラビア数字混合パターンが正確にアラビア数字へ変換されるかをテストすること。
2. **コマンドパース単体テスト (`CommandParserTest`)**:
- `NAVIGATE_AI`(「〇〇に行きたい」「ナビ開始」等)を含め、全 7 種のコマンドパターンの抽出正確性をテストすること。
3. **ViewModel 状態遷移非同期テスト (`MainViewModelTest`)**:
- Turbine を活用し、`VoiceAppState.Error` へ遷移後、 precisely 3000ms 後に `VoiceAppState.Idle` へ自動復帰する非同期制御をテストすること。
4. **パーミッション拒否時の堅牢性テスト**:
- 位置情報権限 (`ACCESS_FINE_LOCATION`) やマイク権限 (`RECORD_AUDIO`) を拒否された状態でアプリおよび音声ナビリクエストがクラッシュせず、エラーメッセージを TTS 読み上げ/ダイアログ表示するか確認すること。
5. **NaviCon 未インストール端末でのフォールバックテスト**:
- NaviCon 非インストールの実機/エミュレータにて目的地設定を実行し、Google Maps または Google Play 画面へ安全にフォールバックすることを確認すること。