反復アラームのスケジュールを設定する

AlarmManager クラスをベースにしたアラームを使用すると、アプリの実行期間外に時間ベースで処理を実行できます。たとえば、1 日に 1 回サービスを開始して天気予報をダウンロードするなど、アラームを使用することによって実行時間が長い処理を開始できます。

アラームには以下の特徴があります。

  • 設定した時間または周期的(あるいはその両方)にインテントを開始できます。
  • ブロードキャスト レシーバと組み合わせて使用すると、サービスを開始して他の処理を実行することができます。
  • アラームはアプリの外部で動作するため、アプリが実行されていないときや、デバイス自体がスリープ状態になっているときでも、アラームを使用してイベントやアクションをトリガーできます。
  • アラームを通じてアプリのリソース要件を最小限に抑えることができます。タイマーを使用したり、バックグラウンド サービスを継続的に実行したりしなくても、処理のスケジュールを設定できます。

注: アプリの実行期間中に必ず実行されるタイミング処理には、Handler クラスと Timer および Thread を同時に使用することを検討してください。このアプローチを使用すると、Android でシステム リソースを詳細に管理できます。

トレードオフを理解する

反復アラームは、メカニズムは比較的シンプルですが、柔軟性が不足しています。ネットワーク操作をトリガーする必要がある場合は特に、アプリにとって最適な選択肢とはならないかもしれません。アラームが適切に設計されていないと、電池を消耗させるだけでなく、サーバーへの負荷が大きくなる可能性があります。

アプリの実行期間外に処理をトリガーする場合、通常のシナリオではデータをサーバーと同期します。このようなケースでは、反復アラームを使用したくなるかもしれません。しかし、アプリのデータをホストしているサーバーを所有している場合は、Google Cloud Messaging(GCM)を同期アダプターと組み合わせて使用する方が AlarmManager より効果的なソリューションになります。同期アダプターのスケジュール設定オプションは AlarmManager とまったく同じですが、柔軟性は同期アダプターの方がはるかに優れています。たとえば同期は、サーバーまたはデバイスからの「新しいデータ」メッセージ(同期アダプターの実行を参照)、ユーザーのアクティビティ(または非アクティブ状態)、時間帯などに基づいて行うことができます。GCM および同期アダプターを使用するタイミングと方法について詳しくは、このページの最上部のリンク先の動画をご覧ください。

デバイスが Doze モードでアイドル状態になっている場合、アラームは起動しません。スケジュール設定されたアラームは、デバイスが Doze モードを終了するまで延期されます。デバイスがアイドル状態でも処理を完了させる必要がある場合に使用できる方法はいくつかあります。setAndAllowWhileIdle() または setExactAndAllowWhileIdle() を使用すると、アラームが必ず実行されるようにすることができます。また、新しい WorkManager API を使用する方法もあります。この API は、バックグラウンド処理を 1 回または定期的に実行するように設定できます。詳しくは、WorkManager によるタスクのスケジュール設定をご覧ください。

おすすめの方法

反復アラームを設計する際に行うあらゆる選択が、アプリによるシステム リソースの使用方法に影響を及ぼし、選択次第でシステム リソースの乱用につながることもあります。たとえば、サーバーと同期する一般的なアプリについて考えてみましょう。同期処理が時刻に基づいて行われ、アプリのすべてのインスタンスが午後 11 時に同期される場合、サーバーに対する負荷により、レイテンシが高くなる可能性があるほか、「サービス拒否」につながる恐れもあります。アラームを使用する場合は、以下のおすすめの方法に従ってください。

  • 反復アラームの結果としてトリガーされるすべてのネットワーク リクエストにランダム性(ジッター)を追加します。
    • アラームがトリガーされたときにローカル処理を実行します。「ローカル処理」とは、サーバーにアクセスしない処理、またはサーバーのデータを必要としない処理を意味します。
    • 一方、ネットワーク リクエストを含むアラームは、ランダムな時間帯にトリガーされるようにスケジュール設定します。
  • アラームの頻度は最小限に維持します。
  • 不必要にデバイスのスリープを解除しないでください(アラームタイプを選択するで説明するように、この動作はアラームタイプによって決まります)。
  • アラームのトリガー時間を必要以上に正確に設定しないでください。

    そのためには、setRepeating() ではなく setInexactRepeating() を使用します。setInexactRepeating() を使用すると、Android によって複数のアプリの反復アラームが同期され、それらが同時にトリガーされます。これにより、デバイスのスリープを解除する回数が減り、電池の消耗を抑えることができます。Android 4.4(API レベル 19)の時点では、すべての反復アラームが不正確です。setInexactRepeating()setRepeating() に比べて改善されていますが、それでも、アプリのすべてのインスタンスがほぼ同時にサーバーにアクセスすると、サーバーに大きな負荷がかかる可能性があります。そのため、ネットワーク リクエストの場合は、前述のようにアラームにランダム性を追加します。

  • 可能であれば、時刻に基づいてアラームをトリガーすることは避けてください。

    正確なトリガー時刻に基づく反復アラームはうまく調整できません。できれば ELAPSED_REALTIME を使用してください。アラームタイプについては次のセクションで詳しく説明します。

反復アラームを設定する

前述のとおり、反復アラームは定期的なイベントやデータ検索のスケジュール設定に適しています。反復アラームには以下の特徴があります。

  • アラームタイプ。詳しくは、アラームタイプを選択するをご覧ください。
  • トリガー時間。指定したトリガー時間が過去の場合、アラームがすぐにトリガーされます。
  • アラームの間隔。たとえば、1 日 1 回、1 時間ごと、5 分ごとなどです。
  • アラームがトリガーされたときに開始するペンディング インテント。同じペンディング インテントを使用する 2 つ目のアラームを設定すると、そのアラームで元のアラームが置き換えられます。

アラームタイプを選択する

反復アラームを使用する場合、最初にどのタイプにするかを検討する必要があります。

アラームの一般的なクロックタイプには、「実経過時間」と「リアルタイム クロック」(RTC)の 2 つがあります。実経過時間ではリファレンスとして「システムが起動してからの経過時間」を使用し、リアルタイム クロックでは UTC(ウォール クロック)時間を使用します。つまり、実経過時間はタイムゾーンやロケールの影響を受けないため、時間の経過に基づくアラームの設定に適しています(30 秒ごとにトリガーされるアラームなど)。リアルタイム クロックは、現在のロケールに依存するアラームに適しています。

どちらのタイプにも、画面がオフの場合にデバイスの CPU のスリープを解除するように指示する「wakeup」バージョンが用意されています。これにより、スケジュール設定された時間にアラームを確実にトリガーできます。この機能は、アプリが時間に依存している場合(特定の処理を制限時間内に行う必要がある場合など)に便利です。wakeup バージョンのアラームタイプを使用しない場合、デバイスが次に起動したときにすべての反復アラームがトリガーされます。

単に特定の間隔(30 分ごとなど)でアラームをトリガーする必要がある場合は、実経過時間タイプのいずれかを使用します。通常はこのタイプを選択することをおすすめします。

特定の時間帯にアラームをトリガーする必要がある場合は、時間ベースのリアルタイム クロックタイプのいずれかを選択します。ただし、このアプローチには欠点がいくつかあります。アプリは他のロケールに適切に翻訳されない場合があり、ユーザーがデバイスの時間設定を変更すると、アプリで予期しない動作が発生することがあります。また、アラームタイプにリアルタイム クロックを使用すると、前述のとおり、アラームをうまく調整できません。できれば、アラームタイプには「実経過時間」を使用することをおすすめします。

以下に、アラームタイプの一覧を示します。

  • ELAPSED_REALTIME - デバイスが起動してからの経過時間に基づいてペンディング インテントを開始しますが、デバイスのスリープは解除しません。経過時間には、デバイスがスリープしていた時間が含まれます。
  • ELAPSED_REALTIME_WAKEUP - デバイスが起動してから指定された時間が経過した後にデバイスのスリープを解除し、ペンディング インテントを開始します。
  • RTC - 指定された時間にペンディング インテントを開始します。ただし、デバイスのスリープは解除しません。
  • RTC_WAKEUP - 指定された時間にデバイスのスリープを解除してペンディング インテントを開始します。

実経過時間タイプのアラームの例

以下に、ELAPSED_REALTIME_WAKEUP の使用例をいくつか示します。

30 分後にデバイスのスリープを解除してアラームをトリガーします。その後は 30 分ごとにこの処理を行います。

Kotlin

    // Hopefully your alarm will have a lower frequency than this!
    alarmMgr?.setInexactRepeating(
            AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR,
            AlarmManager.INTERVAL_HALF_HOUR,
            alarmIntent
    )
    

Java

    // Hopefully your alarm will have a lower frequency than this!
    alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR,
            AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
    

1 分後にデバイスのスリープを解除し、アラームを 1 回だけ(反復なし)トリガーします。

Kotlin

    private var alarmMgr: AlarmManager? = null
    private lateinit var alarmIntent: PendingIntent
    ...
    alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
        PendingIntent.getBroadcast(context, 0, intent, 0)
    }

    alarmMgr?.set(
            AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime() + 60 * 1000,
            alarmIntent
    )
    

Java

    private AlarmManager alarmMgr;
    private PendingIntent alarmIntent;
    ...
    alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(context, AlarmReceiver.class);
    alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

    alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime() +
            60 * 1000, alarmIntent);
    

リアルタイム クロックタイプのアラームの例

以下に、RTC_WAKEUP の使用例をいくつか示します。

午後 2 時ごろにデバイスのスリープを解除してアラームをトリガーします。毎日 1 回、同じ時間にこの処理を行います。

Kotlin

    // Set the alarm to start at approximately 2:00 p.m.
    val calendar: Calendar = Calendar.getInstance().apply {
        timeInMillis = System.currentTimeMillis()
        set(Calendar.HOUR_OF_DAY, 14)
    }

    // With setInexactRepeating(), you have to use one of the AlarmManager interval
    // constants--in this case, AlarmManager.INTERVAL_DAY.
    alarmMgr?.setInexactRepeating(
            AlarmManager.RTC_WAKEUP,
            calendar.timeInMillis,
            AlarmManager.INTERVAL_DAY,
            alarmIntent
    )
    

Java

    // Set the alarm to start at approximately 2:00 p.m.
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(System.currentTimeMillis());
    calendar.set(Calendar.HOUR_OF_DAY, 14);

    // With setInexactRepeating(), you have to use one of the AlarmManager interval
    // constants--in this case, AlarmManager.INTERVAL_DAY.
    alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
            AlarmManager.INTERVAL_DAY, alarmIntent);
    

午前 8 時半ちょうどにデバイスのスリープを解除してアラームをトリガーします。その後は 20 分ごとにこの処理を行います。

Kotlin

    private var alarmMgr: AlarmManager? = null
    private lateinit var alarmIntent: PendingIntent
    ...
    alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
        PendingIntent.getBroadcast(context, 0, intent, 0)
    }

    // Set the alarm to start at 8:30 a.m.
    val calendar: Calendar = Calendar.getInstance().apply {
        timeInMillis = System.currentTimeMillis()
        set(Calendar.HOUR_OF_DAY, 8)
        set(Calendar.MINUTE, 30)
    }

    // setRepeating() lets you specify a precise custom interval--in this case,
    // 20 minutes.
    alarmMgr?.setRepeating(
            AlarmManager.RTC_WAKEUP,
            calendar.timeInMillis,
            1000 * 60 * 20,
            alarmIntent
    )
    

Java

    private AlarmManager alarmMgr;
    private PendingIntent alarmIntent;
    ...
    alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(context, AlarmReceiver.class);
    alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

    // Set the alarm to start at 8:30 a.m.
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(System.currentTimeMillis());
    calendar.set(Calendar.HOUR_OF_DAY, 8);
    calendar.set(Calendar.MINUTE, 30);

    // setRepeating() lets you specify a precise custom interval--in this case,
    // 20 minutes.
    alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
            1000 * 60 * 20, alarmIntent);
    

アラームに必要な精度を決定する

前述のとおり、アラームの作成では通常、最初にアラームタイプを選択します。さらに、アラームに必要な精度も選択する必要があります。ほとんどのアプリでは、setInexactRepeating() を選択することをおすすめします。このメソッドを使用すると、Android によって複数の不正確な反復アラームが同期され、それらが同時にトリガーされます。これにより、電池の消耗を抑えることができます。

厳密な時間要件のあるアプリもまれにありますが(午前 8 時半ちょうどにアラームをトリガーし、その後は毎正時にトリガーする必要があるなど)、その場合は setRepeating() を使用します。ただし、可能な場合は正確なアラームを使用しないでください。

setInexactRepeating() では、カスタムの間隔を指定できません(setRepeating() では指定可能)。代わりに、間隔定数(INTERVAL_FIFTEEN_MINUTESINTERVAL_DAY など)のいずれかを使用する必要があります。定数の完全なリストについては、AlarmManager をご覧ください。

アラームをキャンセルする

アプリによっては、アラームをキャンセルする機能を追加することをおすすめします。アラームをキャンセルするには、Alarm Manager で cancel() を呼び出し、開始する必要がなくなった PendingIntent を渡します。次に例を示します。

Kotlin

    // If the alarm has been set, cancel it.
    alarmMgr?.cancel(alarmIntent)
    

Java

    // If the alarm has been set, cancel it.
    if (alarmMgr!= null) {
        alarmMgr.cancel(alarmIntent);
    }
    

デバイスの再起動時にアラームを開始する

デフォルトでは、デバイスがシャットダウンするとすべてのアラームがキャンセルされます。アラームがキャンセルされないようにするには、ユーザーがデバイスを再起動したときに自動的に反復アラームを再開するようにアプリを設計します。こうすることで、ユーザーが手動でアラームを再開しなくても AlarmManager によってタスクの実行が継続されます。

手順は次のとおりです。

  1. アプリのマニフェストで RECEIVE_BOOT_COMPLETED 権限を設定します。これにより、システムが起動を終了した後にブロードキャストされる ACTION_BOOT_COMPLETED をアプリで受信できます(この処理は、アプリがユーザーによって 1 回以上起動されたことがある場合にのみ行われます)。
        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  2. ブロードキャストを受信する BroadcastReceiver を実装します。

    Kotlin

        class SampleBootReceiver : BroadcastReceiver() {
    
            override fun onReceive(context: Context, intent: Intent) {
                if (intent.action == "android.intent.action.BOOT_COMPLETED") {
                    // Set the alarm here.
                }
            }
        }
        

    Java

        public class SampleBootReceiver extends BroadcastReceiver {
    
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
                    // Set the alarm here.
                }
            }
        }
        
  3. アプリのマニフェスト ファイルにレシーバを追加し、ACTION_BOOT_COMPLETED アクションに基づいてフィルタするインテント フィルタを指定します。
    <receiver android:name=".SampleBootReceiver"
                android:enabled="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"></action>
            </intent-filter>
        </receiver>

    マニフェストでは、起動レシーバが android:enabled="false" に設定されます。つまり、アプリで明示的にレシーバを有効にしない限り、レシーバは呼び出されません。このため、起動レシーバが不必要に呼び出されるのを防ぐことができます。レシーバを有効にするには次のように記述します(ユーザーがアラームを設定する場合など)。

    Kotlin

        val receiver = ComponentName(context, SampleBootReceiver::class.java)
    
        context.packageManager.setComponentEnabledSetting(
                receiver,
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP
        )
        

    Java

        ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
        PackageManager pm = context.getPackageManager();
    
        pm.setComponentEnabledSetting(receiver,
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);
        

    この方法でレシーバを有効にすると、ユーザーがデバイスを再起動しても有効なままになります。つまり、レシーバをプログラムで有効にした場合、再起動後もマニフェストの設定より優先されます。レシーバは、アプリによって無効にされるまで有効なままになります。レシーバを無効にするには次のように記述します(ユーザーがアラームをキャンセルする場合など)。

    Kotlin

        val receiver = ComponentName(context, SampleBootReceiver::class.java)
    
        context.packageManager.setComponentEnabledSetting(
                receiver,
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP
        )
        

    Java

        ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
        PackageManager pm = context.getPackageManager();
    
        pm.setComponentEnabledSetting(receiver,
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
        

Doze とアプリ スタンバイの影響

Doze とアプリ スタンバイは、デバイスの電池寿命を延ばすための取り組みの一環として、Android 6.0(API レベル 23)で導入されました。デバイスが Doze モードの場合、標準のアラームはすべて、デバイスが Doze モードを終了するか、メンテナンスの時間枠が始まるまで延期されます。Doze モードでもアラームをトリガーする必要がある場合は、setAndAllowWhileIdle() または setExactAndAllowWhileIdle() を使用します。アプリがアイドル状態になると、つまりユーザーが一定期間アプリを使用しておらず、アプリにフォアグラウンド プロセスがない場合、アプリはアプリ スタンバイ モードになります。アプリがアプリ スタンバイの状態になると、Doze モードの場合と同様にアラームが延期されます。この制限は、アプリがアイドル状態でなくなったとき、またはデバイスが充電状態になった場合に解除されます。これらのモードがアプリに及ぼす影響について詳しくは、Doze とアプリ スタンバイ用の最適化をご覧ください。

サンプルアプリ

このガイドのコンセプトを試すには、サンプルアプリをダウンロードしてください。