fix(android): improve notification scheduling and UX

- Fix cron parsing to correctly calculate next run time based on hour/minute
- Always schedule prefetch 5 minutes before notification (even without URL)
- Make notifications dismissable with setAutoCancel(true)
- Add click action to launch app when notification is tapped
- Conditionally require network only when URL is provided for prefetch
- Generate mock content when no URL is specified

These changes ensure notifications fire at the correct time, are
user-friendly (dismissable and clickable), and prefetch works reliably
even without a content URL.
This commit is contained in:
Matthew Raymer
2025-11-06 07:52:40 +00:00
parent 18106e5ba8
commit 9f8e295234
3 changed files with 130 additions and 22 deletions

View File

@@ -570,24 +570,31 @@ open class DailyNotificationPlugin : Plugin() {
val nextRunTime = calculateNextRunTime(cronExpression)
// Schedule AlarmManager notification
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
// Schedule AlarmManager notification as static reminder
// (doesn't require cached content)
val scheduleId = "daily_${System.currentTimeMillis()}"
NotifyReceiver.scheduleExactNotification(
context,
nextRunTime,
config,
isStaticReminder = true,
reminderId = scheduleId
)
// Schedule prefetch 5 minutes before notification (if URL provided)
if (url != null) {
val fetchTime = nextRunTime - (5 * 60 * 1000L) // 5 minutes before
FetchWorker.scheduleDelayedFetch(
context,
fetchTime,
nextRunTime,
url
)
Log.i(TAG, "Prefetch scheduled: fetchTime=$fetchTime, notificationTime=$nextRunTime")
}
// Always schedule prefetch 5 minutes before notification
// (URL is optional - generates mock content if not provided)
val fetchTime = nextRunTime - (5 * 60 * 1000L) // 5 minutes before
FetchWorker.scheduleDelayedFetch(
context,
fetchTime,
nextRunTime,
url // Can be null - FetchWorker will generate mock content
)
Log.i(TAG, "Prefetch scheduled: fetchTime=$fetchTime, notificationTime=$nextRunTime, url=${url ?: "none (will generate mock)"}")
// Store schedule in database
val schedule = Schedule(
id = "daily_${System.currentTimeMillis()}",
id = scheduleId,
kind = "notify",
cron = cronExpression,
clockTime = time,
@@ -1563,9 +1570,48 @@ open class DailyNotificationPlugin : Plugin() {
}
private fun calculateNextRunTime(schedule: String): Long {
// Simple implementation - for production, use proper cron parsing
val now = System.currentTimeMillis()
return now + (24 * 60 * 60 * 1000L) // Next day
// Parse cron expression: "minute hour * * *" (daily schedule)
// Example: "9 7 * * *" = 07:09 daily
try {
val parts = schedule.trim().split("\\s+".toRegex())
if (parts.size < 2) {
Log.w(TAG, "Invalid cron format: $schedule, defaulting to 24h from now")
return System.currentTimeMillis() + (24 * 60 * 60 * 1000L)
}
val minute = parts[0].toIntOrNull() ?: 0
val hour = parts[1].toIntOrNull() ?: 9
if (minute < 0 || minute > 59 || hour < 0 || hour > 23) {
Log.w(TAG, "Invalid time values in cron: $schedule, defaulting to 24h from now")
return System.currentTimeMillis() + (24 * 60 * 60 * 1000L)
}
// Calculate next occurrence of this time
val calendar = java.util.Calendar.getInstance()
val now = calendar.timeInMillis
// Set to today at the specified time
calendar.set(java.util.Calendar.HOUR_OF_DAY, hour)
calendar.set(java.util.Calendar.MINUTE, minute)
calendar.set(java.util.Calendar.SECOND, 0)
calendar.set(java.util.Calendar.MILLISECOND, 0)
var nextRun = calendar.timeInMillis
// If the time has already passed today, schedule for tomorrow
if (nextRun <= now) {
calendar.add(java.util.Calendar.DAY_OF_YEAR, 1)
nextRun = calendar.timeInMillis
}
Log.d(TAG, "Calculated next run time: cron=$schedule, nextRun=${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US).format(java.util.Date(nextRun))}")
return nextRun
} catch (e: Exception) {
Log.e(TAG, "Error calculating next run time for schedule: $schedule", e)
// Fallback: 24 hours from now
return System.currentTimeMillis() + (24 * 60 * 60 * 1000L)
}
}
/**