• 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 androidx.annotation.StringRes;
32 import androidx.core.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. 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      */
resetOrDeleteExpiredTimers(@tringRes int eventLabelId)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      */
resetMissedTimers(@tringRes int eventLabelId)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      */
resetUnexpiredTimers(@tringRes int eventLabelId)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      */
getDefaultTimerRingtoneUri()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      */
isTimerRingtoneSilent()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      */
getTimerRingtoneUri()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      */
setTimerRingtoneUri(Uri uri)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      */
getTimerRingtoneTitle()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      */
getTimerCrescendoDuration()455     long getTimerCrescendoDuration() {
456         return mSettingsModel.getTimerCrescendoDuration();
457     }
458 
459     /**
460      * @return {@code true} if the device vibrates when timers expire
461      */
getTimerVibrate()462     boolean getTimerVibrate() {
463         return mSettingsModel.getTimerVibrate();
464     }
465 
466     /**
467      * @param enabled {@code true} if the device should vibrate when timers expire
468      */
setTimerVibrate(boolean enabled)469     void setTimerVibrate(boolean enabled) {
470         mSettingsModel.setTimerVibrate(enabled);
471     }
472 
getMutableTimers()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 
getMutableExpiredTimers()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 
getMutableMissedTimers()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      */
doUpdateTimer(Timer timer)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      */
doRemoveTimer(Timer timer)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      */
doResetOrDeleteTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId)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      */
doUpdateAfterRebootTimer(Timer timer)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 
doUpdateAfterTimeSetTimer(Timer timer)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      */
updateAlarmManager()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      */
updateRinger(Timer before, Timer after)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      */
updateNotification()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      */
updateMissedNotification()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      */
updateHeadsUpNotification()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
onReceive(Context context, Intent intent)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
onSharedPreferenceChanged(SharedPreferences prefs, String key)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 
schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi)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