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.content.Context;
24 import android.content.Intent;
25 import android.content.res.Resources;
26 import android.os.Bundle;
27 import android.os.SystemClock;
28 import android.support.annotation.VisibleForTesting;
29 import android.support.v4.view.ViewPager;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.animation.AccelerateInterpolator;
34 import android.view.animation.DecelerateInterpolator;
35 import android.widget.ImageButton;
36 import android.widget.ImageView;
37 
38 import com.android.deskclock.DeskClock;
39 import com.android.deskclock.DeskClockFragment;
40 import com.android.deskclock.HandleDeskClockApiCalls;
41 import com.android.deskclock.R;
42 import com.android.deskclock.data.DataModel;
43 import com.android.deskclock.data.Timer;
44 import com.android.deskclock.data.TimerListener;
45 import com.android.deskclock.events.Events;
46 
47 import java.io.Serializable;
48 import java.util.Arrays;
49 
50 import static android.view.View.ALPHA;
51 import static android.view.View.GONE;
52 import static android.view.View.INVISIBLE;
53 import static android.view.View.OnClickListener;
54 import static android.view.View.SCALE_X;
55 import static android.view.View.VISIBLE;
56 import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
57 
58 /**
59  * Displays a vertical list of timers in all states.
60  */
61 public 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      */
createTimerSetupIntent(Context context)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. */
TimerFragment()99     public TimerFragment() {}
100 
101     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onResume()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
onPause()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
onStop()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
onDestroyView()214     public void onDestroyView() {
215         super.onDestroyView();
216 
217         DataModel.getDataModel().removeTimerListener(mAdapter);
218         DataModel.getDataModel().removeTimerListener(mTimerWatcher);
219     }
220 
221     @Override
onSaveInstanceState(Bundle outState)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
setFabAppearance()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
onFabClick(View view)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
setLeftRightButtonAppearance()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
onLeftButtonClick(View view)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
onRightButtonClick(View view)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      */
updatePageIndicators()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
computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount)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      */
showCreateTimerView()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      */
showTimersView()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      */
animateTimerRemove(final Timer timerToRemove)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      */
animateToView(View toView, final Timer timerToRemove)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 
hasTimers()545     private boolean hasTimers() {
546         return mAdapter.getCount() > 0;
547     }
548 
getTimer()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 
startUpdatingTime()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 
stopUpdatingTime()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
run()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
onPageSelected(int position)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
onPageScrollStateChanged(int state)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
timerAdded(Timer timer)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
timerUpdated(Timer before, Timer after)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
timerRemoved(Timer timer)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
onClick(View v)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
onClick(View view)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 }