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.app.PendingIntent;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.content.res.Resources;
     26 import android.os.SystemClock;
     27 import android.support.annotation.IdRes;
     28 import android.support.annotation.StringRes;
     29 import android.support.v4.app.NotificationCompat;
     30 import android.support.v4.app.NotificationManagerCompat;
     31 import android.widget.RemoteViews;
     32 
     33 import com.android.deskclock.HandleDeskClockApiCalls;
     34 import com.android.deskclock.R;
     35 import com.android.deskclock.stopwatch.StopwatchService;
     36 
     37 import java.util.Collections;
     38 import java.util.List;
     39 
     40 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
     41 import static android.view.View.GONE;
     42 import static android.view.View.INVISIBLE;
     43 import static android.view.View.VISIBLE;
     44 
     45 /**
     46  * All {@link Stopwatch} data is accessed via this model.
     47  */
     48 final class StopwatchModel {
     49 
     50     private final Context mContext;
     51 
     52     /** The model from which notification data are fetched. */
     53     private final NotificationModel mNotificationModel;
     54 
     55     /** Used to create and destroy system notifications related to the stopwatch. */
     56     private final NotificationManagerCompat mNotificationManager;
     57 
     58     /** Update stopwatch notification when locale changes. */
     59     private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
     60 
     61     /** The current state of the stopwatch. */
     62     private Stopwatch mStopwatch;
     63 
     64     /** A mutable copy of the recorded stopwatch laps. */
     65     private List<Lap> mLaps;
     66 
     67     StopwatchModel(Context context, NotificationModel notificationModel) {
     68         mContext = context;
     69         mNotificationModel = notificationModel;
     70         mNotificationManager = NotificationManagerCompat.from(context);
     71 
     72         // Update stopwatch notification when locale changes.
     73         final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
     74         mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
     75     }
     76 
     77     /**
     78      * @return the current state of the stopwatch
     79      */
     80     Stopwatch getStopwatch() {
     81         if (mStopwatch == null) {
     82             mStopwatch = StopwatchDAO.getStopwatch(mContext);
     83         }
     84 
     85         return mStopwatch;
     86     }
     87 
     88     /**
     89      * @param stopwatch the new state of the stopwatch
     90      */
     91     Stopwatch setStopwatch(Stopwatch stopwatch) {
     92         if (mStopwatch != stopwatch) {
     93             StopwatchDAO.setStopwatch(mContext, stopwatch);
     94             mStopwatch = stopwatch;
     95 
     96             // Refresh the stopwatch notification to reflect the latest stopwatch state.
     97             if (!mNotificationModel.isApplicationInForeground()) {
     98                 updateNotification();
     99             }
    100         }
    101 
    102         return stopwatch;
    103     }
    104 
    105     /**
    106      * @return the laps recorded for this stopwatch
    107      */
    108     List<Lap> getLaps() {
    109         return Collections.unmodifiableList(getMutableLaps());
    110     }
    111 
    112     /**
    113      * @return a newly recorded lap completed now; {@code null} if no more laps can be added
    114      */
    115     Lap addLap() {
    116         if (!canAddMoreLaps()) {
    117             return null;
    118         }
    119 
    120         final long totalTime = getStopwatch().getTotalTime();
    121         final List<Lap> laps = getMutableLaps();
    122 
    123         final int lapNumber = laps.size() + 1;
    124         StopwatchDAO.addLap(mContext, lapNumber, totalTime);
    125 
    126         final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime();
    127         final long lapTime = totalTime - prevAccumulatedTime;
    128 
    129         final Lap lap = new Lap(lapNumber, lapTime, totalTime);
    130         laps.add(0, lap);
    131 
    132         // Refresh the stopwatch notification to reflect the latest stopwatch state.
    133         if (!mNotificationModel.isApplicationInForeground()) {
    134             updateNotification();
    135         }
    136 
    137         return lap;
    138     }
    139 
    140     /**
    141      * Clears the laps recorded for this stopwatch.
    142      */
    143     void clearLaps() {
    144         StopwatchDAO.clearLaps(mContext);
    145         getMutableLaps().clear();
    146     }
    147 
    148     /**
    149      * @return {@code true} iff more laps can be recorded
    150      */
    151     boolean canAddMoreLaps() {
    152         return getLaps().size() < 98;
    153     }
    154 
    155     /**
    156      * @return the longest lap time of all recorded laps and the current lap
    157      */
    158     long getLongestLapTime() {
    159         long maxLapTime = 0;
    160 
    161         final List<Lap> laps = getLaps();
    162         if (!laps.isEmpty()) {
    163             // Compute the maximum lap time across all recorded laps.
    164             for (Lap lap : getLaps()) {
    165                 maxLapTime = Math.max(maxLapTime, lap.getLapTime());
    166             }
    167 
    168             // Compare with the maximum lap time for the current lap.
    169             final Stopwatch stopwatch = getStopwatch();
    170             final long currentLapTime = stopwatch.getTotalTime() - laps.get(0).getAccumulatedTime();
    171             maxLapTime = Math.max(maxLapTime, currentLapTime);
    172         }
    173 
    174         return maxLapTime;
    175     }
    176 
    177     /**
    178      * In practice, {@code time} can be any value due to device reboots. When the real-time clock is
    179      * reset, there is no more guarantee that this time falls after the last recorded lap.
    180      *
    181      * @param time a point in time expected, but not required, to be after the end of the prior lap
    182      * @return the elapsed time between the given {@code time} and the end of the prior lap;
    183      *      negative elapsed times are normalized to {@code 0}
    184      */
    185     long getCurrentLapTime(long time) {
    186         final Lap previousLap = getLaps().get(0);
    187         final long currentLapTime = time - previousLap.getAccumulatedTime();
    188         return Math.max(0, currentLapTime);
    189     }
    190 
    191     /**
    192      * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
    193      */
    194     void updateNotification() {
    195         final Stopwatch stopwatch = getStopwatch();
    196 
    197         // Notification should be hidden if the stopwatch has no time or the app is open.
    198         if (stopwatch.isReset() || mNotificationModel.isApplicationInForeground()) {
    199             mNotificationManager.cancel(mNotificationModel.getStopwatchNotificationId());
    200             return;
    201         }
    202 
    203         @StringRes final int eventLabel = R.string.label_notification;
    204 
    205         // Intent to load the app when the notification is tapped.
    206         final Intent showApp = new Intent(mContext, HandleDeskClockApiCalls.class)
    207                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    208                 .setAction(HandleDeskClockApiCalls.ACTION_SHOW_STOPWATCH)
    209                 .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, eventLabel);
    210 
    211         final PendingIntent pendingShowApp = PendingIntent.getActivity(mContext, 0, showApp,
    212                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    213 
    214         // Compute some values required below.
    215         final boolean running = stopwatch.isRunning();
    216         final String pname = mContext.getPackageName();
    217         final Resources res = mContext.getResources();
    218         final long base = SystemClock.elapsedRealtime() - stopwatch.getTotalTime();
    219 
    220         final RemoteViews collapsed = new RemoteViews(pname, R.layout.stopwatch_notif_collapsed);
    221         collapsed.setChronometer(R.id.swn_collapsed_chronometer, base, null, running);
    222         collapsed.setOnClickPendingIntent(R.id.swn_collapsed_hitspace, pendingShowApp);
    223         collapsed.setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
    224 
    225         final RemoteViews expanded = new RemoteViews(pname, R.layout.stopwatch_notif_expanded);
    226         expanded.setChronometer(R.id.swn_expanded_chronometer, base, null, running);
    227         expanded.setOnClickPendingIntent(R.id.swn_expanded_hitspace, pendingShowApp);
    228         expanded.setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
    229 
    230         @IdRes final int leftButtonId = R.id.swn_left_button;
    231         @IdRes final int rightButtonId = R.id.swn_right_button;
    232         if (running) {
    233             // Left button: Pause
    234             expanded.setTextViewText(leftButtonId, res.getText(R.string.sw_pause_button));
    235             setTextViewDrawable(expanded, leftButtonId, R.drawable.ic_pause_24dp);
    236             final Intent pause = new Intent(mContext, StopwatchService.class)
    237                     .setAction(HandleDeskClockApiCalls.ACTION_PAUSE_STOPWATCH)
    238                     .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, eventLabel);
    239             expanded.setOnClickPendingIntent(leftButtonId, pendingServiceIntent(pause));
    240 
    241             // Right button: Add Lap
    242             if (canAddMoreLaps()) {
    243                 expanded.setTextViewText(rightButtonId, res.getText(R.string.sw_lap_button));
    244                 setTextViewDrawable(expanded, rightButtonId, R.drawable.ic_sw_lap_24dp);
    245 
    246                 final Intent lap = new Intent(mContext, StopwatchService.class)
    247                         .setAction(HandleDeskClockApiCalls.ACTION_LAP_STOPWATCH)
    248                         .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, eventLabel);
    249                 expanded.setOnClickPendingIntent(rightButtonId, pendingServiceIntent(lap));
    250                 expanded.setViewVisibility(rightButtonId, VISIBLE);
    251             } else {
    252                 expanded.setViewVisibility(rightButtonId, INVISIBLE);
    253             }
    254 
    255             // Show the current lap number if any laps have been recorded.
    256             final int lapCount = getLaps().size();
    257             if (lapCount > 0) {
    258                 final int lapNumber = lapCount + 1;
    259                 final String lap = res.getString(R.string.sw_notification_lap_number, lapNumber);
    260                 collapsed.setTextViewText(R.id.swn_collapsed_laps, lap);
    261                 collapsed.setViewVisibility(R.id.swn_collapsed_laps, VISIBLE);
    262                 expanded.setTextViewText(R.id.swn_expanded_laps, lap);
    263                 expanded.setViewVisibility(R.id.swn_expanded_laps, VISIBLE);
    264             } else {
    265                 collapsed.setViewVisibility(R.id.swn_collapsed_laps, GONE);
    266                 expanded.setViewVisibility(R.id.swn_expanded_laps, GONE);
    267             }
    268         } else {
    269             // Left button: Start
    270             expanded.setTextViewText(leftButtonId, res.getText(R.string.sw_start_button));
    271             setTextViewDrawable(expanded, leftButtonId, R.drawable.ic_start_24dp);
    272             final Intent start = new Intent(mContext, StopwatchService.class)
    273                     .setAction(HandleDeskClockApiCalls.ACTION_START_STOPWATCH)
    274                     .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, eventLabel);
    275             expanded.setOnClickPendingIntent(leftButtonId, pendingServiceIntent(start));
    276 
    277             // Right button: Reset (HandleDeskClockApiCalls will also bring forward the app)
    278             expanded.setViewVisibility(rightButtonId, VISIBLE);
    279             expanded.setTextViewText(rightButtonId, res.getText(R.string.sw_reset_button));
    280             setTextViewDrawable(expanded, rightButtonId, R.drawable.ic_reset_24dp);
    281             final Intent reset = new Intent(mContext, HandleDeskClockApiCalls.class)
    282                     .setAction(HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH)
    283                     .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, eventLabel);
    284             expanded.setOnClickPendingIntent(rightButtonId, pendingActivityIntent(reset));
    285 
    286             // Indicate the stopwatch is paused.
    287             collapsed.setTextViewText(R.id.swn_collapsed_laps, res.getString(R.string.swn_paused));
    288             collapsed.setViewVisibility(R.id.swn_collapsed_laps, VISIBLE);
    289             expanded.setTextViewText(R.id.swn_expanded_laps, res.getString(R.string.swn_paused));
    290             expanded.setViewVisibility(R.id.swn_expanded_laps, VISIBLE);
    291         }
    292 
    293         // Swipe away will reset the stopwatch without bringing forward the app.
    294         final Intent reset = new Intent(mContext, StopwatchService.class)
    295                 .setAction(HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH)
    296                 .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, eventLabel);
    297 
    298         final Notification notification = new NotificationCompat.Builder(mContext)
    299                 .setLocalOnly(true)
    300                 .setOngoing(running)
    301                 .setContent(collapsed)
    302                 .setAutoCancel(stopwatch.isPaused())
    303                 .setPriority(Notification.PRIORITY_MAX)
    304                 .setDeleteIntent(pendingServiceIntent(reset))
    305                 .setSmallIcon(R.drawable.ic_tab_stopwatch_activated)
    306                 .build();
    307         notification.bigContentView = expanded;
    308         mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification);
    309     }
    310 
    311     private PendingIntent pendingServiceIntent(Intent intent) {
    312         return PendingIntent.getService(mContext, 0, intent, FLAG_UPDATE_CURRENT);
    313     }
    314 
    315     private PendingIntent pendingActivityIntent(Intent intent) {
    316         return PendingIntent.getActivity(mContext, 0, intent, FLAG_UPDATE_CURRENT);
    317     }
    318 
    319     private static void setTextViewDrawable(RemoteViews rv, int viewId, int drawableId) {
    320         rv.setTextViewCompoundDrawablesRelative(viewId, drawableId, 0, 0, 0);
    321     }
    322 
    323     private List<Lap> getMutableLaps() {
    324         if (mLaps == null) {
    325             mLaps = StopwatchDAO.getLaps(mContext);
    326         }
    327 
    328         return mLaps;
    329     }
    330 
    331     /**
    332      * Update the stopwatch notification in response to a locale change.
    333      */
    334     private final class LocaleChangedReceiver extends BroadcastReceiver {
    335         @Override
    336         public void onReceive(Context context, Intent intent) {
    337             updateNotification();
    338         }
    339     }
    340 }