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