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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -74,6 +74,14 @@
|
||||
<button class="button" onclick="requestPermissions()">Request Permissions</button>
|
||||
<button class="button" onclick="testNotification()">Test Notification</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">
|
||||
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
|
||||
window.configurePlugin = configurePlugin;
|
||||
window.testNotification = testNotification;
|
||||
window.requestPermissions = requestPermissions;
|
||||
window.checkComprehensiveStatus = checkComprehensiveStatus;
|
||||
window.loadAlarmList = loadAlarmList;
|
||||
|
||||
function loadPermissionStatus() {
|
||||
const notificationPermStatus = document.getElementById('notificationPermStatus');
|
||||
|
||||
Reference in New Issue
Block a user