Home | History | Annotate | Download | only in stopwatch
      1 package com.android.deskclock.stopwatch;
      2 
      3 import android.animation.LayoutTransition;
      4 import android.content.ActivityNotFoundException;
      5 import android.content.Context;
      6 import android.content.Intent;
      7 import android.content.SharedPreferences;
      8 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
      9 import android.content.res.Configuration;
     10 import android.os.Bundle;
     11 import android.os.PowerManager;
     12 import android.os.PowerManager.WakeLock;
     13 import android.preference.PreferenceManager;
     14 import android.text.format.DateUtils;
     15 import android.view.LayoutInflater;
     16 import android.view.View;
     17 import android.view.ViewGroup;
     18 import android.view.animation.Animation;
     19 import android.view.animation.TranslateAnimation;
     20 import android.widget.BaseAdapter;
     21 import android.widget.ListView;
     22 import android.widget.TextView;
     23 
     24 import com.android.deskclock.CircleButtonsLayout;
     25 import com.android.deskclock.CircleTimerView;
     26 import com.android.deskclock.DeskClock;
     27 import com.android.deskclock.DeskClockFragment;
     28 import com.android.deskclock.LogUtils;
     29 import com.android.deskclock.R;
     30 import com.android.deskclock.Utils;
     31 import com.android.deskclock.timer.CountingTimerView;
     32 
     33 import java.util.ArrayList;
     34 
     35 public class StopwatchFragment extends DeskClockFragment
     36         implements OnSharedPreferenceChangeListener {
     37     private static final boolean DEBUG = false;
     38 
     39     private static final String TAG = "StopwatchFragment";
     40     private static final int STOPWATCH_REFRESH_INTERVAL_MILLIS = 25;
     41 
     42     int mState = Stopwatches.STOPWATCH_RESET;
     43 
     44     // Stopwatch views that are accessed by the activity
     45     private CircleTimerView mTime;
     46     private CountingTimerView mTimeText;
     47     private ListView mLapsList;
     48     private WakeLock mWakeLock;
     49     private CircleButtonsLayout mCircleLayout;
     50 
     51     // Animation constants and objects
     52     private LayoutTransition mLayoutTransition;
     53     private LayoutTransition mCircleLayoutTransition;
     54     private View mStartSpace;
     55     private View mEndSpace;
     56     private View mBottomSpace;
     57     private boolean mSpacersUsed;
     58 
     59     // Used for calculating the time from the start taking into account the pause times
     60     long mStartTime = 0;
     61     long mAccumulatedTime = 0;
     62 
     63     // Lap information
     64     class Lap {
     65 
     66         Lap (long time, long total) {
     67             mLapTime = time;
     68             mTotalTime = total;
     69         }
     70         public long mLapTime;
     71         public long mTotalTime;
     72 
     73         public void updateView() {
     74             View lapInfo = mLapsList.findViewWithTag(this);
     75             if (lapInfo != null) {
     76                 mLapsAdapter.setTimeText(lapInfo, this);
     77             }
     78         }
     79     }
     80 
     81     // Adapter for the ListView that shows the lap times.
     82     class LapsListAdapter extends BaseAdapter {
     83 
     84         private static final int VIEW_TYPE_LAP = 0;
     85         private static final int VIEW_TYPE_SPACE = 1;
     86         private static final int VIEW_TYPE_COUNT = 2;
     87 
     88         ArrayList<Lap> mLaps = new ArrayList<Lap>();
     89         private final LayoutInflater mInflater;
     90         private final String[] mFormats;
     91         private final String[] mLapFormatSet;
     92         // Size of this array must match the size of formats
     93         private final long[] mThresholds = {
     94                 10 * DateUtils.MINUTE_IN_MILLIS, // < 10 minutes
     95                 DateUtils.HOUR_IN_MILLIS, // < 1 hour
     96                 10 * DateUtils.HOUR_IN_MILLIS, // < 10 hours
     97                 100 * DateUtils.HOUR_IN_MILLIS, // < 100 hours
     98                 1000 * DateUtils.HOUR_IN_MILLIS // < 1000 hours
     99         };
    100         private int mLapIndex = 0;
    101         private int mTotalIndex = 0;
    102         private String mLapFormat;
    103 
    104         public LapsListAdapter(Context context) {
    105             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    106             mFormats = context.getResources().getStringArray(R.array.stopwatch_format_set);
    107             mLapFormatSet = context.getResources().getStringArray(R.array.sw_lap_number_set);
    108             updateLapFormat();
    109         }
    110 
    111         @Override
    112         public long getItemId(int position) {
    113             return position;
    114         }
    115 
    116         @Override
    117         public int getItemViewType(int position) {
    118             return position < mLaps.size() ? VIEW_TYPE_LAP : VIEW_TYPE_SPACE;
    119         }
    120 
    121         @Override
    122         public int getViewTypeCount() {
    123             return VIEW_TYPE_COUNT;
    124         }
    125 
    126         @Override
    127         public View getView(int position, View convertView, ViewGroup parent) {
    128             if (getCount() == 0) {
    129                 return null;
    130             }
    131 
    132             // Handle request for the Spacer at the end
    133             if (getItemViewType(position) == VIEW_TYPE_SPACE) {
    134                 return convertView != null ? convertView
    135                         : mInflater.inflate(R.layout.stopwatch_spacer, parent, false);
    136             }
    137 
    138             final View lapInfo = convertView != null ? convertView
    139                     : mInflater.inflate(R.layout.lap_view, parent, false);
    140             Lap lap = getItem(position);
    141             lapInfo.setTag(lap);
    142 
    143             TextView count = (TextView) lapInfo.findViewById(R.id.lap_number);
    144             count.setText(String.format(mLapFormat, mLaps.size() - position).toUpperCase());
    145             setTimeText(lapInfo, lap);
    146 
    147             return lapInfo;
    148         }
    149 
    150         protected void setTimeText(View lapInfo, Lap lap) {
    151             TextView lapTime = (TextView)lapInfo.findViewById(R.id.lap_time);
    152             TextView totalTime = (TextView)lapInfo.findViewById(R.id.lap_total);
    153             lapTime.setText(Stopwatches.formatTimeText(lap.mLapTime, mFormats[mLapIndex]));
    154             totalTime.setText(Stopwatches.formatTimeText(lap.mTotalTime, mFormats[mTotalIndex]));
    155         }
    156 
    157         @Override
    158         public int getCount() {
    159             // Add 1 for the spacer if list is not empty
    160             return mLaps.isEmpty() ? 0 : mLaps.size() + 1;
    161         }
    162 
    163         @Override
    164         public Lap getItem(int position) {
    165             if (position >= mLaps.size()) {
    166                 return null;
    167             }
    168             return mLaps.get(position);
    169         }
    170 
    171         private void updateLapFormat() {
    172             // Note Stopwatches.MAX_LAPS < 100
    173             mLapFormat = mLapFormatSet[mLaps.size() < 10 ? 0 : 1];
    174         }
    175 
    176         private void resetTimeFormats() {
    177             mLapIndex = mTotalIndex = 0;
    178         }
    179 
    180         /**
    181          * A lap is printed into two columns: the total time and the lap time. To make this print
    182          * as pretty as possible, multiple formats were created which minimize the width of the
    183          * print. As the total or lap time exceed the limit of that format, this code updates
    184          * the format used for the total and/or lap times.
    185          *
    186          * @param lap to measure
    187          * @return true if this lap exceeded either threshold and a format was updated.
    188          */
    189         public boolean updateTimeFormats(Lap lap) {
    190             boolean formatChanged = false;
    191             while (mLapIndex + 1 < mThresholds.length && lap.mLapTime >= mThresholds[mLapIndex]) {
    192                 mLapIndex++;
    193                 formatChanged = true;
    194             }
    195             while (mTotalIndex + 1 < mThresholds.length &&
    196                 lap.mTotalTime >= mThresholds[mTotalIndex]) {
    197                 mTotalIndex++;
    198                 formatChanged = true;
    199             }
    200             return formatChanged;
    201         }
    202 
    203         public void addLap(Lap l) {
    204             mLaps.add(0, l);
    205             // for efficiency caller also calls notifyDataSetChanged()
    206         }
    207 
    208         public void clearLaps() {
    209             mLaps.clear();
    210             updateLapFormat();
    211             resetTimeFormats();
    212             notifyDataSetChanged();
    213         }
    214 
    215         // Helper function used to get the lap data to be stored in the activity's bundle
    216         public long [] getLapTimes() {
    217             int size = mLaps.size();
    218             if (size == 0) {
    219                 return null;
    220             }
    221             long [] laps = new long[size];
    222             for (int i = 0; i < size; i ++) {
    223                 laps[i] = mLaps.get(i).mTotalTime;
    224             }
    225             return laps;
    226         }
    227 
    228         // Helper function to restore adapter's data from the activity's bundle
    229         public void setLapTimes(long [] laps) {
    230             if (laps == null || laps.length == 0) {
    231                 return;
    232             }
    233 
    234             int size = laps.length;
    235             mLaps.clear();
    236             for (long lap : laps) {
    237                 mLaps.add(new Lap(lap, 0));
    238             }
    239             long totalTime = 0;
    240             for (int i = size -1; i >= 0; i --) {
    241                 totalTime += laps[i];
    242                 mLaps.get(i).mTotalTime = totalTime;
    243                 updateTimeFormats(mLaps.get(i));
    244             }
    245             updateLapFormat();
    246             showLaps();
    247             notifyDataSetChanged();
    248         }
    249     }
    250 
    251     LapsListAdapter mLapsAdapter;
    252 
    253     public StopwatchFragment() {
    254     }
    255 
    256     private void toggleStopwatchState() {
    257         long time = Utils.getTimeNow();
    258         Context context = getActivity().getApplicationContext();
    259         Intent intent = new Intent(context, StopwatchService.class);
    260         intent.putExtra(Stopwatches.MESSAGE_TIME, time);
    261         intent.putExtra(Stopwatches.SHOW_NOTIF, false);
    262         switch (mState) {
    263             case Stopwatches.STOPWATCH_RUNNING:
    264                 // do stop
    265                 long curTime = Utils.getTimeNow();
    266                 mAccumulatedTime += (curTime - mStartTime);
    267                 doStop();
    268                 intent.setAction(Stopwatches.STOP_STOPWATCH);
    269                 context.startService(intent);
    270                 releaseWakeLock();
    271                 break;
    272             case Stopwatches.STOPWATCH_RESET:
    273             case Stopwatches.STOPWATCH_STOPPED:
    274                 // do start
    275                 doStart(time);
    276                 intent.setAction(Stopwatches.START_STOPWATCH);
    277                 context.startService(intent);
    278                 acquireWakeLock();
    279                 break;
    280             default:
    281                 LogUtils.wtf("Illegal state " + mState
    282                         + " while pressing the right stopwatch button");
    283                 break;
    284         }
    285     }
    286 
    287     @Override
    288     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    289                              Bundle savedInstanceState) {
    290         // Inflate the layout for this fragment
    291         ViewGroup v = (ViewGroup)inflater.inflate(R.layout.stopwatch_fragment, container, false);
    292 
    293         mTime = (CircleTimerView)v.findViewById(R.id.stopwatch_time);
    294         mTimeText = (CountingTimerView)v.findViewById(R.id.stopwatch_time_text);
    295         mLapsList = (ListView)v.findViewById(R.id.laps_list);
    296         mLapsList.setDividerHeight(0);
    297         mLapsAdapter = new LapsListAdapter(getActivity());
    298         mLapsList.setAdapter(mLapsAdapter);
    299 
    300         // Timer text serves as a virtual start/stop button.
    301         mTimeText.registerVirtualButtonAction(new Runnable() {
    302             @Override
    303             public void run() {
    304                 toggleStopwatchState();
    305             }
    306         });
    307         mTimeText.setVirtualButtonEnabled(true);
    308 
    309         mCircleLayout = (CircleButtonsLayout)v.findViewById(R.id.stopwatch_circle);
    310         mCircleLayout.setCircleTimerViewIds(R.id.stopwatch_time, 0 /* stopwatchId */ ,
    311                 0 /* labelId */,  0 /* labeltextId */);
    312 
    313         // Animation setup
    314         mLayoutTransition = new LayoutTransition();
    315         mCircleLayoutTransition = new LayoutTransition();
    316 
    317         // The CircleButtonsLayout only needs to undertake location changes
    318         mCircleLayoutTransition.enableTransitionType(LayoutTransition.CHANGING);
    319         mCircleLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
    320         mCircleLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING);
    321         mCircleLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
    322         mCircleLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
    323         mCircleLayoutTransition.setAnimateParentHierarchy(false);
    324 
    325         // These spacers assist in keeping the size of CircleButtonsLayout constant
    326         mStartSpace = v.findViewById(R.id.start_space);
    327         mEndSpace = v.findViewById(R.id.end_space);
    328         mSpacersUsed = mStartSpace != null || mEndSpace != null;
    329 
    330         // Only applicable on portrait, only visible when there is no lap
    331         mBottomSpace = v.findViewById(R.id.bottom_space);
    332 
    333         // Listener to invoke extra animation within the laps-list
    334         mLayoutTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
    335             @Override
    336             public void startTransition(LayoutTransition transition, ViewGroup container,
    337                                         View view, int transitionType) {
    338                 if (view == mLapsList) {
    339                     if (transitionType == LayoutTransition.DISAPPEARING) {
    340                         if (DEBUG) LogUtils.v("StopwatchFragment.start laps-list disappearing");
    341                         boolean shiftX = view.getResources().getConfiguration().orientation
    342                                 == Configuration.ORIENTATION_LANDSCAPE;
    343                         int first = mLapsList.getFirstVisiblePosition();
    344                         int last = mLapsList.getLastVisiblePosition();
    345                         // Ensure index range will not cause a divide by zero
    346                         if (last < first) {
    347                             last = first;
    348                         }
    349                         long duration = transition.getDuration(LayoutTransition.DISAPPEARING);
    350                         long offset = duration / (last - first + 1) / 5;
    351                         for (int visibleIndex = first; visibleIndex <= last; visibleIndex++) {
    352                             View lapView = mLapsList.getChildAt(visibleIndex - first);
    353                             if (lapView != null) {
    354                                 float toXValue = shiftX ? 1.0f * (visibleIndex - first + 1) : 0;
    355                                 float toYValue = shiftX ? 0 : 4.0f * (visibleIndex - first + 1);
    356                                         TranslateAnimation animation = new TranslateAnimation(
    357                                         Animation.RELATIVE_TO_SELF, 0,
    358                                         Animation.RELATIVE_TO_SELF, toXValue,
    359                                         Animation.RELATIVE_TO_SELF, 0,
    360                                         Animation.RELATIVE_TO_SELF, toYValue);
    361                                 animation.setStartOffset((last - visibleIndex) * offset);
    362                                 animation.setDuration(duration);
    363                                 lapView.startAnimation(animation);
    364                             }
    365                         }
    366                     }
    367                 }
    368             }
    369 
    370             @Override
    371             public void endTransition(LayoutTransition transition, ViewGroup container,
    372                                       View view, int transitionType) {
    373                 if (transitionType == LayoutTransition.DISAPPEARING) {
    374                     if (DEBUG) LogUtils.v("StopwatchFragment.end laps-list disappearing");
    375                     int last = mLapsList.getLastVisiblePosition();
    376                     for (int visibleIndex = mLapsList.getFirstVisiblePosition();
    377                          visibleIndex <= last; visibleIndex++) {
    378                         View lapView = mLapsList.getChildAt(visibleIndex);
    379                         if (lapView != null) {
    380                             Animation animation = lapView.getAnimation();
    381                             if (animation != null) {
    382                                 animation.cancel();
    383                             }
    384                         }
    385                     }
    386                 }
    387             }
    388         });
    389 
    390         return v;
    391     }
    392 
    393     /**
    394      * Make the final display setup.
    395      *
    396      * If the fragment is starting with an existing list of laps, shows the laps list and if the
    397      * spacers around the clock exist, hide them. If there are not laps at the start, hide the laps
    398      * list and show the clock spacers if they exist.
    399      */
    400     @Override
    401     public void onStart() {
    402         super.onStart();
    403 
    404         boolean lapsVisible = mLapsAdapter.getCount() > 0;
    405 
    406         mLapsList.setVisibility(lapsVisible ? View.VISIBLE : View.GONE);
    407         if (mSpacersUsed) {
    408             showSpacerVisibility(lapsVisible);
    409         }
    410         showBottomSpacerVisibility(lapsVisible);
    411 
    412         ((ViewGroup)getView()).setLayoutTransition(mLayoutTransition);
    413         mCircleLayout.setLayoutTransition(mCircleLayoutTransition);
    414     }
    415 
    416     @Override
    417     public void onResume() {
    418         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
    419         prefs.registerOnSharedPreferenceChangeListener(this);
    420         readFromSharedPref(prefs);
    421         mTime.readFromSharedPref(prefs, "sw");
    422         mTime.postInvalidate();
    423 
    424         setFabAppearance();
    425         setLeftRightButtonAppearance();
    426         mTimeText.setTime(mAccumulatedTime, true, true);
    427         if (mState == Stopwatches.STOPWATCH_RUNNING) {
    428             acquireWakeLock();
    429             startUpdateThread();
    430         } else if (mState == Stopwatches.STOPWATCH_STOPPED && mAccumulatedTime != 0) {
    431             mTimeText.blinkTimeStr(true);
    432         }
    433         showLaps();
    434         ((DeskClock)getActivity()).registerPageChangedListener(this);
    435         // View was hidden in onPause, make sure it is visible now.
    436         View v = getView();
    437         if (v != null) {
    438             v.setVisibility(View.VISIBLE);
    439         }
    440         super.onResume();
    441     }
    442 
    443     @Override
    444     public void onPause() {
    445         if (mState == Stopwatches.STOPWATCH_RUNNING) {
    446             stopUpdateThread();
    447 
    448             // This is called because the lock screen was activated, the window stay
    449             // active under it and when we unlock the screen, we see the old time for
    450             // a fraction of a second.
    451             View v = getView();
    452             if (v != null) {
    453                 v.setVisibility(View.INVISIBLE);
    454             }
    455         }
    456         // The stopwatch must keep running even if the user closes the app so save stopwatch state
    457         // in shared prefs
    458         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
    459         prefs.unregisterOnSharedPreferenceChangeListener(this);
    460         writeToSharedPref(prefs);
    461         mTime.writeToSharedPref(prefs, "sw");
    462         mTimeText.blinkTimeStr(false);
    463         ((DeskClock)getActivity()).unregisterPageChangedListener(this);
    464         releaseWakeLock();
    465         super.onPause();
    466     }
    467 
    468     @Override
    469     public void onPageChanged(int page) {
    470         if (page == DeskClock.STOPWATCH_TAB_INDEX && mState == Stopwatches.STOPWATCH_RUNNING) {
    471             acquireWakeLock();
    472         } else {
    473             releaseWakeLock();
    474         }
    475     }
    476 
    477     private void doStop() {
    478         if (DEBUG) LogUtils.v("StopwatchFragment.doStop");
    479         stopUpdateThread();
    480         mTime.pauseIntervalAnimation();
    481         mTimeText.setTime(mAccumulatedTime, true, true);
    482         mTimeText.blinkTimeStr(true);
    483         updateCurrentLap(mAccumulatedTime);
    484         mState = Stopwatches.STOPWATCH_STOPPED;
    485         setFabAppearance();
    486         setLeftRightButtonAppearance();
    487     }
    488 
    489     private void doStart(long time) {
    490         if (DEBUG) LogUtils.v("StopwatchFragment.doStart");
    491         mStartTime = time;
    492         startUpdateThread();
    493         mTimeText.blinkTimeStr(false);
    494         if (mTime.isAnimating()) {
    495             mTime.startIntervalAnimation();
    496         }
    497         mState = Stopwatches.STOPWATCH_RUNNING;
    498         setFabAppearance();
    499         setLeftRightButtonAppearance();
    500     }
    501 
    502     private void doLap() {
    503         if (DEBUG) LogUtils.v("StopwatchFragment.doLap");
    504         showLaps();
    505         setFabAppearance();
    506         setLeftRightButtonAppearance();
    507     }
    508 
    509     private void doReset() {
    510         if (DEBUG) LogUtils.v("StopwatchFragment.doReset");
    511         SharedPreferences prefs =
    512                 PreferenceManager.getDefaultSharedPreferences(getActivity());
    513         Utils.clearSwSharedPref(prefs);
    514         mTime.clearSharedPref(prefs, "sw");
    515         mAccumulatedTime = 0;
    516         mLapsAdapter.clearLaps();
    517         showLaps();
    518         mTime.stopIntervalAnimation();
    519         mTime.reset();
    520         mTimeText.setTime(mAccumulatedTime, true, true);
    521         mTimeText.blinkTimeStr(false);
    522         mState = Stopwatches.STOPWATCH_RESET;
    523         setFabAppearance();
    524         setLeftRightButtonAppearance();
    525     }
    526 
    527     private void shareResults() {
    528         final Context context = getActivity();
    529         final Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
    530         shareIntent.setType("text/plain");
    531         shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    532         shareIntent.putExtra(Intent.EXTRA_SUBJECT,
    533                 Stopwatches.getShareTitle(context.getApplicationContext()));
    534         shareIntent.putExtra(Intent.EXTRA_TEXT, Stopwatches.buildShareResults(
    535                 getActivity().getApplicationContext(), mTimeText.getTimeString(),
    536                 getLapShareTimes(mLapsAdapter.getLapTimes())));
    537 
    538         final Intent launchIntent = Intent.createChooser(shareIntent,
    539                 context.getString(R.string.sw_share_button));
    540         try {
    541             context.startActivity(launchIntent);
    542         } catch (ActivityNotFoundException e) {
    543             LogUtils.e("No compatible receiver is found");
    544         }
    545     }
    546 
    547     /** Turn laps as they would be saved in prefs into format for sharing. **/
    548     private long[] getLapShareTimes(long[] input) {
    549         if (input == null) {
    550             return null;
    551         }
    552 
    553         int numLaps = input.length;
    554         long[] output = new long[numLaps];
    555         long prevLapElapsedTime = 0;
    556         for (int lap_i = numLaps - 1; lap_i >= 0; lap_i--) {
    557             long lap = input[lap_i];
    558             LogUtils.v("lap " + lap_i + ": " + lap);
    559             output[lap_i] = lap - prevLapElapsedTime;
    560             prevLapElapsedTime = lap;
    561         }
    562         return output;
    563     }
    564 
    565     private boolean reachedMaxLaps() {
    566         return mLapsAdapter.getCount() >= Stopwatches.MAX_LAPS;
    567     }
    568 
    569     /***
    570      * Handle action when user presses the lap button
    571      * @param time - in hundredth of a second
    572      */
    573     private void addLapTime(long time) {
    574         // The total elapsed time
    575         final long curTime = time - mStartTime + mAccumulatedTime;
    576         int size = mLapsAdapter.getCount();
    577         if (size == 0) {
    578             // Create and add the first lap
    579             Lap firstLap = new Lap(curTime, curTime);
    580             mLapsAdapter.addLap(firstLap);
    581             // Create the first active lap
    582             mLapsAdapter.addLap(new Lap(0, curTime));
    583             // Update the interval on the clock and check the lap and total time formatting
    584             mTime.setIntervalTime(curTime);
    585             mLapsAdapter.updateTimeFormats(firstLap);
    586         } else {
    587             // Finish active lap
    588             final long lapTime = curTime - mLapsAdapter.getItem(1).mTotalTime;
    589             mLapsAdapter.getItem(0).mLapTime = lapTime;
    590             mLapsAdapter.getItem(0).mTotalTime = curTime;
    591             // Create a new active lap
    592             mLapsAdapter.addLap(new Lap(0, curTime));
    593             // Update marker on clock and check that formatting for the lap number
    594             mTime.setMarkerTime(lapTime);
    595             mLapsAdapter.updateLapFormat();
    596         }
    597         // Repaint the laps list
    598         mLapsAdapter.notifyDataSetChanged();
    599 
    600         // Start lap animation starting from the second lap
    601         mTime.stopIntervalAnimation();
    602         if (!reachedMaxLaps()) {
    603             mTime.startIntervalAnimation();
    604         }
    605     }
    606 
    607     private void updateCurrentLap(long totalTime) {
    608         // There are either 0, 2 or more Laps in the list See {@link #addLapTime}
    609         if (mLapsAdapter.getCount() > 0) {
    610             Lap curLap = mLapsAdapter.getItem(0);
    611             curLap.mLapTime = totalTime - mLapsAdapter.getItem(1).mTotalTime;
    612             curLap.mTotalTime = totalTime;
    613             // If this lap has caused a change in the format for total and/or lap time, all of
    614             // the rows need a fresh print. The simplest way to refresh all of the rows is
    615             // calling notifyDataSetChanged.
    616             if (mLapsAdapter.updateTimeFormats(curLap)) {
    617                 mLapsAdapter.notifyDataSetChanged();
    618             } else {
    619                 curLap.updateView();
    620             }
    621         }
    622     }
    623 
    624     /**
    625      * Show or hide the laps-list
    626      */
    627     private void showLaps() {
    628         if (DEBUG) LogUtils.v(String.format("StopwatchFragment.showLaps: count=%d",
    629                 mLapsAdapter.getCount()));
    630 
    631         boolean lapsVisible = mLapsAdapter.getCount() > 0;
    632 
    633         // Layout change animations will start upon the first add/hide view. Temporarily disable
    634         // the layout transition animation for the spacers, make the changes, then re-enable
    635         // the animation for the add/hide laps-list
    636         if (mSpacersUsed) {
    637             ViewGroup rootView = (ViewGroup) getView();
    638             if (rootView != null) {
    639                 rootView.setLayoutTransition(null);
    640 
    641                 showSpacerVisibility(lapsVisible);
    642 
    643                 rootView.setLayoutTransition(mLayoutTransition);
    644             }
    645         }
    646 
    647         showBottomSpacerVisibility(lapsVisible);
    648 
    649         if (lapsVisible) {
    650             // There are laps - show the laps-list
    651             // No delay for the CircleButtonsLayout changes - start immediately so that the
    652             // circle has shifted before the laps-list starts appearing.
    653             mCircleLayoutTransition.setStartDelay(LayoutTransition.CHANGING, 0);
    654 
    655             mLapsList.setVisibility(View.VISIBLE);
    656         } else {
    657             // There are no laps - hide the laps list
    658 
    659             // Delay the CircleButtonsLayout animation until after the laps-list disappears
    660             long startDelay = mLayoutTransition.getStartDelay(LayoutTransition.DISAPPEARING) +
    661                     mLayoutTransition.getDuration(LayoutTransition.DISAPPEARING);
    662             mCircleLayoutTransition.setStartDelay(LayoutTransition.CHANGING, startDelay);
    663             mLapsList.setVisibility(View.GONE);
    664         }
    665     }
    666 
    667     private void showSpacerVisibility(boolean lapsVisible) {
    668         final int spacersVisibility = lapsVisible ? View.GONE : View.VISIBLE;
    669         if (mStartSpace != null) {
    670             mStartSpace.setVisibility(spacersVisibility);
    671         }
    672         if (mEndSpace != null) {
    673             mEndSpace.setVisibility(spacersVisibility);
    674         }
    675     }
    676 
    677     private void showBottomSpacerVisibility(boolean lapsVisible) {
    678         if (mBottomSpace != null) {
    679             mBottomSpace.setVisibility(lapsVisible ? View.GONE : View.VISIBLE);
    680         }
    681     }
    682 
    683     private void startUpdateThread() {
    684         mTime.post(mTimeUpdateThread);
    685     }
    686 
    687     private void stopUpdateThread() {
    688         mTime.removeCallbacks(mTimeUpdateThread);
    689     }
    690 
    691     Runnable mTimeUpdateThread = new Runnable() {
    692         @Override
    693         public void run() {
    694             long curTime = Utils.getTimeNow();
    695             long totalTime = mAccumulatedTime + (curTime - mStartTime);
    696             if (mTime != null) {
    697                 mTimeText.setTime(totalTime, true, true);
    698             }
    699             if (mLapsAdapter.getCount() > 0) {
    700                 updateCurrentLap(totalTime);
    701             }
    702             mTime.postDelayed(mTimeUpdateThread, STOPWATCH_REFRESH_INTERVAL_MILLIS);
    703         }
    704     };
    705 
    706     private void writeToSharedPref(SharedPreferences prefs) {
    707         SharedPreferences.Editor editor = prefs.edit();
    708         editor.putLong (Stopwatches.PREF_START_TIME, mStartTime);
    709         editor.putLong (Stopwatches.PREF_ACCUM_TIME, mAccumulatedTime);
    710         editor.putInt (Stopwatches.PREF_STATE, mState);
    711         if (mLapsAdapter != null) {
    712             long [] laps = mLapsAdapter.getLapTimes();
    713             if (laps != null) {
    714                 editor.putInt (Stopwatches.PREF_LAP_NUM, laps.length);
    715                 for (int i = 0; i < laps.length; i++) {
    716                     String key = Stopwatches.PREF_LAP_TIME + Integer.toString(laps.length - i);
    717                     editor.putLong (key, laps[i]);
    718                 }
    719             }
    720         }
    721         if (mState == Stopwatches.STOPWATCH_RUNNING) {
    722             editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, mStartTime-mAccumulatedTime);
    723             editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1);
    724             editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, true);
    725         } else if (mState == Stopwatches.STOPWATCH_STOPPED) {
    726             editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, mAccumulatedTime);
    727             editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, -1);
    728             editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
    729         } else if (mState == Stopwatches.STOPWATCH_RESET) {
    730             editor.remove(Stopwatches.NOTIF_CLOCK_BASE);
    731             editor.remove(Stopwatches.NOTIF_CLOCK_RUNNING);
    732             editor.remove(Stopwatches.NOTIF_CLOCK_ELAPSED);
    733         }
    734         editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, false);
    735         editor.apply();
    736     }
    737 
    738     private void readFromSharedPref(SharedPreferences prefs) {
    739         mStartTime = prefs.getLong(Stopwatches.PREF_START_TIME, 0);
    740         mAccumulatedTime = prefs.getLong(Stopwatches.PREF_ACCUM_TIME, 0);
    741         mState = prefs.getInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RESET);
    742         int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
    743         if (mLapsAdapter != null) {
    744             long[] oldLaps = mLapsAdapter.getLapTimes();
    745             if (oldLaps == null || oldLaps.length < numLaps) {
    746                 long[] laps = new long[numLaps];
    747                 long prevLapElapsedTime = 0;
    748                 for (int lap_i = 0; lap_i < numLaps; lap_i++) {
    749                     String key = Stopwatches.PREF_LAP_TIME + Integer.toString(lap_i + 1);
    750                     long lap = prefs.getLong(key, 0);
    751                     laps[numLaps - lap_i - 1] = lap - prevLapElapsedTime;
    752                     prevLapElapsedTime = lap;
    753                 }
    754                 mLapsAdapter.setLapTimes(laps);
    755             }
    756         }
    757         if (prefs.getBoolean(Stopwatches.PREF_UPDATE_CIRCLE, true)) {
    758             if (mState == Stopwatches.STOPWATCH_STOPPED) {
    759                 doStop();
    760             } else if (mState == Stopwatches.STOPWATCH_RUNNING) {
    761                 doStart(mStartTime);
    762             } else if (mState == Stopwatches.STOPWATCH_RESET) {
    763                 doReset();
    764             }
    765         }
    766     }
    767 
    768     @Override
    769     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    770         if (prefs.equals(PreferenceManager.getDefaultSharedPreferences(getActivity()))) {
    771             if (! (key.equals(Stopwatches.PREF_LAP_NUM) ||
    772                     key.startsWith(Stopwatches.PREF_LAP_TIME))) {
    773                 readFromSharedPref(prefs);
    774                 if (prefs.getBoolean(Stopwatches.PREF_UPDATE_CIRCLE, true)) {
    775                     mTime.readFromSharedPref(prefs, "sw");
    776                 }
    777             }
    778         }
    779     }
    780 
    781     // Used to keeps screen on when stopwatch is running.
    782 
    783     private void acquireWakeLock() {
    784         if (mWakeLock == null) {
    785             final PowerManager pm =
    786                     (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
    787             mWakeLock = pm.newWakeLock(
    788                     PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG);
    789             mWakeLock.setReferenceCounted(false);
    790         }
    791         mWakeLock.acquire();
    792     }
    793 
    794     private void releaseWakeLock() {
    795         if (mWakeLock != null && mWakeLock.isHeld()) {
    796             mWakeLock.release();
    797         }
    798     }
    799 
    800     @Override
    801     public void onFabClick(View view){
    802         toggleStopwatchState();
    803     }
    804 
    805     @Override
    806     public void onLeftButtonClick(View view) {
    807         final long time = Utils.getTimeNow();
    808         final Context context = getActivity().getApplicationContext();
    809         final Intent intent = new Intent(context, StopwatchService.class);
    810         intent.putExtra(Stopwatches.MESSAGE_TIME, time);
    811         intent.putExtra(Stopwatches.SHOW_NOTIF, false);
    812         switch (mState) {
    813             case Stopwatches.STOPWATCH_RUNNING:
    814                 // Save lap time
    815                 addLapTime(time);
    816                 doLap();
    817                 intent.setAction(Stopwatches.LAP_STOPWATCH);
    818                 context.startService(intent);
    819                 break;
    820             case Stopwatches.STOPWATCH_STOPPED:
    821                 // do reset
    822                 doReset();
    823                 intent.setAction(Stopwatches.RESET_STOPWATCH);
    824                 context.startService(intent);
    825                 releaseWakeLock();
    826                 break;
    827             default:
    828                 // Happens in monkey tests
    829                 LogUtils.i("Illegal state " + mState + " while pressing the left stopwatch button");
    830                 break;
    831         }
    832     }
    833 
    834     @Override
    835     public void onRightButtonClick(View view) {
    836         shareResults();
    837     }
    838 
    839     @Override
    840     public void setFabAppearance() {
    841         final DeskClock activity = (DeskClock) getActivity();
    842         if (mFab == null || activity.getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
    843             return;
    844         }
    845         if (mState == Stopwatches.STOPWATCH_RUNNING) {
    846             mFab.setImageResource(R.drawable.ic_fab_pause);
    847             mFab.setContentDescription(getString(R.string.sw_stop_button));
    848         } else {
    849             mFab.setImageResource(R.drawable.ic_fab_play);
    850             mFab.setContentDescription(getString(R.string.sw_start_button));
    851         }
    852         mFab.setVisibility(View.VISIBLE);
    853     }
    854 
    855     @Override
    856     public void setLeftRightButtonAppearance() {
    857         final DeskClock activity = (DeskClock) getActivity();
    858         if (mLeftButton == null || mRightButton == null ||
    859                 activity.getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
    860             return;
    861         }
    862         mRightButton.setImageResource(R.drawable.ic_share);
    863         mRightButton.setContentDescription(getString(R.string.sw_share_button));
    864 
    865         switch (mState) {
    866             case Stopwatches.STOPWATCH_RESET:
    867                 mLeftButton.setImageResource(R.drawable.ic_lap);
    868                 mLeftButton.setContentDescription(getString(R.string.sw_lap_button));
    869                 mLeftButton.setEnabled(false);
    870                 mLeftButton.setVisibility(View.INVISIBLE);
    871                 mRightButton.setVisibility(View.INVISIBLE);
    872                 break;
    873             case Stopwatches.STOPWATCH_RUNNING:
    874                 mLeftButton.setImageResource(R.drawable.ic_lap);
    875                 mLeftButton.setContentDescription(getString(R.string.sw_lap_button));
    876                 mLeftButton.setEnabled(!reachedMaxLaps());
    877                 mLeftButton.setVisibility(View.VISIBLE);
    878                 mRightButton.setVisibility(View.INVISIBLE);
    879                 break;
    880             case Stopwatches.STOPWATCH_STOPPED:
    881                 mLeftButton.setImageResource(R.drawable.ic_reset);
    882                 mLeftButton.setContentDescription(getString(R.string.sw_reset_button));
    883                 mLeftButton.setEnabled(true);
    884                 mLeftButton.setVisibility(View.VISIBLE);
    885                 mRightButton.setVisibility(View.VISIBLE);
    886                 break;
    887         }
    888     }
    889 }
    890