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
17package com.android.deskclock.data;
18
19import android.annotation.SuppressLint;
20import android.app.AlarmManager;
21import android.app.Notification;
22import android.app.PendingIntent;
23import android.app.Service;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.SharedPreferences;
29import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
30import android.net.Uri;
31import android.support.annotation.StringRes;
32import android.support.v4.app.NotificationManagerCompat;
33import android.util.ArraySet;
34
35import com.android.deskclock.AlarmAlertWakeLock;
36import com.android.deskclock.LogUtils;
37import com.android.deskclock.R;
38import com.android.deskclock.Utils;
39import com.android.deskclock.events.Events;
40import com.android.deskclock.settings.SettingsActivity;
41import com.android.deskclock.timer.TimerKlaxon;
42import com.android.deskclock.timer.TimerService;
43
44import java.util.ArrayList;
45import java.util.Collections;
46import java.util.List;
47import java.util.Set;
48
49import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
50import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
51import static com.android.deskclock.data.Timer.State.EXPIRED;
52import static com.android.deskclock.data.Timer.State.RESET;
53
54/**
55 * All {@link Timer} data is accessed via this model.
56 */
57final 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.
344     *
345     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
346     */
347    void resetExpiredTimers(@StringRes int eventLabelId) {
348        final List<Timer> timers = new ArrayList<>(getTimers());
349        for (Timer timer : timers) {
350            if (timer.isExpired()) {
351                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
352            }
353        }
354
355        // Update the notifications once after all timers are updated.
356        updateHeadsUpNotification();
357    }
358
359    /**
360     * Reset all missed timers.
361     *
362     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
363     */
364    void resetMissedTimers(@StringRes int eventLabelId) {
365        final List<Timer> timers = new ArrayList<>(getTimers());
366        for (Timer timer : timers) {
367            if (timer.isMissed()) {
368                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
369            }
370        }
371
372        // Update the notifications once after all timers are updated.
373        updateMissedNotification();
374    }
375
376    /**
377     * Reset all unexpired timers.
378     *
379     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
380     */
381    void resetUnexpiredTimers(@StringRes int eventLabelId) {
382        final List<Timer> timers = new ArrayList<>(getTimers());
383        for (Timer timer : timers) {
384            if (timer.isRunning() || timer.isPaused()) {
385                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
386            }
387        }
388
389        // Update the notification once after all timers are updated.
390        updateNotification();
391        // Heads-Up notification is unaffected by this change
392    }
393
394    /**
395     * @return the uri of the default ringtone to play for all timers when no user selection exists
396     */
397    Uri getDefaultTimerRingtoneUri() {
398        return mSettingsModel.getDefaultTimerRingtoneUri();
399    }
400
401    /**
402     * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
403     */
404    boolean isTimerRingtoneSilent() {
405        return Uri.EMPTY.equals(getTimerRingtoneUri());
406    }
407
408    /**
409     * @return the uri of the ringtone to play for all timers
410     */
411    Uri getTimerRingtoneUri() {
412        if (mTimerRingtoneUri == null) {
413            mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri();
414        }
415
416        return mTimerRingtoneUri;
417    }
418
419    /**
420     * @param uri the uri of the ringtone to play for all timers
421     */
422    void setTimerRingtoneUri(Uri uri) {
423        mSettingsModel.setTimerRingtoneUri(uri);
424    }
425
426    /**
427     * @return the title of the ringtone that is played for all timers
428     */
429    String getTimerRingtoneTitle() {
430        if (mTimerRingtoneTitle == null) {
431            if (isTimerRingtoneSilent()) {
432                // Special case: no ringtone has a title of "Silent".
433                mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title);
434            } else {
435                final Uri defaultUri = getDefaultTimerRingtoneUri();
436                final Uri uri = getTimerRingtoneUri();
437
438                if (defaultUri.equals(uri)) {
439                    // Special case: default ringtone has a title of "Timer Expired".
440                    mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
441                } else {
442                    mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri);
443                }
444            }
445        }
446
447        return mTimerRingtoneTitle;
448    }
449
450    /**
451     * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
452     *      {@code 0} implies no crescendo should be applied
453     */
454    long getTimerCrescendoDuration() {
455        return mSettingsModel.getTimerCrescendoDuration();
456    }
457
458    /**
459     * @return {@code true} if the device vibrates when timers expire
460     */
461    boolean getTimerVibrate() {
462        return mSettingsModel.getTimerVibrate();
463    }
464
465    /**
466     * @param enabled {@code true} if the device should vibrate when timers expire
467     */
468    void setTimerVibrate(boolean enabled) {
469        mSettingsModel.setTimerVibrate(enabled);
470    }
471
472    private List<Timer> getMutableTimers() {
473        if (mTimers == null) {
474            mTimers = TimerDAO.getTimers(mPrefs);
475            Collections.sort(mTimers, Timer.ID_COMPARATOR);
476        }
477
478        return mTimers;
479    }
480
481    private List<Timer> getMutableExpiredTimers() {
482        if (mExpiredTimers == null) {
483            mExpiredTimers = new ArrayList<>();
484
485            for (Timer timer : getMutableTimers()) {
486                if (timer.isExpired()) {
487                    mExpiredTimers.add(timer);
488                }
489            }
490            Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR);
491        }
492
493        return mExpiredTimers;
494    }
495
496    private List<Timer> getMutableMissedTimers() {
497        if (mMissedTimers == null) {
498            mMissedTimers = new ArrayList<>();
499
500            for (Timer timer : getMutableTimers()) {
501                if (timer.isMissed()) {
502                    mMissedTimers.add(timer);
503                }
504            }
505            Collections.sort(mMissedTimers, Timer.EXPIRY_COMPARATOR);
506        }
507
508        return mMissedTimers;
509    }
510
511    /**
512     * This method updates timer data without updating notifications. This is useful in bulk-update
513     * scenarios so the notifications are only rebuilt once.
514     *
515     * @param timer an updated timer to store
516     * @return the state of the timer prior to the update
517     */
518    private Timer doUpdateTimer(Timer timer) {
519        // Retrieve the cached form of the timer.
520        final List<Timer> timers = getMutableTimers();
521        final int index = timers.indexOf(timer);
522        final Timer before = timers.get(index);
523
524        // If no change occurred, ignore this update.
525        if (timer == before) {
526            return timer;
527        }
528
529        // Update the timer in permanent storage.
530        TimerDAO.updateTimer(mPrefs, timer);
531
532        // Update the timer in the cache.
533        final Timer oldTimer = timers.set(index, timer);
534
535        // Clear the cache of expired timers if the timer changed to/from expired.
536        if (before.isExpired() || timer.isExpired()) {
537            mExpiredTimers = null;
538        }
539        // Clear the cache of missed timers if the timer changed to/from missed.
540        if (before.isMissed() || timer.isMissed()) {
541            mMissedTimers = null;
542        }
543
544        // Update the timer expiration callback.
545        updateAlarmManager();
546
547        // Update the timer ringer.
548        updateRinger(before, timer);
549
550        // Notify listeners of the change.
551        for (TimerListener timerListener : mTimerListeners) {
552            timerListener.timerUpdated(before, timer);
553        }
554
555        return oldTimer;
556    }
557
558    /**
559     * This method removes timer data without updating notifications. This is useful in bulk-remove
560     * scenarios so the notifications are only rebuilt once.
561     *
562     * @param timer an existing timer to be removed
563     */
564    private void doRemoveTimer(Timer timer) {
565        // Remove the timer from permanent storage.
566        TimerDAO.removeTimer(mPrefs, timer);
567
568        // Remove the timer from the cache.
569        final List<Timer> timers = getMutableTimers();
570        final int index = timers.indexOf(timer);
571
572        // If the timer cannot be located there is nothing to remove.
573        if (index == -1) {
574            return;
575        }
576
577        timer = timers.remove(index);
578
579        // Clear the cache of expired timers if a new expired timer was added.
580        if (timer.isExpired()) {
581            mExpiredTimers = null;
582        }
583
584        // Clear the cache of missed timers if a new missed timer was added.
585        if (timer.isMissed()) {
586            mMissedTimers = null;
587        }
588
589        // Update the timer expiration callback.
590        updateAlarmManager();
591
592        // Update the timer ringer.
593        updateRinger(timer, null);
594
595        // Notify listeners of the change.
596        for (TimerListener timerListener : mTimerListeners) {
597            timerListener.timerRemoved(timer);
598        }
599    }
600
601    /**
602     * This method updates/removes timer data without updating notifications. This is useful in
603     * bulk-update scenarios so the notifications are only rebuilt once.
604     *
605     * If the given {@code timer} is expired and marked for deletion after use then this method
606     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
607     * to exist.
608     *
609     * @param timer the timer to be reset
610     * @param allowDelete  {@code true} if the timer is allowed to be deleted instead of reset
611     *                     (e.g. one use timers)
612     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
613     * @return the reset {@code timer} or {@code null} if the timer was deleted
614     */
615    private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete,
616            @StringRes int eventLabelId) {
617        if (allowDelete
618                && (timer.isExpired() || timer.isMissed())
619                && timer.getDeleteAfterUse()) {
620            doRemoveTimer(timer);
621            if (eventLabelId != 0) {
622                Events.sendTimerEvent(R.string.action_delete, eventLabelId);
623            }
624            return null;
625        } else if (!timer.isReset()) {
626            final Timer reset = timer.reset();
627            doUpdateTimer(reset);
628            if (eventLabelId != 0) {
629                Events.sendTimerEvent(R.string.action_reset, eventLabelId);
630            }
631            return reset;
632        }
633
634        return timer;
635    }
636
637    /**
638     * This method updates/removes timer data after a reboot without updating notifications.
639     *
640     * @param timer the timer to be updated
641     */
642    private void doUpdateAfterRebootTimer(Timer timer) {
643        Timer updated = timer.updateAfterReboot();
644        if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) {
645            updated = updated.miss();
646        }
647        doUpdateTimer(updated);
648    }
649
650    private void doUpdateAfterTimeSetTimer(Timer timer) {
651        final Timer updated = timer.updateAfterTimeSet();
652        doUpdateTimer(updated);
653    }
654
655
656    /**
657     * Updates the callback given to this application from the {@link AlarmManager} that signals the
658     * expiration of the next timer. If no timers are currently set to expire (i.e. no running
659     * timers exist) then this method clears the expiration callback from AlarmManager.
660     */
661    private void updateAlarmManager() {
662        // Locate the next firing timer if one exists.
663        Timer nextExpiringTimer = null;
664        for (Timer timer : getMutableTimers()) {
665            if (timer.isRunning()) {
666                if (nextExpiringTimer == null) {
667                    nextExpiringTimer = timer;
668                } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) {
669                    nextExpiringTimer = timer;
670                }
671            }
672        }
673
674        // Build the intent that signals the timer expiration.
675        final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer);
676
677        if (nextExpiringTimer == null) {
678            // Cancel the existing timer expiration callback.
679            final PendingIntent pi = PendingIntent.getService(mContext,
680                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
681            if (pi != null) {
682                mAlarmManager.cancel(pi);
683                pi.cancel();
684            }
685        } else {
686            // Update the existing timer expiration callback.
687            final PendingIntent pi = PendingIntent.getService(mContext,
688                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
689            schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi);
690        }
691    }
692
693    /**
694     * Starts and stops the ringer for timers if the change to the timer demands it.
695     *
696     * @param before the state of the timer before the change; {@code null} indicates added
697     * @param after the state of the timer after the change; {@code null} indicates delete
698     */
699    private void updateRinger(Timer before, Timer after) {
700        // Retrieve the states before and after the change.
701        final Timer.State beforeState = before == null ? null : before.getState();
702        final Timer.State afterState = after == null ? null : after.getState();
703
704        // If the timer state did not change, the ringer state is unchanged.
705        if (beforeState == afterState) {
706            return;
707        }
708
709        // If the timer is the first to expire, start ringing.
710        if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) {
711            AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext);
712            TimerKlaxon.start(mContext);
713        }
714
715        // If the expired timer was the last to reset, stop ringing.
716        if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) {
717            TimerKlaxon.stop(mContext);
718            AlarmAlertWakeLock.releaseCpuLock();
719        }
720    }
721
722    /**
723     * Updates the notification controlling unexpired timers. This notification is only displayed
724     * when the application is not open.
725     */
726    void updateNotification() {
727        // Notifications should be hidden if the app is open.
728        if (mNotificationModel.isApplicationInForeground()) {
729            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
730            return;
731        }
732
733        // Filter the timers to just include unexpired ones.
734        final List<Timer> unexpired = new ArrayList<>();
735        for (Timer timer : getMutableTimers()) {
736            if (timer.isRunning() || timer.isPaused()) {
737                unexpired.add(timer);
738            }
739        }
740
741        // If no unexpired timers exist, cancel the notification.
742        if (unexpired.isEmpty()) {
743            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
744            return;
745        }
746
747        // Sort the unexpired timers to locate the next one scheduled to expire.
748        Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR);
749
750        // Otherwise build and post a notification reflecting the latest unexpired timers.
751        final Notification notification =
752                mNotificationBuilder.build(mContext, mNotificationModel, unexpired);
753        final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
754        mNotificationManager.notify(notificationId, notification);
755
756    }
757
758    /**
759     * Updates the notification controlling missed timers. This notification is only displayed when
760     * the application is not open.
761     */
762    void updateMissedNotification() {
763        // Notifications should be hidden if the app is open.
764        if (mNotificationModel.isApplicationInForeground()) {
765            mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
766            return;
767        }
768
769        final List<Timer> missed = getMissedTimers();
770
771        if (missed.isEmpty()) {
772            mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
773            return;
774        }
775
776        final Notification notification = mNotificationBuilder.buildMissed(mContext,
777                mNotificationModel, missed);
778        final int notificationId = mNotificationModel.getMissedTimerNotificationId();
779        mNotificationManager.notify(notificationId, notification);
780    }
781
782    /**
783     * Updates the heads-up notification controlling expired timers. This heads-up notification is
784     * displayed whether the application is open or not.
785     */
786    private void updateHeadsUpNotification() {
787        // Nothing can be done with the heads-up notification without a valid service reference.
788        if (mService == null) {
789            return;
790        }
791
792        final List<Timer> expired = getExpiredTimers();
793
794        // If no expired timers exist, stop the service (which cancels the foreground notification).
795        if (expired.isEmpty()) {
796            mService.stopSelf();
797            mService = null;
798            return;
799        }
800
801        // Otherwise build and post a foreground notification reflecting the latest expired timers.
802        final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired);
803        final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
804        mService.startForeground(notificationId, notification);
805    }
806
807    /**
808     * Update the timer notification in response to a locale change.
809     */
810    private final class LocaleChangedReceiver extends BroadcastReceiver {
811        @Override
812        public void onReceive(Context context, Intent intent) {
813            mTimerRingtoneTitle = null;
814            updateNotification();
815            updateMissedNotification();
816            updateHeadsUpNotification();
817        }
818    }
819
820    /**
821     * This receiver is notified when shared preferences change. Cached information built on
822     * preferences must be cleared.
823     */
824    private final class PreferenceListener implements OnSharedPreferenceChangeListener {
825        @Override
826        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
827            switch (key) {
828                case SettingsActivity.KEY_TIMER_RINGTONE:
829                    mTimerRingtoneUri = null;
830                    mTimerRingtoneTitle = null;
831                    break;
832            }
833        }
834    }
835
836    static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) {
837        if (Utils.isMOrLater()) {
838            // Ensure the timer fires even if the device is dozing.
839            am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
840        } else {
841            am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
842        }
843    }
844}
845