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.Notification;
     20 import android.content.BroadcastReceiver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.IntentFilter;
     24 import android.content.SharedPreferences;
     25 import android.support.annotation.VisibleForTesting;
     26 import android.support.v4.app.NotificationManagerCompat;
     27 
     28 import java.util.ArrayList;
     29 import java.util.Collections;
     30 import java.util.List;
     31 
     32 /**
     33  * All {@link Stopwatch} data is accessed via this model.
     34  */
     35 final class StopwatchModel {
     36 
     37     private final Context mContext;
     38 
     39     private final SharedPreferences mPrefs;
     40 
     41     /** The model from which notification data are fetched. */
     42     private final NotificationModel mNotificationModel;
     43 
     44     /** Used to create and destroy system notifications related to the stopwatch. */
     45     private final NotificationManagerCompat mNotificationManager;
     46 
     47     /** Update stopwatch notification when locale changes. */
     48     @SuppressWarnings("FieldCanBeLocal")
     49     private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
     50 
     51     /** The listeners to notify when the stopwatch or its laps change. */
     52     private final List<StopwatchListener> mStopwatchListeners = new ArrayList<>();
     53 
     54     /** Delegate that builds platform-specific stopwatch notifications. */
     55     private final StopwatchNotificationBuilder mNotificationBuilder =
     56             new StopwatchNotificationBuilder();
     57 
     58     /** The current state of the stopwatch. */
     59     private Stopwatch mStopwatch;
     60 
     61     /** A mutable copy of the recorded stopwatch laps. */
     62     private List<Lap> mLaps;
     63 
     64     StopwatchModel(Context context, SharedPreferences prefs, NotificationModel notificationModel) {
     65         mContext = context;
     66         mPrefs = prefs;
     67         mNotificationModel = notificationModel;
     68         mNotificationManager = NotificationManagerCompat.from(context);
     69 
     70         // Update stopwatch notification when locale changes.
     71         final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
     72         mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
     73     }
     74 
     75     /**
     76      * @param stopwatchListener to be notified when stopwatch changes or laps are added
     77      */
     78     void addStopwatchListener(StopwatchListener stopwatchListener) {
     79         mStopwatchListeners.add(stopwatchListener);
     80     }
     81 
     82     /**
     83      * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
     84      */
     85     void removeStopwatchListener(StopwatchListener stopwatchListener) {
     86         mStopwatchListeners.remove(stopwatchListener);
     87     }
     88 
     89     /**
     90      * @return the current state of the stopwatch
     91      */
     92     Stopwatch getStopwatch() {
     93         if (mStopwatch == null) {
     94             mStopwatch = StopwatchDAO.getStopwatch(mPrefs);
     95         }
     96 
     97         return mStopwatch;
     98     }
     99 
    100     /**
    101      * @param stopwatch the new state of the stopwatch
    102      */
    103     Stopwatch setStopwatch(Stopwatch stopwatch) {
    104         final Stopwatch before = getStopwatch();
    105         if (before != stopwatch) {
    106             StopwatchDAO.setStopwatch(mPrefs, stopwatch);
    107             mStopwatch = stopwatch;
    108 
    109             // Refresh the stopwatch notification to reflect the latest stopwatch state.
    110             if (!mNotificationModel.isApplicationInForeground()) {
    111                 updateNotification();
    112             }
    113 
    114             // Resetting the stopwatch implicitly clears the recorded laps.
    115             if (stopwatch.isReset()) {
    116                 clearLaps();
    117             }
    118 
    119             // Notify listeners of the stopwatch change.
    120             for (StopwatchListener stopwatchListener : mStopwatchListeners) {
    121                 stopwatchListener.stopwatchUpdated(before, stopwatch);
    122             }
    123         }
    124 
    125         return stopwatch;
    126     }
    127 
    128     /**
    129      * @return the laps recorded for this stopwatch
    130      */
    131     List<Lap> getLaps() {
    132         return Collections.unmodifiableList(getMutableLaps());
    133     }
    134 
    135     /**
    136      * @return a newly recorded lap completed now; {@code null} if no more laps can be added
    137      */
    138     Lap addLap() {
    139         if (!mStopwatch.isRunning() || !canAddMoreLaps()) {
    140             return null;
    141         }
    142 
    143         final long totalTime = getStopwatch().getTotalTime();
    144         final List<Lap> laps = getMutableLaps();
    145 
    146         final int lapNumber = laps.size() + 1;
    147         StopwatchDAO.addLap(mPrefs, lapNumber, totalTime);
    148 
    149         final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime();
    150         final long lapTime = totalTime - prevAccumulatedTime;
    151 
    152         final Lap lap = new Lap(lapNumber, lapTime, totalTime);
    153         laps.add(0, lap);
    154 
    155         // Refresh the stopwatch notification to reflect the latest stopwatch state.
    156         if (!mNotificationModel.isApplicationInForeground()) {
    157             updateNotification();
    158         }
    159 
    160         // Notify listeners of the new lap.
    161         for (StopwatchListener stopwatchListener : mStopwatchListeners) {
    162             stopwatchListener.lapAdded(lap);
    163         }
    164 
    165         return lap;
    166     }
    167 
    168     /**
    169      * Clears the laps recorded for this stopwatch.
    170      */
    171     @VisibleForTesting
    172     void clearLaps() {
    173         StopwatchDAO.clearLaps(mPrefs);
    174         getMutableLaps().clear();
    175     }
    176 
    177     /**
    178      * @return {@code true} iff more laps can be recorded
    179      */
    180     boolean canAddMoreLaps() {
    181         return getLaps().size() < 98;
    182     }
    183 
    184     /**
    185      * @return the longest lap time of all recorded laps and the current lap
    186      */
    187     long getLongestLapTime() {
    188         long maxLapTime = 0;
    189 
    190         final List<Lap> laps = getLaps();
    191         if (!laps.isEmpty()) {
    192             // Compute the maximum lap time across all recorded laps.
    193             for (Lap lap : getLaps()) {
    194                 maxLapTime = Math.max(maxLapTime, lap.getLapTime());
    195             }
    196 
    197             // Compare with the maximum lap time for the current lap.
    198             final Stopwatch stopwatch = getStopwatch();
    199             final long currentLapTime = stopwatch.getTotalTime() - laps.get(0).getAccumulatedTime();
    200             maxLapTime = Math.max(maxLapTime, currentLapTime);
    201         }
    202 
    203         return maxLapTime;
    204     }
    205 
    206     /**
    207      * In practice, {@code time} can be any value due to device reboots. When the real-time clock is
    208      * reset, there is no more guarantee that this time falls after the last recorded lap.
    209      *
    210      * @param time a point in time expected, but not required, to be after the end of the prior lap
    211      * @return the elapsed time between the given {@code time} and the end of the prior lap;
    212      *      negative elapsed times are normalized to {@code 0}
    213      */
    214     long getCurrentLapTime(long time) {
    215         final Lap previousLap = getLaps().get(0);
    216         final long currentLapTime = time - previousLap.getAccumulatedTime();
    217         return Math.max(0, currentLapTime);
    218     }
    219 
    220     /**
    221      * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
    222      */
    223     void updateNotification() {
    224         final Stopwatch stopwatch = getStopwatch();
    225 
    226         // Notification should be hidden if the stopwatch has no time or the app is open.
    227         if (stopwatch.isReset() || mNotificationModel.isApplicationInForeground()) {
    228             mNotificationManager.cancel(mNotificationModel.getStopwatchNotificationId());
    229             return;
    230         }
    231 
    232         // Otherwise build and post a notification reflecting the latest stopwatch state.
    233         final Notification notification =
    234                 mNotificationBuilder.build(mContext, mNotificationModel, stopwatch);
    235         mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification);
    236     }
    237 
    238     private List<Lap> getMutableLaps() {
    239         if (mLaps == null) {
    240             mLaps = StopwatchDAO.getLaps(mPrefs);
    241         }
    242 
    243         return mLaps;
    244     }
    245 
    246     /**
    247      * Update the stopwatch notification in response to a locale change.
    248      */
    249     private final class LocaleChangedReceiver extends BroadcastReceiver {
    250         @Override
    251         public void onReceive(Context context, Intent intent) {
    252             updateNotification();
    253         }
    254     }
    255 }