• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.deskclock.alarms;
17 
18 import android.annotation.TargetApi;
19 import android.app.AlarmManager;
20 import android.app.AlarmManager.AlarmClockInfo;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.os.Handler;
29 import android.os.PowerManager;
30 import android.provider.Settings;
31 import androidx.core.app.NotificationManagerCompat;
32 import android.text.format.DateFormat;
33 import android.widget.Toast;
34 
35 import com.android.deskclock.AlarmAlertWakeLock;
36 import com.android.deskclock.AlarmClockFragment;
37 import com.android.deskclock.AlarmUtils;
38 import com.android.deskclock.AsyncHandler;
39 import com.android.deskclock.DeskClock;
40 import com.android.deskclock.LogUtils;
41 import com.android.deskclock.R;
42 import com.android.deskclock.Utils;
43 import com.android.deskclock.data.DataModel;
44 import com.android.deskclock.events.Events;
45 import com.android.deskclock.provider.Alarm;
46 import com.android.deskclock.provider.AlarmInstance;
47 
48 import java.util.Calendar;
49 import java.util.Collections;
50 import java.util.Comparator;
51 import java.util.List;
52 
53 import static android.content.Context.ALARM_SERVICE;
54 import static android.provider.Settings.System.NEXT_ALARM_FORMATTED;
55 
56 /**
57  * This class handles all the state changes for alarm instances. You need to
58  * register all alarm instances with the state manager if you want them to
59  * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE),
60  * then you must also re-register instances to fix their states.
61  *
62  * Please see {@link #registerInstance) for special transitions when major time changes
63  * occur.
64  *
65  * Following states:
66  *
67  * SILENT_STATE:
68  * This state is used when the alarm is activated, but doesn't need to display anything. It
69  * is in charge of changing the alarm instance state to a LOW_NOTIFICATION_STATE.
70  *
71  * LOW_NOTIFICATION_STATE:
72  * This state is used to notify the user that the alarm will go off
73  * {@link AlarmInstance#LOW_NOTIFICATION_HOUR_OFFSET}. This
74  * state handles the state changes to HIGH_NOTIFICATION_STATE, HIDE_NOTIFICATION_STATE and
75  * DISMISS_STATE.
76  *
77  * HIDE_NOTIFICATION_STATE:
78  * This is a transient state of the LOW_NOTIFICATION_STATE, where the user wants to hide the
79  * notification. This will sit and wait until the HIGH_PRIORITY_NOTIFICATION should go off.
80  *
81  * HIGH_NOTIFICATION_STATE:
82  * This state behaves like the LOW_NOTIFICATION_STATE, but doesn't allow the user to hide it.
83  * This state is in charge of triggering a FIRED_STATE or DISMISS_STATE.
84  *
85  * SNOOZED_STATE:
86  * The SNOOZED_STATE behaves like a HIGH_NOTIFICATION_STATE, but with a different message. It
87  * also increments the alarm time in the instance to reflect the new snooze time.
88  *
89  * FIRED_STATE:
90  * The FIRED_STATE is used when the alarm is firing. It will start the AlarmService, and wait
91  * until the user interacts with the alarm via SNOOZED_STATE or DISMISS_STATE change. If the user
92  * doesn't then it might be change to MISSED_STATE if auto-silenced was enabled.
93  *
94  * MISSED_STATE:
95  * The MISSED_STATE is used when the alarm already fired, but the user could not interact with
96  * it. At this point the alarm instance is dead and we check the parent alarm to see if we need
97  * to disable or schedule a new alarm_instance. There is also a notification shown to the user
98  * that he/she missed the alarm and that stays for
99  * {@link AlarmInstance#MISSED_TIME_TO_LIVE_HOUR_OFFSET} or until the user acknownledges it.
100  *
101  * DISMISS_STATE:
102  * This is really a transient state that will properly delete the alarm instance. Use this state,
103  * whenever you want to get rid of the alarm instance. This state will also check the alarm
104  * parent to see if it should disable or schedule a new alarm instance.
105  */
106 public final class AlarmStateManager extends BroadcastReceiver {
107     // Intent action to trigger an instance state change.
108     public static final String CHANGE_STATE_ACTION = "change_state";
109 
110     // Intent action to show the alarm and dismiss the instance
111     public static final String SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm";
112 
113     // Intent action for an AlarmManager alarm serving only to set the next alarm indicators
114     private static final String INDICATOR_ACTION = "indicator";
115 
116     // System intent action to notify AppWidget that we changed the alarm text.
117     public static final String ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED";
118 
119     // Extra key to set the desired state change.
120     public static final String ALARM_STATE_EXTRA = "intent.extra.alarm.state";
121 
122     // Extra key to indicate the state change was launched from a notification.
123     public static final String FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification";
124 
125     // Extra key to set the global broadcast id.
126     private static final String ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id";
127 
128     // Intent category tags used to dismiss, snooze or delete an alarm
129     public static final String ALARM_DISMISS_TAG = "DISMISS_TAG";
130     public static final String ALARM_SNOOZE_TAG = "SNOOZE_TAG";
131     public static final String ALARM_DELETE_TAG = "DELETE_TAG";
132 
133     // Intent category tag used when schedule state change intents in alarm manager.
134     private static final String ALARM_MANAGER_TAG = "ALARM_MANAGER";
135 
136     // Buffer time in seconds to fire alarm instead of marking it missed.
137     public static final int ALARM_FIRE_BUFFER = 15;
138 
139     // A factory for the current time; can be mocked for testing purposes.
140     private static CurrentTimeFactory sCurrentTimeFactory;
141 
142     // Schedules alarm state transitions; can be mocked for testing purposes.
143     private static StateChangeScheduler sStateChangeScheduler =
144             new AlarmManagerStateChangeScheduler();
145 
getCurrentTime()146     private static Calendar getCurrentTime() {
147         return sCurrentTimeFactory == null
148                 ? DataModel.getDataModel().getCalendar()
149                 : sCurrentTimeFactory.getCurrentTime();
150     }
151 
setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory)152     static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) {
153         sCurrentTimeFactory = currentTimeFactory;
154     }
155 
setStateChangeScheduler(StateChangeScheduler stateChangeScheduler)156     static void setStateChangeScheduler(StateChangeScheduler stateChangeScheduler) {
157         if (stateChangeScheduler == null) {
158             stateChangeScheduler = new AlarmManagerStateChangeScheduler();
159         }
160         sStateChangeScheduler = stateChangeScheduler;
161     }
162 
163     /**
164      * Update the next alarm stored in framework. This value is also displayed in digital widgets
165      * and the clock tab in this app.
166      */
updateNextAlarm(Context context)167     private static void updateNextAlarm(Context context) {
168         final AlarmInstance nextAlarm = getNextFiringAlarm(context);
169 
170         if (Utils.isPreL()) {
171             updateNextAlarmInSystemSettings(context, nextAlarm);
172         } else {
173             updateNextAlarmInAlarmManager(context, nextAlarm);
174         }
175     }
176 
177     /**
178      * Returns an alarm instance of an alarm that's going to fire next.
179      *
180      * @param context application context
181      * @return an alarm instance that will fire earliest relative to current time.
182      */
getNextFiringAlarm(Context context)183     public static AlarmInstance getNextFiringAlarm(Context context) {
184         final ContentResolver cr = context.getContentResolver();
185         final String activeAlarmQuery = AlarmInstance.ALARM_STATE + "<" + AlarmInstance.FIRED_STATE;
186         final List<AlarmInstance> alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery);
187 
188         AlarmInstance nextAlarm = null;
189         for (AlarmInstance instance : alarmInstances) {
190             if (nextAlarm == null || instance.getAlarmTime().before(nextAlarm.getAlarmTime())) {
191                 nextAlarm = instance;
192             }
193         }
194         return nextAlarm;
195     }
196 
197     /**
198      * Used in pre-L devices, where "next alarm" is stored in system settings.
199      */
200     @SuppressWarnings("deprecation")
201     @TargetApi(Build.VERSION_CODES.KITKAT)
updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm)202     private static void updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm) {
203         // Format the next alarm time if an alarm is scheduled.
204         String time = "";
205         if (nextAlarm != null) {
206             time = AlarmUtils.getFormattedTime(context, nextAlarm.getAlarmTime());
207         }
208 
209         try {
210             // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions
211             Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time);
212 
213             LogUtils.i("Updated next alarm time to: \'" + time + '\'');
214 
215             // Send broadcast message so pre-L AppWidgets will recognize an update.
216             context.sendBroadcast(new Intent(ACTION_ALARM_CHANGED));
217         } catch (SecurityException se) {
218             // The user has most likely revoked WRITE_SETTINGS.
219             LogUtils.e("Unable to update next alarm to: \'" + time + '\'', se);
220         }
221     }
222 
223     /**
224      * Used in L and later devices where "next alarm" is stored in the Alarm Manager.
225      */
226     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm)227     private static void updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm) {
228         // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the
229         // alarm that is going to fire next. The operation is constructed such that it is ignored
230         // by AlarmStateManager.
231 
232         final AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
233 
234         final int flags = nextAlarm == null ? PendingIntent.FLAG_NO_CREATE : 0;
235         final PendingIntent operation = PendingIntent.getBroadcast(context, 0 /* requestCode */,
236                 AlarmStateManager.createIndicatorIntent(context), flags);
237 
238         if (nextAlarm != null) {
239             LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId);
240             long alarmTime = nextAlarm.getAlarmTime().getTimeInMillis();
241 
242             // Create an intent that can be used to show or edit details of the next alarm.
243             PendingIntent viewIntent = PendingIntent.getActivity(context, nextAlarm.hashCode(),
244                     AlarmNotifications.createViewAlarmIntent(context, nextAlarm),
245                     PendingIntent.FLAG_UPDATE_CURRENT);
246 
247             final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent);
248             Utils.updateNextAlarm(alarmManager, info, operation);
249         } else if (operation != null) {
250             LogUtils.i("Canceling upcoming AlarmClockInfo");
251             alarmManager.cancel(operation);
252         }
253     }
254 
255     /**
256      * Used by dismissed and missed states, to update parent alarm. This will either
257      * disable, delete or reschedule parent alarm.
258      *
259      * @param context  application context
260      * @param instance to update parent for
261      */
updateParentAlarm(Context context, AlarmInstance instance)262     private static void updateParentAlarm(Context context, AlarmInstance instance) {
263         ContentResolver cr = context.getContentResolver();
264         Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
265         if (alarm == null) {
266             LogUtils.e("Parent has been deleted with instance: " + instance.toString());
267             return;
268         }
269 
270         if (!alarm.daysOfWeek.isRepeating()) {
271             if (alarm.deleteAfterUse) {
272                 LogUtils.i("Deleting parent alarm: " + alarm.id);
273                 Alarm.deleteAlarm(cr, alarm.id);
274             } else {
275                 LogUtils.i("Disabling parent alarm: " + alarm.id);
276                 alarm.enabled = false;
277                 Alarm.updateAlarm(cr, alarm);
278             }
279         } else {
280             // Schedule the next repeating instance which may be before the current instance if a
281             // time jump has occurred. Otherwise, if the current instance is the next instance
282             // and has already been fired, schedule the subsequent instance.
283             AlarmInstance nextRepeatedInstance = alarm.createInstanceAfter(getCurrentTime());
284             if (instance.mAlarmState > AlarmInstance.FIRED_STATE
285                     && nextRepeatedInstance.getAlarmTime().equals(instance.getAlarmTime())) {
286                 nextRepeatedInstance = alarm.createInstanceAfter(instance.getAlarmTime());
287             }
288 
289             LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " +
290                     AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime()));
291             AlarmInstance.addInstance(cr, nextRepeatedInstance);
292             registerInstance(context, nextRepeatedInstance, true);
293         }
294     }
295 
296     /**
297      * Utility method to create a proper change state intent.
298      *
299      * @param context  application context
300      * @param tag      used to make intent differ from other state change intents.
301      * @param instance to change state to
302      * @param state    to change to.
303      * @return intent that can be used to change an alarm instance state
304      */
createStateChangeIntent(Context context, String tag, AlarmInstance instance, Integer state)305     public static Intent createStateChangeIntent(Context context, String tag,
306             AlarmInstance instance, Integer state) {
307         // This intent is directed to AlarmService, though the actual handling of it occurs here
308         // in AlarmStateManager. The reason is that evidence exists showing the jump between the
309         // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by the
310         // Out Of Memory killer. If clock is killed during that jump, firing an alarm can fail to
311         // occur. To be safer, the call begins in AlarmService, which has the power to display the
312         // firing alarm if needed, so no jump is needed.
313         Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId);
314         intent.setAction(CHANGE_STATE_ACTION);
315         intent.addCategory(tag);
316         intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.getDataModel().getGlobalIntentId());
317         if (state != null) {
318             intent.putExtra(ALARM_STATE_EXTRA, state.intValue());
319         }
320         return intent;
321     }
322 
323     /**
324      * Schedule alarm instance state changes with {@link AlarmManager}.
325      *
326      * @param ctx      application context
327      * @param time     to trigger state change
328      * @param instance to change state to
329      * @param newState to change to
330      */
scheduleInstanceStateChange(Context ctx, Calendar time, AlarmInstance instance, int newState)331     private static void scheduleInstanceStateChange(Context ctx, Calendar time,
332             AlarmInstance instance, int newState) {
333         sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState);
334     }
335 
336     /**
337      * Cancel all {@link AlarmManager} timers for instance.
338      *
339      * @param ctx      application context
340      * @param instance to disable all {@link AlarmManager} timers
341      */
cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance)342     private static void cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance) {
343         sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance);
344     }
345 
346 
347     /**
348      * This will set the alarm instance to the SILENT_STATE and update
349      * the application notifications and schedule any state changes that need
350      * to occur in the future.
351      *
352      * @param context  application context
353      * @param instance to set state to
354      */
setSilentState(Context context, AlarmInstance instance)355     public static void setSilentState(Context context, AlarmInstance instance) {
356         LogUtils.i("Setting silent state to instance " + instance.mId);
357 
358         // Update alarm in db
359         ContentResolver contentResolver = context.getContentResolver();
360         instance.mAlarmState = AlarmInstance.SILENT_STATE;
361         AlarmInstance.updateInstance(contentResolver, instance);
362 
363         // Setup instance notification and scheduling timers
364         AlarmNotifications.clearNotification(context, instance);
365         scheduleInstanceStateChange(context, instance.getLowNotificationTime(),
366                 instance, AlarmInstance.LOW_NOTIFICATION_STATE);
367     }
368 
369     /**
370      * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update
371      * the application notifications and schedule any state changes that need
372      * to occur in the future.
373      *
374      * @param context  application context
375      * @param instance to set state to
376      */
setLowNotificationState(Context context, AlarmInstance instance)377     public static void setLowNotificationState(Context context, AlarmInstance instance) {
378         LogUtils.i("Setting low notification state to instance " + instance.mId);
379 
380         // Update alarm state in db
381         ContentResolver contentResolver = context.getContentResolver();
382         instance.mAlarmState = AlarmInstance.LOW_NOTIFICATION_STATE;
383         AlarmInstance.updateInstance(contentResolver, instance);
384 
385         // Setup instance notification and scheduling timers
386         AlarmNotifications.showLowPriorityNotification(context, instance);
387         scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
388                 instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
389     }
390 
391     /**
392      * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update
393      * the application notifications and schedule any state changes that need
394      * to occur in the future.
395      *
396      * @param context  application context
397      * @param instance to set state to
398      */
setHideNotificationState(Context context, AlarmInstance instance)399     public static void setHideNotificationState(Context context, AlarmInstance instance) {
400         LogUtils.i("Setting hide notification state to instance " + instance.mId);
401 
402         // Update alarm state in db
403         ContentResolver contentResolver = context.getContentResolver();
404         instance.mAlarmState = AlarmInstance.HIDE_NOTIFICATION_STATE;
405         AlarmInstance.updateInstance(contentResolver, instance);
406 
407         // Setup instance notification and scheduling timers
408         AlarmNotifications.clearNotification(context, instance);
409         scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
410                 instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
411     }
412 
413     /**
414      * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update
415      * the application notifications and schedule any state changes that need
416      * to occur in the future.
417      *
418      * @param context  application context
419      * @param instance to set state to
420      */
setHighNotificationState(Context context, AlarmInstance instance)421     public static void setHighNotificationState(Context context, AlarmInstance instance) {
422         LogUtils.i("Setting high notification state to instance " + instance.mId);
423 
424         // Update alarm state in db
425         ContentResolver contentResolver = context.getContentResolver();
426         instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE;
427         AlarmInstance.updateInstance(contentResolver, instance);
428 
429         // Setup instance notification and scheduling timers
430         AlarmNotifications.showHighPriorityNotification(context, instance);
431         scheduleInstanceStateChange(context, instance.getAlarmTime(),
432                 instance, AlarmInstance.FIRED_STATE);
433     }
434 
435     /**
436      * This will set the alarm instance to the FIRED_STATE and update
437      * the application notifications and schedule any state changes that need
438      * to occur in the future.
439      *
440      * @param context  application context
441      * @param instance to set state to
442      */
setFiredState(Context context, AlarmInstance instance)443     public static void setFiredState(Context context, AlarmInstance instance) {
444         LogUtils.i("Setting fire state to instance " + instance.mId);
445 
446         // Update alarm state in db
447         ContentResolver contentResolver = context.getContentResolver();
448         instance.mAlarmState = AlarmInstance.FIRED_STATE;
449         AlarmInstance.updateInstance(contentResolver, instance);
450 
451         if (instance.mAlarmId != null) {
452             // if the time changed *backward* and pushed an instance from missed back to fired,
453             // remove any other scheduled instances that may exist
454             AlarmInstance.deleteOtherInstances(context, contentResolver, instance.mAlarmId,
455                     instance.mId);
456         }
457 
458         Events.sendAlarmEvent(R.string.action_fire, 0);
459 
460         Calendar timeout = instance.getTimeout();
461         if (timeout != null) {
462             scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE);
463         }
464 
465         // Instance not valid anymore, so find next alarm that will fire and notify system
466         updateNextAlarm(context);
467     }
468 
469     /**
470      * This will set the alarm instance to the SNOOZE_STATE and update
471      * the application notifications and schedule any state changes that need
472      * to occur in the future.
473      *
474      * @param context  application context
475      * @param instance to set state to
476      */
setSnoozeState(final Context context, AlarmInstance instance, boolean showToast)477     public static void setSnoozeState(final Context context, AlarmInstance instance,
478             boolean showToast) {
479         // Stop alarm if this instance is firing it
480         AlarmService.stopAlarm(context, instance);
481 
482         // Calculate the new snooze alarm time
483         final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
484         Calendar newAlarmTime = Calendar.getInstance();
485         newAlarmTime.add(Calendar.MINUTE, snoozeMinutes);
486 
487         // Update alarm state and new alarm time in db.
488         LogUtils.i("Setting snoozed state to instance " + instance.mId + " for "
489                 + AlarmUtils.getFormattedTime(context, newAlarmTime));
490         instance.setAlarmTime(newAlarmTime);
491         instance.mAlarmState = AlarmInstance.SNOOZE_STATE;
492         AlarmInstance.updateInstance(context.getContentResolver(), instance);
493 
494         // Setup instance notification and scheduling timers
495         AlarmNotifications.showSnoozeNotification(context, instance);
496         scheduleInstanceStateChange(context, instance.getAlarmTime(),
497                 instance, AlarmInstance.FIRED_STATE);
498 
499         // Display the snooze minutes in a toast.
500         if (showToast) {
501             final Handler mainHandler = new Handler(context.getMainLooper());
502             final Runnable myRunnable = new Runnable() {
503                 @Override
504                 public void run() {
505                     String displayTime = String.format(context.getResources().getQuantityText
506                             (R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(),
507                             snoozeMinutes);
508                     Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show();
509                 }
510             };
511             mainHandler.post(myRunnable);
512         }
513 
514         // Instance time changed, so find next alarm that will fire and notify system
515         updateNextAlarm(context);
516     }
517 
518     /**
519      * This will set the alarm instance to the MISSED_STATE and update
520      * the application notifications and schedule any state changes that need
521      * to occur in the future.
522      *
523      * @param context  application context
524      * @param instance to set state to
525      */
setMissedState(Context context, AlarmInstance instance)526     public static void setMissedState(Context context, AlarmInstance instance) {
527         LogUtils.i("Setting missed state to instance " + instance.mId);
528         // Stop alarm if this instance is firing it
529         AlarmService.stopAlarm(context, instance);
530 
531         // Check parent if it needs to reschedule, disable or delete itself
532         if (instance.mAlarmId != null) {
533             updateParentAlarm(context, instance);
534         }
535 
536         // Update alarm state
537         ContentResolver contentResolver = context.getContentResolver();
538         instance.mAlarmState = AlarmInstance.MISSED_STATE;
539         AlarmInstance.updateInstance(contentResolver, instance);
540 
541         // Setup instance notification and scheduling timers
542         AlarmNotifications.showMissedNotification(context, instance);
543         scheduleInstanceStateChange(context, instance.getMissedTimeToLive(),
544                 instance, AlarmInstance.DISMISSED_STATE);
545 
546         // Instance is not valid anymore, so find next alarm that will fire and notify system
547         updateNextAlarm(context);
548     }
549 
550     /**
551      * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state
552      * change to DISMISSED_STATE at the regularly scheduled firing time.
553      *
554      * @param context  application context
555      * @param instance to set state to
556      */
setPreDismissState(Context context, AlarmInstance instance)557     public static void setPreDismissState(Context context, AlarmInstance instance) {
558         LogUtils.i("Setting predismissed state to instance " + instance.mId);
559 
560         // Update alarm in db
561         final ContentResolver contentResolver = context.getContentResolver();
562         instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE;
563         AlarmInstance.updateInstance(contentResolver, instance);
564 
565         // Setup instance notification and scheduling timers
566         AlarmNotifications.clearNotification(context, instance);
567         scheduleInstanceStateChange(context, instance.getAlarmTime(), instance,
568                 AlarmInstance.DISMISSED_STATE);
569 
570         // Check parent if it needs to reschedule, disable or delete itself
571         if (instance.mAlarmId != null) {
572             updateParentAlarm(context, instance);
573         }
574 
575         updateNextAlarm(context);
576     }
577 
578     /**
579      * This just sets the alarm instance to DISMISSED_STATE.
580      */
setDismissState(Context context, AlarmInstance instance)581     public static void setDismissState(Context context, AlarmInstance instance) {
582         LogUtils.i("Setting dismissed state to instance " + instance.mId);
583         instance.mAlarmState = AlarmInstance.DISMISSED_STATE;
584         final ContentResolver contentResolver = context.getContentResolver();
585         AlarmInstance.updateInstance(contentResolver, instance);
586     }
587 
588     /**
589      * This will delete the alarm instance, update the application notifications, and schedule
590      * any state changes that need to occur in the future.
591      *
592      * @param context  application context
593      * @param instance to set state to
594      */
deleteInstanceAndUpdateParent(Context context, AlarmInstance instance)595     public static void deleteInstanceAndUpdateParent(Context context, AlarmInstance instance) {
596         LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm.");
597 
598         // Remove all other timers and notifications associated to it
599         unregisterInstance(context, instance);
600 
601         // Check parent if it needs to reschedule, disable or delete itself
602         if (instance.mAlarmId != null) {
603             updateParentAlarm(context, instance);
604         }
605 
606         // Delete instance as it is not needed anymore
607         AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
608 
609         // Instance is not valid anymore, so find next alarm that will fire and notify system
610         updateNextAlarm(context);
611     }
612 
613     /**
614      * This will set the instance state to DISMISSED_STATE and remove its notifications and
615      * alarm timers.
616      *
617      * @param context  application context
618      * @param instance to unregister
619      */
unregisterInstance(Context context, AlarmInstance instance)620     public static void unregisterInstance(Context context, AlarmInstance instance) {
621         LogUtils.i("Unregistering instance " + instance.mId);
622         // Stop alarm if this instance is firing it
623         AlarmService.stopAlarm(context, instance);
624         AlarmNotifications.clearNotification(context, instance);
625         cancelScheduledInstanceStateChange(context, instance);
626         setDismissState(context, instance);
627     }
628 
629     /**
630      * This registers the AlarmInstance to the state manager. This will look at the instance
631      * and choose the most appropriate state to put it in. This is primarily used by new
632      * alarms, but it can also be called when the system time changes.
633      *
634      * Most state changes are handled by the states themselves, but during major time changes we
635      * have to correct the alarm instance state. This means we have to handle special cases as
636      * describe below:
637      *
638      * <ul>
639      *     <li>Make sure all dismissed alarms are never re-activated</li>
640      *     <li>Make sure pre-dismissed alarms stay predismissed</li>
641      *     <li>Make sure firing alarms stayed fired unless they should be auto-silenced</li>
642      *     <li>Missed instance that have parents should be re-enabled if we went back in time</li>
643      *     <li>If alarm was SNOOZED, then show the notification but don't update time</li>
644      *     <li>If low priority notification was hidden, then make sure it stays hidden</li>
645      * </ul>
646      *
647      * If none of these special case are found, then we just check the time and see what is the
648      * proper state for the instance.
649      *
650      * @param context  application context
651      * @param instance to register
652      */
registerInstance(Context context, AlarmInstance instance, boolean updateNextAlarm)653     public static void registerInstance(Context context, AlarmInstance instance,
654             boolean updateNextAlarm) {
655         LogUtils.i("Registering instance: " + instance.mId);
656         final ContentResolver cr = context.getContentResolver();
657         final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
658         final Calendar currentTime = getCurrentTime();
659         final Calendar alarmTime = instance.getAlarmTime();
660         final Calendar timeoutTime = instance.getTimeout();
661         final Calendar lowNotificationTime = instance.getLowNotificationTime();
662         final Calendar highNotificationTime = instance.getHighNotificationTime();
663         final Calendar missedTTL = instance.getMissedTimeToLive();
664 
665         // Handle special use cases here
666         if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) {
667             // This should never happen, but add a quick check here
668             LogUtils.e("Alarm Instance is dismissed, but never deleted");
669             deleteInstanceAndUpdateParent(context, instance);
670             return;
671         } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) {
672             // Keep alarm firing, unless it should be timed out
673             boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime);
674             if (!hasTimeout) {
675                 setFiredState(context, instance);
676                 return;
677             }
678         } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) {
679             if (currentTime.before(alarmTime)) {
680                 if (instance.mAlarmId == null) {
681                     LogUtils.i("Cannot restore missed instance for one-time alarm");
682                     // This instance parent got deleted (ie. deleteAfterUse), so
683                     // we should not re-activate it.-
684                     deleteInstanceAndUpdateParent(context, instance);
685                     return;
686                 }
687 
688                 // TODO: This will re-activate missed snoozed alarms, but will
689                 // use our normal notifications. This is not ideal, but very rare use-case.
690                 // We should look into fixing this in the future.
691 
692                 // Make sure we re-enable the parent alarm of the instance
693                 // because it will get activated by by the below code
694                 alarm.enabled = true;
695                 Alarm.updateAlarm(cr, alarm);
696             }
697         } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) {
698             if (currentTime.before(alarmTime)) {
699                 setPreDismissState(context, instance);
700             } else {
701                 deleteInstanceAndUpdateParent(context, instance);
702             }
703             return;
704         }
705 
706         // Fix states that are time sensitive
707         if (currentTime.after(missedTTL)) {
708             // Alarm is so old, just dismiss it
709             deleteInstanceAndUpdateParent(context, instance);
710         } else if (currentTime.after(alarmTime)) {
711             // There is a chance that the TIME_SET occurred right when the alarm should go off, so
712             // we need to add a check to see if we should fire the alarm instead of marking it
713             // missed.
714             Calendar alarmBuffer = Calendar.getInstance();
715             alarmBuffer.setTime(alarmTime.getTime());
716             alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER);
717             if (currentTime.before(alarmBuffer)) {
718                 setFiredState(context, instance);
719             } else {
720                 setMissedState(context, instance);
721             }
722         } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
723             // We only want to display snooze notification and not update the time,
724             // so handle showing the notification directly
725             AlarmNotifications.showSnoozeNotification(context, instance);
726             scheduleInstanceStateChange(context, instance.getAlarmTime(),
727                     instance, AlarmInstance.FIRED_STATE);
728         } else if (currentTime.after(highNotificationTime)) {
729             setHighNotificationState(context, instance);
730         } else if (currentTime.after(lowNotificationTime)) {
731             // Only show low notification if it wasn't hidden in the past
732             if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) {
733                 setHideNotificationState(context, instance);
734             } else {
735                 setLowNotificationState(context, instance);
736             }
737         } else {
738             // Alarm is still active, so initialize as a silent alarm
739             setSilentState(context, instance);
740         }
741 
742         // The caller prefers to handle updateNextAlarm for optimization
743         if (updateNextAlarm) {
744             updateNextAlarm(context);
745         }
746     }
747 
748     /**
749      * This will delete and unregister all instances associated with alarmId, without affect
750      * the alarm itself. This should be used whenever modifying or deleting an alarm.
751      *
752      * @param context application context
753      * @param alarmId to find instances to delete.
754      */
deleteAllInstances(Context context, long alarmId)755     public static void deleteAllInstances(Context context, long alarmId) {
756         LogUtils.i("Deleting all instances of alarm: " + alarmId);
757         ContentResolver cr = context.getContentResolver();
758         List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
759         for (AlarmInstance instance : instances) {
760             unregisterInstance(context, instance);
761             AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
762         }
763         updateNextAlarm(context);
764     }
765 
766     /**
767      * Delete and unregister all instances unless they are snoozed. This is used whenever an alarm
768      * is modified superficially (label, vibrate, or ringtone change).
769      */
deleteNonSnoozeInstances(Context context, long alarmId)770     public static void deleteNonSnoozeInstances(Context context, long alarmId) {
771         LogUtils.i("Deleting all non-snooze instances of alarm: " + alarmId);
772         ContentResolver cr = context.getContentResolver();
773         List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
774         for (AlarmInstance instance : instances) {
775             if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
776                 continue;
777             }
778             unregisterInstance(context, instance);
779             AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
780         }
781         updateNextAlarm(context);
782     }
783 
784     /**
785      * Fix and update all alarm instance when a time change event occurs.
786      *
787      * @param context application context
788      */
fixAlarmInstances(Context context)789     public static void fixAlarmInstances(Context context) {
790         LogUtils.i("Fixing alarm instances");
791         // Register all instances after major time changes or when phone restarts
792         final ContentResolver contentResolver = context.getContentResolver();
793         final Calendar currentTime = getCurrentTime();
794 
795         // Sort the instances in reverse chronological order so that later instances are fixed or
796         // deleted before re-scheduling prior instances (which may re-create or update the later
797         // instances).
798         final List<AlarmInstance> instances = AlarmInstance.getInstances(
799                 contentResolver, null /* selection */);
800         Collections.sort(instances, new Comparator<AlarmInstance>() {
801             @Override
802             public int compare(AlarmInstance lhs, AlarmInstance rhs) {
803                 return rhs.getAlarmTime().compareTo(lhs.getAlarmTime());
804             }
805         });
806 
807         for (AlarmInstance instance : instances) {
808             final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId);
809             if (alarm == null) {
810                 unregisterInstance(context, instance);
811                 AlarmInstance.deleteInstance(contentResolver, instance.mId);
812                 LogUtils.e("Found instance without matching alarm; deleting instance %s", instance);
813                 continue;
814             }
815             final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime());
816             final Calendar missedTTLTime = instance.getMissedTimeToLive();
817             if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) {
818                 final Calendar oldAlarmTime = instance.getAlarmTime();
819                 final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime);
820                 final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime);
821                 final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime);
822                 LogUtils.i("A time change has caused an existing alarm scheduled to fire at %s to" +
823                         " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime);
824 
825                 // The time change is so dramatic the AlarmInstance doesn't make any sense;
826                 // remove it and schedule the new appropriate instance.
827                 AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
828             } else {
829                 registerInstance(context, instance, false /* updateNextAlarm */);
830             }
831         }
832 
833         updateNextAlarm(context);
834     }
835 
836     /**
837      * Utility method to set alarm instance state via constants.
838      *
839      * @param context  application context
840      * @param instance to change state on
841      * @param state    to change to
842      */
setAlarmState(Context context, AlarmInstance instance, int state)843     private static void setAlarmState(Context context, AlarmInstance instance, int state) {
844         if (instance == null) {
845             LogUtils.e("Null alarm instance while setting state to %d", state);
846             return;
847         }
848         switch (state) {
849             case AlarmInstance.SILENT_STATE:
850                 setSilentState(context, instance);
851                 break;
852             case AlarmInstance.LOW_NOTIFICATION_STATE:
853                 setLowNotificationState(context, instance);
854                 break;
855             case AlarmInstance.HIDE_NOTIFICATION_STATE:
856                 setHideNotificationState(context, instance);
857                 break;
858             case AlarmInstance.HIGH_NOTIFICATION_STATE:
859                 setHighNotificationState(context, instance);
860                 break;
861             case AlarmInstance.FIRED_STATE:
862                 setFiredState(context, instance);
863                 break;
864             case AlarmInstance.SNOOZE_STATE:
865                 setSnoozeState(context, instance, true /* showToast */);
866                 break;
867             case AlarmInstance.MISSED_STATE:
868                 setMissedState(context, instance);
869                 break;
870             case AlarmInstance.PREDISMISSED_STATE:
871                 setPreDismissState(context, instance);
872                 break;
873             case AlarmInstance.DISMISSED_STATE:
874                 deleteInstanceAndUpdateParent(context, instance);
875                 break;
876             default:
877                 LogUtils.e("Trying to change to unknown alarm state: " + state);
878         }
879     }
880 
881     @Override
onReceive(final Context context, final Intent intent)882     public void onReceive(final Context context, final Intent intent) {
883         if (INDICATOR_ACTION.equals(intent.getAction())) {
884             return;
885         }
886 
887         final PendingResult result = goAsync();
888         final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
889         wl.acquire();
890         AsyncHandler.post(new Runnable() {
891             @Override
892             public void run() {
893                 handleIntent(context, intent);
894                 result.finish();
895                 wl.release();
896             }
897         });
898     }
899 
handleIntent(Context context, Intent intent)900     public static void handleIntent(Context context, Intent intent) {
901         final String action = intent.getAction();
902         LogUtils.v("AlarmStateManager received intent " + intent);
903         if (CHANGE_STATE_ACTION.equals(action)) {
904             Uri uri = intent.getData();
905             AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
906                     AlarmInstance.getId(uri));
907             if (instance == null) {
908                 LogUtils.e("Can not change state for unknown instance: " + uri);
909                 return;
910             }
911 
912             int globalId = DataModel.getDataModel().getGlobalIntentId();
913             int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1);
914             int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1);
915             if (intentId != globalId) {
916                 LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " +
917                         alarmState);
918                 // Allows dismiss/snooze requests to go through
919                 if (!intent.hasCategory(ALARM_DISMISS_TAG) &&
920                         !intent.hasCategory(ALARM_SNOOZE_TAG)) {
921                     LogUtils.i("Ignoring old Intent");
922                     return;
923                 }
924             }
925 
926             if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
927                 if (intent.hasCategory(ALARM_DISMISS_TAG)) {
928                     Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification);
929                 } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) {
930                     Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification);
931                 }
932             }
933 
934             if (alarmState >= 0) {
935                 setAlarmState(context, instance, alarmState);
936             } else {
937                 registerInstance(context, instance, true);
938             }
939         } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) {
940             Uri uri = intent.getData();
941             AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
942                     AlarmInstance.getId(uri));
943 
944             if (instance == null) {
945                 LogUtils.e("Null alarminstance for SHOW_AND_DISMISS");
946                 // dismiss the notification
947                 final int id = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1);
948                 if (id != -1) {
949                     NotificationManagerCompat.from(context).cancel(id);
950                 }
951                 return;
952             }
953 
954             long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
955             final Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId)
956                     .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
957                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
958 
959             // Open DeskClock which is now positioned on the alarms tab.
960             context.startActivity(viewAlarmIntent);
961 
962             deleteInstanceAndUpdateParent(context, instance);
963         }
964     }
965 
966     /**
967      * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm
968      * indicators.
969      */
createIndicatorIntent(Context context)970     public static Intent createIndicatorIntent(Context context) {
971         return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION);
972     }
973 
974     /**
975      * Abstract away how the current time is computed. If no implementation of this interface is
976      * given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory
977      * instance is consulted for the current time.
978      */
979     interface CurrentTimeFactory {
getCurrentTime()980         Calendar getCurrentTime();
981     }
982 
983     /**
984      * Abstracts away how state changes are scheduled. The {@link AlarmManagerStateChangeScheduler}
985      * implementation schedules callbacks within the system AlarmManager. Alternate
986      * implementations, such as test case mocks can subvert this behavior.
987      */
988     interface StateChangeScheduler {
scheduleInstanceStateChange(Context context, Calendar time, AlarmInstance instance, int newState)989         void scheduleInstanceStateChange(Context context, Calendar time,
990                 AlarmInstance instance, int newState);
991 
cancelScheduledInstanceStateChange(Context context, AlarmInstance instance)992         void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance);
993     }
994 
995     /**
996      * Schedules state change callbacks within the AlarmManager.
997      */
998     private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
999         @Override
scheduleInstanceStateChange(Context context, Calendar time, AlarmInstance instance, int newState)1000         public void scheduleInstanceStateChange(Context context, Calendar time,
1001                 AlarmInstance instance, int newState) {
1002             final long timeInMillis = time.getTimeInMillis();
1003             LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
1004                     instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);
1005             final Intent stateChangeIntent =
1006                     createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState);
1007             // Treat alarm state change as high priority, use foreground broadcasts
1008             stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
1009             PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
1010                     stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
1011 
1012             final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
1013             if (Utils.isMOrLater()) {
1014                 // Ensure the alarm fires even if the device is dozing.
1015                 am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
1016             } else {
1017                 am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
1018             }
1019         }
1020 
1021         @Override
cancelScheduledInstanceStateChange(Context context, AlarmInstance instance)1022         public void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance) {
1023             LogUtils.v("Canceling instance " + instance.mId + " timers");
1024 
1025             // Create a PendingIntent that will match any one set for this instance
1026             PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
1027                     createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null),
1028                     PendingIntent.FLAG_NO_CREATE);
1029 
1030             if (pendingIntent != null) {
1031                 AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
1032                 am.cancel(pendingIntent);
1033                 pendingIntent.cancel();
1034             }
1035         }
1036     }
1037 }
1038