需要支持用戶自定義重復周期(如:每周三、周五 7:30),計算從當前時間到下一次鬧鐘觸發的時間間隔(毫秒)。單次/每天邏輯簡單,但多天周期(尤其是 4-5 天)邏輯較複雜。
將「一周」展開為連續時間軸,以周一 00:00:00 為基準點 (Epoch),所有時間點轉換為「距離基準點的毫秒數」,問題轉化為在有序集合中查找第一個大於當前時間的值。
private static final long DAY_MS = 24 * 60 * 60 * 1000;
private static final long WEEK_MS = 7 * DAY_MS;
// 周一=0 ... 周日=6 (Calendar 規則: SUN=1, MON=2... 需轉換)
// 例如:周三 07:30 → (2 * DAY_MS) + (7*3600+30*60)*1000
// 周五 07:30 → (4 * DAY_MS) + (7*3600+30*60)*1000
List alarmOffsets = new ArrayList<>();
for (DayTimeSetting setting : userSettings) { // setting: {dayOfWeek, hour, minute}
int dayIndex = convertToMonBasedIndex(setting.dayOfWeek); // 周一=0
long offset = dayIndex * DAY_MS + setting.hour * 3600 * 1000 + setting.minute * 60 * 1000;
alarmOffsets.add(offset);
}
Collections.sort(alarmOffsets); // 確保有序
Calendar now = Calendar.getInstance();
int todayIndex = convertToMonBasedIndex(now.get(Calendar.DAY_OF_WEEK));
long currentOffset = todayIndex * DAY_MS + now.get(Calendar.HOUR_OF_DAY) * 3600 * 1000
+ now.get(Calendar.MINUTE) * 60 * 1000 + now.get(Calendar.SECOND) * 1000
+ now.get(Calendar.MILLISECOND);
long nextTriggerOffset = -1;
// 情況 A: 本周內有未來的鬧鐘
for (long offset : alarmOffsets) {
if (offset > currentOffset) {
nextTriggerOffset = offset;
break;
}
}
// 情況 B: 本周無未來鬧鐘,取下周第一個 (加一周毫秒數)
if (nextTriggerOffset == -1) {
nextTriggerOffset = alarmOffsets.get(0) + WEEK_MS;
}
// 5. 計算最終間隔
long intervalMs = nextTriggerOffset - currentOffset;
// 即可用於 AlarmManager.setExact / setWindow
public class AlarmScheduleUtil {
private static final long DAY_MS = 86400000L;
private static final long WEEK_MS = 604800000L;
/** 將 Calendar.DAY_OF_WEEK (SUN=1..SAT=7) 轉為 周一=0..周日=6 */
private static int toMonBasedIndex(int calDayOfWeek) {
return (calDayOfWeek + 5) % 7; // SUN=1→6, MON=2→0, TUE=3→1 ...
}
/** 計算下次觸發間隔毫秒數
* @param alarmDays 重復的星期集合,如 {Calendar.WEDNESDAY, Calendar.FRIDAY}
* @param hourOfDay 時 (0-23)
* @param minute 分 (0-59)
* @return 距離下次觸發的毫秒數,>0
*/
public static long getNextIntervalMs(Set alarmDays, int hourOfDay, int minute) {
if (alarmDays == null || alarmDays.isEmpty()) return -1;
// 1. 構建本周所有鬧鐘偏移量
List offsets = new ArrayList<>();
for (int day : alarmDays) {
long offset = toMonBasedIndex(day) * DAY_MS + hourOfDay * 3600000L + minute * 60000L;
offsets.add(offset);
}
Collections.sort(offsets);
// 2. 當前時間偏移
Calendar now = Calendar.getInstance();
long currentOffset = toMonBasedIndex(now.get(Calendar.DAY_OF_WEEK)) * DAY_MS
+ now.get(Calendar.HOUR_OF_DAY) * 3600000L
+ now.get(Calendar.MINUTE) * 60000L
+ now.get(Calendar.SECOND) * 1000L
+ now.get(Calendar.MILLISECOND);
// 3. 查找下一個
for (long off : offsets) {
if (off > currentOffset) return off - currentOffset;
}
// 本周無,取下周第一個
return offsets.get(0) + WEEK_MS - currentOffset;
}
}
| 場景 | 處理方式 |
|---|---|
| 用戶修改系統時間 | 註冊 `ACTION_TIME_CHANGED` / `ACTION_TIMEZONE_CHANGED` 廣播,重新計算並重置鬧鐘 |
| 跨夏令時 | 使用 `Calendar` 自動處理,或改用 `ZonedDateTime` (API 26+) |
| 精確鬧鐘權限 (API 19+) | 需 `SET_ALARM` 權限或用戶授權「鬧鐘與提醒」特殊權限 |
| Doze 模式/待機優化 | 使用 `setExactAndAllowWhileIdle` 或 `setAlarmClock` (顯示鬧鐘圖標) |