Java tutorial
/* * 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 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.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.StringRes; import android.support.annotation.VisibleForTesting; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.text.TextUtils; import android.util.ArraySet; import com.android.deskclock.AlarmAlertWakeLock; import com.android.deskclock.HandleDeskClockApiCalls; import com.android.deskclock.LogUtils; import com.android.deskclock.R; import com.android.deskclock.Utils; import com.android.deskclock.events.Events; import com.android.deskclock.settings.SettingsActivity; import com.android.deskclock.timer.ExpiredTimersActivity; 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; import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; import static android.text.format.DateUtils.HOUR_IN_MILLIS; 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; /** * All {@link Timer} data is accessed via this model. */ final class TimerModel { private final Context mContext; /** 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; /** Used to create and destroy system notifications related to timers. */ private final NotificationManagerCompat mNotificationManager; /** Update timer notification when locale changes. */ 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. */ 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<>(); /** * 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. */ 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; /** * 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, SettingsModel settingsModel, NotificationModel notificationModel) { mContext = context; mSettingsModel = settingsModel; mNotificationModel = notificationModel; mNotificationManager = NotificationManagerCompat.from(context); mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); // Clear caches affected by preferences when preferences change. final SharedPreferences prefs = Utils.getDefaultSharedPreferences(mContext); prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener); // Update stopwatch notification when locale changes. final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter); } /** * @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()); } /** * @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, Long.MIN_VALUE, length, label, deleteAfterUse); // Add the timer to permanent storage. timer = TimerDAO.addTimer(mContext, 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. updateNotification(); if (timer.isExpired()) { updateHeadsUpNotification(); } } /** * 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 eventLabelId the label of the timer event to send; 0 if no event should be sent */ void resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) { doResetOrDeleteTimer(timer, eventLabelId); // Update the notification after updating the timer data. updateNotification(); // If the timer stopped being expired, update the heads-up notification. if (timer.isExpired()) { updateHeadsUpNotification(); } } /** * Reset all timers. * * @param eventLabelId the label of the timer event to send; 0 if no event should be sent */ void resetTimers(@StringRes int eventLabelId) { final List<Timer> timers = new ArrayList<>(getTimers()); for (Timer timer : timers) { doResetOrDeleteTimer(timer, eventLabelId); } // Update the notifications once after all timers are reset. updateNotification(); updateHeadsUpNotification(); } /** * Reset all expired timers. * * @param eventLabelId the label of the timer event to send; 0 if no event should be sent */ void resetExpiredTimers(@StringRes int eventLabelId) { final List<Timer> timers = new ArrayList<>(getTimers()); for (Timer timer : timers) { if (timer.isExpired()) { doResetOrDeleteTimer(timer, eventLabelId); } } // Update the notifications once after all timers are updated. updateNotification(); updateHeadsUpNotification(); } /** * 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, 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; } /** * @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_timer_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 { final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri); mTimerRingtoneTitle = ringtone.getTitle(mContext); } } } return mTimerRingtoneTitle; } private List<Timer> getMutableTimers() { if (mTimers == null) { mTimers = TimerDAO.getTimers(mContext); Collections.sort(mTimers, 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); } } Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR); } return mExpiredTimers; } /** * 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(mContext, 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; } // 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 */ void doRemoveTimer(Timer timer) { // Remove the timer from permanent storage. TimerDAO.removeTimer(mContext, 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; } // 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 eventLabelId the label of the timer event to send; 0 if no event should be sent */ private void doResetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) { if (timer.isExpired() && timer.getDeleteAfterUse()) { doRemoveTimer(timer); if (eventLabelId != 0) { Events.sendTimerEvent(R.string.action_delete, eventLabelId); } } else if (!timer.isReset()) { doUpdateTimer(timer.reset()); if (eventLabelId != 0) { Events.sendTimerEvent(R.string.action_reset, eventLabelId); } } } /** * 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); 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); schedulePendingIntent(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. Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR); final Timer timer = unexpired.get(0); final long remainingTime = timer.getRemainingTime(); // Generate some descriptive text, a title, and some actions based on timer states. final String contentText; final String contentTitle; @DrawableRes int firstActionIconId, secondActionIconId = 0; @StringRes int firstActionTitleId, secondActionTitleId = 0; Intent firstActionIntent, secondActionIntent = null; if (unexpired.size() == 1) { contentText = formatElapsedTimeUntilExpiry(remainingTime); if (timer.isRunning()) { // Single timer is running. if (TextUtils.isEmpty(timer.getLabel())) { contentTitle = mContext.getString(R.string.timer_notification_label); } else { contentTitle = timer.getLabel(); } firstActionIconId = R.drawable.ic_pause_24dp; firstActionTitleId = R.string.timer_pause; firstActionIntent = new Intent(mContext, TimerService.class) .setAction(HandleDeskClockApiCalls.ACTION_PAUSE_TIMER) .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId()); secondActionIconId = R.drawable.ic_add_24dp; secondActionTitleId = R.string.timer_plus_1_min; secondActionIntent = new Intent(mContext, TimerService.class) .setAction(HandleDeskClockApiCalls.ACTION_ADD_MINUTE_TIMER) .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId()); } else { // Single timer is paused. contentTitle = mContext.getString(R.string.timer_paused); firstActionIconId = R.drawable.ic_start_24dp; firstActionTitleId = R.string.sw_resume_button; firstActionIntent = new Intent(mContext, TimerService.class) .setAction(HandleDeskClockApiCalls.ACTION_START_TIMER) .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId()); secondActionIconId = R.drawable.ic_reset_24dp; secondActionTitleId = R.string.sw_reset_button; secondActionIntent = new Intent(mContext, TimerService.class) .setAction(HandleDeskClockApiCalls.ACTION_RESET_TIMER) .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId()); } } else { if (timer.isRunning()) { // At least one timer is running. final String timeRemaining = formatElapsedTimeUntilExpiry(remainingTime); contentText = mContext.getString(R.string.next_timer_notif, timeRemaining); contentTitle = mContext.getString(R.string.timers_in_use, unexpired.size()); } else { // All timers are paused. contentText = mContext.getString(R.string.all_timers_stopped_notif); contentTitle = mContext.getString(R.string.timers_stopped, unexpired.size()); } firstActionIconId = R.drawable.ic_reset_24dp; firstActionTitleId = R.string.timer_reset_all; firstActionIntent = TimerService.createResetUnexpiredTimersIntent(mContext); } // Intent to load the app and show the timer when the notification is tapped. final Intent showApp = new Intent(mContext, HandleDeskClockApiCalls.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setAction(HandleDeskClockApiCalls.ACTION_SHOW_TIMERS) .putExtra(HandleDeskClockApiCalls.EXTRA_TIMER_ID, timer.getId()) .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, R.string.label_notification); final PendingIntent pendingShowApp = PendingIntent.getActivity(mContext, 0, showApp, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext).setOngoing(true) .setLocalOnly(true).setShowWhen(false).setAutoCancel(false).setContentText(contentText) .setContentTitle(contentTitle).setContentIntent(pendingShowApp) .setSmallIcon(R.drawable.stat_notify_timer).setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_ALARM).setVisibility(NotificationCompat.VISIBILITY_PUBLIC); final PendingIntent firstAction = PendingIntent.getService(mContext, 0, firstActionIntent, PendingIntent.FLAG_UPDATE_CURRENT); final String firstActionTitle = mContext.getString(firstActionTitleId); builder.addAction(firstActionIconId, firstActionTitle, firstAction); if (secondActionIntent != null) { final PendingIntent secondAction = PendingIntent.getService(mContext, 0, secondActionIntent, PendingIntent.FLAG_UPDATE_CURRENT); final String secondActionTitle = mContext.getString(secondActionTitleId); builder.addAction(secondActionIconId, secondActionTitle, secondAction); } // Update the notification. final Notification notification = builder.build(); final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId(); mNotificationManager.notify(notificationId, notification); final Intent updateNotification = TimerService.createUpdateNotificationIntent(mContext); if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) { // Schedule a callback to update the time-sensitive information of the running timer. final PendingIntent pi = PendingIntent.getService(mContext, 0, updateNotification, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS; final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange; schedulePendingIntent(triggerTime, pi); } else { // Cancel the update notification callback. final PendingIntent pi = PendingIntent.getService(mContext, 0, updateNotification, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE); if (pi != null) { mAlarmManager.cancel(pi); pi.cancel(); } } } /** * 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; } // Generate some descriptive text, a title, and an action name based on the timer count. final int timerId; final String contentText; final String contentTitle; final String resetActionTitle; if (expired.size() > 1) { timerId = -1; contentText = mContext.getString(R.string.timer_multi_times_up, expired.size()); contentTitle = mContext.getString(R.string.timer_notification_label); resetActionTitle = mContext.getString(R.string.timer_stop_all); } else { final Timer timer = expired.get(0); timerId = timer.getId(); resetActionTitle = mContext.getString(R.string.timer_stop); contentText = mContext.getString(R.string.timer_times_up); final String label = timer.getLabel(); if (TextUtils.isEmpty(label)) { contentTitle = mContext.getString(R.string.timer_notification_label); } else { contentTitle = label; } } // Content intent shows the timer full screen when clicked. final Intent content = new Intent(mContext, ExpiredTimersActivity.class); final PendingIntent pendingContent = PendingIntent.getActivity(mContext, 0, content, PendingIntent.FLAG_UPDATE_CURRENT); // Full screen intent has flags so it is different than the content intent. final Intent fullScreen = new Intent(mContext, ExpiredTimersActivity.class) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); final PendingIntent pendingFullScreen = PendingIntent.getActivity(mContext, 0, fullScreen, PendingIntent.FLAG_UPDATE_CURRENT); // First action intent is either reset single timer or reset all timers. final Intent reset = TimerService.createResetExpiredTimersIntent(mContext); final PendingIntent pendingReset = PendingIntent.getService(mContext, 0, reset, PendingIntent.FLAG_UPDATE_CURRENT); final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext).setWhen(0) .setOngoing(true).setLocalOnly(true).setAutoCancel(false).setContentText(contentText) .setContentTitle(contentTitle).setContentIntent(pendingContent) .setSmallIcon(R.drawable.stat_notify_timer).setFullScreenIntent(pendingFullScreen, true) .setPriority(NotificationCompat.PRIORITY_MAX).setDefaults(NotificationCompat.DEFAULT_LIGHTS) .addAction(R.drawable.ic_stop_24dp, resetActionTitle, pendingReset); // Add a second action if only a single timer is expired. if (expired.size() == 1) { // Second action intent adds a minute to a single timer. final Intent addMinute = TimerService.createAddMinuteTimerIntent(mContext, timerId); final PendingIntent pendingAddMinute = PendingIntent.getService(mContext, 0, addMinute, PendingIntent.FLAG_UPDATE_CURRENT); final String addMinuteTitle = mContext.getString(R.string.timer_plus_1_min); builder.addAction(R.drawable.ic_add_24dp, addMinuteTitle, pendingAddMinute); } // Update the notification. final Notification notification = builder.build(); final int notificationId = mNotificationModel.getExpiredTimerNotificationId(); mService.startForeground(notificationId, notification); } /** * Format "7 hours 52 minutes remaining" */ @VisibleForTesting String formatElapsedTimeUntilExpiry(long remainingTime) { final int hours = (int) remainingTime / (int) HOUR_IN_MILLIS; final int minutes = (int) remainingTime / ((int) MINUTE_IN_MILLIS) % 60; String minSeq = Utils.getNumberFormattedQuantityString(mContext, R.plurals.minutes, minutes); String hourSeq = Utils.getNumberFormattedQuantityString(mContext, R.plurals.hours, hours); // The verb "remaining" may have to change tense for singular subjects in some languages. final String verb = mContext.getString( (minutes > 1 || hours > 1) ? R.string.timer_remaining_multiple : R.string.timer_remaining_single); final boolean showHours = hours > 0; final boolean showMinutes = minutes > 0; int formatStringId; if (showHours) { if (showMinutes) { formatStringId = R.string.timer_notifications_hours_minutes; } else { formatStringId = R.string.timer_notifications_hours; } } else if (showMinutes) { formatStringId = R.string.timer_notifications_minutes; } else { formatStringId = R.string.timer_notifications_less_min; } return String.format(mContext.getString(formatStringId), hourSeq, minSeq, verb); } private void schedulePendingIntent(long triggerTime, PendingIntent pi) { if (Utils.isMOrLater()) { // Make sure the timer fires when the device is in doze mode. The timer is not // guaranteed to fire at the requested time. It may be delayed up to 15 minutes. mAlarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); } else { mAlarmManager.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); } } /** * Update the stopwatch notification in response to a locale change. */ private final class LocaleChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { updateNotification(); 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) { switch (key) { case SettingsActivity.KEY_TIMER_RINGTONE: mTimerRingtoneUri = null; mTimerRingtoneTitle = null; break; } } } }