Issue #15(親Issue #3、track #13/#14のadopted判断を受けた続き)の事前検証結果と、フェーズ3・4への判断をまとめる。手順はdocs/検討用/15_Androidタブレット対応_フェーズ3A実装手順.mdを参照。対象実機はRedmi Pad SE 8.7 (HyperOS 2.0.203.0.VHXMIXM, Android 15 / API 35, arm64-v8a)、DG-PRO1RWS。
5シナリオ中4件を実施(シナリオ5「電波範囲外への移動」は保留)。
| シナリオ | 検出方法・例外 | 検出時間 | 再接続 |
|---|---|---|---|
| DG-PRO1RWS電源off/on | IOException: bt socket closed, read return: -1 | 約5026ms | 手動再接続3383ms |
| タブレットBluetooth off/on | 同上のIOException | 約40ms(電源offより大幅に速い) | 545ms |
| socket close(切断ボタン) | シナリオ1・2と同一の例外 | - | - |
| 接続中のBLUETOOTH_CONNECT権限取り消し | アプリ内例外なし。OSがプロセスごと強制終了 | プロセス消滅で検知 | 再許可→再起動→再接続で復旧 |
| 電波範囲外への移動 | 未実施(保留) | - | - |
所見:
disconnect()ログの有無でしか区別できない。製品実装では例外から推測せず、利用者操作・Bluetooth状態・権限状態・watchdog発火を明示的な切断原因として保持する(詳細は判断条件5「接続状態遷移の暫定仕様」参照)adb tcpipでのワイヤレスadbに切り替えてから別途実施する判断(実測を待たずに確定): InputStream.read()がタイムアウトなしに無限ブロックしうるという仮説はほぼ確実であり、位置情報表示アプリで無反応のまま古い位置が表示され続けるのは運用上のリスクが大きい。電波切れ時のタイムアウト監視は実測結果を待たず「必要」と確定し、フェーズ3実装の最初のコミットから組み込む。シナリオ5の実測(具体的な検出時間)は、タイムアウト閾値の妥当性検証を目的として別途行う。
実装方式(暫定): BluetoothSocketにはread timeoutの一般的な設定手段がないため、最終受信時刻(lastLineAt)を別Coroutineで定期的に監視し、閾値(初期値15秒。シナリオ5の実測後に調整可能)を超えたらBluetoothSocket.close()を呼んでblocking readを強制的に解除し、Disconnectedとして扱う。監視コルーチンの停止タイミングとsocket close呼び出しの競合(close済みsocketへの二重close、close呼び出し直後に正常受信していた場合の扱い)をtestで確認する。
5シナリオすべて実施。
| シナリオ | 結果 |
|---|---|
| 画面回転 | instance変化なし、受信継続、二重接続なし |
| Activity再作成(「アクティビティを保持しない」+ホーム往復) | 二重接続による接続破壊を実機で確認(詳細下記) |
| ホーム復帰 | PID・instance変化なし、受信継続 |
| 画面off/on(20秒ロック) | instance変化なし、受信・heartbeatともgapなく継続 |
| 強制停止 | 設定UIはグレーアウト/タスク一覧残存で分かりづらいが、pidofで確認するとプロセス終了は成功。再起動後instance=1から正常に採番し直され受信再開 |
Activity再作成の詳細: ホーム往復後も旧instance=1が生存(remember{}ベースの現行実装が旧インスタンスを破棄しない欠陥)。アプリ復帰時に生成された新instance=2が同一デバイスへ接続を試み、112ms後にIOException: read failed, socket might closed or timeout, read ret: -1で失敗。その6ms後、直前まで正常だったinstance=1もIOException: bt socket closed, read return: -1で切断された。DG-PRO1RWSは同時1接続までしか受け付けられず、2本目の接続要求で既存接続ごとリセットされたとみられる。両インスタンスとも失われ、UI操作のみでは復旧できずam force-stopによるプロセス完全終了が必要だった。
判断: 二重接続はActivity再作成シナリオでのみ再現する。フェーズ3では接続元をViewModelが単独で所有し、ViewModel.onCleared()で確実にdisconnect()を呼ぶ設計に変更する。Composable(画面)は接続元を所有しない。画面表示期間だけ接続する仕様にする場合も、DisposableEffectが接続の生成・破棄を直接担うのではなく、明示されたUIイベントからViewModelへ開始・停止を要求する形にとどめる(DisposableEffectを接続の所有者にすると、同じViewModelを維持した画面再構成でも接続を切ってしまう可能性があるため)。
| 条件 | 結果 |
|---|---|
| 初期状態・前景30分(約40.5分実施) | instance変化なし、切断・二重接続なし |
| 初期状態・画面off自然経過(約12分13秒) | instance変化なし、Doze自然遷移なし(mState=ACTIVEのまま) |
| 初期状態・force-idle補助試験(約5分) | 強制Doze中(mState=IDLE)も受信継続、gapなし |
| 電池最適化除外・前景30分(約35分) | instance変化なし |
| 電池最適化除外・画面off(約12分3秒) | instance変化なし、Doze自然遷移なし |
| HyperOS自動起動管理の初期状態 | OFF(拒否) |
判断: 電池最適化・Doze(自然/強制)いずれの組み合わせでも、切断・二重接続・受信遅延は一切発生しなかった。現行のフォアグラウンド限定・スパイク実装の範囲では、Foreground Serviceや電池最適化除外の案内は必須ではない。ただし検証時間は最大約40分・12分にとどまり、現地での数時間規模の連続稼働・画面OFF長時間・メモリ圧迫・OSによるプロセス再生成は未検証のため、現時点ではForeground Serviceを採用しないが、リリース前にこれらのシナリオを別途検証し、受信継続に問題があればForeground Serviceの採用を再度判断する。「運用開始後に問題が報告されたら対応する」という事後対応は製品検証として不十分なため採らない。
電源投入後の自動起動について(製品要件として確定): HyperOSの自動起動権限が初期状態でOFFのため、BOOT_COMPLETEDによるアプリ自動launchはブロックされる。診断用BroadcastReceiver(track内で追加・検証後に削除)で検証した結果、自動起動権限をONにしアプリのstopped stateを解除した状態ならBOOT_COMPLETEDは受信できるが、Androidのバックグラウンドactivity起動制限によりstartActivity()だけでは画面前面に表示されないことを実機で確認した。この所見を踏まえ、docs/decisions/p3-android-deployment-modes.mdでstandard/dedicatedのProduct Flavor分離とDevice Owner+カスタムHome方式が正式に決定された。専用端末のプロビジョニング確認は本trackとは別の検証trackで行う。
| 項目 | 結果 |
|---|---|
| 初回コピー時間・容量 | 1,141,387 bytesを15〜27ms(複数回測定で安定) |
| コピー中断のシミュレーション | truncate -s 1024でファイルを1024 bytesに切り詰め |
| 再起動後の状態 | ネイティブクラッシュが反復する再起動ループを実機で確認(詳細下記) |
| 試験後の復元 | pm clearでアプリデータ初期化後、正常にPMTilesが再コピーされ地図・航空写真・筆ポリゴンが正常表示された |
| 機内モード表示 | 電池最適化設定変更後も機内モードで地図・航空写真・筆ポリゴンが正常表示された |
再起動後の状態の詳細: ensurePmtilesCopiedToInternalStorageがdest.exists()のみで完了判定するため、破損した1024バイトファイルがそのまま「コピー完了済み」として扱われた。MapLibre Nativeが読み込もうとした結果、以下のネイティブクラッシュが3回連続で再現した(PID 11139→13508→18930、約3秒おきに自動的に再起動を繰り返すクラッシュ・再起動ループ)。3回連続の観測であり、無限に続くことを確認したわけではない。
F libc : Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid ... (PMTilesFileSour), pid ... (.drogger.tablet)
F DEBUG : Abort message: 'terminating due to uncaught exception of type std::runtime_error: decompression error'
判断: 判断条件4で懸念されていた「不完全ファイルを完成済みと誤認する」問題は、「古い地図が表示される」程度ではなく、アプリ全体が起動不能になる、反復するクラッシュ・再起動ループという、想定より深刻な形で実機再現した。フェーズ4のPMTiles対策は、次の受け入れ条件を満たすこと。
| 論点 | 判断 |
|---|---|
| 接続所有範囲 | ViewModelが単独で所有し、onCleared()で確実にdisconnect()を呼ぶ。Composable/DisposableEffectは接続を所有せず、ViewModelへの開始・停止要求のみ行う(判断条件2の実機結果による。詳細は判断条件2参照) |
| 有限バックオフの初期値・停止条件 | 3_Androidタブレット対応_実装案.md7節の1秒→2秒→5秒→10秒案を踏襲し、以降は10秒間隔を継続する。停止条件(暫定): 合計6回(1・2・5・10・10・10秒間隔)試行しても接続できない場合、自動再接続を停止し、手動再接続を促すUI状態(ReconnectFailed)へ遷移する。具体的な回数・間隔はシナリオ5(電波範囲外)の実測後に見直す余地を残す |
| 権限取り消し/Bluetooth off/電波切れの状態遷移 | 下記「接続状態遷移の暫定仕様」のとおり、Disconnected(通信切断)・BluetoothDisabled・StoppedByUser・PermissionRequiredを区別する。権限取り消しはOSがプロセスごと終了させるため、取り消された瞬間にアプリ内UIで案内することはできない。次回起動時に権限状態を検査し、PermissionRequired状態としてUIで案内する |
| Foreground Service・電池最適化除外案内・前景限定 | 前景限定で問題なし(判断条件3参照)。現時点ではForeground Serviceと電池最適化除外案内は採用しないが、リリース前に数時間規模の連続稼働・画面OFF長時間・メモリ圧迫・プロセス再生成を別途検証し、不合格ならForeground Service採用を再度判断する |
| PMTiles atomic copy・サイズ/checksum検証 | 必須。受け入れ条件は判断条件4に記載(一時ファイル→検証→rename、起動時検証、失敗時の復旧可能なエラー画面、破損ファイルでの回帰test) |
| 電源投入後の自動起動 | dedicated FlavorでDevice Owner + カスタムHome方式を採用(判断条件3、p3-android-deployment-modes.md参照)。単純なBOOT_COMPLETED+startActivity()方式は不採用 |
判断条件1・2の実機結果を踏まえ、切断要因ごとに次の状態を区別する。例外の種類・メッセージだけでは区別できないため、切断要因(何が引き金だったか)をアプリ側で明示的に記録して状態遷移に反映する。
| 状態 | 遷移条件 | 振る舞い |
|---|---|---|
Disconnected(通信切断) |
DG-PRO1RWS電源off、予期しないEOF/IOException、電波切れタイムアウト | 有限バックオフで自動再接続する |
BluetoothDisabled |
タブレットのBluetoothがOFFになった | 自動再接続を試行しない。Bluetooth ON復帰イベントを受けてから再接続シーケンスを開始する |
StoppedByUser |
利用者が明示的に「切断」ボタンを操作した(socket close) | 自動再接続を禁止する。利用者の再接続操作を待つ |
PermissionRequired |
BLUETOOTH_CONNECT権限が取り消された(プロセスごと終了するため、次回起動時に検知) | 再接続を試行しない。権限許可を促すUIを表示し、許可後に再接続する |
adb tcpipでのワイヤレスadbに切り替えてから、別途track化して実施する。実測結果でタイムアウト閾値(暫定15秒)と有限バックオフの停止条件(暫定: 合計6回)を見直す