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