You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
13 KiB
384 lines
13 KiB
/**
|
|
* DailyNotificationExactAlarmManager.java
|
|
*
|
|
* Android Exact Alarm Manager with fallback to windowed alarms
|
|
* Implements SCHEDULE_EXACT_ALARM permission handling and fallback logic
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
package com.timesafari.dailynotification;
|
|
|
|
import android.app.AlarmManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageManager;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.provider.Settings;
|
|
import android.util.Log;
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* Manages Android exact alarms with fallback to windowed alarms
|
|
*
|
|
* This class implements the critical Android alarm management:
|
|
* - Requests SCHEDULE_EXACT_ALARM permission
|
|
* - Falls back to windowed alarms (±10m) if exact permission denied
|
|
* - Provides deep-link to enable exact alarms in settings
|
|
* - Handles reboot and time-change recovery
|
|
*/
|
|
public class DailyNotificationExactAlarmManager {
|
|
|
|
// MARK: - Constants
|
|
|
|
private static final String TAG = "DailyNotificationExactAlarmManager";
|
|
|
|
// Permission constants
|
|
private static final String PERMISSION_SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
|
|
|
|
// Fallback window settings
|
|
private static final long FALLBACK_WINDOW_START_MS = TimeUnit.MINUTES.toMillis(-10); // 10 minutes before
|
|
private static final long FALLBACK_WINDOW_LENGTH_MS = TimeUnit.MINUTES.toMillis(20); // 20 minutes total
|
|
|
|
// Deep-link constants
|
|
private static final String EXACT_ALARM_SETTINGS_ACTION = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM";
|
|
private static final String EXACT_ALARM_SETTINGS_PACKAGE = "com.android.settings";
|
|
|
|
// MARK: - Properties
|
|
|
|
private final Context context;
|
|
private final AlarmManager alarmManager;
|
|
private final DailyNotificationScheduler scheduler;
|
|
|
|
// Alarm state
|
|
private boolean exactAlarmsEnabled = false;
|
|
private boolean exactAlarmsSupported = false;
|
|
|
|
// MARK: - Initialization
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param context Application context
|
|
* @param alarmManager System AlarmManager service
|
|
* @param scheduler Notification scheduler
|
|
*/
|
|
public DailyNotificationExactAlarmManager(Context context, AlarmManager alarmManager, DailyNotificationScheduler scheduler) {
|
|
this.context = context;
|
|
this.alarmManager = alarmManager;
|
|
this.scheduler = scheduler;
|
|
|
|
// Check exact alarm support and status
|
|
checkExactAlarmSupport();
|
|
checkExactAlarmStatus();
|
|
|
|
Log.d(TAG, "ExactAlarmManager initialized: supported=" + exactAlarmsSupported + ", enabled=" + exactAlarmsEnabled);
|
|
}
|
|
|
|
// MARK: - Exact Alarm Support
|
|
|
|
/**
|
|
* Check if exact alarms are supported on this device
|
|
*/
|
|
private void checkExactAlarmSupport() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
exactAlarmsSupported = true;
|
|
Log.d(TAG, "Exact alarms supported on Android S+");
|
|
} else {
|
|
exactAlarmsSupported = false;
|
|
Log.d(TAG, "Exact alarms not supported on Android " + Build.VERSION.SDK_INT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check current exact alarm status
|
|
*/
|
|
private void checkExactAlarmStatus() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
exactAlarmsEnabled = alarmManager.canScheduleExactAlarms();
|
|
Log.d(TAG, "Exact alarm status: " + (exactAlarmsEnabled ? "enabled" : "disabled"));
|
|
} else {
|
|
exactAlarmsEnabled = true; // Always available on older Android versions
|
|
Log.d(TAG, "Exact alarms always available on Android " + Build.VERSION.SDK_INT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get exact alarm status
|
|
*
|
|
* @return Status information
|
|
*/
|
|
public ExactAlarmStatus getExactAlarmStatus() {
|
|
return new ExactAlarmStatus(
|
|
exactAlarmsSupported,
|
|
exactAlarmsEnabled,
|
|
canScheduleExactAlarms(),
|
|
getFallbackWindowInfo()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if exact alarms can be scheduled
|
|
*
|
|
* @return true if exact alarms can be scheduled
|
|
*/
|
|
public boolean canScheduleExactAlarms() {
|
|
if (!exactAlarmsSupported) {
|
|
return false;
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
return alarmManager.canScheduleExactAlarms();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get fallback window information
|
|
*
|
|
* @return Fallback window info
|
|
*/
|
|
public FallbackWindowInfo getFallbackWindowInfo() {
|
|
return new FallbackWindowInfo(
|
|
FALLBACK_WINDOW_START_MS,
|
|
FALLBACK_WINDOW_LENGTH_MS,
|
|
"±10 minutes"
|
|
);
|
|
}
|
|
|
|
// MARK: - Alarm Scheduling
|
|
|
|
/**
|
|
* Schedule alarm with exact or fallback logic
|
|
*
|
|
* @param pendingIntent PendingIntent to trigger
|
|
* @param triggerTime Exact trigger time
|
|
* @return true if scheduling was successful
|
|
*/
|
|
public boolean scheduleAlarm(PendingIntent pendingIntent, long triggerTime) {
|
|
try {
|
|
Log.d(TAG, "Scheduling alarm for " + triggerTime);
|
|
|
|
if (canScheduleExactAlarms()) {
|
|
return scheduleExactAlarm(pendingIntent, triggerTime);
|
|
} else {
|
|
return scheduleWindowedAlarm(pendingIntent, triggerTime);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling alarm", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule exact alarm
|
|
*
|
|
* @param pendingIntent PendingIntent to trigger
|
|
* @param triggerTime Exact trigger time
|
|
* @return true if scheduling was successful
|
|
*/
|
|
private boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) {
|
|
try {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
|
|
Log.i(TAG, "Exact alarm scheduled for " + triggerTime);
|
|
return true;
|
|
} else {
|
|
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
|
|
Log.i(TAG, "Exact alarm scheduled for " + triggerTime + " (pre-M)");
|
|
return true;
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling exact alarm", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule windowed alarm as fallback
|
|
*
|
|
* @param pendingIntent PendingIntent to trigger
|
|
* @param triggerTime Target trigger time
|
|
* @return true if scheduling was successful
|
|
*/
|
|
private boolean scheduleWindowedAlarm(PendingIntent pendingIntent, long triggerTime) {
|
|
try {
|
|
// Calculate window start time (10 minutes before target)
|
|
long windowStartTime = triggerTime + FALLBACK_WINDOW_START_MS;
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
alarmManager.setWindow(AlarmManager.RTC_WAKEUP, windowStartTime, FALLBACK_WINDOW_LENGTH_MS, pendingIntent);
|
|
Log.i(TAG, "Windowed alarm scheduled: target=" + triggerTime + ", window=" + windowStartTime + " to " + (windowStartTime + FALLBACK_WINDOW_LENGTH_MS));
|
|
return true;
|
|
} else {
|
|
// Fallback to inexact alarm on older versions
|
|
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
|
|
Log.i(TAG, "Inexact alarm scheduled for " + triggerTime + " (pre-KitKat)");
|
|
return true;
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling windowed alarm", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// MARK: - Permission Management
|
|
|
|
/**
|
|
* Request exact alarm permission
|
|
*
|
|
* @return true if permission request was initiated
|
|
*/
|
|
public boolean requestExactAlarmPermission() {
|
|
if (!exactAlarmsSupported) {
|
|
Log.w(TAG, "Exact alarms not supported on this device");
|
|
return false;
|
|
}
|
|
|
|
if (exactAlarmsEnabled) {
|
|
Log.d(TAG, "Exact alarms already enabled");
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// Open exact alarm settings
|
|
Intent intent = new Intent(EXACT_ALARM_SETTINGS_ACTION);
|
|
intent.setPackage(EXACT_ALARM_SETTINGS_PACKAGE);
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
context.startActivity(intent);
|
|
Log.i(TAG, "Exact alarm permission request initiated");
|
|
return true;
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error requesting exact alarm permission", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open exact alarm settings
|
|
*
|
|
* @return true if settings were opened
|
|
*/
|
|
public boolean openExactAlarmSettings() {
|
|
try {
|
|
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
context.startActivity(intent);
|
|
Log.i(TAG, "Exact alarm settings opened");
|
|
return true;
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error opening exact alarm settings", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if exact alarm permission is granted
|
|
*
|
|
* @return true if permission is granted
|
|
*/
|
|
public boolean hasExactAlarmPermission() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
return context.checkSelfPermission(PERMISSION_SCHEDULE_EXACT_ALARM) == PackageManager.PERMISSION_GRANTED;
|
|
}
|
|
return true; // Always available on older versions
|
|
}
|
|
|
|
// MARK: - Reboot and Time Change Recovery
|
|
|
|
/**
|
|
* Handle system reboot
|
|
*
|
|
* This method should be called when the system boots to restore
|
|
* scheduled alarms that were lost during reboot.
|
|
*/
|
|
public void handleSystemReboot() {
|
|
try {
|
|
Log.i(TAG, "Handling system reboot - restoring scheduled alarms");
|
|
|
|
// Re-schedule all pending notifications
|
|
scheduler.restoreScheduledNotifications();
|
|
|
|
Log.i(TAG, "System reboot handling completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error handling system reboot", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle time change
|
|
*
|
|
* This method should be called when the system time changes
|
|
* to adjust scheduled alarms accordingly.
|
|
*/
|
|
public void handleTimeChange() {
|
|
try {
|
|
Log.i(TAG, "Handling time change - adjusting scheduled alarms");
|
|
|
|
// Re-schedule all pending notifications with adjusted times
|
|
scheduler.adjustScheduledNotifications();
|
|
|
|
Log.i(TAG, "Time change handling completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error handling time change", e);
|
|
}
|
|
}
|
|
|
|
// MARK: - Status Classes
|
|
|
|
/**
|
|
* Exact alarm status information
|
|
*/
|
|
public static class ExactAlarmStatus {
|
|
public final boolean supported;
|
|
public final boolean enabled;
|
|
public final boolean canSchedule;
|
|
public final FallbackWindowInfo fallbackWindow;
|
|
|
|
public ExactAlarmStatus(boolean supported, boolean enabled, boolean canSchedule, FallbackWindowInfo fallbackWindow) {
|
|
this.supported = supported;
|
|
this.enabled = enabled;
|
|
this.canSchedule = canSchedule;
|
|
this.fallbackWindow = fallbackWindow;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("ExactAlarmStatus{supported=%s, enabled=%s, canSchedule=%s, fallbackWindow=%s}",
|
|
supported, enabled, canSchedule, fallbackWindow);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fallback window information
|
|
*/
|
|
public static class FallbackWindowInfo {
|
|
public final long startMs;
|
|
public final long lengthMs;
|
|
public final String description;
|
|
|
|
public FallbackWindowInfo(long startMs, long lengthMs, String description) {
|
|
this.startMs = startMs;
|
|
this.lengthMs = lengthMs;
|
|
this.description = description;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("FallbackWindowInfo{start=%dms, length=%dms, description='%s'}",
|
|
startMs, lengthMs, description);
|
|
}
|
|
}
|
|
}
|
|
|