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 */
16package com.android.deskclock.alarms;
17
18import android.annotation.TargetApi;
19import android.app.AlarmManager;
20import android.app.AlarmManager.AlarmClockInfo;
21import android.app.PendingIntent;
22import android.content.BroadcastReceiver;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.net.Uri;
27import android.os.Build;
28import android.os.Handler;
29import android.os.PowerManager;
30import android.provider.Settings;
31import android.support.v4.app.NotificationManagerCompat;
32import android.text.format.DateFormat;
33import android.widget.Toast;
34
35import com.android.deskclock.AlarmAlertWakeLock;
36import com.android.deskclock.AlarmClockFragment;
37import com.android.deskclock.AlarmUtils;
38import com.android.deskclock.AsyncHandler;
39import com.android.deskclock.DeskClock;
40import com.android.deskclock.LogUtils;
41import com.android.deskclock.R;
42import com.android.deskclock.Utils;
43import com.android.deskclock.data.DataModel;
44import com.android.deskclock.events.Events;
45import com.android.deskclock.provider.Alarm;
46import com.android.deskclock.provider.AlarmInstance;
47
48import java.util.Calendar;
49import java.util.Collections;
50import java.util.Comparator;
51import java.util.List;
52
53import static android.content.Context.ALARM_SERVICE;
54import 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 */
106public 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
146    private static Calendar getCurrentTime() {
147        return sCurrentTimeFactory == null
148                ? DataModel.getDataModel().getCalendar()
149                : sCurrentTimeFactory.getCurrentTime();
150    }
151
152    static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) {
153        sCurrentTimeFactory = currentTimeFactory;
154    }
155
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     */
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     */
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)
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)
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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 {
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 {
989        void scheduleInstanceStateChange(Context context, Calendar time,
990                AlarmInstance instance, int newState);
991
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
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
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