fix(android): explicitly set component and package for AlarmManager broadcasts

AlarmManager was firing alarms but DailyNotificationReceiver was not
receiving broadcasts. The issue was that Intents created with
Intent(context, Class) constructor were not reliably matched by
AlarmManager when delivering broadcasts.

Solution: Explicitly set ComponentName and package on all Intents used
for AlarmManager broadcasts. This ensures AlarmManager can correctly
match PendingIntents to the registered receiver.

Changes:
- NotifyReceiver.kt: Fixed Intent creation in scheduleNotification(),
  cancelNotification(), isAlarmScheduled(), and idempotence checks
- ReactivationManager.kt: Fixed alarmsExist() to use
  DailyNotificationReceiver with explicit component/package
- DailyNotificationScheduler.java: Fixed Intent creation to explicitly
  set component and package

This fixes the critical bug where alarms fire but receivers are not
triggered, resolving the gap between AlarmManager delivery and receiver
execution.
This commit is contained in:
Matthew
2026-02-03 00:33:33 -08:00
parent dd55c6b4e1
commit 367325452a
3 changed files with 62 additions and 20 deletions

View File

@@ -12,6 +12,7 @@ package com.timesafari.dailynotification;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
@@ -154,9 +155,15 @@ public class DailyNotificationScheduler {
cancelNotification(duplicateId); cancelNotification(duplicateId);
} }
// Create intent for the notification // CRITICAL FIX: Explicitly set component and package for AlarmManager broadcasts
Intent intent = new Intent(context, DailyNotificationReceiver.class); // AlarmManager requires explicit component matching when delivering broadcasts
intent.setAction(com.timesafari.dailynotification.DailyNotificationConstants.ACTION_NOTIFICATION); ComponentName receiverComponent = new ComponentName(
context.getPackageName(),
"com.timesafari.dailynotification.DailyNotificationReceiver"
);
Intent intent = new Intent(com.timesafari.dailynotification.DailyNotificationConstants.ACTION_NOTIFICATION);
intent.setComponent(receiverComponent);
intent.setPackage(context.getPackageName());
intent.putExtra(com.timesafari.dailynotification.DailyNotificationConstants.EXTRA_NOTIFICATION_ID, content.getId()); intent.putExtra(com.timesafari.dailynotification.DailyNotificationConstants.EXTRA_NOTIFICATION_ID, content.getId());
// Check if this is a static reminder // Check if this is a static reminder

View File

@@ -6,6 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@@ -146,8 +147,15 @@ class NotifyReceiver : BroadcastReceiver() {
// This prevents duplicate alarms when multiple scheduling paths race // This prevents duplicate alarms when multiple scheduling paths race
// Strategy: Check both by scheduleId (stable) and by trigger time (catches different scheduleIds for same time) // Strategy: Check both by scheduleId (stable) and by trigger time (catches different scheduleIds for same time)
val requestCode = getRequestCode(stableScheduleId) val requestCode = getRequestCode(stableScheduleId)
val checkIntent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { // CRITICAL FIX: Explicitly set component and package for AlarmManager broadcasts
action = "com.timesafari.daily.NOTIFICATION" // AlarmManager requires explicit component matching when delivering broadcasts
val receiverComponent = ComponentName(
context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val checkIntent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
// Check 1: Same scheduleId (stable requestCode) - most reliable // Check 1: Same scheduleId (stable requestCode) - most reliable
@@ -269,12 +277,21 @@ class NotifyReceiver : BroadcastReceiver() {
Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e) Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e)
} }
// FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver // CRITICAL FIX: Explicitly set component and package for AlarmManager broadcasts
// FIX: Set action to match manifest registration // AlarmManager requires explicit component matching when delivering broadcasts.
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { // Using Intent(context, Class) constructor may not work reliably with AlarmManager
action = "com.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action // on all Android versions, especially when the app is in certain states.
putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra // Solution: Create Intent with action, then explicitly set component and package.
putExtra("schedule_id", stableScheduleId) // Add stable scheduleId for tracking val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
// Explicitly set component to ensure AlarmManager can match it to the receiver
setComponent(receiverComponent)
// Explicitly set package to ensure it matches the app's package (not plugin's)
setPackage(context.packageName)
// Must match manifest intent-filter action
// DailyNotificationReceiver expects this extra
putExtra("notification_id", notificationId)
// Add stable scheduleId for tracking
putExtra("schedule_id", stableScheduleId)
// Also preserve original extras for backward compatibility if needed // Also preserve original extras for backward compatibility if needed
putExtra("title", config.title) putExtra("title", config.title)
putExtra("body", config.body) putExtra("body", config.body)
@@ -282,7 +299,8 @@ class NotifyReceiver : BroadcastReceiver() {
putExtra("vibration", config.vibration ?: true) putExtra("vibration", config.vibration ?: true)
putExtra("priority", config.priority ?: "normal") putExtra("priority", config.priority ?: "normal")
putExtra("is_static_reminder", isStaticReminder) putExtra("is_static_reminder", isStaticReminder)
putExtra("trigger_time", triggerAtMillis) // Store trigger time for debugging // Store trigger time for debugging
putExtra("trigger_time", triggerAtMillis)
if (reminderId != null) { if (reminderId != null) {
putExtra("reminder_id", reminderId) putExtra("reminder_id", reminderId)
} }
@@ -407,9 +425,14 @@ class NotifyReceiver : BroadcastReceiver() {
*/ */
fun cancelNotification(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null) { fun cancelNotification(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
// FIX: Use DailyNotificationReceiver to match what was scheduled // CRITICAL FIX: Use same Intent format as scheduling (explicit component and package)
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val receiverComponent = ComponentName(
action = "com.timesafari.daily.NOTIFICATION" context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
val requestCode = when { val requestCode = when {
scheduleId != null -> getRequestCode(scheduleId) scheduleId != null -> getRequestCode(scheduleId)
@@ -438,9 +461,14 @@ class NotifyReceiver : BroadcastReceiver() {
* @return true if alarm is scheduled, false otherwise * @return true if alarm is scheduled, false otherwise
*/ */
fun isAlarmScheduled(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null): Boolean { fun isAlarmScheduled(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null): Boolean {
// FIX: Use DailyNotificationReceiver to match what was scheduled // CRITICAL FIX: Use same Intent format as scheduling (explicit component and package)
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val receiverComponent = ComponentName(
action = "com.timesafari.daily.NOTIFICATION" context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
val requestCode = when { val requestCode = when {
scheduleId != null -> getRequestCode(scheduleId) scheduleId != null -> getRequestCode(scheduleId)

View File

@@ -1,6 +1,7 @@
package com.timesafari.dailynotification package com.timesafari.dailynotification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -442,8 +443,14 @@ class ReactivationManager(private val context: Context) {
return try { return try {
// Check if any PendingIntent for our receiver exists // Check if any PendingIntent for our receiver exists
// This is more reliable than nextAlarmClock // This is more reliable than nextAlarmClock
val intent = Intent(context, NotifyReceiver::class.java).apply { // CRITICAL FIX: Use DailyNotificationReceiver with explicit component/package
action = "com.timesafari.daily.NOTIFICATION" val receiverComponent = ComponentName(
context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(
context, context,