• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
StopwatchModel(Context context, SharedPreferences prefs, NotificationModel notificationModel)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      */
addStopwatchListener(StopwatchListener stopwatchListener)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      */
removeStopwatchListener(StopwatchListener stopwatchListener)85     void removeStopwatchListener(StopwatchListener stopwatchListener) {
86         mStopwatchListeners.remove(stopwatchListener);
87     }
88 
89     /**
90      * @return the current state of the stopwatch
91      */
getStopwatch()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      */
setStopwatch(Stopwatch stopwatch)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      */
getLaps()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      */
addLap()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
clearLaps()172     void clearLaps() {
173         StopwatchDAO.clearLaps(mPrefs);
174         getMutableLaps().clear();
175     }
176 
177     /**
178      * @return {@code true} iff more laps can be recorded
179      */
canAddMoreLaps()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      */
getLongestLapTime()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      */
getCurrentLapTime(long time)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      */
updateNotification()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 
getMutableLaps()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
onReceive(Context context, Intent intent)251         public void onReceive(Context context, Intent intent) {
252             updateNotification();
253         }
254     }
255 }