• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Lap(long time, long total)67         Lap (long time, long total) {
68             mLapTime = time;
69             mTotalTime = total;
70         }
71         public long mLapTime;
72         public long mTotalTime;
73 
updateView()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 
LapsListAdapter(Context context)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
getItemId(int position)109         public long getItemId(int position) {
110             return position;
111         }
112 
113         @Override
getView(int position, View convertView, ViewGroup parent)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 
setTimeText(View lapInfo, Lap lap)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
getCount()141         public int getCount() {
142             return mLaps.size();
143         }
144 
145         @Override
getItem(int position)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 
updateLapFormat()153         private void updateLapFormat() {
154             // Note Stopwatches.MAX_LAPS < 100
155             mLapFormat = mLapFormatSet[mLaps.size() < 10 ? 0 : 1];
156         }
157 
resetTimeFormats()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          */
updateTimeFormats(Lap lap)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 
addLap(Lap l)185         public void addLap(Lap l) {
186             mLaps.add(0, l);
187             // for efficiency caller also calls notifyDataSetChanged()
188         }
189 
clearLaps()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
getLapTimes()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
setLapTimes(long [] laps)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 
StopwatchFragment()235     public StopwatchFragment() {
236     }
237 
rightButtonAction()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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onStart()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
onResume()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
onPause()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
onPageChanged(int page)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 
doStop()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 
doStart(long time)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 
doLap()477     private void doLap() {
478         if (DEBUG) LogUtils.v("StopwatchFragment.doLap");
479         showLaps();
480         setFabAppearance();
481         setLeftRightButtonAppearance();
482     }
483 
doReset()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 
shareResults()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. **/
getLapShareTimes(long[] input)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 
reachedMaxLaps()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      */
addLapTime(long time)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 
updateCurrentLap(long totalTime)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      */
showLaps()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 
startUpdateThread()644     private void startUpdateThread() {
645         mTime.post(mTimeUpdateThread);
646     }
647 
stopUpdateThread()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 
writeToSharedPref(SharedPreferences prefs)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 
readFromSharedPref(SharedPreferences prefs)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
onSharedPreferenceChanged(SharedPreferences prefs, String key)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 
acquireWakeLock()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 
releaseWakeLock()755     private void releaseWakeLock() {
756         if (mWakeLock != null && mWakeLock.isHeld()) {
757             mWakeLock.release();
758         }
759     }
760 
761     @Override
onFabClick(View view)762     public void onFabClick(View view){
763         rightButtonAction();
764     }
765 
766     @Override
onLeftButtonClick(View view)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
onRightButtonClick(View view)796     public void onRightButtonClick(View view) {
797         shareResults();
798     }
799 
800     @Override
setFabAppearance()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
setLeftRightButtonAppearance()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