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