Home | History | Annotate | Download | only in data
      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 android.support.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 
     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 
     95         public @StringRes int getLabelResId() { return mLabelResId; }
     96         public @StringRes int getActionResId() { return mActionResId; }
     97         public View.OnClickListener getActionListener() { return mActionListener; }
     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
    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
    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
    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
    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 
    197     public static DataModel getDataModel() {
    198         return sDataModel;
    199     }
    200 
    201     private DataModel() {}
    202 
    203     /**
    204      * Initializes the data model with the context and shared preferences to be used.
    205      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    338     public City getHomeCity() {
    339         enforceMainLooper();
    340         return mCityModel.getHomeCity();
    341     }
    342 
    343     /**
    344      * @return a list of cities not selected for display
    345      */
    346     public List<City> getUnselectedCities() {
    347         enforceMainLooper();
    348         return mCityModel.getUnselectedCities();
    349     }
    350 
    351     /**
    352      * @return a list of cities selected for display
    353      */
    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      */
    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      */
    370     public Comparator<City> getCityIndexComparator() {
    371         enforceMainLooper();
    372         return mCityModel.getCityIndexComparator();
    373     }
    374 
    375     /**
    376      * @return the order in which cities are sorted
    377      */
    378     public CitySort getCitySort() {
    379         enforceMainLooper();
    380         return mCityModel.getCitySort();
    381     }
    382 
    383     /**
    384      * Adjust the order in which cities are sorted.
    385      */
    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      */
    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      */
    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      */
    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      */
    422     public void removeTimerListener(TimerListener timerListener) {
    423         enforceMainLooper();
    424         mTimerModel.removeTimerListener(timerListener);
    425     }
    426 
    427     /**
    428      * @return a list of timers for display
    429      */
    430     public List<Timer> getTimers() {
    431         enforceMainLooper();
    432         return mTimerModel.getTimers();
    433     }
    434 
    435     /**
    436      * @return a list of expired timers for display
    437      */
    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      */
    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      */
    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      */
    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      */
    475     public void removeTimer(Timer timer) {
    476         enforceMainLooper();
    477         mTimerModel.removeTimer(timer);
    478     }
    479 
    480     /**
    481      * @param timer the timer to be started
    482      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    666     public long getTimerCrescendoDuration() {
    667         enforceMainLooper();
    668         return mTimerModel.getTimerCrescendoDuration();
    669     }
    670 
    671     /**
    672      * @return whether vibrate is enabled for all timers.
    673      */
    674     public boolean getTimerVibrate() {
    675         enforceMainLooper();
    676         return mTimerModel.getTimerVibrate();
    677     }
    678 
    679     /**
    680      * @param enabled whether vibrate is enabled for all timers.
    681      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    753     public void removeStopwatchListener(StopwatchListener stopwatchListener) {
    754         enforceMainLooper();
    755         mStopwatchModel.removeStopwatchListener(stopwatchListener);
    756     }
    757 
    758     /**
    759      * @return the current state of the stopwatch
    760      */
    761     public Stopwatch getStopwatch() {
    762         enforceMainLooper();
    763         return mStopwatchModel.getStopwatch();
    764     }
    765 
    766     /**
    767      * @return the stopwatch after being started
    768      */
    769     public Stopwatch startStopwatch() {
    770         enforceMainLooper();
    771         return mStopwatchModel.setStopwatch(getStopwatch().start());
    772     }
    773 
    774     /**
    775      * @return the stopwatch after being paused
    776      */
    777     public Stopwatch pauseStopwatch() {
    778         enforceMainLooper();
    779         return mStopwatchModel.setStopwatch(getStopwatch().pause());
    780     }
    781 
    782     /**
    783      * @return the stopwatch after being reset
    784      */
    785     public Stopwatch resetStopwatch() {
    786         enforceMainLooper();
    787         return mStopwatchModel.setStopwatch(getStopwatch().reset());
    788     }
    789 
    790     /**
    791      * @return the laps recorded for this stopwatch
    792      */
    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      */
    801     public Lap addLap() {
    802         enforceMainLooper();
    803         return mStopwatchModel.addLap();
    804     }
    805 
    806     /**
    807      * @return {@code true} iff more laps can be recorded
    808      */
    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      */
    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      */
    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      */
    839     public long currentTimeMillis() {
    840         return mTimeModel.currentTimeMillis();
    841     }
    842 
    843     /**
    844      * @return milliseconds since boot, including time spent in sleep
    845      */
    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      */
    853     public boolean is24HourFormat() {
    854         return mTimeModel.is24HourFormat();
    855     }
    856 
    857     /**
    858      * @return a new calendar object initialized to the {@link #currentTimeMillis()}
    859      */
    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      */
    873     public void loadRingtoneTitles() {
    874         enforceNotMainLooper();
    875         mRingtoneModel.loadRingtoneTitles();
    876     }
    877 
    878     /**
    879      * Recheck the permission to read each custom ringtone.
    880      */
    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      */
    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      */
    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      */
    908     public void removeCustomRingtone(Uri uri) {
    909         enforceMainLooper();
    910         mRingtoneModel.removeCustomRingtone(uri);
    911     }
    912 
    913     /**
    914      * @return all available custom ringtones
    915      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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 
   1058         private ExecutedRunnable(Runnable delegate) {
   1059             this.mDelegate = delegate;
   1060         }
   1061 
   1062         @Override
   1063         public void run() {
   1064             mDelegate.run();
   1065 
   1066             synchronized (this) {
   1067                 mExecuted = true;
   1068                 notifyAll();
   1069             }
   1070         }
   1071 
   1072         private boolean isExecuted() {
   1073             return mExecuted;
   1074         }
   1075     }
   1076 }
   1077