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