feat(android): implement P0 PendingIntent flags and exact alarm fixes
- Add PendingIntentManager class for proper PendingIntent handling - Implement correct FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE flags for Android 12+ - Add comprehensive exact alarm permission checking and handling - Add fallback to windowed alarms when exact alarm permission denied - Update DailyNotificationScheduler to use PendingIntentManager - Add detailed alarm status reporting with Android version info - Improve error handling for SecurityException on exact alarm scheduling - Add comprehensive alarm status to checkStatus() method P0 Priority Implementation: - Fixes PendingIntent flags for modern Android compatibility - Ensures exact alarm permissions are properly checked before scheduling - Provides actionable error messages when exact alarm permission denied - Adds fallback to windowed alarms for better reliability - Improves alarm scheduling status reporting and debugging This addresses the critical P0 issues with PendingIntent flags and exact alarm permission handling for production reliability.
This commit is contained in:
@@ -1170,15 +1170,9 @@ public class DailyNotificationPlugin extends Plugin {
|
|||||||
boolean channelEnabled = channelManager.isChannelEnabled();
|
boolean channelEnabled = channelManager.isChannelEnabled();
|
||||||
int channelImportance = channelManager.getChannelImportance();
|
int channelImportance = channelManager.getChannelImportance();
|
||||||
|
|
||||||
// Check exact alarm permission
|
// Check exact alarm permission using PendingIntentManager
|
||||||
boolean exactAlarmsGranted = false;
|
PendingIntentManager.AlarmStatus alarmStatus = scheduler.getAlarmStatus();
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
boolean exactAlarmsGranted = alarmStatus.exactAlarmsGranted;
|
||||||
exactAlarmsGranted = alarmManager.canScheduleExactAlarms();
|
|
||||||
} else {
|
|
||||||
exactAlarmsGranted = true; // Pre-Android 12, always granted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we can schedule now
|
|
||||||
boolean canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted;
|
boolean canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted;
|
||||||
|
|
||||||
// Get next scheduled notification time (if any)
|
// Get next scheduled notification time (if any)
|
||||||
@@ -1195,14 +1189,17 @@ public class DailyNotificationPlugin extends Plugin {
|
|||||||
result.put("channelEnabled", channelEnabled);
|
result.put("channelEnabled", channelEnabled);
|
||||||
result.put("channelImportance", channelImportance);
|
result.put("channelImportance", channelImportance);
|
||||||
result.put("exactAlarmsGranted", exactAlarmsGranted);
|
result.put("exactAlarmsGranted", exactAlarmsGranted);
|
||||||
|
result.put("exactAlarmsSupported", alarmStatus.exactAlarmsSupported);
|
||||||
result.put("canScheduleNow", canScheduleNow);
|
result.put("canScheduleNow", canScheduleNow);
|
||||||
result.put("nextScheduledAt", nextScheduledAt);
|
result.put("nextScheduledAt", nextScheduledAt);
|
||||||
result.put("channelId", channelManager.getDefaultChannelId());
|
result.put("channelId", channelManager.getDefaultChannelId());
|
||||||
|
result.put("androidVersion", alarmStatus.androidVersion);
|
||||||
|
|
||||||
Log.i(TAG, "Status check - canSchedule: " + canScheduleNow +
|
Log.i(TAG, "Status check - canSchedule: " + canScheduleNow +
|
||||||
", postNotifications: " + postNotificationsGranted +
|
", postNotifications: " + postNotificationsGranted +
|
||||||
", channelEnabled: " + channelEnabled +
|
", channelEnabled: " + channelEnabled +
|
||||||
", exactAlarms: " + exactAlarmsGranted);
|
", exactAlarms: " + exactAlarmsGranted +
|
||||||
|
", alarmStatus: " + alarmStatus.toString());
|
||||||
|
|
||||||
call.resolve(result);
|
call.resolve(result);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public class DailyNotificationScheduler {
|
|||||||
private final AlarmManager alarmManager;
|
private final AlarmManager alarmManager;
|
||||||
private final ConcurrentHashMap<String, PendingIntent> scheduledAlarms;
|
private final ConcurrentHashMap<String, PendingIntent> scheduledAlarms;
|
||||||
|
|
||||||
|
// PendingIntent management
|
||||||
|
private PendingIntentManager pendingIntentManager;
|
||||||
|
|
||||||
// TTL enforcement
|
// TTL enforcement
|
||||||
private DailyNotificationTTLEnforcer ttlEnforcer;
|
private DailyNotificationTTLEnforcer ttlEnforcer;
|
||||||
|
|
||||||
@@ -52,6 +55,9 @@ public class DailyNotificationScheduler {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
this.alarmManager = alarmManager;
|
this.alarmManager = alarmManager;
|
||||||
this.scheduledAlarms = new ConcurrentHashMap<>();
|
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");
|
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
|
* Set exact alarm manager for alarm scheduling
|
||||||
*
|
*
|
||||||
@@ -119,14 +134,9 @@ public class DailyNotificationScheduler {
|
|||||||
intent.putExtra("priority", content.getPriority());
|
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();
|
int requestCode = content.getId().hashCode();
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
PendingIntent pendingIntent = pendingIntentManager.createBroadcastPendingIntent(intent, requestCode);
|
||||||
context,
|
|
||||||
requestCode,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store the pending intent
|
// Store the pending intent
|
||||||
scheduledAlarms.put(content.getId(), pendingIntent);
|
scheduledAlarms.put(content.getId(), pendingIntent);
|
||||||
@@ -165,11 +175,12 @@ public class DailyNotificationScheduler {
|
|||||||
return exactAlarmManager.scheduleAlarm(pendingIntent, triggerTime);
|
return exactAlarmManager.scheduleAlarm(pendingIntent, triggerTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to legacy scheduling
|
// Use PendingIntentManager for modern alarm scheduling
|
||||||
if (canUseExactAlarms()) {
|
if (pendingIntentManager.canScheduleExactAlarms()) {
|
||||||
return scheduleExactAlarm(pendingIntent, triggerTime);
|
return pendingIntentManager.scheduleExactAlarm(pendingIntent, triggerTime);
|
||||||
} else {
|
} else {
|
||||||
return scheduleInexactAlarm(pendingIntent, triggerTime);
|
// Fallback to windowed alarm
|
||||||
|
return pendingIntentManager.scheduleWindowedAlarm(pendingIntent, triggerTime, 20 * 60 * 1000); // 20 minute window
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error scheduling alarm", e);
|
Log.e(TAG, "Error scheduling alarm", e);
|
||||||
@@ -255,7 +266,7 @@ public class DailyNotificationScheduler {
|
|||||||
try {
|
try {
|
||||||
PendingIntent pendingIntent = scheduledAlarms.remove(notificationId);
|
PendingIntent pendingIntent = scheduledAlarms.remove(notificationId);
|
||||||
if (pendingIntent != null) {
|
if (pendingIntent != null) {
|
||||||
alarmManager.cancel(pendingIntent);
|
pendingIntentManager.cancelAlarm(pendingIntent);
|
||||||
pendingIntent.cancel();
|
pendingIntent.cancel();
|
||||||
Log.d(TAG, "Cancelled notification: " + notificationId);
|
Log.d(TAG, "Cancelled notification: " + notificationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user