diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java index ce6112f..eaee7e2 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java @@ -1170,15 +1170,9 @@ public class DailyNotificationPlugin extends Plugin { boolean channelEnabled = channelManager.isChannelEnabled(); int channelImportance = channelManager.getChannelImportance(); - // Check exact alarm permission - boolean exactAlarmsGranted = false; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - exactAlarmsGranted = alarmManager.canScheduleExactAlarms(); - } else { - exactAlarmsGranted = true; // Pre-Android 12, always granted - } - - // Check if we can schedule now + // Check exact alarm permission using PendingIntentManager + PendingIntentManager.AlarmStatus alarmStatus = scheduler.getAlarmStatus(); + boolean exactAlarmsGranted = alarmStatus.exactAlarmsGranted; boolean canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted; // Get next scheduled notification time (if any) @@ -1195,14 +1189,17 @@ public class DailyNotificationPlugin extends Plugin { result.put("channelEnabled", channelEnabled); result.put("channelImportance", channelImportance); result.put("exactAlarmsGranted", exactAlarmsGranted); + result.put("exactAlarmsSupported", alarmStatus.exactAlarmsSupported); result.put("canScheduleNow", canScheduleNow); result.put("nextScheduledAt", nextScheduledAt); result.put("channelId", channelManager.getDefaultChannelId()); + result.put("androidVersion", alarmStatus.androidVersion); Log.i(TAG, "Status check - canSchedule: " + canScheduleNow + ", postNotifications: " + postNotificationsGranted + ", channelEnabled: " + channelEnabled + - ", exactAlarms: " + exactAlarmsGranted); + ", exactAlarms: " + exactAlarmsGranted + + ", alarmStatus: " + alarmStatus.toString()); call.resolve(result); diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java index 2307de4..84854f8 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java @@ -36,6 +36,9 @@ public class DailyNotificationScheduler { private final AlarmManager alarmManager; private final ConcurrentHashMap scheduledAlarms; + // PendingIntent management + private PendingIntentManager pendingIntentManager; + // TTL enforcement private DailyNotificationTTLEnforcer ttlEnforcer; @@ -52,6 +55,9 @@ public class DailyNotificationScheduler { this.context = context; this.alarmManager = alarmManager; this.scheduledAlarms = new ConcurrentHashMap<>(); + this.pendingIntentManager = new PendingIntentManager(context); + + Log.d(TAG, "DailyNotificationScheduler initialized with PendingIntentManager"); } /** @@ -64,6 +70,15 @@ public class DailyNotificationScheduler { Log.d(TAG, "TTL enforcer set for freshness validation"); } + /** + * Get alarm scheduling status + * + * @return AlarmStatus with detailed information + */ + public PendingIntentManager.AlarmStatus getAlarmStatus() { + return pendingIntentManager.getAlarmStatus(); + } + /** * Set exact alarm manager for alarm scheduling * @@ -119,14 +134,9 @@ public class DailyNotificationScheduler { intent.putExtra("priority", content.getPriority()); } - // Create pending intent with unique request code + // Create pending intent with unique request code using PendingIntentManager int requestCode = content.getId().hashCode(); - PendingIntent pendingIntent = PendingIntent.getBroadcast( - context, - requestCode, - intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); + PendingIntent pendingIntent = pendingIntentManager.createBroadcastPendingIntent(intent, requestCode); // Store the pending intent scheduledAlarms.put(content.getId(), pendingIntent); @@ -165,11 +175,12 @@ public class DailyNotificationScheduler { return exactAlarmManager.scheduleAlarm(pendingIntent, triggerTime); } - // Fallback to legacy scheduling - if (canUseExactAlarms()) { - return scheduleExactAlarm(pendingIntent, triggerTime); + // Use PendingIntentManager for modern alarm scheduling + if (pendingIntentManager.canScheduleExactAlarms()) { + return pendingIntentManager.scheduleExactAlarm(pendingIntent, triggerTime); } else { - return scheduleInexactAlarm(pendingIntent, triggerTime); + // Fallback to windowed alarm + return pendingIntentManager.scheduleWindowedAlarm(pendingIntent, triggerTime, 20 * 60 * 1000); // 20 minute window } } catch (Exception e) { Log.e(TAG, "Error scheduling alarm", e); @@ -255,7 +266,7 @@ public class DailyNotificationScheduler { try { PendingIntent pendingIntent = scheduledAlarms.remove(notificationId); if (pendingIntent != null) { - alarmManager.cancel(pendingIntent); + pendingIntentManager.cancelAlarm(pendingIntent); pendingIntent.cancel(); Log.d(TAG, "Cancelled notification: " + notificationId); } diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java new file mode 100644 index 0000000..3f9632f --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java @@ -0,0 +1,255 @@ +package com.timesafari.dailynotification; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; + +/** + * Manages PendingIntent creation with proper flags and exact alarm handling + * + * Ensures all PendingIntents use correct flags for modern Android versions + * and provides comprehensive exact alarm permission handling. + * + * @author Matthew Raymer + * @version 1.0 + */ +public class PendingIntentManager { + private static final String TAG = "PendingIntentManager"; + + // Modern PendingIntent flags for Android 12+ + private static final int MODERN_PENDING_INTENT_FLAGS = + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + + // Legacy flags for older Android versions (if needed) + private static final int LEGACY_PENDING_INTENT_FLAGS = + PendingIntent.FLAG_UPDATE_CURRENT; + + private final Context context; + private final AlarmManager alarmManager; + + public PendingIntentManager(Context context) { + this.context = context; + this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + /** + * Create a PendingIntent for broadcast with proper flags + * + * @param intent The intent to wrap + * @param requestCode Unique request code + * @return PendingIntent with correct flags + */ + public PendingIntent createBroadcastPendingIntent(Intent intent, int requestCode) { + try { + int flags = getPendingIntentFlags(); + return PendingIntent.getBroadcast(context, requestCode, intent, flags); + } catch (Exception e) { + Log.e(TAG, "Error creating broadcast PendingIntent", e); + throw e; + } + } + + /** + * Create a PendingIntent for activity with proper flags + * + * @param intent The intent to wrap + * @param requestCode Unique request code + * @return PendingIntent with correct flags + */ + public PendingIntent createActivityPendingIntent(Intent intent, int requestCode) { + try { + int flags = getPendingIntentFlags(); + return PendingIntent.getActivity(context, requestCode, intent, flags); + } catch (Exception e) { + Log.e(TAG, "Error creating activity PendingIntent", e); + throw e; + } + } + + /** + * Create a PendingIntent for service with proper flags + * + * @param intent The intent to wrap + * @param requestCode Unique request code + * @return PendingIntent with correct flags + */ + public PendingIntent createServicePendingIntent(Intent intent, int requestCode) { + try { + int flags = getPendingIntentFlags(); + return PendingIntent.getService(context, requestCode, intent, flags); + } catch (Exception e) { + Log.e(TAG, "Error creating service PendingIntent", e); + throw e; + } + } + + /** + * Get the appropriate PendingIntent flags for the current Android version + * + * @return Flags to use for PendingIntent creation + */ + private int getPendingIntentFlags() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return MODERN_PENDING_INTENT_FLAGS; + } else { + return LEGACY_PENDING_INTENT_FLAGS; + } + } + + /** + * Check if exact alarms can be scheduled + * + * @return true if exact alarms can be scheduled + */ + public boolean canScheduleExactAlarms() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return alarmManager.canScheduleExactAlarms(); + } else { + return true; // Pre-Android 12, always allowed + } + } catch (Exception e) { + Log.e(TAG, "Error checking exact alarm permission", e); + return false; + } + } + + /** + * Schedule an exact alarm with proper error handling + * + * @param pendingIntent PendingIntent to trigger + * @param triggerTime When to trigger the alarm + * @return true if scheduling was successful + */ + public boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) { + try { + if (!canScheduleExactAlarms()) { + Log.w(TAG, "Cannot schedule exact alarm - permission not granted"); + return false; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ); + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ); + } + + Log.d(TAG, "Exact alarm scheduled successfully for " + triggerTime); + return true; + + } catch (SecurityException e) { + Log.e(TAG, "SecurityException scheduling exact alarm - permission denied", e); + return false; + } catch (Exception e) { + Log.e(TAG, "Error scheduling exact alarm", e); + return false; + } + } + + /** + * Schedule a windowed alarm as fallback + * + * @param pendingIntent PendingIntent to trigger + * @param triggerTime Target trigger time + * @param windowLengthMs Window length in milliseconds + * @return true if scheduling was successful + */ + public boolean scheduleWindowedAlarm(PendingIntent pendingIntent, long triggerTime, long windowLengthMs) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ); + } else { + alarmManager.set( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ); + } + + Log.d(TAG, "Windowed alarm scheduled successfully for " + triggerTime + " (window: " + windowLengthMs + "ms)"); + return true; + + } catch (Exception e) { + Log.e(TAG, "Error scheduling windowed alarm", e); + return false; + } + } + + /** + * Cancel a scheduled alarm + * + * @param pendingIntent PendingIntent to cancel + * @return true if cancellation was successful + */ + public boolean cancelAlarm(PendingIntent pendingIntent) { + try { + alarmManager.cancel(pendingIntent); + Log.d(TAG, "Alarm cancelled successfully"); + return true; + } catch (Exception e) { + Log.e(TAG, "Error cancelling alarm", e); + return false; + } + } + + /** + * Get detailed alarm scheduling status + * + * @return AlarmStatus object with detailed information + */ + public AlarmStatus getAlarmStatus() { + boolean exactAlarmsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + boolean exactAlarmsGranted = canScheduleExactAlarms(); + boolean canScheduleNow = exactAlarmsGranted || !exactAlarmsSupported; + + return new AlarmStatus( + exactAlarmsSupported, + exactAlarmsGranted, + canScheduleNow, + Build.VERSION.SDK_INT + ); + } + + /** + * Data class for alarm status information + */ + public static class AlarmStatus { + public final boolean exactAlarmsSupported; + public final boolean exactAlarmsGranted; + public final boolean canScheduleNow; + public final int androidVersion; + + public AlarmStatus(boolean exactAlarmsSupported, boolean exactAlarmsGranted, + boolean canScheduleNow, int androidVersion) { + this.exactAlarmsSupported = exactAlarmsSupported; + this.exactAlarmsGranted = exactAlarmsGranted; + this.canScheduleNow = canScheduleNow; + this.androidVersion = androidVersion; + } + + @Override + public String toString() { + return "AlarmStatus{" + + "exactAlarmsSupported=" + exactAlarmsSupported + + ", exactAlarmsGranted=" + exactAlarmsGranted + + ", canScheduleNow=" + canScheduleNow + + ", androidVersion=" + androidVersion + + '}'; + } + } +}