/** * Fix and update all alarm instance when a time change event occurs. * * @param context application context */ public static void fixAlarmInstances(Context context) { // Register all instances after major time changes or when phone restarts final ContentResolver contentResolver = context.getContentResolver(); final Calendar currentTime = getCurrentTime(); for (AlarmInstance instance : AlarmInstance.getInstances(contentResolver, null)) { final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId); final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime()); final Calendar missedTTLTime = instance.getMissedTimeToLive(); if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) { final Calendar oldAlarmTime = instance.getAlarmTime(); final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime); final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime); final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime); LogUtils.i( "A time change has caused an existing alarm scheduled to fire at %s to" + " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime); // The time change is so dramatic the AlarmInstance doesn't make any sense; // remove it and schedule the new appropriate instance. AlarmStateManager.setDismissState(context, instance); } else { registerInstance(context, instance, false); } } updateNextAlarm(context); }
/** * Returns an alarm instance of an alarm that's going to fire next. * * @param context application context * @return an alarm instance that will fire earliest relative to current time. */ public static AlarmInstance getNextFiringAlarm(Context context) { final ContentResolver cr = context.getContentResolver(); final String activeAlarmQuery = AlarmInstance.ALARM_STATE + "<" + AlarmInstance.FIRED_STATE; final List<AlarmInstance> alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery); AlarmInstance nextAlarm = null; for (AlarmInstance instance : alarmInstances) { if (nextAlarm == null || instance.getAlarmTime().before(nextAlarm.getAlarmTime())) { nextAlarm = instance; } } return nextAlarm; }
/** * Used by dismissed and missed states, to update parent alarm. This will either disable, delete * or reschedule parent alarm. * * @param context application context * @param instance to update parent for */ private static void updateParentAlarm(Context context, AlarmInstance instance) { ContentResolver cr = context.getContentResolver(); Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId); if (alarm == null) { LogUtils.e("Parent has been deleted with instance: " + instance.toString()); return; } if (!alarm.daysOfWeek.isRepeating()) { if (alarm.deleteAfterUse) { LogUtils.i("Deleting parent alarm: " + alarm.id); Alarm.deleteAlarm(cr, alarm.id); } else { LogUtils.i("Disabling parent alarm: " + alarm.id); alarm.enabled = false; Alarm.updateAlarm(cr, alarm); } } else { // Schedule the next repeating instance after the current time AlarmInstance nextRepeatedInstance = alarm.createInstanceAfter(getCurrentTime()); LogUtils.i( "Creating new instance for repeating alarm " + alarm.id + " at " + AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime())); AlarmInstance.addInstance(cr, nextRepeatedInstance); registerInstance(context, nextRepeatedInstance, true); } }
/** Used in L and later devices where "next alarm" is stored in the Alarm Manager. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static void updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm) { // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the // alarm that is going to fire next. The operation is constructed such that it is ignored // by AlarmStateManager. AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); int flags = nextAlarm == null ? PendingIntent.FLAG_NO_CREATE : 0; PendingIntent operation = PendingIntent.getBroadcast( context, 0 /* requestCode */, AlarmStateManager.createIndicatorIntent(context), flags); if (nextAlarm != null) { long alarmTime = nextAlarm.getAlarmTime().getTimeInMillis(); // Create an intent that can be used to show or edit details of the next alarm. PendingIntent viewIntent = PendingIntent.getActivity( context, nextAlarm.hashCode(), AlarmNotifications.createViewAlarmIntent(context, nextAlarm), PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(alarmTime, viewIntent); alarmManager.setAlarmClock(info, operation); } else if (operation != null) { alarmManager.cancel(operation); } }
/** * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update the application * notifications and schedule any state changes that need to occur in the future. * * @param context application context * @param instance to set state to */ public static void setHighNotificationState(Context context, AlarmInstance instance) { LogUtils.v("Setting high notification state to instance " + instance.mId); // Update alarm state in db ContentResolver contentResolver = context.getContentResolver(); instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE; AlarmInstance.updateInstance(contentResolver, instance); // Setup instance notification and scheduling timers AlarmNotifications.showHighPriorityNotification(context, instance); scheduleInstanceStateChange( context, instance.getAlarmTime(), instance, AlarmInstance.FIRED_STATE); }
/** * This will set the alarm instance to the SNOOZE_STATE and update the application notifications * and schedule any state changes that need to occur in the future. * * @param context application context * @param instance to set state to */ public static void setSnoozeState( final Context context, AlarmInstance instance, boolean showToast) { // Stop alarm if this instance is firing it AlarmService.stopAlarm(context, instance); // Calculate the new snooze alarm time String snoozeMinutesStr = PreferenceManager.getDefaultSharedPreferences(context) .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES); final int snoozeMinutes = Integer.parseInt(snoozeMinutesStr); Calendar newAlarmTime = Calendar.getInstance(); newAlarmTime.add(Calendar.MINUTE, snoozeMinutes); // Update alarm state and new alarm time in db. LogUtils.v( "Setting snoozed state to instance " + instance.mId + " for " + AlarmUtils.getFormattedTime(context, newAlarmTime)); instance.setAlarmTime(newAlarmTime); instance.mAlarmState = AlarmInstance.SNOOZE_STATE; AlarmInstance.updateInstance(context.getContentResolver(), instance); // Setup instance notification and scheduling timers AlarmNotifications.showSnoozeNotification(context, instance); scheduleInstanceStateChange( context, instance.getAlarmTime(), instance, AlarmInstance.FIRED_STATE); // Display the snooze minutes in a toast. if (showToast) { final Handler mainHandler = new Handler(context.getMainLooper()); final Runnable myRunnable = new Runnable() { @Override public void run() { String displayTime = String.format( context .getResources() .getQuantityText(R.plurals.alarm_alert_snooze_set, snoozeMinutes) .toString(), snoozeMinutes); Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show(); } }; mainHandler.post(myRunnable); } // Instance time changed, so find next alarm that will fire and notify system updateNextAlarm(context); }
/** Used in pre-L devices, where "next alarm" is stored in system settings. */ private static void updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm) { // Send broadcast message so pre-L AppWidgets will recognize an update String timeString = ""; boolean showStatusIcon = false; if (nextAlarm != null) { timeString = AlarmUtils.getFormattedTime(context, nextAlarm.getAlarmTime()); showStatusIcon = true; } // Set and notify next alarm text to system LogUtils.i("Displaying next alarm time: \'" + timeString + '\''); // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions Settings.System.putString( context.getContentResolver(), Settings.System.NEXT_ALARM_FORMATTED, timeString); Intent alarmChanged = new Intent(SYSTEM_ALARM_CHANGE_ACTION); alarmChanged.putExtra("alarmSet", showStatusIcon); context.sendBroadcast(alarmChanged); }
/** * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state * change to DISMISSED_STATE at the regularly scheduled firing time. * * @param context * @param instance */ public static void setPreDismissState(Context context, AlarmInstance instance) { LogUtils.v("Setting predismissed state to instance " + instance.mId); // Update alarm in db final ContentResolver contentResolver = context.getContentResolver(); instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE; AlarmInstance.updateInstance(contentResolver, instance); // Setup instance notification and scheduling timers AlarmNotifications.clearNotification(context, instance); scheduleInstanceStateChange( context, instance.getAlarmTime(), instance, AlarmInstance.DISMISSED_STATE); final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId); // if it's a one time alarm set the toggle to off if (alarm != null && !alarm.daysOfWeek.isRepeating()) { // Check parent if it needs to reschedule, disable or delete itself if (instance.mAlarmId != null) { updateParentAlarm(context, instance); } } }
@Override public void run() { // only allow on background thread if (Looper.myLooper() == Looper.getMainLooper()) { throw new IllegalStateException("Must be called on a background thread"); } final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE); // if search mode isn't specified show all alarms in the UI picker if (searchMode == null) { mMatchingAlarms.addAll(mAlarms); return; } final ContentResolver cr = mContext.getContentResolver(); switch (searchMode) { case AlarmClock.ALARM_SEARCH_MODE_TIME: // at least one of these has to be specified in this search mode. final int hour = mIntent.getIntExtra(AlarmClock.EXTRA_HOUR, -1); // if minutes weren't specified default to 0 final int minutes = mIntent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0); final Boolean isPm = (Boolean) mIntent.getExtras().get(AlarmClock.EXTRA_IS_PM); boolean badInput = isPm != null && hour > 12 && isPm; badInput |= hour < 0 || hour > 23; badInput |= minutes < 0 || minutes > 59; if (badInput) { final String[] ampm = new DateFormatSymbols().getAmPmStrings(); final String amPm = isPm == null ? "" : (isPm ? ampm[1] : ampm[0]); final String reason = mContext.getString(R.string.invalid_time, hour, minutes, amPm); notifyFailureAndLog(reason, mActivity); return; } final int hour24 = Boolean.TRUE.equals(isPm) && hour < 12 ? (hour + 12) : hour; // there might me multiple alarms at the same time for (Alarm alarm : mAlarms) { if (alarm.hour == hour24 && alarm.minutes == minutes) { mMatchingAlarms.add(alarm); } } if (mMatchingAlarms.isEmpty()) { final String reason = mContext.getString(R.string.no_alarm_at, hour24, minutes); notifyFailureAndLog(reason, mActivity); return; } break; case AlarmClock.ALARM_SEARCH_MODE_NEXT: final AlarmInstance nextAlarm = AlarmStateManager.getNextFiringAlarm(mContext); if (nextAlarm == null) { final String reason = mContext.getString(R.string.no_scheduled_alarms); notifyFailureAndLog(reason, mActivity); return; } // get time from nextAlarm and see if there are any other alarms matching this time final Calendar nextTime = nextAlarm.getAlarmTime(); final List<Alarm> alarmsFiringAtSameTime = getAlarmsByHourMinutes( nextTime.get(Calendar.HOUR_OF_DAY), nextTime.get(Calendar.MINUTE), cr); // there might me multiple alarms firing next mMatchingAlarms.addAll(alarmsFiringAtSameTime); break; case AlarmClock.ALARM_SEARCH_MODE_ALL: mMatchingAlarms.addAll(mAlarms); break; case AlarmClock.ALARM_SEARCH_MODE_LABEL: // EXTRA_MESSAGE has to be set in this mode final String label = mIntent.getStringExtra(AlarmClock.EXTRA_MESSAGE); if (label == null) { final String reason = mContext.getString(R.string.no_label_specified); notifyFailureAndLog(reason, mActivity); return; } // there might me multiple alarms with this label for (Alarm alarm : mAlarms) { if (alarm.label.contains(label)) { mMatchingAlarms.add(alarm); } } if (mMatchingAlarms.isEmpty()) { final String reason = mContext.getString(R.string.no_alarms_with_label); notifyFailureAndLog(reason, mActivity); return; } break; } }
/** * This registers the AlarmInstance to the state manager. This will look at the instance and * choose the most appropriate state to put it in. This is primarily used by new alarms, but it * can also be called when the system time changes. * * <p>Most state changes are handled by the states themselves, but during major time changes we * have to correct the alarm instance state. This means we have to handle special cases as * describe below: * * <ul> * <li>Make sure all dismissed alarms are never re-activated * <li>Make sure pre-dismissed alarms stay predismissed * <li>Make sure firing alarms stayed fired unless they should be auto-silenced * <li>Missed instance that have parents should be re-enabled if we went back in time * <li>If alarm was SNOOZED, then show the notification but don't update time * <li>If low priority notification was hidden, then make sure it stays hidden * </ul> * * If none of these special case are found, then we just check the time and see what is the proper * state for the instance. * * @param context application context * @param instance to register */ public static void registerInstance( Context context, AlarmInstance instance, boolean updateNextAlarm) { final ContentResolver cr = context.getContentResolver(); final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId); final Calendar currentTime = getCurrentTime(); final Calendar alarmTime = instance.getAlarmTime(); final Calendar timeoutTime = instance.getTimeout(context); final Calendar lowNotificationTime = instance.getLowNotificationTime(); final Calendar highNotificationTime = instance.getHighNotificationTime(); final Calendar missedTTL = instance.getMissedTimeToLive(); // Handle special use cases here if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) { // This should never happen, but add a quick check here LogUtils.e("Alarm Instance is dismissed, but never deleted"); setDismissState(context, instance); return; } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) { // Keep alarm firing, unless it should be timed out boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime); if (!hasTimeout) { setFiredState(context, instance); return; } } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) { if (currentTime.before(alarmTime)) { if (instance.mAlarmId == null) { // This instance parent got deleted (ie. deleteAfterUse), so // we should not re-activate it.- setDismissState(context, instance); return; } // TODO: This will re-activate missed snoozed alarms, but will // use our normal notifications. This is not ideal, but very rare use-case. // We should look into fixing this in the future. // Make sure we re-enable the parent alarm of the instance // because it will get activated by by the below code alarm.enabled = true; Alarm.updateAlarm(cr, alarm); } } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) { if (currentTime.before(alarmTime)) { setPreDismissState(context, instance); } else { setDismissState(context, instance); } return; } // Fix states that are time sensitive if (currentTime.after(missedTTL)) { // Alarm is so old, just dismiss it setDismissState(context, instance); } else if (currentTime.after(alarmTime)) { // There is a chance that the TIME_SET occurred right when the alarm should go off, so // we need to add a check to see if we should fire the alarm instead of marking it // missed. Calendar alarmBuffer = Calendar.getInstance(); alarmBuffer.setTime(alarmTime.getTime()); alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER); if (currentTime.before(alarmBuffer)) { setFiredState(context, instance); } else { setMissedState(context, instance); } } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) { // We only want to display snooze notification and not update the time, // so handle showing the notification directly AlarmNotifications.showSnoozeNotification(context, instance); scheduleInstanceStateChange( context, instance.getAlarmTime(), instance, AlarmInstance.FIRED_STATE); } else if (currentTime.after(highNotificationTime)) { setHighNotificationState(context, instance); } else if (currentTime.after(lowNotificationTime)) { // Only show low notification if it wasn't hidden in the past if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) { setHideNotificationState(context, instance); } else { setLowNotificationState(context, instance); } } else { // Alarm is still active, so initialize as a silent alarm setSilentState(context, instance); } // The caller prefers to handle updateNextAlarm for optimization if (updateNextAlarm) { updateNextAlarm(context); } }