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