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}