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.annotation.SuppressLint; 20 import android.app.AlarmManager; 21 import android.app.Notification; 22 import android.app.PendingIntent; 23 import android.app.Service; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.SharedPreferences; 29 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 30 import android.net.Uri; 31 import android.support.annotation.StringRes; 32 import android.support.v4.app.NotificationManagerCompat; 33 import android.util.ArraySet; 34 35 import com.android.deskclock.AlarmAlertWakeLock; 36 import com.android.deskclock.LogUtils; 37 import com.android.deskclock.R; 38 import com.android.deskclock.Utils; 39 import com.android.deskclock.events.Events; 40 import com.android.deskclock.settings.SettingsActivity; 41 import com.android.deskclock.timer.TimerKlaxon; 42 import com.android.deskclock.timer.TimerService; 43 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.List; 47 import java.util.Set; 48 49 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; 50 import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 51 import static com.android.deskclock.data.Timer.State.EXPIRED; 52 import static com.android.deskclock.data.Timer.State.RESET; 53 54 /** 55 * All {@link Timer} data is accessed via this model. 56 */ 57 final class TimerModel { 58 59 /** 60 * Running timers less than this threshold are left running/expired; greater than this 61 * threshold are considered missed. 62 */ 63 private static final long MISSED_THRESHOLD = -MINUTE_IN_MILLIS; 64 65 private final Context mContext; 66 67 private final SharedPreferences mPrefs; 68 69 /** The alarm manager system service that calls back when timers expire. */ 70 private final AlarmManager mAlarmManager; 71 72 /** The model from which settings are fetched. */ 73 private final SettingsModel mSettingsModel; 74 75 /** The model from which notification data are fetched. */ 76 private final NotificationModel mNotificationModel; 77 78 /** The model from which ringtone data are fetched. */ 79 private final RingtoneModel mRingtoneModel; 80 81 /** Used to create and destroy system notifications related to timers. */ 82 private final NotificationManagerCompat mNotificationManager; 83 84 /** Update timer notification when locale changes. */ 85 @SuppressWarnings("FieldCanBeLocal") 86 private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver(); 87 88 /** 89 * Retain a hard reference to the shared preference observer to prevent it from being garbage 90 * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail. 91 */ 92 @SuppressWarnings("FieldCanBeLocal") 93 private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener(); 94 95 /** The listeners to notify when a timer is added, updated or removed. */ 96 private final List<TimerListener> mTimerListeners = new ArrayList<>(); 97 98 /** Delegate that builds platform-specific timer notifications. */ 99 private final TimerNotificationBuilder mNotificationBuilder = new TimerNotificationBuilder(); 100 101 /** 102 * The ids of expired timers for which the ringer is ringing. Not all expired timers have their 103 * ids in this collection. If a timer was already expired when the app was started its id will 104 * be absent from this collection. 105 */ 106 @SuppressLint("NewApi") 107 private final Set<Integer> mRingingIds = new ArraySet<>(); 108 109 /** The uri of the ringtone to play for timers. */ 110 private Uri mTimerRingtoneUri; 111 112 /** The title of the ringtone to play for timers. */ 113 private String mTimerRingtoneTitle; 114 115 /** A mutable copy of the timers. */ 116 private List<Timer> mTimers; 117 118 /** A mutable copy of the expired timers. */ 119 private List<Timer> mExpiredTimers; 120 121 /** A mutable copy of the missed timers. */ 122 private List<Timer> mMissedTimers; 123 124 /** 125 * The service that keeps this application in the foreground while a heads-up timer 126 * notification is displayed. Marking the service as foreground prevents the operating system 127 * from killing this application while expired timers are actively firing. 128 */ 129 private Service mService; 130 131 TimerModel(Context context, SharedPreferences prefs, SettingsModel settingsModel, 132 RingtoneModel ringtoneModel, NotificationModel notificationModel) { 133 mContext = context; 134 mPrefs = prefs; 135 mSettingsModel = settingsModel; 136 mRingtoneModel = ringtoneModel; 137 mNotificationModel = notificationModel; 138 mNotificationManager = NotificationManagerCompat.from(context); 139 140 mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 141 142 // Clear caches affected by preferences when preferences change. 143 prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener); 144 145 // Update timer notification when locale changes. 146 final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 147 mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter); 148 } 149 150 /** 151 * @param timerListener to be notified when timers are added, updated and removed 152 */ 153 void addTimerListener(TimerListener timerListener) { 154 mTimerListeners.add(timerListener); 155 } 156 157 /** 158 * @param timerListener to no longer be notified when timers are added, updated and removed 159 */ 160 void removeTimerListener(TimerListener timerListener) { 161 mTimerListeners.remove(timerListener); 162 } 163 164 /** 165 * @return all defined timers in their creation order 166 */ 167 List<Timer> getTimers() { 168 return Collections.unmodifiableList(getMutableTimers()); 169 } 170 171 /** 172 * @return all expired timers in their expiration order 173 */ 174 List<Timer> getExpiredTimers() { 175 return Collections.unmodifiableList(getMutableExpiredTimers()); 176 } 177 178 /** 179 * @return all missed timers in their expiration order 180 */ 181 private List<Timer> getMissedTimers() { 182 return Collections.unmodifiableList(getMutableMissedTimers()); 183 } 184 185 /** 186 * @param timerId identifies the timer to return 187 * @return the timer with the given {@code timerId} 188 */ 189 Timer getTimer(int timerId) { 190 for (Timer timer : getMutableTimers()) { 191 if (timer.getId() == timerId) { 192 return timer; 193 } 194 } 195 196 return null; 197 } 198 199 /** 200 * @return the timer that last expired and is still expired now; {@code null} if no timers are 201 * expired 202 */ 203 Timer getMostRecentExpiredTimer() { 204 final List<Timer> timers = getMutableExpiredTimers(); 205 return timers.isEmpty() ? null : timers.get(timers.size() - 1); 206 } 207 208 /** 209 * @param length the length of the timer in milliseconds 210 * @param label describes the purpose of the timer 211 * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset 212 * @return the newly added timer 213 */ 214 Timer addTimer(long length, String label, boolean deleteAfterUse) { 215 // Create the timer instance. 216 Timer timer = new Timer(-1, RESET, length, length, Timer.UNUSED, Timer.UNUSED, length, 217 label, deleteAfterUse); 218 219 // Add the timer to permanent storage. 220 timer = TimerDAO.addTimer(mPrefs, timer); 221 222 // Add the timer to the cache. 223 getMutableTimers().add(0, timer); 224 225 // Update the timer notification. 226 updateNotification(); 227 // Heads-Up notification is unaffected by this change 228 229 // Notify listeners of the change. 230 for (TimerListener timerListener : mTimerListeners) { 231 timerListener.timerAdded(timer); 232 } 233 234 return timer; 235 } 236 237 /** 238 * @param service used to start foreground notifications related to expired timers 239 * @param timer the timer to be expired 240 */ 241 void expireTimer(Service service, Timer timer) { 242 if (mService == null) { 243 // If this is the first expired timer, retain the service that will be used to start 244 // the heads-up notification in the foreground. 245 mService = service; 246 } else if (mService != service) { 247 // If this is not the first expired timer, the service should match the one given when 248 // the first timer expired. 249 LogUtils.wtf("Expected TimerServices to be identical"); 250 } 251 252 updateTimer(timer.expire()); 253 } 254 255 /** 256 * @param timer an updated timer to store 257 */ 258 void updateTimer(Timer timer) { 259 final Timer before = doUpdateTimer(timer); 260 261 // Update the notification after updating the timer data. 262 updateNotification(); 263 264 // If the timer started or stopped being expired, update the heads-up notification. 265 if (before.getState() != timer.getState()) { 266 if (before.isExpired() || timer.isExpired()) { 267 updateHeadsUpNotification(); 268 } 269 } 270 } 271 272 /** 273 * @param timer an existing timer to be removed 274 */ 275 void removeTimer(Timer timer) { 276 doRemoveTimer(timer); 277 278 // Update the timer notifications after removing the timer data. 279 if (timer.isExpired()) { 280 updateHeadsUpNotification(); 281 } else { 282 updateNotification(); 283 } 284 } 285 286 /** 287 * If the given {@code timer} is expired and marked for deletion after use then this method 288 * removes the the timer. The timer is otherwise transitioned to the reset state and continues 289 * to exist. 290 * 291 * @param timer the timer to be reset 292 * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset 293 * (e.g. one use timers) 294 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 295 * @return the reset {@code timer} or {@code null} if the timer was deleted 296 */ 297 Timer resetTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId) { 298 final Timer result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId); 299 300 // Update the notification after updating the timer data. 301 if (timer.isMissed()) { 302 updateMissedNotification(); 303 } else if (timer.isExpired()) { 304 updateHeadsUpNotification(); 305 } else { 306 updateNotification(); 307 } 308 309 return result; 310 } 311 312 /** 313 * Update timers after system reboot. 314 */ 315 void updateTimersAfterReboot() { 316 final List<Timer> timers = new ArrayList<>(getTimers()); 317 for (Timer timer : timers) { 318 doUpdateAfterRebootTimer(timer); 319 } 320 321 // Update the notifications once after all timers are updated. 322 updateNotification(); 323 updateMissedNotification(); 324 updateHeadsUpNotification(); 325 } 326 327 /** 328 * Update timers after time set. 329 */ 330 void updateTimersAfterTimeSet() { 331 final List<Timer> timers = new ArrayList<>(getTimers()); 332 for (Timer timer : timers) { 333 doUpdateAfterTimeSetTimer(timer); 334 } 335 336 // Update the notifications once after all timers are updated. 337 updateNotification(); 338 updateMissedNotification(); 339 updateHeadsUpNotification(); 340 } 341 342 /** 343 * Reset all expired timers. Exactly one parameter should be filled, with preference given to 344 * eventLabelId. 345 * 346 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 347 */ 348 void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) { 349 final List<Timer> timers = new ArrayList<>(getTimers()); 350 for (Timer timer : timers) { 351 if (timer.isExpired()) { 352 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); 353 } 354 } 355 356 // Update the notifications once after all timers are updated. 357 updateHeadsUpNotification(); 358 } 359 360 /** 361 * Reset all missed timers. 362 * 363 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 364 */ 365 void resetMissedTimers(@StringRes int eventLabelId) { 366 final List<Timer> timers = new ArrayList<>(getTimers()); 367 for (Timer timer : timers) { 368 if (timer.isMissed()) { 369 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); 370 } 371 } 372 373 // Update the notifications once after all timers are updated. 374 updateMissedNotification(); 375 } 376 377 /** 378 * Reset all unexpired timers. 379 * 380 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 381 */ 382 void resetUnexpiredTimers(@StringRes int eventLabelId) { 383 final List<Timer> timers = new ArrayList<>(getTimers()); 384 for (Timer timer : timers) { 385 if (timer.isRunning() || timer.isPaused()) { 386 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); 387 } 388 } 389 390 // Update the notification once after all timers are updated. 391 updateNotification(); 392 // Heads-Up notification is unaffected by this change 393 } 394 395 /** 396 * @return the uri of the default ringtone to play for all timers when no user selection exists 397 */ 398 Uri getDefaultTimerRingtoneUri() { 399 return mSettingsModel.getDefaultTimerRingtoneUri(); 400 } 401 402 /** 403 * @return {@code true} iff the ringtone to play for all timers is the silent ringtone 404 */ 405 boolean isTimerRingtoneSilent() { 406 return Uri.EMPTY.equals(getTimerRingtoneUri()); 407 } 408 409 /** 410 * @return the uri of the ringtone to play for all timers 411 */ 412 Uri getTimerRingtoneUri() { 413 if (mTimerRingtoneUri == null) { 414 mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri(); 415 } 416 417 return mTimerRingtoneUri; 418 } 419 420 /** 421 * @param uri the uri of the ringtone to play for all timers 422 */ 423 void setTimerRingtoneUri(Uri uri) { 424 mSettingsModel.setTimerRingtoneUri(uri); 425 } 426 427 /** 428 * @return the title of the ringtone that is played for all timers 429 */ 430 String getTimerRingtoneTitle() { 431 if (mTimerRingtoneTitle == null) { 432 if (isTimerRingtoneSilent()) { 433 // Special case: no ringtone has a title of "Silent". 434 mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title); 435 } else { 436 final Uri defaultUri = getDefaultTimerRingtoneUri(); 437 final Uri uri = getTimerRingtoneUri(); 438 439 if (defaultUri.equals(uri)) { 440 // Special case: default ringtone has a title of "Timer Expired". 441 mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title); 442 } else { 443 mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri); 444 } 445 } 446 } 447 448 return mTimerRingtoneTitle; 449 } 450 451 /** 452 * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback; 453 * {@code 0} implies no crescendo should be applied 454 */ 455 long getTimerCrescendoDuration() { 456 return mSettingsModel.getTimerCrescendoDuration(); 457 } 458 459 /** 460 * @return {@code true} if the device vibrates when timers expire 461 */ 462 boolean getTimerVibrate() { 463 return mSettingsModel.getTimerVibrate(); 464 } 465 466 /** 467 * @param enabled {@code true} if the device should vibrate when timers expire 468 */ 469 void setTimerVibrate(boolean enabled) { 470 mSettingsModel.setTimerVibrate(enabled); 471 } 472 473 private List<Timer> getMutableTimers() { 474 if (mTimers == null) { 475 mTimers = TimerDAO.getTimers(mPrefs); 476 Collections.sort(mTimers, Timer.ID_COMPARATOR); 477 } 478 479 return mTimers; 480 } 481 482 private List<Timer> getMutableExpiredTimers() { 483 if (mExpiredTimers == null) { 484 mExpiredTimers = new ArrayList<>(); 485 486 for (Timer timer : getMutableTimers()) { 487 if (timer.isExpired()) { 488 mExpiredTimers.add(timer); 489 } 490 } 491 Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR); 492 } 493 494 return mExpiredTimers; 495 } 496 497 private List<Timer> getMutableMissedTimers() { 498 if (mMissedTimers == null) { 499 mMissedTimers = new ArrayList<>(); 500 501 for (Timer timer : getMutableTimers()) { 502 if (timer.isMissed()) { 503 mMissedTimers.add(timer); 504 } 505 } 506 Collections.sort(mMissedTimers, Timer.EXPIRY_COMPARATOR); 507 } 508 509 return mMissedTimers; 510 } 511 512 /** 513 * This method updates timer data without updating notifications. This is useful in bulk-update 514 * scenarios so the notifications are only rebuilt once. 515 * 516 * @param timer an updated timer to store 517 * @return the state of the timer prior to the update 518 */ 519 private Timer doUpdateTimer(Timer timer) { 520 // Retrieve the cached form of the timer. 521 final List<Timer> timers = getMutableTimers(); 522 final int index = timers.indexOf(timer); 523 final Timer before = timers.get(index); 524 525 // If no change occurred, ignore this update. 526 if (timer == before) { 527 return timer; 528 } 529 530 // Update the timer in permanent storage. 531 TimerDAO.updateTimer(mPrefs, timer); 532 533 // Update the timer in the cache. 534 final Timer oldTimer = timers.set(index, timer); 535 536 // Clear the cache of expired timers if the timer changed to/from expired. 537 if (before.isExpired() || timer.isExpired()) { 538 mExpiredTimers = null; 539 } 540 // Clear the cache of missed timers if the timer changed to/from missed. 541 if (before.isMissed() || timer.isMissed()) { 542 mMissedTimers = null; 543 } 544 545 // Update the timer expiration callback. 546 updateAlarmManager(); 547 548 // Update the timer ringer. 549 updateRinger(before, timer); 550 551 // Notify listeners of the change. 552 for (TimerListener timerListener : mTimerListeners) { 553 timerListener.timerUpdated(before, timer); 554 } 555 556 return oldTimer; 557 } 558 559 /** 560 * This method removes timer data without updating notifications. This is useful in bulk-remove 561 * scenarios so the notifications are only rebuilt once. 562 * 563 * @param timer an existing timer to be removed 564 */ 565 private void doRemoveTimer(Timer timer) { 566 // Remove the timer from permanent storage. 567 TimerDAO.removeTimer(mPrefs, timer); 568 569 // Remove the timer from the cache. 570 final List<Timer> timers = getMutableTimers(); 571 final int index = timers.indexOf(timer); 572 573 // If the timer cannot be located there is nothing to remove. 574 if (index == -1) { 575 return; 576 } 577 578 timer = timers.remove(index); 579 580 // Clear the cache of expired timers if a new expired timer was added. 581 if (timer.isExpired()) { 582 mExpiredTimers = null; 583 } 584 585 // Clear the cache of missed timers if a new missed timer was added. 586 if (timer.isMissed()) { 587 mMissedTimers = null; 588 } 589 590 // Update the timer expiration callback. 591 updateAlarmManager(); 592 593 // Update the timer ringer. 594 updateRinger(timer, null); 595 596 // Notify listeners of the change. 597 for (TimerListener timerListener : mTimerListeners) { 598 timerListener.timerRemoved(timer); 599 } 600 } 601 602 /** 603 * This method updates/removes timer data without updating notifications. This is useful in 604 * bulk-update scenarios so the notifications are only rebuilt once. 605 * 606 * If the given {@code timer} is expired and marked for deletion after use then this method 607 * removes the the timer. The timer is otherwise transitioned to the reset state and continues 608 * to exist. 609 * 610 * @param timer the timer to be reset 611 * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset 612 * (e.g. one use timers) 613 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 614 * @return the reset {@code timer} or {@code null} if the timer was deleted 615 */ 616 private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete, 617 @StringRes int eventLabelId) { 618 if (allowDelete 619 && (timer.isExpired() || timer.isMissed()) 620 && timer.getDeleteAfterUse()) { 621 doRemoveTimer(timer); 622 if (eventLabelId != 0) { 623 Events.sendTimerEvent(R.string.action_delete, eventLabelId); 624 } 625 return null; 626 } else if (!timer.isReset()) { 627 final Timer reset = timer.reset(); 628 doUpdateTimer(reset); 629 if (eventLabelId != 0) { 630 Events.sendTimerEvent(R.string.action_reset, eventLabelId); 631 } 632 return reset; 633 } 634 635 return timer; 636 } 637 638 /** 639 * This method updates/removes timer data after a reboot without updating notifications. 640 * 641 * @param timer the timer to be updated 642 */ 643 private void doUpdateAfterRebootTimer(Timer timer) { 644 Timer updated = timer.updateAfterReboot(); 645 if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) { 646 updated = updated.miss(); 647 } 648 doUpdateTimer(updated); 649 } 650 651 private void doUpdateAfterTimeSetTimer(Timer timer) { 652 final Timer updated = timer.updateAfterTimeSet(); 653 doUpdateTimer(updated); 654 } 655 656 657 /** 658 * Updates the callback given to this application from the {@link AlarmManager} that signals the 659 * expiration of the next timer. If no timers are currently set to expire (i.e. no running 660 * timers exist) then this method clears the expiration callback from AlarmManager. 661 */ 662 private void updateAlarmManager() { 663 // Locate the next firing timer if one exists. 664 Timer nextExpiringTimer = null; 665 for (Timer timer : getMutableTimers()) { 666 if (timer.isRunning()) { 667 if (nextExpiringTimer == null) { 668 nextExpiringTimer = timer; 669 } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) { 670 nextExpiringTimer = timer; 671 } 672 } 673 } 674 675 // Build the intent that signals the timer expiration. 676 final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer); 677 678 if (nextExpiringTimer == null) { 679 // Cancel the existing timer expiration callback. 680 final PendingIntent pi = PendingIntent.getService(mContext, 681 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE); 682 if (pi != null) { 683 mAlarmManager.cancel(pi); 684 pi.cancel(); 685 } 686 } else { 687 // Update the existing timer expiration callback. 688 final PendingIntent pi = PendingIntent.getService(mContext, 689 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 690 schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi); 691 } 692 } 693 694 /** 695 * Starts and stops the ringer for timers if the change to the timer demands it. 696 * 697 * @param before the state of the timer before the change; {@code null} indicates added 698 * @param after the state of the timer after the change; {@code null} indicates delete 699 */ 700 private void updateRinger(Timer before, Timer after) { 701 // Retrieve the states before and after the change. 702 final Timer.State beforeState = before == null ? null : before.getState(); 703 final Timer.State afterState = after == null ? null : after.getState(); 704 705 // If the timer state did not change, the ringer state is unchanged. 706 if (beforeState == afterState) { 707 return; 708 } 709 710 // If the timer is the first to expire, start ringing. 711 if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) { 712 AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext); 713 TimerKlaxon.start(mContext); 714 } 715 716 // If the expired timer was the last to reset, stop ringing. 717 if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) { 718 TimerKlaxon.stop(mContext); 719 AlarmAlertWakeLock.releaseCpuLock(); 720 } 721 } 722 723 /** 724 * Updates the notification controlling unexpired timers. This notification is only displayed 725 * when the application is not open. 726 */ 727 void updateNotification() { 728 // Notifications should be hidden if the app is open. 729 if (mNotificationModel.isApplicationInForeground()) { 730 mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId()); 731 return; 732 } 733 734 // Filter the timers to just include unexpired ones. 735 final List<Timer> unexpired = new ArrayList<>(); 736 for (Timer timer : getMutableTimers()) { 737 if (timer.isRunning() || timer.isPaused()) { 738 unexpired.add(timer); 739 } 740 } 741 742 // If no unexpired timers exist, cancel the notification. 743 if (unexpired.isEmpty()) { 744 mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId()); 745 return; 746 } 747 748 // Sort the unexpired timers to locate the next one scheduled to expire. 749 Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR); 750 751 // Otherwise build and post a notification reflecting the latest unexpired timers. 752 final Notification notification = 753 mNotificationBuilder.build(mContext, mNotificationModel, unexpired); 754 final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId(); 755 mNotificationManager.notify(notificationId, notification); 756 757 } 758 759 /** 760 * Updates the notification controlling missed timers. This notification is only displayed when 761 * the application is not open. 762 */ 763 void updateMissedNotification() { 764 // Notifications should be hidden if the app is open. 765 if (mNotificationModel.isApplicationInForeground()) { 766 mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId()); 767 return; 768 } 769 770 final List<Timer> missed = getMissedTimers(); 771 772 if (missed.isEmpty()) { 773 mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId()); 774 return; 775 } 776 777 final Notification notification = mNotificationBuilder.buildMissed(mContext, 778 mNotificationModel, missed); 779 final int notificationId = mNotificationModel.getMissedTimerNotificationId(); 780 mNotificationManager.notify(notificationId, notification); 781 } 782 783 /** 784 * Updates the heads-up notification controlling expired timers. This heads-up notification is 785 * displayed whether the application is open or not. 786 */ 787 private void updateHeadsUpNotification() { 788 // Nothing can be done with the heads-up notification without a valid service reference. 789 if (mService == null) { 790 return; 791 } 792 793 final List<Timer> expired = getExpiredTimers(); 794 795 // If no expired timers exist, stop the service (which cancels the foreground notification). 796 if (expired.isEmpty()) { 797 mService.stopSelf(); 798 mService = null; 799 return; 800 } 801 802 // Otherwise build and post a foreground notification reflecting the latest expired timers. 803 final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired); 804 final int notificationId = mNotificationModel.getExpiredTimerNotificationId(); 805 mService.startForeground(notificationId, notification); 806 } 807 808 /** 809 * Update the timer notification in response to a locale change. 810 */ 811 private final class LocaleChangedReceiver extends BroadcastReceiver { 812 @Override 813 public void onReceive(Context context, Intent intent) { 814 mTimerRingtoneTitle = null; 815 updateNotification(); 816 updateMissedNotification(); 817 updateHeadsUpNotification(); 818 } 819 } 820 821 /** 822 * This receiver is notified when shared preferences change. Cached information built on 823 * preferences must be cleared. 824 */ 825 private final class PreferenceListener implements OnSharedPreferenceChangeListener { 826 @Override 827 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 828 switch (key) { 829 case SettingsActivity.KEY_TIMER_RINGTONE: 830 mTimerRingtoneUri = null; 831 mTimerRingtoneTitle = null; 832 break; 833 } 834 } 835 } 836 837 static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) { 838 if (Utils.isMOrLater()) { 839 // Ensure the timer fires even if the device is dozing. 840 am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); 841 } else { 842 am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); 843 } 844 } 845 } 846