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