Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.deskclock.data;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.app.AlarmManager;
     21 import android.app.Notification;
     22 import android.app.PendingIntent;
     23 import android.app.Service;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.content.SharedPreferences;
     29 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
     30 import android.net.Uri;
     31 import android.support.annotation.StringRes;
     32 import android.support.v4.app.NotificationManagerCompat;
     33 import android.util.ArraySet;
     34 
     35 import com.android.deskclock.AlarmAlertWakeLock;
     36 import com.android.deskclock.LogUtils;
     37 import com.android.deskclock.R;
     38 import com.android.deskclock.Utils;
     39 import com.android.deskclock.events.Events;
     40 import com.android.deskclock.settings.SettingsActivity;
     41 import com.android.deskclock.timer.TimerKlaxon;
     42 import com.android.deskclock.timer.TimerService;
     43 
     44 import java.util.ArrayList;
     45 import java.util.Collections;
     46 import java.util.List;
     47 import java.util.Set;
     48 
     49 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
     50 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
     51 import static com.android.deskclock.data.Timer.State.EXPIRED;
     52 import static com.android.deskclock.data.Timer.State.RESET;
     53 
     54 /**
     55  * All {@link Timer} data is accessed via this model.
     56  */
     57 final class TimerModel {
     58 
     59     /**
     60      * Running timers less than this threshold are left running/expired; greater than this
     61      * threshold are considered missed.
     62      */
     63     private static final long MISSED_THRESHOLD = -MINUTE_IN_MILLIS;
     64 
     65     private final Context mContext;
     66 
     67     private final SharedPreferences mPrefs;
     68 
     69     /** The alarm manager system service that calls back when timers expire. */
     70     private final AlarmManager mAlarmManager;
     71 
     72     /** The model from which settings are fetched. */
     73     private final SettingsModel mSettingsModel;
     74 
     75     /** The model from which notification data are fetched. */
     76     private final NotificationModel mNotificationModel;
     77 
     78     /** The model from which ringtone data are fetched. */
     79     private final RingtoneModel mRingtoneModel;
     80 
     81     /** Used to create and destroy system notifications related to timers. */
     82     private final NotificationManagerCompat mNotificationManager;
     83 
     84     /** Update timer notification when locale changes. */
     85     @SuppressWarnings("FieldCanBeLocal")
     86     private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
     87 
     88     /**
     89      * Retain a hard reference to the shared preference observer to prevent it from being garbage
     90      * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
     91      */
     92     @SuppressWarnings("FieldCanBeLocal")
     93     private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
     94 
     95     /** The listeners to notify when a timer is added, updated or removed. */
     96     private final List<TimerListener> mTimerListeners = new ArrayList<>();
     97 
     98     /** Delegate that builds platform-specific timer notifications. */
     99     private final TimerNotificationBuilder mNotificationBuilder = new TimerNotificationBuilder();
    100 
    101     /**
    102      * The ids of expired timers for which the ringer is ringing. Not all expired timers have their
    103      * ids in this collection. If a timer was already expired when the app was started its id will
    104      * be absent from this collection.
    105      */
    106     @SuppressLint("NewApi")
    107     private final Set<Integer> mRingingIds = new ArraySet<>();
    108 
    109     /** The uri of the ringtone to play for timers. */
    110     private Uri mTimerRingtoneUri;
    111 
    112     /** The title of the ringtone to play for timers. */
    113     private String mTimerRingtoneTitle;
    114 
    115     /** A mutable copy of the timers. */
    116     private List<Timer> mTimers;
    117 
    118     /** A mutable copy of the expired timers. */
    119     private List<Timer> mExpiredTimers;
    120 
    121     /** A mutable copy of the missed timers. */
    122     private List<Timer> mMissedTimers;
    123 
    124     /**
    125      * The service that keeps this application in the foreground while a heads-up timer
    126      * notification is displayed. Marking the service as foreground prevents the operating system
    127      * from killing this application while expired timers are actively firing.
    128      */
    129     private Service mService;
    130 
    131     TimerModel(Context context, SharedPreferences prefs, SettingsModel settingsModel,
    132             RingtoneModel ringtoneModel, NotificationModel notificationModel) {
    133         mContext = context;
    134         mPrefs = prefs;
    135         mSettingsModel = settingsModel;
    136         mRingtoneModel = ringtoneModel;
    137         mNotificationModel = notificationModel;
    138         mNotificationManager = NotificationManagerCompat.from(context);
    139 
    140         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    141 
    142         // Clear caches affected by preferences when preferences change.
    143         prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
    144 
    145         // Update timer notification when locale changes.
    146         final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
    147         mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
    148     }
    149 
    150     /**
    151      * @param timerListener to be notified when timers are added, updated and removed
    152      */
    153     void addTimerListener(TimerListener timerListener) {
    154         mTimerListeners.add(timerListener);
    155     }
    156 
    157     /**
    158      * @param timerListener to no longer be notified when timers are added, updated and removed
    159      */
    160     void removeTimerListener(TimerListener timerListener) {
    161         mTimerListeners.remove(timerListener);
    162     }
    163 
    164     /**
    165      * @return all defined timers in their creation order
    166      */
    167     List<Timer> getTimers() {
    168         return Collections.unmodifiableList(getMutableTimers());
    169     }
    170 
    171     /**
    172      * @return all expired timers in their expiration order
    173      */
    174     List<Timer> getExpiredTimers() {
    175         return Collections.unmodifiableList(getMutableExpiredTimers());
    176     }
    177 
    178     /**
    179      * @return all missed timers in their expiration order
    180      */
    181     private List<Timer> getMissedTimers() {
    182         return Collections.unmodifiableList(getMutableMissedTimers());
    183     }
    184 
    185     /**
    186      * @param timerId identifies the timer to return
    187      * @return the timer with the given {@code timerId}
    188      */
    189     Timer getTimer(int timerId) {
    190         for (Timer timer : getMutableTimers()) {
    191             if (timer.getId() == timerId) {
    192                 return timer;
    193             }
    194         }
    195 
    196         return null;
    197     }
    198 
    199     /**
    200      * @return the timer that last expired and is still expired now; {@code null} if no timers are
    201      *      expired
    202      */
    203     Timer getMostRecentExpiredTimer() {
    204         final List<Timer> timers = getMutableExpiredTimers();
    205         return timers.isEmpty() ? null : timers.get(timers.size() - 1);
    206     }
    207 
    208     /**
    209      * @param length the length of the timer in milliseconds
    210      * @param label describes the purpose of the timer
    211      * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
    212      * @return the newly added timer
    213      */
    214     Timer addTimer(long length, String label, boolean deleteAfterUse) {
    215         // Create the timer instance.
    216         Timer timer = new Timer(-1, RESET, length, length, Timer.UNUSED, Timer.UNUSED, length,
    217                 label, deleteAfterUse);
    218 
    219         // Add the timer to permanent storage.
    220         timer = TimerDAO.addTimer(mPrefs, timer);
    221 
    222         // Add the timer to the cache.
    223         getMutableTimers().add(0, timer);
    224 
    225         // Update the timer notification.
    226         updateNotification();
    227         // Heads-Up notification is unaffected by this change
    228 
    229         // Notify listeners of the change.
    230         for (TimerListener timerListener : mTimerListeners) {
    231             timerListener.timerAdded(timer);
    232         }
    233 
    234         return timer;
    235     }
    236 
    237     /**
    238      * @param service used to start foreground notifications related to expired timers
    239      * @param timer the timer to be expired
    240      */
    241     void expireTimer(Service service, Timer timer) {
    242         if (mService == null) {
    243             // If this is the first expired timer, retain the service that will be used to start
    244             // the heads-up notification in the foreground.
    245             mService = service;
    246         } else if (mService != service) {
    247             // If this is not the first expired timer, the service should match the one given when
    248             // the first timer expired.
    249             LogUtils.wtf("Expected TimerServices to be identical");
    250         }
    251 
    252         updateTimer(timer.expire());
    253     }
    254 
    255     /**
    256      * @param timer an updated timer to store
    257      */
    258     void updateTimer(Timer timer) {
    259         final Timer before = doUpdateTimer(timer);
    260 
    261         // Update the notification after updating the timer data.
    262         updateNotification();
    263 
    264         // If the timer started or stopped being expired, update the heads-up notification.
    265         if (before.getState() != timer.getState()) {
    266             if (before.isExpired() || timer.isExpired()) {
    267                 updateHeadsUpNotification();
    268             }
    269         }
    270     }
    271 
    272     /**
    273      * @param timer an existing timer to be removed
    274      */
    275     void removeTimer(Timer timer) {
    276         doRemoveTimer(timer);
    277 
    278         // Update the timer notifications after removing the timer data.
    279         if (timer.isExpired()) {
    280             updateHeadsUpNotification();
    281         } else {
    282             updateNotification();
    283         }
    284     }
    285 
    286     /**
    287      * If the given {@code timer} is expired and marked for deletion after use then this method
    288      * removes the the timer. The timer is otherwise transitioned to the reset state and continues
    289      * to exist.
    290      *
    291      * @param timer        the timer to be reset
    292      * @param allowDelete  {@code true} if the timer is allowed to be deleted instead of reset
    293      *                     (e.g. one use timers)
    294      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
    295      * @return the reset {@code timer} or {@code null} if the timer was deleted
    296      */
    297     Timer resetTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId) {
    298         final Timer result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId);
    299 
    300         // Update the notification after updating the timer data.
    301         if (timer.isMissed()) {
    302             updateMissedNotification();
    303         } else if (timer.isExpired()) {
    304             updateHeadsUpNotification();
    305         } else {
    306             updateNotification();
    307         }
    308 
    309         return result;
    310     }
    311 
    312     /**
    313      * Update timers after system reboot.
    314      */
    315     void updateTimersAfterReboot() {
    316         final List<Timer> timers = new ArrayList<>(getTimers());
    317         for (Timer timer : timers) {
    318             doUpdateAfterRebootTimer(timer);
    319         }
    320 
    321         // Update the notifications once after all timers are updated.
    322         updateNotification();
    323         updateMissedNotification();
    324         updateHeadsUpNotification();
    325     }
    326 
    327     /**
    328      * Update timers after time set.
    329      */
    330     void updateTimersAfterTimeSet() {
    331         final List<Timer> timers = new ArrayList<>(getTimers());
    332         for (Timer timer : timers) {
    333             doUpdateAfterTimeSetTimer(timer);
    334         }
    335 
    336         // Update the notifications once after all timers are updated.
    337         updateNotification();
    338         updateMissedNotification();
    339         updateHeadsUpNotification();
    340     }
    341 
    342     /**
    343      * Reset all expired timers. Exactly one parameter should be filled, with preference given to
    344      * eventLabelId.
    345      *
    346      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
    347      */
    348     void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) {
    349         final List<Timer> timers = new ArrayList<>(getTimers());
    350         for (Timer timer : timers) {
    351             if (timer.isExpired()) {
    352                 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
    353             }
    354         }
    355 
    356         // Update the notifications once after all timers are updated.
    357         updateHeadsUpNotification();
    358     }
    359 
    360     /**
    361      * Reset all missed timers.
    362      *
    363      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
    364      */
    365     void resetMissedTimers(@StringRes int eventLabelId) {
    366         final List<Timer> timers = new ArrayList<>(getTimers());
    367         for (Timer timer : timers) {
    368             if (timer.isMissed()) {
    369                 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
    370             }
    371         }
    372 
    373         // Update the notifications once after all timers are updated.
    374         updateMissedNotification();
    375     }
    376 
    377     /**
    378      * Reset all unexpired timers.
    379      *
    380      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
    381      */
    382     void resetUnexpiredTimers(@StringRes int eventLabelId) {
    383         final List<Timer> timers = new ArrayList<>(getTimers());
    384         for (Timer timer : timers) {
    385             if (timer.isRunning() || timer.isPaused()) {
    386                 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
    387             }
    388         }
    389 
    390         // Update the notification once after all timers are updated.
    391         updateNotification();
    392         // Heads-Up notification is unaffected by this change
    393     }
    394 
    395     /**
    396      * @return the uri of the default ringtone to play for all timers when no user selection exists
    397      */
    398     Uri getDefaultTimerRingtoneUri() {
    399         return mSettingsModel.getDefaultTimerRingtoneUri();
    400     }
    401 
    402     /**
    403      * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
    404      */
    405     boolean isTimerRingtoneSilent() {
    406         return Uri.EMPTY.equals(getTimerRingtoneUri());
    407     }
    408 
    409     /**
    410      * @return the uri of the ringtone to play for all timers
    411      */
    412     Uri getTimerRingtoneUri() {
    413         if (mTimerRingtoneUri == null) {
    414             mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri();
    415         }
    416 
    417         return mTimerRingtoneUri;
    418     }
    419 
    420     /**
    421      * @param uri the uri of the ringtone to play for all timers
    422      */
    423     void setTimerRingtoneUri(Uri uri) {
    424         mSettingsModel.setTimerRingtoneUri(uri);
    425     }
    426 
    427     /**
    428      * @return the title of the ringtone that is played for all timers
    429      */
    430     String getTimerRingtoneTitle() {
    431         if (mTimerRingtoneTitle == null) {
    432             if (isTimerRingtoneSilent()) {
    433                 // Special case: no ringtone has a title of "Silent".
    434                 mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title);
    435             } else {
    436                 final Uri defaultUri = getDefaultTimerRingtoneUri();
    437                 final Uri uri = getTimerRingtoneUri();
    438 
    439                 if (defaultUri.equals(uri)) {
    440                     // Special case: default ringtone has a title of "Timer Expired".
    441                     mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
    442                 } else {
    443                     mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri);
    444                 }
    445             }
    446         }
    447 
    448         return mTimerRingtoneTitle;
    449     }
    450 
    451     /**
    452      * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
    453      *      {@code 0} implies no crescendo should be applied
    454      */
    455     long getTimerCrescendoDuration() {
    456         return mSettingsModel.getTimerCrescendoDuration();
    457     }
    458 
    459     /**
    460      * @return {@code true} if the device vibrates when timers expire
    461      */
    462     boolean getTimerVibrate() {
    463         return mSettingsModel.getTimerVibrate();
    464     }
    465 
    466     /**
    467      * @param enabled {@code true} if the device should vibrate when timers expire
    468      */
    469     void setTimerVibrate(boolean enabled) {
    470         mSettingsModel.setTimerVibrate(enabled);
    471     }
    472 
    473     private List<Timer> getMutableTimers() {
    474         if (mTimers == null) {
    475             mTimers = TimerDAO.getTimers(mPrefs);
    476             Collections.sort(mTimers, Timer.ID_COMPARATOR);
    477         }
    478 
    479         return mTimers;
    480     }
    481 
    482     private List<Timer> getMutableExpiredTimers() {
    483         if (mExpiredTimers == null) {
    484             mExpiredTimers = new ArrayList<>();
    485 
    486             for (Timer timer : getMutableTimers()) {
    487                 if (timer.isExpired()) {
    488                     mExpiredTimers.add(timer);
    489                 }
    490             }
    491             Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR);
    492         }
    493 
    494         return mExpiredTimers;
    495     }
    496 
    497     private List<Timer> getMutableMissedTimers() {
    498         if (mMissedTimers == null) {
    499             mMissedTimers = new ArrayList<>();
    500 
    501             for (Timer timer : getMutableTimers()) {
    502                 if (timer.isMissed()) {
    503                     mMissedTimers.add(timer);
    504                 }
    505             }
    506             Collections.sort(mMissedTimers, Timer.EXPIRY_COMPARATOR);
    507         }
    508 
    509         return mMissedTimers;
    510     }
    511 
    512     /**
    513      * This method updates timer data without updating notifications. This is useful in bulk-update
    514      * scenarios so the notifications are only rebuilt once.
    515      *
    516      * @param timer an updated timer to store
    517      * @return the state of the timer prior to the update
    518      */
    519     private Timer doUpdateTimer(Timer timer) {
    520         // Retrieve the cached form of the timer.
    521         final List<Timer> timers = getMutableTimers();
    522         final int index = timers.indexOf(timer);
    523         final Timer before = timers.get(index);
    524 
    525         // If no change occurred, ignore this update.
    526         if (timer == before) {
    527             return timer;
    528         }
    529 
    530         // Update the timer in permanent storage.
    531         TimerDAO.updateTimer(mPrefs, timer);
    532 
    533         // Update the timer in the cache.
    534         final Timer oldTimer = timers.set(index, timer);
    535 
    536         // Clear the cache of expired timers if the timer changed to/from expired.
    537         if (before.isExpired() || timer.isExpired()) {
    538             mExpiredTimers = null;
    539         }
    540         // Clear the cache of missed timers if the timer changed to/from missed.
    541         if (before.isMissed() || timer.isMissed()) {
    542             mMissedTimers = null;
    543         }
    544 
    545         // Update the timer expiration callback.
    546         updateAlarmManager();
    547 
    548         // Update the timer ringer.
    549         updateRinger(before, timer);
    550 
    551         // Notify listeners of the change.
    552         for (TimerListener timerListener : mTimerListeners) {
    553             timerListener.timerUpdated(before, timer);
    554         }
    555 
    556         return oldTimer;
    557     }
    558 
    559     /**
    560      * This method removes timer data without updating notifications. This is useful in bulk-remove
    561      * scenarios so the notifications are only rebuilt once.
    562      *
    563      * @param timer an existing timer to be removed
    564      */
    565     private void doRemoveTimer(Timer timer) {
    566         // Remove the timer from permanent storage.
    567         TimerDAO.removeTimer(mPrefs, timer);
    568 
    569         // Remove the timer from the cache.
    570         final List<Timer> timers = getMutableTimers();
    571         final int index = timers.indexOf(timer);
    572 
    573         // If the timer cannot be located there is nothing to remove.
    574         if (index == -1) {
    575             return;
    576         }
    577 
    578         timer = timers.remove(index);
    579 
    580         // Clear the cache of expired timers if a new expired timer was added.
    581         if (timer.isExpired()) {
    582             mExpiredTimers = null;
    583         }
    584 
    585         // Clear the cache of missed timers if a new missed timer was added.
    586         if (timer.isMissed()) {
    587             mMissedTimers = null;
    588         }
    589 
    590         // Update the timer expiration callback.
    591         updateAlarmManager();
    592 
    593         // Update the timer ringer.
    594         updateRinger(timer, null);
    595 
    596         // Notify listeners of the change.
    597         for (TimerListener timerListener : mTimerListeners) {
    598             timerListener.timerRemoved(timer);
    599         }
    600     }
    601 
    602     /**
    603      * This method updates/removes timer data without updating notifications. This is useful in
    604      * bulk-update scenarios so the notifications are only rebuilt once.
    605      *
    606      * If the given {@code timer} is expired and marked for deletion after use then this method
    607      * removes the the timer. The timer is otherwise transitioned to the reset state and continues
    608      * to exist.
    609      *
    610      * @param timer the timer to be reset
    611      * @param allowDelete  {@code true} if the timer is allowed to be deleted instead of reset
    612      *                     (e.g. one use timers)
    613      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
    614      * @return the reset {@code timer} or {@code null} if the timer was deleted
    615      */
    616     private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete,
    617             @StringRes int eventLabelId) {
    618         if (allowDelete
    619                 && (timer.isExpired() || timer.isMissed())
    620                 && timer.getDeleteAfterUse()) {
    621             doRemoveTimer(timer);
    622             if (eventLabelId != 0) {
    623                 Events.sendTimerEvent(R.string.action_delete, eventLabelId);
    624             }
    625             return null;
    626         } else if (!timer.isReset()) {
    627             final Timer reset = timer.reset();
    628             doUpdateTimer(reset);
    629             if (eventLabelId != 0) {
    630                 Events.sendTimerEvent(R.string.action_reset, eventLabelId);
    631             }
    632             return reset;
    633         }
    634 
    635         return timer;
    636     }
    637 
    638     /**
    639      * This method updates/removes timer data after a reboot without updating notifications.
    640      *
    641      * @param timer the timer to be updated
    642      */
    643     private void doUpdateAfterRebootTimer(Timer timer) {
    644         Timer updated = timer.updateAfterReboot();
    645         if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) {
    646             updated = updated.miss();
    647         }
    648         doUpdateTimer(updated);
    649     }
    650 
    651     private void doUpdateAfterTimeSetTimer(Timer timer) {
    652         final Timer updated = timer.updateAfterTimeSet();
    653         doUpdateTimer(updated);
    654     }
    655 
    656 
    657     /**
    658      * Updates the callback given to this application from the {@link AlarmManager} that signals the
    659      * expiration of the next timer. If no timers are currently set to expire (i.e. no running
    660      * timers exist) then this method clears the expiration callback from AlarmManager.
    661      */
    662     private void updateAlarmManager() {
    663         // Locate the next firing timer if one exists.
    664         Timer nextExpiringTimer = null;
    665         for (Timer timer : getMutableTimers()) {
    666             if (timer.isRunning()) {
    667                 if (nextExpiringTimer == null) {
    668                     nextExpiringTimer = timer;
    669                 } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) {
    670                     nextExpiringTimer = timer;
    671                 }
    672             }
    673         }
    674 
    675         // Build the intent that signals the timer expiration.
    676         final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer);
    677 
    678         if (nextExpiringTimer == null) {
    679             // Cancel the existing timer expiration callback.
    680             final PendingIntent pi = PendingIntent.getService(mContext,
    681                     0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
    682             if (pi != null) {
    683                 mAlarmManager.cancel(pi);
    684                 pi.cancel();
    685             }
    686         } else {
    687             // Update the existing timer expiration callback.
    688             final PendingIntent pi = PendingIntent.getService(mContext,
    689                     0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    690             schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi);
    691         }
    692     }
    693 
    694     /**
    695      * Starts and stops the ringer for timers if the change to the timer demands it.
    696      *
    697      * @param before the state of the timer before the change; {@code null} indicates added
    698      * @param after the state of the timer after the change; {@code null} indicates delete
    699      */
    700     private void updateRinger(Timer before, Timer after) {
    701         // Retrieve the states before and after the change.
    702         final Timer.State beforeState = before == null ? null : before.getState();
    703         final Timer.State afterState = after == null ? null : after.getState();
    704 
    705         // If the timer state did not change, the ringer state is unchanged.
    706         if (beforeState == afterState) {
    707             return;
    708         }
    709 
    710         // If the timer is the first to expire, start ringing.
    711         if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) {
    712             AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext);
    713             TimerKlaxon.start(mContext);
    714         }
    715 
    716         // If the expired timer was the last to reset, stop ringing.
    717         if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) {
    718             TimerKlaxon.stop(mContext);
    719             AlarmAlertWakeLock.releaseCpuLock();
    720         }
    721     }
    722 
    723     /**
    724      * Updates the notification controlling unexpired timers. This notification is only displayed
    725      * when the application is not open.
    726      */
    727     void updateNotification() {
    728         // Notifications should be hidden if the app is open.
    729         if (mNotificationModel.isApplicationInForeground()) {
    730             mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
    731             return;
    732         }
    733 
    734         // Filter the timers to just include unexpired ones.
    735         final List<Timer> unexpired = new ArrayList<>();
    736         for (Timer timer : getMutableTimers()) {
    737             if (timer.isRunning() || timer.isPaused()) {
    738                 unexpired.add(timer);
    739             }
    740         }
    741 
    742         // If no unexpired timers exist, cancel the notification.
    743         if (unexpired.isEmpty()) {
    744             mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
    745             return;
    746         }
    747 
    748         // Sort the unexpired timers to locate the next one scheduled to expire.
    749         Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR);
    750 
    751         // Otherwise build and post a notification reflecting the latest unexpired timers.
    752         final Notification notification =
    753                 mNotificationBuilder.build(mContext, mNotificationModel, unexpired);
    754         final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
    755         mNotificationManager.notify(notificationId, notification);
    756 
    757     }
    758 
    759     /**
    760      * Updates the notification controlling missed timers. This notification is only displayed when
    761      * the application is not open.
    762      */
    763     void updateMissedNotification() {
    764         // Notifications should be hidden if the app is open.
    765         if (mNotificationModel.isApplicationInForeground()) {
    766             mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
    767             return;
    768         }
    769 
    770         final List<Timer> missed = getMissedTimers();
    771 
    772         if (missed.isEmpty()) {
    773             mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
    774             return;
    775         }
    776 
    777         final Notification notification = mNotificationBuilder.buildMissed(mContext,
    778                 mNotificationModel, missed);
    779         final int notificationId = mNotificationModel.getMissedTimerNotificationId();
    780         mNotificationManager.notify(notificationId, notification);
    781     }
    782 
    783     /**
    784      * Updates the heads-up notification controlling expired timers. This heads-up notification is
    785      * displayed whether the application is open or not.
    786      */
    787     private void updateHeadsUpNotification() {
    788         // Nothing can be done with the heads-up notification without a valid service reference.
    789         if (mService == null) {
    790             return;
    791         }
    792 
    793         final List<Timer> expired = getExpiredTimers();
    794 
    795         // If no expired timers exist, stop the service (which cancels the foreground notification).
    796         if (expired.isEmpty()) {
    797             mService.stopSelf();
    798             mService = null;
    799             return;
    800         }
    801 
    802         // Otherwise build and post a foreground notification reflecting the latest expired timers.
    803         final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired);
    804         final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
    805         mService.startForeground(notificationId, notification);
    806     }
    807 
    808     /**
    809      * Update the timer notification in response to a locale change.
    810      */
    811     private final class LocaleChangedReceiver extends BroadcastReceiver {
    812         @Override
    813         public void onReceive(Context context, Intent intent) {
    814             mTimerRingtoneTitle = null;
    815             updateNotification();
    816             updateMissedNotification();
    817             updateHeadsUpNotification();
    818         }
    819     }
    820 
    821     /**
    822      * This receiver is notified when shared preferences change. Cached information built on
    823      * preferences must be cleared.
    824      */
    825     private final class PreferenceListener implements OnSharedPreferenceChangeListener {
    826         @Override
    827         public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    828             switch (key) {
    829                 case SettingsActivity.KEY_TIMER_RINGTONE:
    830                     mTimerRingtoneUri = null;
    831                     mTimerRingtoneTitle = null;
    832                     break;
    833             }
    834         }
    835     }
    836 
    837     static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) {
    838         if (Utils.isMOrLater()) {
    839             // Ensure the timer fires even if the device is dozing.
    840             am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
    841         } else {
    842             am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
    843         }
    844     }
    845 }
    846