Androidでアラームアプリを作る

## はじめに

HAL大阪Web開発学科の3年生です。
学校ではWeb開発学科ということでWebのフロントエンドやサーバサイドの勉強をしているのですが、趣味やアルバイトでAndroidのアプリを作っていたりします。

さて、HALでは3年生の10月に1ヶ月間、インターンシップに行くことになっているのですが、今回はそのときにアラームアプリを作成したのでその知見を備忘録として残しておきます。

## AlarmManager

AndroidはiOSと違い、OSに対して「指定時刻になったら〜をして」と登録することができます。
これによりアプリケーションが起動していなくてもAlarmManagerで登録しておくことで指定したタイミングに処理を走らせることが可能です。
(ちなみにアプリ起動中に定期的に処理を行うとかであればHandlerを使ったほうがいいです)

## 基本的な流れ

1. AlarmManagerにPendingIntent#getBroadcastでBroadcastReceiverを登録
2. 時刻になるとBroadcastReceiverが起動するので再生画面のActivityを起動
3. Activityが起動したら、アラーム音再生のServiceを起動
4. Serviceが起動したらアラーム音を鳴らす

ソースコードは[Github](https://github.com/hiroaki-dev/AlarmSample)で公開しているので詳しくはそちらを見てください。

## ポイント

### API LevelごとにAlarmManagerに登録するメソッドが違う

AndroidではAPI Levelが上がるごとにバッテリー消耗を抑えるための仕組みが色々と追加されています。
AlarmManagerも同様で、アプリケーションごとに個別に発火させず、できるだけ同じタイミングで一気に発火させることでバッテリーを抑えようという動きがあり、API Levelごとに適切なメソッドを使っていないとBroadcastReceiverが呼ばれるタイミングがずれてしまいます。

```java:MainActivity.java
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = getPendingIntent();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(alarmTimeMillis, null), pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pendingIntent);
} else {
    alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pendingIntent);
}
```

上記のように、

| API Level | メソッド |
|:----------|---------|
|21（Android5.0)〜|AlarmManager#setAlarmClock()|
|19(Android4.4）|AlarmManager#setExact()|
|〜18(Android4.3)|AlarmManager#set()|

を使うことでほぼ指定時刻通りにPendingIntentが呼び出されます。
AlarmManagerには他にもsetRepeating()などのリピート可能なメソッドがあり、"毎日この時間にアラームをセットする"といった場合に使えそうに思うのですが、これらも先に示した理由や後述するDozeへの対応を考えた場合、Android4.4以上では利用しない方が懸命です。
AlarmManagerが発火したタイミングで次のアラームをセットするという方法を取りましょう。


### Dozeへの対応

Android6.0からDozeモードという一定時間操作をしていないと端末が機能を制限して電力消費を抑えるモードに入ります。
このDozeモードではAlarmManagerの動作にも影響があり、set()やsetExact()などはうまく動作しません。
代わりにsetAndAllowWhileIdle()やsetExactAndAllowWhileIdle()というメソッドが提供されているのですが、これらには

* 実行時間が10秒以内
* 9分に一回以上実行できない

という制限があり、アラームアプリでこれらの制限は厳しいです。
そこでsetAlarmClock()を使います。
このメソッドを利用した場合、Doze中でも指定時刻が近づくとDozeを解除して実行されるため前述の制限を受けません。
ただし、Dozeを解除して端末自体を起こすためバッテリーへの影響が大きく、使い所には注意する必要があります。

ちなみにDozeモードを再現するには、端末の画面をオフにし、

```bash
# バッテリー駆動状態にする
$ adb shell dumpsys battery unplug
# Idleを有効化
$ adb shell dumpsys deviceidle enable
# 状態を以降（何回か繰り返す）
$ adb shell dumpsys deviceidle step
```
とすることで再現できます。
setAlarmClock()を利用していて、実行時刻がもうすぐの場合、``adb shell dumpsys deviceidle step``を実行してもIdle状態にならないことが確認できると思います。

### getWindow().addFlags()

ReceiverからActivity起動する際以下のようにFlagを追加する必要があります。
これがないと画面が点灯しません。

```java:PlaySoundActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
            WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
            WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    …
```

**※エミュレータを利用している場合、このFlagを追加していても画面が付かない場合があるので注意(バグ？)**


## 参考

* [Android M/NのDozeによる制限とバックグラウンドタスク実行に関するまとめ](http://qiita.com/FumihikoSHIROYAMA/items/b1d6dbda120462d0e209)
* [Android FLAG_DISMISS_KEYGUARD と FLAG_SHOW_WHEN_LOCKED を使う](http://y-anz-m.blogspot.jp/2011/08/androidflagdismisskeyguard.html)