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