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 
652         // when multiple timers are firing, some timers will be off-screen and they will not
653         // have Fragment instances unless user scrolls down further. t.mView is null in this case.
654         if (t.mView != null) {
655             t.mView.stop();
656             t.mView.setTime(t.mTimeLeft, false);
657             t.mView.set(t.mOriginalLength, t.mTimeLeft, false);
658         }
659         updateTimersState(t, Timers.TIMER_RESET);
660     }
661 
updateAllTimesUpTimers(boolean stop)662     public void updateAllTimesUpTimers(boolean stop) {
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             final TimerObj t = timesupTimers.remove();
678             if (stop) {
679                 onStopButtonPressed(t);
680             } else {
681                 resetTimer(t);
682             }
683         }
684 
685         if (notifyChange) {
686             SharedPreferences.Editor editor = mPrefs.edit();
687             editor.putBoolean(Timers.FROM_ALERT, true);
688             editor.apply();
689         }
690     }
691 
gotoSetupView()692     private void gotoSetupView() {
693         if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timer_setup) {
694             mTimerSetup.setVisibility(View.VISIBLE);
695             mTimerSetup.setScaleX(1f);
696             mTimersListPage.setVisibility(View.GONE);
697         } else {
698             // Animate
699             ObjectAnimator a = ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 1f, 0f);
700             a.setInterpolator(new AccelerateInterpolator());
701             a.setDuration(125);
702             a.addListener(new AnimatorListenerAdapter() {
703                 @Override
704                 public void onAnimationEnd(Animator animation) {
705                     mTimersListPage.setVisibility(View.GONE);
706                     mTimerSetup.setScaleX(0);
707                     mTimerSetup.setVisibility(View.VISIBLE);
708                     ObjectAnimator b = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 0f, 1f);
709                     b.setInterpolator(new DecelerateInterpolator());
710                     b.setDuration(225);
711                     b.start();
712                 }
713             });
714             a.start();
715 
716         }
717         stopClockTicks();
718         mTimerSetup.updateDeleteButtonAndDivider();
719         mLastVisibleView = mTimerSetup;
720     }
721 
gotoTimersView()722     private void gotoTimersView() {
723         if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timers_list_page) {
724             mTimerSetup.setVisibility(View.GONE);
725             mTimersListPage.setVisibility(View.VISIBLE);
726             mTimersListPage.setScaleX(1f);
727         } else {
728             // Animate
729             ObjectAnimator a = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 1f, 0f);
730             a.setInterpolator(new AccelerateInterpolator());
731             a.setDuration(125);
732             a.addListener(new AnimatorListenerAdapter() {
733                 @Override
734                 public void onAnimationEnd(Animator animation) {
735                     mTimerSetup.setVisibility(View.GONE);
736                     mTimersListPage.setScaleX(0);
737                     mTimersListPage.setVisibility(View.VISIBLE);
738                     ObjectAnimator b =
739                             ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 0f, 1f);
740                     b.setInterpolator(new DecelerateInterpolator());
741                     b.setDuration(225);
742                     b.start();
743                 }
744             });
745             a.start();
746         }
747         startClockTicks();
748         mLastVisibleView = mTimersListPage;
749     }
750 
751     @Override
onClick(View v)752     public void onClick(View v) {
753         ClickAction tag = (ClickAction) v.getTag();
754         onClickHelper(tag);
755     }
756 
onClickHelper(ClickAction clickAction)757     private void onClickHelper(ClickAction clickAction) {
758         switch (clickAction.mAction) {
759             case ClickAction.ACTION_DELETE:
760                 final TimerObj t = clickAction.mTimer;
761                 if (t.mState == TimerObj.STATE_TIMESUP) {
762                     cancelTimerNotification(t.mTimerId);
763                 }
764                 // Tell receiver the timer was deleted.
765                 // It will stop all activity related to the
766                 // timer
767                 t.mState = TimerObj.STATE_DELETED;
768                 updateTimersState(t, Timers.DELETE_TIMER);
769                 break;
770             case ClickAction.ACTION_PLUS_ONE:
771                 onPlusOneButtonPressed(clickAction.mTimer);
772                 break;
773             case ClickAction.ACTION_STOP:
774                 onStopButtonPressed(clickAction.mTimer);
775                 break;
776             default:
777                 break;
778         }
779     }
780 
onPlusOneButtonPressed(TimerObj t)781     private void onPlusOneButtonPressed(TimerObj t) {
782         switch (t.mState) {
783             case TimerObj.STATE_RUNNING:
784                 t.addTime(TimerObj.MINUTE_IN_MILLIS);
785                 long timeLeft = t.updateTimeLeft(false);
786                 ((TimerListItem) (t.mView)).setTime(timeLeft, false);
787                 ((TimerListItem) (t.mView)).setLength(timeLeft);
788                 mAdapter.notifyDataSetChanged();
789                 updateTimersState(t, Timers.TIMER_UPDATE);
790                 break;
791             case TimerObj.STATE_TIMESUP:
792                 // +1 min when the time is up will restart the timer with 1 minute left.
793                 t.mState = TimerObj.STATE_RUNNING;
794                 t.mStartTime = Utils.getTimeNow();
795                 t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS;
796                 updateTimersState(t, Timers.TIMER_RESET);
797                 updateTimersState(t, Timers.START_TIMER);
798                 updateTimesUpMode(t);
799                 cancelTimerNotification(t.mTimerId);
800                 break;
801             case TimerObj.STATE_STOPPED:
802             case TimerObj.STATE_DONE:
803                 t.mState = TimerObj.STATE_RESTART;
804                 t.mTimeLeft = t.mOriginalLength = t.mSetupLength;
805                 ((TimerListItem) t.mView).stop();
806                 ((TimerListItem) t.mView).setTime(t.mTimeLeft, false);
807                 ((TimerListItem) t.mView).set(t.mOriginalLength, t.mTimeLeft, false);
808                 updateTimersState(t, Timers.TIMER_RESET);
809                 break;
810             default:
811                 break;
812         }
813     }
814 
onStopButtonPressed(TimerObj t)815     private void onStopButtonPressed(TimerObj t) {
816         switch (t.mState) {
817             case TimerObj.STATE_RUNNING:
818                 // Stop timer and save the remaining time of the timer
819                 t.mState = TimerObj.STATE_STOPPED;
820                 ((TimerListItem) t.mView).pause();
821                 t.updateTimeLeft(true);
822                 updateTimersState(t, Timers.TIMER_STOP);
823                 break;
824             case TimerObj.STATE_STOPPED:
825                 // Reset the remaining time and continue timer
826                 t.mState = TimerObj.STATE_RUNNING;
827                 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft);
828                 ((TimerListItem) t.mView).start();
829                 updateTimersState(t, Timers.START_TIMER);
830                 break;
831             case TimerObj.STATE_TIMESUP:
832                 if (t.mDeleteAfterUse) {
833                     cancelTimerNotification(t.mTimerId);
834                     // Tell receiver the timer was deleted.
835                     // It will stop all activity related to the
836                     // timer
837                     t.mState = TimerObj.STATE_DELETED;
838                     updateTimersState(t, Timers.DELETE_TIMER);
839                 } else {
840                     t.mState = TimerObj.STATE_DONE;
841                     // Used in a context where the timer could be off-screen and without a view
842                     if (t.mView != null) {
843                         ((TimerListItem) t.mView).done();
844                     }
845                     updateTimersState(t, Timers.TIMER_DONE);
846                     cancelTimerNotification(t.mTimerId);
847                     updateTimesUpMode(t);
848                 }
849                 break;
850             case TimerObj.STATE_DONE:
851                 break;
852             case TimerObj.STATE_RESTART:
853                 t.mState = TimerObj.STATE_RUNNING;
854                 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft);
855                 ((TimerListItem) t.mView).start();
856                 updateTimersState(t, Timers.START_TIMER);
857                 break;
858             default:
859                 break;
860         }
861     }
862 
onLabelPressed(TimerObj t)863     private void onLabelPressed(TimerObj t) {
864         final FragmentTransaction ft = getFragmentManager().beginTransaction();
865         final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog");
866         if (prev != null) {
867             ft.remove(prev);
868         }
869         ft.addToBackStack(null);
870 
871         // Create and show the dialog.
872         final LabelDialogFragment newFragment =
873                 LabelDialogFragment.newInstance(t, t.mLabel, getTag());
874         newFragment.show(ft, "label_dialog");
875     }
876 
877     // Starts the ticks that animate the timers.
startClockTicks()878     private void startClockTicks() {
879         mTimersList.postDelayed(mClockTick, 20);
880         mTicking = true;
881     }
882 
883     // Stops the ticks that animate the timers.
stopClockTicks()884     private void stopClockTicks() {
885         if (mTicking) {
886             mTimersList.removeCallbacks(mClockTick);
887             mTicking = false;
888         }
889     }
890 
updateTimersState(TimerObj t, String action)891     private void updateTimersState(TimerObj t, String action) {
892         if (Timers.DELETE_TIMER.equals(action)) {
893             LogUtils.e("~~ update timer state");
894             t.deleteFromSharedPref(mPrefs);
895         } else {
896             t.writeToSharedPref(mPrefs);
897         }
898         Intent i = new Intent();
899         i.setAction(action);
900         i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId);
901         // Make sure the receiver is getting the intent ASAP.
902         i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
903         getActivity().sendBroadcast(i);
904     }
905 
cancelTimerNotification(int timerId)906     private void cancelTimerNotification(int timerId) {
907         mNotificationManager.cancel(timerId);
908     }
909 
updateTimesUpMode(TimerObj timerObj)910     private void updateTimesUpMode(TimerObj timerObj) {
911         if (mOnEmptyListListener != null && timerObj.mState != TimerObj.STATE_TIMESUP) {
912             mAdapter.removeTimer(timerObj);
913             if (mAdapter.getCount() == 0) {
914                 mOnEmptyListListener.onEmptyList();
915             } else {
916                 mOnEmptyListListener.onListChanged();
917             }
918         }
919     }
920 
restartAdapter()921     public void restartAdapter() {
922         mAdapter = createAdapter(getActivity(), mPrefs);
923         mAdapter.onRestoreInstanceState(null);
924     }
925 
926     // Process extras that were sent to the app and were intended for the timer
927     // fragment
processIntent(Intent intent)928     public void processIntent(Intent intent) {
929         // switch to timer setup view
930         if (intent.getBooleanExtra(GOTO_SETUP_VIEW, false)) {
931             gotoSetupView();
932         }
933     }
934 
935     @Override
onSharedPreferenceChanged(SharedPreferences prefs, String key)936     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
937         if (prefs.equals(mPrefs)) {
938             if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false))
939                     || (key.equals(Timers.FROM_NOTIFICATION)
940                     && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) {
941                 // The data-changed flag was set in the alert or notification so the adapter needs
942                 // to re-sync with the database
943                 SharedPreferences.Editor editor = mPrefs.edit();
944                 editor.putBoolean(key, false);
945                 editor.apply();
946                 mAdapter = createAdapter(getActivity(), mPrefs);
947                 mAdapter.onRestoreInstanceState(null);
948                 mTimersList.setAdapter(mAdapter);
949             }
950         }
951     }
952 
953     @Override
onFabClick(View view)954     public void onFabClick(View view) {
955         if (mLastVisibleView != mTimersListPage) {
956             // New timer create if timer length is not zero
957             // Create a new timer object to track the timer and
958             // switch to the timers view.
959             int timerLength = mTimerSetup.getTime();
960             if (timerLength == 0) {
961                 return;
962             }
963             TimerObj t = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS, getActivity());
964             t.mState = TimerObj.STATE_RUNNING;
965             mAdapter.addTimer(t);
966             updateTimersState(t, Timers.START_TIMER);
967             gotoTimersView();
968             mTimerSetup.reset(); // Make sure the setup is cleared for next time
969 
970             mTimersList.setFirstPositionAndOffsets(
971                     mAdapter.findTimerPositionById(t.mTimerId), 0);
972         } else {
973             mTimerSetup.reset();
974             gotoSetupView();
975         }
976     }
977 }
978