AlarmManagerを利用して特定の時間に処理をスケジューリングする

株式会社スタメンのTUNAGプロダクト開発部で Android アプリを開発しているカーキ(X: @khaki_ngy)です。

自分はスタメンには2020年に新卒入社しており、6年目の春が始まりソワソワした気持ちを抱いています🌸

直近、TUNAGの機能開発で AlarmManager の API を利用し、指定した時間にローカル通知を発行する機能を開発しました。 AlarmManager 自体はかなり古くからある API ですが、権限周りで Android12 から変更が加えられるなど近年のセキュリティ強化の影響も受けています。

今回のブログでは、AlarmManager を利用する一連の流れと、プロダクションで運用するための注意事項を紹介します。

AlarmManager とは

AlarmManager とは android.app API に存在するクラスであり、指定した時間に特定の処理を実行することができます。

アプリが起動していない状態(バックグラウンドにいる状態)でも、指定の時刻に処理を行うことができます。ユースケースとしては時計アプリのアラーム機能などでユーザーが指定した時刻にアラームや通知を発行するような時に利用されます。

今回 TUNAG では、指定された日時にローカル通知を発行するために採用しましたが、本当に AlarmManager を使うべきかどうかの見極めが必要です。

なぜならば、先述した通り AlarmManager は指定した時刻でバックグラウンドで強力な処理を実行できるポテンシャルがあるためです。正確な時刻にバックグラウンドで処理を起動できること自体が、バッテリーやリソースに影響を与える可能性があるため、AlarmManager を使って正確な時刻にアラームを設定するには後述する特別なアプリアクセス権の取得が必要になります。ユースケースに応じて、本当に AlarmManager を利用するべきかどうかを一度考えた方が良いでしょう。

代替手段との比較では、正確な時間指定が必要かどうかが軸になってきます。 正確な時間指定が必要でない場合、代替手段として公式ドキュメントでは Handler クラスの利用や、WorkManager での定期実行のスケジュールなどが紹介されています。WorkManager はライフサイクルの考慮や、処理を開始する上でのシステム的な制約を指定することができるので、遅延実行して問題のないバックグラウンド処理であれば WorkManager を利用するのが良いでしょう。

AlarmManager と権限

権限の種別

AlarmManager を利用して正確な時間に処理をスケジュールするには、以下の権限のうちどちらか一つが必要になります。

  • USE_EXACT_ALARM
  • SCHEDULE_EXACT_ALARM

どちらの権限も AlarmManager を通して、正確な時間に処理をスケジュールするのに必要な権限になりますが、どのような機能を提供するアプリかに応じてどちらの権限を利用するかが変わります。

前者のUSE_EXACT_ALARMは、アラーム・カレンダー機能が主となるアプリ向けの権限になります。Android の APIレベル33(Android 13相当)から登場した権限です。 アラーム・カレンダーアプリが主の機能となっていれば、指定の時刻に処理を行なったり、通知を送ったりする機能はほとんど必須の機能となります。そのため、AndroidManifest で権限の利用を宣言していれば、ユーザーの許可なしで正確な時間の処理が可能になります。 ただし、前提となっているアラーム・カレンダーアプリがメインの機能かどうかという点がアプリの審査で判断されることになります。アラーム・カレンダー機能が主となるアプリでない場合は、こちらの権限を利用してもストアへの公開は難しいでしょう。

後者のSCHEDULE_EXACT_ALARMは、前者の対象となる「アラーム・カレンダー機能が主となる」アプリ以外を対象とした権限になります。こちらはAndroidのAPIレベル31(Android 12相当)から登場した権限です。 こちらの権限では、アラームやカレンダー機能を主としたアプリ以外での利用を想定されています。 こちらの権限は Android の API 34以降(Android 14相当)では、デフォルトで拒否されるようになっており、ユーザー自身がアプリに「アラームとリマインダー」の権限を許可する必要があります。

「アラームとリマインダー」は特別なアプリアクセス権に分類され、通常の権限リクエストとは若干方法が異なり、設定画面でユーザーに『許可』をしてもらう必要があります。

システムのアプリ設定内にある「アラームとリマインダー」

正確な時間のアラームであっても AlarmManager のOnAlarmListenerオブジェクトを利用する場合は、SCHEDULE_EXACT_ALARMは不要になります。ただOnAlarmListenerを利用したアラームの場合はアプリのプロセスが生きている期間の間は有効になりますが、アプリキルをされた場合などプロセスが終了している状態では、アラームを起動させることができません。この後の内容に関してもOnAlarmListenerを利用せず、バックグラウンドでも機能するスケジューリング処理について紹介をしていきます。

権限のハンドリング

先述の通り、AlarmManager による(バックグラウンドで動作する)正確な時間での処理のスケジュールには、SCHEDULE_EXACT_ALARM権限が必要になります。

権限を獲得するフローは大体以下の流れです 1. ユーザーがすでに権限を持っているか確認する 2. 持っていなければ、権限のリクエストを要求する

1. 権限の確認

通常、権限の許可がされているかを確認する場合にはContextCompat.checkSelfPermissionを利用して、対象のパーミッションが許可されているかどうかを確認します。ただSCHEDULE_EXACT_ALARMは特別な権限となるため、AlarmManager に用意されている専用のメソッドcanScheduleExactAlarms()を利用して確認する必要があります。 (ContextCompat.checkSelfPermissionで確認しても常に許可されていないと返ってきてしまいます)

具体的なコードのイメージとしては以下のようになります。

val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
// Android 12 より前、もしくは権限が許可されている場合
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms()) {
    // alarmManagerによるスケジューリングを実施
} else {
         // 権限をリクエストする
}

2. 権限のリクエスト

権限が許可されていない場合は、ユーザーに権限をリクエストする必要があります。 一般的な権限のようにアプリ内のダイアログで権限リクエストをする仕組みが用意されていないため、設定画面に遷移させて、ユーザーに設定を変更してもらう必要があります。

以下のように『アラームとリマインダー』に関する設定ページに直接遷移するようなIntentを発行することで、ユーザーを設定画面に導くことができます。

val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
startActivity(intent)

上記では単純な Intent 遷移の例を示していますが、Activity Result API による遷移を利用すれば、設定画面から戻ってきたタイミングをハンドルできるので、より状況に適した処理を実装できると思います。

権限リクエスト時の注意点

SCHEDULE_EXACT_ALARMはシステムでの権限付与のダイアログが提供されていないため、アプリからユーザーに対して権限リクエストの旨を伝えるのが良いでしょう。 権限の付与が必要なタイミングで突然システムの設定画面に飛ばされてもユーザーは困惑してしまうためです。

そのため、下記のようなダイアログを一つ挟んで、アラームの設定を行うかどうかをユーザーに事前に確認をすると良いです。

ユーザーに向けたダイアログの例

AlarmManager を利用する

指定した時間にアラームをスケジュールする

AlarmManagerによりアラームをスケジュールする流れとしては、以下の2ステップです。

  1. PendingIntent を作成
  2. AlarmManager インスタンスからスケジューリングのメソッドを実行

それぞれについて解説していきます。

1. PendingIntent を作成

指定した時間にアラームを受け取る際に BroadcastReceiver を利用し、バックグラウンドでも BroadcastReceiver を受け取れるように PendingIntent を作成します。 この際、アラームのスケジュールを設定する上で考慮するべき点がいくつかあります。

val pendingIntent = PendingIntent.getBroadcast(
         context,
         requestCode,
         intent,
         PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

まずgetBroadcastの第二引数になっているrequestCodeです。 これは予約するアラームを一意に識別する役割があります。そのため、例えば、同じrequestCodeで複数アラームをスケジュールしても、最後にスケジュールした PendingIntent しか有効になりません。 また後からスケジュールしたアラームをキャンセルしたい場合にも同じrequestCodeから作成された PendingIntent が必要になります。

次に第三引数として指定しているintentについてです。 この Intent は、アラームが起動した時に実行させたいBroadcastReceiverに向けられた Intent を指定してください。

val intent = Intent(context, MyAlarmReceiver::class.java).apply {
   putExtra(MESSAGE_KEY, "アラームです")
}

このintentに対して、actionextraを指定することができるので、これらを指定することで、BroadcastReceiverの中で処理に必要な情報を渡すことができます。

第4引数の PendingIntent のフラグに何を渡すのかでもアラームの挙動が少し変化します。 PendingIntent.FLAG_IMMUTABLEは、PendingIntentの中身が他のアプリによって変更されることを防ぐフラグになります。Android API 31以降ではPendingIntent.FLAG_IMMUTABLEPendingIntent.MUTABLEのどちらかが必須となっています。アプリによってどちらを選択するべきかは異なりますが、他のアプリによって遷移先が勝手に書き換えられてしまう可能性があるので、基本的にはPendingIntent.FLAG_IMMUTABLEを設定しておいた方が良いでしょう。

またPendingIntent.FLAG_CANCEL_CURRENTは既に同じrequestCodeで生成されたPendingIntentがある場合に前の内容をキャンセルして、新たなPendingIntentとして登録するために設定をします。 近いものとしてはPendingIntent.FLAG_UPDATE_CURRENTというフラグも存在します。こちらは同じrequestCodeで生成されたPendingIntentがある場合に、中のIntentだけを新しいものに更新するフラグになります。 requestCodeを被らないように設定している場合であれば、どちらを選んでも結果は変わりませんが、同じ値が入る可能性があれば、どちらを選択するべきかをよく考える必要があります。

2. スケジューリングのメソッドを使う

アラームをセットするメソッドは複数存在し、アラームが「繰り返しなのか」「正確な時刻が必要なのか」に応じて適切なメソッドを選択する必要があります。

今回は1回きりのアラームsetを中心に紹介をします。 一回きりのアラームのsetをいくつか種類があります。

  • set : 最も効率の良い1回きりのアラームメソッド。効率は良いものの、端末の状態に大きく左右されるため、指定した時刻通りに発火することは保証されていません。
  • setExact : setメソッドよりは正確に発火することを目的としたメソッド。ただ端末がDozeモードに入ってしまうと発火しないので注意が必要。
  • setAndAllowWhileIdle : Dozeモードなど、端末がアイドル状態の場合でもスケジュール通りに発火するメソッド。端末の状態に関わらず実行させたい場合はこれを利用する。

またsetメソッドの第一引数にはAlarmManagerのTypeを指定する必要があります。 こちらは4種類のタイプがあります。

  • ELAPSED_REALTIME: デバイスが起動してからの経過時間に基づいてPendingIntentを開始する、デバイスのスリープは解除しない。
  • ELAPSED_REALTIME_WAKEUP: デバイスが起動してから指定された時間が経過した後にデバイスのスリープを解除し、PendingIntentを開始する
  • RTC: 指定された時間にPendingIntentを開始する。ただし、デバイスのスリープは解除しません。
  • RTC_WAKEUP: 指定された時刻にデバイスを復帰させてPendingIntentを開始する。

それぞれアラームの特性に合わせて実装をするのが良いと思います。

まとめると

アラームを発行する流れをまとめると以下のようになります。

val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val alarmIntent = Intent(context, MyAlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
    context,
    requestCode,
    alarmIntent,
    PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.RTC_WAKEUP,
    notifiedDateTime.toInstant().toEpochMilli(),
    pendingIntent
)

アラームされたタイミングで実行される処理

アラームされたタイミングで実行される処理はBroadcastReceiverで実施されるので、こちらの準備も必要になります。

class CalendarEventAlarmReceiver: BroadcastReceiver() {
    override fun onReceive(context: Context, intent : Intent) {
        // アラーム発火時に行う処理
    }
}

BroadcastReceiver の利用にはAndroidManifestでの利用の宣言も必要になるので忘れずに行いましょう。

まとめ

今回は、AlarmManager を使った実装の紹介を行いました。 古くからあるAPIではあるものの、Android API 23 からの Doze モードの登場や、Android API 31 以降の権限の強化によって、ここ数年でも実装や扱い方が変わってきているAPIになります。

AlarmManagerのドキュメントにはUSE_EXACT_ALARMSCHEDULE_EXACT_ALARMを利用する場合の注意点が書かれており、アプリケーションが提供したい機能に応じて、どのような方法でスケジュールをするのか(あるいは、AlarmManagerを使わないのか)を判断して利用する必要があります。

株式会社スタメンでは一緒に働く仲間を募集しています。少し気になる、話を聞いてみたい!という場合は以下のフォームからご連絡ください

herp.careers