feat(android): add getSchedulesWithStatus() and alarm list UI

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
This commit is contained in:
Matthew Raymer
2025-11-28 04:56:19 +00:00
parent 945956dc5a
commit 73301f7d1d
3 changed files with 169 additions and 0 deletions

View File

@@ -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 @PluginMethod
fun createSchedule(call: PluginCall) { fun createSchedule(call: PluginCall) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {

View File

@@ -328,6 +328,15 @@ export interface Schedule {
stateJson?: string; 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 * Input type for creating a new schedule
*/ */
@@ -697,6 +706,29 @@ export interface DailyNotificationPlugin {
*/ */
getSchedules(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: Schedule[] }>; 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 * Get a single schedule by ID
* *

View File

@@ -74,6 +74,14 @@
<button class="button" onclick="requestPermissions()">Request Permissions</button> <button class="button" onclick="requestPermissions()">Request Permissions</button>
<button class="button" onclick="testNotification()">Test Notification</button> <button class="button" onclick="testNotification()">Test Notification</button>
<button class="button" onclick="checkComprehensiveStatus()">Full System Status</button> <button class="button" onclick="checkComprehensiveStatus()">Full System Status</button>
<button class="button" onclick="loadAlarmList()">📋 List Alarms</button>
<div id="alarmListContainer" class="status" style="margin-top: 20px; display: none;">
<strong>📋 Scheduled Alarms</strong>
<div id="alarmList" style="margin-top: 10px; text-align: left;">
Loading...
</div>
</div>
<div id="status" class="status"> <div id="status" class="status">
Ready to test... 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 = '<em>No alarms scheduled</em>';
status.innerHTML = '✅ No alarms found';
status.style.background = 'rgba(255, 255, 255, 0.1)';
return;
}
let html = '<div style="display: flex; flex-direction: column; gap: 10px;">';
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 += `
<div style="padding: 12px; background: ${statusColor}; border-radius: 8px; border-left: 4px solid ${schedule.isActuallyScheduled ? '#0f0' : '#ffa500'};">
<div style="font-weight: bold; margin-bottom: 6px;">
${statusIcon} ${schedule.id}
</div>
<div style="font-size: 0.9em; margin-bottom: 4px;">
📅 Next Run: ${nextRunStr}
</div>
<div style="font-size: 0.85em; color: rgba(255, 255, 255, 0.8);">
${schedule.cron ? `Cron: ${schedule.cron}` : schedule.clockTime ? `Time: ${schedule.clockTime}` : 'No schedule pattern'}
</div>
<div style="font-size: 0.85em; margin-top: 4px; color: rgba(255, 255, 255, 0.9);">
Status: ${statusText}
</div>
</div>
`;
});
html += '</div>';
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 // Attach to window object
window.configurePlugin = configurePlugin; window.configurePlugin = configurePlugin;
window.testNotification = testNotification; window.testNotification = testNotification;
window.requestPermissions = requestPermissions; window.requestPermissions = requestPermissions;
window.checkComprehensiveStatus = checkComprehensiveStatus; window.checkComprehensiveStatus = checkComprehensiveStatus;
window.loadAlarmList = loadAlarmList;
function loadPermissionStatus() { function loadPermissionStatus() {
const notificationPermStatus = document.getElementById('notificationPermStatus'); const notificationPermStatus = document.getElementById('notificationPermStatus');