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.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.SharedPreferences;
26import android.net.Uri;
27import android.os.Build;
28import android.os.Handler;
29import android.os.PowerManager;
30import android.preference.PreferenceManager;
31import android.provider.Settings;
32import android.text.format.DateFormat;
33import android.support.v4.app.NotificationManagerCompat;
34import android.widget.Toast;
35
36import com.android.deskclock.AlarmAlertWakeLock;
37import com.android.deskclock.AlarmClockFragment;
38import com.android.deskclock.AlarmUtils;
39import com.android.deskclock.AsyncHandler;
40import com.android.deskclock.DeskClock;
41import com.android.deskclock.LogUtils;
42import com.android.deskclock.R;
43import com.android.deskclock.SettingsActivity;
44import com.android.deskclock.Utils;
45import com.android.deskclock.events.Events;
46import com.android.deskclock.provider.Alarm;
47import com.android.deskclock.provider.AlarmInstance;
48
49import java.util.Calendar;
50import 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 */
102public 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
145    private static Calendar getCurrentTime() {
146        return sCurrentTimeFactory == null ?
147                Calendar.getInstance() : sCurrentTimeFactory.getCurrentTime();
148    }
149    static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) {
150        sCurrentTimeFactory = currentTimeFactory;
151    }
152
153    static void setStateChangeScheduler(StateChangeScheduler stateChangeScheduler) {
154        if (stateChangeScheduler == null) {
155            stateChangeScheduler = new AlarmManagerStateChangeScheduler();
156        }
157        sStateChangeScheduler = stateChangeScheduler;
158    }
159
160    public static int getGlobalIntentId(Context context) {
161        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
162        return prefs.getInt(ALARM_GLOBAL_ID_EXTRA, -1);
163    }
164
165    public static void updateGlobalIntentId(Context context) {
166        SharedPreferences prefs = PreferenceManager.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     */
177    public static void updateNextAlarm(Context context) {
178        final AlarmInstance nextAlarm = getNextFiringAlarm(context);
179
180        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
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     */
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     */
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)
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     */
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     */
304    public static Intent createStateChangeIntent(Context context, String tag,
305            AlarmInstance instance, Integer state) {
306        Intent intent = AlarmInstance.createIntent(context, AlarmStateManager.class, instance.mId);
307        intent.setAction(CHANGE_STATE_ACTION);
308        intent.addCategory(tag);
309        intent.putExtra(ALARM_GLOBAL_ID_EXTRA, getGlobalIntentId(context));
310        if (state != null) {
311            intent.putExtra(ALARM_STATE_EXTRA, state.intValue());
312        }
313        return intent;
314    }
315
316    /**
317     * Schedule alarm instance state changes with {@link AlarmManager}.
318     *
319     * @param ctx application context
320     * @param time to trigger state change
321     * @param instance to change state to
322     * @param newState to change to
323     */
324    private static void scheduleInstanceStateChange(Context ctx, Calendar time,
325            AlarmInstance instance, int newState) {
326        sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState);
327    }
328
329    /**
330     * Cancel all {@link AlarmManager} timers for instance.
331     *
332     * @param ctx application context
333     * @param instance to disable all {@link AlarmManager} timers
334     */
335    private static void cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance) {
336        sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance);
337    }
338
339
340    /**
341     * This will set the alarm instance to the SILENT_STATE and update
342     * the application notifications and schedule any state changes that need
343     * to occur in the future.
344     *
345     * @param context application context
346     * @param instance to set state to
347     */
348    public static void setSilentState(Context context, AlarmInstance instance) {
349        LogUtils.v("Setting silent state to instance " + instance.mId);
350
351        // Update alarm in db
352        ContentResolver contentResolver = context.getContentResolver();
353        instance.mAlarmState = AlarmInstance.SILENT_STATE;
354        AlarmInstance.updateInstance(contentResolver, instance);
355
356        // Setup instance notification and scheduling timers
357        AlarmNotifications.clearNotification(context, instance);
358        scheduleInstanceStateChange(context, instance.getLowNotificationTime(),
359                instance, AlarmInstance.LOW_NOTIFICATION_STATE);
360    }
361
362    /**
363     * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update
364     * the application notifications and schedule any state changes that need
365     * to occur in the future.
366     *
367     * @param context application context
368     * @param instance to set state to
369     */
370    public static void setLowNotificationState(Context context, AlarmInstance instance) {
371        LogUtils.v("Setting low notification state to instance " + instance.mId);
372
373        // Update alarm state in db
374        ContentResolver contentResolver = context.getContentResolver();
375        instance.mAlarmState = AlarmInstance.LOW_NOTIFICATION_STATE;
376        AlarmInstance.updateInstance(contentResolver, instance);
377
378        // Setup instance notification and scheduling timers
379        AlarmNotifications.showLowPriorityNotification(context, instance);
380        scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
381                instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
382    }
383
384    /**
385     * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update
386     * the application notifications and schedule any state changes that need
387     * to occur in the future.
388     *
389     * @param context application context
390     * @param instance to set state to
391     */
392    public static void setHideNotificationState(Context context, AlarmInstance instance) {
393        LogUtils.v("Setting hide notification state to instance " + instance.mId);
394
395        // Update alarm state in db
396        ContentResolver contentResolver = context.getContentResolver();
397        instance.mAlarmState = AlarmInstance.HIDE_NOTIFICATION_STATE;
398        AlarmInstance.updateInstance(contentResolver, instance);
399
400        // Setup instance notification and scheduling timers
401        AlarmNotifications.clearNotification(context, instance);
402        scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
403                instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
404    }
405
406    /**
407     * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update
408     * the application notifications and schedule any state changes that need
409     * to occur in the future.
410     *
411     * @param context application context
412     * @param instance to set state to
413     */
414    public static void setHighNotificationState(Context context, AlarmInstance instance) {
415        LogUtils.v("Setting high notification state to instance " + instance.mId);
416
417        // Update alarm state in db
418        ContentResolver contentResolver = context.getContentResolver();
419        instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE;
420        AlarmInstance.updateInstance(contentResolver, instance);
421
422        // Setup instance notification and scheduling timers
423        AlarmNotifications.showHighPriorityNotification(context, instance);
424        scheduleInstanceStateChange(context, instance.getAlarmTime(),
425                instance, AlarmInstance.FIRED_STATE);
426    }
427
428    /**
429     * This will set the alarm instance to the FIRED_STATE and update
430     * the application notifications and schedule any state changes that need
431     * to occur in the future.
432     *
433     * @param context application context
434     * @param instance to set state to
435     */
436    public static void setFiredState(Context context, AlarmInstance instance) {
437        LogUtils.v("Setting fire state to instance " + instance.mId);
438
439        // Update alarm state in db
440        ContentResolver contentResolver = context.getContentResolver();
441        instance.mAlarmState = AlarmInstance.FIRED_STATE;
442        AlarmInstance.updateInstance(contentResolver, instance);
443
444        if (instance.mAlarmId != null) {
445            // if the time changed *backward* and pushed an instance from missed back to fired,
446            // remove any other scheduled instances that may exist
447            AlarmInstance.deleteOtherInstances(contentResolver, instance.mAlarmId, instance.mId);
448        }
449
450        // Start the alarm and schedule timeout timer for it
451        AlarmService.startAlarm(context, instance);
452
453        Calendar timeout = instance.getTimeout(context);
454        if (timeout != null) {
455            scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE);
456        }
457
458        // Instance not valid anymore, so find next alarm that will fire and notify system
459        updateNextAlarm(context);
460    }
461
462    /**
463     * This will set the alarm instance to the SNOOZE_STATE and update
464     * the application notifications and schedule any state changes that need
465     * to occur in the future.
466     *
467     * @param context application context
468     * @param instance to set state to
469     *
470     */
471    public static void setSnoozeState(final Context context, AlarmInstance instance,
472                                      boolean showToast) {
473        // Stop alarm if this instance is firing it
474        AlarmService.stopAlarm(context, instance);
475
476        // Calculate the new snooze alarm time
477        String snoozeMinutesStr = PreferenceManager.getDefaultSharedPreferences(context)
478                .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES);
479        final int snoozeMinutes = Integer.parseInt(snoozeMinutesStr);
480        Calendar newAlarmTime = Calendar.getInstance();
481        newAlarmTime.add(Calendar.MINUTE, snoozeMinutes);
482
483        // Update alarm state and new alarm time in db.
484        LogUtils.v("Setting snoozed state to instance " + instance.mId + " for "
485                + AlarmUtils.getFormattedTime(context, newAlarmTime));
486        instance.setAlarmTime(newAlarmTime);
487        instance.mAlarmState = AlarmInstance.SNOOZE_STATE;
488        AlarmInstance.updateInstance(context.getContentResolver(), instance);
489
490        // Setup instance notification and scheduling timers
491        AlarmNotifications.showSnoozeNotification(context, instance);
492        scheduleInstanceStateChange(context, instance.getAlarmTime(),
493                instance, AlarmInstance.FIRED_STATE);
494
495        // Display the snooze minutes in a toast.
496        if (showToast) {
497            final Handler mainHandler = new Handler(context.getMainLooper());
498            final Runnable myRunnable = new Runnable() {
499                @Override
500                public void run() {
501                    String displayTime = String.format(context.getResources().getQuantityText
502                            (R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(),
503                            snoozeMinutes);
504                    Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show();
505                }
506            };
507            mainHandler.post(myRunnable);
508        }
509
510        // Instance time changed, so find next alarm that will fire and notify system
511        updateNextAlarm(context);
512    }
513
514    public static int getSnoozedMinutes(Context context) {
515        final String snoozeMinutesStr = PreferenceManager.getDefaultSharedPreferences(context)
516                .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES);
517        return Integer.parseInt(snoozeMinutesStr);
518    }
519
520    /**
521     * This will set the alarm instance to the MISSED_STATE and update
522     * the application notifications and schedule any state changes that need
523     * to occur in the future.
524     *
525     * @param context application context
526     * @param instance to set state to
527     */
528    public static void setMissedState(Context context, AlarmInstance instance) {
529        LogUtils.v("Setting missed state to instance " + instance.mId);
530        // Stop alarm if this instance is firing it
531        AlarmService.stopAlarm(context, instance);
532
533        // Check parent if it needs to reschedule, disable or delete itself
534        if (instance.mAlarmId != null) {
535            updateParentAlarm(context, instance);
536        }
537
538        // Update alarm state
539        ContentResolver contentResolver = context.getContentResolver();
540        instance.mAlarmState = AlarmInstance.MISSED_STATE;
541        AlarmInstance.updateInstance(contentResolver, instance);
542
543        // Setup instance notification and scheduling timers
544        AlarmNotifications.showMissedNotification(context, instance);
545        scheduleInstanceStateChange(context, instance.getMissedTimeToLive(),
546                instance, AlarmInstance.DISMISSED_STATE);
547
548        // Instance is not valid anymore, so find next alarm that will fire and notify system
549        updateNextAlarm(context);
550    }
551
552    /**
553     * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state
554     * change to DISMISSED_STATE at the regularly scheduled firing time.
555     * @param context
556     * @param instance
557     */
558    public static void setPreDismissState(Context context, AlarmInstance instance) {
559        LogUtils.v("Setting predismissed state to instance " + instance.mId);
560
561        // Update alarm in db
562        final ContentResolver contentResolver = context.getContentResolver();
563        instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE;
564        AlarmInstance.updateInstance(contentResolver, instance);
565
566        // Setup instance notification and scheduling timers
567        AlarmNotifications.clearNotification(context, instance);
568        scheduleInstanceStateChange(context, instance.getAlarmTime(), instance,
569                AlarmInstance.DISMISSED_STATE);
570
571        final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId);
572        // if it's a one time alarm set the toggle to off
573        if (alarm != null && !alarm.daysOfWeek.isRepeating()) {
574            // Check parent if it needs to reschedule, disable or delete itself
575            if (instance.mAlarmId != null) {
576                updateParentAlarm(context, instance);
577            }
578        }
579    }
580
581    /**
582     * This will set the alarm instance to the SILENT_STATE and update
583     * the application notifications and schedule any state changes that need
584     * to occur in the future.
585     *
586     * @param context application context
587     * @param instance to set state to
588     */
589    public static void setDismissState(Context context, AlarmInstance instance) {
590        LogUtils.v("Setting dismissed state to instance " + instance.mId);
591
592        // Remove all other timers and notifications associated to it
593        unregisterInstance(context, instance);
594
595        // Check parent if it needs to reschedule, disable or delete itself
596        if (instance.mAlarmId != null) {
597            updateParentAlarm(context, instance);
598        }
599
600        // Delete instance as it is not needed anymore
601        AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
602
603        // Instance is not valid anymore, so find next alarm that will fire and notify system
604        updateNextAlarm(context);
605    }
606
607    /**
608     * This will not change the state of instance, but remove it's notifications and
609     * alarm timers.
610     *
611     * @param context application context
612     * @param instance to unregister
613     */
614    public static void unregisterInstance(Context context, AlarmInstance instance) {
615        // Stop alarm if this instance is firing it
616        AlarmService.stopAlarm(context, instance);
617        AlarmNotifications.clearNotification(context, instance);
618        cancelScheduledInstanceStateChange(context, instance);
619    }
620
621    /**
622     * This registers the AlarmInstance to the state manager. This will look at the instance
623     * and choose the most appropriate state to put it in. This is primarily used by new
624     * alarms, but it can also be called when the system time changes.
625     *
626     * Most state changes are handled by the states themselves, but during major time changes we
627     * have to correct the alarm instance state. This means we have to handle special cases as
628     * describe below:
629     *
630     * <ul>
631     *     <li>Make sure all dismissed alarms are never re-activated</li>
632     *     <li>Make sure pre-dismissed alarms stay predismissed</li>
633     *     <li>Make sure firing alarms stayed fired unless they should be auto-silenced</li>
634     *     <li>Missed instance that have parents should be re-enabled if we went back in time</li>
635     *     <li>If alarm was SNOOZED, then show the notification but don't update time</li>
636     *     <li>If low priority notification was hidden, then make sure it stays hidden</li>
637     * </ul>
638     *
639     * If none of these special case are found, then we just check the time and see what is the
640     * proper state for the instance.
641     *
642     * @param context application context
643     * @param instance to register
644     */
645    public static void registerInstance(Context context, AlarmInstance instance,
646            boolean updateNextAlarm) {
647        final ContentResolver cr = context.getContentResolver();
648        final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
649        final Calendar currentTime = getCurrentTime();
650        final Calendar alarmTime = instance.getAlarmTime();
651        final Calendar timeoutTime = instance.getTimeout(context);
652        final Calendar lowNotificationTime = instance.getLowNotificationTime();
653        final Calendar highNotificationTime = instance.getHighNotificationTime();
654        final Calendar missedTTL = instance.getMissedTimeToLive();
655
656        // Handle special use cases here
657        if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) {
658            // This should never happen, but add a quick check here
659            LogUtils.e("Alarm Instance is dismissed, but never deleted");
660            setDismissState(context, instance);
661            return;
662        } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) {
663            // Keep alarm firing, unless it should be timed out
664            boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime);
665            if (!hasTimeout) {
666                setFiredState(context, instance);
667                return;
668            }
669        } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) {
670            if (currentTime.before(alarmTime)) {
671                if (instance.mAlarmId == null) {
672                    // This instance parent got deleted (ie. deleteAfterUse), so
673                    // we should not re-activate it.-
674                    setDismissState(context, instance);
675                    return;
676                }
677
678                // TODO: This will re-activate missed snoozed alarms, but will
679                // use our normal notifications. This is not ideal, but very rare use-case.
680                // We should look into fixing this in the future.
681
682                // Make sure we re-enable the parent alarm of the instance
683                // because it will get activated by by the below code
684                alarm.enabled = true;
685                Alarm.updateAlarm(cr, alarm);
686            }
687        } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) {
688            if (currentTime.before(alarmTime)) {
689                setPreDismissState(context, instance);
690            } else {
691                setDismissState(context, instance);
692            }
693            return;
694        }
695
696        // Fix states that are time sensitive
697        if (currentTime.after(missedTTL)) {
698            // Alarm is so old, just dismiss it
699            setDismissState(context, instance);
700        } else if (currentTime.after(alarmTime)) {
701            // There is a chance that the TIME_SET occurred right when the alarm should go off, so
702            // we need to add a check to see if we should fire the alarm instead of marking it
703            // missed.
704            Calendar alarmBuffer = Calendar.getInstance();
705            alarmBuffer.setTime(alarmTime.getTime());
706            alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER);
707            if (currentTime.before(alarmBuffer)) {
708                setFiredState(context, instance);
709            } else {
710                setMissedState(context, instance);
711            }
712        } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
713            // We only want to display snooze notification and not update the time,
714            // so handle showing the notification directly
715            AlarmNotifications.showSnoozeNotification(context, instance);
716            scheduleInstanceStateChange(context, instance.getAlarmTime(),
717                    instance, AlarmInstance.FIRED_STATE);
718        } else if (currentTime.after(highNotificationTime)) {
719            setHighNotificationState(context, instance);
720        } else if (currentTime.after(lowNotificationTime)) {
721            // Only show low notification if it wasn't hidden in the past
722            if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) {
723                setHideNotificationState(context, instance);
724            } else {
725                setLowNotificationState(context, instance);
726            }
727        } else {
728          // Alarm is still active, so initialize as a silent alarm
729          setSilentState(context, instance);
730        }
731
732        // The caller prefers to handle updateNextAlarm for optimization
733        if (updateNextAlarm) {
734            updateNextAlarm(context);
735        }
736    }
737
738    /**
739     * This will delete and unregister all instances associated with alarmId, without affect
740     * the alarm itself. This should be used whenever modifying or deleting an alarm.
741     *
742     * @param context application context
743     * @param alarmId to find instances to delete.
744     */
745    public static void deleteAllInstances(Context context, long alarmId) {
746        ContentResolver cr = context.getContentResolver();
747        List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
748        for (AlarmInstance instance : instances) {
749            unregisterInstance(context, instance);
750            AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
751        }
752        updateNextAlarm(context);
753    }
754
755    /**
756     * Fix and update all alarm instance when a time change event occurs.
757     *
758     * @param context application context
759     */
760    public static void fixAlarmInstances(Context context) {
761        // Register all instances after major time changes or when phone restarts
762        final ContentResolver contentResolver = context.getContentResolver();
763        final Calendar currentTime = getCurrentTime();
764        for (AlarmInstance instance : AlarmInstance.getInstances(contentResolver, null)) {
765            final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId);
766            final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime());
767            final Calendar missedTTLTime = instance.getMissedTimeToLive();
768            if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) {
769                final Calendar oldAlarmTime = instance.getAlarmTime();
770                final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime);
771                final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime);
772                final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime);
773                LogUtils.i("A time change has caused an existing alarm scheduled to fire at %s to" +
774                        " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime);
775
776                // The time change is so dramatic the AlarmInstance doesn't make any sense;
777                // remove it and schedule the new appropriate instance.
778                AlarmStateManager.setDismissState(context, instance);
779            } else {
780                registerInstance(context, instance, false);
781            }
782        }
783
784        updateNextAlarm(context);
785    }
786
787    /**
788     * Utility method to set alarm instance state via constants.
789     *
790     * @param context application context
791     * @param instance to change state on
792     * @param state to change to
793     */
794    public void setAlarmState(Context context, AlarmInstance instance, int state) {
795        if (instance == null) {
796            LogUtils.e("Null alarm instance while setting state to %d", state);
797            return;
798        }
799        switch(state) {
800            case AlarmInstance.SILENT_STATE:
801                setSilentState(context, instance);
802                break;
803            case AlarmInstance.LOW_NOTIFICATION_STATE:
804                setLowNotificationState(context, instance);
805                break;
806            case AlarmInstance.HIDE_NOTIFICATION_STATE:
807                setHideNotificationState(context, instance);
808                break;
809            case AlarmInstance.HIGH_NOTIFICATION_STATE:
810                setHighNotificationState(context, instance);
811                break;
812            case AlarmInstance.FIRED_STATE:
813                setFiredState(context, instance);
814                break;
815            case AlarmInstance.SNOOZE_STATE:
816                setSnoozeState(context, instance, true /* showToast */);
817                break;
818            case AlarmInstance.MISSED_STATE:
819                setMissedState(context, instance);
820                break;
821            case AlarmInstance.PREDISMISSED_STATE:
822                setPreDismissState(context, instance);
823                break;
824            case AlarmInstance.DISMISSED_STATE:
825                setDismissState(context, instance);
826                break;
827            default:
828                LogUtils.e("Trying to change to unknown alarm state: " + state);
829        }
830    }
831
832    @Override
833    public void onReceive(final Context context, final Intent intent) {
834        if (INDICATOR_ACTION.equals(intent.getAction())) {
835            return;
836        }
837
838        final PendingResult result = goAsync();
839        final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
840        wl.acquire();
841        AsyncHandler.post(new Runnable() {
842            @Override
843            public void run() {
844                handleIntent(context, intent);
845                result.finish();
846                wl.release();
847            }
848        });
849    }
850
851    private void handleIntent(Context context, Intent intent) {
852        final String action = intent.getAction();
853        LogUtils.v("AlarmStateManager received intent " + intent);
854        if (CHANGE_STATE_ACTION.equals(action)) {
855            Uri uri = intent.getData();
856            AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
857                    AlarmInstance.getId(uri));
858            if (instance == null) {
859                // Not a big deal, but it shouldn't happen
860                LogUtils.e("Can not change state for unknown instance: " + uri);
861                return;
862            }
863
864            int globalId = getGlobalIntentId(context);
865            int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1);
866            int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1);
867            if (intentId != globalId) {
868                LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " +
869                        alarmState);
870                // Allows dismiss/snooze requests to go through
871                if (!intent.hasCategory(ALARM_DISMISS_TAG) &&
872                        !intent.hasCategory(ALARM_SNOOZE_TAG)) {
873                    LogUtils.i("Ignoring old Intent");
874                    return;
875                }
876            }
877
878            if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
879                if (intent.hasCategory(ALARM_DISMISS_TAG)) {
880                    Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification);
881                } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) {
882                    Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification);
883                }
884            }
885
886            if (alarmState >= 0) {
887                setAlarmState(context, instance, alarmState);
888            } else {
889                registerInstance(context, instance, true);
890            }
891        } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) {
892            Uri uri = intent.getData();
893            AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
894                    AlarmInstance.getId(uri));
895
896            if (instance == null) {
897                LogUtils.e("Null alarminstance for SHOW_AND_DISMISS");
898                // dismiss the notification
899                final int id = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1);
900                if (id != -1) {
901                    NotificationManagerCompat.from(context).cancel(id);
902                }
903                return;
904            }
905
906            long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
907            Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId);
908            viewAlarmIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX);
909            viewAlarmIntent.putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId);
910            viewAlarmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
911            context.startActivity(viewAlarmIntent);
912            setDismissState(context, instance);
913        }
914    }
915
916    /**
917     * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm
918     * indicators.
919     */
920    public static Intent createIndicatorIntent(Context context) {
921        return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION);
922    }
923
924    /**
925     * Abstract away how the current time is computed. If no implementation of this interface is
926     * given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory
927     * instance is consulted for the current time.
928     */
929    interface CurrentTimeFactory {
930        Calendar getCurrentTime();
931    }
932
933    /**
934     * Abstracts away how state changes are scheduled. The {@link AlarmManagerStateChangeScheduler}
935     * implementation schedules callbacks within the system AlarmManager. Alternate
936     * implementations, such as test case mocks can subvert this behavior.
937     */
938    interface StateChangeScheduler {
939        void scheduleInstanceStateChange(Context context, Calendar time,
940                AlarmInstance instance, int newState);
941
942        void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance);
943    }
944
945    /**
946     * Schedules state change callbacks within the AlarmManager.
947     */
948    private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
949        @Override
950        public void scheduleInstanceStateChange(Context context, Calendar time,
951                AlarmInstance instance, int newState) {
952            final long timeInMillis = time.getTimeInMillis();
953            LogUtils.v("Scheduling state change %d to instance %d at %s (%d)", newState,
954                    instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);
955            final Intent stateChangeIntent =
956                    createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState);
957            // Treat alarm state change as high priority, use foreground broadcasts
958            stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
959            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, instance.hashCode(),
960                    stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
961
962            final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
963            if (Utils.isKitKatOrLater()) {
964                am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
965            } else {
966                am.set(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
967            }
968        }
969
970        @Override
971        public void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance) {
972            LogUtils.v("Canceling instance " + instance.mId + " timers");
973
974            // Create a PendingIntent that will match any one set for this instance
975            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, instance.hashCode(),
976                    createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null),
977                    PendingIntent.FLAG_NO_CREATE);
978
979            if (pendingIntent != null) {
980                AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
981                am.cancel(pendingIntent);
982                pendingIntent.cancel();
983            }
984        }
985    }
986}
987