Compare commits
16 Commits
android-fi
...
839e167c98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
839e167c98 | ||
|
|
f40562b68a | ||
|
|
f1830e5f6f | ||
|
|
f38b06abed | ||
|
|
ea4bc88808 | ||
|
|
63e5b4535e | ||
|
|
d913f03e23 | ||
|
|
4c1281754e | ||
|
|
9655fa10f8 | ||
|
|
6ac7b35566 | ||
|
|
62559cd546 | ||
|
|
7b1f1200bc | ||
|
|
39eed856f5 | ||
|
|
f83e799254 | ||
|
|
7725f19387 | ||
| 76b3fa8199 |
@@ -38,6 +38,15 @@ The plugin has been optimized for **native-first deployment** with the following
|
|||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
|
|
||||||
|
### **Overview**
|
||||||
|
|
||||||
|
Dec 17
|
||||||
|
- test-apps
|
||||||
|
- android has been seen to work
|
||||||
|
- ios is being developed (Jose)
|
||||||
|
- after ios, will work on daily-notification-test (that includes Vue)
|
||||||
|
- need to test with real data in the API
|
||||||
|
|
||||||
### ✅ **Phase 2 Complete - Production Ready**
|
### ✅ **Phase 2 Complete - Production Ready**
|
||||||
|
|
||||||
| Component | Status | Implementation |
|
| Component | Status | Implementation |
|
||||||
|
|||||||
@@ -2072,6 +2072,8 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
val options = call.getObject("options")
|
val options = call.getObject("options")
|
||||||
val timesafariDid = options?.getString("timesafariDid")
|
val timesafariDid = options?.getString("timesafariDid")
|
||||||
|
|
||||||
|
Log.d(TAG, "DNP-CONFIG: Loading config from database: key=$key, timesafariDid=${timesafariDid?.take(20)}...")
|
||||||
|
|
||||||
val entity = if (timesafariDid != null) {
|
val entity = if (timesafariDid != null) {
|
||||||
getDatabase().notificationConfigDao().getConfigByKeyAndDid(key, timesafariDid)
|
getDatabase().notificationConfigDao().getConfigByKeyAndDid(key, timesafariDid)
|
||||||
} else {
|
} else {
|
||||||
@@ -2079,8 +2081,10 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entity != null) {
|
if (entity != null) {
|
||||||
|
Log.i(TAG, "DNP-CONFIG: Configuration restored from database: key=$key, configType=${entity.configType}, hasValue=${entity.configValue.isNotEmpty()}")
|
||||||
call.resolve(configToJson(entity))
|
call.resolve(configToJson(entity))
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "DNP-CONFIG: Configuration not found in database: key=$key")
|
||||||
call.resolve(JSObject().apply { put("config", null) })
|
call.resolve(JSObject().apply { put("config", null) })
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -2631,6 +2635,12 @@ object ScheduleHelper {
|
|||||||
return try {
|
return try {
|
||||||
val nextRunTime = calculateNextRunTime(config.schedule)
|
val nextRunTime = calculateNextRunTime(config.schedule)
|
||||||
|
|
||||||
|
// CRITICAL: Cancel any existing alarm for this scheduleId BEFORE scheduling new one
|
||||||
|
// This ensures "one per day" semantics - when updating schedule time, old alarm is canceled
|
||||||
|
// The cleanupExistingNotificationSchedules() above only cancels OTHER schedules, not the current one
|
||||||
|
NotifyReceiver.cancelNotification(context, scheduleId)
|
||||||
|
Log.i("ScheduleHelper", "Cancelled existing alarm for scheduleId=$scheduleId before scheduling new one at $nextRunTime")
|
||||||
|
|
||||||
// Schedule AlarmManager notification as static reminder
|
// Schedule AlarmManager notification as static reminder
|
||||||
// (doesn't require cached content)
|
// (doesn't require cached content)
|
||||||
NotifyReceiver.scheduleExactNotification(
|
NotifyReceiver.scheduleExactNotification(
|
||||||
|
|||||||
@@ -396,6 +396,61 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
)
|
)
|
||||||
Log.i(TAG, "Inexact alarm scheduled (fallback): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
Log.i(TAG, "Inexact alarm scheduled (fallback): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update database schedule with new nextRunAt so getNotificationStatus() returns correct value
|
||||||
|
// This is critical for rollover scenarios where the UI needs to show the updated time
|
||||||
|
// Strategy: Find existing enabled notify schedule and update it (there should only be one)
|
||||||
|
// This ensures getNotificationStatus() finds the updated schedule, not a stale one
|
||||||
|
try {
|
||||||
|
runBlocking {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
|
||||||
|
// First, try to find schedule by the provided stableScheduleId
|
||||||
|
var scheduleToUpdate = db.scheduleDao().getById(stableScheduleId)
|
||||||
|
|
||||||
|
// If not found by ID, find the existing enabled notify schedule (for rollover scenarios)
|
||||||
|
// getNotificationStatus() finds schedules with kind="notify" && enabled=true
|
||||||
|
if (scheduleToUpdate == null) {
|
||||||
|
val allSchedules = db.scheduleDao().getAll()
|
||||||
|
scheduleToUpdate = allSchedules.firstOrNull { it.kind == "notify" && it.enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate cron expression from trigger time (HH:mm format)
|
||||||
|
val calendar = java.util.Calendar.getInstance().apply {
|
||||||
|
timeInMillis = triggerAtMillis
|
||||||
|
}
|
||||||
|
val hour = calendar.get(java.util.Calendar.HOUR_OF_DAY)
|
||||||
|
val minute = calendar.get(java.util.Calendar.MINUTE)
|
||||||
|
val cronExpression = "${minute} ${hour} * * *"
|
||||||
|
val clockTime = String.format("%02d:%02d", hour, minute)
|
||||||
|
|
||||||
|
if (scheduleToUpdate != null) {
|
||||||
|
// Update existing schedule with new nextRunAt
|
||||||
|
// Use the existing schedule's ID (not stableScheduleId) to ensure we update the right one
|
||||||
|
db.scheduleDao().updateRunTimes(scheduleToUpdate.id, scheduleToUpdate.lastRunAt, triggerAtMillis)
|
||||||
|
Log.d(SCHEDULE_TAG, "Updated schedule in database: id=${scheduleToUpdate.id}, nextRunAt=$triggerAtMillis (rollover)")
|
||||||
|
} else {
|
||||||
|
// No existing schedule found - create new one (shouldn't happen in normal flow)
|
||||||
|
val newSchedule = Schedule(
|
||||||
|
id = stableScheduleId,
|
||||||
|
kind = "notify",
|
||||||
|
cron = cronExpression,
|
||||||
|
clockTime = clockTime,
|
||||||
|
enabled = true,
|
||||||
|
lastRunAt = null,
|
||||||
|
nextRunAt = triggerAtMillis,
|
||||||
|
jitterMs = 0,
|
||||||
|
backoffPolicy = "exp",
|
||||||
|
stateJson = null
|
||||||
|
)
|
||||||
|
db.scheduleDao().upsert(newSchedule)
|
||||||
|
Log.d(SCHEDULE_TAG, "Created new schedule in database: id=$stableScheduleId, nextRunAt=$triggerAtMillis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Log but don't fail - alarm is already scheduled, DB update is best-effort
|
||||||
|
Log.w(SCHEDULE_TAG, "Failed to update schedule in database: $stableScheduleId (alarm still scheduled)", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -419,14 +474,38 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
|
||||||
|
// CRITICAL: Use FLAG_NO_CREATE to get existing PendingIntent, don't create new one
|
||||||
|
// This matches the pattern used in scheduleExactNotification for proper cancellation
|
||||||
|
val existingPendingIntent = PendingIntent.getBroadcast(
|
||||||
context,
|
context,
|
||||||
requestCode,
|
requestCode,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
alarmManager.cancel(pendingIntent)
|
|
||||||
Log.i(TAG, "Notification alarm cancelled: scheduleId=$scheduleId, triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
if (existingPendingIntent != null) {
|
||||||
|
// Cancel both the alarm in AlarmManager AND the PendingIntent itself
|
||||||
|
// This matches the pattern in scheduleExactNotification (lines 311-312)
|
||||||
|
alarmManager.cancel(existingPendingIntent)
|
||||||
|
existingPendingIntent.cancel()
|
||||||
|
Log.i(TAG, "DNP-CANCEL: Notification alarm cancelled: scheduleId=$scheduleId, triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
||||||
|
|
||||||
|
// Verify cancellation by checking if alarm still exists
|
||||||
|
val verifyIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
requestCode,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
if (verifyIntent == null) {
|
||||||
|
Log.d(TAG, "DNP-CANCEL: ✅ Cancellation verified - no PendingIntent found for requestCode=$requestCode")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "DNP-CANCEL: ⚠️ Cancellation may have failed - PendingIntent still exists for requestCode=$requestCode")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "DNP-CANCEL: No existing PendingIntent found to cancel: scheduleId=$scheduleId, requestCode=$requestCode")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Both test apps are configured to **automatically build the plugin** as part of their build process. The plugin is included as a Gradle project dependency, so Gradle handles building it automatically.
|
Both test apps are configured to **automatically build the plugin** as part of their build process. The plugin is included as a Gradle project dependency, so Gradle handles building it automatically.
|
||||||
|
|
||||||
|
Note that Test App 1 `android-test-app` is used most frequently.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test App 1: `android-test-app` (Standalone Android)
|
## Test App 1: `android-test-app` (Standalone Android)
|
||||||
@@ -59,7 +61,7 @@ avdmanager list avd
|
|||||||
# Run one
|
# Run one
|
||||||
emulator -avd AVD_NAME
|
emulator -avd AVD_NAME
|
||||||
|
|
||||||
# Check that one is running
|
# Simply see that one is running
|
||||||
adb devices
|
adb devices
|
||||||
|
|
||||||
# Now install on the emulator
|
# Now install on the emulator
|
||||||
|
|||||||
@@ -196,22 +196,27 @@
|
|||||||
console.log('[UI Refresh] Calling getNotificationStatus()...');
|
console.log('[UI Refresh] Calling getNotificationStatus()...');
|
||||||
window.DailyNotification.getNotificationStatus()
|
window.DailyNotification.getNotificationStatus()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log('[UI Refresh] getNotificationStatus result:', {
|
console.log('[UI Refresh] getNotificationStatus result:', JSON.stringify({
|
||||||
nextNotificationTime: result.nextNotificationTime,
|
nextNotificationTime: result.nextNotificationTime,
|
||||||
|
nextNotificationTimeDate: result.nextNotificationTime ? new Date(result.nextNotificationTime).toISOString() : null,
|
||||||
isEnabled: result.isEnabled,
|
isEnabled: result.isEnabled,
|
||||||
pending: result.pending,
|
pending: result.pending,
|
||||||
lastNotificationTime: result.lastNotificationTime
|
lastNotificationTime: result.lastNotificationTime,
|
||||||
});
|
lastNotificationTimeDate: result.lastNotificationTime ? new Date(result.lastNotificationTime).toISOString() : null
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
const nextTime = formatDateTimeNormalized(result.nextNotificationTime);
|
const nextTime = formatDateTimeNormalized(result.nextNotificationTime);
|
||||||
const hasSchedules = result.isEnabled || (result.pending && result.pending > 0);
|
const hasSchedules = result.isEnabled || (result.pending && result.pending > 0);
|
||||||
const statusIcon = hasSchedules ? '✅' : '⏸️';
|
const statusIcon = hasSchedules ? '✅' : '⏸️';
|
||||||
|
|
||||||
console.log('[UI Refresh] Updating UI:', {
|
console.log('[UI Refresh] Updating UI:', JSON.stringify({
|
||||||
nextTime: nextTime,
|
nextTime: nextTime,
|
||||||
|
nextNotificationTimeRaw: result.nextNotificationTime,
|
||||||
|
nextNotificationTimeDate: result.nextNotificationTime ? new Date(result.nextNotificationTime).toISOString() : null,
|
||||||
hasSchedules: hasSchedules,
|
hasSchedules: hasSchedules,
|
||||||
statusIcon: statusIcon
|
statusIcon: statusIcon,
|
||||||
});
|
pending: result.pending
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
pluginStatusContent.innerHTML = `${statusIcon} Active Schedules: ${hasSchedules ? 'Yes' : 'No'}<br>
|
pluginStatusContent.innerHTML = `${statusIcon} Active Schedules: ${hasSchedules ? 'Yes' : 'No'}<br>
|
||||||
📅 Next Notification: ${nextTime}<br>
|
📅 Next Notification: ${nextTime}<br>
|
||||||
@@ -341,6 +346,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load configuration status (plugin settings and native fetcher)
|
||||||
|
function loadConfigurationStatus() {
|
||||||
|
console.log('[Config Check] Checking configuration status...');
|
||||||
|
const configStatus = document.getElementById('configStatus');
|
||||||
|
const fetcherStatus = document.getElementById('fetcherStatus');
|
||||||
|
|
||||||
|
if (!window.DailyNotification) {
|
||||||
|
console.warn('[Config Check] DailyNotification plugin not available');
|
||||||
|
configStatus.innerHTML = '❌ Plugin unavailable';
|
||||||
|
fetcherStatus.innerHTML = '❌ Plugin unavailable';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if plugin settings are configured
|
||||||
|
// Plugin settings are stored internally, so we check by trying to get status
|
||||||
|
// For now, we'll check if native fetcher config exists as a proxy
|
||||||
|
// TODO: Add explicit plugin settings check method
|
||||||
|
window.DailyNotification.getConfig({ key: 'native_fetcher_config' })
|
||||||
|
.then(result => {
|
||||||
|
console.log('[Config Check] Native fetcher config result:', JSON.stringify(result));
|
||||||
|
if (result && result.config && result.config.configValue) {
|
||||||
|
try {
|
||||||
|
const configValue = JSON.parse(result.config.configValue);
|
||||||
|
if (configValue.apiBaseUrl && configValue.apiBaseUrl.length > 0) {
|
||||||
|
console.log('[Config Check] ✅ Native fetcher is configured');
|
||||||
|
fetcherStatus.innerHTML = '✅ Configured';
|
||||||
|
} else {
|
||||||
|
console.log('[Config Check] ⚠️ Native fetcher config exists but is empty');
|
||||||
|
fetcherStatus.innerHTML = '❌ Not configured';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Config Check] Failed to parse config value:', e);
|
||||||
|
fetcherStatus.innerHTML = '❌ Error';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[Config Check] ❌ Native fetcher config not found');
|
||||||
|
fetcherStatus.innerHTML = '❌ Not configured';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For plugin settings, we assume configured if native fetcher is configured
|
||||||
|
// This is a heuristic - in production, add explicit check
|
||||||
|
if (fetcherStatus.innerHTML.includes('✅')) {
|
||||||
|
configStatus.innerHTML = '✅ Configured';
|
||||||
|
} else {
|
||||||
|
configStatus.innerHTML = '❌ Not configured';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[Config Check] Failed to check configuration:', error);
|
||||||
|
configStatus.innerHTML = '❌ Error';
|
||||||
|
fetcherStatus.innerHTML = '❌ Error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadChannelStatus() {
|
function loadChannelStatus() {
|
||||||
const channelStatus = document.getElementById('channelStatus');
|
const channelStatus = document.getElementById('channelStatus');
|
||||||
|
|
||||||
@@ -472,11 +531,14 @@
|
|||||||
console.log('[Poll] checkNotificationDelivery called');
|
console.log('[Poll] checkNotificationDelivery called');
|
||||||
window.DailyNotification.getNotificationStatus()
|
window.DailyNotification.getNotificationStatus()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log('[Poll] Status check result:', {
|
console.log('[Poll] Status check result:', JSON.stringify({
|
||||||
nextNotificationTime: result.nextNotificationTime,
|
nextNotificationTime: result.nextNotificationTime,
|
||||||
|
nextNotificationTimeDate: result.nextNotificationTime ? new Date(result.nextNotificationTime).toISOString() : null,
|
||||||
lastNotificationTime: result.lastNotificationTime,
|
lastNotificationTime: result.lastNotificationTime,
|
||||||
lastKnownNextNotificationTime: lastKnownNextNotificationTime
|
lastNotificationTimeDate: result.lastNotificationTime ? new Date(result.lastNotificationTime).toISOString() : null,
|
||||||
});
|
lastKnownNextNotificationTime: lastKnownNextNotificationTime,
|
||||||
|
lastKnownNextNotificationTimeDate: lastKnownNextNotificationTime ? new Date(lastKnownNextNotificationTime).toISOString() : null
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
// Check for notification delivery
|
// Check for notification delivery
|
||||||
if (result.lastNotificationTime) {
|
if (result.lastNotificationTime) {
|
||||||
@@ -520,11 +582,13 @@
|
|||||||
|
|
||||||
// Detect if nextNotificationTime changed (rollover occurred)
|
// Detect if nextNotificationTime changed (rollover occurred)
|
||||||
const currentNextTime = result.nextNotificationTime;
|
const currentNextTime = result.nextNotificationTime;
|
||||||
console.log('[Poll] Comparing nextNotificationTime:', {
|
console.log('[Poll] Comparing nextNotificationTime:', JSON.stringify({
|
||||||
current: currentNextTime,
|
current: currentNextTime,
|
||||||
|
currentDate: currentNextTime ? new Date(currentNextTime).toISOString() : null,
|
||||||
lastKnown: lastKnownNextNotificationTime,
|
lastKnown: lastKnownNextNotificationTime,
|
||||||
|
lastKnownDate: lastKnownNextNotificationTime ? new Date(lastKnownNextNotificationTime).toISOString() : null,
|
||||||
changed: currentNextTime && currentNextTime !== lastKnownNextNotificationTime
|
changed: currentNextTime && currentNextTime !== lastKnownNextNotificationTime
|
||||||
});
|
}, null, 2));
|
||||||
|
|
||||||
if (currentNextTime && currentNextTime !== lastKnownNextNotificationTime) {
|
if (currentNextTime && currentNextTime !== lastKnownNextNotificationTime) {
|
||||||
if (lastKnownNextNotificationTime !== null) {
|
if (lastKnownNextNotificationTime !== null) {
|
||||||
@@ -558,11 +622,12 @@
|
|||||||
// Load plugin status automatically on page load
|
// Load plugin status automatically on page load
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
console.log('Page loaded, loading plugin status...');
|
console.log('Page loaded, loading plugin status...');
|
||||||
// Small delay to ensure Capacitor is ready
|
// Small delay to ensure Capacitor is ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadPluginStatus();
|
loadPluginStatus();
|
||||||
loadPermissionStatus();
|
loadPermissionStatus();
|
||||||
loadChannelStatus();
|
loadChannelStatus();
|
||||||
|
loadConfigurationStatus();
|
||||||
|
|
||||||
// Initialize last known next notification time
|
// Initialize last known next notification time
|
||||||
if (window.DailyNotification) {
|
if (window.DailyNotification) {
|
||||||
@@ -580,6 +645,57 @@
|
|||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Refresh UI when app comes back to foreground (after force-stop, app resume, etc.)
|
||||||
|
// This ensures the UI updates after recovery from force-stop
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
console.log('[Visibility] App became visible, refreshing UI status...');
|
||||||
|
// Small delay to allow recovery to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
loadPluginStatus();
|
||||||
|
loadPermissionStatus();
|
||||||
|
loadChannelStatus();
|
||||||
|
loadConfigurationStatus();
|
||||||
|
|
||||||
|
// Also check for recent notifications that might have been missed
|
||||||
|
if (window.DailyNotification) {
|
||||||
|
window.DailyNotification.getNotificationStatus()
|
||||||
|
.then(result => {
|
||||||
|
// Check if a notification was received recently (within last 2 minutes)
|
||||||
|
if (result.lastNotificationTime) {
|
||||||
|
const lastTime = new Date(result.lastNotificationTime);
|
||||||
|
const now = new Date();
|
||||||
|
const timeDiff = now - lastTime;
|
||||||
|
|
||||||
|
if (timeDiff > 0 && timeDiff < 120000) {
|
||||||
|
console.log('[Visibility] Recent notification detected, showing indicator');
|
||||||
|
const indicator = document.getElementById('notificationReceivedIndicator');
|
||||||
|
const timeSpan = document.getElementById('notificationReceivedTime');
|
||||||
|
|
||||||
|
if (indicator && timeSpan) {
|
||||||
|
indicator.style.display = 'block';
|
||||||
|
lastTime.setSeconds(0, 0);
|
||||||
|
timeSpan.textContent = `Received at ${lastTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true })}`;
|
||||||
|
|
||||||
|
// Hide after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
indicator.style.display = 'none';
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last known next notification time
|
||||||
|
lastKnownNextNotificationTime = result.nextNotificationTime;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[Visibility] Failed to get notification status:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000); // Wait 1 second for recovery to complete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log('Functions attached to window:', {
|
console.log('Functions attached to window:', {
|
||||||
|
|||||||
@@ -824,18 +824,36 @@ main() {
|
|||||||
info "Post-rollover alarm time: ${post_rollover_alarm_time} (normalized)"
|
info "Post-rollover alarm time: ${post_rollover_alarm_time} (normalized)"
|
||||||
|
|
||||||
# Verify alarm time changed (rollover occurred)
|
# Verify alarm time changed (rollover occurred)
|
||||||
# Compare dates only (YYYY-MM-DD) to detect day change
|
# Compare alarm date to current date - if alarm is scheduled for tomorrow or later, rollover worked
|
||||||
if [ -n "${initial_alarm_time}" ] && [ -n "${post_rollover_alarm_time}" ]; then
|
if [ -n "${post_rollover_alarm_time}" ]; then
|
||||||
local initial_date=$(echo "${initial_alarm_time}" | cut -d' ' -f1)
|
local current_date=$(date +%Y-%m-%d)
|
||||||
local post_date=$(echo "${post_rollover_alarm_time}" | cut -d' ' -f1)
|
local post_date=$(echo "${post_rollover_alarm_time}" | cut -d' ' -f1)
|
||||||
|
|
||||||
if [ "${initial_date}" != "${post_date}" ]; then
|
# Compare dates: if alarm date is >= current date, it's scheduled for today or future (correct)
|
||||||
ok "Alarm date changed: ${initial_alarm_time} → ${post_rollover_alarm_time}"
|
# If we also have initial_alarm_time, check if it advanced
|
||||||
rollover_verified=true
|
if [ -n "${initial_alarm_time}" ]; then
|
||||||
|
local initial_date=$(echo "${initial_alarm_time}" | cut -d' ' -f1)
|
||||||
|
if [ "${initial_date}" != "${post_date}" ]; then
|
||||||
|
ok "Alarm date changed: ${initial_alarm_time} → ${post_rollover_alarm_time}"
|
||||||
|
rollover_verified=true
|
||||||
|
elif [ "${post_date}" \> "${current_date}" ] || [ "${post_date}" = "${current_date}" ]; then
|
||||||
|
# Alarm is scheduled for today or future - this is correct
|
||||||
|
# If initial was also tomorrow, that's fine - rollover logs will confirm it occurred
|
||||||
|
info "Alarm scheduled for ${post_date} (current: ${current_date}) - date unchanged from initial, checking rollover logs"
|
||||||
|
# Don't set rollover_verified yet - let log check determine it
|
||||||
|
else
|
||||||
|
warn "Alarm date ${post_date} is in the past (current: ${current_date}) - rollover may have failed"
|
||||||
|
rollover_verified=false
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
warn "Alarm date did NOT change: ${post_rollover_alarm_time} (same date as initial: ${initial_date})"
|
# No initial alarm time to compare, just check if it's scheduled for future
|
||||||
warn "This indicates the notification did not fire and rollover did not occur"
|
if [ "${post_date}" \> "${current_date}" ] || [ "${post_date}" = "${current_date}" ]; then
|
||||||
rollover_verified=false
|
info "Alarm scheduled for ${post_date} (current: ${current_date}) - checking rollover logs for confirmation"
|
||||||
|
# Don't set rollover_verified yet - let log check determine it
|
||||||
|
else
|
||||||
|
warn "Alarm date ${post_date} is in the past (current: ${current_date}) - rollover may have failed"
|
||||||
|
rollover_verified=false
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1203,43 +1221,62 @@ main() {
|
|||||||
|
|
||||||
evidence_block "test1_force_stop_recovery"
|
evidence_block "test1_force_stop_recovery"
|
||||||
|
|
||||||
# Optional: Verify alarm fires (controlled by VERIFY_FIRE flag)
|
# Verify alarm fires if it's scheduled within reasonable time window (< 5 minutes)
|
||||||
if [ "${VERIFY_FIRE}" = "true" ] && [ -n "${alarm_trigger_ms}" ] && [ "${goto_test1_end}" != "true" ]; then
|
# This ensures restored alarms actually work, not just that they were restored
|
||||||
substep "Step 8: Verify alarm fires at scheduled time (optional)"
|
if [ -n "${alarm_trigger_ms}" ] && [ "${goto_test1_end}" != "true" ]; then
|
||||||
|
|
||||||
local current_time_sec current_time_ms wait_ms wait_sec
|
local current_time_sec current_time_ms wait_ms wait_sec
|
||||||
current_time_sec=$(get_current_time)
|
current_time_sec=$(get_current_time)
|
||||||
current_time_ms=$((current_time_sec * 1000))
|
current_time_ms=$((current_time_sec * 1000))
|
||||||
wait_ms=$((alarm_trigger_ms - current_time_ms))
|
wait_ms=$((alarm_trigger_ms - current_time_ms))
|
||||||
|
|
||||||
if [ "${wait_ms}" -lt 0 ]; then
|
# Auto-enable fire verification if alarm is within 5 minutes (300 seconds)
|
||||||
warn "Alarm time already passed (${wait_ms} ms ago); skipping fire verification"
|
# Or if VERIFY_FIRE is explicitly set to true
|
||||||
else
|
local should_verify_fire=false
|
||||||
wait_sec=$((wait_ms / 1000))
|
if [ "${VERIFY_FIRE}" = "true" ]; then
|
||||||
|
should_verify_fire=true
|
||||||
|
elif [ "${wait_ms}" -gt 0 ] && [ "${wait_ms}" -lt 300000 ]; then
|
||||||
|
# Alarm is in the future and within 5 minutes - auto-verify
|
||||||
|
should_verify_fire=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${should_verify_fire}" = "true" ]; then
|
||||||
|
substep "Step 8: Verify restored alarm fires at scheduled time"
|
||||||
|
set_test_context "phase1" "phase1_test1" "p1_t1_s6"
|
||||||
|
|
||||||
# Clamp upper bound to prevent accidentally waiting 30+ minutes
|
if [ "${wait_ms}" -lt 0 ]; then
|
||||||
if [ "${wait_sec}" -gt 600 ]; then
|
step_warn "p1_t1_s6" "Alarm time already passed"
|
||||||
warn "Alarm is >10 minutes away (${wait_sec}s); skipping fire verification"
|
warn "Alarm time already passed (${wait_ms} ms ago); cannot verify fire"
|
||||||
info "To test fire verification, schedule alarm closer to current time"
|
|
||||||
else
|
else
|
||||||
info "Alarm scheduled for: ${alarm_readable}"
|
wait_sec=$((wait_ms / 1000))
|
||||||
info "Current time: $(date -d "@${current_time_sec}" 2>/dev/null || echo "${current_time_sec}")"
|
|
||||||
info "Waiting ~${wait_sec} seconds for alarm to fire..."
|
|
||||||
|
|
||||||
clear_logs
|
# Clamp upper bound to prevent accidentally waiting too long
|
||||||
sleep ${wait_sec}
|
if [ "${wait_sec}" -gt 600 ]; then
|
||||||
sleep 2
|
step_warn "p1_t1_s6" "Alarm too far in future"
|
||||||
|
warn "Alarm is >10 minutes away (${wait_sec}s); skipping fire verification"
|
||||||
info "Checking logs for fired alarm..."
|
info "To test fire verification, schedule alarm closer to current time"
|
||||||
local alarm_fired
|
|
||||||
alarm_fired="$($ADB_BIN logcat -d | grep -E "DNP-RECEIVE|DNP-NOTIFY|DNP-WORK|Alarm fired|Notification displayed" | tail -10)"
|
|
||||||
|
|
||||||
if [ -n "${alarm_fired}" ]; then
|
|
||||||
ok "Alarm fired! Logs:"
|
|
||||||
echo "${alarm_fired}"
|
|
||||||
else
|
else
|
||||||
warn "No alarm fire logs found"
|
step_start "p1_t1_s6" "Waiting for restored alarm to fire"
|
||||||
info "Check notification tray manually or review recent logs"
|
info "Restored alarm scheduled for: ${alarm_readable}"
|
||||||
|
info "Current time: $(date -d "@${current_time_sec}" 2>/dev/null || echo "${current_time_sec}")"
|
||||||
|
info "Waiting ~${wait_sec} seconds for restored alarm to fire..."
|
||||||
|
|
||||||
|
clear_logs
|
||||||
|
sleep ${wait_sec}
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
info "Checking logs for fired alarm..."
|
||||||
|
local alarm_fired
|
||||||
|
alarm_fired="$($ADB_BIN logcat -d | grep -E "DNP-RECEIVE|DNP-NOTIFY|DNP-WORK|Alarm fired|Notification displayed" | tail -10)"
|
||||||
|
|
||||||
|
if [ -n "${alarm_fired}" ]; then
|
||||||
|
step_pass "p1_t1_s6" "Restored alarm fired successfully"
|
||||||
|
ok "✅ Restored alarm fired! Logs:"
|
||||||
|
echo "${alarm_fired}"
|
||||||
|
else
|
||||||
|
step_fail "p1_t1_s6" "Restored alarm did not fire"
|
||||||
|
warn "⚠️ No alarm fire logs found - restored alarm may not have fired"
|
||||||
|
info "Check notification tray manually or review recent logs"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
163
www/index.html
163
www/index.html
@@ -196,22 +196,27 @@
|
|||||||
console.log('[UI Refresh] Calling getNotificationStatus()...');
|
console.log('[UI Refresh] Calling getNotificationStatus()...');
|
||||||
window.DailyNotification.getNotificationStatus()
|
window.DailyNotification.getNotificationStatus()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log('[UI Refresh] getNotificationStatus result:', {
|
console.log('[UI Refresh] getNotificationStatus result:', JSON.stringify({
|
||||||
nextNotificationTime: result.nextNotificationTime,
|
nextNotificationTime: result.nextNotificationTime,
|
||||||
|
nextNotificationTimeDate: result.nextNotificationTime ? new Date(result.nextNotificationTime).toISOString() : null,
|
||||||
isEnabled: result.isEnabled,
|
isEnabled: result.isEnabled,
|
||||||
pending: result.pending,
|
pending: result.pending,
|
||||||
lastNotificationTime: result.lastNotificationTime
|
lastNotificationTime: result.lastNotificationTime,
|
||||||
});
|
lastNotificationTimeDate: result.lastNotificationTime ? new Date(result.lastNotificationTime).toISOString() : null
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
const nextTime = formatDateTimeNormalized(result.nextNotificationTime);
|
const nextTime = formatDateTimeNormalized(result.nextNotificationTime);
|
||||||
const hasSchedules = result.isEnabled || (result.pending && result.pending > 0);
|
const hasSchedules = result.isEnabled || (result.pending && result.pending > 0);
|
||||||
const statusIcon = hasSchedules ? '✅' : '⏸️';
|
const statusIcon = hasSchedules ? '✅' : '⏸️';
|
||||||
|
|
||||||
console.log('[UI Refresh] Updating UI:', {
|
console.log('[UI Refresh] Updating UI:', JSON.stringify({
|
||||||
nextTime: nextTime,
|
nextTime: nextTime,
|
||||||
|
nextNotificationTimeRaw: result.nextNotificationTime,
|
||||||
|
nextNotificationTimeDate: result.nextNotificationTime ? new Date(result.nextNotificationTime).toISOString() : null,
|
||||||
hasSchedules: hasSchedules,
|
hasSchedules: hasSchedules,
|
||||||
statusIcon: statusIcon
|
statusIcon: statusIcon,
|
||||||
});
|
pending: result.pending
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
pluginStatusContent.innerHTML = `${statusIcon} Active Schedules: ${hasSchedules ? 'Yes' : 'No'}<br>
|
pluginStatusContent.innerHTML = `${statusIcon} Active Schedules: ${hasSchedules ? 'Yes' : 'No'}<br>
|
||||||
📅 Next Notification: ${nextTime}<br>
|
📅 Next Notification: ${nextTime}<br>
|
||||||
@@ -341,6 +346,68 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load configuration status (plugin settings and native fetcher)
|
||||||
|
function loadConfigurationStatus() {
|
||||||
|
console.log('[Config Check] Checking configuration status...');
|
||||||
|
const configStatus = document.getElementById('configStatus');
|
||||||
|
const fetcherStatus = document.getElementById('fetcherStatus');
|
||||||
|
|
||||||
|
if (!window.DailyNotification) {
|
||||||
|
console.warn('[Config Check] DailyNotification plugin not available');
|
||||||
|
configStatus.innerHTML = '❌ Plugin unavailable';
|
||||||
|
fetcherStatus.innerHTML = '❌ Plugin unavailable';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if plugin settings are configured
|
||||||
|
// Plugin settings are stored internally, so we check by trying to get status
|
||||||
|
// For now, we'll check if native fetcher config exists as a proxy
|
||||||
|
// TODO: Add explicit plugin settings check method
|
||||||
|
window.DailyNotification.getConfig({ key: 'native_fetcher_config' })
|
||||||
|
.then(result => {
|
||||||
|
console.log('[Config Check] Native fetcher config result:', JSON.stringify(result));
|
||||||
|
if (result && result.config && result.config.configValue) {
|
||||||
|
try {
|
||||||
|
const configValue = JSON.parse(result.config.configValue);
|
||||||
|
if (configValue.apiBaseUrl && configValue.apiBaseUrl.length > 0) {
|
||||||
|
console.log('[Config Check] ✅ Native fetcher is configured');
|
||||||
|
fetcherStatus.innerHTML = '✅ Configured';
|
||||||
|
} else {
|
||||||
|
console.log('[Config Check] ⚠️ Native fetcher config exists but is empty');
|
||||||
|
fetcherStatus.innerHTML = '❌ Not configured';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Config Check] Failed to parse config value:', e);
|
||||||
|
fetcherStatus.innerHTML = '❌ Error';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[Config Check] ❌ Native fetcher config not found in database');
|
||||||
|
console.log('[Config Check] This may be normal after app uninstall/reinstall (database wiped)');
|
||||||
|
fetcherStatus.innerHTML = '❌ Not configured';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For plugin settings, we assume configured if native fetcher is configured
|
||||||
|
// This is a heuristic - in production, add explicit check
|
||||||
|
if (fetcherStatus.innerHTML.includes('✅')) {
|
||||||
|
configStatus.innerHTML = '✅ Configured';
|
||||||
|
} else {
|
||||||
|
configStatus.innerHTML = '❌ Not configured';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[Config Check] Failed to check configuration:', error);
|
||||||
|
// Don't show error if database might not be ready yet (recovery in progress)
|
||||||
|
if (error.message && error.message.includes('database')) {
|
||||||
|
console.log('[Config Check] Database may not be ready yet, will retry...');
|
||||||
|
fetcherStatus.innerHTML = '⏳ Checking...';
|
||||||
|
configStatus.innerHTML = '⏳ Checking...';
|
||||||
|
} else {
|
||||||
|
configStatus.innerHTML = '❌ Error';
|
||||||
|
fetcherStatus.innerHTML = '❌ Error';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadChannelStatus() {
|
function loadChannelStatus() {
|
||||||
const channelStatus = document.getElementById('channelStatus');
|
const channelStatus = document.getElementById('channelStatus');
|
||||||
|
|
||||||
@@ -472,11 +539,14 @@
|
|||||||
console.log('[Poll] checkNotificationDelivery called');
|
console.log('[Poll] checkNotificationDelivery called');
|
||||||
window.DailyNotification.getNotificationStatus()
|
window.DailyNotification.getNotificationStatus()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log('[Poll] Status check result:', {
|
console.log('[Poll] Status check result:', JSON.stringify({
|
||||||
nextNotificationTime: result.nextNotificationTime,
|
nextNotificationTime: result.nextNotificationTime,
|
||||||
|
nextNotificationTimeDate: result.nextNotificationTime ? new Date(result.nextNotificationTime).toISOString() : null,
|
||||||
lastNotificationTime: result.lastNotificationTime,
|
lastNotificationTime: result.lastNotificationTime,
|
||||||
lastKnownNextNotificationTime: lastKnownNextNotificationTime
|
lastNotificationTimeDate: result.lastNotificationTime ? new Date(result.lastNotificationTime).toISOString() : null,
|
||||||
});
|
lastKnownNextNotificationTime: lastKnownNextNotificationTime,
|
||||||
|
lastKnownNextNotificationTimeDate: lastKnownNextNotificationTime ? new Date(lastKnownNextNotificationTime).toISOString() : null
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
// Check for notification delivery
|
// Check for notification delivery
|
||||||
if (result.lastNotificationTime) {
|
if (result.lastNotificationTime) {
|
||||||
@@ -520,11 +590,13 @@
|
|||||||
|
|
||||||
// Detect if nextNotificationTime changed (rollover occurred)
|
// Detect if nextNotificationTime changed (rollover occurred)
|
||||||
const currentNextTime = result.nextNotificationTime;
|
const currentNextTime = result.nextNotificationTime;
|
||||||
console.log('[Poll] Comparing nextNotificationTime:', {
|
console.log('[Poll] Comparing nextNotificationTime:', JSON.stringify({
|
||||||
current: currentNextTime,
|
current: currentNextTime,
|
||||||
|
currentDate: currentNextTime ? new Date(currentNextTime).toISOString() : null,
|
||||||
lastKnown: lastKnownNextNotificationTime,
|
lastKnown: lastKnownNextNotificationTime,
|
||||||
|
lastKnownDate: lastKnownNextNotificationTime ? new Date(lastKnownNextNotificationTime).toISOString() : null,
|
||||||
changed: currentNextTime && currentNextTime !== lastKnownNextNotificationTime
|
changed: currentNextTime && currentNextTime !== lastKnownNextNotificationTime
|
||||||
});
|
}, null, 2));
|
||||||
|
|
||||||
if (currentNextTime && currentNextTime !== lastKnownNextNotificationTime) {
|
if (currentNextTime && currentNextTime !== lastKnownNextNotificationTime) {
|
||||||
if (lastKnownNextNotificationTime !== null) {
|
if (lastKnownNextNotificationTime !== null) {
|
||||||
@@ -558,11 +630,12 @@
|
|||||||
// Load plugin status automatically on page load
|
// Load plugin status automatically on page load
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
console.log('Page loaded, loading plugin status...');
|
console.log('Page loaded, loading plugin status...');
|
||||||
// Small delay to ensure Capacitor is ready
|
// Small delay to ensure Capacitor is ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadPluginStatus();
|
loadPluginStatus();
|
||||||
loadPermissionStatus();
|
loadPermissionStatus();
|
||||||
loadChannelStatus();
|
loadChannelStatus();
|
||||||
|
loadConfigurationStatus();
|
||||||
|
|
||||||
// Initialize last known next notification time
|
// Initialize last known next notification time
|
||||||
if (window.DailyNotification) {
|
if (window.DailyNotification) {
|
||||||
@@ -580,6 +653,64 @@
|
|||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Refresh UI when app comes back to foreground (after force-stop, app resume, etc.)
|
||||||
|
// This ensures the UI updates after recovery from force-stop
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
console.log('[Visibility] App became visible, refreshing UI status...');
|
||||||
|
// Longer delay to allow recovery to complete (force-stop recovery can take a few seconds)
|
||||||
|
// Also refresh immediately, then again after delay to catch any late recovery
|
||||||
|
loadPluginStatus();
|
||||||
|
loadPermissionStatus();
|
||||||
|
loadChannelStatus();
|
||||||
|
loadConfigurationStatus();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[Visibility] Delayed refresh after recovery period...');
|
||||||
|
loadPluginStatus();
|
||||||
|
loadPermissionStatus();
|
||||||
|
loadChannelStatus();
|
||||||
|
loadConfigurationStatus();
|
||||||
|
|
||||||
|
// Also check for recent notifications that might have been missed
|
||||||
|
if (window.DailyNotification) {
|
||||||
|
window.DailyNotification.getNotificationStatus()
|
||||||
|
.then(result => {
|
||||||
|
// Check if a notification was received recently (within last 2 minutes)
|
||||||
|
if (result.lastNotificationTime) {
|
||||||
|
const lastTime = new Date(result.lastNotificationTime);
|
||||||
|
const now = new Date();
|
||||||
|
const timeDiff = now - lastTime;
|
||||||
|
|
||||||
|
if (timeDiff > 0 && timeDiff < 120000) {
|
||||||
|
console.log('[Visibility] Recent notification detected, showing indicator');
|
||||||
|
const indicator = document.getElementById('notificationReceivedIndicator');
|
||||||
|
const timeSpan = document.getElementById('notificationReceivedTime');
|
||||||
|
|
||||||
|
if (indicator && timeSpan) {
|
||||||
|
indicator.style.display = 'block';
|
||||||
|
lastTime.setSeconds(0, 0);
|
||||||
|
timeSpan.textContent = `Received at ${lastTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true })}`;
|
||||||
|
|
||||||
|
// Hide after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
indicator.style.display = 'none';
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last known next notification time
|
||||||
|
lastKnownNextNotificationTime = result.nextNotificationTime;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[Visibility] Failed to get notification status:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 3000); // Wait 3 seconds for recovery to complete (force-stop recovery can take time)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log('Functions attached to window:', {
|
console.log('Functions attached to window:', {
|
||||||
|
|||||||
Reference in New Issue
Block a user