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 onCreate(Bundle savedInstanceState)139 public void onCreate(Bundle savedInstanceState) { 140 super.onCreate(savedInstanceState); 141 mViewState = savedInstanceState; 142 } 143 144 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onActivityCreated(Bundle savedInstanceState)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 onResume()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 222 && newIntent.getBooleanExtra(TimerFullScreenFragment.GOTO_SETUP_VIEW, false)) { 223 goToSetUpView = true; 224 } else if (newIntent != null 225 && newIntent.getBooleanExtra(Timers.FIRST_LAUNCH_FROM_API_CALL, false)) { 226 // We use this extra to identify if a. this activity is launched from api call, 227 // and b. this fragment is resumed for the first time. If both are true, 228 // we should show the timer view instead of setup view. 229 goToSetUpView = false; 230 // Show the first timer because that's the newly created one 231 highlightPageIndicator(0); 232 mViewPager.setCurrentItem(0); 233 234 // Reset the extra to false to ensure when next time the fragment resume, 235 // we no longer care if it's from api call or not. 236 newIntent.putExtra(Timers.FIRST_LAUNCH_FROM_API_CALL, false); 237 } else { 238 if (mViewState != null) { 239 final int currPage = mViewState.getInt(CURR_PAGE); 240 mViewPager.setCurrentItem(currPage); 241 highlightPageIndicator(currPage); 242 final boolean hasPreviousInput = mViewState.getBoolean(KEY_SETUP_SELECTED, false); 243 goToSetUpView = hasPreviousInput || mAdapter.getCount() == 0; 244 mSetupView.restoreEntryState(mViewState, KEY_ENTRY_STATE); 245 } else { 246 highlightPageIndicator(0); 247 // If user was not previously using the setup, determine which view to go by count 248 goToSetUpView = mAdapter.getCount() == 0; 249 } 250 } 251 if (goToSetUpView) { 252 goToSetUpView(); 253 } else { 254 goToPagerView(); 255 } 256 } 257 258 @Override onPause()259 public void onPause() { 260 super.onPause(); 261 if (getActivity() instanceof DeskClock) { 262 ((DeskClock) getActivity()).unregisterPageChangedListener(this); 263 } 264 mPrefs.unregisterOnSharedPreferenceChangeListener(this); 265 if (mAdapter != null) { 266 mAdapter.saveTimersToSharedPrefs(); 267 } 268 stopClockTicks(); 269 } 270 271 @Override onSaveInstanceState(Bundle outState)272 public void onSaveInstanceState(Bundle outState) { 273 super.onSaveInstanceState(outState); 274 if (mAdapter != null) { 275 mAdapter.saveTimersToSharedPrefs(); 276 } 277 if (mSetupView != null) { 278 outState.putBoolean(KEY_SETUP_SELECTED, mSetupView.getVisibility() == View.VISIBLE); 279 mSetupView.saveEntryState(outState, KEY_ENTRY_STATE); 280 } 281 outState.putInt(CURR_PAGE, mViewPager.getCurrentItem()); 282 mViewState = outState; 283 } 284 285 @Override onDestroyView()286 public void onDestroyView() { 287 super.onDestroyView(); 288 mViewState = null; 289 } 290 291 @Override onPageChanged(int page)292 public void onPageChanged(int page) { 293 if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) { 294 mAdapter.notifyDataSetChanged(); 295 } 296 } 297 298 // Starts the ticks that animate the timers. startClockTicks()299 private void startClockTicks() { 300 mTimerView.postDelayed(mClockTick, 20); 301 mTicking = true; 302 } 303 304 // Stops the ticks that animate the timers. stopClockTicks()305 private void stopClockTicks() { 306 if (mTicking) { 307 mViewPager.removeCallbacks(mClockTick); 308 mTicking = false; 309 } 310 } 311 goToPagerView()312 private void goToPagerView() { 313 mTimerView.setVisibility(View.VISIBLE); 314 mSetupView.setVisibility(View.GONE); 315 mLastView = mTimerView; 316 setLeftRightButtonAppearance(); 317 setFabAppearance(); 318 startClockTicks(); 319 } 320 goToSetUpView()321 private void goToSetUpView() { 322 if (mAdapter.getCount() == 0) { 323 mCancel.setVisibility(View.INVISIBLE); 324 } else { 325 mCancel.setVisibility(View.VISIBLE); 326 } 327 mTimerView.setVisibility(View.GONE); 328 mSetupView.setVisibility(View.VISIBLE); 329 mSetupView.updateDeleteButtonAndDivider(); 330 mSetupView.registerStartButton(mFab); 331 mLastView = mSetupView; 332 setLeftRightButtonAppearance(); 333 setFabAppearance(); 334 stopClockTicks(); 335 } 336 updateTimerState(TimerObj t, String action)337 private void updateTimerState(TimerObj t, String action) { 338 if (Timers.DELETE_TIMER.equals(action)) { 339 mAdapter.deleteTimer(t.mTimerId); 340 if (mAdapter.getCount() == 0) { 341 mSetupView.reset(); 342 goToSetUpView(); 343 } 344 } else { 345 t.writeToSharedPref(mPrefs); 346 } 347 final Intent i = new Intent(); 348 i.setAction(action); 349 i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId); 350 // Make sure the receiver is getting the intent ASAP. 351 i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 352 getActivity().sendBroadcast(i); 353 } 354 setTimerViewFabIcon(TimerObj timer)355 private void setTimerViewFabIcon(TimerObj timer) { 356 final Context context = getActivity(); 357 if (context == null || timer == null || mFab == null) { 358 return; 359 } 360 final Resources r = context.getResources(); 361 switch (timer.mState) { 362 case TimerObj.STATE_RUNNING: 363 mFab.setVisibility(View.VISIBLE); 364 mFab.setContentDescription(r.getString(R.string.timer_stop)); 365 mFab.setImageResource(R.drawable.ic_fab_pause); 366 break; 367 case TimerObj.STATE_STOPPED: 368 case TimerObj.STATE_RESTART: 369 mFab.setVisibility(View.VISIBLE); 370 mFab.setContentDescription(r.getString(R.string.timer_start)); 371 mFab.setImageResource(R.drawable.ic_fab_play); 372 break; 373 case TimerObj.STATE_DONE: // time-up then stopped 374 mFab.setVisibility(View.INVISIBLE); 375 break; 376 case TimerObj.STATE_TIMESUP: // time-up but didn't stopped, continue negative ticking 377 mFab.setVisibility(View.VISIBLE); 378 mFab.setContentDescription(r.getString(R.string.timer_stop)); 379 mFab.setImageResource(R.drawable.ic_fab_stop); 380 break; 381 default: 382 } 383 } 384 getRotateFromAnimator(View view)385 private Animator getRotateFromAnimator(View view) { 386 final Animator animator = new ObjectAnimator().ofFloat(view, View.SCALE_X, 1.0f, 0.0f); 387 animator.setDuration(ROTATE_ANIM_DURATION_MILIS); 388 animator.setInterpolator(DECELERATE_INTERPOLATOR); 389 return animator; 390 } 391 getRotateToAnimator(View view)392 private Animator getRotateToAnimator(View view) { 393 final Animator animator = new ObjectAnimator().ofFloat(view, View.SCALE_X, 0.0f, 1.0f); 394 animator.setDuration(ROTATE_ANIM_DURATION_MILIS); 395 animator.setInterpolator(ACCELERATE_INTERPOLATOR); 396 return animator; 397 } 398 getScaleFooterButtonsAnimator(final boolean show)399 private Animator getScaleFooterButtonsAnimator(final boolean show) { 400 final AnimatorSet animatorSet = new AnimatorSet(); 401 final Animator leftButtonAnimator = AnimatorUtils.getScaleAnimator( 402 mLeftButton, show ? 0.0f : 1.0f, show ? 1.0f : 0.0f); 403 final Animator rightButtonAnimator = AnimatorUtils.getScaleAnimator( 404 mRightButton, show ? 0.0f : 1.0f, show ? 1.0f : 0.0f); 405 final float fabStartScale = (show && mFab.getVisibility() == View.INVISIBLE) ? 0.0f : 1.0f; 406 final Animator fabAnimator = AnimatorUtils.getScaleAnimator( 407 mFab, fabStartScale, show ? 1.0f : 0.0f); 408 animatorSet.addListener(new AnimatorListenerAdapter() { 409 @Override 410 public void onAnimationEnd(Animator animation) { 411 mLeftButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE); 412 mRightButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE); 413 restoreScale(mLeftButton); 414 restoreScale(mRightButton); 415 restoreScale(mFab); 416 } 417 }); 418 // If not show, means transiting from timer view to setup view, 419 // when the setup view starts to rotate, the footer buttons are already invisible, 420 // so the scaling has to finish before the setup view starts rotating 421 animatorSet.setDuration(show ? ROTATE_ANIM_DURATION_MILIS * 2 : ROTATE_ANIM_DURATION_MILIS); 422 animatorSet.play(leftButtonAnimator).with(rightButtonAnimator).with(fabAnimator); 423 return animatorSet; 424 } 425 restoreScale(View view)426 private void restoreScale(View view) { 427 view.setScaleX(1.0f); 428 view.setScaleY(1.0f); 429 } 430 createRotateAnimator(AnimatorListenerAdapter adapter, boolean toSetup)431 private Animator createRotateAnimator(AnimatorListenerAdapter adapter, boolean toSetup) { 432 final AnimatorSet animatorSet = new AnimatorSet(); 433 final Animator rotateFrom = getRotateFromAnimator(toSetup ? mTimerView : mSetupView); 434 rotateFrom.addListener(adapter); 435 final Animator rotateTo = getRotateToAnimator(toSetup ? mSetupView : mTimerView); 436 final Animator expandFooterButton = getScaleFooterButtonsAnimator(!toSetup); 437 animatorSet.play(rotateFrom).before(rotateTo).with(expandFooterButton); 438 return animatorSet; 439 } 440 441 @Override onFabClick(View view)442 public void onFabClick(View view) { 443 if (mLastView != mTimerView) { 444 // Timer is at Setup View, so fab is "play", rotate from setup view to timer view 445 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 446 @Override 447 public void onAnimationStart(Animator animation) { 448 final int timerLength = mSetupView.getTime(); 449 final TimerObj timerObj = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS, 450 getActivity()); 451 timerObj.mState = TimerObj.STATE_RUNNING; 452 updateTimerState(timerObj, Timers.START_TIMER); 453 454 // Go to the newly created timer view 455 mAdapter.addTimer(timerObj); 456 mViewPager.setCurrentItem(0); 457 highlightPageIndicator(0); 458 } 459 460 @Override 461 public void onAnimationEnd(Animator animation) { 462 mSetupView.reset(); // Make sure the setup is cleared for next time 463 mSetupView.setScaleX(1.0f); // Reset the scale for setup view 464 goToPagerView(); 465 } 466 }; 467 createRotateAnimator(adapter, false).start(); 468 } else { 469 // Timer is at view pager, so fab is "play" or "pause" or "square that means reset" 470 final TimerObj t = getCurrentTimer(); 471 switch (t.mState) { 472 case TimerObj.STATE_RUNNING: 473 // Stop timer and save the remaining time of the timer 474 t.mState = TimerObj.STATE_STOPPED; 475 t.mView.pause(); 476 t.updateTimeLeft(true); 477 updateTimerState(t, Timers.TIMER_STOP); 478 break; 479 case TimerObj.STATE_STOPPED: 480 case TimerObj.STATE_RESTART: 481 // Reset the remaining time and continue timer 482 t.mState = TimerObj.STATE_RUNNING; 483 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft); 484 t.mView.start(); 485 updateTimerState(t, Timers.START_TIMER); 486 break; 487 case TimerObj.STATE_TIMESUP: 488 if (t.mDeleteAfterUse) { 489 cancelTimerNotification(t.mTimerId); 490 // Tell receiver the timer was deleted. 491 // It will stop all activity related to the 492 // timer 493 t.mState = TimerObj.STATE_DELETED; 494 updateTimerState(t, Timers.DELETE_TIMER); 495 } else { 496 t.mState = TimerObj.STATE_RESTART; 497 t.mOriginalLength = t.mSetupLength; 498 t.mTimeLeft = t.mSetupLength; 499 t.mView.stop(); 500 t.mView.setTime(t.mTimeLeft, false); 501 t.mView.set(t.mOriginalLength, t.mTimeLeft, false); 502 updateTimerState(t, Timers.TIMER_RESET); 503 cancelTimerNotification(t.mTimerId); 504 } 505 break; 506 } 507 setTimerViewFabIcon(t); 508 } 509 } 510 511 getCurrentTimer()512 private TimerObj getCurrentTimer() { 513 if (mViewPager == null) { 514 return null; 515 } 516 final int currPage = mViewPager.getCurrentItem(); 517 if (currPage < mAdapter.getCount()) { 518 TimerObj o = mAdapter.getTimerAt(currPage); 519 return o; 520 } else { 521 return null; 522 } 523 } 524 525 @Override setFabAppearance()526 public void setFabAppearance() { 527 final DeskClock activity = (DeskClock) getActivity(); 528 if (mFab == null) { 529 return; 530 } 531 532 if (activity.getSelectedTab() != DeskClock.TIMER_TAB_INDEX) { 533 mFab.setVisibility(View.VISIBLE); 534 return; 535 } 536 537 if (mLastView == mTimerView) { 538 setTimerViewFabIcon(getCurrentTimer()); 539 } else if (mSetupView != null) { 540 mSetupView.registerStartButton(mFab); 541 mFab.setImageResource(R.drawable.ic_fab_play); 542 mFab.setContentDescription(getString(R.string.timer_start)); 543 } 544 } 545 546 @Override setLeftRightButtonAppearance()547 public void setLeftRightButtonAppearance() { 548 final DeskClock activity = (DeskClock) getActivity(); 549 if (mLeftButton == null || mRightButton == null || 550 activity.getSelectedTab() != DeskClock.TIMER_TAB_INDEX) { 551 return; 552 } 553 554 mLeftButton.setEnabled(true); 555 mRightButton.setEnabled(true); 556 mLeftButton.setVisibility(mLastView != mTimerView ? View.GONE : View.VISIBLE); 557 mRightButton.setVisibility(mLastView != mTimerView ? View.GONE : View.VISIBLE); 558 mLeftButton.setImageResource(R.drawable.ic_delete); 559 mLeftButton.setContentDescription(getString(R.string.timer_delete)); 560 mRightButton.setImageResource(R.drawable.ic_add_timer); 561 mRightButton.setContentDescription(getString(R.string.timer_add_timer)); 562 } 563 564 @Override onRightButtonClick(View view)565 public void onRightButtonClick(View view) { 566 // Respond to add another timer 567 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 568 @Override 569 public void onAnimationEnd(Animator animation) { 570 mSetupView.reset(); 571 mTimerView.setScaleX(1.0f); // Reset the scale for timer view 572 goToSetUpView(); 573 } 574 }; 575 createRotateAnimator(adapter, true).start(); 576 } 577 578 @Override onLeftButtonClick(View view)579 public void onLeftButtonClick(View view) { 580 // Respond to delete timer 581 final TimerObj timer = getCurrentTimer(); 582 if (timer == null) { 583 return; // Prevent NPE if user click delete faster than the fade animation 584 } 585 if (timer.mState == TimerObj.STATE_TIMESUP) { 586 mNotificationManager.cancel(timer.mTimerId); 587 } 588 if (mAdapter.getCount() == 1) { 589 final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { 590 @Override 591 public void onAnimationEnd(Animator animation) { 592 mTimerView.setScaleX(1.0f); // Reset the scale for timer view 593 deleteTimer(timer); 594 } 595 }; 596 createRotateAnimator(adapter, true).start(); 597 } else { 598 TransitionManager.beginDelayedTransition(mContentView, mDeleteTransition); 599 deleteTimer(timer); 600 } 601 } 602 deleteTimer(TimerObj timer)603 private void deleteTimer(TimerObj timer) { 604 // Tell receiver the timer was deleted, it will stop all activity related to the 605 // timer 606 timer.mState = TimerObj.STATE_DELETED; 607 updateTimerState(timer, Timers.DELETE_TIMER); 608 highlightPageIndicator(mViewPager.getCurrentItem()); 609 // When deleting a negative timer (hidden fab), since deleting will not trigger 610 // onResume(), in order to ensure the fab showing correctly, we need to manually 611 // set fab appearance here. 612 setFabAppearance(); 613 } 614 highlightPageIndicator(int position)615 private void highlightPageIndicator(int position) { 616 final int count = mAdapter.getCount(); 617 if (count <= PAGINATION_DOTS_COUNT) { 618 for (int i = 0; i < PAGINATION_DOTS_COUNT; i++) { 619 if (count < 2 || i >= count) { 620 mPageIndicators[i].setVisibility(View.GONE); 621 } else { 622 paintIndicator(i, position == i ? R.drawable.ic_swipe_circle_light : 623 R.drawable.ic_swipe_circle_dark); 624 } 625 } 626 } else { 627 /** 628 * If there are more than 4 timers, the top and/or bottom dot might need to show a 629 * half fade, to indicate there are more timers in that direction. 630 */ 631 final int aboveCount = position; // How many timers are above the current timer 632 final int belowCount = count - position - 1; // How many timers are below 633 if (aboveCount < PAGINATION_DOTS_COUNT - 1) { 634 // There's enough room for the above timers, so top dot need not to fade 635 for (int i = 0; i < aboveCount; i++) { 636 paintIndicator(i, R.drawable.ic_swipe_circle_dark); 637 } 638 paintIndicator(position, R.drawable.ic_swipe_circle_light); 639 for (int i = position + 1; i < PAGINATION_DOTS_COUNT - 1 ; i++) { 640 paintIndicator(i, R.drawable.ic_swipe_circle_dark); 641 } 642 paintIndicator(PAGINATION_DOTS_COUNT - 1, R.drawable.ic_swipe_circle_bottom); 643 } else { 644 // There's not enough room for the above timers, top dot needs to fade 645 paintIndicator(0, R.drawable.ic_swipe_circle_top); 646 for (int i = 1; i < PAGINATION_DOTS_COUNT - 2; i++) { 647 paintIndicator(i, R.drawable.ic_swipe_circle_dark); 648 } 649 // Determine which resource to use for the "second indicator" from the bottom. 650 paintIndicator(PAGINATION_DOTS_COUNT - 2, belowCount == 0 ? 651 R.drawable.ic_swipe_circle_dark : R.drawable.ic_swipe_circle_light); 652 final int lastDotRes; 653 if (belowCount == 0) { 654 // The current timer is the last one 655 lastDotRes = R.drawable.ic_swipe_circle_light; 656 } else if (belowCount == 1) { 657 // There's only one timer below the current 658 lastDotRes = R.drawable.ic_swipe_circle_dark; 659 } else { 660 // There are more than one timer below, bottom dot needs to fade 661 lastDotRes = R.drawable.ic_swipe_circle_bottom; 662 } 663 paintIndicator(PAGINATION_DOTS_COUNT - 1, lastDotRes); 664 } 665 } 666 } 667 paintIndicator(int position, int res)668 private void paintIndicator(int position, int res) { 669 mPageIndicators[position].setVisibility(View.VISIBLE); 670 mPageIndicators[position].setImageResource(res); 671 } 672 673 @Override onSharedPreferenceChanged(SharedPreferences prefs, String key)674 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 675 if (prefs.equals(mPrefs)) { 676 if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false)) 677 || (key.equals(Timers.FROM_NOTIFICATION) 678 && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) { 679 // The data-changed flag was set in the alert or notification so the adapter needs 680 // to re-sync with the database 681 SharedPreferences.Editor editor = mPrefs.edit(); 682 editor.putBoolean(key, false); 683 editor.apply(); 684 mAdapter.populateTimersFromPref(); 685 mViewPager.setAdapter(mAdapter); 686 if (mViewState != null) { 687 final int currPage = mViewState.getInt(CURR_PAGE); 688 mViewPager.setCurrentItem(currPage); 689 highlightPageIndicator(currPage); 690 } else { 691 highlightPageIndicator(0); 692 } 693 setFabAppearance(); 694 return; 695 } 696 } 697 } 698 setLabel(TimerObj timer, String label)699 public void setLabel(TimerObj timer, String label) { 700 timer.mLabel = label; 701 updateTimerState(timer, Timers.TIMER_UPDATE); 702 // Make sure the new label is visible. 703 mAdapter.notifyDataSetChanged(); 704 } 705 onPlusOneButtonPressed(TimerObj t)706 public void onPlusOneButtonPressed(TimerObj t) { 707 switch (t.mState) { 708 case TimerObj.STATE_RUNNING: 709 t.addTime(TimerObj.MINUTE_IN_MILLIS); 710 long timeLeft = t.updateTimeLeft(false); 711 t.mView.setTime(timeLeft, false); 712 t.mView.setLength(timeLeft); 713 mAdapter.notifyDataSetChanged(); 714 updateTimerState(t, Timers.TIMER_UPDATE); 715 break; 716 case TimerObj.STATE_STOPPED: 717 case TimerObj.STATE_DONE: 718 t.mState = TimerObj.STATE_RESTART; 719 t.mTimeLeft = t.mSetupLength; 720 t.mOriginalLength = t.mSetupLength; 721 t.mView.stop(); 722 t.mView.setTime(t.mTimeLeft, false); 723 t.mView.set(t.mOriginalLength, t.mTimeLeft, false); 724 updateTimerState(t, Timers.TIMER_RESET); 725 break; 726 case TimerObj.STATE_TIMESUP: 727 // +1 min when the time is up will restart the timer with 1 minute left. 728 t.mState = TimerObj.STATE_RUNNING; 729 t.mStartTime = Utils.getTimeNow(); 730 t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS; 731 t.mView.setTime(t.mTimeLeft, false); 732 t.mView.set(t.mOriginalLength, t.mTimeLeft, true); 733 t.mView.start(); 734 updateTimerState(t, Timers.TIMER_RESET); 735 updateTimerState(t, Timers.START_TIMER); 736 cancelTimerNotification(t.mTimerId); 737 break; 738 } 739 // This will change status of the timer, so update fab 740 setFabAppearance(); 741 } 742 cancelTimerNotification(int timerId)743 private void cancelTimerNotification(int timerId) { 744 mNotificationManager.cancel(timerId); 745 } 746 } 747