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
17package com.android.deskclock.timer;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.content.Intent;
25import android.os.Bundle;
26import android.os.SystemClock;
27import android.support.annotation.NonNull;
28import android.support.annotation.VisibleForTesting;
29import android.support.v4.view.ViewPager;
30import android.view.KeyEvent;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewTreeObserver;
35import android.view.animation.AccelerateInterpolator;
36import android.view.animation.DecelerateInterpolator;
37import android.widget.Button;
38import android.widget.ImageView;
39
40import com.android.deskclock.AnimatorUtils;
41import com.android.deskclock.DeskClock;
42import com.android.deskclock.DeskClockFragment;
43import com.android.deskclock.R;
44import com.android.deskclock.Utils;
45import com.android.deskclock.data.DataModel;
46import com.android.deskclock.data.Timer;
47import com.android.deskclock.data.TimerListener;
48import com.android.deskclock.data.TimerStringFormatter;
49import com.android.deskclock.events.Events;
50import com.android.deskclock.uidata.UiDataModel;
51
52import java.io.Serializable;
53import java.util.Arrays;
54
55import static android.view.View.ALPHA;
56import static android.view.View.GONE;
57import static android.view.View.INVISIBLE;
58import static android.view.View.TRANSLATION_Y;
59import static android.view.View.VISIBLE;
60import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
61
62/**
63 * Displays a vertical list of timers in all states.
64 */
65public final class TimerFragment extends DeskClockFragment {
66
67    private static final String EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP";
68
69    private static final String KEY_TIMER_SETUP_STATE = "timer_setup_input";
70
71    /** Notified when the user swipes vertically to change the visible timer. */
72    private final TimerPageChangeListener mTimerPageChangeListener = new TimerPageChangeListener();
73
74    /** Scheduled to update the timers while at least one is running. */
75    private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
76
77    /** Updates the {@link #mPageIndicators} in response to timers being added or removed. */
78    private final TimerListener mTimerWatcher = new TimerWatcher();
79
80    private TimerSetupView mCreateTimerView;
81    private ViewPager mViewPager;
82    private TimerPagerAdapter mAdapter;
83    private View mTimersView;
84    private View mCurrentView;
85    private ImageView[] mPageIndicators;
86
87    private Serializable mTimerSetupState;
88
89    /** {@code true} while this fragment is creating a new timer; {@code false} otherwise. */
90    private boolean mCreatingTimer;
91
92    /**
93     * @return an Intent that selects the timers tab with the setup screen for a new timer in place.
94     */
95    public static Intent createTimerSetupIntent(Context context) {
96        return new Intent(context, DeskClock.class).putExtra(EXTRA_TIMER_SETUP, true);
97    }
98
99    /** The public no-arg constructor required by all fragments. */
100    public TimerFragment() {
101        super(TIMERS);
102    }
103
104    @Override
105    public View onCreateView(LayoutInflater inflater, ViewGroup container,
106            Bundle savedInstanceState) {
107        final View view = inflater.inflate(R.layout.timer_fragment, container, false);
108
109        mAdapter = new TimerPagerAdapter(getFragmentManager());
110        mViewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager);
111        mViewPager.setAdapter(mAdapter);
112        mViewPager.addOnPageChangeListener(mTimerPageChangeListener);
113
114        mTimersView = view.findViewById(R.id.timer_view);
115        mCreateTimerView = (TimerSetupView) view.findViewById(R.id.timer_setup);
116        mCreateTimerView.setFabContainer(this);
117        mPageIndicators = new ImageView[] {
118                (ImageView) view.findViewById(R.id.page_indicator0),
119                (ImageView) view.findViewById(R.id.page_indicator1),
120                (ImageView) view.findViewById(R.id.page_indicator2),
121                (ImageView) view.findViewById(R.id.page_indicator3)
122        };
123
124        DataModel.getDataModel().addTimerListener(mAdapter);
125        DataModel.getDataModel().addTimerListener(mTimerWatcher);
126
127        // If timer setup state is present, retrieve it to be later honored.
128        if (savedInstanceState != null) {
129            mTimerSetupState = savedInstanceState.getSerializable(KEY_TIMER_SETUP_STATE);
130        }
131
132        return view;
133    }
134
135    @Override
136    public void onStart() {
137        super.onStart();
138
139        // Initialize the page indicators.
140        updatePageIndicators();
141
142        boolean createTimer = false;
143        int showTimerId = -1;
144
145        // Examine the intent of the parent activity to determine which view to display.
146        final Intent intent = getActivity().getIntent();
147        if (intent != null) {
148            // These extras are single-use; remove them after honoring them.
149            createTimer = intent.getBooleanExtra(EXTRA_TIMER_SETUP, false);
150            intent.removeExtra(EXTRA_TIMER_SETUP);
151
152            showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
153            intent.removeExtra(TimerService.EXTRA_TIMER_ID);
154        }
155
156        // Choose the view to display in this fragment.
157        if (showTimerId != -1) {
158            // A specific timer must be shown; show the list of timers.
159            showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
160        } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
161            // No timers exist, a timer is being created, or the last view was timer setup;
162            // show the timer setup view.
163            showCreateTimerView(FAB_AND_BUTTONS_IMMEDIATE);
164
165            if (mTimerSetupState != null) {
166                mCreateTimerView.setState(mTimerSetupState);
167                mTimerSetupState = null;
168            }
169        } else {
170            // Otherwise, default to showing the list of timers.
171            showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
172        }
173
174        // If the intent did not specify a timer to show, show the last timer that expired.
175        if (showTimerId == -1) {
176            final Timer timer = DataModel.getDataModel().getMostRecentExpiredTimer();
177            showTimerId = timer == null ? -1 : timer.getId();
178        }
179
180        // If a specific timer should be displayed, display the corresponding timer tab.
181        if (showTimerId != -1) {
182            final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
183            if (timer != null) {
184                final int index = DataModel.getDataModel().getTimers().indexOf(timer);
185                mViewPager.setCurrentItem(index);
186            }
187        }
188    }
189
190    @Override
191    public void onResume() {
192        super.onResume();
193
194        // We may have received a new intent while paused.
195        final Intent intent = getActivity().getIntent();
196        if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
197            // This extra is single-use; remove after honoring it.
198            final int showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
199            intent.removeExtra(TimerService.EXTRA_TIMER_ID);
200
201            final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
202            if (timer != null) {
203                // A specific timer must be shown; show the list of timers.
204                final int index = DataModel.getDataModel().getTimers().indexOf(timer);
205                mViewPager.setCurrentItem(index);
206
207                animateToView(mTimersView, null, false);
208            }
209        }
210    }
211
212    @Override
213    public void onStop() {
214        super.onStop();
215
216        // Stop updating the timers when this fragment is no longer visible.
217        stopUpdatingTime();
218    }
219
220    @Override
221    public void onDestroyView() {
222        super.onDestroyView();
223
224        DataModel.getDataModel().removeTimerListener(mAdapter);
225        DataModel.getDataModel().removeTimerListener(mTimerWatcher);
226    }
227
228    @Override
229    public void onSaveInstanceState(Bundle outState) {
230        super.onSaveInstanceState(outState);
231
232        // If the timer creation view is visible, store the input for later restoration.
233        if (mCurrentView == mCreateTimerView) {
234            mTimerSetupState = mCreateTimerView.getState();
235            outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState);
236        }
237    }
238
239    private void updateFab(@NonNull ImageView fab, boolean animate) {
240        if (mCurrentView == mTimersView) {
241            final Timer timer = getTimer();
242            if (timer == null) {
243                fab.setVisibility(INVISIBLE);
244                return;
245            }
246
247            fab.setVisibility(VISIBLE);
248            switch (timer.getState()) {
249                case RUNNING:
250                    if (animate) {
251                        fab.setImageResource(R.drawable.ic_play_pause_animation);
252                    } else {
253                        fab.setImageResource(R.drawable.ic_play_pause);
254                    }
255                    fab.setContentDescription(fab.getResources().getString(R.string.timer_stop));
256                    break;
257                case RESET:
258                    if (animate) {
259                        fab.setImageResource(R.drawable.ic_stop_play_animation);
260                    } else {
261                        fab.setImageResource(R.drawable.ic_pause_play);
262                    }
263                    fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
264                    break;
265                case PAUSED:
266                    if (animate) {
267                        fab.setImageResource(R.drawable.ic_pause_play_animation);
268                    } else {
269                        fab.setImageResource(R.drawable.ic_pause_play);
270                    }
271                    fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
272                    break;
273                case MISSED:
274                case EXPIRED:
275                    fab.setImageResource(R.drawable.ic_stop_white_24dp);
276                    fab.setContentDescription(fab.getResources().getString(R.string.timer_stop));
277                    break;
278            }
279        } else if (mCurrentView == mCreateTimerView) {
280            if (mCreateTimerView.hasValidInput()) {
281                fab.setImageResource(R.drawable.ic_start_white_24dp);
282                fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
283                fab.setVisibility(VISIBLE);
284            } else {
285                fab.setContentDescription(null);
286                fab.setVisibility(INVISIBLE);
287            }
288        }
289    }
290
291    @Override
292    public void onUpdateFab(@NonNull ImageView fab) {
293        updateFab(fab, false);
294    }
295
296    @Override
297    public void onMorphFab(@NonNull ImageView fab) {
298        // Update the fab's drawable to match the current timer state.
299        updateFab(fab, Utils.isNOrLater());
300        // Animate the drawable.
301        AnimatorUtils.startDrawableAnimation(fab);
302    }
303
304    @Override
305    public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
306        if (mCurrentView == mTimersView) {
307            left.setClickable(true);
308            left.setText(R.string.timer_delete);
309            left.setContentDescription(left.getResources().getString(R.string.timer_delete));
310            left.setVisibility(VISIBLE);
311
312            right.setClickable(true);
313            right.setText(R.string.timer_add_timer);
314            right.setContentDescription(right.getResources().getString(R.string.timer_add_timer));
315            right.setVisibility(VISIBLE);
316
317        } else if (mCurrentView == mCreateTimerView) {
318            left.setClickable(true);
319            left.setText(R.string.timer_cancel);
320            left.setContentDescription(left.getResources().getString(R.string.timer_cancel));
321            // If no timers yet exist, the user is forced to create the first one.
322            left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE);
323
324            right.setVisibility(INVISIBLE);
325        }
326    }
327
328    @Override
329    public void onFabClick(@NonNull ImageView fab) {
330        if (mCurrentView == mTimersView) {
331            final Timer timer = getTimer();
332
333            // If no timer is currently showing a fab action is meaningless.
334            if (timer == null) {
335                return;
336            }
337
338            final Context context = fab.getContext();
339            final long currentTime = timer.getRemainingTime();
340
341            switch (timer.getState()) {
342                case RUNNING:
343                    DataModel.getDataModel().pauseTimer(timer);
344                    Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock);
345                    if (currentTime > 0) {
346                        mTimersView.announceForAccessibility(TimerStringFormatter.formatString(
347                                context, R.string.timer_accessibility_stopped, currentTime, true));
348                    }
349                    break;
350                case PAUSED:
351                case RESET:
352                    DataModel.getDataModel().startTimer(timer);
353                    Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
354                    if (currentTime > 0) {
355                        mTimersView.announceForAccessibility(TimerStringFormatter.formatString(
356                                context, R.string.timer_accessibility_started, currentTime, true));
357                    }
358                    break;
359                case MISSED:
360                case EXPIRED:
361                    DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_deskclock);
362                    break;
363            }
364
365        } else if (mCurrentView == mCreateTimerView) {
366            mCreatingTimer = true;
367            try {
368                // Create the new timer.
369                final long timerLength = mCreateTimerView.getTimeInMillis();
370                final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", false);
371                Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock);
372
373                // Start the new timer.
374                DataModel.getDataModel().startTimer(timer);
375                Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
376
377                // Display the freshly created timer view.
378                mViewPager.setCurrentItem(0);
379            } finally {
380                mCreatingTimer = false;
381            }
382
383            // Return to the list of timers.
384            animateToView(mTimersView, null, true);
385        }
386    }
387
388    @Override
389    public void onLeftButtonClick(@NonNull Button left) {
390        if (mCurrentView == mTimersView) {
391            // Clicking the "delete" button.
392            final Timer timer = getTimer();
393            if (timer == null) {
394                return;
395            }
396
397            if (mAdapter.getCount() > 1) {
398                animateTimerRemove(timer);
399            } else {
400                animateToView(mCreateTimerView, timer, false);
401            }
402
403            left.announceForAccessibility(getActivity().getString(R.string.timer_deleted));
404
405        } else if (mCurrentView == mCreateTimerView) {
406            // Clicking the "cancel" button on the timer creation page returns to the timers list.
407            mCreateTimerView.reset();
408
409            animateToView(mTimersView, null, false);
410
411            left.announceForAccessibility(getActivity().getString(R.string.timer_canceled));
412        }
413    }
414
415    @Override
416    public void onRightButtonClick(@NonNull Button right) {
417        if (mCurrentView != mCreateTimerView) {
418            animateToView(mCreateTimerView, null, true);
419        }
420    }
421
422    @Override
423    public boolean onKeyDown(int keyCode, KeyEvent event) {
424        if (mCurrentView == mCreateTimerView) {
425            return mCreateTimerView.onKeyDown(keyCode, event);
426        }
427        return super.onKeyDown(keyCode, event);
428    }
429
430    /**
431     * Updates the state of the page indicators so they reflect the selected page in the context of
432     * all pages.
433     */
434    private void updatePageIndicators() {
435        final int page = mViewPager.getCurrentItem();
436        final int pageIndicatorCount = mPageIndicators.length;
437        final int pageCount = mAdapter.getCount();
438
439        final int[] states = computePageIndicatorStates(page, pageIndicatorCount, pageCount);
440        for (int i = 0; i < states.length; i++) {
441            final int state = states[i];
442            final ImageView pageIndicator = mPageIndicators[i];
443            if (state == 0) {
444                pageIndicator.setVisibility(GONE);
445            } else {
446                pageIndicator.setVisibility(VISIBLE);
447                pageIndicator.setImageResource(state);
448            }
449        }
450    }
451
452    /**
453     * @param page the selected page; value between 0 and {@code pageCount}
454     * @param pageIndicatorCount the number of indicators displaying the {@code page} location
455     * @param pageCount the number of pages that exist
456     * @return an array of length {@code pageIndicatorCount} specifying which image to display for
457     *      each page indicator or 0 if the page indicator should be hidden
458     */
459    @VisibleForTesting
460    static int[] computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount) {
461        // Compute the number of page indicators that will be visible.
462        final int rangeSize = Math.min(pageIndicatorCount, pageCount);
463
464        // Compute the inclusive range of pages to indicate centered around the selected page.
465        int rangeStart = page - (rangeSize / 2);
466        int rangeEnd = rangeStart + rangeSize - 1;
467
468        // Clamp the range of pages if they extend beyond the last page.
469        if (rangeEnd >= pageCount) {
470            rangeEnd = pageCount - 1;
471            rangeStart = rangeEnd - rangeSize + 1;
472        }
473
474        // Clamp the range of pages if they extend beyond the first page.
475        if (rangeStart < 0) {
476            rangeStart = 0;
477            rangeEnd = rangeSize - 1;
478        }
479
480        // Build the result with all page indicators initially hidden.
481        final int[] states = new int[pageIndicatorCount];
482        Arrays.fill(states, 0);
483
484        // If 0 or 1 total pages exist, all page indicators must remain hidden.
485        if (rangeSize < 2) {
486            return states;
487        }
488
489        // Initialize the visible page indicators to be dark.
490        Arrays.fill(states, 0, rangeSize, R.drawable.ic_swipe_circle_dark);
491
492        // If more pages exist before the first page indicator, make it a fade-in gradient.
493        if (rangeStart > 0) {
494            states[0] = R.drawable.ic_swipe_circle_top;
495        }
496
497        // If more pages exist after the last page indicator, make it a fade-out gradient.
498        if (rangeEnd < pageCount - 1) {
499            states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom;
500        }
501
502        // Set the indicator of the selected page to be light.
503        states[page - rangeStart] = R.drawable.ic_swipe_circle_light;
504
505        return states;
506    }
507
508    /**
509     * Display the view that creates a new timer.
510     */
511    private void showCreateTimerView(int updateTypes) {
512        // Stop animating the timers.
513        stopUpdatingTime();
514
515        // Show the creation view; hide the timer view.
516        mTimersView.setVisibility(GONE);
517        mCreateTimerView.setVisibility(VISIBLE);
518
519        // Record the fact that the create view is visible.
520        mCurrentView = mCreateTimerView;
521
522        // Update the fab and buttons.
523        updateFab(updateTypes);
524    }
525
526    /**
527     * Display the view that lists all existing timers.
528     */
529    private void showTimersView(int updateTypes) {
530        // Clear any defunct timer creation state; the next timer creation starts fresh.
531        mTimerSetupState = null;
532
533        // Show the timer view; hide the creation view.
534        mTimersView.setVisibility(VISIBLE);
535        mCreateTimerView.setVisibility(GONE);
536
537        // Record the fact that the create view is visible.
538        mCurrentView = mTimersView;
539
540        // Update the fab and buttons.
541        updateFab(updateTypes);
542
543        // Start animating the timers.
544        startUpdatingTime();
545    }
546
547    /**
548     * @param timerToRemove the timer to be removed during the animation
549     */
550    private void animateTimerRemove(final Timer timerToRemove) {
551        final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
552
553        final Animator fadeOut = ObjectAnimator.ofFloat(mViewPager, ALPHA, 1, 0);
554        fadeOut.setDuration(duration);
555        fadeOut.setInterpolator(new DecelerateInterpolator());
556        fadeOut.addListener(new AnimatorListenerAdapter() {
557            @Override
558            public void onAnimationEnd(Animator animation) {
559                DataModel.getDataModel().removeTimer(timerToRemove);
560                Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
561            }
562        });
563
564        final Animator fadeIn = ObjectAnimator.ofFloat(mViewPager, ALPHA, 0, 1);
565        fadeIn.setDuration(duration);
566        fadeIn.setInterpolator(new AccelerateInterpolator());
567
568        final AnimatorSet animatorSet = new AnimatorSet();
569        animatorSet.play(fadeOut).before(fadeIn);
570        animatorSet.start();
571    }
572
573    /**
574     * @param toView one of {@link #mTimersView} or {@link #mCreateTimerView}
575     * @param timerToRemove the timer to be removed during the animation; {@code null} if no timer
576     *      should be removed
577     * @param animateDown {@code true} if the views should animate upwards, otherwise downwards
578     */
579    private void animateToView(final View toView, final Timer timerToRemove,
580            final boolean animateDown) {
581        if (mCurrentView == toView) {
582            return;
583        }
584
585        final boolean toTimers = toView == mTimersView;
586        if (toTimers) {
587            mTimersView.setVisibility(VISIBLE);
588        } else {
589            mCreateTimerView.setVisibility(VISIBLE);
590        }
591        // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
592        updateFab(BUTTONS_DISABLE);
593
594        final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration();
595
596        final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver();
597        viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
598            @Override
599            public boolean onPreDraw() {
600                if (viewTreeObserver.isAlive()) {
601                    viewTreeObserver.removeOnPreDrawListener(this);
602                }
603
604                final View view = mTimersView.findViewById(R.id.timer_time);
605                final float distanceY = view != null ? view.getHeight() + view.getY() : 0;
606                final float translationDistance = animateDown ? distanceY : -distanceY;
607
608                toView.setTranslationY(-translationDistance);
609                mCurrentView.setTranslationY(0f);
610                toView.setAlpha(0f);
611                mCurrentView.setAlpha(1f);
612
613                final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView,
614                        TRANSLATION_Y, translationDistance);
615                final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f);
616                final AnimatorSet translationAnimatorSet = new AnimatorSet();
617                translationAnimatorSet.playTogether(translateCurrent, translateNew);
618                translationAnimatorSet.setDuration(animationDuration);
619                translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
620
621                final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f);
622                fadeOutAnimator.setDuration(animationDuration / 2);
623                fadeOutAnimator.addListener(new AnimatorListenerAdapter() {
624                    @Override
625                    public void onAnimationStart(Animator animation) {
626                        super.onAnimationStart(animation);
627
628                        // The fade-out animation and fab-shrinking animation should run together.
629                        updateFab(FAB_AND_BUTTONS_SHRINK);
630                    }
631
632                    @Override
633                    public void onAnimationEnd(Animator animation) {
634                        super.onAnimationEnd(animation);
635                        if (toTimers) {
636                            showTimersView(FAB_AND_BUTTONS_EXPAND);
637
638                            // Reset the state of the create view.
639                            mCreateTimerView.reset();
640                        } else {
641                            showCreateTimerView(FAB_AND_BUTTONS_EXPAND);
642                        }
643
644                        if (timerToRemove != null) {
645                            DataModel.getDataModel().removeTimer(timerToRemove);
646                            Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
647                        }
648
649                        // Update the fab and button states now that the correct view is visible and
650                        // before the animation to expand the fab and buttons starts.
651                        updateFab(FAB_AND_BUTTONS_IMMEDIATE);
652                    }
653                });
654
655                final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f);
656                fadeInAnimator.setDuration(animationDuration / 2);
657                fadeInAnimator.setStartDelay(animationDuration / 2);
658
659                final AnimatorSet animatorSet = new AnimatorSet();
660                animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet);
661                animatorSet.addListener(new AnimatorListenerAdapter() {
662                    @Override
663                    public void onAnimationEnd(Animator animation) {
664                        super.onAnimationEnd(animation);
665                        mTimersView.setTranslationY(0f);
666                        mCreateTimerView.setTranslationY(0f);
667                        mTimersView.setAlpha(1f);
668                        mCreateTimerView.setAlpha(1f);
669                    }
670                });
671                animatorSet.start();
672
673                return true;
674            }
675        });
676    }
677
678    private boolean hasTimers() {
679        return mAdapter.getCount() > 0;
680    }
681
682    private Timer getTimer() {
683        if (mViewPager == null) {
684            return null;
685        }
686
687        return mAdapter.getCount() == 0 ? null : mAdapter.getTimer(mViewPager.getCurrentItem());
688    }
689
690    private void startUpdatingTime() {
691        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
692        stopUpdatingTime();
693        mViewPager.post(mTimeUpdateRunnable);
694    }
695
696    private void stopUpdatingTime() {
697        mViewPager.removeCallbacks(mTimeUpdateRunnable);
698    }
699
700    /**
701     * Periodically refreshes the state of each timer.
702     */
703    private class TimeUpdateRunnable implements Runnable {
704        @Override
705        public void run() {
706            final long startTime = SystemClock.elapsedRealtime();
707            // If no timers require continuous updates, avoid scheduling the next update.
708            if (!mAdapter.updateTime()) {
709                return;
710            }
711            final long endTime = SystemClock.elapsedRealtime();
712
713            // Try to maintain a consistent period of time between redraws.
714            final long delay = Math.max(0, startTime + 20 - endTime);
715            mTimersView.postDelayed(this, delay);
716        }
717    }
718
719    /**
720     * Update the page indicators and fab in response to a new timer becoming visible.
721     */
722    private class TimerPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
723        @Override
724        public void onPageSelected(int position) {
725            updatePageIndicators();
726            updateFab(FAB_AND_BUTTONS_IMMEDIATE);
727
728            // Showing a new timer page may introduce a timer requiring continuous updates.
729            startUpdatingTime();
730        }
731
732        @Override
733        public void onPageScrollStateChanged(int state) {
734            // Teasing a neighboring timer may introduce a timer requiring continuous updates.
735            if (state == ViewPager.SCROLL_STATE_DRAGGING) {
736                startUpdatingTime();
737            }
738        }
739    }
740
741    /**
742     * Update the page indicators in response to timers being added or removed.
743     * Update the fab in response to the visible timer changing.
744     */
745    private class TimerWatcher implements TimerListener {
746        @Override
747        public void timerAdded(Timer timer) {
748            updatePageIndicators();
749            // If the timer is being created via this fragment avoid adjusting the fab.
750            // Timer setup view is about to be animated away in response to this timer creation.
751            // Changes to the fab immediately preceding that animation are jarring.
752            if (!mCreatingTimer) {
753                updateFab(FAB_AND_BUTTONS_IMMEDIATE);
754            }
755        }
756
757        @Override
758        public void timerUpdated(Timer before, Timer after) {
759            // If the timer started, animate the timers.
760            if (before.isReset() && !after.isReset()) {
761                startUpdatingTime();
762            }
763
764            // Fetch the index of the change.
765            final int index = DataModel.getDataModel().getTimers().indexOf(after);
766
767            // If the timer just expired but is not displayed, display it now.
768            if (!before.isExpired() && after.isExpired() && index != mViewPager.getCurrentItem()) {
769                mViewPager.setCurrentItem(index, true);
770
771            } else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) {
772                // Morph the fab from its old state to new state if necessary.
773                if (before.getState() != after.getState()
774                        && !(before.isPaused() && after.isReset())) {
775                    updateFab(FAB_MORPH);
776                }
777            }
778        }
779
780        @Override
781        public void timerRemoved(Timer timer) {
782            updatePageIndicators();
783            updateFab(FAB_AND_BUTTONS_IMMEDIATE);
784
785            if (mCurrentView == mTimersView && mAdapter.getCount() == 0) {
786                animateToView(mCreateTimerView, null, false);
787            }
788        }
789    }
790}