1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.deskclock.data;
18
19import android.app.AlarmManager;
20import android.app.Notification;
21import android.app.PendingIntent;
22import android.app.Service;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.SharedPreferences;
28import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
29import android.media.Ringtone;
30import android.media.RingtoneManager;
31import android.net.Uri;
32import android.os.SystemClock;
33import android.preference.PreferenceManager;
34import android.support.annotation.DrawableRes;
35import android.support.annotation.StringRes;
36import android.support.annotation.VisibleForTesting;
37import android.support.v4.app.NotificationCompat;
38import android.support.v4.app.NotificationManagerCompat;
39import android.text.TextUtils;
40import android.util.ArraySet;
41
42import com.android.deskclock.AlarmAlertWakeLock;
43import com.android.deskclock.HandleDeskClockApiCalls;
44import com.android.deskclock.LogUtils;
45import com.android.deskclock.R;
46import com.android.deskclock.Utils;
47import com.android.deskclock.events.Events;
48import com.android.deskclock.settings.SettingsActivity;
49import com.android.deskclock.timer.ExpiredTimersActivity;
50import com.android.deskclock.timer.TimerKlaxon;
51import com.android.deskclock.timer.TimerService;
52
53import java.util.ArrayList;
54import java.util.Collections;
55import java.util.List;
56import java.util.Set;
57
58import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
59import static android.text.format.DateUtils.HOUR_IN_MILLIS;
60import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
61import static com.android.deskclock.data.Timer.State.EXPIRED;
62import static com.android.deskclock.data.Timer.State.RESET;
63
64/**
65 * All {@link Timer} data is accessed via this model.
66 */
67final class TimerModel {
68
69    private final Context mContext;
70
71    /** The alarm manager system service that calls back when timers expire. */
72    private final AlarmManager mAlarmManager;
73
74    /** The model from which settings are fetched. */
75    private final SettingsModel mSettingsModel;
76
77    /** The model from which notification data are fetched. */
78    private final NotificationModel mNotificationModel;
79
80    /** Used to create and destroy system notifications related to timers. */
81    private final NotificationManagerCompat mNotificationManager;
82
83    /** Update timer notification when locale changes. */
84    private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
85
86    /**
87     * Retain a hard reference to the shared preference observer to prevent it from being garbage
88     * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
89     */
90    private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
91
92    /** The listeners to notify when a timer is added, updated or removed. */
93    private final List<TimerListener> mTimerListeners = new ArrayList<>();
94
95    /**
96     * The ids of expired timers for which the ringer is ringing. Not all expired timers have their
97     * ids in this collection. If a timer was already expired when the app was started its id will
98     * be absent from this collection.
99     */
100    private final Set<Integer> mRingingIds = new ArraySet<>();
101
102    /** The uri of the ringtone to play for timers. */
103    private Uri mTimerRingtoneUri;
104
105    /** The title of the ringtone to play for timers. */
106    private String mTimerRingtoneTitle;
107
108    /** A mutable copy of the timers. */
109    private List<Timer> mTimers;
110
111    /** A mutable copy of the expired timers. */
112    private List<Timer> mExpiredTimers;
113
114    /**
115     * The service that keeps this application in the foreground while a heads-up timer
116     * notification is displayed. Marking the service as foreground prevents the operating system
117     * from killing this application while expired timers are actively firing.
118     */
119    private Service mService;
120
121    TimerModel(Context context, SettingsModel settingsModel, NotificationModel notificationModel) {
122        mContext = context;
123        mSettingsModel = settingsModel;
124        mNotificationModel = notificationModel;
125        mNotificationManager = NotificationManagerCompat.from(context);
126
127        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
128
129        // Clear caches affected by preferences when preferences change.
130        final SharedPreferences prefs = Utils.getDefaultSharedPreferences(mContext);
131        prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
132
133        // Update stopwatch notification when locale changes.
134        final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
135        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
136    }
137
138    /**
139     * @param timerListener to be notified when timers are added, updated and removed
140     */
141    void addTimerListener(TimerListener timerListener) {
142        mTimerListeners.add(timerListener);
143    }
144
145    /**
146     * @param timerListener to no longer be notified when timers are added, updated and removed
147     */
148    void removeTimerListener(TimerListener timerListener) {
149        mTimerListeners.remove(timerListener);
150    }
151
152    /**
153     * @return all defined timers in their creation order
154     */
155    List<Timer> getTimers() {
156        return Collections.unmodifiableList(getMutableTimers());
157    }
158
159    /**
160     * @return all expired timers in their expiration order
161     */
162    List<Timer> getExpiredTimers() {
163        return Collections.unmodifiableList(getMutableExpiredTimers());
164    }
165
166    /**
167     * @param timerId identifies the timer to return
168     * @return the timer with the given {@code timerId}
169     */
170    Timer getTimer(int timerId) {
171        for (Timer timer : getMutableTimers()) {
172            if (timer.getId() == timerId) {
173                return timer;
174            }
175        }
176
177        return null;
178    }
179
180    /**
181     * @return the timer that last expired and is still expired now; {@code null} if no timers are
182     *      expired
183     */
184    Timer getMostRecentExpiredTimer() {
185        final List<Timer> timers = getMutableExpiredTimers();
186        return timers.isEmpty() ? null : timers.get(timers.size() - 1);
187    }
188
189    /**
190     * @param length the length of the timer in milliseconds
191     * @param label describes the purpose of the timer
192     * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
193     * @return the newly added timer
194     */
195    Timer addTimer(long length, String label, boolean deleteAfterUse) {
196        // Create the timer instance.
197        Timer timer = new Timer(-1, RESET, length, length, Long.MIN_VALUE, length, label,
198                deleteAfterUse);
199
200        // Add the timer to permanent storage.
201        timer = TimerDAO.addTimer(mContext, timer);
202
203        // Add the timer to the cache.
204        getMutableTimers().add(0, timer);
205
206        // Update the timer notification.
207        updateNotification();
208        // Heads-Up notification is unaffected by this change
209
210        // Notify listeners of the change.
211        for (TimerListener timerListener : mTimerListeners) {
212            timerListener.timerAdded(timer);
213        }
214
215        return timer;
216    }
217
218    /**
219     * @param service used to start foreground notifications related to expired timers
220     * @param timer the timer to be expired
221     */
222    void expireTimer(Service service, Timer timer) {
223        if (mService == null) {
224            // If this is the first expired timer, retain the service that will be used to start
225            // the heads-up notification in the foreground.
226            mService = service;
227        } else if (mService != service) {
228            // If this is not the first expired timer, the service should match the one given when
229            // the first timer expired.
230            LogUtils.wtf("Expected TimerServices to be identical");
231        }
232
233        updateTimer(timer.expire());
234    }
235
236    /**
237     * @param timer an updated timer to store
238     */
239    void updateTimer(Timer timer) {
240        final Timer before = doUpdateTimer(timer);
241
242        // Update the notification after updating the timer data.
243        updateNotification();
244
245        // If the timer started or stopped being expired, update the heads-up notification.
246        if (before.getState() != timer.getState()) {
247            if (before.isExpired() || timer.isExpired()) {
248                updateHeadsUpNotification();
249            }
250        }
251    }
252
253    /**
254     * @param timer an existing timer to be removed
255     */
256    void removeTimer(Timer timer) {
257        doRemoveTimer(timer);
258
259        // Update the timer notifications after removing the timer data.
260        updateNotification();
261        if (timer.isExpired()) {
262            updateHeadsUpNotification();
263        }
264    }
265
266    /**
267     * If the given {@code timer} is expired and marked for deletion after use then this method
268     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
269     * to exist.
270     *
271     * @param timer the timer to be reset
272     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
273     */
274    void resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
275        doResetOrDeleteTimer(timer, eventLabelId);
276
277        // Update the notification after updating the timer data.
278        updateNotification();
279
280        // If the timer stopped being expired, update the heads-up notification.
281        if (timer.isExpired()) {
282            updateHeadsUpNotification();
283        }
284    }
285
286    /**
287     * Reset all timers.
288     *
289     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
290     */
291    void resetTimers(@StringRes int eventLabelId) {
292        final List<Timer> timers = new ArrayList<>(getTimers());
293        for (Timer timer : timers) {
294            doResetOrDeleteTimer(timer, eventLabelId);
295        }
296
297        // Update the notifications once after all timers are reset.
298        updateNotification();
299        updateHeadsUpNotification();
300    }
301
302    /**
303     * Reset all expired timers.
304     *
305     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
306     */
307    void resetExpiredTimers(@StringRes int eventLabelId) {
308        final List<Timer> timers = new ArrayList<>(getTimers());
309        for (Timer timer : timers) {
310            if (timer.isExpired()) {
311                doResetOrDeleteTimer(timer, eventLabelId);
312            }
313        }
314
315        // Update the notifications once after all timers are updated.
316        updateNotification();
317        updateHeadsUpNotification();
318    }
319
320    /**
321     * Reset all unexpired timers.
322     *
323     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
324     */
325    void resetUnexpiredTimers(@StringRes int eventLabelId) {
326        final List<Timer> timers = new ArrayList<>(getTimers());
327        for (Timer timer : timers) {
328            if (timer.isRunning() || timer.isPaused()) {
329                doResetOrDeleteTimer(timer, eventLabelId);
330            }
331        }
332
333        // Update the notification once after all timers are updated.
334        updateNotification();
335        // Heads-Up notification is unaffected by this change
336    }
337
338    /**
339     * @return the uri of the default ringtone to play for all timers when no user selection exists
340     */
341    Uri getDefaultTimerRingtoneUri() {
342        return mSettingsModel.getDefaultTimerRingtoneUri();
343    }
344
345    /**
346     * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
347     */
348    boolean isTimerRingtoneSilent() {
349        return Uri.EMPTY.equals(getTimerRingtoneUri());
350    }
351
352    /**
353     * @return the uri of the ringtone to play for all timers
354     */
355    Uri getTimerRingtoneUri() {
356        if (mTimerRingtoneUri == null) {
357            mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri();
358        }
359
360        return mTimerRingtoneUri;
361    }
362
363    /**
364     * @return the title of the ringtone that is played for all timers
365     */
366    String getTimerRingtoneTitle() {
367        if (mTimerRingtoneTitle == null) {
368            if (isTimerRingtoneSilent()) {
369                // Special case: no ringtone has a title of "Silent".
370                mTimerRingtoneTitle = mContext.getString(R.string.silent_timer_ringtone_title);
371            } else {
372                final Uri defaultUri = getDefaultTimerRingtoneUri();
373                final Uri uri = getTimerRingtoneUri();
374
375                if (defaultUri.equals(uri)) {
376                    // Special case: default ringtone has a title of "Timer Expired".
377                    mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
378                } else {
379                    final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
380                    mTimerRingtoneTitle = ringtone.getTitle(mContext);
381                }
382            }
383        }
384
385        return mTimerRingtoneTitle;
386    }
387
388    private List<Timer> getMutableTimers() {
389        if (mTimers == null) {
390            mTimers = TimerDAO.getTimers(mContext);
391            Collections.sort(mTimers, Timer.ID_COMPARATOR);
392        }
393
394        return mTimers;
395    }
396
397    private List<Timer> getMutableExpiredTimers() {
398        if (mExpiredTimers == null) {
399            mExpiredTimers = new ArrayList<>();
400
401            for (Timer timer : getMutableTimers()) {
402                if (timer.isExpired()) {
403                    mExpiredTimers.add(timer);
404                }
405            }
406            Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR);
407        }
408
409        return mExpiredTimers;
410    }
411
412    /**
413     * This method updates timer data without updating notifications. This is useful in bulk-update
414     * scenarios so the notifications are only rebuilt once.
415     *
416     * @param timer an updated timer to store
417     * @return the state of the timer prior to the update
418     */
419    private Timer doUpdateTimer(Timer timer) {
420        // Retrieve the cached form of the timer.
421        final List<Timer> timers = getMutableTimers();
422        final int index = timers.indexOf(timer);
423        final Timer before = timers.get(index);
424
425        // If no change occurred, ignore this update.
426        if (timer == before) {
427            return timer;
428        }
429
430        // Update the timer in permanent storage.
431        TimerDAO.updateTimer(mContext, timer);
432
433        // Update the timer in the cache.
434        final Timer oldTimer = timers.set(index, timer);
435
436        // Clear the cache of expired timers if the timer changed to/from expired.
437        if (before.isExpired() || timer.isExpired()) {
438            mExpiredTimers = null;
439        }
440
441        // Update the timer expiration callback.
442        updateAlarmManager();
443
444        // Update the timer ringer.
445        updateRinger(before, timer);
446
447        // Notify listeners of the change.
448        for (TimerListener timerListener : mTimerListeners) {
449            timerListener.timerUpdated(before, timer);
450        }
451
452        return oldTimer;
453    }
454
455    /**
456     * This method removes timer data without updating notifications. This is useful in bulk-remove
457     * scenarios so the notifications are only rebuilt once.
458     *
459     * @param timer an existing timer to be removed
460     */
461    void doRemoveTimer(Timer timer) {
462        // Remove the timer from permanent storage.
463        TimerDAO.removeTimer(mContext, timer);
464
465        // Remove the timer from the cache.
466        final List<Timer> timers = getMutableTimers();
467        final int index = timers.indexOf(timer);
468
469        // If the timer cannot be located there is nothing to remove.
470        if (index == -1) {
471            return;
472        }
473
474        timer = timers.remove(index);
475
476        // Clear the cache of expired timers if a new expired timer was added.
477        if (timer.isExpired()) {
478            mExpiredTimers = null;
479        }
480
481        // Update the timer expiration callback.
482        updateAlarmManager();
483
484        // Update the timer ringer.
485        updateRinger(timer, null);
486
487        // Notify listeners of the change.
488        for (TimerListener timerListener : mTimerListeners) {
489            timerListener.timerRemoved(timer);
490        }
491    }
492
493    /**
494     * This method updates/removes timer data without updating notifications. This is useful in
495     * bulk-update scenarios so the notifications are only rebuilt once.
496     *
497     * If the given {@code timer} is expired and marked for deletion after use then this method
498     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
499     * to exist.
500     *
501     * @param timer the timer to be reset
502     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
503     */
504    private void doResetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
505        if (timer.isExpired() && timer.getDeleteAfterUse()) {
506            doRemoveTimer(timer);
507            if (eventLabelId != 0) {
508                Events.sendTimerEvent(R.string.action_delete, eventLabelId);
509            }
510        } else if (!timer.isReset()) {
511            doUpdateTimer(timer.reset());
512            if (eventLabelId != 0) {
513                Events.sendTimerEvent(R.string.action_reset, eventLabelId);
514            }
515        }
516    }
517
518    /**
519     * Updates the callback given to this application from the {@link AlarmManager} that signals the
520     * expiration of the next timer. If no timers are currently set to expire (i.e. no running
521     * timers exist) then this method clears the expiration callback from AlarmManager.
522     */
523    private void updateAlarmManager() {
524        // Locate the next firing timer if one exists.
525        Timer nextExpiringTimer = null;
526        for (Timer timer : getMutableTimers()) {
527            if (timer.isRunning()) {
528                if (nextExpiringTimer == null) {
529                    nextExpiringTimer = timer;
530                } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) {
531                    nextExpiringTimer = timer;
532                }
533            }
534        }
535
536        // Build the intent that signals the timer expiration.
537        final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer);
538
539        if (nextExpiringTimer == null) {
540            // Cancel the existing timer expiration callback.
541            final PendingIntent pi = PendingIntent.getService(mContext,
542                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
543            if (pi != null) {
544                mAlarmManager.cancel(pi);
545                pi.cancel();
546            }
547        } else {
548            // Update the existing timer expiration callback.
549            final PendingIntent pi = PendingIntent.getService(mContext,
550                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
551            schedulePendingIntent(nextExpiringTimer.getExpirationTime(), pi);
552        }
553    }
554
555    /**
556     * Starts and stops the ringer for timers if the change to the timer demands it.
557     *
558     * @param before the state of the timer before the change; {@code null} indicates added
559     * @param after the state of the timer after the change; {@code null} indicates delete
560     */
561    private void updateRinger(Timer before, Timer after) {
562        // Retrieve the states before and after the change.
563        final Timer.State beforeState = before == null ? null : before.getState();
564        final Timer.State afterState = after == null ? null : after.getState();
565
566        // If the timer state did not change, the ringer state is unchanged.
567        if (beforeState == afterState) {
568            return;
569        }
570
571        // If the timer is the first to expire, start ringing.
572        if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) {
573            AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext);
574            TimerKlaxon.start(mContext);
575        }
576
577        // If the expired timer was the last to reset, stop ringing.
578        if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) {
579            TimerKlaxon.stop(mContext);
580            AlarmAlertWakeLock.releaseCpuLock();
581        }
582    }
583
584    /**
585     * Updates the notification controlling unexpired timers. This notification is only displayed
586     * when the application is not open.
587     */
588    void updateNotification() {
589        // Notifications should be hidden if the app is open.
590        if (mNotificationModel.isApplicationInForeground()) {
591            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
592            return;
593        }
594
595        // Filter the timers to just include unexpired ones.
596        final List<Timer> unexpired = new ArrayList<>();
597        for (Timer timer : getMutableTimers()) {
598            if (timer.isRunning() || timer.isPaused()) {
599                unexpired.add(timer);
600            }
601        }
602
603        // If no unexpired timers exist, cancel the notification.
604        if (unexpired.isEmpty()) {
605            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
606            return;
607        }
608
609        // Sort the unexpired timers to locate the next one scheduled to expire.
610        Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR);
611        final Timer timer = unexpired.get(0);
612        final long remainingTime = timer.getRemainingTime();
613
614        // Generate some descriptive text, a title, and some actions based on timer states.
615        final String contentText;
616        final String contentTitle;
617        @DrawableRes int firstActionIconId, secondActionIconId = 0;
618        @StringRes int firstActionTitleId, secondActionTitleId = 0;
619        Intent firstActionIntent, secondActionIntent = null;
620
621        if (unexpired.size() == 1) {
622            contentText = formatElapsedTimeUntilExpiry(remainingTime);
623
624            if (timer.isRunning()) {
625                // Single timer is running.
626                if (TextUtils.isEmpty(timer.getLabel())) {
627                    contentTitle = mContext.getString(R.string.timer_notification_label);
628                } else {
629                    contentTitle = timer.getLabel();
630                }
631
632                firstActionIconId = R.drawable.ic_pause_24dp;
633                firstActionTitleId = R.string.timer_pause;
634                firstActionIntent = new Intent(mContext, TimerService.class)
635                        .setAction(HandleDeskClockApiCalls.ACTION_PAUSE_TIMER)
636                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
637
638                secondActionIconId = R.drawable.ic_add_24dp;
639                secondActionTitleId = R.string.timer_plus_1_min;
640                secondActionIntent = new Intent(mContext, TimerService.class)
641                        .setAction(HandleDeskClockApiCalls.ACTION_ADD_MINUTE_TIMER)
642                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
643            } else {
644                // Single timer is paused.
645                contentTitle = mContext.getString(R.string.timer_paused);
646
647                firstActionIconId = R.drawable.ic_start_24dp;
648                firstActionTitleId = R.string.sw_resume_button;
649                firstActionIntent = new Intent(mContext, TimerService.class)
650                        .setAction(HandleDeskClockApiCalls.ACTION_START_TIMER)
651                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
652
653                secondActionIconId = R.drawable.ic_reset_24dp;
654                secondActionTitleId = R.string.sw_reset_button;
655                secondActionIntent = new Intent(mContext, TimerService.class)
656                        .setAction(HandleDeskClockApiCalls.ACTION_RESET_TIMER)
657                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
658            }
659        } else {
660            if (timer.isRunning()) {
661                // At least one timer is running.
662                final String timeRemaining = formatElapsedTimeUntilExpiry(remainingTime);
663                contentText = mContext.getString(R.string.next_timer_notif, timeRemaining);
664                contentTitle = mContext.getString(R.string.timers_in_use, unexpired.size());
665            } else {
666                // All timers are paused.
667                contentText = mContext.getString(R.string.all_timers_stopped_notif);
668                contentTitle = mContext.getString(R.string.timers_stopped, unexpired.size());
669            }
670
671            firstActionIconId = R.drawable.ic_reset_24dp;
672            firstActionTitleId = R.string.timer_reset_all;
673            firstActionIntent = TimerService.createResetUnexpiredTimersIntent(mContext);
674        }
675
676        // Intent to load the app and show the timer when the notification is tapped.
677        final Intent showApp = new Intent(mContext, HandleDeskClockApiCalls.class)
678                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
679                .setAction(HandleDeskClockApiCalls.ACTION_SHOW_TIMERS)
680                .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId())
681                .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, R.string.label_notification);
682
683        final PendingIntent pendingShowApp = PendingIntent.getActivity(mContext, 0, showApp,
684                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
685
686        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
687                .setOngoing(true)
688                .setLocalOnly(true)
689                .setShowWhen(false)
690                .setAutoCancel(false)
691                .setContentText(contentText)
692                .setContentTitle(contentTitle)
693                .setContentIntent(pendingShowApp)
694                .setSmallIcon(R.drawable.stat_notify_timer)
695                .setPriority(NotificationCompat.PRIORITY_HIGH)
696                .setCategory(NotificationCompat.CATEGORY_ALARM)
697                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
698
699        final PendingIntent firstAction = PendingIntent.getService(mContext, 0,
700                firstActionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
701        final String firstActionTitle = mContext.getString(firstActionTitleId);
702        builder.addAction(firstActionIconId, firstActionTitle, firstAction);
703
704        if (secondActionIntent != null) {
705            final PendingIntent secondAction = PendingIntent.getService(mContext, 0,
706                    secondActionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
707            final String secondActionTitle = mContext.getString(secondActionTitleId);
708            builder.addAction(secondActionIconId, secondActionTitle, secondAction);
709        }
710
711        // Update the notification.
712        final Notification notification = builder.build();
713        final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
714        mNotificationManager.notify(notificationId, notification);
715
716        final Intent updateNotification = TimerService.createUpdateNotificationIntent(mContext);
717        if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
718            // Schedule a callback to update the time-sensitive information of the running timer.
719            final PendingIntent pi = PendingIntent.getService(mContext, 0, updateNotification,
720                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
721
722            final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
723            final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
724
725            schedulePendingIntent(triggerTime, pi);
726        } else {
727            // Cancel the update notification callback.
728            final PendingIntent pi = PendingIntent.getService(mContext, 0, updateNotification,
729                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
730            if (pi != null) {
731                mAlarmManager.cancel(pi);
732                pi.cancel();
733            }
734        }
735    }
736
737    /**
738     * Updates the heads-up notification controlling expired timers. This heads-up notification is
739     * displayed whether the application is open or not.
740     */
741    private void updateHeadsUpNotification() {
742        // Nothing can be done with the heads-up notification without a valid service reference.
743        if (mService == null) {
744            return;
745        }
746
747        final List<Timer> expired = getExpiredTimers();
748
749        // If no expired timers exist, stop the service (which cancels the foreground notification).
750        if (expired.isEmpty()) {
751            mService.stopSelf();
752            mService = null;
753            return;
754        }
755
756        // Generate some descriptive text, a title, and an action name based on the timer count.
757        final int timerId;
758        final String contentText;
759        final String contentTitle;
760        final String resetActionTitle;
761        if (expired.size() > 1) {
762            timerId = -1;
763            contentText = mContext.getString(R.string.timer_multi_times_up, expired.size());
764            contentTitle = mContext.getString(R.string.timer_notification_label);
765            resetActionTitle = mContext.getString(R.string.timer_stop_all);
766        } else {
767            final Timer timer = expired.get(0);
768            timerId = timer.getId();
769            resetActionTitle = mContext.getString(R.string.timer_stop);
770            contentText = mContext.getString(R.string.timer_times_up);
771
772            final String label = timer.getLabel();
773            if (TextUtils.isEmpty(label)) {
774                contentTitle = mContext.getString(R.string.timer_notification_label);
775            } else {
776                contentTitle = label;
777            }
778        }
779
780        // Content intent shows the timer full screen when clicked.
781        final Intent content = new Intent(mContext, ExpiredTimersActivity.class);
782        final PendingIntent pendingContent = PendingIntent.getActivity(mContext, 0, content,
783                PendingIntent.FLAG_UPDATE_CURRENT);
784
785        // Full screen intent has flags so it is different than the content intent.
786        final Intent fullScreen = new Intent(mContext, ExpiredTimersActivity.class)
787                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
788        final PendingIntent pendingFullScreen = PendingIntent.getActivity(mContext, 0, fullScreen,
789                PendingIntent.FLAG_UPDATE_CURRENT);
790
791        // First action intent is either reset single timer or reset all timers.
792        final Intent reset = TimerService.createResetExpiredTimersIntent(mContext);
793        final PendingIntent pendingReset = PendingIntent.getService(mContext, 0, reset,
794                PendingIntent.FLAG_UPDATE_CURRENT);
795
796        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
797                .setWhen(0)
798                .setOngoing(true)
799                .setLocalOnly(true)
800                .setAutoCancel(false)
801                .setContentText(contentText)
802                .setContentTitle(contentTitle)
803                .setContentIntent(pendingContent)
804                .setSmallIcon(R.drawable.stat_notify_timer)
805                .setFullScreenIntent(pendingFullScreen, true)
806                .setPriority(NotificationCompat.PRIORITY_MAX)
807                .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
808                .addAction(R.drawable.ic_stop_24dp, resetActionTitle, pendingReset);
809
810        // Add a second action if only a single timer is expired.
811        if (expired.size() == 1) {
812            // Second action intent adds a minute to a single timer.
813            final Intent addMinute = TimerService.createAddMinuteTimerIntent(mContext, timerId);
814            final PendingIntent pendingAddMinute = PendingIntent.getService(mContext, 0, addMinute,
815                    PendingIntent.FLAG_UPDATE_CURRENT);
816            final String addMinuteTitle = mContext.getString(R.string.timer_plus_1_min);
817            builder.addAction(R.drawable.ic_add_24dp, addMinuteTitle, pendingAddMinute);
818        }
819
820        // Update the notification.
821        final Notification notification = builder.build();
822        final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
823        mService.startForeground(notificationId, notification);
824    }
825
826    /**
827     * Format "7 hours 52 minutes remaining"
828     */
829    @VisibleForTesting
830    String formatElapsedTimeUntilExpiry(long remainingTime) {
831        final int hours = (int) remainingTime / (int) HOUR_IN_MILLIS;
832        final int minutes = (int) remainingTime / ((int) MINUTE_IN_MILLIS) % 60;
833
834        String minSeq = Utils.getNumberFormattedQuantityString(mContext, R.plurals.minutes, minutes);
835        String hourSeq = Utils.getNumberFormattedQuantityString(mContext, R.plurals.hours, hours);
836
837        // The verb "remaining" may have to change tense for singular subjects in some languages.
838        final String verb = mContext.getString((minutes > 1 || hours > 1)
839                ? R.string.timer_remaining_multiple
840                : R.string.timer_remaining_single);
841
842        final boolean showHours = hours > 0;
843        final boolean showMinutes = minutes > 0;
844
845        int formatStringId;
846        if (showHours) {
847            if (showMinutes) {
848                formatStringId = R.string.timer_notifications_hours_minutes;
849            } else {
850                formatStringId = R.string.timer_notifications_hours;
851            }
852        } else if (showMinutes) {
853            formatStringId = R.string.timer_notifications_minutes;
854        } else {
855            formatStringId = R.string.timer_notifications_less_min;
856        }
857        return String.format(mContext.getString(formatStringId), hourSeq, minSeq, verb);
858    }
859
860    private void schedulePendingIntent(long triggerTime, PendingIntent pi) {
861        if (Utils.isMOrLater()) {
862            // Make sure the timer fires when the device is in doze mode. The timer is not
863            // guaranteed to fire at the requested time. It may be delayed up to 15 minutes.
864            mAlarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
865        } else {
866            mAlarmManager.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
867        }
868    }
869
870    /**
871     * Update the stopwatch notification in response to a locale change.
872     */
873    private final class LocaleChangedReceiver extends BroadcastReceiver {
874        @Override
875        public void onReceive(Context context, Intent intent) {
876            updateNotification();
877            updateHeadsUpNotification();
878        }
879    }
880
881    /**
882     * This receiver is notified when shared preferences change. Cached information built on
883     * preferences must be cleared.
884     */
885    private final class PreferenceListener implements OnSharedPreferenceChangeListener {
886        @Override
887        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
888            switch (key) {
889                case SettingsActivity.KEY_TIMER_RINGTONE:
890                    mTimerRingtoneUri = null;
891                    mTimerRingtoneTitle = null;
892                    break;
893            }
894        }
895    }
896}