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