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.Service; 20 import android.content.Context; 21 import android.net.Uri; 22 import android.os.Handler; 23 import android.os.Looper; 24 import android.support.annotation.StringRes; 25 26 import java.util.Collection; 27 import java.util.Comparator; 28 import java.util.List; 29 30 import static com.android.deskclock.Utils.enforceMainLooper; 31 32 /** 33 * All application-wide data is accessible through this singleton. 34 */ 35 public final class DataModel { 36 37 /** Indicates the display style of clocks. */ 38 public enum ClockStyle {ANALOG, DIGITAL} 39 40 /** Indicates the preferred sort order of cities. */ 41 public enum CitySort {NAME, UTC_OFFSET} 42 43 public static final String ACTION_DIGITAL_WIDGET_CHANGED = 44 "com.android.deskclock.DIGITAL_WIDGET_CHANGED"; 45 46 /** The single instance of this data model that exists for the life of the application. */ 47 private static final DataModel sDataModel = new DataModel(); 48 49 private Handler mHandler; 50 51 private Context mContext; 52 53 /** The model from which settings are fetched. */ 54 private SettingsModel mSettingsModel; 55 56 /** The model from which city data are fetched. */ 57 private CityModel mCityModel; 58 59 /** The model from which timer data are fetched. */ 60 private TimerModel mTimerModel; 61 62 /** The model from which alarm data are fetched. */ 63 private AlarmModel mAlarmModel; 64 65 /** The model from which stopwatch data are fetched. */ 66 private StopwatchModel mStopwatchModel; 67 68 /** The model from which notification data are fetched. */ 69 private NotificationModel mNotificationModel; 70 71 public static DataModel getDataModel() { 72 return sDataModel; 73 } 74 75 private DataModel() {} 76 77 /** 78 * The context may be set precisely once during the application life. 79 */ 80 public void setContext(Context context) { 81 if (mContext != null) { 82 throw new IllegalStateException("context has already been set"); 83 } 84 mContext = context.getApplicationContext(); 85 86 mSettingsModel = new SettingsModel(mContext); 87 mNotificationModel = new NotificationModel(); 88 mCityModel = new CityModel(mContext, mSettingsModel); 89 mAlarmModel = new AlarmModel(mContext, mSettingsModel); 90 mStopwatchModel = new StopwatchModel(mContext, mNotificationModel); 91 mTimerModel = new TimerModel(mContext, mSettingsModel, mNotificationModel); 92 } 93 94 /** 95 * Posts a runnable to the main thread and blocks until the runnable executes. Used to access 96 * the data model from the main thread. 97 */ 98 public void run(Runnable runnable) { 99 if (Looper.myLooper() == Looper.getMainLooper()) { 100 runnable.run(); 101 return; 102 } 103 104 final ExecutedRunnable er = new ExecutedRunnable(runnable); 105 getHandler().post(er); 106 107 // Wait for the data to arrive, if it has not. 108 synchronized (er) { 109 if (!er.isExecuted()) { 110 try { 111 er.wait(); 112 } catch (InterruptedException ignored) { 113 // ignore 114 } 115 } 116 } 117 } 118 119 /** 120 * @return a handler associated with the main thread 121 */ 122 private synchronized Handler getHandler() { 123 if (mHandler == null) { 124 mHandler = new Handler(Looper.getMainLooper()); 125 } 126 return mHandler; 127 } 128 129 // 130 // Application 131 // 132 133 /** 134 * @param inForeground {@code true} to indicate the application is open in the foreground 135 */ 136 public void setApplicationInForeground(boolean inForeground) { 137 enforceMainLooper(); 138 139 if (mNotificationModel.isApplicationInForeground() != inForeground) { 140 mNotificationModel.setApplicationInForeground(inForeground); 141 142 // Refresh all notifications in response to a change in app open state. 143 mTimerModel.updateNotification(); 144 mStopwatchModel.updateNotification(); 145 } 146 } 147 148 /** 149 * @return {@code true} when the application is open in the foreground; {@code false} otherwise 150 */ 151 public boolean isApplicationInForeground() { 152 return mNotificationModel.isApplicationInForeground(); 153 } 154 155 /** 156 * Called when the notifications may be stale or absent from the notification manager and must 157 * be rebuilt. e.g. after upgrading the application 158 */ 159 public void updateAllNotifications() { 160 mTimerModel.updateNotification(); 161 mStopwatchModel.updateNotification(); 162 } 163 164 // 165 // Cities 166 // 167 168 /** 169 * @return a list of all cities in their display order 170 */ 171 public List<City> getAllCities() { 172 enforceMainLooper(); 173 return mCityModel.getAllCities(); 174 } 175 176 /** 177 * @param cityName the case-insensitive city name to search for 178 * @return the city with the given {@code cityName}; {@code null} if no such city exists 179 */ 180 public City getCity(String cityName) { 181 enforceMainLooper(); 182 return mCityModel.getCity(cityName); 183 } 184 185 /** 186 * @return a city representing the user's home timezone 187 */ 188 public City getHomeCity() { 189 enforceMainLooper(); 190 return mCityModel.getHomeCity(); 191 } 192 193 /** 194 * @return a list of cities not selected for display 195 */ 196 public List<City> getUnselectedCities() { 197 enforceMainLooper(); 198 return mCityModel.getUnselectedCities(); 199 } 200 201 /** 202 * @return a list of cities selected for display 203 */ 204 public List<City> getSelectedCities() { 205 enforceMainLooper(); 206 return mCityModel.getSelectedCities(); 207 } 208 209 /** 210 * @param cities the new collection of cities selected for display by the user 211 */ 212 public void setSelectedCities(Collection<City> cities) { 213 enforceMainLooper(); 214 mCityModel.setSelectedCities(cities); 215 } 216 217 /** 218 * @return a comparator used to locate index positions 219 */ 220 public Comparator<City> getCityIndexComparator() { 221 enforceMainLooper(); 222 return mCityModel.getCityIndexComparator(); 223 } 224 225 /** 226 * @return the order in which cities are sorted 227 */ 228 public CitySort getCitySort() { 229 enforceMainLooper(); 230 return mCityModel.getCitySort(); 231 } 232 233 /** 234 * Adjust the order in which cities are sorted. 235 */ 236 public void toggleCitySort() { 237 enforceMainLooper(); 238 mCityModel.toggleCitySort(); 239 } 240 241 // 242 // Timers 243 // 244 245 /** 246 * @param timerListener to be notified when timers are added, updated and removed 247 */ 248 public void addTimerListener(TimerListener timerListener) { 249 enforceMainLooper(); 250 mTimerModel.addTimerListener(timerListener); 251 } 252 253 /** 254 * @param timerListener to no longer be notified when timers are added, updated and removed 255 */ 256 public void removeTimerListener(TimerListener timerListener) { 257 enforceMainLooper(); 258 mTimerModel.removeTimerListener(timerListener); 259 } 260 261 /** 262 * @return a list of timers for display 263 */ 264 public List<Timer> getTimers() { 265 enforceMainLooper(); 266 return mTimerModel.getTimers(); 267 } 268 269 /** 270 * @return a list of expired timers for display 271 */ 272 public List<Timer> getExpiredTimers() { 273 enforceMainLooper(); 274 return mTimerModel.getExpiredTimers(); 275 } 276 277 /** 278 * @param timerId identifies the timer to return 279 * @return the timer with the given {@code timerId} 280 */ 281 public Timer getTimer(int timerId) { 282 enforceMainLooper(); 283 return mTimerModel.getTimer(timerId); 284 } 285 286 /** 287 * @return the timer that last expired and is still expired now; {@code null} if no timers are 288 * expired 289 */ 290 public Timer getMostRecentExpiredTimer() { 291 enforceMainLooper(); 292 return mTimerModel.getMostRecentExpiredTimer(); 293 } 294 295 /** 296 * @param length the length of the timer in milliseconds 297 * @param label describes the purpose of the timer 298 * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset 299 * @return the newly added timer 300 */ 301 public Timer addTimer(long length, String label, boolean deleteAfterUse) { 302 enforceMainLooper(); 303 return mTimerModel.addTimer(length, label, deleteAfterUse); 304 } 305 306 /** 307 * @param timer the timer to be removed 308 */ 309 public void removeTimer(Timer timer) { 310 enforceMainLooper(); 311 mTimerModel.removeTimer(timer); 312 } 313 314 /** 315 * @param timer the timer to be started 316 */ 317 public void startTimer(Timer timer) { 318 enforceMainLooper(); 319 mTimerModel.updateTimer(timer.start()); 320 } 321 322 /** 323 * @param timer the timer to be paused 324 */ 325 public void pauseTimer(Timer timer) { 326 enforceMainLooper(); 327 mTimerModel.updateTimer(timer.pause()); 328 } 329 330 /** 331 * @param service used to start foreground notifications for expired timers 332 * @param timer the timer to be expired 333 */ 334 public void expireTimer(Service service, Timer timer) { 335 enforceMainLooper(); 336 mTimerModel.expireTimer(service, timer); 337 } 338 339 /** 340 * If the given {@code timer} is expired and marked for deletion after use then this method 341 * removes the the timer. The timer is otherwise transitioned to the reset state and continues 342 * to exist. 343 * 344 * @param timer the timer to be reset 345 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 346 */ 347 public void resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) { 348 enforceMainLooper(); 349 mTimerModel.resetOrDeleteTimer(timer, eventLabelId); 350 } 351 352 /** 353 * Resets all timers. 354 * 355 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 356 */ 357 public void resetTimers(@StringRes int eventLabelId) { 358 enforceMainLooper(); 359 mTimerModel.resetTimers(eventLabelId); 360 } 361 362 /** 363 * Resets all expired timers. 364 * 365 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 366 */ 367 public void resetExpiredTimers(@StringRes int eventLabelId) { 368 enforceMainLooper(); 369 mTimerModel.resetExpiredTimers(eventLabelId); 370 } 371 372 /** 373 * Resets all unexpired timers. 374 * 375 * @param eventLabelId the label of the timer event to send; 0 if no event should be sent 376 */ 377 public void resetUnexpiredTimers(@StringRes int eventLabelId) { 378 enforceMainLooper(); 379 mTimerModel.resetUnexpiredTimers(eventLabelId); 380 } 381 382 /** 383 * @param timer the timer to which a minute should be added to the remaining time 384 */ 385 public void addTimerMinute(Timer timer) { 386 enforceMainLooper(); 387 mTimerModel.updateTimer(timer.addMinute()); 388 } 389 390 /** 391 * @param timer the timer to which the new {@code label} belongs 392 * @param label the new label to store for the {@code timer} 393 */ 394 public void setTimerLabel(Timer timer, String label) { 395 enforceMainLooper(); 396 mTimerModel.updateTimer(timer.setLabel(label)); 397 } 398 399 /** 400 * Updates the timer notifications to be current. 401 */ 402 public void updateTimerNotification() { 403 enforceMainLooper(); 404 mTimerModel.updateNotification(); 405 } 406 407 /** 408 * @return the uri of the default ringtone to play for all timers when no user selection exists 409 */ 410 public Uri getDefaultTimerRingtoneUri() { 411 enforceMainLooper(); 412 return mTimerModel.getDefaultTimerRingtoneUri(); 413 } 414 415 /** 416 * @return {@code true} iff the ringtone to play for all timers is the silent ringtone 417 */ 418 public boolean isTimerRingtoneSilent() { 419 enforceMainLooper(); 420 return mTimerModel.isTimerRingtoneSilent(); 421 } 422 423 /** 424 * @return the uri of the ringtone to play for all timers 425 */ 426 public Uri getTimerRingtoneUri() { 427 enforceMainLooper(); 428 return mTimerModel.getTimerRingtoneUri(); 429 } 430 431 /** 432 * @return the title of the ringtone that is played for all timers 433 */ 434 public String getTimerRingtoneTitle() { 435 enforceMainLooper(); 436 return mTimerModel.getTimerRingtoneTitle(); 437 } 438 439 // 440 // Alarms 441 // 442 443 /** 444 * @return the uri of the ringtone to which all new alarms default 445 */ 446 public Uri getDefaultAlarmRingtoneUri() { 447 enforceMainLooper(); 448 return mAlarmModel.getDefaultAlarmRingtoneUri(); 449 } 450 451 /** 452 * @param uri the uri of the ringtone to which future new alarms will default 453 */ 454 public void setDefaultAlarmRingtoneUri(Uri uri) { 455 enforceMainLooper(); 456 mAlarmModel.setDefaultAlarmRingtoneUri(uri); 457 } 458 459 /** 460 * @param uri the uri of a ringtone 461 * @return the title of the ringtone with the {@code uri}; {@code null} if it cannot be fetched 462 */ 463 public String getAlarmRingtoneTitle(Uri uri) { 464 enforceMainLooper(); 465 return mAlarmModel.getAlarmRingtoneTitle(uri); 466 } 467 468 // 469 // Stopwatch 470 // 471 472 /** 473 * @return the current state of the stopwatch 474 */ 475 public Stopwatch getStopwatch() { 476 enforceMainLooper(); 477 return mStopwatchModel.getStopwatch(); 478 } 479 480 /** 481 * @return the stopwatch after being started 482 */ 483 public Stopwatch startStopwatch() { 484 enforceMainLooper(); 485 return mStopwatchModel.setStopwatch(getStopwatch().start()); 486 } 487 488 /** 489 * @return the stopwatch after being paused 490 */ 491 public Stopwatch pauseStopwatch() { 492 enforceMainLooper(); 493 return mStopwatchModel.setStopwatch(getStopwatch().pause()); 494 } 495 496 /** 497 * @return the stopwatch after being reset 498 */ 499 public Stopwatch resetStopwatch() { 500 enforceMainLooper(); 501 return mStopwatchModel.setStopwatch(getStopwatch().reset()); 502 } 503 504 /** 505 * @return the laps recorded for this stopwatch 506 */ 507 public List<Lap> getLaps() { 508 enforceMainLooper(); 509 return mStopwatchModel.getLaps(); 510 } 511 512 /** 513 * @return a newly recorded lap completed now; {@code null} if no more laps can be added 514 */ 515 public Lap addLap() { 516 enforceMainLooper(); 517 return mStopwatchModel.addLap(); 518 } 519 520 /** 521 * Clears the laps recorded for this stopwatch. 522 */ 523 public void clearLaps() { 524 enforceMainLooper(); 525 mStopwatchModel.clearLaps(); 526 } 527 528 /** 529 * @return {@code true} iff more laps can be recorded 530 */ 531 public boolean canAddMoreLaps() { 532 enforceMainLooper(); 533 return mStopwatchModel.canAddMoreLaps(); 534 } 535 536 /** 537 * @return the longest lap time of all recorded laps and the current lap 538 */ 539 public long getLongestLapTime() { 540 enforceMainLooper(); 541 return mStopwatchModel.getLongestLapTime(); 542 } 543 544 /** 545 * @param time a point in time after the end of the last lap 546 * @return the elapsed time between the given {@code time} and the end of the previous lap 547 */ 548 public long getCurrentLapTime(long time) { 549 enforceMainLooper(); 550 return mStopwatchModel.getCurrentLapTime(time); 551 } 552 553 // 554 // Settings 555 // 556 557 /** 558 * @return the style of clock to display in the clock application 559 */ 560 public ClockStyle getClockStyle() { 561 enforceMainLooper(); 562 return mSettingsModel.getClockStyle(); 563 } 564 565 /** 566 * @return the style of clock to display in the clock screensaver 567 */ 568 public ClockStyle getScreensaverClockStyle() { 569 enforceMainLooper(); 570 return mSettingsModel.getScreensaverClockStyle(); 571 } 572 573 /** 574 * @return {@code true} if the users wants to automatically show a clock for their home timezone 575 * when they have travelled outside of that timezone 576 */ 577 public boolean getShowHomeClock() { 578 enforceMainLooper(); 579 return mSettingsModel.getShowHomeClock(); 580 } 581 582 /** 583 * Used to execute a delegate runnable and track its completion. 584 */ 585 private static class ExecutedRunnable implements Runnable { 586 587 private final Runnable mDelegate; 588 private boolean mExecuted; 589 590 private ExecutedRunnable(Runnable delegate) { 591 this.mDelegate = delegate; 592 } 593 594 @Override 595 public void run() { 596 mDelegate.run(); 597 598 synchronized (this) { 599 mExecuted = true; 600 notifyAll(); 601 } 602 } 603 604 private boolean isExecuted() { 605 return mExecuted; 606 } 607 } 608 }