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