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