diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index 4848106..e863d2c 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -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) + } } /** diff --git a/android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt b/android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt index 183b32d..886b7fa 100644 --- a/android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt +++ b/android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt @@ -82,8 +82,16 @@ class FetchWorker( return } + // Only require network if URL is provided (mock content doesn't need network) val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) + .apply { + if (url != null) { + setRequiredNetworkType(NetworkType.CONNECTED) + } else { + // No network required for mock content generation + setRequiredNetworkType(NetworkType.NOT_REQUIRED) + } + } .build() // Create unique work name based on notification time to prevent duplicate fetches @@ -129,8 +137,16 @@ class FetchWorker( notificationTime: Long, url: String? = null ) { + // Only require network if URL is provided (mock content doesn't need network) val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) + .apply { + if (url != null) { + setRequiredNetworkType(NetworkType.CONNECTED) + } else { + // No network required for mock content generation + setRequiredNetworkType(NetworkType.NOT_REQUIRED) + } + } .build() val workRequest = OneTimeWorkRequestBuilder() diff --git a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt index 8998b0c..f8b1d20 100644 --- a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt +++ b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt @@ -32,7 +32,9 @@ class NotifyReceiver : BroadcastReceiver() { fun scheduleExactNotification( context: Context, triggerAtMillis: Long, - config: UserNotificationConfig + config: UserNotificationConfig, + isStaticReminder: Boolean = false, + reminderId: String? = null ) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, NotifyReceiver::class.java).apply { @@ -41,6 +43,10 @@ class NotifyReceiver : BroadcastReceiver() { putExtra("sound", config.sound ?: true) putExtra("vibration", config.vibration ?: true) putExtra("priority", config.priority ?: "normal") + putExtra("is_static_reminder", isStaticReminder) + if (reminderId != null) { + putExtra("reminder_id", reminderId) + } } val pendingIntent = PendingIntent.getBroadcast( @@ -187,6 +193,25 @@ class NotifyReceiver : BroadcastReceiver() { notificationManager.createNotificationChannel(channel) } + // Create intent to launch app when notification is clicked + val intent = try { + Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } catch (e: ClassNotFoundException) { + Log.w(TAG, "MainActivity not found, using package launcher", e) + // Fallback: launch app by package name + context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } ?: return + } + val pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(title) .setContentText(body) @@ -198,7 +223,8 @@ class NotifyReceiver : BroadcastReceiver() { else -> NotificationCompat.PRIORITY_DEFAULT } ) - .setAutoCancel(true) + .setAutoCancel(true) // Dismissible when user swipes it away + .setContentIntent(pendingIntent) // Launch app when clicked .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null) .build() @@ -286,6 +312,25 @@ class NotifyReceiver : BroadcastReceiver() { // Create notification channel for reminders createReminderNotificationChannel(context, notificationManager) + // Create intent to launch app when notification is clicked + val intent = try { + Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } catch (e: ClassNotFoundException) { + Log.w(TAG, "MainActivity not found, using package launcher", e) + // Fallback: launch app by package name + context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } ?: return + } + val pendingIntent = PendingIntent.getActivity( + context, + reminderId.hashCode(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val notification = NotificationCompat.Builder(context, "daily_reminders") .setSmallIcon(android.R.drawable.ic_dialog_info) .setContentTitle(title) @@ -298,7 +343,8 @@ class NotifyReceiver : BroadcastReceiver() { } ) .setSound(if (sound) null else null) // Use default sound if enabled - .setAutoCancel(true) + .setAutoCancel(true) // Dismissible when user swipes it away + .setContentIntent(pendingIntent) // Launch app when clicked .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null) .setCategory(NotificationCompat.CATEGORY_REMINDER) .build()