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 
getDataModel()71     public static DataModel getDataModel() {
72         return sDataModel;
73     }
74 
DataModel()75     private DataModel() {}
76 
77     /**
78      * The context may be set precisely once during the application life.
79      */
setContext(Context context)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      */
run(Runnable runnable)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      */
getHandler()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      */
setApplicationInForeground(boolean inForeground)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      */
isApplicationInForeground()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      */
updateAllNotifications()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      */
getAllCities()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      */
getCity(String cityName)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      */
getHomeCity()188     public City getHomeCity() {
189         enforceMainLooper();
190         return mCityModel.getHomeCity();
191     }
192 
193     /**
194      * @return a list of cities not selected for display
195      */
getUnselectedCities()196     public List<City> getUnselectedCities() {
197         enforceMainLooper();
198         return mCityModel.getUnselectedCities();
199     }
200 
201     /**
202      * @return a list of cities selected for display
203      */
getSelectedCities()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      */
setSelectedCities(Collection<City> cities)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      */
getCityIndexComparator()220     public Comparator<City> getCityIndexComparator() {
221         enforceMainLooper();
222         return mCityModel.getCityIndexComparator();
223     }
224 
225     /**
226      * @return the order in which cities are sorted
227      */
getCitySort()228     public CitySort getCitySort() {
229         enforceMainLooper();
230         return mCityModel.getCitySort();
231     }
232 
233     /**
234      * Adjust the order in which cities are sorted.
235      */
toggleCitySort()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      */
addTimerListener(TimerListener timerListener)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      */
removeTimerListener(TimerListener timerListener)256     public void removeTimerListener(TimerListener timerListener) {
257         enforceMainLooper();
258         mTimerModel.removeTimerListener(timerListener);
259     }
260 
261     /**
262      * @return a list of timers for display
263      */
getTimers()264     public List<Timer> getTimers() {
265         enforceMainLooper();
266         return mTimerModel.getTimers();
267     }
268 
269     /**
270      * @return a list of expired timers for display
271      */
getExpiredTimers()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      */
getTimer(int timerId)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      */
getMostRecentExpiredTimer()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      */
addTimer(long length, String label, boolean deleteAfterUse)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      */
removeTimer(Timer timer)309     public void removeTimer(Timer timer) {
310         enforceMainLooper();
311         mTimerModel.removeTimer(timer);
312     }
313 
314     /**
315      * @param timer the timer to be started
316      */
startTimer(Timer timer)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      */
pauseTimer(Timer timer)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      */
expireTimer(Service service, Timer timer)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      */
resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId)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      */
resetTimers(@tringRes int eventLabelId)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      */
resetExpiredTimers(@tringRes int eventLabelId)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      */
resetUnexpiredTimers(@tringRes int eventLabelId)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      */
addTimerMinute(Timer timer)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      */
setTimerLabel(Timer timer, String label)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      */
updateTimerNotification()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      */
getDefaultTimerRingtoneUri()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      */
isTimerRingtoneSilent()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      */
getTimerRingtoneUri()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      */
getTimerRingtoneTitle()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      */
getDefaultAlarmRingtoneUri()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      */
setDefaultAlarmRingtoneUri(Uri uri)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      */
getAlarmRingtoneTitle(Uri uri)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      */
getStopwatch()475     public Stopwatch getStopwatch() {
476         enforceMainLooper();
477         return mStopwatchModel.getStopwatch();
478     }
479 
480     /**
481      * @return the stopwatch after being started
482      */
startStopwatch()483     public Stopwatch startStopwatch() {
484         enforceMainLooper();
485         return mStopwatchModel.setStopwatch(getStopwatch().start());
486     }
487 
488     /**
489      * @return the stopwatch after being paused
490      */
pauseStopwatch()491     public Stopwatch pauseStopwatch() {
492         enforceMainLooper();
493         return mStopwatchModel.setStopwatch(getStopwatch().pause());
494     }
495 
496     /**
497      * @return the stopwatch after being reset
498      */
resetStopwatch()499     public Stopwatch resetStopwatch() {
500         enforceMainLooper();
501         return mStopwatchModel.setStopwatch(getStopwatch().reset());
502     }
503 
504     /**
505      * @return the laps recorded for this stopwatch
506      */
getLaps()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      */
addLap()515     public Lap addLap() {
516         enforceMainLooper();
517         return mStopwatchModel.addLap();
518     }
519 
520     /**
521      * Clears the laps recorded for this stopwatch.
522      */
clearLaps()523     public void clearLaps() {
524         enforceMainLooper();
525         mStopwatchModel.clearLaps();
526     }
527 
528     /**
529      * @return {@code true} iff more laps can be recorded
530      */
canAddMoreLaps()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      */
getLongestLapTime()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      */
getCurrentLapTime(long time)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      */
getClockStyle()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      */
getScreensaverClockStyle()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      */
getShowHomeClock()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 
ExecutedRunnable(Runnable delegate)590         private ExecutedRunnable(Runnable delegate) {
591             this.mDelegate = delegate;
592         }
593 
594         @Override
run()595         public void run() {
596             mDelegate.run();
597 
598             synchronized (this) {
599                 mExecuted = true;
600                 notifyAll();
601             }
602         }
603 
isExecuted()604         private boolean isExecuted() {
605             return mExecuted;
606         }
607     }
608 }