| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.deskclock.data; |
| |
| import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; |
| import static android.text.format.DateUtils.MINUTE_IN_MILLIS; |
| import static com.android.deskclock.data.Timer.State.EXPIRED; |
| import static com.android.deskclock.data.Timer.State.RESET; |
| |
| import android.annotation.SuppressLint; |
| import android.app.AlarmManager; |
| import android.app.Notification; |
| import android.app.PendingIntent; |
| import android.app.Service; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.SharedPreferences; |
| import android.content.SharedPreferences.OnSharedPreferenceChangeListener; |
| import android.net.Uri; |
| import android.util.ArraySet; |
| |
| import androidx.annotation.StringRes; |
| import androidx.core.app.NotificationManagerCompat; |
| |
| import com.android.deskclock.AlarmAlertWakeLock; |
| import com.android.deskclock.LogUtils; |
| import com.android.deskclock.R; |
| import com.android.deskclock.events.Events; |
| import com.android.deskclock.settings.SettingsActivity; |
| import com.android.deskclock.timer.TimerKlaxon; |
| import com.android.deskclock.timer.TimerService; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * All {@link Timer} data is accessed via this model. |
| */ |
| final class TimerModel { |
| |
| /** |
| * Running timers less than this threshold are left running/expired; greater than this |
| * threshold are considered missed. |
| */ |
| private static final long MISSED_THRESHOLD = -MINUTE_IN_MILLIS; |
| |
| private final Context mContext; |
| |
| private final SharedPreferences mPrefs; |
| |
| /** The alarm manager system service that calls back when timers expire. */ |
| private final AlarmManager mAlarmManager; |
| |
| /** The model from which settings are fetched. */ |
| private final SettingsModel mSettingsModel; |
| |
| /** The model from which notification data are fetched. */ |
| private final NotificationModel mNotificationModel; |
| |
| /** The model from which ringtone data are fetched. */ |
| private final RingtoneModel mRingtoneModel; |
| |
| /** Used to create and destroy system notifications related to timers. */ |
| private final NotificationManagerCompat mNotificationManager; |
| |
| /** Update timer notification when locale changes. */ |
| @SuppressWarnings("FieldCanBeLocal") |
| private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver(); |
| |
| /** |
| * Retain a hard reference to the shared preference observer to prevent it from being garbage |
| * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail. |
| */ |
| @SuppressWarnings("FieldCanBeLocal") |
| private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener(); |
| |
| /** The listeners to notify when a timer is added, updated or removed. */ |
| private final List<TimerListener> mTimerListeners = new ArrayList<>(); |
| |
| /** Delegate that builds platform-specific timer notifications. */ |
| private final TimerNotificationBuilder mNotificationBuilder = new TimerNotificationBuilder(); |
| |
| /** |
| * The ids of expired timers for which the ringer is ringing. Not all expired timers have their |
| * ids in this collection. If a timer was already expired when the app was started its id will |
| * be absent from this collection. |
| */ |
| @SuppressLint("NewApi") |
| private final Set<Integer> mRingingIds = new ArraySet<>(); |
| |
| /** The uri of the ringtone to play for timers. */ |
| private Uri mTimerRingtoneUri; |
| |
| /** The title of the ringtone to play for timers. */ |
| private String mTimerRingtoneTitle; |
| |
| /** A mutable copy of the timers. */ |
| private List<Timer> mTimers; |
| |
| /** A mutable copy of the expired timers. */ |
| private List<Timer> mExpiredTimers; |
| |
| /** A mutable copy of the missed timers. */ |
| private List<Timer> mMissedTimers; |
| |
| /** |
| * The service that keeps this application in the foreground while a heads-up timer |
| * notification is displayed. Marking the service as foreground prevents the operating system |
| * from killing this application while expired timers are actively firing. |
| */ |
| private Service mService; |
| |
| TimerModel(Context context, SharedPreferences prefs, SettingsModel settingsModel, |
| RingtoneModel ringtoneModel, NotificationModel notificationModel) { |
| mContext = context; |
| mPrefs = prefs; |
| mSettingsModel = settingsModel; |
| mRingtoneModel = ringtoneModel; |
| mNotificationModel = notificationModel; |
| mNotificationManager = NotificationManagerCompat.from(context); |
| |
| mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); |
| |
| // Clear caches affected by preferences when preferences change. |
| prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener); |
| |
| // Update timer notification when locale changes. |
| final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); |
| mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter, |
| Context.RECEIVER_NOT_EXPORTED); |
| } |
| |
| /** |
| * @param timerListener to be notified when timers are added, updated and removed |
| */ |
| void addTimerListener(TimerListener timerListener) { |
| mTimerListeners.add(timerListener); |
| } |
| |
| /** |
| * @param timerListener to no longer be notified when timers are added, updated and removed |
| */ |
| void removeTimerListener(TimerListener timerListener) { |
| mTimerListeners.remove(timerListener); |
| } |
| |
| /** |
| * @return all defined timers in their creation order |
| */ |
| List<Timer> getTimers() { |
| return Collections.unmodifiableList(getMutableTimers()); |
| } |
| |
| /** |
| * @return all expired timers in their expiration order |
| */ |
| List<Timer> getExpiredTimers() { |
| return Collections.unmodifiableList(getMutableExpiredTimers()); |
| } |
| |
| /** |
| * @return all missed timers in their expiration order |
| */ |
| private List<Timer> getMissedTimers() { |
| return Collections.unmodifiableList(getMutableMissedTimers()); |
| } |
| |
| /** |
| * @param timerId identifies the timer to return |
| * @return the timer with the given {@code timerId} |
| */ |
| Timer getTimer(int timerId) { |
| for (Timer timer : getMutableTimers()) { |
| if (timer.getId() == timerId) { |
| return timer; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @return the timer that last expired and is still expired now; {@code null} if no timers are |
| * expired |
| */ |
| Timer getMostRecentExpiredTimer() { |
| final List<Timer> timers = getMutableExpiredTimers(); |
| return timers.isEmpty() ? null : timers.get(timers.size() - 1); |
| } |
| |
| /** |
| * @param length the length of the timer in milliseconds |
| * @param label describes the purpose of the timer |
| * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset |
| * @return the newly added timer |
| */ |
| Timer addTimer(long length, String label, boolean deleteAfterUse) { |
| // Create the timer instance. |
| Timer timer = new Timer(-1, RESET, length, length, Timer.UNUSED, Timer.UNUSED, length, |
| label, deleteAfterUse); |
| |
| // Add the timer to permanent storage. |
| timer = TimerDAO.addTimer(mPrefs, timer); |
| |
| // Add the timer to the cache. |
| getMutableTimers().add(0, timer); |
| |
| // Update the timer notification. |
| updateNotification(); |
| // Heads-Up notification is unaffected by this change |
| |
| // Notify listeners of the change. |
| for (TimerListener timerListener : mTimerListeners) { |
| timerListener.timerAdded(timer); |
| } |
| |
| return timer; |
| } |
| |
| /** |
| * @param service used to start foreground notifications related to expired timers |
| * @param timer the timer to be expired |
| */ |
| void expireTimer(Service service, Timer timer) { |
| if (mService == null) { |
| // If this is the first expired timer, retain the service that will be used to start |
| // the heads-up notification in the foreground. |
| mService = service; |
| } else if (mService != service) { |
| // If this is not the first expired timer, the service should match the one given when |
| // the first timer expired. |
| LogUtils.wtf("Expected TimerServices to be identical"); |
| } |
| |
| updateTimer(timer.expire()); |
| } |
| |
| /** |
| * @param timer an updated timer to store |
| */ |
| void updateTimer(Timer timer) { |
| final Timer before = doUpdateTimer(timer); |
| |
| // Update the notification after updating the timer data. |
| updateNotification(); |
| |
| // If the timer started or stopped being expired, update the heads-up notification. |
| if (before.getState() != timer.getState()) { |
| if (before.isExpired() || timer.isExpired()) { |
| updateHeadsUpNotification(); |
| } |
| } |
| } |
| |
| /** |
| * @param timer an existing timer to be removed |
| */ |
| void removeTimer(Timer timer) { |
| doRemoveTimer(timer); |
| |
| // Update the timer notifications after removing the timer data. |
| if (timer.isExpired()) { |
| updateHeadsUpNotification(); |
| } else { |
| updateNotification(); |
| } |
| } |
| |
| /** |
| * If the given {@code timer} is expired and marked for deletion after use then this method |
| * removes the the timer. The timer is otherwise transitioned to the reset state and continues |
| * to exist. |
| * |
| * @param timer the timer to be reset |
| * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset |
| * (e.g. one use timers) |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| * @return the reset {@code timer} or {@code null} if the timer was deleted |
| */ |
| void resetTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId) { |
| final Timer result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId); |
| |
| // Update the notification after updating the timer data. |
| if (timer.isMissed()) { |
| updateMissedNotification(); |
| } else if (timer.isExpired()) { |
| updateHeadsUpNotification(); |
| } else { |
| updateNotification(); |
| } |
| |
| return; |
| } |
| |
| /** |
| * Update timers after system reboot. |
| */ |
| void updateTimersAfterReboot() { |
| final List<Timer> timers = new ArrayList<>(getTimers()); |
| for (Timer timer : timers) { |
| doUpdateAfterRebootTimer(timer); |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateNotification(); |
| updateMissedNotification(); |
| updateHeadsUpNotification(); |
| } |
| |
| /** |
| * Update timers after time set. |
| */ |
| void updateTimersAfterTimeSet() { |
| final List<Timer> timers = new ArrayList<>(getTimers()); |
| for (Timer timer : timers) { |
| doUpdateAfterTimeSetTimer(timer); |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateNotification(); |
| updateMissedNotification(); |
| updateHeadsUpNotification(); |
| } |
| |
| /** |
| * Reset all expired timers. Exactly one parameter should be filled, with preference given to |
| * eventLabelId. |
| * |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| */ |
| void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) { |
| final List<Timer> timers = new ArrayList<>(getTimers()); |
| for (Timer timer : timers) { |
| if (timer.isExpired()) { |
| doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); |
| } |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateHeadsUpNotification(); |
| } |
| |
| /** |
| * Reset all missed timers. |
| * |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| */ |
| void resetMissedTimers(@StringRes int eventLabelId) { |
| final List<Timer> timers = new ArrayList<>(getTimers()); |
| for (Timer timer : timers) { |
| if (timer.isMissed()) { |
| doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); |
| } |
| } |
| |
| // Update the notifications once after all timers are updated. |
| updateMissedNotification(); |
| } |
| |
| /** |
| * Reset all unexpired timers. |
| * |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| */ |
| void resetUnexpiredTimers(@StringRes int eventLabelId) { |
| final List<Timer> timers = new ArrayList<>(getTimers()); |
| for (Timer timer : timers) { |
| if (timer.isRunning() || timer.isPaused()) { |
| doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); |
| } |
| } |
| |
| // Update the notification once after all timers are updated. |
| updateNotification(); |
| // Heads-Up notification is unaffected by this change |
| } |
| |
| /** |
| * @return the uri of the default ringtone to play for all timers when no user selection exists |
| */ |
| Uri getDefaultTimerRingtoneUri() { |
| return mSettingsModel.getDefaultTimerRingtoneUri(); |
| } |
| |
| /** |
| * @return {@code true} iff the ringtone to play for all timers is the silent ringtone |
| */ |
| boolean isTimerRingtoneSilent() { |
| return Uri.EMPTY.equals(getTimerRingtoneUri()); |
| } |
| |
| /** |
| * @return the uri of the ringtone to play for all timers |
| */ |
| Uri getTimerRingtoneUri() { |
| if (mTimerRingtoneUri == null) { |
| mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri(); |
| } |
| |
| return mTimerRingtoneUri; |
| } |
| |
| /** |
| * @param uri the uri of the ringtone to play for all timers |
| */ |
| void setTimerRingtoneUri(Uri uri) { |
| mSettingsModel.setTimerRingtoneUri(uri); |
| } |
| |
| /** |
| * @return the title of the ringtone that is played for all timers |
| */ |
| String getTimerRingtoneTitle() { |
| if (mTimerRingtoneTitle == null) { |
| if (isTimerRingtoneSilent()) { |
| // Special case: no ringtone has a title of "Silent". |
| mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title); |
| } else { |
| final Uri defaultUri = getDefaultTimerRingtoneUri(); |
| final Uri uri = getTimerRingtoneUri(); |
| |
| if (defaultUri.equals(uri)) { |
| // Special case: default ringtone has a title of "Timer Expired". |
| mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title); |
| } else { |
| mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri); |
| } |
| } |
| } |
| |
| return mTimerRingtoneTitle; |
| } |
| |
| /** |
| * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback; |
| * {@code 0} implies no crescendo should be applied |
| */ |
| long getTimerCrescendoDuration() { |
| return mSettingsModel.getTimerCrescendoDuration(); |
| } |
| |
| /** |
| * @return {@code true} if the device vibrates when timers expire |
| */ |
| boolean getTimerVibrate() { |
| return mSettingsModel.getTimerVibrate(); |
| } |
| |
| /** |
| * @param enabled {@code true} if the device should vibrate when timers expire |
| */ |
| void setTimerVibrate(boolean enabled) { |
| mSettingsModel.setTimerVibrate(enabled); |
| } |
| |
| private List<Timer> getMutableTimers() { |
| if (mTimers == null) { |
| mTimers = TimerDAO.getTimers(mPrefs); |
| mTimers.sort(Timer.ID_COMPARATOR); |
| } |
| |
| return mTimers; |
| } |
| |
| private List<Timer> getMutableExpiredTimers() { |
| if (mExpiredTimers == null) { |
| mExpiredTimers = new ArrayList<>(); |
| |
| for (Timer timer : getMutableTimers()) { |
| if (timer.isExpired()) { |
| mExpiredTimers.add(timer); |
| } |
| } |
| mExpiredTimers.sort(Timer.EXPIRY_COMPARATOR); |
| } |
| |
| return mExpiredTimers; |
| } |
| |
| private List<Timer> getMutableMissedTimers() { |
| if (mMissedTimers == null) { |
| mMissedTimers = new ArrayList<>(); |
| |
| for (Timer timer : getMutableTimers()) { |
| if (timer.isMissed()) { |
| mMissedTimers.add(timer); |
| } |
| } |
| mMissedTimers.sort(Timer.EXPIRY_COMPARATOR); |
| } |
| |
| return mMissedTimers; |
| } |
| |
| /** |
| * This method updates timer data without updating notifications. This is useful in bulk-update |
| * scenarios so the notifications are only rebuilt once. |
| * |
| * @param timer an updated timer to store |
| * @return the state of the timer prior to the update |
| */ |
| private Timer doUpdateTimer(Timer timer) { |
| // Retrieve the cached form of the timer. |
| final List<Timer> timers = getMutableTimers(); |
| final int index = timers.indexOf(timer); |
| final Timer before = timers.get(index); |
| |
| // If no change occurred, ignore this update. |
| if (timer == before) { |
| return timer; |
| } |
| |
| // Update the timer in permanent storage. |
| TimerDAO.updateTimer(mPrefs, timer); |
| |
| // Update the timer in the cache. |
| final Timer oldTimer = timers.set(index, timer); |
| |
| // Clear the cache of expired timers if the timer changed to/from expired. |
| if (before.isExpired() || timer.isExpired()) { |
| mExpiredTimers = null; |
| } |
| // Clear the cache of missed timers if the timer changed to/from missed. |
| if (before.isMissed() || timer.isMissed()) { |
| mMissedTimers = null; |
| } |
| |
| // Update the timer expiration callback. |
| updateAlarmManager(); |
| |
| // Update the timer ringer. |
| updateRinger(before, timer); |
| |
| // Notify listeners of the change. |
| for (TimerListener timerListener : mTimerListeners) { |
| timerListener.timerUpdated(before, timer); |
| } |
| |
| return oldTimer; |
| } |
| |
| /** |
| * This method removes timer data without updating notifications. This is useful in bulk-remove |
| * scenarios so the notifications are only rebuilt once. |
| * |
| * @param timer an existing timer to be removed |
| */ |
| private void doRemoveTimer(Timer timer) { |
| // Remove the timer from permanent storage. |
| TimerDAO.removeTimer(mPrefs, timer); |
| |
| // Remove the timer from the cache. |
| final List<Timer> timers = getMutableTimers(); |
| final int index = timers.indexOf(timer); |
| |
| // If the timer cannot be located there is nothing to remove. |
| if (index == -1) { |
| return; |
| } |
| |
| timer = timers.remove(index); |
| |
| // Clear the cache of expired timers if a new expired timer was added. |
| if (timer.isExpired()) { |
| mExpiredTimers = null; |
| } |
| |
| // Clear the cache of missed timers if a new missed timer was added. |
| if (timer.isMissed()) { |
| mMissedTimers = null; |
| } |
| |
| // Update the timer expiration callback. |
| updateAlarmManager(); |
| |
| // Update the timer ringer. |
| updateRinger(timer, null); |
| |
| // Notify listeners of the change. |
| for (TimerListener timerListener : mTimerListeners) { |
| timerListener.timerRemoved(timer); |
| } |
| } |
| |
| /** |
| * This method updates/removes timer data without updating notifications. This is useful in |
| * bulk-update scenarios so the notifications are only rebuilt once. |
| * |
| * If the given {@code timer} is expired and marked for deletion after use then this method |
| * removes the the timer. The timer is otherwise transitioned to the reset state and continues |
| * to exist. |
| * |
| * @param timer the timer to be reset |
| * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset |
| * (e.g. one use timers) |
| * @param eventLabelId the label of the timer event to send; 0 if no event should be sent |
| * @return the reset {@code timer} or {@code null} if the timer was deleted |
| */ |
| private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete, |
| @StringRes int eventLabelId) { |
| if (allowDelete |
| && (timer.isExpired() || timer.isMissed()) |
| && timer.getDeleteAfterUse()) { |
| doRemoveTimer(timer); |
| if (eventLabelId != 0) { |
| Events.sendTimerEvent(R.string.action_delete, eventLabelId); |
| } |
| return null; |
| } else if (!timer.isReset()) { |
| final Timer reset = timer.reset(); |
| doUpdateTimer(reset); |
| if (eventLabelId != 0) { |
| Events.sendTimerEvent(R.string.action_reset, eventLabelId); |
| } |
| return reset; |
| } |
| |
| return timer; |
| } |
| |
| /** |
| * This method updates/removes timer data after a reboot without updating notifications. |
| * |
| * @param timer the timer to be updated |
| */ |
| private void doUpdateAfterRebootTimer(Timer timer) { |
| Timer updated = timer.updateAfterReboot(); |
| if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) { |
| updated = updated.miss(); |
| } |
| doUpdateTimer(updated); |
| } |
| |
| private void doUpdateAfterTimeSetTimer(Timer timer) { |
| final Timer updated = timer.updateAfterTimeSet(); |
| doUpdateTimer(updated); |
| } |
| |
| |
| /** |
| * Updates the callback given to this application from the {@link AlarmManager} that signals the |
| * expiration of the next timer. If no timers are currently set to expire (i.e. no running |
| * timers exist) then this method clears the expiration callback from AlarmManager. |
| */ |
| private void updateAlarmManager() { |
| // Locate the next firing timer if one exists. |
| Timer nextExpiringTimer = null; |
| for (Timer timer : getMutableTimers()) { |
| if (timer.isRunning()) { |
| if (nextExpiringTimer == null) { |
| nextExpiringTimer = timer; |
| } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) { |
| nextExpiringTimer = timer; |
| } |
| } |
| } |
| |
| // Build the intent that signals the timer expiration. |
| final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer); |
| |
| if (nextExpiringTimer == null) { |
| // Cancel the existing timer expiration callback. |
| final PendingIntent pi = PendingIntent.getService(mContext, 0, intent, |
| PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE | |
| PendingIntent.FLAG_IMMUTABLE); |
| |
| if (pi != null) { |
| mAlarmManager.cancel(pi); |
| pi.cancel(); |
| } |
| } else { |
| // Update the existing timer expiration callback. |
| final PendingIntent pi = PendingIntent.getService(mContext, 0, intent, |
| PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT | |
| PendingIntent.FLAG_IMMUTABLE); |
| schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi); |
| } |
| } |
| |
| /** |
| * Starts and stops the ringer for timers if the change to the timer demands it. |
| * |
| * @param before the state of the timer before the change; {@code null} indicates added |
| * @param after the state of the timer after the change; {@code null} indicates delete |
| */ |
| private void updateRinger(Timer before, Timer after) { |
| // Retrieve the states before and after the change. |
| final Timer.State beforeState = before == null ? null : before.getState(); |
| final Timer.State afterState = after == null ? null : after.getState(); |
| |
| // If the timer state did not change, the ringer state is unchanged. |
| if (beforeState == afterState) { |
| return; |
| } |
| |
| // If the timer is the first to expire, start ringing. |
| if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) { |
| AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext); |
| TimerKlaxon.start(mContext); |
| } |
| |
| // If the expired timer was the last to reset, stop ringing. |
| if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) { |
| TimerKlaxon.stop(mContext); |
| AlarmAlertWakeLock.releaseCpuLock(); |
| } |
| } |
| |
| /** |
| * Updates the notification controlling unexpired timers. This notification is only displayed |
| * when the application is not open. |
| */ |
| void updateNotification() { |
| // Notifications should be hidden if the app is open. |
| if (mNotificationModel.isApplicationInForeground()) { |
| mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId()); |
| return; |
| } |
| |
| // Filter the timers to just include unexpired ones. |
| final List<Timer> unexpired = new ArrayList<>(); |
| for (Timer timer : getMutableTimers()) { |
| if (timer.isRunning() || timer.isPaused()) { |
| unexpired.add(timer); |
| } |
| } |
| |
| // If no unexpired timers exist, cancel the notification. |
| if (unexpired.isEmpty()) { |
| mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId()); |
| return; |
| } |
| |
| // Sort the unexpired timers to locate the next one scheduled to expire. |
| unexpired.sort(Timer.EXPIRY_COMPARATOR); |
| |
| // Otherwise build and post a notification reflecting the latest unexpired timers. |
| final Notification notification = |
| mNotificationBuilder.build(mContext, mNotificationModel, unexpired); |
| final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId(); |
| mNotificationManager.notify(notificationId, notification); |
| } |
| |
| /** |
| * Updates the notification controlling missed timers. This notification is only displayed when |
| * the application is not open. |
| */ |
| void updateMissedNotification() { |
| // Notifications should be hidden if the app is open. |
| if (mNotificationModel.isApplicationInForeground()) { |
| mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId()); |
| return; |
| } |
| |
| final List<Timer> missed = getMissedTimers(); |
| |
| if (missed.isEmpty()) { |
| mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId()); |
| return; |
| } |
| |
| final Notification notification = mNotificationBuilder.buildMissed(mContext, |
| mNotificationModel, missed); |
| final int notificationId = mNotificationModel.getMissedTimerNotificationId(); |
| mNotificationManager.notify(notificationId, notification); |
| } |
| |
| /** |
| * Updates the heads-up notification controlling expired timers. This heads-up notification is |
| * displayed whether the application is open or not. |
| */ |
| private void updateHeadsUpNotification() { |
| // Nothing can be done with the heads-up notification without a valid service reference. |
| if (mService == null) { |
| return; |
| } |
| |
| final List<Timer> expired = getExpiredTimers(); |
| |
| // If no expired timers exist, stop the service (which cancels the foreground notification). |
| if (expired.isEmpty()) { |
| mService.stopSelf(); |
| mService = null; |
| return; |
| } |
| |
| // Otherwise build and post a foreground notification reflecting the latest expired timers. |
| final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired); |
| final int notificationId = mNotificationModel.getExpiredTimerNotificationId(); |
| mService.startForeground(notificationId, notification); |
| } |
| |
| /** |
| * Update the timer notification in response to a locale change. |
| */ |
| private final class LocaleChangedReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| mTimerRingtoneTitle = null; |
| updateNotification(); |
| updateMissedNotification(); |
| updateHeadsUpNotification(); |
| } |
| } |
| |
| /** |
| * This receiver is notified when shared preferences change. Cached information built on |
| * preferences must be cleared. |
| */ |
| private final class PreferenceListener implements OnSharedPreferenceChangeListener { |
| @Override |
| public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { |
| if (SettingsActivity.KEY_TIMER_RINGTONE.equals(key)) { |
| mTimerRingtoneUri = null; |
| mTimerRingtoneTitle = null; |
| } |
| } |
| } |
| |
| static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) { |
| // Ensure the timer fires even if the device is dozing. |
| am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); |
| } |
| } |