Android の AlarmManager を改めて整理してみる

# Android の AlarmManager

[AlarmManager](https://developer.android.com/reference/android/app/AlarmManager.html) といえば、Android において文字通りアラームやリマインダを実装する際に用いたり、或いはバックグラウンドで定期的に行いたい処理のスケジューリングに用いたり（こちらは [JobScheduler](https://developer.android.com/reference/android/app/job/JobScheduler.html) に取って代わられた感じですが）、割りと使用する機会が多いものです。

同時に、Androidの歴史において、バージョンアップに伴って挙動が変更になったりと、割りと罠も多い存在だったりします。

挙動が変更になるたびに「変更への対応方法」を記した記事が出回るので助かるのですが、「今からAndroidアプリの開発を始めます！」という人にとってはそれらを全部拾い集めるのも大変でしょうし、ここらで AlarmManager の変化の歴史と共にざっくりまとめてみたいと思います。


# そもそも登録内容が永続化されない罠

AlarmManager を使い始めて最初に面食らう点。

AlarmManager は `set(int type, long triggerAtMillis, PendingIntent operation)` といった形で登録を行うのですが、この登録内容は永続化されそうな雰囲気なのに、実際は端末を再起動すると揮発してしまいます。

ではどうするのかと言うと、「端末起動時に再登録」すればOKなわけです。

- スケジュール情報を自分でDBなり SharedPreferencesなりに保存しておく
- `android.intent.action.BOOT_COMPLETED` インテントを受け取る `BroadcastReceiver` で、上記の保存情報を元に再登録する

## Android 7.0 の Direct Boot 有効化時の注意

Android 7.0 から [Direct Boot](https://developer.android.com/about/versions/nougat/android-7.0.html?hl=ja#direct_boot) なる機能が追加されました。

> ダイレクト ブートはデバイスのスタートアップ時間を短縮し、予期しない再起動後でも、登録されたアプリの一部の機能が使用できるようにします。
> たとえば、ユーザーの就寝中に暗号化された端末が再起動した場合でも、登録したアラーム、メッセージ、電話の着信をユーザーに通常どおり通知することができます。
> また、再起動後にユーザー補助機能サービスをすぐに使用することもできます。

Direct Boot はそもそも [開発者オプション] > [ファイル暗号化に変換する] を実行しないと有効にならない（もしくは [adbの fastbootコマンド](https://developer.android.com/training/articles/direct-boot.html?hl=ja#migrating) から有効化）のですが、仮に有効にしているとどうなるかと言うと、 **端末再起動後の初回のロック解除までは、 `BOOT_COMPLETED` が発行されなくなります。**

つまり「`BOOT_COMPLETED` を受け取ってアラーム再設定」などしていたら、前述の例にある「就寝中に再起動」なんてのが発生した場合、アラームが通知されないという悲劇が起こるのです。

代わりに、従来 `BOOT_COMPLETED` が発行されていたタイミングでは、 `ACTION_LOCKED_BOOT_COMPLETED` というインテントが発行されます。
よってこれを受け取る Receiver を実装すればよいわけです。

```xml
<receiver
  android:directBootAware="true" >
  <intent-filter>
    <action android:name="android.intent.action.ACTION_LOCKED_BOOT_COMPLETED" />
  </intent-filter>
</receiver>
```

その際、 `android:directBootAware` を true に設定する必要があります。

これでOK！…とは行かず、「再起動から初回ロック解除までの間」特有の制限に気を付ける必要があります。
この状態ではアクセスできるストレージに制限が掛かり、具体的には **端末暗号化ストレージ** にしかアクセスできません。

通常は Application の Context の対して、ファイルパス取得系の操作をするのですが、 `Context.createDeviceProtectedStorageContext()` を呼び出すことで、ロック解除前用の Context を生成し、これに対してファイルパス取得系の操作をすることになります。

```java
// 通常の Application の Context に対し、Direct Boot 用の Context を生成
Context directBootContext = context.createDeviceProtectedStorageContext();
```

その他細かい諸々は、 [公式を参照](https://developer.android.com/training/articles/direct-boot.html?hl=ja#access) するのが早いです。


# 正しく登録しないと ズレる／発火しない

## Android の歴史は サボりの歴史

きっとAndroidは「電池持たねーよ！」と言われ続けてきたのでしょう（勝手な推測）。
Android は「バッテリー長持ち」を目指して、「いかにして処理をサボるか」を追求しながら進化してきたのです。
その過程で AlarmManager も何度も挙動を変更されてきたのでした。

## API Level 19 （Android 4.4）から、タイミングが雑になった

…という見出しは雑なのですが、従来は [set](https://developer.android.com/reference/android/app/AlarmManager.html#set(int,%20long,%20android.app.PendingIntent)) メソッドで登録しておけば正確な時刻に発火してくれていたのですが、この正確性が保証されなくなりました。

じゃあアラームなどの正確性が要されるものはどうすればいいのかと言うと、代わりに [setExact](http://qiita.com/FumihikoSHIROYAMA/items/d/app/AlarmManager.html#setExact(int,%20long,%20android.app.PendingIntent)) というメソッドが新設されたのでした。

- アラームなどの正確性が必要なものは `setExact`
- 定期処理などの多少タイミングがズレても構わないものは `set`

という使い分けになったのです。

## API Level 23 （Android 6.0）から、Dozeモードが追加された

Android は更にサボりを極め、Android 6.0 からは遂に居眠りするようになりました。 **Dozeモード** です。

Dozeモード自体はここでは語りきれないぐらい色んな制約やルールがあるので、 [公式ドキュメント](https://developer.android.com/training/monitoring-device-state/doze-standby.html) を読みましょう。

さて、当の AlarmManager は Doze中にどうなるかと言うと、 **発火されなくなります** 。酷い。
加えて、Doze が解除されたタイミングで、これまで発火されなかったものが一気に発火されるので、予定と違うタイミングでの発火が起こります。

じゃあアラームなどの正確性が要されるものはどうすればいいのかと言うと、 [setExactAndAllowWhileIdle](https://developer.android.com/reference/android/app/AlarmManager.html#setExactAndAllowWhileIdle(int,%20long,%20android.app.PendingIntent)) なんてものが追加されました。
こちらを使用すればOKです。

ちなみに「タイミングの正確性は要らないけど、発火は欲しい」という場合は、 [setAndAllowWhileIdle](https://developer.android.com/reference/android/app/AlarmManager.html#setAndAllowWhileIdle(int,%20long,%20android.app.PendingIntent)) というものが用意されました。

### しかしそれすら完璧ではない

実はまだ制約がありまして、例え `setExactAndAllowWhileIdle` を使ったとしても、「（当時は）15分間に1回までしか発火できない」という制限があるのです。

じゃあアラームなどの正確性が要（中略）言うと、実は API Level 21 （Android 5.0）からあった [setAlarmClock](https://developer.android.com/reference/android/app/AlarmManager.html#setAlarmClock(android.app.AlarmManager.AlarmClockInfo,%20android.app.PendingIntent)) というメソッドを使えばOK。
これで本当にOK。

ただしこれを使うと、ステータスバーにアラームアイコンが表示されます。
なので、そのものズバリ「アラームアプリ」とかならともかく、それ以外のアプリでは使いづらいですね。

というか、折角 Android が処理をサボって電池を長持ちさせようとしているのを阻害することになりますので、**多様は禁物**。


## Android 7.0 から Doze中の最小発火間隔が厳しくなった

Android 7.0 からは、 [Doze が2段階になったり](https://developer.android.com/about/versions/nougat/android-7.0-changes.html#doze) しているのですが、ここでは割愛します。

細かい点で気になったのは、 [Doze中のアラーム（setAlarmClock ではない方）の最小発火間隔が9分](https://developer.android.com/training/monitoring-device-state/doze-standby.html) と記載されていたこと。

あれ？以前は 15分 って書いてなかったっけ？
…と思って [WebArchive を見てみたら](https://web.archive.org/web/20151001042926/http://developer.android.com/training/monitoring-device-state/doze-standby.html) 確かに以前は 15分 だった！

しれっと縮んどる！

## バージョン別 登録の方法まとめ

というわけで、今日日うっかり「Android 4.3もサポート」なアプリで、正確性を求めるスケジューリング機構を実装しようとすると、

- API Level 19未満は、 `set`
- API Level 19以上 23未満は、 `setExact`
- API Level 23以上は、 `setExactAndAllowWhileIdle`

なんて分岐が必要になったりします。


中々ややこしいですが、是非使いこなして、AlarmManager と仲良くなって下さい。
気難しいですがイイ奴なんです。