1 /* 2 * Copyright (C) 2014 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.timer; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.TimeInterpolator; 24 import android.app.NotificationManager; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 29 import android.content.res.Resources; 30 import android.os.Bundle; 31 import android.preference.PreferenceManager; 32 import android.support.v4.view.ViewPager; 33 import android.text.format.DateUtils; 34 import android.transition.AutoTransition; 35 import android.transition.Transition; 36 import android.transition.TransitionManager; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.View.OnClickListener; 40 import android.view.ViewGroup; 41 import android.view.animation.AccelerateDecelerateInterpolator; 42 import android.view.animation.AccelerateInterpolator; 43 import android.view.animation.DecelerateInterpolator; 44 import android.widget.ImageButton; 45 import android.widget.ImageView; 46 47 import com.android.deskclock.AnimatorUtils; 48 import com.android.deskclock.DeskClock; 49 import com.android.deskclock.DeskClockFragment; 50 import com.android.deskclock.R; 51 import com.android.deskclock.TimerSetupView; 52 import com.android.deskclock.Utils; 53 import com.android.deskclock.VerticalViewPager; 54 55 public class TimerFragment extends DeskClockFragment implements OnSharedPreferenceChangeListener { 56 public static final long ANIMATION_TIME_MILLIS = DateUtils.SECOND_IN_MILLIS / 3; 57 58 private static final String KEY_SETUP_SELECTED = "_setup_selected"; 59 private static final String KEY_ENTRY_STATE = "entry_state"; 60 private static final int PAGINATION_DOTS_COUNT = 4; 61 private static final String CURR_PAGE = "_currPage"; 62 private static final TimeInterpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator(); 63 private static final TimeInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); 64 private static final long ROTATE_ANIM_DURATION_MILIS = 150; 65 66 private boolean mTicking = false; 67 private TimerSetupView mSetupView; 68 private VerticalViewPager mViewPager; 69 private TimerFragmentAdapter mAdapter; 70 private ImageButton mCancel; 71 private ViewGroup mContentView; 72 private View mTimerView; 73 private View mLastView; 74 private ImageView[] mPageIndicators = new ImageView[PAGINATION_DOTS_COUNT]; 75 private Transition mDeleteTransition; 76 private SharedPreferences mPrefs; 77 private Bundle mViewState = null; 78 private NotificationManager mNotificationManager; 79 80 private final ViewPager.OnPageChangeListener mOnPageChangeListener = 81 new ViewPager.SimpleOnPageChangeListener() { 82 @Override 83 public void onPageSelected(int position) { 84 highlightPageIndicator(position); 85 TimerFragment.this.setTimerViewFabIcon(getCurrentTimer()); 86 } 87 }; 88 89 private final Runnable mClockTick = new Runnable() { 90 boolean mVisible = true; 91 final static int TIME_PERIOD_MS = 1000; 92 final static int TIME_DELAY_MS = 20; 93 final static int SPLIT = TIME_PERIOD_MS / 2; 94 95 @Override 96 public void run() { 97 // Setup for blinking 98 final boolean visible = Utils.getTimeNow() % TIME_PERIOD_MS < SPLIT; 99 final boolean toggle = mVisible != visible; 100 mVisible = visible; 101 for (int i = 0; i < mAdapter.getCount(); i++) { 102 final TimerObj t = mAdapter.getTimerAt(i); 103 if (t.mState == TimerObj.STATE_RUNNING || t.mState == TimerObj.STATE_TIMESUP) { 104 final long timeLeft = t.updateTimeLeft(false); 105 if (t.mView != null) { 106 t.mView.setTime(timeLeft, false); 107 // Update button every 1/2 second 108 if (toggle) { 109 final ImageButton addMinuteButton = (ImageButton) 110 t.mView.findViewById(R.id.reset_add); 111 final boolean canAddMinute = TimerObj.MAX_TIMER_LENGTH - t.mTimeLeft 112 > TimerObj.MINUTE_IN_MILLIS; 113 addMinuteButton.setEnabled(canAddMinute); 114 } 115 } 116 } 117 if (t.mTimeLeft <= 0 && t.mState != TimerObj.STATE_DONE 118 && t.mState != TimerObj.STATE_RESTART) { 119 t.mState = TimerObj.STATE_TIMESUP; 120 if (t.mView != null) { 121 t.mView.timesUp(); 122 } 123 } 124 // The blinking 125 if (toggle && t.mView != null) { 126 if (t.mState == TimerObj.STATE_TIMESUP) { 127 t.mView.setCircleBlink(mVisible); 128 } 129 if (t.mState == TimerObj.STATE_STOPPED) { 130 t.mView.setTextBlink(mVisible); 131 } 132 } 133 } 134 mTimerView.postDelayed(mClockTick, TIME_DELAY_MS); 135 } 136 }; 137 138 @Override 139 public void onCreate(Bundle savedInstanceState) { 140 super.onCreate(savedInstanceState); 141 mViewState = savedInstanceState; 142 } 143 144 @Override 145 public View onCreateView(LayoutInflater inflater, ViewGroup container, 146 Bundle savedInstanceState) { 147 final View view = inflater.inflate(R.layout.timer_fragment, container, false); 148 mContentView = (ViewGroup) view; 149 mTimerView = view.findViewById(R.id.timer_view); 150 mSetupView = (TimerSetupView) view.findViewById(R.id.timer_setup); 151 mViewPager = (VerticalViewPager) view.findViewById(R.id.vertical_view_pager); 152 mPageIndicators[0] = (ImageView) view.findViewById(R.id.page_indicator0); 153 mPageIndicators[1] = (ImageView) view.findViewById(R.id.page_indicator1); 154 mPageIndicators[2] = (ImageView) view.findViewById(R.id.page_indicator2); 155 mPageIndicators[3] = (ImageView) view.findViewById(R.id.page_indicator3); 156 mCancel = (ImageButton) view.findViewById(R.id.timer_cancel); 157 mCancel.setOnClickListener(new OnClickListener() { 158 @Override 159 public void onClick(View v) { 160 if (mAdapter.getCount() != 0) { 161 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 162 @Override 163 public void onAnimationEnd(Animator animation) { 164 mSetupView.reset(); // Make sure the setup is cleared for next time 165 mSetupView.setScaleX(1.0f); // Reset the scale for setup view 166 goToPagerView(); 167 } 168 }; 169 createRotateAnimator(adapter, false).start(); 170 } 171 } 172 }); 173 mDeleteTransition = new AutoTransition(); 174 mDeleteTransition.setDuration(ANIMATION_TIME_MILLIS / 2); 175 mDeleteTransition.setInterpolator(new AccelerateDecelerateInterpolator()); 176 177 return view; 178 } 179 180 @Override 181 public void onActivityCreated(Bundle savedInstanceState) { 182 super.onActivityCreated(savedInstanceState); 183 final Context context = getActivity(); 184 mPrefs = PreferenceManager.getDefaultSharedPreferences(context); 185 mNotificationManager = (NotificationManager) context.getSystemService(Context 186 .NOTIFICATION_SERVICE); 187 } 188 189 @Override 190 public void onResume() { 191 super.onResume(); 192 if (getActivity() instanceof DeskClock) { 193 DeskClock activity = (DeskClock) getActivity(); 194 activity.registerPageChangedListener(this); 195 } 196 197 if (mAdapter == null) { 198 mAdapter = new TimerFragmentAdapter(getChildFragmentManager(), mPrefs); 199 } 200 mAdapter.populateTimersFromPref(); 201 mViewPager.setAdapter(mAdapter); 202 mViewPager.setOnPageChangeListener(mOnPageChangeListener); 203 mPrefs.registerOnSharedPreferenceChangeListener(this); 204 205 // Clear the flag set in the notification and alert because the adapter was just 206 // created and is thus in sync with the database 207 final SharedPreferences.Editor editor = mPrefs.edit(); 208 if (mPrefs.getBoolean(Timers.FROM_NOTIFICATION, false)) { 209 editor.putBoolean(Timers.FROM_NOTIFICATION, false); 210 } 211 if (mPrefs.getBoolean(Timers.FROM_ALERT, false)) { 212 editor.putBoolean(Timers.FROM_ALERT, false); 213 } 214 editor.apply(); 215 216 mCancel.setVisibility(mAdapter.getCount() == 0 ? View.INVISIBLE : View.VISIBLE); 217 218 boolean goToSetUpView; 219 // Process extras that were sent to the app and were intended for the timer fragment 220 final Intent newIntent = getActivity().getIntent(); 221 if (newIntent != null && newIntent.getBooleanExtra( 222 TimerFullScreenFragment.GOTO_SETUP_VIEW, false)) { 223 goToSetUpView = true; 224 } else { 225 if (mViewState != null) { 226 final int currPage = mViewState.getInt(CURR_PAGE); 227 mViewPager.setCurrentItem(currPage); 228 highlightPageIndicator(currPage); 229 final boolean hasPreviousInput = mViewState.getBoolean(KEY_SETUP_SELECTED, false); 230 goToSetUpView = hasPreviousInput || mAdapter.getCount() == 0; 231 mSetupView.restoreEntryState(mViewState, KEY_ENTRY_STATE); 232 } else { 233 highlightPageIndicator(0); 234 // If user was not previously using the setup, determine which view to go by count 235 goToSetUpView = mAdapter.getCount() == 0; 236 } 237 } 238 if (goToSetUpView) { 239 goToSetUpView(); 240 } else { 241 goToPagerView(); 242 } 243 } 244 245 @Override 246 public void onPause() { 247 super.onPause(); 248 if (getActivity() instanceof DeskClock) { 249 ((DeskClock) getActivity()).unregisterPageChangedListener(this); 250 } 251 mPrefs.unregisterOnSharedPreferenceChangeListener(this); 252 if (mAdapter != null) { 253 mAdapter.saveTimersToSharedPrefs(); 254 } 255 stopClockTicks(); 256 } 257 258 @Override 259 public void onSaveInstanceState(Bundle outState) { 260 super.onSaveInstanceState(outState); 261 if (mAdapter != null) { 262 mAdapter.saveTimersToSharedPrefs(); 263 } 264 if (mSetupView != null) { 265 outState.putBoolean(KEY_SETUP_SELECTED, mSetupView.getVisibility() == View.VISIBLE); 266 mSetupView.saveEntryState(outState, KEY_ENTRY_STATE); 267 } 268 outState.putInt(CURR_PAGE, mViewPager.getCurrentItem()); 269 mViewState = outState; 270 } 271 272 @Override 273 public void onDestroyView() { 274 super.onDestroyView(); 275 mViewState = null; 276 } 277 278 @Override 279 public void onPageChanged(int page) { 280 if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) { 281 mAdapter.notifyDataSetChanged(); 282 } 283 } 284 285 // Starts the ticks that animate the timers. 286 private void startClockTicks() { 287 mTimerView.postDelayed(mClockTick, 20); 288 mTicking = true; 289 } 290 291 // Stops the ticks that animate the timers. 292 private void stopClockTicks() { 293 if (mTicking) { 294 mViewPager.removeCallbacks(mClockTick); 295 mTicking = false; 296 } 297 } 298 299 private void goToPagerView() { 300 mTimerView.setVisibility(View.VISIBLE); 301 mSetupView.setVisibility(View.GONE); 302 mLastView = mTimerView; 303 setLeftRightButtonAppearance(); 304 setFabAppearance(); 305 startClockTicks(); 306 } 307 308 private void goToSetUpView() { 309 if (mAdapter.getCount() == 0) { 310 mCancel.setVisibility(View.INVISIBLE); 311 } else { 312 mCancel.setVisibility(View.VISIBLE); 313 } 314 mTimerView.setVisibility(View.GONE); 315 mSetupView.setVisibility(View.VISIBLE); 316 mSetupView.updateDeleteButtonAndDivider(); 317 mSetupView.registerStartButton(mFab); 318 mLastView = mSetupView; 319 setLeftRightButtonAppearance(); 320 setFabAppearance(); 321 stopClockTicks(); 322 } 323 324 private void updateTimerState(TimerObj t, String action) { 325 if (Timers.DELETE_TIMER.equals(action)) { 326 mAdapter.deleteTimer(t.mTimerId); 327 if (mAdapter.getCount() == 0) { 328 mSetupView.reset(); 329 goToSetUpView(); 330 } 331 } else { 332 t.writeToSharedPref(mPrefs); 333 } 334 final Intent i = new Intent(); 335 i.setAction(action); 336 i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId); 337 // Make sure the receiver is getting the intent ASAP. 338 i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 339 getActivity().sendBroadcast(i); 340 } 341 342 private void setTimerViewFabIcon(TimerObj timer) { 343 final Context context = getActivity(); 344 if (context == null || timer == null || mFab == null) { 345 return; 346 } 347 final Resources r = context.getResources(); 348 switch (timer.mState) { 349 case TimerObj.STATE_RUNNING: 350 mFab.setVisibility(View.VISIBLE); 351 mFab.setContentDescription(r.getString(R.string.timer_stop)); 352 mFab.setImageResource(R.drawable.ic_fab_pause); 353 break; 354 case TimerObj.STATE_STOPPED: 355 case TimerObj.STATE_RESTART: 356 mFab.setVisibility(View.VISIBLE); 357 mFab.setContentDescription(r.getString(R.string.timer_start)); 358 mFab.setImageResource(R.drawable.ic_fab_play); 359 break; 360 case TimerObj.STATE_DONE: // time-up then stopped 361 mFab.setVisibility(View.INVISIBLE); 362 break; 363 case TimerObj.STATE_TIMESUP: // time-up but didn't stopped, continue negative ticking 364 mFab.setVisibility(View.VISIBLE); 365 mFab.setContentDescription(r.getString(R.string.timer_stop)); 366 mFab.setImageResource(R.drawable.ic_fab_stop); 367 break; 368 default: 369 } 370 } 371 372 private Animator getRotateFromAnimator(View view) { 373 final Animator animator = new ObjectAnimator().ofFloat(view, View.SCALE_X, 1.0f, 0.0f); 374 animator.setDuration(ROTATE_ANIM_DURATION_MILIS); 375 animator.setInterpolator(DECELERATE_INTERPOLATOR); 376 return animator; 377 } 378 379 private Animator getRotateToAnimator(View view) { 380 final Animator animator = new ObjectAnimator().ofFloat(view, View.SCALE_X, 0.0f, 1.0f); 381 animator.setDuration(ROTATE_ANIM_DURATION_MILIS); 382 animator.setInterpolator(ACCELERATE_INTERPOLATOR); 383 return animator; 384 } 385 386 private Animator getScaleFooterButtonsAnimator(final boolean show) { 387 final AnimatorSet animatorSet = new AnimatorSet(); 388 final Animator leftButtonAnimator = AnimatorUtils.getScaleAnimator( 389 mLeftButton, show ? 0.0f : 1.0f, show ? 1.0f : 0.0f); 390 final Animator rightButtonAnimator = AnimatorUtils.getScaleAnimator( 391 mRightButton, show ? 0.0f : 1.0f, show ? 1.0f : 0.0f); 392 final float fabStartScale = (show && mFab.getVisibility() == View.INVISIBLE) ? 0.0f : 1.0f; 393 final Animator fabAnimator = AnimatorUtils.getScaleAnimator( 394 mFab, fabStartScale, show ? 1.0f : 0.0f); 395 animatorSet.addListener(new AnimatorListenerAdapter() { 396 @Override 397 public void onAnimationEnd(Animator animation) { 398 mLeftButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE); 399 mRightButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE); 400 restoreScale(mLeftButton); 401 restoreScale(mRightButton); 402 restoreScale(mFab); 403 } 404 }); 405 // If not show, means transiting from timer view to setup view, 406 // when the setup view starts to rotate, the footer buttons are already invisible, 407 // so the scaling has to finish before the setup view starts rotating 408 animatorSet.setDuration(show ? ROTATE_ANIM_DURATION_MILIS * 2 : ROTATE_ANIM_DURATION_MILIS); 409 animatorSet.play(leftButtonAnimator).with(rightButtonAnimator).with(fabAnimator); 410 return animatorSet; 411 } 412 413 private void restoreScale(View view) { 414 view.setScaleX(1.0f); 415 view.setScaleY(1.0f); 416 } 417 418 private Animator createRotateAnimator(AnimatorListenerAdapter adapter, boolean toSetup) { 419 final AnimatorSet animatorSet = new AnimatorSet(); 420 final Animator rotateFrom = getRotateFromAnimator(toSetup ? mTimerView : mSetupView); 421 rotateFrom.addListener(adapter); 422 final Animator rotateTo = getRotateToAnimator(toSetup ? mSetupView : mTimerView); 423 final Animator expandFooterButton = getScaleFooterButtonsAnimator(!toSetup); 424 animatorSet.play(rotateFrom).before(rotateTo).with(expandFooterButton); 425 return animatorSet; 426 } 427 428 @Override 429 public void onFabClick(View view) { 430 if (mLastView != mTimerView) { 431 // Timer is at Setup View, so fab is "play", rotate from setup view to timer view 432 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 433 @Override 434 public void onAnimationStart(Animator animation) { 435 final int timerLength = mSetupView.getTime(); 436 final TimerObj timerObj = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS); 437 timerObj.mState = TimerObj.STATE_RUNNING; 438 updateTimerState(timerObj, Timers.START_TIMER); 439 440 // Go to the newly created timer view 441 mAdapter.addTimer(timerObj); 442 mViewPager.setCurrentItem(0); 443 highlightPageIndicator(0); 444 } 445 446 @Override 447 public void onAnimationEnd(Animator animation) { 448 mSetupView.reset(); // Make sure the setup is cleared for next time 449 mSetupView.setScaleX(1.0f); // Reset the scale for setup view 450 goToPagerView(); 451 } 452 }; 453 createRotateAnimator(adapter, false).start(); 454 } else { 455 // Timer is at view pager, so fab is "play" or "pause" or "square that means reset" 456 final TimerObj t = getCurrentTimer(); 457 switch (t.mState) { 458 case TimerObj.STATE_RUNNING: 459 // Stop timer and save the remaining time of the timer 460 t.mState = TimerObj.STATE_STOPPED; 461 t.mView.pause(); 462 t.updateTimeLeft(true); 463 updateTimerState(t, Timers.TIMER_STOP); 464 break; 465 case TimerObj.STATE_STOPPED: 466 case TimerObj.STATE_RESTART: 467 // Reset the remaining time and continue timer 468 t.mState = TimerObj.STATE_RUNNING; 469 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft); 470 t.mView.start(); 471 updateTimerState(t, Timers.START_TIMER); 472 break; 473 case TimerObj.STATE_TIMESUP: 474 if (t.mDeleteAfterUse) { 475 cancelTimerNotification(t.mTimerId); 476 // Tell receiver the timer was deleted. 477 // It will stop all activity related to the 478 // timer 479 t.mState = TimerObj.STATE_DELETED; 480 updateTimerState(t, Timers.DELETE_TIMER); 481 } else { 482 t.mState = TimerObj.STATE_RESTART; 483 t.mOriginalLength = t.mSetupLength; 484 t.mTimeLeft = t.mSetupLength; 485 t.mView.stop(); 486 t.mView.setTime(t.mTimeLeft, false); 487 t.mView.set(t.mOriginalLength, t.mTimeLeft, false); 488 updateTimerState(t, Timers.TIMER_RESET); 489 cancelTimerNotification(t.mTimerId); 490 } 491 break; 492 } 493 setTimerViewFabIcon(t); 494 } 495 } 496 497 498 private TimerObj getCurrentTimer() { 499 if (mViewPager == null) { 500 return null; 501 } 502 final int currPage = mViewPager.getCurrentItem(); 503 if (currPage < mAdapter.getCount()) { 504 TimerObj o = mAdapter.getTimerAt(currPage); 505 return o; 506 } else { 507 return null; 508 } 509 } 510 511 @Override 512 public void setFabAppearance() { 513 final DeskClock activity = (DeskClock) getActivity(); 514 if (mFab == null) { 515 return; 516 } 517 518 if (activity.getSelectedTab() != DeskClock.TIMER_TAB_INDEX) { 519 mFab.setVisibility(View.VISIBLE); 520 return; 521 } 522 523 if (mLastView == mTimerView) { 524 setTimerViewFabIcon(getCurrentTimer()); 525 } else if (mSetupView != null) { 526 mSetupView.registerStartButton(mFab); 527 mFab.setImageResource(R.drawable.ic_fab_play); 528 mFab.setContentDescription(getString(R.string.timer_start)); 529 } 530 } 531 532 @Override 533 public void setLeftRightButtonAppearance() { 534 final DeskClock activity = (DeskClock) getActivity(); 535 if (mLeftButton == null || mRightButton == null || 536 activity.getSelectedTab() != DeskClock.TIMER_TAB_INDEX) { 537 return; 538 } 539 540 mLeftButton.setEnabled(true); 541 mRightButton.setEnabled(true); 542 mLeftButton.setVisibility(mLastView != mTimerView ? View.GONE : View.VISIBLE); 543 mRightButton.setVisibility(mLastView != mTimerView ? View.GONE : View.VISIBLE); 544 mLeftButton.setImageResource(R.drawable.ic_delete); 545 mLeftButton.setContentDescription(getString(R.string.timer_delete)); 546 mRightButton.setImageResource(R.drawable.ic_add_timer); 547 mRightButton.setContentDescription(getString(R.string.timer_add_timer)); 548 } 549 550 @Override 551 public void onRightButtonClick(View view) { 552 // Respond to add another timer 553 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 554 @Override 555 public void onAnimationEnd(Animator animation) { 556 mSetupView.reset(); 557 mTimerView.setScaleX(1.0f); // Reset the scale for timer view 558 goToSetUpView(); 559 } 560 }; 561 createRotateAnimator(adapter, true).start(); 562 } 563 564 @Override 565 public void onLeftButtonClick(View view) { 566 // Respond to delete timer 567 final TimerObj timer = getCurrentTimer(); 568 if (timer == null) { 569 return; // Prevent NPE if user click delete faster than the fade animation 570 } 571 if (timer.mState == TimerObj.STATE_TIMESUP) { 572 mNotificationManager.cancel(timer.mTimerId); 573 } 574 if (mAdapter.getCount() == 1) { 575 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 576 @Override 577 public void onAnimationEnd(Animator animation) { 578 mTimerView.setScaleX(1.0f); // Reset the scale for timer view 579 deleteTimer(timer); 580 } 581 }; 582 createRotateAnimator(adapter, true).start(); 583 } else { 584 TransitionManager.beginDelayedTransition(mContentView, mDeleteTransition); 585 deleteTimer(timer); 586 } 587 } 588 589 private void deleteTimer(TimerObj timer) { 590 // Tell receiver the timer was deleted, it will stop all activity related to the 591 // timer 592 timer.mState = TimerObj.STATE_DELETED; 593 updateTimerState(timer, Timers.DELETE_TIMER); 594 highlightPageIndicator(mViewPager.getCurrentItem()); 595 // When deleting a negative timer (hidden fab), since deleting will not trigger 596 // onResume(), in order to ensure the fab showing correctly, we need to manually 597 // set fab appearance here. 598 setFabAppearance(); 599 } 600 601 private void highlightPageIndicator(int position) { 602 final int count = mAdapter.getCount(); 603 if (count <= PAGINATION_DOTS_COUNT) { 604 for (int i = 0; i < PAGINATION_DOTS_COUNT; i++) { 605 if (count < 2 || i >= count) { 606 mPageIndicators[i].setVisibility(View.GONE); 607 } else { 608 paintIndicator(i, position == i ? R.drawable.ic_swipe_circle_light : 609 R.drawable.ic_swipe_circle_dark); 610 } 611 } 612 } else { 613 /** 614 * If there are more than 4 timers, the top and/or bottom dot might need to show a 615 * half fade, to indicate there are more timers in that direction. 616 */ 617 final int aboveCount = position; // How many timers are above the current timer 618 final int belowCount = count - position - 1; // How many timers are below 619 if (aboveCount < PAGINATION_DOTS_COUNT - 1) { 620 // There's enough room for the above timers, so top dot need not to fade 621 for (int i = 0; i < aboveCount; i++) { 622 paintIndicator(i, R.drawable.ic_swipe_circle_dark); 623 } 624 paintIndicator(position, R.drawable.ic_swipe_circle_light); 625 for (int i = position + 1; i < PAGINATION_DOTS_COUNT - 1 ; i++) { 626 paintIndicator(i, R.drawable.ic_swipe_circle_dark); 627 } 628 paintIndicator(PAGINATION_DOTS_COUNT - 1, R.drawable.ic_swipe_circle_bottom); 629 } else { 630 // There's not enough room for the above timers, top dot needs to fade 631 paintIndicator(0, R.drawable.ic_swipe_circle_top); 632 for (int i = 1; i < PAGINATION_DOTS_COUNT - 2; i++) { 633 paintIndicator(i, R.drawable.ic_swipe_circle_dark); 634 } 635 // Determine which resource to use for the "second indicator" from the bottom. 636 paintIndicator(PAGINATION_DOTS_COUNT - 2, belowCount == 0 ? 637 R.drawable.ic_swipe_circle_dark : R.drawable.ic_swipe_circle_light); 638 final int lastDotRes; 639 if (belowCount == 0) { 640 // The current timer is the last one 641 lastDotRes = R.drawable.ic_swipe_circle_light; 642 } else if (belowCount == 1) { 643 // There's only one timer below the current 644 lastDotRes = R.drawable.ic_swipe_circle_dark; 645 } else { 646 // There are more than one timer below, bottom dot needs to fade 647 lastDotRes = R.drawable.ic_swipe_circle_bottom; 648 } 649 paintIndicator(PAGINATION_DOTS_COUNT - 1, lastDotRes); 650 } 651 } 652 } 653 654 private void paintIndicator(int position, int res) { 655 mPageIndicators[position].setVisibility(View.VISIBLE); 656 mPageIndicators[position].setImageResource(res); 657 } 658 659 @Override 660 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 661 if (prefs.equals(mPrefs)) { 662 if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false)) 663 || (key.equals(Timers.FROM_NOTIFICATION) 664 && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) { 665 // The data-changed flag was set in the alert or notification so the adapter needs 666 // to re-sync with the database 667 SharedPreferences.Editor editor = mPrefs.edit(); 668 editor.putBoolean(key, false); 669 editor.apply(); 670 mAdapter.populateTimersFromPref(); 671 mViewPager.setAdapter(mAdapter); 672 if (mViewState != null) { 673 final int currPage = mViewState.getInt(CURR_PAGE); 674 mViewPager.setCurrentItem(currPage); 675 highlightPageIndicator(currPage); 676 } else { 677 highlightPageIndicator(0); 678 } 679 setFabAppearance(); 680 return; 681 } 682 } 683 } 684 685 public void setLabel(TimerObj timer, String label) { 686 timer.mLabel = label; 687 updateTimerState(timer, Timers.TIMER_UPDATE); 688 // Make sure the new label is visible. 689 mAdapter.notifyDataSetChanged(); 690 } 691 692 public void onPlusOneButtonPressed(TimerObj t) { 693 switch (t.mState) { 694 case TimerObj.STATE_RUNNING: 695 t.addTime(TimerObj.MINUTE_IN_MILLIS); 696 long timeLeft = t.updateTimeLeft(false); 697 t.mView.setTime(timeLeft, false); 698 t.mView.setLength(timeLeft); 699 mAdapter.notifyDataSetChanged(); 700 updateTimerState(t, Timers.TIMER_UPDATE); 701 break; 702 case TimerObj.STATE_STOPPED: 703 case TimerObj.STATE_DONE: 704 t.mState = TimerObj.STATE_RESTART; 705 t.mTimeLeft = t.mSetupLength; 706 t.mOriginalLength = t.mSetupLength; 707 t.mView.stop(); 708 t.mView.setTime(t.mTimeLeft, false); 709 t.mView.set(t.mOriginalLength, t.mTimeLeft, false); 710 updateTimerState(t, Timers.TIMER_RESET); 711 break; 712 case TimerObj.STATE_TIMESUP: 713 // +1 min when the time is up will restart the timer with 1 minute left. 714 t.mState = TimerObj.STATE_RUNNING; 715 t.mStartTime = Utils.getTimeNow(); 716 t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS; 717 t.mView.setTime(t.mTimeLeft, false); 718 t.mView.set(t.mOriginalLength, t.mTimeLeft, true); 719 t.mView.start(); 720 updateTimerState(t, Timers.TIMER_RESET); 721 updateTimerState(t, Timers.START_TIMER); 722 cancelTimerNotification(t.mTimerId); 723 break; 724 } 725 // This will change status of the timer, so update fab 726 setFabAppearance(); 727 } 728 729 private void cancelTimerNotification(int timerId) { 730 mNotificationManager.cancel(timerId); 731 } 732 } 733