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
|
@PluginMethod
|
||||||
fun createSchedule(call: PluginCall) {
|
fun createSchedule(call: PluginCall) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user