fix(android): show dialog for overdue reminders instead of skipping (#6068)

- Fix issue where overdue reminders were invisible on Android
- Worker correctly detected overdue reminders but dialog was skipped
- Native AlarmManager only schedules future reminders, not overdue ones
- Now checks if reminders are overdue and shows dialog appropriately
- Future reminders: skip dialog (native notification handles them)
- Overdue reminders: show dialog (no native notification exists)
- Add comprehensive diagnostic logging to reminder scheduling/cancellation
- Add logging to track reminder dialog trigger decisions

This fixes the issue where changing any task's reminder time would
appear to trigger all overdue reminders - they were always there but
hidden due to the dialog being skipped on Android.
This commit is contained in:
Johannes Millan 2026-01-20 13:43:33 +01:00
parent 4de1155280
commit f784c9c0b9
2 changed files with 76 additions and 6 deletions

View file

@ -90,11 +90,26 @@ export class CapacitorReminderService {
const now = Date.now();
const triggerAt = options.triggerAtMs <= now ? now + 1000 : options.triggerAtMs;
Log.log('📱 CapacitorReminderService.scheduleReminder called', {
notificationId: options.notificationId,
title: options.title.substring(0, 30),
triggerAt: new Date(triggerAt).toISOString(),
triggerInMs: triggerAt - now,
triggerInMinutes: Math.round((triggerAt - now) / 1000 / 60),
isAndroidWebView: IS_ANDROID_WEB_VIEW,
hasNativeScheduler: !!androidInterface.scheduleNativeReminder,
});
// On Android, use native AlarmManager for precision
if (IS_ANDROID_WEB_VIEW && androidInterface.scheduleNativeReminder) {
try {
const useAlarmStyle =
this._globalConfigService.cfg()?.reminder?.useAlarmStyleReminders ?? false;
Log.log('🔔 Calling androidInterface.scheduleNativeReminder', {
notificationId: options.notificationId,
useAlarmStyle,
});
androidInterface.scheduleNativeReminder(
options.notificationId,
options.reminderId,
@ -104,14 +119,18 @@ export class CapacitorReminderService {
triggerAt,
useAlarmStyle,
);
Log.log('CapacitorReminderService: Android reminder scheduled', {
Log.log('✅ CapacitorReminderService: Android reminder scheduled successfully', {
notificationId: options.notificationId,
title: options.title,
triggerAt: new Date(triggerAt).toISOString(),
});
return true;
} catch (error) {
Log.err('CapacitorReminderService: Failed to schedule Android reminder', error);
Log.err(
'❌ CapacitorReminderService: Failed to schedule Android reminder',
error,
);
return false;
}
}
@ -174,16 +193,22 @@ export class CapacitorReminderService {
return false;
}
Log.log('🚫 CapacitorReminderService.cancelReminder called', {
notificationId,
isAndroidWebView: IS_ANDROID_WEB_VIEW,
hasNativeCanceller: !!androidInterface.cancelNativeReminder,
});
// On Android, use native cancellation
if (IS_ANDROID_WEB_VIEW && androidInterface.cancelNativeReminder) {
try {
androidInterface.cancelNativeReminder(notificationId);
Log.log('CapacitorReminderService: Android reminder cancelled', {
Log.log('CapacitorReminderService: Android reminder cancelled', {
notificationId,
});
return true;
} catch (error) {
Log.err('CapacitorReminderService: Failed to cancel Android reminder', error);
Log.err('CapacitorReminderService: Failed to cancel Android reminder', error);
return false;
}
}

View file

@ -99,13 +99,41 @@ export class ReminderModule {
),
)
.subscribe((reminders: TaskWithReminderData[]) => {
const now = Date.now();
const overdueReminders = reminders.filter(
(r) => r.reminderData?.remindAt && r.reminderData.remindAt < now,
);
const futureReminders = reminders.filter(
(r) => r.reminderData?.remindAt && r.reminderData.remindAt >= now,
);
Log.log('=== REMINDER DIALOG TRIGGER ===', {
platform: IS_ANDROID_NATIVE ? 'Android' : IS_ELECTRON ? 'Electron' : 'Web',
reminderCount: reminders.length,
overdueCount: overdueReminders.length,
futureCount: futureReminders.length,
reminders: reminders.map((r) => ({
id: r.id.substring(0, 8),
title: r.title.substring(0, 30),
remindAt: r.reminderData?.remindAt
? new Date(r.reminderData.remindAt).toISOString()
: 'unknown',
isOverdue: r.reminderData?.remindAt ? r.reminderData.remindAt < now : false,
})),
willShowNotification: !IS_NATIVE_PLATFORM,
willShowDialog: !IS_ANDROID_NATIVE || overdueReminders.length > 0,
});
if (IS_ELECTRON && this._globalConfigService.cfg()?.reminder?.isFocusWindow) {
this._uiHelperService.focusApp();
}
this._showNotification(reminders);
// Skip dialog on Android - native notifications handle reminders
// On Android:
// - Future reminders: Native AlarmManager handles them (skip dialog)
// - Overdue reminders: No native notification exists (show dialog)
// This is because android.effects.ts only schedules future reminders.
// TODO: Native Android reminder notification actions (snooze/done buttons) currently
// work entirely in the background without notifying TypeScript. This means:
// 1. Snooze: Works correctly (native code reschedules the alarm)
@ -113,10 +141,27 @@ export class ReminderModule {
// app state. The reminder will be cancelled when the task is marked done in the app.
// To fully fix: Add onReminderDone$ subject to android-interface.ts and wire it up
// in the Kotlin ReminderBroadcastReceiver to call dismissReminderOnly action.
if (IS_ANDROID_NATIVE) {
if (
IS_ANDROID_NATIVE &&
futureReminders.length > 0 &&
overdueReminders.length === 0
) {
Log.log(
'⏭️ SKIPPING dialog on Android - all reminders are future, native notifications will handle them',
);
return;
}
if (IS_ANDROID_NATIVE && overdueReminders.length > 0) {
Log.log(
'📱 SHOWING dialog on Android for overdue reminders (no native notification exists)',
{
overdueCount: overdueReminders.length,
futureCount: futureReminders.length,
},
);
}
const oldest = reminders[0];
const taskId = oldest.id;