Browse Source
- 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.master
3 changed files with 285 additions and 22 deletions
@ -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 + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue