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