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