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