※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開発なのです。