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