概要
Android で落ちないサービスを作ろうとした時の記録です。 使用履歴を参照してアプリの起動を監視するサービスを作っています。
確認環境
- Android Studio 2.1.2
- Build Tools Version 24.0.1
- Compile SDK Version 24
- Target SDK Version 24
- Min SDK Version 22
- 実行環境 Sony SOV32 Android 6.0, API 23
参考情報
公式の情報。
- 他、落ちないサービスで検索
解説
下記のソースコードが最終的に作成されたコード。
MainActivity
startService によりサービスを起動するだけ。
StartupReceiver
端末を起動した時にサービスを自動的に起動するために作成。
AndroidManifest.xml で BOOT_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 を許容するか、しないかを指定する。
onStartCommand
や onHandleIntent
の引数の型は Intent?
(null を許容する)とも Intent
(null を許容しない)とも書ける。
Intent
と書いていていると null が渡って来た時に例外が発生する。
この例外を補足できず、原因不明でサービスが落ちるという不具合に悩まされた。
(USBで繋いでいない場合に log を見る方法を見つけないと、、、。)
ここまで書いておいて何だが、AlarmManager を使う方法の方がシンプルで良いかもしれない。