1package com.android.deskclock.stopwatch;
2
3import android.animation.LayoutTransition;
4import android.content.ActivityNotFoundException;
5import android.content.Context;
6import android.content.Intent;
7import android.content.SharedPreferences;
8import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
9import android.content.res.Configuration;
10import android.os.Bundle;
11import android.os.PowerManager;
12import android.os.PowerManager.WakeLock;
13import android.preference.PreferenceManager;
14import android.text.format.DateUtils;
15import android.view.LayoutInflater;
16import android.view.View;
17import android.view.ViewGroup;
18import android.view.animation.Animation;
19import android.view.animation.TranslateAnimation;
20import android.widget.BaseAdapter;
21import android.widget.ListPopupWindow;
22import android.widget.ListView;
23import android.widget.TextView;
24
25import com.android.deskclock.CircleButtonsLayout;
26import com.android.deskclock.CircleTimerView;
27import com.android.deskclock.DeskClock;
28import com.android.deskclock.DeskClockFragment;
29import com.android.deskclock.LogUtils;
30import com.android.deskclock.R;
31import com.android.deskclock.Utils;
32import com.android.deskclock.timer.CountingTimerView;
33
34import java.util.ArrayList;
35
36public 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