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