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