• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TimerModel(Context context, SharedPreferences prefs, SettingsModel settingsModel, RingtoneModel ringtoneModel, NotificationModel notificationModel)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      */
addTimerListener(TimerListener timerListener)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      */
removeTimerListener(TimerListener timerListener)160     void removeTimerListener(TimerListener timerListener) {
161         mTimerListeners.remove(timerListener);
162     }
163 
164     /**
165      * @return all defined timers in their creation order
166      */
getTimers()167     List<Timer> getTimers() {
168         return Collections.unmodifiableList(getMutableTimers());
169     }
170 
171     /**
172      * @return all expired timers in their expiration order
173      */
getExpiredTimers()174     List<Timer> getExpiredTimers() {
175         return Collections.unmodifiableList(getMutableExpiredTimers());
176     }
177 
178     /**
179      * @return all missed timers in their expiration order
180      */
getMissedTimers()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      */
getTimer(int timerId)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      */
getMostRecentExpiredTimer()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      */
addTimer(long length, String label, boolean deleteAfterUse)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      */
expireTimer(Service service, Timer timer)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      */
updateTimer(Timer timer)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      */
removeTimer(Timer timer)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      */
resetTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId)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      */
updateTimersAfterReboot()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      */
updateTimersAfterTimeSet()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      */
resetExpiredTimers(@tringRes int eventLabelId)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      */
resetMissedTimers(@tringRes int eventLabelId)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      */
resetUnexpiredTimers(@tringRes int eventLabelId)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      */
getDefaultTimerRingtoneUri()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      */
isTimerRingtoneSilent()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      */
getTimerRingtoneUri()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      */
setTimerRingtoneUri(Uri uri)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      */
getTimerRingtoneTitle()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      */
getTimerCrescendoDuration()454     long getTimerCrescendoDuration() {
455         return mSettingsModel.getTimerCrescendoDuration();
456     }
457 
458     /**
459      * @return {@code true} if the device vibrates when timers expire
460      */
getTimerVibrate()461     boolean getTimerVibrate() {
462         return mSettingsModel.getTimerVibrate();
463     }
464 
465     /**
466      * @param enabled {@code true} if the device should vibrate when timers expire
467      */
setTimerVibrate(boolean enabled)468     void setTimerVibrate(boolean enabled) {
469         mSettingsModel.setTimerVibrate(enabled);
470     }
471 
getMutableTimers()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 
getMutableExpiredTimers()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 
getMutableMissedTimers()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      */
doUpdateTimer(Timer timer)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      */
doRemoveTimer(Timer timer)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      */
doResetOrDeleteTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId)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      */
doUpdateAfterRebootTimer(Timer timer)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 
doUpdateAfterTimeSetTimer(Timer timer)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      */
updateAlarmManager()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      */
updateRinger(Timer before, Timer after)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      */
updateNotification()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      */
updateMissedNotification()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      */
updateHeadsUpNotification()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
onReceive(Context context, Intent intent)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
onSharedPreferenceChanged(SharedPreferences prefs, String key)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 
schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi)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