Home | History | Annotate | Download | only in timer
      1 /*
      2  * Copyright (C) 2012 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.ValueAnimator;
     24 import android.app.Activity;
     25 import android.app.Fragment;
     26 import android.app.FragmentTransaction;
     27 import android.app.NotificationManager;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.SharedPreferences;
     31 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
     32 import android.os.Bundle;
     33 import android.os.Handler;
     34 import android.preference.PreferenceManager;
     35 import android.text.format.DateUtils;
     36 import android.util.Log;
     37 import android.view.LayoutInflater;
     38 import android.view.View;
     39 import android.view.View.OnClickListener;
     40 import android.view.ViewAnimationUtils;
     41 import android.view.ViewGroup;
     42 import android.view.ViewGroup.LayoutParams;
     43 import android.view.ViewGroupOverlay;
     44 import android.view.animation.AccelerateInterpolator;
     45 import android.view.animation.DecelerateInterpolator;
     46 import android.view.animation.Interpolator;
     47 import android.view.animation.PathInterpolator;
     48 import android.widget.FrameLayout;
     49 import android.widget.ImageButton;
     50 import android.widget.TextView;
     51 
     52 import com.android.deskclock.CircleButtonsLayout;
     53 import com.android.deskclock.DeskClock;
     54 import com.android.deskclock.DeskClock.OnTapListener;
     55 import com.android.deskclock.DeskClockFragment;
     56 import com.android.deskclock.LabelDialogFragment;
     57 import com.android.deskclock.LogUtils;
     58 import com.android.deskclock.R;
     59 import com.android.deskclock.TimerSetupView;
     60 import com.android.deskclock.Utils;
     61 import com.android.deskclock.widget.sgv.GridAdapter;
     62 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn;
     63 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut;
     64 import com.android.deskclock.widget.sgv.StaggeredGridView;
     65 
     66 import java.util.ArrayList;
     67 import java.util.Collections;
     68 import java.util.Comparator;
     69 import java.util.LinkedList;
     70 
     71 // TODO: This class is renamed from TimerFragment to TimerFullScreenFragment with no change. It
     72 // is responsible for the timer list in full screen timer alert and should be deprecated shortly.
     73 public class TimerFullScreenFragment extends DeskClockFragment
     74         implements OnClickListener, OnSharedPreferenceChangeListener {
     75 
     76     private static final String TAG = "TimerFragment1";
     77     private static final String KEY_ENTRY_STATE = "entry_state";
     78     private static final Interpolator REVEAL_INTERPOLATOR =
     79             new PathInterpolator(0.0f, 0.0f, 0.2f, 1.0f);
     80     public static final String GOTO_SETUP_VIEW = "deskclock.timers.gotosetup";
     81 
     82     private Bundle mViewState;
     83     private StaggeredGridView mTimersList;
     84     private View mTimersListPage;
     85     private int mColumnCount;
     86     private ImageButton mFab;
     87     private TimerSetupView mTimerSetup;
     88     private TimersListAdapter mAdapter;
     89     private boolean mTicking = false;
     90     private SharedPreferences mPrefs;
     91     private NotificationManager mNotificationManager;
     92     private OnEmptyListListener mOnEmptyListListener;
     93     private View mLastVisibleView = null;  // used to decide if to set the view or animate to it.
     94 
     95     class ClickAction {
     96         public static final int ACTION_STOP = 1;
     97         public static final int ACTION_PLUS_ONE = 2;
     98         public static final int ACTION_DELETE = 3;
     99 
    100         public int mAction;
    101         public TimerObj mTimer;
    102 
    103         public ClickAction(int action, TimerObj t) {
    104             mAction = action;
    105             mTimer = t;
    106         }
    107     }
    108 
    109     // Container Activity that requests TIMESUP_MODE must implement this interface
    110     public interface OnEmptyListListener {
    111         public void onEmptyList();
    112 
    113         public void onListChanged();
    114     }
    115 
    116     TimersListAdapter createAdapter(Context context, SharedPreferences prefs) {
    117         if (mOnEmptyListListener == null) {
    118             return new TimersListAdapter(context, prefs);
    119         } else {
    120             return new TimesUpListAdapter(context, prefs);
    121         }
    122     }
    123 
    124     private class TimersListAdapter extends GridAdapter {
    125 
    126         ArrayList<TimerObj> mTimers = new ArrayList<TimerObj>();
    127         Context mContext;
    128         SharedPreferences mmPrefs;
    129 
    130         private void clear() {
    131             mTimers.clear();
    132             notifyDataSetChanged();
    133         }
    134 
    135         public TimersListAdapter(Context context, SharedPreferences prefs) {
    136             mContext = context;
    137             mmPrefs = prefs;
    138         }
    139 
    140         @Override
    141         public int getCount() {
    142             return mTimers.size();
    143         }
    144 
    145         @Override
    146         public boolean hasStableIds() {
    147             return true;
    148         }
    149 
    150         @Override
    151         public TimerObj getItem(int p) {
    152             return mTimers.get(p);
    153         }
    154 
    155         @Override
    156         public long getItemId(int p) {
    157             if (p >= 0 && p < mTimers.size()) {
    158                 return mTimers.get(p).mTimerId;
    159             }
    160             return 0;
    161         }
    162 
    163         public void deleteTimer(int id) {
    164             for (int i = 0; i < mTimers.size(); i++) {
    165                 TimerObj t = mTimers.get(i);
    166 
    167                 if (t.mTimerId == id) {
    168                     if (t.mView != null) {
    169                         ((TimerListItem) t.mView).stop();
    170                     }
    171                     t.deleteFromSharedPref(mmPrefs);
    172                     mTimers.remove(i);
    173                     if (mTimers.size() == 1 && mColumnCount > 1) {
    174                         // If we're going from two timers to one (in the same row), we don't want to
    175                         // animate the translation because we're changing the layout params span
    176                         // from 1 to 2, and the animation doesn't handle that very well. So instead,
    177                         // just fade out and in.
    178                         mTimersList.setAnimationMode(AnimationIn.FADE, AnimationOut.FADE);
    179                     } else {
    180                         mTimersList.setAnimationMode(
    181                                 AnimationIn.FLY_IN_NEW_VIEWS, AnimationOut.FADE);
    182                     }
    183                     notifyDataSetChanged();
    184                     return;
    185                 }
    186             }
    187         }
    188 
    189         protected int findTimerPositionById(int id) {
    190             for (int i = 0; i < mTimers.size(); i++) {
    191                 TimerObj t = mTimers.get(i);
    192                 if (t.mTimerId == id) {
    193                     return i;
    194                 }
    195             }
    196             return -1;
    197         }
    198 
    199         public void removeTimer(TimerObj timerObj) {
    200             int position = findTimerPositionById(timerObj.mTimerId);
    201             if (position >= 0) {
    202                 mTimers.remove(position);
    203                 notifyDataSetChanged();
    204             }
    205         }
    206 
    207         @Override
    208         public View getView(int position, View convertView, ViewGroup parent) {
    209             final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
    210                     Context.LAYOUT_INFLATER_SERVICE);
    211             final TimerListItem v = (TimerListItem) inflater.inflate(R.layout.timer_list_item,
    212                     null);
    213             final TimerObj o = (TimerObj) getItem(position);
    214             o.mView = v;
    215             long timeLeft = o.updateTimeLeft(false);
    216             boolean drawRed = o.mState != TimerObj.STATE_RESTART;
    217             v.set(o.mOriginalLength, timeLeft, drawRed);
    218             v.setTime(timeLeft, true);
    219             switch (o.mState) {
    220                 case TimerObj.STATE_RUNNING:
    221                     v.start();
    222                     break;
    223                 case TimerObj.STATE_TIMESUP:
    224                     v.timesUp();
    225                     break;
    226                 case TimerObj.STATE_DONE:
    227                     v.done();
    228                     break;
    229                 default:
    230                     break;
    231             }
    232 
    233             // Timer text serves as a virtual start/stop button.
    234             final CountingTimerView countingTimerView = (CountingTimerView)
    235                     v.findViewById(R.id.timer_time_text);
    236             countingTimerView.registerVirtualButtonAction(new Runnable() {
    237                 @Override
    238                 public void run() {
    239                     TimerFullScreenFragment.this.onClickHelper(
    240                             new ClickAction(ClickAction.ACTION_STOP, o));
    241                 }
    242             });
    243 
    244             CircleButtonsLayout circleLayout =
    245                     (CircleButtonsLayout) v.findViewById(R.id.timer_circle);
    246             circleLayout.setCircleTimerViewIds(R.id.timer_time, R.id.reset_add, R.id.timer_label,
    247                     R.id.timer_label_text);
    248 
    249             ImageButton resetAddButton = (ImageButton) v.findViewById(R.id.reset_add);
    250             resetAddButton.setTag(new ClickAction(ClickAction.ACTION_PLUS_ONE, o));
    251             v.setResetAddButton(true, TimerFullScreenFragment.this);
    252             FrameLayout label = (FrameLayout) v.findViewById(R.id.timer_label);
    253             TextView labelIcon = (TextView) v.findViewById(R.id.timer_label_placeholder);
    254             TextView labelText = (TextView) v.findViewById(R.id.timer_label_text);
    255             if (o.mLabel.equals("")) {
    256                 labelText.setVisibility(View.GONE);
    257                 labelIcon.setVisibility(View.VISIBLE);
    258             } else {
    259                 labelText.setText(o.mLabel);
    260                 labelText.setVisibility(View.VISIBLE);
    261                 labelIcon.setVisibility(View.GONE);
    262             }
    263             if (getActivity() instanceof DeskClock) {
    264                 label.setOnTouchListener(new OnTapListener(getActivity(), labelText) {
    265                     @Override
    266                     protected void processClick(View v) {
    267                         onLabelPressed(o);
    268                     }
    269                 });
    270             } else {
    271                 labelIcon.setVisibility(View.INVISIBLE);
    272             }
    273             return v;
    274         }
    275 
    276         @Override
    277         public int getItemColumnSpan(Object item, int position) {
    278             // This returns the width for a specified position. If we only have one item, have it
    279             // span all columns so that it's centered. Otherwise, all timers should just span one.
    280             if (getCount() == 1) {
    281                 return mColumnCount;
    282             } else {
    283                 return 1;
    284             }
    285         }
    286 
    287         public void addTimer(TimerObj t) {
    288             mTimers.add(0, t);
    289             sort();
    290         }
    291 
    292         public void onSaveInstanceState(Bundle outState) {
    293             TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers);
    294         }
    295 
    296         public void onRestoreInstanceState(Bundle outState) {
    297             TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers);
    298             sort();
    299         }
    300 
    301         public void saveGlobalState() {
    302             TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers);
    303         }
    304 
    305         public void sort() {
    306             if (getCount() > 0) {
    307                 Collections.sort(mTimers, mTimersCompare);
    308                 notifyDataSetChanged();
    309             }
    310         }
    311 
    312         private final Comparator<TimerObj> mTimersCompare = new Comparator<TimerObj>() {
    313             static final int BUZZING = 0;
    314             static final int IN_USE = 1;
    315             static final int NOT_USED = 2;
    316 
    317             protected int getSection(TimerObj timerObj) {
    318                 switch (timerObj.mState) {
    319                     case TimerObj.STATE_TIMESUP:
    320                         return BUZZING;
    321                     case TimerObj.STATE_RUNNING:
    322                     case TimerObj.STATE_STOPPED:
    323                         return IN_USE;
    324                     default:
    325                         return NOT_USED;
    326                 }
    327             }
    328 
    329             @Override
    330             public int compare(TimerObj o1, TimerObj o2) {
    331                 int section1 = getSection(o1);
    332                 int section2 = getSection(o2);
    333                 if (section1 != section2) {
    334                     return (section1 < section2) ? -1 : 1;
    335                 } else if (section1 == BUZZING || section1 == IN_USE) {
    336                     return (o1.mTimeLeft < o2.mTimeLeft) ? -1 : 1;
    337                 } else {
    338                     return (o1.mSetupLength < o2.mSetupLength) ? -1 : 1;
    339                 }
    340             }
    341         };
    342     }
    343 
    344     private class TimesUpListAdapter extends TimersListAdapter {
    345 
    346         public TimesUpListAdapter(Context context, SharedPreferences prefs) {
    347             super(context, prefs);
    348         }
    349 
    350         @Override
    351         public void onSaveInstanceState(Bundle outState) {
    352             // This adapter has a data subset and never updates entire database
    353             // Individual timers are updated in button handlers.
    354         }
    355 
    356         @Override
    357         public void saveGlobalState() {
    358             // This adapter has a data subset and never updates entire database
    359             // Individual timers are updated in button handlers.
    360         }
    361 
    362         @Override
    363         public void onRestoreInstanceState(Bundle outState) {
    364             // This adapter loads a subset
    365             TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers, TimerObj.STATE_TIMESUP);
    366 
    367             if (getCount() == 0) {
    368                 mOnEmptyListListener.onEmptyList();
    369             } else {
    370                 Collections.sort(mTimers, new Comparator<TimerObj>() {
    371                     @Override
    372                     public int compare(TimerObj o1, TimerObj o2) {
    373                         return (o1.mTimeLeft < o2.mTimeLeft) ? -1 : 1;
    374                     }
    375                 });
    376             }
    377         }
    378     }
    379 
    380     private final Runnable mClockTick = new Runnable() {
    381         boolean mVisible = true;
    382         final static int TIME_PERIOD_MS = 1000;
    383         final static int SPLIT = TIME_PERIOD_MS / 2;
    384 
    385         @Override
    386         public void run() {
    387             // Setup for blinking
    388             boolean visible = Utils.getTimeNow() % TIME_PERIOD_MS < SPLIT;
    389             boolean toggle = mVisible != visible;
    390             mVisible = visible;
    391             for (int i = 0; i < mAdapter.getCount(); i++) {
    392                 TimerObj t = mAdapter.getItem(i);
    393                 if (t.mState == TimerObj.STATE_RUNNING || t.mState == TimerObj.STATE_TIMESUP) {
    394                     long timeLeft = t.updateTimeLeft(false);
    395                     if (t.mView != null) {
    396                         ((TimerListItem) (t.mView)).setTime(timeLeft, false);
    397                     }
    398                 }
    399                 if (t.mTimeLeft <= 0 && t.mState != TimerObj.STATE_DONE
    400                         && t.mState != TimerObj.STATE_RESTART) {
    401                     t.mState = TimerObj.STATE_TIMESUP;
    402                     if (t.mView != null) {
    403                         ((TimerListItem) (t.mView)).timesUp();
    404                     }
    405                 }
    406 
    407                 // The blinking
    408                 if (toggle && t.mView != null) {
    409                     if (t.mState == TimerObj.STATE_TIMESUP) {
    410                         ((TimerListItem) (t.mView)).setCircleBlink(mVisible);
    411                     }
    412                     if (t.mState == TimerObj.STATE_STOPPED) {
    413                         ((TimerListItem) (t.mView)).setTextBlink(mVisible);
    414                     }
    415                 }
    416             }
    417             mTimersList.postDelayed(mClockTick, 20);
    418         }
    419     };
    420 
    421     @Override
    422     public void onCreate(Bundle savedInstanceState) {
    423         // Cache instance data and consume in first call to setupPage()
    424         if (savedInstanceState != null) {
    425             mViewState = savedInstanceState;
    426         }
    427 
    428         super.onCreate(savedInstanceState);
    429     }
    430 
    431     @Override
    432     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    433             Bundle savedInstanceState) {
    434         // Inflate the layout for this fragment
    435         View v = inflater.inflate(R.layout.timer_full_screen_fragment, container, false);
    436 
    437         // Handle arguments from parent
    438         Bundle bundle = getArguments();
    439         if (bundle != null && bundle.containsKey(Timers.TIMESUP_MODE)) {
    440             if (bundle.getBoolean(Timers.TIMESUP_MODE, false)) {
    441                 try {
    442                     mOnEmptyListListener = (OnEmptyListListener) getActivity();
    443                 } catch (ClassCastException e) {
    444                     Log.wtf(TAG, getActivity().toString() + " must implement OnEmptyListListener");
    445                 }
    446             }
    447         }
    448 
    449         mFab = (ImageButton) v.findViewById(R.id.fab);
    450         mTimersList = (StaggeredGridView) v.findViewById(R.id.timers_list);
    451         // For tablets in landscape, the count will be 2. All else will be 1.
    452         mColumnCount = getResources().getInteger(R.integer.timer_column_count);
    453         mTimersList.setColumnCount(mColumnCount);
    454         // Set this to true; otherwise adding new views to the end of the list won't cause
    455         // everything above it to be filled in correctly.
    456         mTimersList.setGuardAgainstJaggedEdges(true);
    457 
    458         mTimersListPage = v.findViewById(R.id.timers_list_page);
    459         mTimerSetup = (TimerSetupView) v.findViewById(R.id.timer_setup);
    460 
    461         mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
    462         mNotificationManager = (NotificationManager)
    463                 getActivity().getSystemService(Context.NOTIFICATION_SERVICE);
    464 
    465         return v;
    466     }
    467 
    468     @Override
    469     public void onDestroyView() {
    470         mViewState = new Bundle();
    471         saveViewState(mViewState);
    472         super.onDestroyView();
    473     }
    474 
    475     @Override
    476     public void onResume() {
    477         Intent newIntent = null;
    478 
    479         if (getActivity() instanceof DeskClock) {
    480             DeskClock activity = (DeskClock) getActivity();
    481             activity.registerPageChangedListener(this);
    482             newIntent = activity.getIntent();
    483         }
    484         super.onResume();
    485         mPrefs.registerOnSharedPreferenceChangeListener(this);
    486 
    487         mAdapter = createAdapter(getActivity(), mPrefs);
    488         mAdapter.onRestoreInstanceState(null);
    489 
    490         LayoutParams params;
    491         float dividerHeight = getResources().getDimension(R.dimen.timer_divider_height);
    492         if (getActivity() instanceof DeskClock) {
    493             // If this is a DeskClock fragment (i.e. not a FullScreenTimerAlert), add a footer to
    494             // the bottom of the list so that it can scroll underneath the bottom button bar.
    495             // StaggeredGridView doesn't support a footer view, but GridAdapter does, so this
    496             // can't happen until the Adapter itself is instantiated.
    497             View footerView = getActivity().getLayoutInflater().inflate(
    498                     R.layout.blank_footer_view, mTimersList, false);
    499             params = footerView.getLayoutParams();
    500             params.height -= dividerHeight;
    501             footerView.setLayoutParams(params);
    502             mAdapter.setFooterView(footerView);
    503         }
    504 
    505         if (mPrefs.getBoolean(Timers.FROM_NOTIFICATION, false)) {
    506             // Clear the flag set in the notification because the adapter was just
    507             // created and is thus in sync with the database
    508             SharedPreferences.Editor editor = mPrefs.edit();
    509             editor.putBoolean(Timers.FROM_NOTIFICATION, false);
    510             editor.apply();
    511         }
    512         if (mPrefs.getBoolean(Timers.FROM_ALERT, false)) {
    513             // Clear the flag set in the alert because the adapter was just
    514             // created and is thus in sync with the database
    515             SharedPreferences.Editor editor = mPrefs.edit();
    516             editor.putBoolean(Timers.FROM_ALERT, false);
    517             editor.apply();
    518         }
    519 
    520         mTimersList.setAdapter(mAdapter);
    521         mLastVisibleView = null;   // Force a non animation setting of the view
    522         setPage();
    523         // View was hidden in onPause, make sure it is visible now.
    524         View v = getView();
    525         if (v != null) {
    526             getView().setVisibility(View.VISIBLE);
    527         }
    528 
    529         if (newIntent != null) {
    530             processIntent(newIntent);
    531         }
    532 
    533         mFab.setOnClickListener(new OnClickListener() {
    534             @Override
    535             public void onClick(View view) {
    536                 revealAnimation(mFab, getActivity().getResources().getColor(R.color.clock_white));
    537                 new Handler().postDelayed(new Runnable() {
    538                     @Override
    539                     public void run() {
    540                         updateAllTimesUpTimers(false /* stop */);
    541                     }
    542                 }, TimerFragment.ANIMATION_TIME_MILLIS);
    543             }
    544         });
    545     }
    546 
    547     private  void revealAnimation(final View centerView, int color) {
    548         final Activity activity = getActivity();
    549         final View decorView = activity.getWindow().getDecorView();
    550         final ViewGroupOverlay overlay = (ViewGroupOverlay) decorView.getOverlay();
    551 
    552         // Create a transient view for performing the reveal animation.
    553         final View revealView = new View(activity);
    554         revealView.setRight(decorView.getWidth());
    555         revealView.setBottom(decorView.getHeight());
    556         revealView.setBackgroundColor(color);
    557         overlay.add(revealView);
    558 
    559         final int[] clearLocation = new int[2];
    560         centerView.getLocationInWindow(clearLocation);
    561         clearLocation[0] += centerView.getWidth() / 2;
    562         clearLocation[1] += centerView.getHeight() / 2;
    563         final int revealCenterX = clearLocation[0] - revealView.getLeft();
    564         final int revealCenterY = clearLocation[1] - revealView.getTop();
    565 
    566         final int xMax = Math.max(revealCenterX, decorView.getWidth() - revealCenterX);
    567         final int yMax = Math.max(revealCenterY, decorView.getHeight() - revealCenterY);
    568         final float revealRadius = (float) Math.sqrt(Math.pow(xMax, 2.0) + Math.pow(yMax, 2.0));
    569 
    570         final Animator revealAnimator = ViewAnimationUtils.createCircularReveal(
    571                 revealView, revealCenterX, revealCenterY, 0.0f, revealRadius);
    572         revealAnimator.setInterpolator(REVEAL_INTERPOLATOR);
    573 
    574         final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 1.0f);
    575         fadeAnimator.addListener(new AnimatorListenerAdapter() {
    576             @Override
    577             public void onAnimationEnd(Animator animation) {
    578                 overlay.remove(revealView);
    579             }
    580         });
    581 
    582         final AnimatorSet alertAnimator = new AnimatorSet();
    583         alertAnimator.setDuration(TimerFragment.ANIMATION_TIME_MILLIS);
    584         alertAnimator.play(revealAnimator).before(fadeAnimator);
    585         alertAnimator.start();
    586     }
    587 
    588     @Override
    589     public void onPause() {
    590         if (getActivity() instanceof DeskClock) {
    591             ((DeskClock) getActivity()).unregisterPageChangedListener(this);
    592         }
    593         super.onPause();
    594         stopClockTicks();
    595         if (mAdapter != null) {
    596             mAdapter.saveGlobalState();
    597         }
    598         mPrefs.unregisterOnSharedPreferenceChangeListener(this);
    599         // This is called because the lock screen was activated, the window stay
    600         // active under it and when we unlock the screen, we see the old time for
    601         // a fraction of a second.
    602         View v = getView();
    603         if (v != null) {
    604             v.setVisibility(View.INVISIBLE);
    605         }
    606     }
    607 
    608     @Override
    609     public void onPageChanged(int page) {
    610         if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) {
    611             mAdapter.sort();
    612         }
    613     }
    614 
    615     @Override
    616     public void onSaveInstanceState(Bundle outState) {
    617         super.onSaveInstanceState(outState);
    618         if (mAdapter != null) {
    619             mAdapter.onSaveInstanceState(outState);
    620         }
    621         if (mTimerSetup != null) {
    622             saveViewState(outState);
    623         } else if (mViewState != null) {
    624             outState.putAll(mViewState);
    625         }
    626     }
    627 
    628     private void saveViewState(Bundle outState) {
    629         mTimerSetup.saveEntryState(outState, KEY_ENTRY_STATE);
    630     }
    631 
    632     public void setPage() {
    633         boolean switchToSetupView;
    634         if (mViewState != null) {
    635             switchToSetupView = false;
    636             mTimerSetup.restoreEntryState(mViewState, KEY_ENTRY_STATE);
    637             mViewState = null;
    638         } else {
    639             switchToSetupView = mAdapter.getCount() == 0;
    640         }
    641         if (switchToSetupView) {
    642             gotoSetupView();
    643         } else {
    644             gotoTimersView();
    645         }
    646     }
    647 
    648     private void resetTimer(TimerObj t) {
    649         t.mState = TimerObj.STATE_RESTART;
    650         t.mTimeLeft = t.mOriginalLength = t.mSetupLength;
    651         ((TimerListItem) t.mView).stop();
    652         ((TimerListItem) t.mView).setTime(t.mTimeLeft, false);
    653         ((TimerListItem) t.mView).set(t.mOriginalLength, t.mTimeLeft, false);
    654         updateTimersState(t, Timers.TIMER_RESET);
    655     }
    656 
    657     public void updateAllTimesUpTimers(boolean stop) {
    658         boolean notifyChange = false;
    659         //  To avoid race conditions where a timer was dismissed and it is still in the timers list
    660         // and can be picked again, create a temporary list of timers to be removed first and
    661         // then removed them one by one
    662         LinkedList<TimerObj> timesupTimers = new LinkedList<TimerObj>();
    663         for (int i = 0; i < mAdapter.getCount(); i++) {
    664             TimerObj timerObj = mAdapter.getItem(i);
    665             if (timerObj.mState == TimerObj.STATE_TIMESUP) {
    666                 timesupTimers.addFirst(timerObj);
    667                 notifyChange = true;
    668             }
    669         }
    670 
    671         while (timesupTimers.size() > 0) {
    672             final TimerObj t = timesupTimers.remove();
    673             if (stop) {
    674                 onStopButtonPressed(t);
    675             } else {
    676                 resetTimer(t);
    677             }
    678         }
    679 
    680         if (notifyChange) {
    681             SharedPreferences.Editor editor = mPrefs.edit();
    682             editor.putBoolean(Timers.FROM_ALERT, true);
    683             editor.apply();
    684         }
    685     }
    686 
    687     private void gotoSetupView() {
    688         if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timer_setup) {
    689             mTimerSetup.setVisibility(View.VISIBLE);
    690             mTimerSetup.setScaleX(1f);
    691             mTimersListPage.setVisibility(View.GONE);
    692         } else {
    693             // Animate
    694             ObjectAnimator a = ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 1f, 0f);
    695             a.setInterpolator(new AccelerateInterpolator());
    696             a.setDuration(125);
    697             a.addListener(new AnimatorListenerAdapter() {
    698                 @Override
    699                 public void onAnimationEnd(Animator animation) {
    700                     mTimersListPage.setVisibility(View.GONE);
    701                     mTimerSetup.setScaleX(0);
    702                     mTimerSetup.setVisibility(View.VISIBLE);
    703                     ObjectAnimator b = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 0f, 1f);
    704                     b.setInterpolator(new DecelerateInterpolator());
    705                     b.setDuration(225);
    706                     b.start();
    707                 }
    708             });
    709             a.start();
    710 
    711         }
    712         stopClockTicks();
    713         mTimerSetup.updateDeleteButtonAndDivider();
    714         mLastVisibleView = mTimerSetup;
    715     }
    716 
    717     private void gotoTimersView() {
    718         if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timers_list_page) {
    719             mTimerSetup.setVisibility(View.GONE);
    720             mTimersListPage.setVisibility(View.VISIBLE);
    721             mTimersListPage.setScaleX(1f);
    722         } else {
    723             // Animate
    724             ObjectAnimator a = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 1f, 0f);
    725             a.setInterpolator(new AccelerateInterpolator());
    726             a.setDuration(125);
    727             a.addListener(new AnimatorListenerAdapter() {
    728                 @Override
    729                 public void onAnimationEnd(Animator animation) {
    730                     mTimerSetup.setVisibility(View.GONE);
    731                     mTimersListPage.setScaleX(0);
    732                     mTimersListPage.setVisibility(View.VISIBLE);
    733                     ObjectAnimator b =
    734                             ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 0f, 1f);
    735                     b.setInterpolator(new DecelerateInterpolator());
    736                     b.setDuration(225);
    737                     b.start();
    738                 }
    739             });
    740             a.start();
    741         }
    742         startClockTicks();
    743         mLastVisibleView = mTimersListPage;
    744     }
    745 
    746     @Override
    747     public void onClick(View v) {
    748         ClickAction tag = (ClickAction) v.getTag();
    749         onClickHelper(tag);
    750     }
    751 
    752     private void onClickHelper(ClickAction clickAction) {
    753         switch (clickAction.mAction) {
    754             case ClickAction.ACTION_DELETE:
    755                 final TimerObj t = clickAction.mTimer;
    756                 if (t.mState == TimerObj.STATE_TIMESUP) {
    757                     cancelTimerNotification(t.mTimerId);
    758                 }
    759                 // Tell receiver the timer was deleted.
    760                 // It will stop all activity related to the
    761                 // timer
    762                 t.mState = TimerObj.STATE_DELETED;
    763                 updateTimersState(t, Timers.DELETE_TIMER);
    764                 break;
    765             case ClickAction.ACTION_PLUS_ONE:
    766                 onPlusOneButtonPressed(clickAction.mTimer);
    767                 break;
    768             case ClickAction.ACTION_STOP:
    769                 onStopButtonPressed(clickAction.mTimer);
    770                 break;
    771             default:
    772                 break;
    773         }
    774     }
    775 
    776     private void onPlusOneButtonPressed(TimerObj t) {
    777         switch (t.mState) {
    778             case TimerObj.STATE_RUNNING:
    779                 t.addTime(TimerObj.MINUTE_IN_MILLIS);
    780                 long timeLeft = t.updateTimeLeft(false);
    781                 ((TimerListItem) (t.mView)).setTime(timeLeft, false);
    782                 ((TimerListItem) (t.mView)).setLength(timeLeft);
    783                 mAdapter.notifyDataSetChanged();
    784                 updateTimersState(t, Timers.TIMER_UPDATE);
    785                 break;
    786             case TimerObj.STATE_TIMESUP:
    787                 // +1 min when the time is up will restart the timer with 1 minute left.
    788                 t.mState = TimerObj.STATE_RUNNING;
    789                 t.mStartTime = Utils.getTimeNow();
    790                 t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS;
    791                 updateTimersState(t, Timers.TIMER_RESET);
    792                 updateTimersState(t, Timers.START_TIMER);
    793                 updateTimesUpMode(t);
    794                 cancelTimerNotification(t.mTimerId);
    795                 break;
    796             case TimerObj.STATE_STOPPED:
    797             case TimerObj.STATE_DONE:
    798                 t.mState = TimerObj.STATE_RESTART;
    799                 t.mTimeLeft = t.mOriginalLength = t.mSetupLength;
    800                 ((TimerListItem) t.mView).stop();
    801                 ((TimerListItem) t.mView).setTime(t.mTimeLeft, false);
    802                 ((TimerListItem) t.mView).set(t.mOriginalLength, t.mTimeLeft, false);
    803                 updateTimersState(t, Timers.TIMER_RESET);
    804                 break;
    805             default:
    806                 break;
    807         }
    808     }
    809 
    810     private void onStopButtonPressed(TimerObj t) {
    811         switch (t.mState) {
    812             case TimerObj.STATE_RUNNING:
    813                 // Stop timer and save the remaining time of the timer
    814                 t.mState = TimerObj.STATE_STOPPED;
    815                 ((TimerListItem) t.mView).pause();
    816                 t.updateTimeLeft(true);
    817                 updateTimersState(t, Timers.TIMER_STOP);
    818                 break;
    819             case TimerObj.STATE_STOPPED:
    820                 // Reset the remaining time and continue timer
    821                 t.mState = TimerObj.STATE_RUNNING;
    822                 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft);
    823                 ((TimerListItem) t.mView).start();
    824                 updateTimersState(t, Timers.START_TIMER);
    825                 break;
    826             case TimerObj.STATE_TIMESUP:
    827                 if (t.mDeleteAfterUse) {
    828                     cancelTimerNotification(t.mTimerId);
    829                     // Tell receiver the timer was deleted.
    830                     // It will stop all activity related to the
    831                     // timer
    832                     t.mState = TimerObj.STATE_DELETED;
    833                     updateTimersState(t, Timers.DELETE_TIMER);
    834                 } else {
    835                     t.mState = TimerObj.STATE_DONE;
    836                     // Used in a context where the timer could be off-screen and without a view
    837                     if (t.mView != null) {
    838                         ((TimerListItem) t.mView).done();
    839                     }
    840                     updateTimersState(t, Timers.TIMER_DONE);
    841                     cancelTimerNotification(t.mTimerId);
    842                     updateTimesUpMode(t);
    843                 }
    844                 break;
    845             case TimerObj.STATE_DONE:
    846                 break;
    847             case TimerObj.STATE_RESTART:
    848                 t.mState = TimerObj.STATE_RUNNING;
    849                 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft);
    850                 ((TimerListItem) t.mView).start();
    851                 updateTimersState(t, Timers.START_TIMER);
    852                 break;
    853             default:
    854                 break;
    855         }
    856     }
    857 
    858     private void onLabelPressed(TimerObj t) {
    859         final FragmentTransaction ft = getFragmentManager().beginTransaction();
    860         final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog");
    861         if (prev != null) {
    862             ft.remove(prev);
    863         }
    864         ft.addToBackStack(null);
    865 
    866         // Create and show the dialog.
    867         final LabelDialogFragment newFragment =
    868                 LabelDialogFragment.newInstance(t, t.mLabel, getTag());
    869         newFragment.show(ft, "label_dialog");
    870     }
    871 
    872     // Starts the ticks that animate the timers.
    873     private void startClockTicks() {
    874         mTimersList.postDelayed(mClockTick, 20);
    875         mTicking = true;
    876     }
    877 
    878     // Stops the ticks that animate the timers.
    879     private void stopClockTicks() {
    880         if (mTicking) {
    881             mTimersList.removeCallbacks(mClockTick);
    882             mTicking = false;
    883         }
    884     }
    885 
    886     private void updateTimersState(TimerObj t, String action) {
    887         if (Timers.DELETE_TIMER.equals(action)) {
    888             LogUtils.e("~~ update timer state");
    889             t.deleteFromSharedPref(mPrefs);
    890         } else {
    891             t.writeToSharedPref(mPrefs);
    892         }
    893         Intent i = new Intent();
    894         i.setAction(action);
    895         i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId);
    896         // Make sure the receiver is getting the intent ASAP.
    897         i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    898         getActivity().sendBroadcast(i);
    899     }
    900 
    901     private void cancelTimerNotification(int timerId) {
    902         mNotificationManager.cancel(timerId);
    903     }
    904 
    905     private void updateTimesUpMode(TimerObj timerObj) {
    906         if (mOnEmptyListListener != null && timerObj.mState != TimerObj.STATE_TIMESUP) {
    907             mAdapter.removeTimer(timerObj);
    908             if (mAdapter.getCount() == 0) {
    909                 mOnEmptyListListener.onEmptyList();
    910             } else {
    911                 mOnEmptyListListener.onListChanged();
    912             }
    913         }
    914     }
    915 
    916     public void restartAdapter() {
    917         mAdapter = createAdapter(getActivity(), mPrefs);
    918         mAdapter.onRestoreInstanceState(null);
    919     }
    920 
    921     // Process extras that were sent to the app and were intended for the timer
    922     // fragment
    923     public void processIntent(Intent intent) {
    924         // switch to timer setup view
    925         if (intent.getBooleanExtra(GOTO_SETUP_VIEW, false)) {
    926             gotoSetupView();
    927         }
    928     }
    929 
    930     @Override
    931     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    932         if (prefs.equals(mPrefs)) {
    933             if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false))
    934                     || (key.equals(Timers.FROM_NOTIFICATION)
    935                     && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) {
    936                 // The data-changed flag was set in the alert or notification so the adapter needs
    937                 // to re-sync with the database
    938                 SharedPreferences.Editor editor = mPrefs.edit();
    939                 editor.putBoolean(key, false);
    940                 editor.apply();
    941                 mAdapter = createAdapter(getActivity(), mPrefs);
    942                 mAdapter.onRestoreInstanceState(null);
    943                 mTimersList.setAdapter(mAdapter);
    944             }
    945         }
    946     }
    947 
    948     @Override
    949     public void onFabClick(View view) {
    950         if (mLastVisibleView != mTimersListPage) {
    951             // New timer create if timer length is not zero
    952             // Create a new timer object to track the timer and
    953             // switch to the timers view.
    954             int timerLength = mTimerSetup.getTime();
    955             if (timerLength == 0) {
    956                 return;
    957             }
    958             TimerObj t = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS);
    959             t.mState = TimerObj.STATE_RUNNING;
    960             mAdapter.addTimer(t);
    961             updateTimersState(t, Timers.START_TIMER);
    962             gotoTimersView();
    963             mTimerSetup.reset(); // Make sure the setup is cleared for next time
    964 
    965             mTimersList.setFirstPositionAndOffsets(
    966                     mAdapter.findTimerPositionById(t.mTimerId), 0);
    967         } else {
    968             mTimerSetup.reset();
    969             gotoSetupView();
    970         }
    971     }
    972 }
    973