• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ClickAction(int action, TimerObj t)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 {
onEmptyList()111         public void onEmptyList();
112 
onListChanged()113         public void onListChanged();
114     }
115 
createAdapter(Context context, SharedPreferences prefs)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 
clear()130         private void clear() {
131             mTimers.clear();
132             notifyDataSetChanged();
133         }
134 
TimersListAdapter(Context context, SharedPreferences prefs)135         public TimersListAdapter(Context context, SharedPreferences prefs) {
136             mContext = context;
137             mmPrefs = prefs;
138         }
139 
140         @Override
getCount()141         public int getCount() {
142             return mTimers.size();
143         }
144 
145         @Override
hasStableIds()146         public boolean hasStableIds() {
147             return true;
148         }
149 
150         @Override
getItem(int p)151         public TimerObj getItem(int p) {
152             return mTimers.get(p);
153         }
154 
155         @Override
getItemId(int p)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 
deleteTimer(int id)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 
findTimerPositionById(int id)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 
removeTimer(TimerObj timerObj)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
getView(int position, View convertView, ViewGroup parent)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
getItemColumnSpan(Object item, int position)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 
addTimer(TimerObj t)287         public void addTimer(TimerObj t) {
288             mTimers.add(0, t);
289             sort();
290         }
291 
onSaveInstanceState(Bundle outState)292         public void onSaveInstanceState(Bundle outState) {
293             TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers);
294         }
295 
onRestoreInstanceState(Bundle outState)296         public void onRestoreInstanceState(Bundle outState) {
297             TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers);
298             sort();
299         }
300 
saveGlobalState()301         public void saveGlobalState() {
302             TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers);
303         }
304 
sort()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 
TimesUpListAdapter(Context context, SharedPreferences prefs)346         public TimesUpListAdapter(Context context, SharedPreferences prefs) {
347             super(context, prefs);
348         }
349 
350         @Override
onSaveInstanceState(Bundle outState)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
saveGlobalState()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
onRestoreInstanceState(Bundle outState)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
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onDestroyView()469     public void onDestroyView() {
470         mViewState = new Bundle();
471         saveViewState(mViewState);
472         super.onDestroyView();
473     }
474 
475     @Override
onResume()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 
revealAnimation(final View centerView, int color)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
onPause()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
onPageChanged(int page)609     public void onPageChanged(int page) {
610         if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) {
611             mAdapter.sort();
612         }
613     }
614 
615     @Override
onSaveInstanceState(Bundle outState)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 
saveViewState(Bundle outState)628     private void saveViewState(Bundle outState) {
629         mTimerSetup.saveEntryState(outState, KEY_ENTRY_STATE);
630     }
631 
setPage()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 
resetTimer(TimerObj t)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 
updateAllTimesUpTimers(boolean stop)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 
gotoSetupView()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 
gotoTimersView()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
onClick(View v)747     public void onClick(View v) {
748         ClickAction tag = (ClickAction) v.getTag();
749         onClickHelper(tag);
750     }
751 
onClickHelper(ClickAction clickAction)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 
onPlusOneButtonPressed(TimerObj t)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 
onStopButtonPressed(TimerObj t)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 
onLabelPressed(TimerObj t)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.
startClockTicks()873     private void startClockTicks() {
874         mTimersList.postDelayed(mClockTick, 20);
875         mTicking = true;
876     }
877 
878     // Stops the ticks that animate the timers.
stopClockTicks()879     private void stopClockTicks() {
880         if (mTicking) {
881             mTimersList.removeCallbacks(mClockTick);
882             mTicking = false;
883         }
884     }
885 
updateTimersState(TimerObj t, String action)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 
cancelTimerNotification(int timerId)901     private void cancelTimerNotification(int timerId) {
902         mNotificationManager.cancel(timerId);
903     }
904 
updateTimesUpMode(TimerObj timerObj)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 
restartAdapter()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
processIntent(Intent intent)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
onSharedPreferenceChanged(SharedPreferences prefs, String key)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
onFabClick(View view)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