古いPendingIntentが残って大混乱した話〜フラグひとつで通知が全部おかしくなった〜

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

「通知をタップしたら、前に見た記事が開いたんですけど?」

「リマインダー押したら、全然違う画面に飛ばされました」

ある日、ユーザーから寄せられた謎の報告。

ログを確認してもクラッシュはなし。でも、遷移先が明らかにおかしい

調べてみると、原因は……古いPendingIntentが再利用されていたことでした。

今回は、通知やAlarmManagerを扱ううえで避けて通れないPendingIntentという仕組みと、
その意図しない“再利用”によって引き起こされた大混乱の顛末を語ります。

発端:通知タップ後に意図しない画面が開く

あるニュースアプリで、通知から記事詳細にジャンプする仕様を実装していました。

ユーザーが通知をタップすると、その記事の内容が表示される──それだけのシンプルなもの。

コードも非常に単純:

val intent = Intent(context, ArticleDetailActivity::class.java).apply {
    putExtra("articleId", article.id)
}

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

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle(article.title)
    .setContentText("記事をチェックしよう")
    .setSmallIcon(R.drawable.ic_notification)
    .setContentIntent(pendingIntent)
    .build()

しかしある日、あるユーザーからこんな報告が寄せられました。

「昨日見た記事が、今日の通知でも開かれるんだけど?」

最初は「誤解かな」と思いました。

でも複数のユーザーから報告が上がってきて、再現もできるようになってきました。

原因:Intentの中身は変わっても、同じPendingIntentが使われていた

実は、PendingIntent.getActivity()の第2引数であるrequestCodeが常に0になっており、
Intentの内容が変わっても、OSは同一のPendingIntentとして扱って再利用していたのです。

つまり、

  • articleId = 1001 の通知を最初に出したときのIntentがキャッシュされ、
  • 次に articleId = 1002 で通知を出しても、中身が上書きされないまま古いIntentが使われる

という事態が発生していました。

PendingIntentの仕様:Intentの“同一性”って?

Androidは、以下の4つの要素を使ってPendingIntentを識別しています:

  1. Intentアクション
  2. Intentデータ(URI)
  3. Intentカテゴリ
  4. Intentコンポーネント名(class)

つまり、putExtra()で渡しているようなExtrasは識別対象に含まれないのです。

そのため、Intentの見た目が違っていても、OSから見れば「同じもの」と判断され、前に生成されたPendingIntentが再利用されるというわけです。

これが地獄:AlarmManagerとの併用でさらなる混乱

この問題は通知だけにとどまりませんでした。

ユーザーの「毎日20時にリマインダーを送る」という機能をAlarmManagerで実装していたのですが、
ここでも同じようにPendingIntentが使われていました。

val intent = Intent(context, ReminderReceiver::class.java).apply {
    putExtra("type", "DAILY_REMINDER")
}

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

val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setExact(
    AlarmManager.RTC_WAKEUP,
    triggerTime,
    pendingIntent
)

これを複数のリマインダーで同じrequestCode = 0、同じIntent構成で出していたため、「最新のリマインダー設定が、すべての古いリマインダーを上書きしてしまう」という事態が発生。

  • 毎週のリマインダー
  • 1回限りの特別通知
  • アプリ起動後の遅延通知

これらがひとつのPendingIntentに集約され、同じ動作しかしなくなったのです。

暴れ回る古いIntentたち

アプリの利用が進むほど、これらの問題が積もり積もってユーザー体験を崩壊させていきました。

  • 過去の通知をタップ → 関係ない記事が開く
  • リマインダーが何度も届く or 一度も届かない
  • アプリ起動時に通知が出るはずなのに出ない

原因はすべて、古いPendingIntentが“いつまでも使い回される”設計にしていたことにありました。

解決策:PendingIntentに一意性を持たせよ

✅ 対策①:requestCodeに一意の値を使う

val pendingIntent = PendingIntent.getActivity(
    context,
    article.id.hashCode(), // 記事ごとに異なるrequestCode
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT
)

これにより、Intentの中身が違えば異なるPendingIntentとして扱われるようになります。

✅ 対策②:Intentの内容を変える(DataやAction)

val intent = Intent(context, ArticleDetailActivity::class.java).apply {
    action = "com.example.ACTION_VIEW_${article.id}"
    data = Uri.parse("myapp://article/${article.id}")
    putExtra("articleId", article.id)
}

Intentのdataactionを工夫することで、識別情報が変わり、OSが別のPendingIntentとして認識してくれます。

✅ 対策③:APIレベルに応じたフラグ指定を確認

val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
    PendingIntent.FLAG_UPDATE_CURRENT
}

Android 12以降ではFLAG_MUTABLE / FLAG_IMMUTABLEの指定が必須になりました。

これを忘れると、通知が出ない・タップできないなどの別の問題が起きるため、要注意です。

あとがき:フラグひとつで大混乱

この一連の事件を通して感じたのは、PendingIntentは“慣れ”で扱ってはいけないということ。

たった1つのrequestCodeの使い回し、たった1つのIntentの構成ミスが、数万人のユーザーに混乱をもたらす可能性があります。

表面上は動いているように見えても、内部では古いIntentが暴れ続けている。

そうした“見えない地雷”を踏まないためにも、PendingIntentの再利用性と識別ルールをきちんと理解することが重要だと痛感しました。

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