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 17package com.android.deskclock.data; 18 19import android.annotation.SuppressLint; 20import android.app.AlarmManager; 21import android.app.Notification; 22import android.app.PendingIntent; 23import android.app.Service; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.content.SharedPreferences; 29import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 30import android.net.Uri; 31import android.support.annotation.StringRes; 32import android.support.v4.app.NotificationManagerCompat; 33import android.util.ArraySet; 34 35import com.android.deskclock.AlarmAlertWakeLock; 36import com.android.deskclock.LogUtils; 37import com.android.deskclock.R; 38import com.android.deskclock.Utils; 39import com.android.deskclock.events.Events; 40import com.android.deskclock.settings.SettingsActivity; 41import com.android.deskclock.timer.TimerKlaxon; 42import com.android.deskclock.timer.TimerService; 43 44import java.util.ArrayList; 45import java.util.Collections; 46import java.util.List; 47import java.util.Set; 48 49import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; 50import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 51import static com.android.deskclock.data.Timer.State.EXPIRED; 52import static com.android.deskclock.data.Timer.State.RESET; 53 54/** 55 * All {@link Timer} data is accessed via this model. 56 */ 57final 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. 344 * 345 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 346 */ 347 void resetExpiredTimers(@StringRes int eventLabelId) { 348 final List<Timer> timers = new ArrayList<>(getTimers()); 349 for (Timer timer : timers) { 350 if (timer.isExpired()) { 351 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); 352 } 353 } 354 355 // Update the notifications once after all timers are updated. 356 updateHeadsUpNotification(); 357 } 358 359 /** 360 * Reset all missed timers. 361 * 362 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 363 */ 364 void resetMissedTimers(@StringRes int eventLabelId) { 365 final List<Timer> timers = new ArrayList<>(getTimers()); 366 for (Timer timer : timers) { 367 if (timer.isMissed()) { 368 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); 369 } 370 } 371 372 // Update the notifications once after all timers are updated. 373 updateMissedNotification(); 374 } 375 376 /** 377 * Reset all unexpired timers. 378 * 379 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 380 */ 381 void resetUnexpiredTimers(@StringRes int eventLabelId) { 382 final List<Timer> timers = new ArrayList<>(getTimers()); 383 for (Timer timer : timers) { 384 if (timer.isRunning() || timer.isPaused()) { 385 doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId); 386 } 387 } 388 389 // Update the notification once after all timers are updated. 390 updateNotification(); 391 // Heads-Up notification is unaffected by this change 392 } 393 394 /** 395 * @return the uri of the default ringtone to play for all timers when no user selection exists 396 */ 397 Uri getDefaultTimerRingtoneUri() { 398 return mSettingsModel.getDefaultTimerRingtoneUri(); 399 } 400 401 /** 402 * @return {@code true} iff the ringtone to play for all timers is the silent ringtone 403 */ 404 boolean isTimerRingtoneSilent() { 405 return Uri.EMPTY.equals(getTimerRingtoneUri()); 406 } 407 408 /** 409 * @return the uri of the ringtone to play for all timers 410 */ 411 Uri getTimerRingtoneUri() { 412 if (mTimerRingtoneUri == null) { 413 mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri(); 414 } 415 416 return mTimerRingtoneUri; 417 } 418 419 /** 420 * @param uri the uri of the ringtone to play for all timers 421 */ 422 void setTimerRingtoneUri(Uri uri) { 423 mSettingsModel.setTimerRingtoneUri(uri); 424 } 425 426 /** 427 * @return the title of the ringtone that is played for all timers 428 */ 429 String getTimerRingtoneTitle() { 430 if (mTimerRingtoneTitle == null) { 431 if (isTimerRingtoneSilent()) { 432 // Special case: no ringtone has a title of "Silent". 433 mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title); 434 } else { 435 final Uri defaultUri = getDefaultTimerRingtoneUri(); 436 final Uri uri = getTimerRingtoneUri(); 437 438 if (defaultUri.equals(uri)) { 439 // Special case: default ringtone has a title of "Timer Expired". 440 mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title); 441 } else { 442 mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri); 443 } 444 } 445 } 446 447 return mTimerRingtoneTitle; 448 } 449 450 /** 451 * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback; 452 * {@code 0} implies no crescendo should be applied 453 */ 454 long getTimerCrescendoDuration() { 455 return mSettingsModel.getTimerCrescendoDuration(); 456 } 457 458 /** 459 * @return {@code true} if the device vibrates when timers expire 460 */ 461 boolean getTimerVibrate() { 462 return mSettingsModel.getTimerVibrate(); 463 } 464 465 /** 466 * @param enabled {@code true} if the device should vibrate when timers expire 467 */ 468 void setTimerVibrate(boolean enabled) { 469 mSettingsModel.setTimerVibrate(enabled); 470 } 471 472 private List<Timer> getMutableTimers() { 473 if (mTimers == null) { 474 mTimers = TimerDAO.getTimers(mPrefs); 475 Collections.sort(mTimers, Timer.ID_COMPARATOR); 476 } 477 478 return mTimers; 479 } 480 481 private List<Timer> getMutableExpiredTimers() { 482 if (mExpiredTimers == null) { 483 mExpiredTimers = new ArrayList<>(); 484 485 for (Timer timer : getMutableTimers()) { 486 if (timer.isExpired()) { 487 mExpiredTimers.add(timer); 488 } 489 } 490 Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR); 491 } 492 493 return mExpiredTimers; 494 } 495 496 private List<Timer> getMutableMissedTimers() { 497 if (mMissedTimers == null) { 498 mMissedTimers = new ArrayList<>(); 499 500 for (Timer timer : getMutableTimers()) { 501 if (timer.isMissed()) { 502 mMissedTimers.add(timer); 503 } 504 } 505 Collections.sort(mMissedTimers, Timer.EXPIRY_COMPARATOR); 506 } 507 508 return mMissedTimers; 509 } 510 511 /** 512 * This method updates timer data without updating notifications. This is useful in bulk-update 513 * scenarios so the notifications are only rebuilt once. 514 * 515 * @param timer an updated timer to store 516 * @return the state of the timer prior to the update 517 */ 518 private Timer doUpdateTimer(Timer timer) { 519 // Retrieve the cached form of the timer. 520 final List<Timer> timers = getMutableTimers(); 521 final int index = timers.indexOf(timer); 522 final Timer before = timers.get(index); 523 524 // If no change occurred, ignore this update. 525 if (timer == before) { 526 return timer; 527 } 528 529 // Update the timer in permanent storage. 530 TimerDAO.updateTimer(mPrefs, timer); 531 532 // Update the timer in the cache. 533 final Timer oldTimer = timers.set(index, timer); 534 535 // Clear the cache of expired timers if the timer changed to/from expired. 536 if (before.isExpired() || timer.isExpired()) { 537 mExpiredTimers = null; 538 } 539 // Clear the cache of missed timers if the timer changed to/from missed. 540 if (before.isMissed() || timer.isMissed()) { 541 mMissedTimers = null; 542 } 543 544 // Update the timer expiration callback. 545 updateAlarmManager(); 546 547 // Update the timer ringer. 548 updateRinger(before, timer); 549 550 // Notify listeners of the change. 551 for (TimerListener timerListener : mTimerListeners) { 552 timerListener.timerUpdated(before, timer); 553 } 554 555 return oldTimer; 556 } 557 558 /** 559 * This method removes timer data without updating notifications. This is useful in bulk-remove 560 * scenarios so the notifications are only rebuilt once. 561 * 562 * @param timer an existing timer to be removed 563 */ 564 private void doRemoveTimer(Timer timer) { 565 // Remove the timer from permanent storage. 566 TimerDAO.removeTimer(mPrefs, timer); 567 568 // Remove the timer from the cache. 569 final List<Timer> timers = getMutableTimers(); 570 final int index = timers.indexOf(timer); 571 572 // If the timer cannot be located there is nothing to remove. 573 if (index == -1) { 574 return; 575 } 576 577 timer = timers.remove(index); 578 579 // Clear the cache of expired timers if a new expired timer was added. 580 if (timer.isExpired()) { 581 mExpiredTimers = null; 582 } 583 584 // Clear the cache of missed timers if a new missed timer was added. 585 if (timer.isMissed()) { 586 mMissedTimers = null; 587 } 588 589 // Update the timer expiration callback. 590 updateAlarmManager(); 591 592 // Update the timer ringer. 593 updateRinger(timer, null); 594 595 // Notify listeners of the change. 596 for (TimerListener timerListener : mTimerListeners) { 597 timerListener.timerRemoved(timer); 598 } 599 } 600 601 /** 602 * This method updates/removes timer data without updating notifications. This is useful in 603 * bulk-update scenarios so the notifications are only rebuilt once. 604 * 605 * If the given {@code timer} is expired and marked for deletion after use then this method 606 * removes the the timer. The timer is otherwise transitioned to the reset state and continues 607 * to exist. 608 * 609 * @param timer the timer to be reset 610 * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset 611 * (e.g. one use timers) 612 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 613 * @return the reset {@code timer} or {@code null} if the timer was deleted 614 */ 615 private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete, 616 @StringRes int eventLabelId) { 617 if (allowDelete 618 && (timer.isExpired() || timer.isMissed()) 619 && timer.getDeleteAfterUse()) { 620 doRemoveTimer(timer); 621 if (eventLabelId != 0) { 622 Events.sendTimerEvent(R.string.action_delete, eventLabelId); 623 } 624 return null; 625 } else if (!timer.isReset()) { 626 final Timer reset = timer.reset(); 627 doUpdateTimer(reset); 628 if (eventLabelId != 0) { 629 Events.sendTimerEvent(R.string.action_reset, eventLabelId); 630 } 631 return reset; 632 } 633 634 return timer; 635 } 636 637 /** 638 * This method updates/removes timer data after a reboot without updating notifications. 639 * 640 * @param timer the timer to be updated 641 */ 642 private void doUpdateAfterRebootTimer(Timer timer) { 643 Timer updated = timer.updateAfterReboot(); 644 if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) { 645 updated = updated.miss(); 646 } 647 doUpdateTimer(updated); 648 } 649 650 private void doUpdateAfterTimeSetTimer(Timer timer) { 651 final Timer updated = timer.updateAfterTimeSet(); 652 doUpdateTimer(updated); 653 } 654 655 656 /** 657 * Updates the callback given to this application from the {@link AlarmManager} that signals the 658 * expiration of the next timer. If no timers are currently set to expire (i.e. no running 659 * timers exist) then this method clears the expiration callback from AlarmManager. 660 */ 661 private void updateAlarmManager() { 662 // Locate the next firing timer if one exists. 663 Timer nextExpiringTimer = null; 664 for (Timer timer : getMutableTimers()) { 665 if (timer.isRunning()) { 666 if (nextExpiringTimer == null) { 667 nextExpiringTimer = timer; 668 } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) { 669 nextExpiringTimer = timer; 670 } 671 } 672 } 673 674 // Build the intent that signals the timer expiration. 675 final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer); 676 677 if (nextExpiringTimer == null) { 678 // Cancel the existing timer expiration callback. 679 final PendingIntent pi = PendingIntent.getService(mContext, 680 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE); 681 if (pi != null) { 682 mAlarmManager.cancel(pi); 683 pi.cancel(); 684 } 685 } else { 686 // Update the existing timer expiration callback. 687 final PendingIntent pi = PendingIntent.getService(mContext, 688 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 689 schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi); 690 } 691 } 692 693 /** 694 * Starts and stops the ringer for timers if the change to the timer demands it. 695 * 696 * @param before the state of the timer before the change; {@code null} indicates added 697 * @param after the state of the timer after the change; {@code null} indicates delete 698 */ 699 private void updateRinger(Timer before, Timer after) { 700 // Retrieve the states before and after the change. 701 final Timer.State beforeState = before == null ? null : before.getState(); 702 final Timer.State afterState = after == null ? null : after.getState(); 703 704 // If the timer state did not change, the ringer state is unchanged. 705 if (beforeState == afterState) { 706 return; 707 } 708 709 // If the timer is the first to expire, start ringing. 710 if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) { 711 AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext); 712 TimerKlaxon.start(mContext); 713 } 714 715 // If the expired timer was the last to reset, stop ringing. 716 if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) { 717 TimerKlaxon.stop(mContext); 718 AlarmAlertWakeLock.releaseCpuLock(); 719 } 720 } 721 722 /** 723 * Updates the notification controlling unexpired timers. This notification is only displayed 724 * when the application is not open. 725 */ 726 void updateNotification() { 727 // Notifications should be hidden if the app is open. 728 if (mNotificationModel.isApplicationInForeground()) { 729 mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId()); 730 return; 731 } 732 733 // Filter the timers to just include unexpired ones. 734 final List<Timer> unexpired = new ArrayList<>(); 735 for (Timer timer : getMutableTimers()) { 736 if (timer.isRunning() || timer.isPaused()) { 737 unexpired.add(timer); 738 } 739 } 740 741 // If no unexpired timers exist, cancel the notification. 742 if (unexpired.isEmpty()) { 743 mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId()); 744 return; 745 } 746 747 // Sort the unexpired timers to locate the next one scheduled to expire. 748 Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR); 749 750 // Otherwise build and post a notification reflecting the latest unexpired timers. 751 final Notification notification = 752 mNotificationBuilder.build(mContext, mNotificationModel, unexpired); 753 final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId(); 754 mNotificationManager.notify(notificationId, notification); 755 756 } 757 758 /** 759 * Updates the notification controlling missed timers. This notification is only displayed when 760 * the application is not open. 761 */ 762 void updateMissedNotification() { 763 // Notifications should be hidden if the app is open. 764 if (mNotificationModel.isApplicationInForeground()) { 765 mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId()); 766 return; 767 } 768 769 final List<Timer> missed = getMissedTimers(); 770 771 if (missed.isEmpty()) { 772 mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId()); 773 return; 774 } 775 776 final Notification notification = mNotificationBuilder.buildMissed(mContext, 777 mNotificationModel, missed); 778 final int notificationId = mNotificationModel.getMissedTimerNotificationId(); 779 mNotificationManager.notify(notificationId, notification); 780 } 781 782 /** 783 * Updates the heads-up notification controlling expired timers. This heads-up notification is 784 * displayed whether the application is open or not. 785 */ 786 private void updateHeadsUpNotification() { 787 // Nothing can be done with the heads-up notification without a valid service reference. 788 if (mService == null) { 789 return; 790 } 791 792 final List<Timer> expired = getExpiredTimers(); 793 794 // If no expired timers exist, stop the service (which cancels the foreground notification). 795 if (expired.isEmpty()) { 796 mService.stopSelf(); 797 mService = null; 798 return; 799 } 800 801 // Otherwise build and post a foreground notification reflecting the latest expired timers. 802 final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired); 803 final int notificationId = mNotificationModel.getExpiredTimerNotificationId(); 804 mService.startForeground(notificationId, notification); 805 } 806 807 /** 808 * Update the timer notification in response to a locale change. 809 */ 810 private final class LocaleChangedReceiver extends BroadcastReceiver { 811 @Override 812 public void onReceive(Context context, Intent intent) { 813 mTimerRingtoneTitle = null; 814 updateNotification(); 815 updateMissedNotification(); 816 updateHeadsUpNotification(); 817 } 818 } 819 820 /** 821 * This receiver is notified when shared preferences change. Cached information built on 822 * preferences must be cleared. 823 */ 824 private final class PreferenceListener implements OnSharedPreferenceChangeListener { 825 @Override 826 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 827 switch (key) { 828 case SettingsActivity.KEY_TIMER_RINGTONE: 829 mTimerRingtoneUri = null; 830 mTimerRingtoneTitle = null; 831 break; 832 } 833 } 834 } 835 836 static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) { 837 if (Utils.isMOrLater()) { 838 // Ensure the timer fires even if the device is dozing. 839 am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); 840 } else { 841 am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi); 842 } 843 } 844} 845