• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 && 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
onPause()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
onSaveInstanceState(Bundle outState)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
onDestroyView()273     public void onDestroyView() {
274         super.onDestroyView();
275         mViewState = null;
276     }
277 
278     @Override
onPageChanged(int page)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.
startClockTicks()286     private void startClockTicks() {
287         mTimerView.postDelayed(mClockTick, 20);
288         mTicking = true;
289     }
290 
291     // Stops the ticks that animate the timers.
stopClockTicks()292     private void stopClockTicks() {
293         if (mTicking) {
294             mViewPager.removeCallbacks(mClockTick);
295             mTicking = false;
296         }
297     }
298 
goToPagerView()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 
goToSetUpView()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 
updateTimerState(TimerObj t, String action)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 
setTimerViewFabIcon(TimerObj timer)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 
getRotateFromAnimator(View view)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 
getRotateToAnimator(View view)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 
getScaleFooterButtonsAnimator(final boolean show)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 
restoreScale(View view)413     private void restoreScale(View view) {
414         view.setScaleX(1.0f);
415         view.setScaleY(1.0f);
416     }
417 
createRotateAnimator(AnimatorListenerAdapter adapter, boolean toSetup)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
onFabClick(View view)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 
getCurrentTimer()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
setFabAppearance()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
setLeftRightButtonAppearance()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
onRightButtonClick(View view)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
onLeftButtonClick(View view)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 
deleteTimer(TimerObj timer)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 
highlightPageIndicator(int position)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 
paintIndicator(int position, int res)654     private void paintIndicator(int position, int res) {
655         mPageIndicators[position].setVisibility(View.VISIBLE);
656         mPageIndicators[position].setImageResource(res);
657     }
658 
659     @Override
onSharedPreferenceChanged(SharedPreferences prefs, String key)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 
setLabel(TimerObj timer, String label)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 
onPlusOneButtonPressed(TimerObj t)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 
cancelTimerNotification(int timerId)729     private void cancelTimerNotification(int timerId) {
730         mNotificationManager.cancel(timerId);
731     }
732 }
733