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 }