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 17 package com.android.deskclock.stopwatch; 18 19 import android.content.ActivityNotFoundException; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.os.Bundle; 23 import android.os.PowerManager; 24 import android.os.SystemClock; 25 import android.support.v7.widget.LinearLayoutManager; 26 import android.support.v7.widget.RecyclerView; 27 import android.support.v7.widget.SimpleItemAnimator; 28 import android.transition.AutoTransition; 29 import android.transition.Transition; 30 import android.transition.TransitionManager; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.accessibility.AccessibilityManager; 35 36 import com.android.deskclock.DeskClock; 37 import com.android.deskclock.DeskClockFragment; 38 import com.android.deskclock.LogUtils; 39 import com.android.deskclock.R; 40 import com.android.deskclock.data.DataModel; 41 import com.android.deskclock.data.Lap; 42 import com.android.deskclock.data.Stopwatch; 43 import com.android.deskclock.events.Events; 44 import com.android.deskclock.timer.CountingTimerView; 45 46 import static android.content.Context.ACCESSIBILITY_SERVICE; 47 import static android.content.Context.POWER_SERVICE; 48 import static android.os.PowerManager.ON_AFTER_RELEASE; 49 import static android.os.PowerManager.SCREEN_BRIGHT_WAKE_LOCK; 50 import static android.view.View.GONE; 51 import static android.view.View.INVISIBLE; 52 import static android.view.View.VISIBLE; 53 54 /** 55 * Fragment that shows the stopwatch and recorded laps. 56 */ 57 public final class StopwatchFragment extends DeskClockFragment { 58 59 private static final String TAG = "StopwatchFragment"; 60 61 /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */ 62 private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable(); 63 64 /** Used to determine when talk back is on in order to lower the time update rate. */ 65 private AccessibilityManager mAccessibilityManager; 66 67 /** {@code true} while the {@link #mLapsList} is transitioning between shown and hidden. */ 68 private boolean mLapsListIsTransitioning; 69 70 /** The data source for {@link #mLapsList}. */ 71 private LapsAdapter mLapsAdapter; 72 73 /** The layout manager for the {@link #mLapsAdapter}. */ 74 private LinearLayoutManager mLapsLayoutManager; 75 76 /** Draws the reference lap while the stopwatch is running. */ 77 private StopwatchCircleView mTime; 78 79 /** Displays the recorded lap times. */ 80 private RecyclerView mLapsList; 81 82 /** Displays the current stopwatch time. */ 83 private CountingTimerView mTimeText; 84 85 /** Held while the stopwatch is running and this fragment is forward to keep the screen on. */ 86 private PowerManager.WakeLock mWakeLock; 87 88 /** The public no-arg constructor required by all fragments. */ StopwatchFragment()89 public StopwatchFragment() {} 90 91 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state)92 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { 93 mLapsAdapter = new LapsAdapter(getActivity()); 94 mLapsLayoutManager = new LinearLayoutManager(getActivity()); 95 96 final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false); 97 mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_time); 98 mLapsList = (RecyclerView) v.findViewById(R.id.laps_list); 99 ((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false); 100 mLapsList.setLayoutManager(mLapsLayoutManager); 101 mLapsList.setAdapter(mLapsAdapter); 102 103 // Timer text serves as a virtual start/stop button. 104 mTimeText = (CountingTimerView) v.findViewById(R.id.stopwatch_time_text); 105 mTimeText.setVirtualButtonEnabled(true); 106 mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable()); 107 108 return v; 109 } 110 111 @Override onActivityCreated(Bundle savedInstanceState)112 public void onActivityCreated(Bundle savedInstanceState) { 113 super.onActivityCreated(savedInstanceState); 114 115 mAccessibilityManager = 116 (AccessibilityManager) getActivity().getSystemService(ACCESSIBILITY_SERVICE); 117 } 118 119 @Override onResume()120 public void onResume() { 121 super.onResume(); 122 123 // Conservatively assume the data in the adapter has changed while the fragment was paused. 124 mLapsAdapter.notifyDataSetChanged(); 125 126 // Update the state of the buttons. 127 setFabAppearance(); 128 setLeftRightButtonAppearance(); 129 130 // Draw the current stopwatch and lap times. 131 updateTime(); 132 133 // Start updates if the stopwatch is running; blink text if it is paused. 134 switch (getStopwatch().getState()) { 135 case RUNNING: 136 acquireWakeLock(); 137 mTime.update(); 138 startUpdatingTime(); 139 break; 140 case PAUSED: 141 mTimeText.blinkTimeStr(true); 142 break; 143 } 144 145 // Adjust the visibility of the list of laps. 146 showOrHideLaps(false); 147 148 // Start watching for page changes away from this fragment. 149 getDeskClock().registerPageChangedListener(this); 150 151 // View is hidden in onPause, make sure it is visible now. 152 final View view = getView(); 153 if (view != null) { 154 view.setVisibility(VISIBLE); 155 } 156 } 157 158 @Override onPause()159 public void onPause() { 160 super.onPause(); 161 162 final View view = getView(); 163 if (view != null) { 164 // Make the view invisible because when the lock screen is activated, the window stays 165 // active under it. Later, when unlocking the screen, we see the old stopwatch time for 166 // a fraction of a second. 167 getView().setVisibility(INVISIBLE); 168 } 169 170 // Stop all updates while the fragment is not visible. 171 stopUpdatingTime(); 172 mTimeText.blinkTimeStr(false); 173 174 // Stop watching for page changes away from this fragment. 175 getDeskClock().unregisterPageChangedListener(this); 176 177 // Release the wake lock if it is currently held. 178 releaseWakeLock(); 179 } 180 181 @Override onPageChanged(int page)182 public void onPageChanged(int page) { 183 if (page == DeskClock.STOPWATCH_TAB_INDEX && getStopwatch().isRunning()) { 184 acquireWakeLock(); 185 } else { 186 releaseWakeLock(); 187 } 188 } 189 190 @Override onFabClick(View view)191 public void onFabClick(View view) { 192 toggleStopwatchState(); 193 } 194 195 @Override onLeftButtonClick(View view)196 public void onLeftButtonClick(View view) { 197 switch (getStopwatch().getState()) { 198 case RUNNING: 199 doAddLap(); 200 break; 201 case PAUSED: 202 doReset(); 203 break; 204 } 205 } 206 207 @Override onRightButtonClick(View view)208 public void onRightButtonClick(View view) { 209 doShare(); 210 } 211 212 @Override setFabAppearance()213 public void setFabAppearance() { 214 if (mFab == null || getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) { 215 return; 216 } 217 218 if (getStopwatch().isRunning()) { 219 mFab.setImageResource(R.drawable.ic_pause_white_24dp); 220 mFab.setContentDescription(getString(R.string.sw_pause_button)); 221 } else { 222 mFab.setImageResource(R.drawable.ic_start_white_24dp); 223 mFab.setContentDescription(getString(R.string.sw_start_button)); 224 } 225 mFab.setVisibility(VISIBLE); 226 } 227 228 @Override setLeftRightButtonAppearance()229 public void setLeftRightButtonAppearance() { 230 if (mLeftButton == null || mRightButton == null || 231 getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) { 232 return; 233 } 234 235 mRightButton.setImageResource(R.drawable.ic_share); 236 mRightButton.setContentDescription(getString(R.string.sw_share_button)); 237 238 switch (getStopwatch().getState()) { 239 case RESET: 240 mLeftButton.setEnabled(false); 241 mLeftButton.setVisibility(INVISIBLE); 242 mRightButton.setVisibility(INVISIBLE); 243 break; 244 case RUNNING: 245 mLeftButton.setImageResource(R.drawable.ic_lap); 246 mLeftButton.setContentDescription(getString(R.string.sw_lap_button)); 247 mLeftButton.setEnabled(canRecordMoreLaps()); 248 mLeftButton.setVisibility(canRecordMoreLaps() ? VISIBLE : INVISIBLE); 249 mRightButton.setVisibility(INVISIBLE); 250 break; 251 case PAUSED: 252 mLeftButton.setEnabled(true); 253 mLeftButton.setImageResource(R.drawable.ic_reset); 254 mLeftButton.setContentDescription(getString(R.string.sw_reset_button)); 255 mLeftButton.setVisibility(VISIBLE); 256 mRightButton.setVisibility(VISIBLE); 257 break; 258 } 259 } 260 261 /** 262 * Start the stopwatch. 263 */ doStart()264 private void doStart() { 265 Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock); 266 267 // Update the stopwatch state. 268 DataModel.getDataModel().startStopwatch(); 269 270 // Start UI updates. 271 startUpdatingTime(); 272 mTime.update(); 273 mTimeText.blinkTimeStr(false); 274 275 // Update button states. 276 setFabAppearance(); 277 setLeftRightButtonAppearance(); 278 279 // Acquire the wake lock. 280 acquireWakeLock(); 281 } 282 283 /** 284 * Pause the stopwatch. 285 */ doPause()286 private void doPause() { 287 Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock); 288 289 // Update the stopwatch state 290 DataModel.getDataModel().pauseStopwatch(); 291 292 // Redraw the paused stopwatch time. 293 updateTime(); 294 295 // Stop UI updates. 296 stopUpdatingTime(); 297 mTimeText.blinkTimeStr(true); 298 299 // Update button states. 300 setFabAppearance(); 301 setLeftRightButtonAppearance(); 302 303 // Release the wake lock. 304 releaseWakeLock(); 305 } 306 307 /** 308 * Reset the stopwatch. 309 */ doReset()310 private void doReset() { 311 Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock); 312 313 // Update the stopwatch state. 314 DataModel.getDataModel().resetStopwatch(); 315 316 // Clear the laps. 317 showOrHideLaps(true); 318 319 // Clear the times. 320 mTime.postInvalidateOnAnimation(); 321 mTimeText.setTime(0, true, true); 322 mTimeText.blinkTimeStr(false); 323 324 // Update button states. 325 setFabAppearance(); 326 setLeftRightButtonAppearance(); 327 328 // Release the wake lock. 329 releaseWakeLock(); 330 } 331 332 /** 333 * Send stopwatch time and lap times to an external sharing application. 334 */ doShare()335 private void doShare() { 336 final String[] subjects = getResources().getStringArray(R.array.sw_share_strings); 337 final String subject = subjects[(int)(Math.random() * subjects.length)]; 338 final String text = mLapsAdapter.getShareText(); 339 340 final Intent shareIntent = new Intent(Intent.ACTION_SEND) 341 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) 342 .putExtra(Intent.EXTRA_SUBJECT, subject) 343 .putExtra(Intent.EXTRA_TEXT, text) 344 .setType("text/plain"); 345 346 final Context context = getActivity(); 347 final String title = context.getString(R.string.sw_share_button); 348 final Intent shareChooserIntent = Intent.createChooser(shareIntent, title); 349 try { 350 context.startActivity(shareChooserIntent); 351 } catch (ActivityNotFoundException anfe) { 352 LogUtils.e("No compatible receiver is found"); 353 } 354 } 355 356 /** 357 * Record and add a new lap ending now. 358 */ doAddLap()359 private void doAddLap() { 360 Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock); 361 362 // Record a new lap. 363 final Lap lap = mLapsAdapter.addLap(); 364 if (lap == null) { 365 return; 366 } 367 368 // Update button states. 369 setLeftRightButtonAppearance(); 370 371 if (lap.getLapNumber() == 1) { 372 // Child views from prior lap sets hang around and blit to the screen when adding the 373 // first lap of the subsequent lap set. Remove those superfluous children here manually 374 // to ensure they aren't seen as the first lap is drawn. 375 mLapsList.removeAllViewsInLayout(); 376 377 // Start animating the reference lap. 378 mTime.update(); 379 380 // Recording the first lap transitions the UI to display the laps list. 381 showOrHideLaps(false); 382 } 383 384 // Ensure the newly added lap is visible on screen. 385 mLapsList.scrollToPosition(0); 386 } 387 388 /** 389 * Show or hide the list of laps. 390 */ showOrHideLaps(boolean clearLaps)391 private void showOrHideLaps(boolean clearLaps) { 392 final Transition transition = new AutoTransition() 393 .addListener(new Transition.TransitionListener() { 394 @Override 395 public void onTransitionStart(Transition transition) { 396 mLapsListIsTransitioning = true; 397 } 398 399 @Override 400 public void onTransitionEnd(Transition transition) { 401 mLapsListIsTransitioning = false; 402 } 403 404 @Override 405 public void onTransitionCancel(Transition transition) { 406 } 407 408 @Override 409 public void onTransitionPause(Transition transition) { 410 } 411 412 @Override 413 public void onTransitionResume(Transition transition) { 414 } 415 }); 416 417 final ViewGroup sceneRoot = (ViewGroup) getView(); 418 TransitionManager.beginDelayedTransition(sceneRoot, transition); 419 420 if (clearLaps) { 421 mLapsAdapter.clearLaps(); 422 } 423 424 final boolean lapsVisible = mLapsAdapter.getItemCount() > 0; 425 mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE); 426 } 427 acquireWakeLock()428 private void acquireWakeLock() { 429 if (mWakeLock == null) { 430 final PowerManager pm = (PowerManager) getActivity().getSystemService(POWER_SERVICE); 431 mWakeLock = pm.newWakeLock(SCREEN_BRIGHT_WAKE_LOCK | ON_AFTER_RELEASE, TAG); 432 mWakeLock.setReferenceCounted(false); 433 } 434 mWakeLock.acquire(); 435 } 436 releaseWakeLock()437 private void releaseWakeLock() { 438 if (mWakeLock != null && mWakeLock.isHeld()) { 439 mWakeLock.release(); 440 } 441 } 442 443 /** 444 * Either pause or start the stopwatch based on its current state. 445 */ toggleStopwatchState()446 private void toggleStopwatchState() { 447 if (getStopwatch().isRunning()) { 448 doPause(); 449 } else { 450 doStart(); 451 } 452 } 453 getStopwatch()454 private Stopwatch getStopwatch() { 455 return DataModel.getDataModel().getStopwatch(); 456 } 457 canRecordMoreLaps()458 private boolean canRecordMoreLaps() { 459 return DataModel.getDataModel().canAddMoreLaps(); 460 } 461 462 /** 463 * Post the first runnable to update times within the UI. It will reschedule itself as needed. 464 */ startUpdatingTime()465 private void startUpdatingTime() { 466 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 467 stopUpdatingTime(); 468 mTime.post(mTimeUpdateRunnable); 469 } 470 471 /** 472 * Remove the runnable that updates times within the UI. 473 */ stopUpdatingTime()474 private void stopUpdatingTime() { 475 mTime.removeCallbacks(mTimeUpdateRunnable); 476 } 477 478 /** 479 * Update all time displays based on a single snapshot of the stopwatch progress. This includes 480 * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in 481 * the list of laps. 482 */ updateTime()483 private void updateTime() { 484 // Compute the total time of the stopwatch. 485 final long totalTime = getStopwatch().getTotalTime(); 486 487 // Update the total time display. 488 mTimeText.setTime(totalTime, true, true); 489 490 // Update the current lap. 491 final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0; 492 if (!mLapsListIsTransitioning && currentLapIsVisible) { 493 mLapsAdapter.updateCurrentLap(mLapsList, totalTime); 494 } 495 } 496 497 /** 498 * This runnable periodically updates times throughout the UI. It stops these updates when the 499 * stopwatch is no longer running. 500 */ 501 private final class TimeUpdateRunnable implements Runnable { 502 @Override run()503 public void run() { 504 final long startTime = SystemClock.elapsedRealtime(); 505 506 updateTime(); 507 508 if (getStopwatch().isRunning()) { 509 // The stopwatch is still running so execute this runnable again after a delay. 510 final boolean talkBackOn = mAccessibilityManager.isTouchExplorationEnabled(); 511 512 // Grant longer time between redraws when talk-back is on to let it catch up. 513 final int period = talkBackOn ? 500 : 25; 514 515 // Try to maintain a consistent period of time between redraws. 516 final long endTime = SystemClock.elapsedRealtime(); 517 final long delay = Math.max(0, startTime + period - endTime); 518 519 mTime.postDelayed(this, delay); 520 } 521 } 522 } 523 524 /** 525 * Tapping the stopwatch text also toggles the stopwatch state, just like the fab. 526 */ 527 private final class ToggleStopwatchRunnable implements Runnable { 528 @Override run()529 public void run() { 530 toggleStopwatchState(); 531 } 532 } 533 } 534