diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index 028773f..79884c3 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -3,6 +3,7 @@ package com.timesafari.dailynotification import android.Manifest import android.app.Activity import android.app.AlarmManager +import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -14,6 +15,7 @@ import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import androidx.work.WorkManager import com.getcapacitor.JSObject import com.getcapacitor.Plugin import com.getcapacitor.PluginCall @@ -562,6 +564,132 @@ open class DailyNotificationPlugin : Plugin() { } } + /** + * Cancel all scheduled notifications + * + * This method: + * 1. Cancels all AlarmManager alarms (both exact and inexact) + * 2. Cancels all WorkManager prefetch jobs + * 3. Clears notification schedules from database + * 4. Updates plugin state to reflect cancellation + * + * The method is idempotent - safe to call multiple times even if nothing is scheduled. + */ + @PluginMethod + fun cancelAllNotifications(call: PluginCall) { + CoroutineScope(Dispatchers.IO).launch { + try { + if (context == null) { + return@launch call.reject("Context not available") + } + + Log.i(TAG, "Cancelling all notifications") + + // 1. Get all scheduled notifications from database + val schedules = getDatabase().scheduleDao().getAll() + val notifySchedules = schedules.filter { it.kind == "notify" && it.enabled } + + // 2. Cancel all AlarmManager alarms + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager + if (alarmManager != null) { + var cancelledAlarms = 0 + notifySchedules.forEach { schedule -> + try { + // Cancel alarm using the scheduled time (used for request code) + val nextRunAt = schedule.nextRunAt + if (nextRunAt != null && nextRunAt > 0) { + NotifyReceiver.cancelNotification(context, nextRunAt) + cancelledAlarms++ + } + } catch (e: Exception) { + // Log but don't fail - alarm might not exist + Log.w(TAG, "Failed to cancel alarm for schedule ${schedule.id}", e) + } + } + + // Also try to cancel any alarms that might not be in database + // Cancel by attempting to cancel with a generic intent + try { + val intent = Intent(context, NotifyReceiver::class.java) + // Try cancelling with common request codes (0-65535) + // This is a fallback for any orphaned alarms + for (requestCode in 0..100 step 10) { + try { + val pendingIntent = PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE + ) + if (pendingIntent != null) { + alarmManager.cancel(pendingIntent) + pendingIntent.cancel() + } + } catch (e: Exception) { + // Ignore - this is a best-effort cleanup + } + } + } catch (e: Exception) { + Log.w(TAG, "Error during fallback alarm cancellation", e) + } + + Log.i(TAG, "Cancelled $cancelledAlarms alarm(s)") + } else { + Log.w(TAG, "AlarmManager not available") + } + + // 3. Cancel all WorkManager jobs + try { + val workManager = WorkManager.getInstance(context) + + // Cancel all prefetch jobs + workManager.cancelAllWorkByTag("prefetch") + + // Cancel fetch jobs (if using DailyNotificationFetcher tags) + workManager.cancelAllWorkByTag("daily_notification_fetch") + workManager.cancelAllWorkByTag("daily_notification_maintenance") + workManager.cancelAllWorkByTag("soft_refetch") + workManager.cancelAllWorkByTag("daily_notification_display") + workManager.cancelAllWorkByTag("daily_notification_dismiss") + + // Cancel unique work by name pattern (prefetch_*) + // Note: WorkManager doesn't support wildcard cancellation, so we cancel by tag + // The unique work names will be replaced when new work is scheduled + + Log.i(TAG, "Cancelled all WorkManager jobs") + } catch (e: Exception) { + Log.w(TAG, "Failed to cancel WorkManager jobs", e) + // Don't fail - continue with database cleanup + } + + // 4. Clear database state - disable all notification schedules + try { + notifySchedules.forEach { schedule -> + getDatabase().scheduleDao().setEnabled(schedule.id, false) + } + + // Also clear any fetch schedules + val fetchSchedules = schedules.filter { it.kind == "fetch" && it.enabled } + fetchSchedules.forEach { schedule -> + getDatabase().scheduleDao().setEnabled(schedule.id, false) + } + + Log.i(TAG, "Disabled ${notifySchedules.size} notification schedule(s) and ${fetchSchedules.size} fetch schedule(s)") + } catch (e: Exception) { + Log.e(TAG, "Failed to clear database state", e) + // Continue - alarms and jobs are already cancelled + } + + Log.i(TAG, "All notifications cancelled successfully") + call.resolve() + + } catch (e: Exception) { + Log.e(TAG, "Failed to cancel all notifications", e) + call.reject("Failed to cancel notifications: ${e.message}") + } + } + } + @PluginMethod fun scheduleDailyReminder(call: PluginCall) { // Alias for scheduleDailyNotification for backward compatibility diff --git a/package.json b/package.json index 8088930..4c45ac6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@timesafari/daily-notification-plugin", - "version": "1.0.2", + "version": "1.0.3", "description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms", "main": "dist/plugin.js", "module": "dist/esm/index.js",