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.content.Intent;
22 import android.content.SharedPreferences;
23 import android.media.AudioManager;
24 import android.net.Uri;
25 import android.os.Handler;
26 import android.os.Looper;
27 import androidx.annotation.StringRes;
28 import android.view.View;
29 
30 import com.android.deskclock.Predicate;
31 import com.android.deskclock.R;
32 import com.android.deskclock.Utils;
33 import com.android.deskclock.timer.TimerService;
34 
35 import java.util.Calendar;
36 import java.util.Collection;
37 import java.util.Comparator;
38 import java.util.List;
39 
40 import static android.content.Context.AUDIO_SERVICE;
41 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
42 import static android.media.AudioManager.FLAG_SHOW_UI;
43 import static android.media.AudioManager.STREAM_ALARM;
44 import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
45 import static android.provider.Settings.ACTION_SOUND_SETTINGS;
46 import static com.android.deskclock.Utils.enforceMainLooper;
47 import static com.android.deskclock.Utils.enforceNotMainLooper;
48 
49 /**
50  * All application-wide data is accessible through this singleton.
51  */
52 public final class DataModel {
53 
54     /** Indicates the display style of clocks. */
55     public enum ClockStyle {ANALOG, DIGITAL}
56 
57     /** Indicates the preferred sort order of cities. */
58     public enum CitySort {NAME, UTC_OFFSET}
59 
60     /** Indicates the preferred behavior of hardware volume buttons when firing alarms. */
61     public enum AlarmVolumeButtonBehavior {NOTHING, SNOOZE, DISMISS}
62 
63     /** Indicates the reason alarms may not fire or may fire silently. */
64     public enum SilentSetting {
65         @SuppressWarnings("unchecked")
66         DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd, 0, Predicate.FALSE, null),
67         @SuppressWarnings("unchecked")
68         MUTED_VOLUME(R.string.alarm_volume_muted,
69                 R.string.unmute_alarm_volume,
70                 Predicate.TRUE,
71                 new UnmuteAlarmVolumeListener()),
72         SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
73                 R.string.change_setting_action,
74                 new ChangeSoundActionPredicate(),
75                 new ChangeSoundSettingsListener()),
76         @SuppressWarnings("unchecked")
77         BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
78                 R.string.change_setting_action,
79                 Predicate.TRUE,
80                 new ChangeAppNotificationSettingsListener());
81 
82         private final @StringRes int mLabelResId;
83         private final @StringRes int mActionResId;
84         private final Predicate<Context> mActionEnabled;
85         private final View.OnClickListener mActionListener;
86 
SilentSetting(int labelResId, int actionResId, Predicate<Context> actionEnabled, View.OnClickListener actionListener)87         SilentSetting(int labelResId, int actionResId, Predicate<Context> actionEnabled,
88                 View.OnClickListener actionListener) {
89             mLabelResId = labelResId;
90             mActionResId = actionResId;
91             mActionEnabled = actionEnabled;
92             mActionListener = actionListener;
93         }
94 
getLabelResId()95         public @StringRes int getLabelResId() { return mLabelResId; }
getActionResId()96         public @StringRes int getActionResId() { return mActionResId; }
getActionListener()97         public View.OnClickListener getActionListener() { return mActionListener; }
isActionEnabled(Context context)98         public boolean isActionEnabled(Context context) {
99             return mLabelResId != 0 && mActionEnabled.apply(context);
100         }
101 
102         private static class UnmuteAlarmVolumeListener implements View.OnClickListener {
103             @Override
onClick(View v)104             public void onClick(View v) {
105                 // Set the alarm volume to 11/16th of max and show the slider UI.
106                 // 11/16th of max is the initial volume of the alarm stream on a fresh install.
107                 final Context context = v.getContext();
108                 final AudioManager am = (AudioManager) context.getSystemService(AUDIO_SERVICE);
109                 final int index = Math.round(am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f);
110                 am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI);
111             }
112         }
113 
114         private static class ChangeSoundSettingsListener implements View.OnClickListener {
115             @Override
onClick(View v)116             public void onClick(View v) {
117                 final Context context = v.getContext();
118                 context.startActivity(new Intent(ACTION_SOUND_SETTINGS)
119                         .addFlags(FLAG_ACTIVITY_NEW_TASK));
120             }
121         }
122 
123         private static class ChangeSoundActionPredicate implements Predicate<Context> {
124             @Override
apply(Context context)125             public boolean apply(Context context) {
126                 final Intent intent = new Intent(ACTION_SOUND_SETTINGS);
127                 return intent.resolveActivity(context.getPackageManager()) != null;
128             }
129         }
130 
131         private static class ChangeAppNotificationSettingsListener implements View.OnClickListener {
132             @Override
onClick(View v)133             public void onClick(View v) {
134                 final Context context = v.getContext();
135                 if (Utils.isLOrLater()) {
136                     try {
137                         // Attempt to open the notification settings for this app.
138                         context.startActivity(
139                                 new Intent("android.settings.APP_NOTIFICATION_SETTINGS")
140                                 .putExtra("app_package", context.getPackageName())
141                                 .putExtra("app_uid", context.getApplicationInfo().uid)
142                                 .addFlags(FLAG_ACTIVITY_NEW_TASK));
143                         return;
144                     } catch (Exception ignored) {
145                         // best attempt only; recovery code below
146                     }
147                 }
148 
149                 // Fall back to opening the app settings page.
150                 context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
151                         .setData(Uri.fromParts("package", context.getPackageName(), null))
152                         .addFlags(FLAG_ACTIVITY_NEW_TASK));
153             }
154         }
155     }
156 
157     public static final String ACTION_WORLD_CITIES_CHANGED =
158             "com.android.deskclock.WORLD_CITIES_CHANGED";
159 
160     /** The single instance of this data model that exists for the life of the application. */
161     private static final DataModel sDataModel = new DataModel();
162 
163     private Handler mHandler;
164 
165     private Context mContext;
166 
167     /** The model from which settings are fetched. */
168     private SettingsModel mSettingsModel;
169 
170     /** The model from which city data are fetched. */
171     private CityModel mCityModel;
172 
173     /** The model from which timer data are fetched. */
174     private TimerModel mTimerModel;
175 
176     /** The model from which alarm data are fetched. */
177     private AlarmModel mAlarmModel;
178 
179     /** The model from which widget data are fetched. */
180     private WidgetModel mWidgetModel;
181 
182     /** The model from which data about settings that silence alarms are fetched. */
183     private SilentSettingsModel mSilentSettingsModel;
184 
185     /** The model from which stopwatch data are fetched. */
186     private StopwatchModel mStopwatchModel;
187 
188     /** The model from which notification data are fetched. */
189     private NotificationModel mNotificationModel;
190 
191     /** The model from which time data are fetched. */
192     private TimeModel mTimeModel;
193 
194     /** The model from which ringtone data are fetched. */
195     private RingtoneModel mRingtoneModel;
196 
getDataModel()197     public static DataModel getDataModel() {
198         return sDataModel;
199     }
200 
DataModel()201     private DataModel() {}
202 
203     /**
204      * Initializes the data model with the context and shared preferences to be used.
205      */
init(Context context, SharedPreferences prefs)206     public void init(Context context, SharedPreferences prefs) {
207         if (mContext != context) {
208             mContext = context.getApplicationContext();
209 
210             mTimeModel = new TimeModel(mContext);
211             mWidgetModel = new WidgetModel(prefs);
212             mNotificationModel = new NotificationModel();
213             mRingtoneModel = new RingtoneModel(mContext, prefs);
214             mSettingsModel = new SettingsModel(mContext, prefs, mTimeModel);
215             mCityModel = new CityModel(mContext, prefs, mSettingsModel);
216             mAlarmModel = new AlarmModel(mContext, mSettingsModel);
217             mSilentSettingsModel = new SilentSettingsModel(mContext, mNotificationModel);
218             mStopwatchModel = new StopwatchModel(mContext, prefs, mNotificationModel);
219             mTimerModel = new TimerModel(mContext, prefs, mSettingsModel, mRingtoneModel,
220                     mNotificationModel);
221         }
222     }
223 
224     /**
225      * Convenience for {@code run(runnable, 0)}, i.e. waits indefinitely.
226      */
run(Runnable runnable)227     public void run(Runnable runnable) {
228         try {
229             run(runnable, 0 /* waitMillis */);
230         } catch (InterruptedException ignored) {
231         }
232     }
233 
234     /**
235      * Updates all timers and the stopwatch after the device has shutdown and restarted.
236      */
updateAfterReboot()237     public void updateAfterReboot() {
238         enforceMainLooper();
239         mTimerModel.updateTimersAfterReboot();
240         mStopwatchModel.setStopwatch(getStopwatch().updateAfterReboot());
241     }
242 
243     /**
244      * Updates all timers and the stopwatch after the device's time has changed.
245      */
updateAfterTimeSet()246     public void updateAfterTimeSet() {
247         enforceMainLooper();
248         mTimerModel.updateTimersAfterTimeSet();
249         mStopwatchModel.setStopwatch(getStopwatch().updateAfterTimeSet());
250     }
251 
252     /**
253      * Posts a runnable to the main thread and blocks until the runnable executes. Used to access
254      * the data model from the main thread.
255      */
run(Runnable runnable, long waitMillis)256     public void run(Runnable runnable, long waitMillis) throws InterruptedException {
257         if (Looper.myLooper() == Looper.getMainLooper()) {
258             runnable.run();
259             return;
260         }
261 
262         final ExecutedRunnable er = new ExecutedRunnable(runnable);
263         getHandler().post(er);
264 
265         // Wait for the data to arrive, if it has not.
266         synchronized (er) {
267             if (!er.isExecuted()) {
268                 er.wait(waitMillis);
269             }
270         }
271     }
272 
273     /**
274      * @return a handler associated with the main thread
275      */
getHandler()276     private synchronized Handler getHandler() {
277         if (mHandler == null) {
278             mHandler = new Handler(Looper.getMainLooper());
279         }
280         return mHandler;
281     }
282 
283     //
284     // Application
285     //
286 
287     /**
288      * @param inForeground {@code true} to indicate the application is open in the foreground
289      */
setApplicationInForeground(boolean inForeground)290     public void setApplicationInForeground(boolean inForeground) {
291         enforceMainLooper();
292 
293         if (mNotificationModel.isApplicationInForeground() != inForeground) {
294             mNotificationModel.setApplicationInForeground(inForeground);
295 
296             // Refresh all notifications in response to a change in app open state.
297             mTimerModel.updateNotification();
298             mTimerModel.updateMissedNotification();
299             mStopwatchModel.updateNotification();
300             mSilentSettingsModel.updateSilentState();
301         }
302     }
303 
304     /**
305      * @return {@code true} when the application is open in the foreground; {@code false} otherwise
306      */
isApplicationInForeground()307     public boolean isApplicationInForeground() {
308         enforceMainLooper();
309         return mNotificationModel.isApplicationInForeground();
310     }
311 
312     /**
313      * Called when the notifications may be stale or absent from the notification manager and must
314      * be rebuilt. e.g. after upgrading the application
315      */
updateAllNotifications()316     public void updateAllNotifications() {
317         enforceMainLooper();
318         mTimerModel.updateNotification();
319         mTimerModel.updateMissedNotification();
320         mStopwatchModel.updateNotification();
321     }
322 
323     //
324     // Cities
325     //
326 
327     /**
328      * @return a list of all cities in their display order
329      */
getAllCities()330     public List<City> getAllCities() {
331         enforceMainLooper();
332         return mCityModel.getAllCities();
333     }
334 
335     /**
336      * @return a city representing the user's home timezone
337      */
getHomeCity()338     public City getHomeCity() {
339         enforceMainLooper();
340         return mCityModel.getHomeCity();
341     }
342 
343     /**
344      * @return a list of cities not selected for display
345      */
getUnselectedCities()346     public List<City> getUnselectedCities() {
347         enforceMainLooper();
348         return mCityModel.getUnselectedCities();
349     }
350 
351     /**
352      * @return a list of cities selected for display
353      */
getSelectedCities()354     public List<City> getSelectedCities() {
355         enforceMainLooper();
356         return mCityModel.getSelectedCities();
357     }
358 
359     /**
360      * @param cities the new collection of cities selected for display by the user
361      */
setSelectedCities(Collection<City> cities)362     public void setSelectedCities(Collection<City> cities) {
363         enforceMainLooper();
364         mCityModel.setSelectedCities(cities);
365     }
366 
367     /**
368      * @return a comparator used to locate index positions
369      */
getCityIndexComparator()370     public Comparator<City> getCityIndexComparator() {
371         enforceMainLooper();
372         return mCityModel.getCityIndexComparator();
373     }
374 
375     /**
376      * @return the order in which cities are sorted
377      */
getCitySort()378     public CitySort getCitySort() {
379         enforceMainLooper();
380         return mCityModel.getCitySort();
381     }
382 
383     /**
384      * Adjust the order in which cities are sorted.
385      */
toggleCitySort()386     public void toggleCitySort() {
387         enforceMainLooper();
388         mCityModel.toggleCitySort();
389     }
390 
391     /**
392      * @param cityListener listener to be notified when the world city list changes
393      */
addCityListener(CityListener cityListener)394     public void addCityListener(CityListener cityListener) {
395         enforceMainLooper();
396         mCityModel.addCityListener(cityListener);
397     }
398 
399     /**
400      * @param cityListener listener that no longer needs to be notified of world city list changes
401      */
removeCityListener(CityListener cityListener)402     public void removeCityListener(CityListener cityListener) {
403         enforceMainLooper();
404         mCityModel.removeCityListener(cityListener);
405     }
406 
407     //
408     // Timers
409     //
410 
411     /**
412      * @param timerListener to be notified when timers are added, updated and removed
413      */
addTimerListener(TimerListener timerListener)414     public void addTimerListener(TimerListener timerListener) {
415         enforceMainLooper();
416         mTimerModel.addTimerListener(timerListener);
417     }
418 
419     /**
420      * @param timerListener to no longer be notified when timers are added, updated and removed
421      */
removeTimerListener(TimerListener timerListener)422     public void removeTimerListener(TimerListener timerListener) {
423         enforceMainLooper();
424         mTimerModel.removeTimerListener(timerListener);
425     }
426 
427     /**
428      * @return a list of timers for display
429      */
getTimers()430     public List<Timer> getTimers() {
431         enforceMainLooper();
432         return mTimerModel.getTimers();
433     }
434 
435     /**
436      * @return a list of expired timers for display
437      */
getExpiredTimers()438     public List<Timer> getExpiredTimers() {
439         enforceMainLooper();
440         return mTimerModel.getExpiredTimers();
441     }
442 
443     /**
444      * @param timerId identifies the timer to return
445      * @return the timer with the given {@code timerId}
446      */
getTimer(int timerId)447     public Timer getTimer(int timerId) {
448         enforceMainLooper();
449         return mTimerModel.getTimer(timerId);
450     }
451 
452     /**
453      * @return the timer that last expired and is still expired now; {@code null} if no timers are
454      *      expired
455      */
getMostRecentExpiredTimer()456     public Timer getMostRecentExpiredTimer() {
457         enforceMainLooper();
458         return mTimerModel.getMostRecentExpiredTimer();
459     }
460 
461     /**
462      * @param length the length of the timer in milliseconds
463      * @param label describes the purpose of the timer
464      * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
465      * @return the newly added timer
466      */
addTimer(long length, String label, boolean deleteAfterUse)467     public Timer addTimer(long length, String label, boolean deleteAfterUse) {
468         enforceMainLooper();
469         return mTimerModel.addTimer(length, label, deleteAfterUse);
470     }
471 
472     /**
473      * @param timer the timer to be removed
474      */
removeTimer(Timer timer)475     public void removeTimer(Timer timer) {
476         enforceMainLooper();
477         mTimerModel.removeTimer(timer);
478     }
479 
480     /**
481      * @param timer the timer to be started
482      */
startTimer(Timer timer)483     public void startTimer(Timer timer) {
484         startTimer(null, timer);
485     }
486 
487     /**
488      * @param service used to start foreground notifications for expired timers
489      * @param timer the timer to be started
490      */
startTimer(Service service, Timer timer)491     public void startTimer(Service service, Timer timer) {
492         enforceMainLooper();
493         final Timer started = timer.start();
494         mTimerModel.updateTimer(started);
495         if (timer.getRemainingTime() <= 0) {
496             if (service != null) {
497                 expireTimer(service, started);
498             } else {
499                 mContext.startService(TimerService.createTimerExpiredIntent(mContext, started));
500             }
501         }
502     }
503 
504     /**
505      * @param timer the timer to be paused
506      */
pauseTimer(Timer timer)507     public void pauseTimer(Timer timer) {
508         enforceMainLooper();
509         mTimerModel.updateTimer(timer.pause());
510     }
511 
512     /**
513      * @param service used to start foreground notifications for expired timers
514      * @param timer the timer to be expired
515      */
expireTimer(Service service, Timer timer)516     public void expireTimer(Service service, Timer timer) {
517         enforceMainLooper();
518         mTimerModel.expireTimer(service, timer);
519     }
520 
521     /**
522      * @param timer the timer to be reset
523      * @return the reset {@code timer}
524      */
resetTimer(Timer timer)525     public Timer resetTimer(Timer timer) {
526         enforceMainLooper();
527         return mTimerModel.resetTimer(timer, false /* allowDelete */, 0 /* eventLabelId */);
528     }
529 
530     /**
531      * If the given {@code timer} is expired and marked for deletion after use then this method
532      * removes the the timer. The timer is otherwise transitioned to the reset state and continues
533      * to exist.
534      *
535      * @param timer the timer to be reset
536      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
537      * @return the reset {@code timer} or {@code null} if the timer was deleted
538      */
resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId)539     public Timer resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
540         enforceMainLooper();
541         return mTimerModel.resetTimer(timer, true /* allowDelete */, eventLabelId);
542     }
543 
544     /**
545      * Resets all expired timers.
546      *
547      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
548      */
resetOrDeleteExpiredTimers(@tringRes int eventLabelId)549     public void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) {
550         enforceMainLooper();
551         mTimerModel.resetOrDeleteExpiredTimers(eventLabelId);
552     }
553 
554     /**
555      * Resets all unexpired timers.
556      *
557      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
558      */
resetUnexpiredTimers(@tringRes int eventLabelId)559     public void resetUnexpiredTimers(@StringRes int eventLabelId) {
560         enforceMainLooper();
561         mTimerModel.resetUnexpiredTimers(eventLabelId);
562     }
563 
564     /**
565      * Resets all missed timers.
566      *
567      * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
568      */
resetMissedTimers(@tringRes int eventLabelId)569     public void resetMissedTimers(@StringRes int eventLabelId) {
570         enforceMainLooper();
571         mTimerModel.resetMissedTimers(eventLabelId);
572     }
573 
574     /**
575      * @param timer the timer to which a minute should be added to the remaining time
576      */
addTimerMinute(Timer timer)577     public void addTimerMinute(Timer timer) {
578         enforceMainLooper();
579         mTimerModel.updateTimer(timer.addMinute());
580     }
581 
582     /**
583      * @param timer the timer to which the new {@code label} belongs
584      * @param label the new label to store for the {@code timer}
585      */
setTimerLabel(Timer timer, String label)586     public void setTimerLabel(Timer timer, String label) {
587         enforceMainLooper();
588         mTimerModel.updateTimer(timer.setLabel(label));
589     }
590 
591     /**
592      * @param timer the timer whose {@code length} to change
593      * @param length the new length of the timer in milliseconds
594      */
setTimerLength(Timer timer, long length)595     public void setTimerLength(Timer timer, long length) {
596         enforceMainLooper();
597         mTimerModel.updateTimer(timer.setLength(length));
598     }
599 
600     /**
601      * @param timer the timer whose {@code remainingTime} to change
602      * @param remainingTime the new remaining time of the timer in milliseconds
603      */
setRemainingTime(Timer timer, long remainingTime)604     public void setRemainingTime(Timer timer, long remainingTime) {
605         enforceMainLooper();
606 
607         final Timer updated = timer.setRemainingTime(remainingTime);
608         mTimerModel.updateTimer(updated);
609         if (timer.isRunning() && timer.getRemainingTime() <= 0) {
610             mContext.startService(TimerService.createTimerExpiredIntent(mContext, updated));
611         }
612     }
613 
614     /**
615      * Updates the timer notifications to be current.
616      */
updateTimerNotification()617     public void updateTimerNotification() {
618         enforceMainLooper();
619         mTimerModel.updateNotification();
620     }
621 
622     /**
623      * @return the uri of the default ringtone to play for all timers when no user selection exists
624      */
getDefaultTimerRingtoneUri()625     public Uri getDefaultTimerRingtoneUri() {
626         enforceMainLooper();
627         return mTimerModel.getDefaultTimerRingtoneUri();
628     }
629 
630     /**
631      * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
632      */
isTimerRingtoneSilent()633     public boolean isTimerRingtoneSilent() {
634         enforceMainLooper();
635         return mTimerModel.isTimerRingtoneSilent();
636     }
637 
638     /**
639      * @return the uri of the ringtone to play for all timers
640      */
getTimerRingtoneUri()641     public Uri getTimerRingtoneUri() {
642         enforceMainLooper();
643         return mTimerModel.getTimerRingtoneUri();
644     }
645 
646     /**
647      * @param uri the uri of the ringtone to play for all timers
648      */
setTimerRingtoneUri(Uri uri)649     public void setTimerRingtoneUri(Uri uri) {
650         enforceMainLooper();
651         mTimerModel.setTimerRingtoneUri(uri);
652     }
653 
654     /**
655      * @return the title of the ringtone that is played for all timers
656      */
getTimerRingtoneTitle()657     public String getTimerRingtoneTitle() {
658         enforceMainLooper();
659         return mTimerModel.getTimerRingtoneTitle();
660     }
661 
662     /**
663      * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
664      *      {@code 0} implies no crescendo should be applied
665      */
getTimerCrescendoDuration()666     public long getTimerCrescendoDuration() {
667         enforceMainLooper();
668         return mTimerModel.getTimerCrescendoDuration();
669     }
670 
671     /**
672      * @return whether vibrate is enabled for all timers.
673      */
getTimerVibrate()674     public boolean getTimerVibrate() {
675         enforceMainLooper();
676         return mTimerModel.getTimerVibrate();
677     }
678 
679     /**
680      * @param enabled whether vibrate is enabled for all timers.
681      */
setTimerVibrate(boolean enabled)682     public void setTimerVibrate(boolean enabled) {
683         enforceMainLooper();
684         mTimerModel.setTimerVibrate(enabled);
685     }
686 
687     //
688     // Alarms
689     //
690 
691     /**
692      * @return the uri of the ringtone to which all new alarms default
693      */
getDefaultAlarmRingtoneUri()694     public Uri getDefaultAlarmRingtoneUri() {
695         enforceMainLooper();
696         return mAlarmModel.getDefaultAlarmRingtoneUri();
697     }
698 
699     /**
700      * @param uri the uri of the ringtone to which future new alarms will default
701      */
setDefaultAlarmRingtoneUri(Uri uri)702     public void setDefaultAlarmRingtoneUri(Uri uri) {
703         enforceMainLooper();
704         mAlarmModel.setDefaultAlarmRingtoneUri(uri);
705     }
706 
707     /**
708      * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
709      *      {@code 0} implies no crescendo should be applied
710      */
getAlarmCrescendoDuration()711     public long getAlarmCrescendoDuration() {
712         enforceMainLooper();
713         return mAlarmModel.getAlarmCrescendoDuration();
714     }
715 
716     /**
717      * @return the behavior to execute when volume buttons are pressed while firing an alarm
718      */
getAlarmVolumeButtonBehavior()719     public AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
720         enforceMainLooper();
721         return mAlarmModel.getAlarmVolumeButtonBehavior();
722     }
723 
724     /**
725      * @return the number of minutes an alarm may ring before it has timed out and becomes missed
726      */
getAlarmTimeout()727     public int getAlarmTimeout() {
728         return mAlarmModel.getAlarmTimeout();
729     }
730 
731     /**
732      * @return the number of minutes an alarm will remain snoozed before it rings again
733      */
getSnoozeLength()734     public int getSnoozeLength() {
735         return mAlarmModel.getSnoozeLength();
736     }
737 
738     //
739     // Stopwatch
740     //
741 
742     /**
743      * @param stopwatchListener to be notified when stopwatch changes or laps are added
744      */
addStopwatchListener(StopwatchListener stopwatchListener)745     public void addStopwatchListener(StopwatchListener stopwatchListener) {
746         enforceMainLooper();
747         mStopwatchModel.addStopwatchListener(stopwatchListener);
748     }
749 
750     /**
751      * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
752      */
removeStopwatchListener(StopwatchListener stopwatchListener)753     public void removeStopwatchListener(StopwatchListener stopwatchListener) {
754         enforceMainLooper();
755         mStopwatchModel.removeStopwatchListener(stopwatchListener);
756     }
757 
758     /**
759      * @return the current state of the stopwatch
760      */
getStopwatch()761     public Stopwatch getStopwatch() {
762         enforceMainLooper();
763         return mStopwatchModel.getStopwatch();
764     }
765 
766     /**
767      * @return the stopwatch after being started
768      */
startStopwatch()769     public Stopwatch startStopwatch() {
770         enforceMainLooper();
771         return mStopwatchModel.setStopwatch(getStopwatch().start());
772     }
773 
774     /**
775      * @return the stopwatch after being paused
776      */
pauseStopwatch()777     public Stopwatch pauseStopwatch() {
778         enforceMainLooper();
779         return mStopwatchModel.setStopwatch(getStopwatch().pause());
780     }
781 
782     /**
783      * @return the stopwatch after being reset
784      */
resetStopwatch()785     public Stopwatch resetStopwatch() {
786         enforceMainLooper();
787         return mStopwatchModel.setStopwatch(getStopwatch().reset());
788     }
789 
790     /**
791      * @return the laps recorded for this stopwatch
792      */
getLaps()793     public List<Lap> getLaps() {
794         enforceMainLooper();
795         return mStopwatchModel.getLaps();
796     }
797 
798     /**
799      * @return a newly recorded lap completed now; {@code null} if no more laps can be added
800      */
addLap()801     public Lap addLap() {
802         enforceMainLooper();
803         return mStopwatchModel.addLap();
804     }
805 
806     /**
807      * @return {@code true} iff more laps can be recorded
808      */
canAddMoreLaps()809     public boolean canAddMoreLaps() {
810         enforceMainLooper();
811         return mStopwatchModel.canAddMoreLaps();
812     }
813 
814     /**
815      * @return the longest lap time of all recorded laps and the current lap
816      */
getLongestLapTime()817     public long getLongestLapTime() {
818         enforceMainLooper();
819         return mStopwatchModel.getLongestLapTime();
820     }
821 
822     /**
823      * @param time a point in time after the end of the last lap
824      * @return the elapsed time between the given {@code time} and the end of the previous lap
825      */
getCurrentLapTime(long time)826     public long getCurrentLapTime(long time) {
827         enforceMainLooper();
828         return mStopwatchModel.getCurrentLapTime(time);
829     }
830 
831     //
832     // Time
833     // (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
834     //
835 
836     /**
837      * @return the current time in milliseconds
838      */
currentTimeMillis()839     public long currentTimeMillis() {
840         return mTimeModel.currentTimeMillis();
841     }
842 
843     /**
844      * @return milliseconds since boot, including time spent in sleep
845      */
elapsedRealtime()846     public long elapsedRealtime() {
847         return mTimeModel.elapsedRealtime();
848     }
849 
850     /**
851      * @return {@code true} if 24 hour time format is selected; {@code false} otherwise
852      */
is24HourFormat()853     public boolean is24HourFormat() {
854         return mTimeModel.is24HourFormat();
855     }
856 
857     /**
858      * @return a new calendar object initialized to the {@link #currentTimeMillis()}
859      */
getCalendar()860     public Calendar getCalendar() {
861         return mTimeModel.getCalendar();
862     }
863 
864     //
865     // Ringtones
866     //
867 
868     /**
869      * Ringtone titles are cached because loading them is expensive. This method
870      * <strong>must</strong> be called on a background thread and is responsible for priming the
871      * cache of ringtone titles to avoid later fetching titles on the main thread.
872      */
loadRingtoneTitles()873     public void loadRingtoneTitles() {
874         enforceNotMainLooper();
875         mRingtoneModel.loadRingtoneTitles();
876     }
877 
878     /**
879      * Recheck the permission to read each custom ringtone.
880      */
loadRingtonePermissions()881     public void loadRingtonePermissions() {
882         enforceNotMainLooper();
883         mRingtoneModel.loadRingtonePermissions();
884     }
885 
886     /**
887      * @param uri the uri of a ringtone
888      * @return the title of the ringtone with the {@code uri}; {@code null} if it cannot be fetched
889      */
getRingtoneTitle(Uri uri)890     public String getRingtoneTitle(Uri uri) {
891         enforceMainLooper();
892         return mRingtoneModel.getRingtoneTitle(uri);
893     }
894 
895     /**
896      * @param uri the uri of an audio file to use as a ringtone
897      * @param title the title of the audio content at the given {@code uri}
898      * @return the ringtone instance created for the audio file
899      */
addCustomRingtone(Uri uri, String title)900     public CustomRingtone addCustomRingtone(Uri uri, String title) {
901         enforceMainLooper();
902         return mRingtoneModel.addCustomRingtone(uri, title);
903     }
904 
905     /**
906      * @param uri identifies the ringtone to remove
907      */
removeCustomRingtone(Uri uri)908     public void removeCustomRingtone(Uri uri) {
909         enforceMainLooper();
910         mRingtoneModel.removeCustomRingtone(uri);
911     }
912 
913     /**
914      * @return all available custom ringtones
915      */
getCustomRingtones()916     public List<CustomRingtone> getCustomRingtones() {
917         enforceMainLooper();
918         return mRingtoneModel.getCustomRingtones();
919     }
920 
921     //
922     // Widgets
923     //
924 
925     /**
926      * @param widgetClass indicates the type of widget being counted
927      * @param count the number of widgets of the given type
928      * @param eventCategoryId identifies the category of event to send
929      */
updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId)930     public void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
931         enforceMainLooper();
932         mWidgetModel.updateWidgetCount(widgetClass, count, eventCategoryId);
933     }
934 
935     //
936     // Settings
937     //
938 
939     /**
940      * @param silentSettingsListener to be notified when alarm-silencing settings change
941      */
addSilentSettingsListener(OnSilentSettingsListener silentSettingsListener)942     public void addSilentSettingsListener(OnSilentSettingsListener silentSettingsListener) {
943         enforceMainLooper();
944         mSilentSettingsModel.addSilentSettingsListener(silentSettingsListener);
945     }
946 
947     /**
948      * @param silentSettingsListener to no longer be notified when alarm-silencing settings change
949      */
removeSilentSettingsListener(OnSilentSettingsListener silentSettingsListener)950     public void removeSilentSettingsListener(OnSilentSettingsListener silentSettingsListener) {
951         enforceMainLooper();
952         mSilentSettingsModel.removeSilentSettingsListener(silentSettingsListener);
953     }
954 
955     /**
956      * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
957      */
getGlobalIntentId()958     public int getGlobalIntentId() {
959         return mSettingsModel.getGlobalIntentId();
960     }
961 
962     /**
963      * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
964      */
updateGlobalIntentId()965     public void updateGlobalIntentId() {
966         enforceMainLooper();
967         mSettingsModel.updateGlobalIntentId();
968     }
969 
970     /**
971      * @return the style of clock to display in the clock application
972      */
getClockStyle()973     public ClockStyle getClockStyle() {
974         enforceMainLooper();
975         return mSettingsModel.getClockStyle();
976     }
977 
978     /**
979      * @return the style of clock to display in the clock application
980      */
getDisplayClockSeconds()981     public boolean getDisplayClockSeconds() {
982         enforceMainLooper();
983         return mSettingsModel.getDisplayClockSeconds();
984     }
985 
986     /**
987      * @param displaySeconds whether or not to display seconds for main clock
988      */
setDisplayClockSeconds(boolean displaySeconds)989     public void setDisplayClockSeconds(boolean displaySeconds) {
990         enforceMainLooper();
991         mSettingsModel.setDisplayClockSeconds(displaySeconds);
992     }
993 
994     /**
995      * @return the style of clock to display in the clock screensaver
996      */
getScreensaverClockStyle()997     public ClockStyle getScreensaverClockStyle() {
998         enforceMainLooper();
999         return mSettingsModel.getScreensaverClockStyle();
1000     }
1001 
1002     /**
1003      * @return {@code true} if the screen saver should be dimmed for lower contrast at night
1004      */
getScreensaverNightModeOn()1005     public boolean getScreensaverNightModeOn() {
1006         enforceMainLooper();
1007         return mSettingsModel.getScreensaverNightModeOn();
1008     }
1009 
1010     /**
1011      * @return {@code true} if the users wants to automatically show a clock for their home timezone
1012      *      when they have travelled outside of that timezone
1013      */
getShowHomeClock()1014     public boolean getShowHomeClock() {
1015         enforceMainLooper();
1016         return mSettingsModel.getShowHomeClock();
1017     }
1018 
1019     /**
1020      * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
1021      *      {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
1022      */
getWeekdayOrder()1023     public Weekdays.Order getWeekdayOrder() {
1024         enforceMainLooper();
1025         return mSettingsModel.getWeekdayOrder();
1026     }
1027 
1028     /**
1029      * @return {@code true} if the restore process (of backup and restore) has completed
1030      */
isRestoreBackupFinished()1031     public boolean isRestoreBackupFinished() {
1032         return mSettingsModel.isRestoreBackupFinished();
1033     }
1034 
1035     /**
1036      * @param finished {@code true} means the restore process (of backup and restore) has completed
1037      */
setRestoreBackupFinished(boolean finished)1038     public void setRestoreBackupFinished(boolean finished) {
1039         mSettingsModel.setRestoreBackupFinished(finished);
1040     }
1041 
1042     /**
1043      * @return a description of the time zones available for selection
1044      */
getTimeZones()1045     public TimeZones getTimeZones() {
1046         enforceMainLooper();
1047         return mSettingsModel.getTimeZones();
1048     }
1049 
1050     /**
1051      * Used to execute a delegate runnable and track its completion.
1052      */
1053     private static class ExecutedRunnable implements Runnable {
1054 
1055         private final Runnable mDelegate;
1056         private boolean mExecuted;
1057 
ExecutedRunnable(Runnable delegate)1058         private ExecutedRunnable(Runnable delegate) {
1059             this.mDelegate = delegate;
1060         }
1061 
1062         @Override
run()1063         public void run() {
1064             mDelegate.run();
1065 
1066             synchronized (this) {
1067                 mExecuted = true;
1068                 notifyAll();
1069             }
1070         }
1071 
isExecuted()1072         private boolean isExecuted() {
1073             return mExecuted;
1074         }
1075     }
1076 }
1077