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 
Lap(long time, long total)66         Lap (long time, long total) {
67             mLapTime = time;
68             mTotalTime = total;
69         }
70         public long mLapTime;
71         public long mTotalTime;
72 
updateView()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 
LapsListAdapter(Context context)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
getItemId(int position)112         public long getItemId(int position) {
113             return position;
114         }
115 
116         @Override
getItemViewType(int position)117         public int getItemViewType(int position) {
118             return position < mLaps.size() ? VIEW_TYPE_LAP : VIEW_TYPE_SPACE;
119         }
120 
121         @Override
getViewTypeCount()122         public int getViewTypeCount() {
123             return VIEW_TYPE_COUNT;
124         }
125 
126         @Override
getView(int position, View convertView, ViewGroup parent)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 
setTimeText(View lapInfo, Lap lap)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
getCount()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
getItem(int position)164         public Lap getItem(int position) {
165             if (position >= mLaps.size()) {
166                 return null;
167             }
168             return mLaps.get(position);
169         }
170 
updateLapFormat()171         private void updateLapFormat() {
172             // Note Stopwatches.MAX_LAPS < 100
173             mLapFormat = mLapFormatSet[mLaps.size() < 10 ? 0 : 1];
174         }
175 
resetTimeFormats()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          */
updateTimeFormats(Lap lap)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 
addLap(Lap l)203         public void addLap(Lap l) {
204             mLaps.add(0, l);
205             // for efficiency caller also calls notifyDataSetChanged()
206         }
207 
clearLaps()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
getLapTimes()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
setLapTimes(long [] laps)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 
StopwatchFragment()253     public StopwatchFragment() {
254     }
255 
toggleStopwatchState()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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onStart()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
onResume()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
onPause()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
onPageChanged(int page)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 
doStop()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 
doStart(long time)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 
doLap()502     private void doLap() {
503         if (DEBUG) LogUtils.v("StopwatchFragment.doLap");
504         showLaps();
505         setFabAppearance();
506         setLeftRightButtonAppearance();
507     }
508 
doReset()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 
shareResults()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. **/
getLapShareTimes(long[] input)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 
reachedMaxLaps()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      */
addLapTime(long time)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 
updateCurrentLap(long totalTime)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      */
showLaps()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 
showSpacerVisibility(boolean lapsVisible)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 
showBottomSpacerVisibility(boolean lapsVisible)677     private void showBottomSpacerVisibility(boolean lapsVisible) {
678         if (mBottomSpace != null) {
679             mBottomSpace.setVisibility(lapsVisible ? View.GONE : View.VISIBLE);
680         }
681     }
682 
startUpdateThread()683     private void startUpdateThread() {
684         mTime.post(mTimeUpdateThread);
685     }
686 
stopUpdateThread()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 
writeToSharedPref(SharedPreferences prefs)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 
readFromSharedPref(SharedPreferences prefs)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
onSharedPreferenceChanged(SharedPreferences prefs, String key)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 
acquireWakeLock()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 
releaseWakeLock()794     private void releaseWakeLock() {
795         if (mWakeLock != null && mWakeLock.isHeld()) {
796             mWakeLock.release();
797         }
798     }
799 
800     @Override
onFabClick(View view)801     public void onFabClick(View view){
802         toggleStopwatchState();
803     }
804 
805     @Override
onLeftButtonClick(View view)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
onRightButtonClick(View view)835     public void onRightButtonClick(View view) {
836         shareResults();
837     }
838 
839     @Override
setFabAppearance()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
setLeftRightButtonAppearance()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