AlarmManagerが信頼できない理由 〜鳴らないアラーム、消えるIntent〜

※ChatGPTを使用して記事を作成しています。

「一定時間後に処理を実行したい」「日次のリマインダーを作りたい」

そんな時に使われてきた古参API ――AlarmManager

その名前に「アラーム」とある通り、確実に鳴ってくれそうな安心感を持っていませんか?

かつての私も、そう信じていました。

ですが、現代のAndroidでは「登録したはずのアラームが鳴らない」ことは珍しくありません。

この記事では、私が実際に経験した「AlarmManagerが全く信頼できないと気付いた出来事」を通じて、現在のAndroidでの正しいアラーム設計について考えていきます。

背景:リマインダー機能にAlarmManagerを使った

あるToDoアプリに、「指定時刻に通知を出す」機能を追加することになりました。

ユーザーが朝8時に通知を受け取るように設定すれば、必ずその時刻に出したい。

開発初期、選んだのは古くからあるAlarmManager

実装はいたってシンプル:

val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, ReminderReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
    context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

val calendar = Calendar.getInstance().apply {
    set(Calendar.HOUR_OF_DAY, 8)
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
}

alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.RTC_WAKEUP,
    calendar.timeInMillis,
    pendingIntent
)

このコードは「Dozeモード中でもアラームを正確に鳴らす」ためのベストプラクティス……のはずでした。

問題発生:「通知が出ない」とのユーザー報告

リリース後、レビューにこんなコメントが増えてきました。

  • 「リマインダーが出たり出なかったりする」
  • 「朝の通知が来ない日がある」
  • 「最初は鳴ってたけど最近鳴らない」

社内でも再現テストを繰り返すうち、ある条件で通知が鳴らないことが判明しました。

しかもそれは、以下のような誰もが使いそうな状況。

  • スマホを充電せずに一晩放置
  • バッテリーセーバーON
  • OSバージョンがAndroid 6.0以上(つまりほぼ全端末)

AlarmManagerの限界:Dozeモードと省電力の壁

Android 6.0以降、端末は一定時間アイドル状態になると「Dozeモード」に入ります。

Doze中は、ネットワークやアラームなど多くのAPIが制限されます。

AlarmManagerも例外ではありません。

🔴 set()やsetExact()はDoze中に無視される

Android公式ドキュメントによると:

アプリがDozeモードのとき、set()、setExact()、setWindow()で設定されたアラームはシステムによって延期されます。

つまり、たとえ「8時ぴったり」にアラームを設定していても、Doze中であればその時刻に実行されないのです。

🟡 setExactAndAllowWhileIdle()を使えば動く?

確かにこれを使うことで、Doze中でもアラームが起動する可能性があります。

が、制限はあります:

  • 1時間に1回しか使えない(Doze中)
  • 頻繁に使うとOSに無視されることがある
  • 一部メーカー機では完全に制限される

つまり、「確実に」「毎日」「同じ時間に」実行するには不向きなのです。

隠れたトラップ:PendingIntentが消える問題

さらに深刻だったのが、PendingIntentが無効化されていたケース

AlarmManagerは、指定されたPendingIntentが一意である必要があります。

しかし、以下のようにIntentの内容が微妙に変わると、同じIDでも別物と見なされることがあります:

val intent = Intent(context, ReminderReceiver::class.java)
intent.putExtra("reminderId", reminderId) // ← これが原因で別物と判定される

val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

結果、重複してアラームが登録されたり、古いアラームが削除されなかったりして、動作がますます不安定に。

メーカー制限の闇:一部海外製端末では壊滅

一部の海外製端末では、AlarmManagerの動作が以下のように制限されます:

  • 画面OFF中はアラームが無視される
  • アプリが「バッテリー最適化対象」だとアラームすら届かない
  • サードパーティの通知制限アプリによってブロックされる

これらは開発者が制御できない領域であり、ユーザーが何かしないとアラームは鳴らないという状態に陥ります。

解決策:AlarmManagerに頼らない設計へ

私たちは最終的に、以下のように設計を見直しました:

✅ WorkManager + Exact Alarm Permission(Android 12+)

WorkManagerはAlarmManagerよりもOSの制限に強い

さらに、Android 12以降では、SCHEDULE_EXACT_ALARMパーミッションを使って明示的に許可を得ることで、アラーム実行の信頼性を高められます。

<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
if (alarmManager.canScheduleExactAlarms()) {
    // setExactAndAllowWhileIdle() を使ってよし
}

✅ Notificationを前日などにプリフェッチしておく設計へ変更

通知は「その瞬間に出す」のではなく、前日にWorkManagerでプリフェッチしておく方式に変更しました。

→ たとえアラームが鳴らなくても、アプリ起動時や翌朝に補完されるように。

教訓:AlarmManagerはもう「確実な選択肢」ではない

昔のAndroidでは「バックグラウンドでも鳴るアラーム」 = AlarmManagerでした。
しかし今は違います。

  • OSがDozeモードで止める
  • メーカーがバッテリー制御で止める
  • PendingIntentの仕様が罠だらけ

「アラームを鳴らす」こと自体が、現代のAndroidではとても難しい行為なのです。

まとめ:信頼できる設計は、AlarmManagerの“先”にある

AlarmManagerはまだ使えます。

ですが、「毎日鳴らす」「必ず鳴らす」といった要件には適していません

今後は以下を意識した設計が必要です:

  • OSバージョンや状態によらないバックグラウンド処理(WorkManagerなど)
  • アラームが鳴らなくても補完できる通知設計
  • canScheduleExactAlarms()を使って確実にアラームが設定できるかをチェック

APIを信じすぎるな。システムの都合は予測不能。

それが現代のAndroid開発なのです。

タイトルとURLをコピーしました