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