From 73301f7d1d76be3e1534dfe857eb29189915f438 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 28 Nov 2025 04:56:19 +0000 Subject: [PATCH] feat(android): add getSchedulesWithStatus() and alarm list UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ability to list alarms with AlarmManager status in web interface. Changes: - Add getSchedulesWithStatus() method to DailyNotificationPlugin - Add ScheduleWithStatus TypeScript interface with isActuallyScheduled flag - Add alarm list UI to android-test-app with status indicators Backend: - getSchedulesWithStatus() returns schedules with AlarmManager verification - Checks isAlarmScheduled() for each notify schedule with nextRunAt - Returns isActuallyScheduled boolean flag for each schedule TypeScript: - New ScheduleWithStatus interface extending Schedule - Method signature with JSDoc and usage examples UI: - New "📋 List Alarms" button in test app - Color-coded alarm cards (green=scheduled, orange=not scheduled) - Shows schedule ID, next run time, pattern, and AlarmManager status - Useful for debugging recovery scenarios and verifying alarm state Use case: - Verify which alarms are in database vs actually scheduled - Debug Phase 1/2/3 recovery scenarios - Visual confirmation of alarm state after app launch/boot Related: - Enhances: android-test-app for Phase 1-3 testing - Supports: Recovery verification and debugging --- .../DailyNotificationPlugin.kt | 51 +++++++++++ src/definitions.ts | 32 +++++++ .../app/src/main/assets/public/index.html | 86 +++++++++++++++++++ 3 files changed, 169 insertions(+) diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index 8a88543..849c84a 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -1864,6 +1864,57 @@ open class DailyNotificationPlugin : Plugin() { } } + /** + * Get all schedules with their AlarmManager status + * Returns schedules from database with isActuallyScheduled flag for each + */ + @PluginMethod + fun getSchedulesWithStatus(call: PluginCall) { + CoroutineScope(Dispatchers.IO).launch { + try { + val options = call.getObject("options") + val kind = options?.getString("kind") + val enabled = options?.getBoolean("enabled") + + val context = context ?: return@launch call.reject("Context not available") + + val schedules = when { + kind != null && enabled != null -> + getDatabase().scheduleDao().getByKindAndEnabled(kind, enabled) + kind != null -> + getDatabase().scheduleDao().getByKind(kind) + enabled != null -> + if (enabled) getDatabase().scheduleDao().getEnabled() else getDatabase().scheduleDao().getAll().filter { !it.enabled } + else -> + getDatabase().scheduleDao().getAll() + } + + // For each schedule, check if it's actually scheduled in AlarmManager + val schedulesArray = org.json.JSONArray() + schedules.forEach { schedule -> + val scheduleJson = scheduleToJson(schedule) + + // Only check AlarmManager status for "notify" schedules with nextRunAt + if (schedule.kind == "notify" && schedule.nextRunAt != null) { + val isScheduled = NotifyReceiver.isAlarmScheduled(context, schedule.nextRunAt!!) + scheduleJson.put("isActuallyScheduled", isScheduled) + } else { + scheduleJson.put("isActuallyScheduled", false) + } + + schedulesArray.put(scheduleJson) + } + + call.resolve(JSObject().apply { + put("schedules", schedulesArray) + }) + } catch (e: Exception) { + Log.e(TAG, "Failed to get schedules with status", e) + call.reject("Failed to get schedules with status: ${e.message}") + } + } + } + @PluginMethod fun createSchedule(call: PluginCall) { CoroutineScope(Dispatchers.IO).launch { diff --git a/src/definitions.ts b/src/definitions.ts index 4b16c71..87106ed 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -328,6 +328,15 @@ export interface Schedule { stateJson?: string; } +/** + * Schedule with AlarmManager status + * Extends Schedule with isActuallyScheduled flag indicating if alarm is registered in AlarmManager + */ +export interface ScheduleWithStatus extends Schedule { + /** Whether the alarm is actually scheduled in AlarmManager (Android only, for 'notify' schedules) */ + isActuallyScheduled: boolean; +} + /** * Input type for creating a new schedule */ @@ -697,6 +706,29 @@ export interface DailyNotificationPlugin { */ getSchedules(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: Schedule[] }>; + /** + * Get all schedules with their AlarmManager status + * Returns schedules from database with isActuallyScheduled flag for each + * + * @param options Optional filters: + * - kind: Filter by schedule type ('fetch' | 'notify') + * - enabled: Filter by enabled status (true = only enabled, false = only disabled, undefined = all) + * @returns Promise resolving to object with schedules array: { schedules: ScheduleWithStatus[] } + * + * @example + * ```typescript + * // Get all notification schedules with AlarmManager status + * const result = await DailyNotification.getSchedulesWithStatus({ + * kind: 'notify', + * enabled: true + * }); + * result.schedules.forEach(schedule => { + * console.log(`${schedule.id}: ${schedule.isActuallyScheduled ? 'Scheduled' : 'Not scheduled'}`); + * }); + * ``` + */ + getSchedulesWithStatus(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: ScheduleWithStatus[] }>; + /** * Get a single schedule by ID * diff --git a/test-apps/android-test-app/app/src/main/assets/public/index.html b/test-apps/android-test-app/app/src/main/assets/public/index.html index 5f60ee0..618650a 100644 --- a/test-apps/android-test-app/app/src/main/assets/public/index.html +++ b/test-apps/android-test-app/app/src/main/assets/public/index.html @@ -74,6 +74,14 @@ + + +
Ready to test... @@ -385,11 +393,89 @@ } } + function loadAlarmList() { + const status = document.getElementById('status'); + const alarmListContainer = document.getElementById('alarmListContainer'); + const alarmList = document.getElementById('alarmList'); + + status.innerHTML = 'Loading alarm list...'; + status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background + alarmListContainer.style.display = 'block'; + alarmList.innerHTML = 'Loading...'; + + try { + if (!window.DailyNotification) { + status.innerHTML = 'DailyNotification plugin not available'; + status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background + alarmList.innerHTML = '❌ Plugin unavailable'; + return; + } + + window.DailyNotification.getSchedulesWithStatus({ + kind: 'notify', + enabled: true + }) + .then(result => { + const schedules = result.schedules || []; + + if (schedules.length === 0) { + alarmList.innerHTML = 'No alarms scheduled'; + status.innerHTML = '✅ No alarms found'; + status.style.background = 'rgba(255, 255, 255, 0.1)'; + return; + } + + let html = '
'; + + schedules.forEach(schedule => { + const nextRun = schedule.nextRunAt ? new Date(schedule.nextRunAt) : null; + const nextRunStr = nextRun ? nextRun.toLocaleString() : 'Not scheduled'; + const statusIcon = schedule.isActuallyScheduled ? '✅' : '⚠️'; + const statusText = schedule.isActuallyScheduled ? 'Scheduled in AlarmManager' : 'Not in AlarmManager'; + const statusColor = schedule.isActuallyScheduled ? 'rgba(0, 255, 0, 0.2)' : 'rgba(255, 165, 0, 0.2)'; + + html += ` +
+
+ ${statusIcon} ${schedule.id} +
+
+ 📅 Next Run: ${nextRunStr} +
+
+ ${schedule.cron ? `Cron: ${schedule.cron}` : schedule.clockTime ? `Time: ${schedule.clockTime}` : 'No schedule pattern'} +
+
+ Status: ${statusText} +
+
+ `; + }); + + html += '
'; + alarmList.innerHTML = html; + + status.innerHTML = `✅ Found ${schedules.length} alarm(s)`; + status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background + }) + .catch(error => { + alarmList.innerHTML = `❌ Error: ${error.message}`; + status.innerHTML = `Failed to load alarms: ${error.message}`; + status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background + }); + } catch (error) { + alarmList.innerHTML = `❌ Error: ${error.message}`; + status.innerHTML = `Failed to load alarms: ${error.message}`; + status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background + } + } + // Attach to window object window.configurePlugin = configurePlugin; window.testNotification = testNotification; window.requestPermissions = requestPermissions; window.checkComprehensiveStatus = checkComprehensiveStatus; + window.loadAlarmList = loadAlarmList; function loadPermissionStatus() { const notificationPermStatus = document.getElementById('notificationPermStatus');