Home | History | Annotate | Download | only in timer
      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
    139     public void onCreate(Bundle savedInstanceState) {
    140         super.onCreate(savedInstanceState);
    141         mViewState = savedInstanceState;
    142     }
    143 
    144     @Override
    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
    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
    190     public void onResume() {
    191         super.onResume();
    192         if (getActivity() instanceof DeskClock) {
    193             DeskClock activity = (DeskClock) getActivity();
    194             activity.registerPageChangedListener(this);
    195         }
    196 
    197         if (mAdapter == null) {
    198             mAdapter = new TimerFragmentAdapter(getChildFragmentManager(), mPrefs);
    199         }
    200         mAdapter.populateTimersFromPref();
    201         mViewPager.setAdapter(mAdapter);
    202         mViewPager.setOnPageChangeListener(mOnPageChangeListener);
    203         mPrefs.registerOnSharedPreferenceChangeListener(this);
    204 
    205         // Clear the flag set in the notification and alert because the adapter was just
    206         // created and is thus in sync with the database
    207         final SharedPreferences.Editor editor = mPrefs.edit();
    208         if (mPrefs.getBoolean(Timers.FROM_NOTIFICATION, false)) {
    209             editor.putBoolean(Timers.FROM_NOTIFICATION, false);
    210         }
    211         if (mPrefs.getBoolean(Timers.FROM_ALERT, false)) {
    212             editor.putBoolean(Timers.FROM_ALERT, false);
    213         }
    214         editor.apply();
    215 
    216         mCancel.setVisibility(mAdapter.getCount() == 0 ? View.INVISIBLE : View.VISIBLE);
    217 
    218         boolean goToSetUpView;
    219         // Process extras that were sent to the app and were intended for the timer fragment
    220         final Intent newIntent = getActivity().getIntent();
    221         if (newIntent != null && newIntent.getBooleanExtra(
    222                 TimerFullScreenFragment.GOTO_SETUP_VIEW, false)) {
    223             goToSetUpView = true;
    224         } else {
    225             if (mViewState != null) {
    226                 final int currPage = mViewState.getInt(CURR_PAGE);
    227                 mViewPager.setCurrentItem(currPage);
    228                 highlightPageIndicator(currPage);
    229                 final boolean hasPreviousInput = mViewState.getBoolean(KEY_SETUP_SELECTED, false);
    230                 goToSetUpView = hasPreviousInput || mAdapter.getCount() == 0;
    231                 mSetupView.restoreEntryState(mViewState, KEY_ENTRY_STATE);
    232             } else {
    233                 highlightPageIndicator(0);
    234                 // If user was not previously using the setup, determine which view to go by count
    235                 goToSetUpView = mAdapter.getCount() == 0;
    236             }
    237         }
    238         if (goToSetUpView) {
    239             goToSetUpView();
    240         } else {
    241             goToPagerView();
    242         }
    243     }
    244 
    245     @Override
    246     public void onPause() {
    247         super.onPause();
    248         if (getActivity() instanceof DeskClock) {
    249             ((DeskClock) getActivity()).unregisterPageChangedListener(this);
    250         }
    251         mPrefs.unregisterOnSharedPreferenceChangeListener(this);
    252         if (mAdapter != null) {
    253             mAdapter.saveTimersToSharedPrefs();
    254         }
    255         stopClockTicks();
    256     }
    257 
    258     @Override
    259     public void onSaveInstanceState(Bundle outState) {
    260         super.onSaveInstanceState(outState);
    261         if (mAdapter != null) {
    262             mAdapter.saveTimersToSharedPrefs();
    263         }
    264         if (mSetupView != null) {
    265             outState.putBoolean(KEY_SETUP_SELECTED, mSetupView.getVisibility() == View.VISIBLE);
    266             mSetupView.saveEntryState(outState, KEY_ENTRY_STATE);
    267         }
    268         outState.putInt(CURR_PAGE, mViewPager.getCurrentItem());
    269         mViewState = outState;
    270     }
    271 
    272     @Override
    273     public void onDestroyView() {
    274         super.onDestroyView();
    275         mViewState = null;
    276     }
    277 
    278     @Override
    279     public void onPageChanged(int page) {
    280         if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) {
    281             mAdapter.notifyDataSetChanged();
    282         }
    283     }
    284 
    285     // Starts the ticks that animate the timers.
    286     private void startClockTicks() {
    287         mTimerView.postDelayed(mClockTick, 20);
    288         mTicking = true;
    289     }
    290 
    291     // Stops the ticks that animate the timers.
    292     private void stopClockTicks() {
    293         if (mTicking) {
    294             mViewPager.removeCallbacks(mClockTick);
    295             mTicking = false;
    296         }
    297     }
    298 
    299     private void goToPagerView() {
    300         mTimerView.setVisibility(View.VISIBLE);
    301         mSetupView.setVisibility(View.GONE);
    302         mLastView = mTimerView;
    303         setLeftRightButtonAppearance();
    304         setFabAppearance();
    305         startClockTicks();
    306     }
    307 
    308     private void goToSetUpView() {
    309         if (mAdapter.getCount() == 0) {
    310             mCancel.setVisibility(View.INVISIBLE);
    311         } else {
    312             mCancel.setVisibility(View.VISIBLE);
    313         }
    314         mTimerView.setVisibility(View.GONE);
    315         mSetupView.setVisibility(View.VISIBLE);
    316         mSetupView.updateDeleteButtonAndDivider();
    317         mSetupView.registerStartButton(mFab);
    318         mLastView = mSetupView;
    319         setLeftRightButtonAppearance();
    320         setFabAppearance();
    321         stopClockTicks();
    322     }
    323 
    324     private void updateTimerState(TimerObj t, String action) {
    325         if (Timers.DELETE_TIMER.equals(action)) {
    326             mAdapter.deleteTimer(t.mTimerId);
    327             if (mAdapter.getCount() == 0) {
    328                 mSetupView.reset();
    329                 goToSetUpView();
    330             }
    331         } else {
    332             t.writeToSharedPref(mPrefs);
    333         }
    334         final Intent i = new Intent();
    335         i.setAction(action);
    336         i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId);
    337         // Make sure the receiver is getting the intent ASAP.
    338         i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    339         getActivity().sendBroadcast(i);
    340     }
    341 
    342     private void setTimerViewFabIcon(TimerObj timer) {
    343         final Context context = getActivity();
    344         if (context == null || timer == null || mFab == null) {
    345             return;
    346         }
    347         final Resources r = context.getResources();
    348         switch (timer.mState) {
    349             case TimerObj.STATE_RUNNING:
    350                 mFab.setVisibility(View.VISIBLE);
    351                 mFab.setContentDescription(r.getString(R.string.timer_stop));
    352                 mFab.setImageResource(R.drawable.ic_fab_pause);
    353                 break;
    354             case TimerObj.STATE_STOPPED:
    355             case TimerObj.STATE_RESTART:
    356                 mFab.setVisibility(View.VISIBLE);
    357                 mFab.setContentDescription(r.getString(R.string.timer_start));
    358                 mFab.setImageResource(R.drawable.ic_fab_play);
    359                 break;
    360             case TimerObj.STATE_DONE: // time-up then stopped
    361                 mFab.setVisibility(View.INVISIBLE);
    362                 break;
    363             case TimerObj.STATE_TIMESUP: // time-up but didn't stopped, continue negative ticking
    364                 mFab.setVisibility(View.VISIBLE);
    365                 mFab.setContentDescription(r.getString(R.string.timer_stop));
    366                 mFab.setImageResource(R.drawable.ic_fab_stop);
    367                 break;
    368             default:
    369         }
    370     }
    371 
    372     private Animator getRotateFromAnimator(View view) {
    373         final Animator animator = new ObjectAnimator().ofFloat(view, View.SCALE_X, 1.0f, 0.0f);
    374         animator.setDuration(ROTATE_ANIM_DURATION_MILIS);
    375         animator.setInterpolator(DECELERATE_INTERPOLATOR);
    376         return animator;
    377     }
    378 
    379     private Animator getRotateToAnimator(View view) {
    380         final Animator animator = new ObjectAnimator().ofFloat(view, View.SCALE_X, 0.0f, 1.0f);
    381         animator.setDuration(ROTATE_ANIM_DURATION_MILIS);
    382         animator.setInterpolator(ACCELERATE_INTERPOLATOR);
    383         return animator;
    384     }
    385 
    386     private Animator getScaleFooterButtonsAnimator(final boolean show) {
    387         final AnimatorSet animatorSet = new AnimatorSet();
    388         final Animator leftButtonAnimator = AnimatorUtils.getScaleAnimator(
    389                 mLeftButton, show ? 0.0f : 1.0f, show ? 1.0f : 0.0f);
    390         final Animator rightButtonAnimator = AnimatorUtils.getScaleAnimator(
    391                 mRightButton, show ? 0.0f : 1.0f, show ? 1.0f : 0.0f);
    392         final float fabStartScale = (show && mFab.getVisibility() == View.INVISIBLE) ? 0.0f : 1.0f;
    393         final Animator fabAnimator = AnimatorUtils.getScaleAnimator(
    394                 mFab, fabStartScale, show ? 1.0f : 0.0f);
    395         animatorSet.addListener(new AnimatorListenerAdapter() {
    396             @Override
    397             public void onAnimationEnd(Animator animation) {
    398                 mLeftButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
    399                 mRightButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
    400                 restoreScale(mLeftButton);
    401                 restoreScale(mRightButton);
    402                 restoreScale(mFab);
    403             }
    404         });
    405         // If not show, means transiting from timer view to setup view,
    406         // when the setup view starts to rotate, the footer buttons are already invisible,
    407         // so the scaling has to finish before the setup view starts rotating
    408         animatorSet.setDuration(show ? ROTATE_ANIM_DURATION_MILIS * 2 : ROTATE_ANIM_DURATION_MILIS);
    409         animatorSet.play(leftButtonAnimator).with(rightButtonAnimator).with(fabAnimator);
    410         return animatorSet;
    411     }
    412 
    413     private void restoreScale(View view) {
    414         view.setScaleX(1.0f);
    415         view.setScaleY(1.0f);
    416     }
    417 
    418     private Animator createRotateAnimator(AnimatorListenerAdapter adapter, boolean toSetup) {
    419         final AnimatorSet animatorSet = new AnimatorSet();
    420         final Animator rotateFrom = getRotateFromAnimator(toSetup ? mTimerView : mSetupView);
    421         rotateFrom.addListener(adapter);
    422         final Animator rotateTo = getRotateToAnimator(toSetup ? mSetupView : mTimerView);
    423         final Animator expandFooterButton = getScaleFooterButtonsAnimator(!toSetup);
    424         animatorSet.play(rotateFrom).before(rotateTo).with(expandFooterButton);
    425         return animatorSet;
    426     }
    427 
    428     @Override
    429     public void onFabClick(View view) {
    430         if (mLastView != mTimerView) {
    431             // Timer is at Setup View, so fab is "play", rotate from setup view to timer view
    432             final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() {
    433                 @Override
    434                 public void onAnimationStart(Animator animation) {
    435                     final int timerLength = mSetupView.getTime();
    436                     final TimerObj timerObj = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS);
    437                     timerObj.mState = TimerObj.STATE_RUNNING;
    438                     updateTimerState(timerObj, Timers.START_TIMER);
    439 
    440                     // Go to the newly created timer view
    441                     mAdapter.addTimer(timerObj);
    442                     mViewPager.setCurrentItem(0);
    443                     highlightPageIndicator(0);
    444                 }
    445 
    446                 @Override
    447                 public void onAnimationEnd(Animator animation) {
    448                     mSetupView.reset(); // Make sure the setup is cleared for next time
    449                     mSetupView.setScaleX(1.0f); // Reset the scale for setup view
    450                     goToPagerView();
    451                 }
    452             };
    453             createRotateAnimator(adapter, false).start();
    454         } else {
    455             // Timer is at view pager, so fab is "play" or "pause" or "square that means reset"
    456             final TimerObj t = getCurrentTimer();
    457             switch (t.mState) {
    458                 case TimerObj.STATE_RUNNING:
    459                     // Stop timer and save the remaining time of the timer
    460                     t.mState = TimerObj.STATE_STOPPED;
    461                     t.mView.pause();
    462                     t.updateTimeLeft(true);
    463                     updateTimerState(t, Timers.TIMER_STOP);
    464                     break;
    465                 case TimerObj.STATE_STOPPED:
    466                 case TimerObj.STATE_RESTART:
    467                     // Reset the remaining time and continue timer
    468                     t.mState = TimerObj.STATE_RUNNING;
    469                     t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft);
    470                     t.mView.start();
    471                     updateTimerState(t, Timers.START_TIMER);
    472                     break;
    473                 case TimerObj.STATE_TIMESUP:
    474                     if (t.mDeleteAfterUse) {
    475                         cancelTimerNotification(t.mTimerId);
    476                         // Tell receiver the timer was deleted.
    477                         // It will stop all activity related to the
    478                         // timer
    479                         t.mState = TimerObj.STATE_DELETED;
    480                         updateTimerState(t, Timers.DELETE_TIMER);
    481                     } else {
    482                         t.mState = TimerObj.STATE_RESTART;
    483                         t.mOriginalLength = t.mSetupLength;
    484                         t.mTimeLeft = t.mSetupLength;
    485                         t.mView.stop();
    486                         t.mView.setTime(t.mTimeLeft, false);
    487                         t.mView.set(t.mOriginalLength, t.mTimeLeft, false);
    488                         updateTimerState(t, Timers.TIMER_RESET);
    489                         cancelTimerNotification(t.mTimerId);
    490                     }
    491                     break;
    492             }
    493             setTimerViewFabIcon(t);
    494         }
    495     }
    496 
    497 
    498     private TimerObj getCurrentTimer() {
    499         if (mViewPager == null) {
    500             return null;
    501         }
    502         final int currPage = mViewPager.getCurrentItem();
    503         if (currPage < mAdapter.getCount()) {
    504             TimerObj o = mAdapter.getTimerAt(currPage);
    505             return o;
    506         } else {
    507             return null;
    508         }
    509     }
    510 
    511     @Override
    512     public void setFabAppearance() {
    513         final DeskClock activity = (DeskClock) getActivity();
    514         if (mFab == null) {
    515             return;
    516         }
    517 
    518         if (activity.getSelectedTab() != DeskClock.TIMER_TAB_INDEX) {
    519             mFab.setVisibility(View.VISIBLE);
    520             return;
    521         }
    522 
    523         if (mLastView == mTimerView) {
    524             setTimerViewFabIcon(getCurrentTimer());
    525         } else if (mSetupView != null) {
    526             mSetupView.registerStartButton(mFab);
    527             mFab.setImageResource(R.drawable.ic_fab_play);
    528             mFab.setContentDescription(getString(R.string.timer_start));
    529         }
    530     }
    531 
    532     @Override
    533     public void setLeftRightButtonAppearance() {
    534         final DeskClock activity = (DeskClock) getActivity();
    535         if (mLeftButton == null || mRightButton == null ||
    536                 activity.getSelectedTab() != DeskClock.TIMER_TAB_INDEX) {
    537             return;
    538         }
    539 
    540         mLeftButton.setEnabled(true);
    541         mRightButton.setEnabled(true);
    542         mLeftButton.setVisibility(mLastView != mTimerView ? View.GONE : View.VISIBLE);
    543         mRightButton.setVisibility(mLastView != mTimerView ? View.GONE : View.VISIBLE);
    544         mLeftButton.setImageResource(R.drawable.ic_delete);
    545         mLeftButton.setContentDescription(getString(R.string.timer_delete));
    546         mRightButton.setImageResource(R.drawable.ic_add_timer);
    547         mRightButton.setContentDescription(getString(R.string.timer_add_timer));
    548     }
    549 
    550     @Override
    551     public void onRightButtonClick(View view) {
    552         // Respond to add another timer
    553         final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() {
    554             @Override
    555             public void onAnimationEnd(Animator animation) {
    556                 mSetupView.reset();
    557                 mTimerView.setScaleX(1.0f); // Reset the scale for timer view
    558                 goToSetUpView();
    559             }
    560         };
    561         createRotateAnimator(adapter, true).start();
    562     }
    563 
    564     @Override
    565     public void onLeftButtonClick(View view) {
    566         // Respond to delete timer
    567         final TimerObj timer = getCurrentTimer();
    568         if (timer == null) {
    569             return; // Prevent NPE if user click delete faster than the fade animation
    570         }
    571         if (timer.mState == TimerObj.STATE_TIMESUP) {
    572             mNotificationManager.cancel(timer.mTimerId);
    573         }
    574         if (mAdapter.getCount() == 1) {
    575             final AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() {
    576                 @Override
    577                 public void onAnimationEnd(Animator animation) {
    578                     mTimerView.setScaleX(1.0f); // Reset the scale for timer view
    579                     deleteTimer(timer);
    580                 }
    581             };
    582             createRotateAnimator(adapter, true).start();
    583         } else {
    584             TransitionManager.beginDelayedTransition(mContentView, mDeleteTransition);
    585             deleteTimer(timer);
    586         }
    587     }
    588 
    589     private void deleteTimer(TimerObj timer) {
    590         // Tell receiver the timer was deleted, it will stop all activity related to the
    591         // timer
    592         timer.mState = TimerObj.STATE_DELETED;
    593         updateTimerState(timer, Timers.DELETE_TIMER);
    594         highlightPageIndicator(mViewPager.getCurrentItem());
    595         // When deleting a negative timer (hidden fab), since deleting will not trigger
    596         // onResume(), in order to ensure the fab showing correctly, we need to manually
    597         // set fab appearance here.
    598         setFabAppearance();
    599     }
    600 
    601     private void highlightPageIndicator(int position) {
    602         final int count = mAdapter.getCount();
    603         if (count <= PAGINATION_DOTS_COUNT) {
    604             for (int i = 0; i < PAGINATION_DOTS_COUNT; i++) {
    605                 if (count < 2 || i >= count) {
    606                     mPageIndicators[i].setVisibility(View.GONE);
    607                 } else {
    608                     paintIndicator(i, position == i ? R.drawable.ic_swipe_circle_light :
    609                             R.drawable.ic_swipe_circle_dark);
    610                 }
    611             }
    612         } else {
    613             /**
    614              * If there are more than 4 timers, the top and/or bottom dot might need to show a
    615              * half fade, to indicate there are more timers in that direction.
    616              */
    617             final int aboveCount = position; // How many timers are above the current timer
    618             final int belowCount = count - position - 1; // How many timers are below
    619             if (aboveCount < PAGINATION_DOTS_COUNT - 1) {
    620                 // There's enough room for the above timers, so top dot need not to fade
    621                 for (int i = 0; i < aboveCount; i++) {
    622                     paintIndicator(i, R.drawable.ic_swipe_circle_dark);
    623                 }
    624                 paintIndicator(position, R.drawable.ic_swipe_circle_light);
    625                 for (int i = position + 1; i < PAGINATION_DOTS_COUNT - 1 ; i++) {
    626                     paintIndicator(i, R.drawable.ic_swipe_circle_dark);
    627                 }
    628                 paintIndicator(PAGINATION_DOTS_COUNT - 1, R.drawable.ic_swipe_circle_bottom);
    629             } else {
    630                 // There's not enough room for the above timers, top dot needs to fade
    631                 paintIndicator(0, R.drawable.ic_swipe_circle_top);
    632                 for (int i = 1; i < PAGINATION_DOTS_COUNT - 2; i++) {
    633                     paintIndicator(i, R.drawable.ic_swipe_circle_dark);
    634                 }
    635                 // Determine which resource to use for the "second indicator" from the bottom.
    636                 paintIndicator(PAGINATION_DOTS_COUNT - 2, belowCount == 0 ?
    637                         R.drawable.ic_swipe_circle_dark : R.drawable.ic_swipe_circle_light);
    638                 final int lastDotRes;
    639                 if (belowCount == 0) {
    640                     // The current timer is the last one
    641                     lastDotRes = R.drawable.ic_swipe_circle_light;
    642                 } else if (belowCount == 1) {
    643                     // There's only one timer below the current
    644                     lastDotRes = R.drawable.ic_swipe_circle_dark;
    645                 } else {
    646                     // There are more than one timer below, bottom dot needs to fade
    647                     lastDotRes = R.drawable.ic_swipe_circle_bottom;
    648                 }
    649                 paintIndicator(PAGINATION_DOTS_COUNT - 1, lastDotRes);
    650             }
    651         }
    652     }
    653 
    654     private void paintIndicator(int position, int res) {
    655         mPageIndicators[position].setVisibility(View.VISIBLE);
    656         mPageIndicators[position].setImageResource(res);
    657     }
    658 
    659     @Override
    660     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    661         if (prefs.equals(mPrefs)) {
    662             if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false))
    663                     || (key.equals(Timers.FROM_NOTIFICATION)
    664                     && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) {
    665                 // The data-changed flag was set in the alert or notification so the adapter needs
    666                 // to re-sync with the database
    667                 SharedPreferences.Editor editor = mPrefs.edit();
    668                 editor.putBoolean(key, false);
    669                 editor.apply();
    670                 mAdapter.populateTimersFromPref();
    671                 mViewPager.setAdapter(mAdapter);
    672                 if (mViewState != null) {
    673                     final int currPage = mViewState.getInt(CURR_PAGE);
    674                     mViewPager.setCurrentItem(currPage);
    675                     highlightPageIndicator(currPage);
    676                 } else {
    677                     highlightPageIndicator(0);
    678                 }
    679                 setFabAppearance();
    680                 return;
    681             }
    682         }
    683     }
    684 
    685     public void setLabel(TimerObj timer, String label) {
    686         timer.mLabel = label;
    687         updateTimerState(timer, Timers.TIMER_UPDATE);
    688         // Make sure the new label is visible.
    689         mAdapter.notifyDataSetChanged();
    690     }
    691 
    692     public void onPlusOneButtonPressed(TimerObj t) {
    693         switch (t.mState) {
    694             case TimerObj.STATE_RUNNING:
    695                 t.addTime(TimerObj.MINUTE_IN_MILLIS);
    696                 long timeLeft = t.updateTimeLeft(false);
    697                 t.mView.setTime(timeLeft, false);
    698                 t.mView.setLength(timeLeft);
    699                 mAdapter.notifyDataSetChanged();
    700                 updateTimerState(t, Timers.TIMER_UPDATE);
    701                 break;
    702             case TimerObj.STATE_STOPPED:
    703             case TimerObj.STATE_DONE:
    704                 t.mState = TimerObj.STATE_RESTART;
    705                 t.mTimeLeft = t.mSetupLength;
    706                 t.mOriginalLength = t.mSetupLength;
    707                 t.mView.stop();
    708                 t.mView.setTime(t.mTimeLeft, false);
    709                 t.mView.set(t.mOriginalLength, t.mTimeLeft, false);
    710                 updateTimerState(t, Timers.TIMER_RESET);
    711                 break;
    712             case TimerObj.STATE_TIMESUP:
    713                 // +1 min when the time is up will restart the timer with 1 minute left.
    714                 t.mState = TimerObj.STATE_RUNNING;
    715                 t.mStartTime = Utils.getTimeNow();
    716                 t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS;
    717                 t.mView.setTime(t.mTimeLeft, false);
    718                 t.mView.set(t.mOriginalLength, t.mTimeLeft, true);
    719                 t.mView.start();
    720                 updateTimerState(t, Timers.TIMER_RESET);
    721                 updateTimerState(t, Timers.START_TIMER);
    722                 cancelTimerNotification(t.mTimerId);
    723                 break;
    724         }
    725         // This will change status of the timer, so update fab
    726         setFabAppearance();
    727     }
    728 
    729     private void cancelTimerNotification(int timerId) {
    730         mNotificationManager.cancel(timerId);
    731     }
    732 }
    733