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