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