Home | History | Annotate | Download | only in timer
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.deskclock.timer;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.app.Fragment;
     23 import android.app.FragmentTransaction;
     24 import android.app.NotificationManager;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.SharedPreferences;
     28 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
     29 import android.content.res.Configuration;
     30 import android.content.res.Resources;
     31 import android.os.Bundle;
     32 import android.preference.PreferenceManager;
     33 import android.util.Log;
     34 import android.view.Gravity;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.view.View.OnClickListener;
     38 import android.view.ViewGroup;
     39 import android.view.ViewGroup.LayoutParams;
     40 import android.view.animation.AccelerateInterpolator;
     41 import android.view.animation.DecelerateInterpolator;
     42 import android.widget.Button;
     43 import android.widget.FrameLayout;
     44 import android.widget.ImageButton;
     45 import android.widget.TextView;
     46 
     47 import com.android.deskclock.CircleButtonsLayout;
     48 import com.android.deskclock.DeskClock;
     49 import com.android.deskclock.DeskClock.OnTapListener;
     50 import com.android.deskclock.DeskClockFragment;
     51 import com.android.deskclock.LabelDialogFragment;
     52 import com.android.deskclock.R;
     53 import com.android.deskclock.TimerSetupView;
     54 import com.android.deskclock.Utils;
     55 
     56 import java.util.ArrayList;
     57 import java.util.Collections;
     58 import java.util.Comparator;
     59 import java.util.LinkedList;
     60 
     61 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn;
     62 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut;
     63 import com.android.deskclock.widget.sgv.StaggeredGridView;
     64 import com.android.deskclock.widget.sgv.GridAdapter;
     65 
     66 public 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