Haseeb

オープニングフック

モスクの中は完全な沈黙に包まれていた。数百人が深い、規則的な跪拝の中にあったとき、合成された甲高い着信音が静けさをナイフのように切り裂いた。それは私の携帯電話だった。朝の通勤に集中しすぎて、サイレントモードを切り替えるのを完全に忘れていたのだ。百人以上の頭がこちらを向いた瞬間、恥ずかしさの波が一瞬で襲ってきた。それは単なる設定のミスではなく、私の自己管理の完全な崩壊だった。その瞬間がMuffle誕生のきっかけとなった。

課題

私たちは、スマートフォンが目に見えないパートナーとして振る舞うことを期待される、高いコンテキスト依存の環境で生活している。会議室、講義室、礼拝所など、どこにいても電話の着信がもたらす社会的コストはゼロではない。Androidに標準搭載されている「おやすみモード」(DND)のスケジュールは便利だが、静的である。人間が厳格な9時から17時の予定を守らないという現実には対応していない。

私は、手動操作が失敗の最大の原因だと気づいた。記憶に頼って端末をミュートしようとすれば、いつか必ず失敗する。コンテキストを理解するシステムが必要だった。ポケットに手を伸ばしてスイッチを操作しなくても、携帯電話が「今オフィスにいる」「ジムにいる」「会議中だ」と認識してほしいのだ。評価したサードパーティ製アプリの多くは、重いポーリングベースの位置情報サービスに依存しており、4時間以内にバッテリーを消耗してしまった。私はCPUに寄生虫のように張り付くバックグラウンドサービスは望まなかった。地理的な変化が実際に起きたときだけ起動する、外科手術のようなイベント駆動型アーキテクチャが欲しかった。

技術的判断と実装

Muffleを構築するにあたり、常時位置情報のポーリングをやめ、com.google.android.gms.locationパッケージのGeofencingClientを利用した。コアとなる設計判断は、重い処理をGoogle PlayサービスのFused Location Providerにオフロードすることだった。アクティブなウェイクロックと常時GPSポーリングを必要とする独自のLocationManager更新を管理する代わりに、システムに円形のジオフェンスを登録した。

ユーザーが場所を定義すると、特定の半径を持つGeofenceオブジェクトを含むGeofencingRequestを作成する。システムはOSレベルでこれらの境界を監視する。境界を越えるまで、アプリケーションはバックグラウンドでアイドル状態を保つ。ユーザーが境界を越えると、システムがIntentBroadcastReceiverにブロードキャストする。これが重要な点で、OSからユーザーがゾーンに出入りしたことを通知されたときにのみ、コードがAudioManagerの切り替えを実行する。

kotlin
val geofence = Geofence.Builder()
.setRequestId(routine.id)
.setCircularRegion(lat, lng, radius)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()

このアプローチにより、ループ内で絶えずdistanceTo()の結果を計算する必要がなくなる。Fused Location Providerに計算を任せることで、Kotlinで自分で書くどんなロジックよりもハードウェア割り込みに最適化されたシステムにバッテリー消費を委ねることができる。またPriorityシステムも実装した。ユーザーが重複する2つのジオフェンスを持っている場合、ローカルのSQLiteデータベースが信頼できる情報源として機能する。BroadcastReceiverAudioManager.setRingerMode()コマンドを発行する前に、データベースで現在最も優先度が高いアクティブなルーチンを確認する。これにより、2つのジオフェンスが音量状態を奪い合う古典的な「ちらつき」を防ぐ。

驚いた点・改善すべき点

本当に謙虚にさせられたのは、都市部の高層ビル街におけるGPS信号の不安定さだった。当初の想定では、Geofenceのトリガーは瞬時に動作するはずだった。ユーザーが建物の中に入った瞬間に電話が無音になることを期待していた。しかし実際には、高層ビルが衛星信号を反射する密集した市街地では信号の遅延があり、ENTER遷移が実際の目標地点から2ブロック離れた場所で発火することがあった。

GPSのみにジオフェンシングを依存するのは、都市部では負け戦であることを身をもって学んだ。やり直すならハイブリッドアプローチを実装するだろう。Geofence APIをWi-Fi信号のフィンガープリンティングで補強する。近隣のルーターのMACアドレスをスキャンすることで、生のGPS座標よりも確実に位置の遷移を検証できる。また、AlarmManagerの挙動がOEMのスキンによって異なることも判明した。一部のメーカー、特に積極的なバッテリー管理を重視するメーカーは、ForegroundServiceの実装にもかかわらず、バックグラウンドサービスを時折強制終了することがあった。

端末がBOOT_COMPLETEDブロードキャストを受信するたびにジオフェンスを再登録する固有のロジックを書く必要があった。それがなければ、システムアップデートや再起動だけで、ユーザーは「exit」遷移が発火しないために、ずっと無音の電話を持ち続けることになる。Androidでは、オペレーティングシステムが状態を維持してくれると信用できないという教訓だ。アプリは常にメモリから強制退去させられているかのように扱い、突然の唐突な終了に対しても耐えうるデータ永続化を設計しなければならない。

実践的なポイント

Android向けユーティリティを開発するなら、「常時稼働」のロジックという考え方をやめるべきだ。最もエレガントな解決策は、可能な限りOSに責任をオフロードするものだ。カスタムのバックグラウンドループを書くたびに、エネルギー消費を10分の1に抑えられるネイティブAPIを見逃している可能性が高い。システムのブロードキャストトリガーを利用しよう。遅延タスクにはWorkManagerを、位置情報対応機能にはGeofencingClientを活用する。

Androidシステムのバッテリー最適化と戦うのではなく、その中でどう働くかを学ぼう。アプリがバッテリー使用量の内訳に表示されなければ、ユーザーは気づくだろう。サウンドプロファイルの管理や端末状態の自動化に苦労しているなら、Muffleでこれらの制約にどうアプローチしたかを見ることができる。これは「必要なときだけ作業を行う」という哲学と、ユーザーデータを端末内に留めることに基づいて構築されたプロジェクトだ。これらのルーチンの実装詳細とロジックは、https://play.google.com/store/apps/details?id=com.muffle.appで確認できる。現実世界向けに開発するとは、ハードウェアの混沌とした性質を認識し、そのノイズに耐えうるコードを作成することだ。