落ちないサービスでアプリの起動を監視する

概要

Android で落ちないサービスを作ろうとした時の記録です。 使用履歴を参照してアプリの起動を監視するサービスを作っています。

確認環境

参考情報

公式の情報。

  • 他、落ちないサービスで検索

解説

下記のソースコードが最終的に作成されたコード。

MainActivity

startService によりサービスを起動するだけ。

StartupReceiver

端末を起動した時にサービスを自動的に起動するために作成。 AndroidManifest.xmlBOOT_COMPLETED アクションを受け取るように設定する必要がある。 RECEIVE_BOOT_COMPLETED 権限の設定も忘れずに。

AppMonitorService

処理の中心は、使用履歴を参照してアプリの起動を検出する処理。 isUsageStatsAllowed, allowUsageStats, getForegroundApps が関係する。 isUsageStatsAllowed メソッドでは使用履歴を参照できることを確認。 使用履歴を参照できない場合は allowUsageStats メソッドを呼んで使用履歴へのアクセス許可を取得。 使用履歴を参照できる場合は getForegroundApps メソッドを呼んで指定した期間の中でフォアグラウンドに移動したアプリのパッケージ名リストを取得。 getForegroundApps(beginTime, endTime).lastOrNull() とすることで、最後にフォアグラウンドに移動したアプリを特定。 指定した期間の中でフォアグラウンドへの移動がなければ null になる。 ?: (エルビス演算子) と let メソッドを使うことで null ではない時のみログに表示。

アプリの起動を検出する処理は繰り返し実行し続ける。 while ループにより処理を継続するためにスレッドを生成する必要がある。 (while ループを使わない別の方法もある。) IntentService を拡張すればスレッドを生成し、複数のサービス起動処理を1つのスレッドで処理させられる。 IntentService を拡張する場合は onHandleIntent メソッドをオーバーライドする。 onHandleIntent はスレッドセーフな実装になっていて、専用のワーカースレッドが生成され、そのワーカースレッドで実行される。 今回は諸事情により、サービス起動中にサービスの再起動を受け付ける。 そのため、onStartCommand メソッドをオーバーライドし、while ループを終了できるようにしている。 while ループはバッテリー消費を考慮して、sleep するようにしている。 interrupt メソッド呼び出しは今回のサンプルでは不要だが、先に述べた諸事情により呼ばれている。 諸事情とは、サービス起動時のパラメーターによりスリープ時間を長くできるようにすること。

繰り返し処理はアプリが終了しても実行し続ける必要があるためサービスとして実装。 サービスは他のアプリの影響によりメモリ不足となった場合に停止されることがある。 サービスを停止されないようにするためにサービスを フォアグラウンド で実行する。 フォラグラウンドで実行されるサービスはユーザーが存在を認知していることを前提とする。 そのため、通知によりサービスの存在を示す必要がある。 通知への表示を行う処理は startNotification メソッドが行う。 通知領域をタップした時に MainActivity が起動されるように setContentIntent によりインテントを設定。 notificationId は通知を識別するための ID。

フォアグラウンドで実行すればサービスは落ちないはずと思いきや、落ちることがある。(なぜ?) 落ちた場合に即座にサービスが復帰されるようにする。 onStartCommand の戻り値を START_STICKY にする。 サービスが復帰する時は intent が null で onStartCommand が実行されるので注意が必要。

Kotlin の場合は、型で null を許容するか、しないかを指定する。 onStartCommandonHandleIntent の引数の型は Intent?(null を許容する)とも Intent(null を許容しない)とも書ける。 Intent と書いていていると null が渡って来た時に例外が発生する。 この例外を補足できず、原因不明でサービスが落ちるという不具合に悩まされた。 (USBで繋いでいない場合に log を見る方法を見つけないと、、、。)

ここまで書いておいて何だが、AlarmManager を使う方法の方がシンプルで良いかもしれない。