静かな昼の礼拝の最中、モスクは静まり返り、集中した雰囲気に包まれていた。そして――突然、けたたましい着信音が空気を引き裂いた。皆が振り返り、私の顔は真っ赤になった。慌てて端末に手を伸ばし、サイドボタンを押して音を止めた。普通の通知音だったが、その場ではサイレンのように響いた。1時間前には着信音を消すつもりだったのに、朝の忙しさですっかり忘れていた。あの瞬間、手動での端末管理が破綻していると痛感した。
私たちはスマートデバイスに囲まれて生活しているのに、最も基本的なコンテキスト認識タスクでさえ失敗し続ける。同期を必要とするカレンダーアプリや、絶えずクラウドサーバーをポーリングする位置情報ツールに依存しているため、地下や圏外に出た瞬間に依存チェーンが崩れる。問題は消音スイッチを押し忘れることだけではない。現在のエコシステムが、ネットワーク依存のループに積極的に参加せざるを得ない状態を強いていることにある。サーバーがダウンしたり信号が弱くなったりすれば、自動化は機能しなくなる。礼拝所、法廷、診療所のように繊細な場所では、その遅延や失敗は許容できない。私は外部の ping やクラウドハートビート、API認証トークンに一切依存しない、完全に端末上で動作するシステムを必要とした。
Muffle を開発するにあたり、コアロジックはユーザーデバイスのサンドボックス内に完全に閉じ込めることにした。ジオフェンシングでは、タイルや検索結果のために大量のネットワークハンドシェイクを必要とするサードパーティの地図 SDK を避ける必要があった。Google Play Services の位置情報ライブラリからネイティブの GeofencingClient を利用したが、オフライン優先の制約を守るためにカスタムロジックレイヤーでラップした。課題はイベントのトリガーだけでなく、定義された半径に出入りした際の状態遷移を管理することだった。画面がオフでアプリがバックグラウンドにあっても AudioManager サービスが音量切り替えを実行できるように、PendingIntent トリガーを扱わなければならなかった。
kotlin
val geofencingRequest = GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofence(geofence)
}.build()
geofencingClient.addGeofences(geofenceRequest, geofencePendingIntent).run {
addOnSuccessListener { /* Geofence added locally / }
addOnFailureListener { e -> / Handle registration failure */ }
}
BroadcastReceiver パターンで Geofence.GEOFENCE_TRANSITION_ENTER インテントを監視することで、ネットワークリクエストを一切送らずに AudioManager の呼び出しをトリガーできた。ここでの難所は Android の Doze モードとアプリスタンバイバケットだ。OS がバッテリー節約のためにバックグラウンドプロセスを殺した場合、ジオフェンス遷移が遅延したり無視されたりする可能性がある。そこで ForegroundService を実装してコンテキストを維持し、OS にアプリをアクティブ参加者として扱わせることにした。これは意図的なアーキテクチャ上のトレードオフである。少量のバッテリーを犠牲にして、指定された場所で確実に着信音を消すことを優先したのだ。テストでは、標準的なバックグラウンドワークマネージャーに依存すると 30〜60 秒の遅延が発生し、会議室に入る際には致命的だった。
開発中に最も驚いたのは、ネットワーク支援型位置情報(NLP)を排除したときの LocationManager と基盤の FusedLocationProvider の脆さだった。当初は GPS で十分だと考えていたが、それは誤りだった。屋内では GPS がロックできず、Wi-Fi スキャンが無効だとジオフェンスが発火しない。私はパッシブ位置情報更新をチェックし、アクティブスキャンがタイムアウトした場合は最後に取得した位置情報を使用するフォールバック機構を構築した。ドキュメントには書かれていなかったが、ユーザーが位置情報権限を様々なレベルで許可・制限していることに気づいた。アプリは「権限拒否」状態をクラッシュや待機ループに陥ることなく、優雅に処理する必要があった。
もう一つの非自明なハードルは競合解決だった。位置情報ベースのルーチンと時間ベースのルーチンが重なった場合、どうなるか。週末をまるごと費やしてデバッグしたのは、システムクロックがまだ終了していないルーチンをトリガーしたために、着信音が消音→バイブレーション→消音と即座に切り替わるループだった。各ルーチンに優先度整数を実装し、現在「アクティブ」なルーチンが明示的に終了するまで音量状態をロックし、下位優先度のルーチンが発火しても上書きされないようにした。これは電話の音量設定に対する Mutex のようなものだ。「緊急バイパス」連絡先の扱い方は将来変更したい。現在は静的なリストだが、Android の NotificationChannel の重要度設定と統合して、どの通知が消音を「突破」できるかをより細かく制御できるようにしたい。
オフライン優先のツールを構築する際の最大の敵は、システム API が常に予測可能で線形な振る舞いをするという前提だ。その前提は崩れる。位置情報信号が死に、OS がバッテリーを絞り、ユーザーが設定を勝手に変えることを想定した防御的なコードを書かなければならない。複雑さをユーザーから切り離し、ハンドリングロジックに埋め込むことが目標だ。静かな部屋でエラーメッセージを表示するより、黙って失敗してリトライする方が遥かに良い。ローカル専用ストレージの美点は、ユーザーのプライバシーを尊重し、データプランや地域の接続状況に関わらずアプリを機能させ続けられることにある。それはよりクリーンで、より敬意のあるソフトウェア設計だ。
今日のアプリを見ると、あのモスクでの恥ずかしい経験を解決するソリューションになっている。ロジックをローカルに保ち、トリガーを堅牢にすることで、私は毎日自分の精神衛生を管理するためにこのツールを使っている。実際にルーチンがどのように動作するのか、または自分で実装を試してみたい場合は、https://play.google.com/store/apps/details?id=com.muffle.app でプロジェクトを見つけることができる。まだ開発中だが、公共の場でのデバイスとの関わり方を確実に変えてくれた。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.