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