1856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux/*
2856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * Copyright (C) 2015 The Android Open Source Project
3856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux *
4856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * Licensed under the Apache License, Version 2.0 (the "License");
5856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * you may not use this file except in compliance with the License.
6856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * You may obtain a copy of the License at
7856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux *
8856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux *      http://www.apache.org/licenses/LICENSE-2.0
9856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux *
10856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * Unless required by applicable law or agreed to in writing, software
11856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * distributed under the License is distributed on an "AS IS" BASIS,
12856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * See the License for the specific language governing permissions and
14856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux * limitations under the License.
15856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux */
16856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
17856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuxpackage com.android.deskclock.data;
18856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.app.AlarmManager;
206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.app.Notification;
216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.app.PendingIntent;
220dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieuximport android.app.Service;
236d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.content.BroadcastReceiver;
24856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.content.Context;
256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.content.Intent;
266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.content.IntentFilter;
27856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.content.SharedPreferences;
28856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.content.SharedPreferences.OnSharedPreferenceChangeListener;
29856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.media.Ringtone;
30856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.media.RingtoneManager;
31856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.net.Uri;
326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.os.SystemClock;
33856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport android.preference.PreferenceManager;
346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.support.annotation.DrawableRes;
356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.support.annotation.StringRes;
366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.support.annotation.VisibleForTesting;
376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.support.v4.app.NotificationCompat;
386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.support.v4.app.NotificationManagerCompat;
396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.text.TextUtils;
406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport android.util.ArraySet;
41856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
426d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.AlarmAlertWakeLock;
436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.HandleDeskClockApiCalls;
440dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieuximport com.android.deskclock.LogUtils;
45856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuximport com.android.deskclock.R;
466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.Utils;
476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.events.Events;
48bd9eae10b13e015d1997d06f13e9abe06a7f306bJames Lemieuximport com.android.deskclock.settings.SettingsActivity;
496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.timer.ExpiredTimersActivity;
506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.timer.TimerKlaxon;
516d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport com.android.deskclock.timer.TimerService;
526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport java.util.ArrayList;
546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport java.util.Collections;
556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport java.util.List;
566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport java.util.Set;
576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport static android.text.format.DateUtils.HOUR_IN_MILLIS;
606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport static android.text.format.DateUtils.MINUTE_IN_MILLIS;
616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport static com.android.deskclock.data.Timer.State.EXPIRED;
626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieuximport static com.android.deskclock.data.Timer.State.RESET;
63856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
64856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux/**
656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux * All {@link Timer} data is accessed via this model.
66856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux */
67856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieuxfinal class TimerModel {
68856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
69856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    private final Context mContext;
70856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** The alarm manager system service that calls back when timers expire. */
726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final AlarmManager mAlarmManager;
736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
74856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    /** The model from which settings are fetched. */
75856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    private final SettingsModel mSettingsModel;
76856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** The model from which notification data are fetched. */
786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final NotificationModel mNotificationModel;
796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** Used to create and destroy system notifications related to timers. */
816d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final NotificationManagerCompat mNotificationManager;
826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** Update timer notification when locale changes. */
846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
86856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    /**
87856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux     * Retain a hard reference to the shared preference observer to prevent it from being garbage
88856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux     * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
89856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux     */
90856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
91856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
926d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** The listeners to notify when a timer is added, updated or removed. */
936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final List<TimerListener> mTimerListeners = new ArrayList<>();
946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * The ids of expired timers for which the ringer is ringing. Not all expired timers have their
976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * ids in this collection. If a timer was already expired when the app was started its id will
986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * be absent from this collection.
996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final Set<Integer> mRingingIds = new ArraySet<>();
1016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
102856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    /** The uri of the ringtone to play for timers. */
103856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    private Uri mTimerRingtoneUri;
104856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
105856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    /** The title of the ringtone to play for timers. */
106856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    private String mTimerRingtoneTitle;
107856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
1086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** A mutable copy of the timers. */
1096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private List<Timer> mTimers;
1106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /** A mutable copy of the expired timers. */
1126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private List<Timer> mExpiredTimers;
1136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1140dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux    /**
1150dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     * The service that keeps this application in the foreground while a heads-up timer
1160dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     * notification is displayed. Marking the service as foreground prevents the operating system
1170dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     * from killing this application while expired timers are actively firing.
1180dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     */
1190dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux    private Service mService;
1200dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux
1216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    TimerModel(Context context, SettingsModel settingsModel, NotificationModel notificationModel) {
122856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        mContext = context;
123856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        mSettingsModel = settingsModel;
1246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mNotificationModel = notificationModel;
1256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mNotificationManager = NotificationManagerCompat.from(context);
1266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
128856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
129856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        // Clear caches affected by preferences when preferences change.
1305239f09fa7309686c66d1fc70c6eacf7bdab0ab8Justin Klaassen        final SharedPreferences prefs = Utils.getDefaultSharedPreferences(mContext);
131856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
1326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update stopwatch notification when locale changes.
1346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
1356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
1366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
1376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timerListener to be notified when timers are added, updated and removed
1406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void addTimerListener(TimerListener timerListener) {
1426d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mTimerListeners.add(timerListener);
1436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
1446d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timerListener to no longer be notified when timers are added, updated and removed
1476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void removeTimerListener(TimerListener timerListener) {
1496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mTimerListeners.remove(timerListener);
1506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
1516d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return all defined timers in their creation order
1546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    List<Timer> getTimers() {
1566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return Collections.unmodifiableList(getMutableTimers());
1576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
1586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return all expired timers in their expiration order
1616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    List<Timer> getExpiredTimers() {
1636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return Collections.unmodifiableList(getMutableExpiredTimers());
1646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
1656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timerId identifies the timer to return
1686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the timer with the given {@code timerId}
1696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    Timer getTimer(int timerId) {
1716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (Timer timer : getMutableTimers()) {
1726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.getId() == timerId) {
1736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                return timer;
1746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
1756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
1766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return null;
1786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
1796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
1806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1816d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the timer that last expired and is still expired now; {@code null} if no timers are
1826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *      expired
1836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
1846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    Timer getMostRecentExpiredTimer() {
1856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> timers = getMutableExpiredTimers();
1866d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return timers.isEmpty() ? null : timers.get(timers.size() - 1);
187856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    }
188856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
1896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
1906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param length the length of the timer in milliseconds
1916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param label describes the purpose of the timer
192437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux     * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
1936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the newly added timer
1946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
195437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux    Timer addTimer(long length, String label, boolean deleteAfterUse) {
1966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Create the timer instance.
1976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        Timer timer = new Timer(-1, RESET, length, length, Long.MIN_VALUE, length, label,
198437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                deleteAfterUse);
1996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Add the timer to permanent storage.
2016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        timer = TimerDAO.addTimer(mContext, timer);
2026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Add the timer to the cache.
2046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        getMutableTimers().add(0, timer);
2056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer notification.
2076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
2086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Heads-Up notification is unaffected by this change
2096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Notify listeners of the change.
2116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (TimerListener timerListener : mTimerListeners) {
2126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            timerListener.timerAdded(timer);
2136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
2146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return timer;
2166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
2176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
2190dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     * @param service used to start foreground notifications related to expired timers
2200dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     * @param timer the timer to be expired
2210dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux     */
2220dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux    void expireTimer(Service service, Timer timer) {
2230dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux        if (mService == null) {
2240dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux            // If this is the first expired timer, retain the service that will be used to start
2250dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux            // the heads-up notification in the foreground.
2260dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux            mService = service;
2270dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux        } else if (mService != service) {
2280dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux            // If this is not the first expired timer, the service should match the one given when
2290dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux            // the first timer expired.
2300dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux            LogUtils.wtf("Expected TimerServices to be identical");
2310dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux        }
2320dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux
2330dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux        updateTimer(timer.expire());
2340dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux    }
2350dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux
2360dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux    /**
2376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timer an updated timer to store
2386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
2396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void updateTimer(Timer timer) {
2406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Timer before = doUpdateTimer(timer);
2416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2426d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notification after updating the timer data.
2436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
2446d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If the timer started or stopped being expired, update the heads-up notification.
2466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (before.getState() != timer.getState()) {
2476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (before.isExpired() || timer.isExpired()) {
2486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                updateHeadsUpNotification();
2496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
2506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
2516d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
2526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
2546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timer an existing timer to be removed
2556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
2566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void removeTimer(Timer timer) {
2576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        doRemoveTimer(timer);
2586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer notifications after removing the timer data.
2606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
2616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (timer.isExpired()) {
2626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            updateHeadsUpNotification();
2636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
2646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
2656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
2676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * If the given {@code timer} is expired and marked for deletion after use then this method
2686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
2696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * to exist.
2706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
2716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timer the timer to be reset
2726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
2736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
2746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
2756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        doResetOrDeleteTimer(timer, eventLabelId);
2766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notification after updating the timer data.
2786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
2796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If the timer stopped being expired, update the heads-up notification.
2816d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (timer.isExpired()) {
2826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            updateHeadsUpNotification();
2836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
2846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
2856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2866d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
2876d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Reset all timers.
2886d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
2896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
2906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
2916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void resetTimers(@StringRes int eventLabelId) {
2926d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> timers = new ArrayList<>(getTimers());
2936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (Timer timer : timers) {
2946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            doResetOrDeleteTimer(timer, eventLabelId);
2956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
2966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
2976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notifications once after all timers are reset.
2986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
2996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateHeadsUpNotification();
3006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
3016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
3036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Reset all expired timers.
3046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
3056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
3066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
3076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void resetExpiredTimers(@StringRes int eventLabelId) {
3086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> timers = new ArrayList<>(getTimers());
3096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (Timer timer : timers) {
3106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.isExpired()) {
3116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                doResetOrDeleteTimer(timer, eventLabelId);
3126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
3136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
3146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notifications once after all timers are updated.
3166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
3176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateHeadsUpNotification();
3186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
3196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
3216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Reset all unexpired timers.
3226d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
3236d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
3246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
3256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void resetUnexpiredTimers(@StringRes int eventLabelId) {
3266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> timers = new ArrayList<>(getTimers());
3276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (Timer timer : timers) {
3286d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.isRunning() || timer.isPaused()) {
3296d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                doResetOrDeleteTimer(timer, eventLabelId);
3306d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
3316d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
3326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notification once after all timers are updated.
3346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateNotification();
3356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Heads-Up notification is unaffected by this change
3366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
3376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
3396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the uri of the default ringtone to play for all timers when no user selection exists
3406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
341bd9eae10b13e015d1997d06f13e9abe06a7f306bJames Lemieux    Uri getDefaultTimerRingtoneUri() {
342bd9eae10b13e015d1997d06f13e9abe06a7f306bJames Lemieux        return mSettingsModel.getDefaultTimerRingtoneUri();
343bd9eae10b13e015d1997d06f13e9abe06a7f306bJames Lemieux    }
344bd9eae10b13e015d1997d06f13e9abe06a7f306bJames Lemieux
3456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
3466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
3476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
348f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux    boolean isTimerRingtoneSilent() {
349f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux        return Uri.EMPTY.equals(getTimerRingtoneUri());
350f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux    }
351f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux
3526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
3536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the uri of the ringtone to play for all timers
3546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
355856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    Uri getTimerRingtoneUri() {
356856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        if (mTimerRingtoneUri == null) {
357856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux            mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri();
358856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        }
359856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
360856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        return mTimerRingtoneUri;
361856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    }
362856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
3636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
3646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the title of the ringtone that is played for all timers
3656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
366856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    String getTimerRingtoneTitle() {
367856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        if (mTimerRingtoneTitle == null) {
368f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux            if (isTimerRingtoneSilent()) {
36932efff252246b90ae870600a7b8db3a62c1ebdc8Annie Chin                // Special case: no ringtone has a title of "Silent".
37032efff252246b90ae870600a7b8db3a62c1ebdc8Annie Chin                mTimerRingtoneTitle = mContext.getString(R.string.silent_timer_ringtone_title);
371856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux            } else {
372f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                final Uri defaultUri = getDefaultTimerRingtoneUri();
373f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                final Uri uri = getTimerRingtoneUri();
374f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux
375f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                if (defaultUri.equals(uri)) {
376f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                    // Special case: default ringtone has a title of "Timer Expired".
377f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                    mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
378f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                } else {
379f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                    final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
380f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                    mTimerRingtoneTitle = ringtone.getTitle(mContext);
381f8faca1961278db2797d122351885ce6e32e4f3dJames Lemieux                }
382856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux            }
383856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        }
384856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
385856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        return mTimerRingtoneTitle;
386856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    }
387856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux
3886d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private List<Timer> getMutableTimers() {
3896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (mTimers == null) {
3906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mTimers = TimerDAO.getTimers(mContext);
3916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            Collections.sort(mTimers, Timer.ID_COMPARATOR);
3926d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
3936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return mTimers;
3956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
3966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
3976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private List<Timer> getMutableExpiredTimers() {
3986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (mExpiredTimers == null) {
3996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mExpiredTimers = new ArrayList<>();
4006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            for (Timer timer : getMutableTimers()) {
4026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                if (timer.isExpired()) {
4036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    mExpiredTimers.add(timer);
4046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                }
4056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
4066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR);
4076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return mExpiredTimers;
4106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
4116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
4136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * This method updates timer data without updating notifications. This is useful in bulk-update
4146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * scenarios so the notifications are only rebuilt once.
4156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
4166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timer an updated timer to store
4176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @return the state of the timer prior to the update
4186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
4196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private Timer doUpdateTimer(Timer timer) {
4206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Retrieve the cached form of the timer.
4216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> timers = getMutableTimers();
4226d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int index = timers.indexOf(timer);
4236d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Timer before = timers.get(index);
4246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If no change occurred, ignore this update.
4266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (timer == before) {
4276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            return timer;
4286d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4296d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4306d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer in permanent storage.
4316d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        TimerDAO.updateTimer(mContext, timer);
4326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer in the cache.
4346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Timer oldTimer = timers.set(index, timer);
4356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Clear the cache of expired timers if the timer changed to/from expired.
4376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (before.isExpired() || timer.isExpired()) {
4386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mExpiredTimers = null;
4396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer expiration callback.
4426d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateAlarmManager();
4436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4446d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer ringer.
4456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateRinger(before, timer);
4466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Notify listeners of the change.
4486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (TimerListener timerListener : mTimerListeners) {
4496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            timerListener.timerUpdated(before, timer);
4506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4516d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return oldTimer;
4536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
4546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
4566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * This method removes timer data without updating notifications. This is useful in bulk-remove
4576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * scenarios so the notifications are only rebuilt once.
4586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
4596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timer an existing timer to be removed
4606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
4616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void doRemoveTimer(Timer timer) {
4626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Remove the timer from permanent storage.
4636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        TimerDAO.removeTimer(mContext, timer);
4646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Remove the timer from the cache.
4666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> timers = getMutableTimers();
4676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int index = timers.indexOf(timer);
4686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If the timer cannot be located there is nothing to remove.
4706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (index == -1) {
4716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            return;
4726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        timer = timers.remove(index);
4756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Clear the cache of expired timers if a new expired timer was added.
4776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (timer.isExpired()) {
4786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mExpiredTimers = null;
4796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4816d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer expiration callback.
4826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateAlarmManager();
4836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the timer ringer.
4856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        updateRinger(timer, null);
4866d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4876d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Notify listeners of the change.
4886d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (TimerListener timerListener : mTimerListeners) {
4896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            timerListener.timerRemoved(timer);
4906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
4916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
4926d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
4936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
4946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * This method updates/removes timer data without updating notifications. This is useful in
4956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * bulk-update scenarios so the notifications are only rebuilt once.
4966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
4976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * If the given {@code timer} is expired and marked for deletion after use then this method
4986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
4996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * to exist.
5006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
5016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param timer the timer to be reset
5026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
5036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
5046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private void doResetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
5056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (timer.isExpired() && timer.getDeleteAfterUse()) {
5066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            doRemoveTimer(timer);
5076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (eventLabelId != 0) {
5086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                Events.sendTimerEvent(R.string.action_delete, eventLabelId);
5096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
5106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else if (!timer.isReset()) {
5116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            doUpdateTimer(timer.reset());
5126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (eventLabelId != 0) {
5136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                Events.sendTimerEvent(R.string.action_reset, eventLabelId);
5146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
5156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
5176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
5196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Updates the callback given to this application from the {@link AlarmManager} that signals the
5206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * expiration of the next timer. If no timers are currently set to expire (i.e. no running
5216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * timers exist) then this method clears the expiration callback from AlarmManager.
5226d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
5236d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private void updateAlarmManager() {
5246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Locate the next firing timer if one exists.
5256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        Timer nextExpiringTimer = null;
5266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (Timer timer : getMutableTimers()) {
5276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.isRunning()) {
5286d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                if (nextExpiringTimer == null) {
5296d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    nextExpiringTimer = timer;
5306d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) {
5316d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    nextExpiringTimer = timer;
5326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                }
5336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
5346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Build the intent that signals the timer expiration.
5376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer);
5386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (nextExpiringTimer == null) {
5406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // Cancel the existing timer expiration callback.
5416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final PendingIntent pi = PendingIntent.getService(mContext,
5426d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
5436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (pi != null) {
5446d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                mAlarmManager.cancel(pi);
5456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                pi.cancel();
5466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
5476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else {
5486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // Update the existing timer expiration callback.
5496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final PendingIntent pi = PendingIntent.getService(mContext,
5506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
5516d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            schedulePendingIntent(nextExpiringTimer.getExpirationTime(), pi);
5526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
5546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
5566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Starts and stops the ringer for timers if the change to the timer demands it.
5576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     *
5586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param before the state of the timer before the change; {@code null} indicates added
5596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * @param after the state of the timer after the change; {@code null} indicates delete
5606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
5616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private void updateRinger(Timer before, Timer after) {
5626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Retrieve the states before and after the change.
5636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Timer.State beforeState = before == null ? null : before.getState();
5646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Timer.State afterState = after == null ? null : after.getState();
5656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If the timer state did not change, the ringer state is unchanged.
5676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (beforeState == afterState) {
5686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            return;
5696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If the timer is the first to expire, start ringing.
5726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) {
5736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext);
5746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            TimerKlaxon.start(mContext);
5756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If the expired timer was the last to reset, stop ringing.
5786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) {
5796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            TimerKlaxon.stop(mContext);
5806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            AlarmAlertWakeLock.releaseCpuLock();
5816d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
5836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
5856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Updates the notification controlling unexpired timers. This notification is only displayed
5866d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * when the application is not open.
5876d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
5886d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    void updateNotification() {
5896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Notifications should be hidden if the app is open.
5906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (mNotificationModel.isApplicationInForeground()) {
5916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
5926d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            return;
5936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
5946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
5956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Filter the timers to just include unexpired ones.
5966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final List<Timer> unexpired = new ArrayList<>();
5976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        for (Timer timer : getMutableTimers()) {
5986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.isRunning() || timer.isPaused()) {
5996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                unexpired.add(timer);
6006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
6016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
6026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // If no unexpired timers exist, cancel the notification.
6046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (unexpired.isEmpty()) {
6056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
6066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            return;
6076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
6086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Sort the unexpired timers to locate the next one scheduled to expire.
6106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR);
6116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Timer timer = unexpired.get(0);
6126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final long remainingTime = timer.getRemainingTime();
6136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Generate some descriptive text, a title, and some actions based on timer states.
6156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String contentText;
6166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String contentTitle;
6176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        @DrawableRes int firstActionIconId, secondActionIconId = 0;
6186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        @StringRes int firstActionTitleId, secondActionTitleId = 0;
6196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        Intent firstActionIntent, secondActionIntent = null;
6206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (unexpired.size() == 1) {
6226d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            contentText = formatElapsedTimeUntilExpiry(remainingTime);
6236d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.isRunning()) {
6256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                // Single timer is running.
6266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                if (TextUtils.isEmpty(timer.getLabel())) {
6276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    contentTitle = mContext.getString(R.string.timer_notification_label);
6286d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                } else {
6296d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    contentTitle = timer.getLabel();
6306d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                }
6316d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionIconId = R.drawable.ic_pause_24dp;
6336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionTitleId = R.string.timer_pause;
6346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionIntent = new Intent(mContext, TimerService.class)
6356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                        .setAction(HandleDeskClockApiCalls.ACTION_PAUSE_TIMER)
636437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
6376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                secondActionIconId = R.drawable.ic_add_24dp;
6396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                secondActionTitleId = R.string.timer_plus_1_min;
6406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                secondActionIntent = new Intent(mContext, TimerService.class)
6416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                        .setAction(HandleDeskClockApiCalls.ACTION_ADD_MINUTE_TIMER)
642437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
6436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            } else {
6446d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                // Single timer is paused.
6456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentTitle = mContext.getString(R.string.timer_paused);
6466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionIconId = R.drawable.ic_start_24dp;
6486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionTitleId = R.string.sw_resume_button;
6496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionIntent = new Intent(mContext, TimerService.class)
6506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                        .setAction(HandleDeskClockApiCalls.ACTION_START_TIMER)
651437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
6526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                secondActionIconId = R.drawable.ic_reset_24dp;
6546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                secondActionTitleId = R.string.sw_reset_button;
6556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                secondActionIntent = new Intent(mContext, TimerService.class)
6566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                        .setAction(HandleDeskClockApiCalls.ACTION_RESET_TIMER)
657437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                        .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId());
6586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
6596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else {
6606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (timer.isRunning()) {
6616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                // At least one timer is running.
6626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                final String timeRemaining = formatElapsedTimeUntilExpiry(remainingTime);
6636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentText = mContext.getString(R.string.next_timer_notif, timeRemaining);
6646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentTitle = mContext.getString(R.string.timers_in_use, unexpired.size());
6656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            } else {
6666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                // All timers are paused.
6676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentText = mContext.getString(R.string.all_timers_stopped_notif);
6686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentTitle = mContext.getString(R.string.timers_stopped, unexpired.size());
6696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
6706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            firstActionIconId = R.drawable.ic_reset_24dp;
6726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            firstActionTitleId = R.string.timer_reset_all;
673437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux            firstActionIntent = TimerService.createResetUnexpiredTimersIntent(mContext);
6746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
6756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Intent to load the app and show the timer when the notification is tapped.
6776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Intent showApp = new Intent(mContext, HandleDeskClockApiCalls.class)
6786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
6796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setAction(HandleDeskClockApiCalls.ACTION_SHOW_TIMERS)
680437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId())
681437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux                .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, R.string.label_notification);
6826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final PendingIntent pendingShowApp = PendingIntent.getActivity(mContext, 0, showApp,
6846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
6856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6866d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
6876d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setOngoing(true)
6886d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setLocalOnly(true)
6896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setShowWhen(false)
6906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setAutoCancel(false)
6916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setContentText(contentText)
6926d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setContentTitle(contentTitle)
6936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setContentIntent(pendingShowApp)
6946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setSmallIcon(R.drawable.stat_notify_timer)
6956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setPriority(NotificationCompat.PRIORITY_HIGH)
6966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setCategory(NotificationCompat.CATEGORY_ALARM)
6976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
6986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
6996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final PendingIntent firstAction = PendingIntent.getService(mContext, 0,
7006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                firstActionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
7016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String firstActionTitle = mContext.getString(firstActionTitleId);
7026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        builder.addAction(firstActionIconId, firstActionTitle, firstAction);
7036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (secondActionIntent != null) {
7056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final PendingIntent secondAction = PendingIntent.getService(mContext, 0,
7066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    secondActionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
7076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final String secondActionTitle = mContext.getString(secondActionTitleId);
7086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            builder.addAction(secondActionIconId, secondActionTitle, secondAction);
7096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
7106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notification.
7126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Notification notification = builder.build();
7136d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
7146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        mNotificationManager.notify(notificationId, notification);
7156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Intent updateNotification = TimerService.createUpdateNotificationIntent(mContext);
7176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
7186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // Schedule a callback to update the time-sensitive information of the running timer.
7196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final PendingIntent pi = PendingIntent.getService(mContext, 0, updateNotification,
7206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
7216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7226d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
7236d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
7246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            schedulePendingIntent(triggerTime, pi);
7266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else {
7276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // Cancel the update notification callback.
7286d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final PendingIntent pi = PendingIntent.getService(mContext, 0, updateNotification,
7296d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
7306d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (pi != null) {
7316d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                mAlarmManager.cancel(pi);
7326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                pi.cancel();
7336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
7346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
7356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
7366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
7386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Updates the heads-up notification controlling expired timers. This heads-up notification is
7396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * displayed whether the application is open or not.
7406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
7416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private void updateHeadsUpNotification() {
742b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux        // Nothing can be done with the heads-up notification without a valid service reference.
743b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux        if (mService == null) {
744b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux            return;
745b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux        }
746b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux
7470dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux        final List<Timer> expired = getExpiredTimers();
7486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7490dd0cac610cd59762c8b604da6c437b18a29246bJames Lemieux        // If no expired timers exist, stop the service (which cancels the foreground notification).
7506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (expired.isEmpty()) {
751b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux            mService.stopSelf();
752b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux            mService = null;
7536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            return;
7546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
7556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Generate some descriptive text, a title, and an action name based on the timer count.
7576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int timerId;
7586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String contentText;
7596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String contentTitle;
7606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String resetActionTitle;
7616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (expired.size() > 1) {
7626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            timerId = -1;
7636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            contentText = mContext.getString(R.string.timer_multi_times_up, expired.size());
7646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            contentTitle = mContext.getString(R.string.timer_notification_label);
7656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            resetActionTitle = mContext.getString(R.string.timer_stop_all);
7666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else {
7676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final Timer timer = expired.get(0);
7686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            timerId = timer.getId();
7696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            resetActionTitle = mContext.getString(R.string.timer_stop);
7706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            contentText = mContext.getString(R.string.timer_times_up);
7716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final String label = timer.getLabel();
7736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (TextUtils.isEmpty(label)) {
7746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentTitle = mContext.getString(R.string.timer_notification_label);
7756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            } else {
7766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                contentTitle = label;
7776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
7786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
7796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Content intent shows the timer full screen when clicked.
7816d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Intent content = new Intent(mContext, ExpiredTimersActivity.class);
7826d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final PendingIntent pendingContent = PendingIntent.getActivity(mContext, 0, content,
7836d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                PendingIntent.FLAG_UPDATE_CURRENT);
7846d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7856d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Full screen intent has flags so it is different than the content intent.
7866d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Intent fullScreen = new Intent(mContext, ExpiredTimersActivity.class)
7876d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
7886d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final PendingIntent pendingFullScreen = PendingIntent.getActivity(mContext, 0, fullScreen,
7896d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                PendingIntent.FLAG_UPDATE_CURRENT);
7906d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7916d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // First action intent is either reset single timer or reset all timers.
792437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux        final Intent reset = TimerService.createResetExpiredTimersIntent(mContext);
7936d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final PendingIntent pendingReset = PendingIntent.getService(mContext, 0, reset,
7946d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                PendingIntent.FLAG_UPDATE_CURRENT);
7956d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
7966d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
7976d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setWhen(0)
7986d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setOngoing(true)
7996d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setLocalOnly(true)
8006d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setAutoCancel(false)
8016d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setContentText(contentText)
8026d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setContentTitle(contentTitle)
8036d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setContentIntent(pendingContent)
8046d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setSmallIcon(R.drawable.stat_notify_timer)
8056d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setFullScreenIntent(pendingFullScreen, true)
8066d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setPriority(NotificationCompat.PRIORITY_MAX)
8076d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
8086d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                .addAction(R.drawable.ic_stop_24dp, resetActionTitle, pendingReset);
8096d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8106d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Add a second action if only a single timer is expired.
8116d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (expired.size() == 1) {
8126d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // Second action intent adds a minute to a single timer.
813437da3b08ce9ce1b32f4e544816cb3431ceb8d4eJames Lemieux            final Intent addMinute = TimerService.createAddMinuteTimerIntent(mContext, timerId);
8146d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final PendingIntent pendingAddMinute = PendingIntent.getService(mContext, 0, addMinute,
8156d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                    PendingIntent.FLAG_UPDATE_CURRENT);
8166d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            final String addMinuteTitle = mContext.getString(R.string.timer_plus_1_min);
8176d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            builder.addAction(R.drawable.ic_add_24dp, addMinuteTitle, pendingAddMinute);
8186d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
8196d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8206d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // Update the notification.
8216d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final Notification notification = builder.build();
8226d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
823b76aa50f9e13e71e81b9d02cc3ebebaa73188d3eJames Lemieux        mService.startForeground(notificationId, notification);
8246d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
8256d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8266d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
8276d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Format "7 hours 52 minutes remaining"
8286d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
8296d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    @VisibleForTesting
8306d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    String formatElapsedTimeUntilExpiry(long remainingTime) {
8316d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int hours = (int) remainingTime / (int) HOUR_IN_MILLIS;
8326d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final int minutes = (int) remainingTime / ((int) MINUTE_IN_MILLIS) % 60;
8336d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8346d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        String minSeq = Utils.getNumberFormattedQuantityString(mContext, R.plurals.minutes, minutes);
8356d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        String hourSeq = Utils.getNumberFormattedQuantityString(mContext, R.plurals.hours, hours);
8366d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8376d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        // The verb "remaining" may have to change tense for singular subjects in some languages.
8386d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final String verb = mContext.getString((minutes > 1 || hours > 1)
8396d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                ? R.string.timer_remaining_multiple
8406d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                : R.string.timer_remaining_single);
8416d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8426d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final boolean showHours = hours > 0;
8436d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        final boolean showMinutes = minutes > 0;
8446d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8456d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        int formatStringId;
8466d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (showHours) {
8476d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            if (showMinutes) {
8486d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                formatStringId = R.string.timer_notifications_hours_minutes;
8496d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            } else {
8506d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux                formatStringId = R.string.timer_notifications_hours;
8516d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            }
8526d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else if (showMinutes) {
8536d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            formatStringId = R.string.timer_notifications_minutes;
8546d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else {
8556d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            formatStringId = R.string.timer_notifications_less_min;
8566d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
8576d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        return String.format(mContext.getString(formatStringId), hourSeq, minSeq, verb);
8586d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
8596d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8606d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private void schedulePendingIntent(long triggerTime, PendingIntent pi) {
8616d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        if (Utils.isMOrLater()) {
8626d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // Make sure the timer fires when the device is in doze mode. The timer is not
8636d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            // guaranteed to fire at the requested time. It may be delayed up to 15 minutes.
8646d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mAlarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
8656d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        } else {
8666d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            mAlarmManager.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
8676d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
8686d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
8696d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
8706d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    /**
8716d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     * Update the stopwatch notification in response to a locale change.
8726d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux     */
8736d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    private final class LocaleChangedReceiver extends BroadcastReceiver {
8746d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        @Override
8756d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        public void onReceive(Context context, Intent intent) {
8766d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            updateNotification();
8776d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux            updateHeadsUpNotification();
8786d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux        }
8796d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux    }
8806d603b7c62bb38d763a681a8bf20fadb1442e833James Lemieux
881856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    /**
882856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux     * This receiver is notified when shared preferences change. Cached information built on
883856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux     * preferences must be cleared.
884856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux     */
885856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    private final class PreferenceListener implements OnSharedPreferenceChangeListener {
886856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        @Override
887856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
888856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux            switch (key) {
889856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux                case SettingsActivity.KEY_TIMER_RINGTONE:
890856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux                    mTimerRingtoneUri = null;
891856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux                    mTimerRingtoneTitle = null;
892856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux                    break;
893856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux            }
894856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux        }
895856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux    }
896856483e7e18d5f042a338f7b3d472e28a386c4adJames Lemieux}