/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deskclock.data; import android.app.Notification; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.support.annotation.VisibleForTesting; import android.support.v4.app.NotificationManagerCompat; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * All {@link Stopwatch} data is accessed via this model. */ final class StopwatchModel { private final Context mContext; private final SharedPreferences mPrefs; /** The model from which notification data are fetched. */ private final NotificationModel mNotificationModel; /** Used to create and destroy system notifications related to the stopwatch. */ private final NotificationManagerCompat mNotificationManager; /** Update stopwatch notification when locale changes. */ @SuppressWarnings("FieldCanBeLocal") private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver(); /** The listeners to notify when the stopwatch or its laps change. */ private final List mStopwatchListeners = new ArrayList<>(); /** Delegate that builds platform-specific stopwatch notifications. */ private final StopwatchNotificationBuilder mNotificationBuilder = new StopwatchNotificationBuilder(); /** The current state of the stopwatch. */ private Stopwatch mStopwatch; /** A mutable copy of the recorded stopwatch laps. */ private List mLaps; StopwatchModel(Context context, SharedPreferences prefs, NotificationModel notificationModel) { mContext = context; mPrefs = prefs; mNotificationModel = notificationModel; mNotificationManager = NotificationManagerCompat.from(context); // Update stopwatch notification when locale changes. final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter); } /** * @param stopwatchListener to be notified when stopwatch changes or laps are added */ void addStopwatchListener(StopwatchListener stopwatchListener) { mStopwatchListeners.add(stopwatchListener); } /** * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added */ void removeStopwatchListener(StopwatchListener stopwatchListener) { mStopwatchListeners.remove(stopwatchListener); } /** * @return the current state of the stopwatch */ Stopwatch getStopwatch() { if (mStopwatch == null) { mStopwatch = StopwatchDAO.getStopwatch(mPrefs); } return mStopwatch; } /** * @param stopwatch the new state of the stopwatch */ Stopwatch setStopwatch(Stopwatch stopwatch) { final Stopwatch before = getStopwatch(); if (before != stopwatch) { StopwatchDAO.setStopwatch(mPrefs, stopwatch); mStopwatch = stopwatch; // Refresh the stopwatch notification to reflect the latest stopwatch state. if (!mNotificationModel.isApplicationInForeground()) { updateNotification(); } // Resetting the stopwatch implicitly clears the recorded laps. if (stopwatch.isReset()) { clearLaps(); } // Notify listeners of the stopwatch change. for (StopwatchListener stopwatchListener : mStopwatchListeners) { stopwatchListener.stopwatchUpdated(before, stopwatch); } } return stopwatch; } /** * @return the laps recorded for this stopwatch */ List getLaps() { return Collections.unmodifiableList(getMutableLaps()); } /** * @return a newly recorded lap completed now; {@code null} if no more laps can be added */ Lap addLap() { if (!mStopwatch.isRunning() || !canAddMoreLaps()) { return null; } final long totalTime = getStopwatch().getTotalTime(); final List laps = getMutableLaps(); final int lapNumber = laps.size() + 1; StopwatchDAO.addLap(mPrefs, lapNumber, totalTime); final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime(); final long lapTime = totalTime - prevAccumulatedTime; final Lap lap = new Lap(lapNumber, lapTime, totalTime); laps.add(0, lap); // Refresh the stopwatch notification to reflect the latest stopwatch state. if (!mNotificationModel.isApplicationInForeground()) { updateNotification(); } // Notify listeners of the new lap. for (StopwatchListener stopwatchListener : mStopwatchListeners) { stopwatchListener.lapAdded(lap); } return lap; } /** * Clears the laps recorded for this stopwatch. */ @VisibleForTesting void clearLaps() { StopwatchDAO.clearLaps(mPrefs); getMutableLaps().clear(); } /** * @return {@code true} iff more laps can be recorded */ boolean canAddMoreLaps() { return getLaps().size() < 98; } /** * @return the longest lap time of all recorded laps and the current lap */ long getLongestLapTime() { long maxLapTime = 0; final List laps = getLaps(); if (!laps.isEmpty()) { // Compute the maximum lap time across all recorded laps. for (Lap lap : getLaps()) { maxLapTime = Math.max(maxLapTime, lap.getLapTime()); } // Compare with the maximum lap time for the current lap. final Stopwatch stopwatch = getStopwatch(); final long currentLapTime = stopwatch.getTotalTime() - laps.get(0).getAccumulatedTime(); maxLapTime = Math.max(maxLapTime, currentLapTime); } return maxLapTime; } /** * In practice, {@code time} can be any value due to device reboots. When the real-time clock is * reset, there is no more guarantee that this time falls after the last recorded lap. * * @param time a point in time expected, but not required, to be after the end of the prior lap * @return the elapsed time between the given {@code time} and the end of the prior lap; * negative elapsed times are normalized to {@code 0} */ long getCurrentLapTime(long time) { final Lap previousLap = getLaps().get(0); final long currentLapTime = time - previousLap.getAccumulatedTime(); return Math.max(0, currentLapTime); } /** * Updates the notification to reflect the latest state of the stopwatch and recorded laps. */ void updateNotification() { final Stopwatch stopwatch = getStopwatch(); // Notification should be hidden if the stopwatch has no time or the app is open. if (stopwatch.isReset() || mNotificationModel.isApplicationInForeground()) { mNotificationManager.cancel(mNotificationModel.getStopwatchNotificationId()); return; } // Otherwise build and post a notification reflecting the latest stopwatch state. final Notification notification = mNotificationBuilder.build(mContext, mNotificationModel, stopwatch); mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification); } private List getMutableLaps() { if (mLaps == null) { mLaps = StopwatchDAO.getLaps(mPrefs); } return mLaps; } /** * Update the stopwatch notification in response to a locale change. */ private final class LocaleChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { updateNotification(); } } }