/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deskclock.timer; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroupOverlay; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; import com.android.deskclock.CircleButtonsLayout; import com.android.deskclock.DeskClock; import com.android.deskclock.DeskClock.OnTapListener; import com.android.deskclock.DeskClockFragment; import com.android.deskclock.LabelDialogFragment; import com.android.deskclock.LogUtils; import com.android.deskclock.R; import com.android.deskclock.TimerSetupView; import com.android.deskclock.Utils; import com.android.deskclock.widget.sgv.GridAdapter; import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn; import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut; import com.android.deskclock.widget.sgv.StaggeredGridView; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; // TODO: This class is renamed from TimerFragment to TimerFullScreenFragment with no change. It // is responsible for the timer list in full screen timer alert and should be deprecated shortly. public class TimerFullScreenFragment extends DeskClockFragment implements OnClickListener, OnSharedPreferenceChangeListener { private static final String TAG = "TimerFragment1"; private static final String KEY_ENTRY_STATE = "entry_state"; private static final Interpolator REVEAL_INTERPOLATOR = new PathInterpolator(0.0f, 0.0f, 0.2f, 1.0f); public static final String GOTO_SETUP_VIEW = "deskclock.timers.gotosetup"; private Bundle mViewState; private StaggeredGridView mTimersList; private View mTimersListPage; private int mColumnCount; private ImageButton mFab; private TimerSetupView mTimerSetup; private TimersListAdapter mAdapter; private boolean mTicking = false; private SharedPreferences mPrefs; private NotificationManager mNotificationManager; private OnEmptyListListener mOnEmptyListListener; private View mLastVisibleView = null; // used to decide if to set the view or animate to it. class ClickAction { public static final int ACTION_STOP = 1; public static final int ACTION_PLUS_ONE = 2; public static final int ACTION_DELETE = 3; public int mAction; public TimerObj mTimer; public ClickAction(int action, TimerObj t) { mAction = action; mTimer = t; } } // Container Activity that requests TIMESUP_MODE must implement this interface public interface OnEmptyListListener { public void onEmptyList(); public void onListChanged(); } TimersListAdapter createAdapter(Context context, SharedPreferences prefs) { if (mOnEmptyListListener == null) { return new TimersListAdapter(context, prefs); } else { return new TimesUpListAdapter(context, prefs); } } private class TimersListAdapter extends GridAdapter { ArrayList mTimers = new ArrayList(); Context mContext; SharedPreferences mmPrefs; private void clear() { mTimers.clear(); notifyDataSetChanged(); } public TimersListAdapter(Context context, SharedPreferences prefs) { mContext = context; mmPrefs = prefs; } @Override public int getCount() { return mTimers.size(); } @Override public boolean hasStableIds() { return true; } @Override public TimerObj getItem(int p) { return mTimers.get(p); } @Override public long getItemId(int p) { if (p >= 0 && p < mTimers.size()) { return mTimers.get(p).mTimerId; } return 0; } public void deleteTimer(int id) { for (int i = 0; i < mTimers.size(); i++) { TimerObj t = mTimers.get(i); if (t.mTimerId == id) { if (t.mView != null) { ((TimerListItem) t.mView).stop(); } t.deleteFromSharedPref(mmPrefs); mTimers.remove(i); if (mTimers.size() == 1 && mColumnCount > 1) { // If we're going from two timers to one (in the same row), we don't want to // animate the translation because we're changing the layout params span // from 1 to 2, and the animation doesn't handle that very well. So instead, // just fade out and in. mTimersList.setAnimationMode(AnimationIn.FADE, AnimationOut.FADE); } else { mTimersList.setAnimationMode( AnimationIn.FLY_IN_NEW_VIEWS, AnimationOut.FADE); } notifyDataSetChanged(); return; } } } protected int findTimerPositionById(int id) { for (int i = 0; i < mTimers.size(); i++) { TimerObj t = mTimers.get(i); if (t.mTimerId == id) { return i; } } return -1; } public void removeTimer(TimerObj timerObj) { int position = findTimerPositionById(timerObj.mTimerId); if (position >= 0) { mTimers.remove(position); notifyDataSetChanged(); } } @Override public View getView(int position, View convertView, ViewGroup parent) { final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); final TimerListItem v = (TimerListItem) inflater.inflate(R.layout.timer_list_item, null); final TimerObj o = (TimerObj) getItem(position); o.mView = v; long timeLeft = o.updateTimeLeft(false); boolean drawRed = o.mState != TimerObj.STATE_RESTART; v.set(o.mOriginalLength, timeLeft, drawRed); v.setTime(timeLeft, true); switch (o.mState) { case TimerObj.STATE_RUNNING: v.start(); break; case TimerObj.STATE_TIMESUP: v.timesUp(); break; case TimerObj.STATE_DONE: v.done(); break; default: break; } // Timer text serves as a virtual start/stop button. final CountingTimerView countingTimerView = (CountingTimerView) v.findViewById(R.id.timer_time_text); countingTimerView.registerVirtualButtonAction(new Runnable() { @Override public void run() { TimerFullScreenFragment.this.onClickHelper( new ClickAction(ClickAction.ACTION_STOP, o)); } }); CircleButtonsLayout circleLayout = (CircleButtonsLayout) v.findViewById(R.id.timer_circle); circleLayout.setCircleTimerViewIds(R.id.timer_time, R.id.reset_add, R.id.timer_label, R.id.timer_label_text); ImageButton resetAddButton = (ImageButton) v.findViewById(R.id.reset_add); resetAddButton.setTag(new ClickAction(ClickAction.ACTION_PLUS_ONE, o)); v.setResetAddButton(true, TimerFullScreenFragment.this); FrameLayout label = (FrameLayout) v.findViewById(R.id.timer_label); TextView labelIcon = (TextView) v.findViewById(R.id.timer_label_placeholder); TextView labelText = (TextView) v.findViewById(R.id.timer_label_text); if (o.mLabel.equals("")) { labelText.setVisibility(View.GONE); labelIcon.setVisibility(View.VISIBLE); } else { labelText.setText(o.mLabel); labelText.setVisibility(View.VISIBLE); labelIcon.setVisibility(View.GONE); } if (getActivity() instanceof DeskClock) { label.setOnTouchListener(new OnTapListener(getActivity(), labelText) { @Override protected void processClick(View v) { onLabelPressed(o); } }); } else { labelIcon.setVisibility(View.INVISIBLE); } return v; } @Override public int getItemColumnSpan(Object item, int position) { // This returns the width for a specified position. If we only have one item, have it // span all columns so that it's centered. Otherwise, all timers should just span one. if (getCount() == 1) { return mColumnCount; } else { return 1; } } public void addTimer(TimerObj t) { mTimers.add(0, t); sort(); } public void onSaveInstanceState(Bundle outState) { TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers); } public void onRestoreInstanceState(Bundle outState) { TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers); sort(); } public void saveGlobalState() { TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers); } public void sort() { if (getCount() > 0) { Collections.sort(mTimers, mTimersCompare); notifyDataSetChanged(); } } private final Comparator mTimersCompare = new Comparator() { static final int BUZZING = 0; static final int IN_USE = 1; static final int NOT_USED = 2; protected int getSection(TimerObj timerObj) { switch (timerObj.mState) { case TimerObj.STATE_TIMESUP: return BUZZING; case TimerObj.STATE_RUNNING: case TimerObj.STATE_STOPPED: return IN_USE; default: return NOT_USED; } } @Override public int compare(TimerObj o1, TimerObj o2) { int section1 = getSection(o1); int section2 = getSection(o2); if (section1 != section2) { return (section1 < section2) ? -1 : 1; } else if (section1 == BUZZING || section1 == IN_USE) { return (o1.mTimeLeft < o2.mTimeLeft) ? -1 : 1; } else { return (o1.mSetupLength < o2.mSetupLength) ? -1 : 1; } } }; } private class TimesUpListAdapter extends TimersListAdapter { public TimesUpListAdapter(Context context, SharedPreferences prefs) { super(context, prefs); } @Override public void onSaveInstanceState(Bundle outState) { // This adapter has a data subset and never updates entire database // Individual timers are updated in button handlers. } @Override public void saveGlobalState() { // This adapter has a data subset and never updates entire database // Individual timers are updated in button handlers. } @Override public void onRestoreInstanceState(Bundle outState) { // This adapter loads a subset TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers, TimerObj.STATE_TIMESUP); if (getCount() == 0) { mOnEmptyListListener.onEmptyList(); } else { Collections.sort(mTimers, new Comparator() { @Override public int compare(TimerObj o1, TimerObj o2) { return (o1.mTimeLeft < o2.mTimeLeft) ? -1 : 1; } }); } } } private final Runnable mClockTick = new Runnable() { boolean mVisible = true; final static int TIME_PERIOD_MS = 1000; final static int SPLIT = TIME_PERIOD_MS / 2; @Override public void run() { // Setup for blinking boolean visible = Utils.getTimeNow() % TIME_PERIOD_MS < SPLIT; boolean toggle = mVisible != visible; mVisible = visible; for (int i = 0; i < mAdapter.getCount(); i++) { TimerObj t = mAdapter.getItem(i); if (t.mState == TimerObj.STATE_RUNNING || t.mState == TimerObj.STATE_TIMESUP) { long timeLeft = t.updateTimeLeft(false); if (t.mView != null) { ((TimerListItem) (t.mView)).setTime(timeLeft, false); } } if (t.mTimeLeft <= 0 && t.mState != TimerObj.STATE_DONE && t.mState != TimerObj.STATE_RESTART) { t.mState = TimerObj.STATE_TIMESUP; if (t.mView != null) { ((TimerListItem) (t.mView)).timesUp(); } } // The blinking if (toggle && t.mView != null) { if (t.mState == TimerObj.STATE_TIMESUP) { ((TimerListItem) (t.mView)).setCircleBlink(mVisible); } if (t.mState == TimerObj.STATE_STOPPED) { ((TimerListItem) (t.mView)).setTextBlink(mVisible); } } } mTimersList.postDelayed(mClockTick, 20); } }; @Override public void onCreate(Bundle savedInstanceState) { // Cache instance data and consume in first call to setupPage() if (savedInstanceState != null) { mViewState = savedInstanceState; } super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View v = inflater.inflate(R.layout.timer_full_screen_fragment, container, false); // Handle arguments from parent Bundle bundle = getArguments(); if (bundle != null && bundle.containsKey(Timers.TIMESUP_MODE)) { if (bundle.getBoolean(Timers.TIMESUP_MODE, false)) { try { mOnEmptyListListener = (OnEmptyListListener) getActivity(); } catch (ClassCastException e) { Log.wtf(TAG, getActivity().toString() + " must implement OnEmptyListListener"); } } } mFab = (ImageButton) v.findViewById(R.id.fab); mTimersList = (StaggeredGridView) v.findViewById(R.id.timers_list); // For tablets in landscape, the count will be 2. All else will be 1. mColumnCount = getResources().getInteger(R.integer.timer_column_count); mTimersList.setColumnCount(mColumnCount); // Set this to true; otherwise adding new views to the end of the list won't cause // everything above it to be filled in correctly. mTimersList.setGuardAgainstJaggedEdges(true); mTimersListPage = v.findViewById(R.id.timers_list_page); mTimerSetup = (TimerSetupView) v.findViewById(R.id.timer_setup); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mNotificationManager = (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE); return v; } @Override public void onDestroyView() { mViewState = new Bundle(); saveViewState(mViewState); super.onDestroyView(); } @Override public void onResume() { Intent newIntent = null; if (getActivity() instanceof DeskClock) { DeskClock activity = (DeskClock) getActivity(); activity.registerPageChangedListener(this); newIntent = activity.getIntent(); } super.onResume(); mPrefs.registerOnSharedPreferenceChangeListener(this); mAdapter = createAdapter(getActivity(), mPrefs); mAdapter.onRestoreInstanceState(null); LayoutParams params; float dividerHeight = getResources().getDimension(R.dimen.timer_divider_height); if (getActivity() instanceof DeskClock) { // If this is a DeskClock fragment (i.e. not a FullScreenTimerAlert), add a footer to // the bottom of the list so that it can scroll underneath the bottom button bar. // StaggeredGridView doesn't support a footer view, but GridAdapter does, so this // can't happen until the Adapter itself is instantiated. View footerView = getActivity().getLayoutInflater().inflate( R.layout.blank_footer_view, mTimersList, false); params = footerView.getLayoutParams(); params.height -= dividerHeight; footerView.setLayoutParams(params); mAdapter.setFooterView(footerView); } if (mPrefs.getBoolean(Timers.FROM_NOTIFICATION, false)) { // Clear the flag set in the notification because the adapter was just // created and is thus in sync with the database SharedPreferences.Editor editor = mPrefs.edit(); editor.putBoolean(Timers.FROM_NOTIFICATION, false); editor.apply(); } if (mPrefs.getBoolean(Timers.FROM_ALERT, false)) { // Clear the flag set in the alert because the adapter was just // created and is thus in sync with the database SharedPreferences.Editor editor = mPrefs.edit(); editor.putBoolean(Timers.FROM_ALERT, false); editor.apply(); } mTimersList.setAdapter(mAdapter); mLastVisibleView = null; // Force a non animation setting of the view setPage(); // View was hidden in onPause, make sure it is visible now. View v = getView(); if (v != null) { getView().setVisibility(View.VISIBLE); } if (newIntent != null) { processIntent(newIntent); } mFab.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { revealAnimation(mFab, getActivity().getResources().getColor(R.color.clock_white)); new Handler().postDelayed(new Runnable() { @Override public void run() { updateAllTimesUpTimers(false /* stop */); } }, TimerFragment.ANIMATION_TIME_MILLIS); } }); } private void revealAnimation(final View centerView, int color) { final Activity activity = getActivity(); final View decorView = activity.getWindow().getDecorView(); final ViewGroupOverlay overlay = (ViewGroupOverlay) decorView.getOverlay(); // Create a transient view for performing the reveal animation. final View revealView = new View(activity); revealView.setRight(decorView.getWidth()); revealView.setBottom(decorView.getHeight()); revealView.setBackgroundColor(color); overlay.add(revealView); final int[] clearLocation = new int[2]; centerView.getLocationInWindow(clearLocation); clearLocation[0] += centerView.getWidth() / 2; clearLocation[1] += centerView.getHeight() / 2; final int revealCenterX = clearLocation[0] - revealView.getLeft(); final int revealCenterY = clearLocation[1] - revealView.getTop(); final int xMax = Math.max(revealCenterX, decorView.getWidth() - revealCenterX); final int yMax = Math.max(revealCenterY, decorView.getHeight() - revealCenterY); final float revealRadius = (float) Math.sqrt(Math.pow(xMax, 2.0) + Math.pow(yMax, 2.0)); final Animator revealAnimator = ViewAnimationUtils.createCircularReveal( revealView, revealCenterX, revealCenterY, 0.0f, revealRadius); revealAnimator.setInterpolator(REVEAL_INTERPOLATOR); final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 1.0f); fadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { overlay.remove(revealView); } }); final AnimatorSet alertAnimator = new AnimatorSet(); alertAnimator.setDuration(TimerFragment.ANIMATION_TIME_MILLIS); alertAnimator.play(revealAnimator).before(fadeAnimator); alertAnimator.start(); } @Override public void onPause() { if (getActivity() instanceof DeskClock) { ((DeskClock) getActivity()).unregisterPageChangedListener(this); } super.onPause(); stopClockTicks(); if (mAdapter != null) { mAdapter.saveGlobalState(); } mPrefs.unregisterOnSharedPreferenceChangeListener(this); // This is called because the lock screen was activated, the window stay // active under it and when we unlock the screen, we see the old time for // a fraction of a second. View v = getView(); if (v != null) { v.setVisibility(View.INVISIBLE); } } @Override public void onPageChanged(int page) { if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) { mAdapter.sort(); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mAdapter != null) { mAdapter.onSaveInstanceState(outState); } if (mTimerSetup != null) { saveViewState(outState); } else if (mViewState != null) { outState.putAll(mViewState); } } private void saveViewState(Bundle outState) { mTimerSetup.saveEntryState(outState, KEY_ENTRY_STATE); } public void setPage() { boolean switchToSetupView; if (mViewState != null) { switchToSetupView = false; mTimerSetup.restoreEntryState(mViewState, KEY_ENTRY_STATE); mViewState = null; } else { switchToSetupView = mAdapter.getCount() == 0; } if (switchToSetupView) { gotoSetupView(); } else { gotoTimersView(); } } private void resetTimer(TimerObj t) { t.mState = TimerObj.STATE_RESTART; t.mTimeLeft = t.mOriginalLength = t.mSetupLength; // when multiple timers are firing, some timers will be off-screen and they will not // have Fragment instances unless user scrolls down further. t.mView is null in this case. if (t.mView != null) { t.mView.stop(); t.mView.setTime(t.mTimeLeft, false); t.mView.set(t.mOriginalLength, t.mTimeLeft, false); } updateTimersState(t, Timers.TIMER_RESET); } public void updateAllTimesUpTimers(boolean stop) { boolean notifyChange = false; // To avoid race conditions where a timer was dismissed and it is still in the timers list // and can be picked again, create a temporary list of timers to be removed first and // then removed them one by one LinkedList timesupTimers = new LinkedList(); for (int i = 0; i < mAdapter.getCount(); i++) { TimerObj timerObj = mAdapter.getItem(i); if (timerObj.mState == TimerObj.STATE_TIMESUP) { timesupTimers.addFirst(timerObj); notifyChange = true; } } while (timesupTimers.size() > 0) { final TimerObj t = timesupTimers.remove(); if (stop) { onStopButtonPressed(t); } else { resetTimer(t); } } if (notifyChange) { SharedPreferences.Editor editor = mPrefs.edit(); editor.putBoolean(Timers.FROM_ALERT, true); editor.apply(); } } private void gotoSetupView() { if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timer_setup) { mTimerSetup.setVisibility(View.VISIBLE); mTimerSetup.setScaleX(1f); mTimersListPage.setVisibility(View.GONE); } else { // Animate ObjectAnimator a = ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 1f, 0f); a.setInterpolator(new AccelerateInterpolator()); a.setDuration(125); a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mTimersListPage.setVisibility(View.GONE); mTimerSetup.setScaleX(0); mTimerSetup.setVisibility(View.VISIBLE); ObjectAnimator b = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 0f, 1f); b.setInterpolator(new DecelerateInterpolator()); b.setDuration(225); b.start(); } }); a.start(); } stopClockTicks(); mTimerSetup.updateDeleteButtonAndDivider(); mLastVisibleView = mTimerSetup; } private void gotoTimersView() { if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timers_list_page) { mTimerSetup.setVisibility(View.GONE); mTimersListPage.setVisibility(View.VISIBLE); mTimersListPage.setScaleX(1f); } else { // Animate ObjectAnimator a = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 1f, 0f); a.setInterpolator(new AccelerateInterpolator()); a.setDuration(125); a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mTimerSetup.setVisibility(View.GONE); mTimersListPage.setScaleX(0); mTimersListPage.setVisibility(View.VISIBLE); ObjectAnimator b = ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 0f, 1f); b.setInterpolator(new DecelerateInterpolator()); b.setDuration(225); b.start(); } }); a.start(); } startClockTicks(); mLastVisibleView = mTimersListPage; } @Override public void onClick(View v) { ClickAction tag = (ClickAction) v.getTag(); onClickHelper(tag); } private void onClickHelper(ClickAction clickAction) { switch (clickAction.mAction) { case ClickAction.ACTION_DELETE: final TimerObj t = clickAction.mTimer; if (t.mState == TimerObj.STATE_TIMESUP) { cancelTimerNotification(t.mTimerId); } // Tell receiver the timer was deleted. // It will stop all activity related to the // timer t.mState = TimerObj.STATE_DELETED; updateTimersState(t, Timers.DELETE_TIMER); break; case ClickAction.ACTION_PLUS_ONE: onPlusOneButtonPressed(clickAction.mTimer); break; case ClickAction.ACTION_STOP: onStopButtonPressed(clickAction.mTimer); break; default: break; } } private void onPlusOneButtonPressed(TimerObj t) { switch (t.mState) { case TimerObj.STATE_RUNNING: t.addTime(TimerObj.MINUTE_IN_MILLIS); long timeLeft = t.updateTimeLeft(false); ((TimerListItem) (t.mView)).setTime(timeLeft, false); ((TimerListItem) (t.mView)).setLength(timeLeft); mAdapter.notifyDataSetChanged(); updateTimersState(t, Timers.TIMER_UPDATE); break; case TimerObj.STATE_TIMESUP: // +1 min when the time is up will restart the timer with 1 minute left. t.mState = TimerObj.STATE_RUNNING; t.mStartTime = Utils.getTimeNow(); t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS; updateTimersState(t, Timers.TIMER_RESET); updateTimersState(t, Timers.START_TIMER); updateTimesUpMode(t); cancelTimerNotification(t.mTimerId); break; case TimerObj.STATE_STOPPED: case TimerObj.STATE_DONE: t.mState = TimerObj.STATE_RESTART; t.mTimeLeft = t.mOriginalLength = t.mSetupLength; ((TimerListItem) t.mView).stop(); ((TimerListItem) t.mView).setTime(t.mTimeLeft, false); ((TimerListItem) t.mView).set(t.mOriginalLength, t.mTimeLeft, false); updateTimersState(t, Timers.TIMER_RESET); break; default: break; } } private void onStopButtonPressed(TimerObj t) { switch (t.mState) { case TimerObj.STATE_RUNNING: // Stop timer and save the remaining time of the timer t.mState = TimerObj.STATE_STOPPED; ((TimerListItem) t.mView).pause(); t.updateTimeLeft(true); updateTimersState(t, Timers.TIMER_STOP); break; case TimerObj.STATE_STOPPED: // Reset the remaining time and continue timer t.mState = TimerObj.STATE_RUNNING; t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft); ((TimerListItem) t.mView).start(); updateTimersState(t, Timers.START_TIMER); break; case TimerObj.STATE_TIMESUP: if (t.mDeleteAfterUse) { cancelTimerNotification(t.mTimerId); // Tell receiver the timer was deleted. // It will stop all activity related to the // timer t.mState = TimerObj.STATE_DELETED; updateTimersState(t, Timers.DELETE_TIMER); } else { t.mState = TimerObj.STATE_DONE; // Used in a context where the timer could be off-screen and without a view if (t.mView != null) { ((TimerListItem) t.mView).done(); } updateTimersState(t, Timers.TIMER_DONE); cancelTimerNotification(t.mTimerId); updateTimesUpMode(t); } break; case TimerObj.STATE_DONE: break; case TimerObj.STATE_RESTART: t.mState = TimerObj.STATE_RUNNING; t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft); ((TimerListItem) t.mView).start(); updateTimersState(t, Timers.START_TIMER); break; default: break; } } private void onLabelPressed(TimerObj t) { final FragmentTransaction ft = getFragmentManager().beginTransaction(); final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog"); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); // Create and show the dialog. final LabelDialogFragment newFragment = LabelDialogFragment.newInstance(t, t.mLabel, getTag()); newFragment.show(ft, "label_dialog"); } // Starts the ticks that animate the timers. private void startClockTicks() { mTimersList.postDelayed(mClockTick, 20); mTicking = true; } // Stops the ticks that animate the timers. private void stopClockTicks() { if (mTicking) { mTimersList.removeCallbacks(mClockTick); mTicking = false; } } private void updateTimersState(TimerObj t, String action) { if (Timers.DELETE_TIMER.equals(action)) { LogUtils.e("~~ update timer state"); t.deleteFromSharedPref(mPrefs); } else { t.writeToSharedPref(mPrefs); } Intent i = new Intent(); i.setAction(action); i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId); // Make sure the receiver is getting the intent ASAP. i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); getActivity().sendBroadcast(i); } private void cancelTimerNotification(int timerId) { mNotificationManager.cancel(timerId); } private void updateTimesUpMode(TimerObj timerObj) { if (mOnEmptyListListener != null && timerObj.mState != TimerObj.STATE_TIMESUP) { mAdapter.removeTimer(timerObj); if (mAdapter.getCount() == 0) { mOnEmptyListListener.onEmptyList(); } else { mOnEmptyListListener.onListChanged(); } } } public void restartAdapter() { mAdapter = createAdapter(getActivity(), mPrefs); mAdapter.onRestoreInstanceState(null); } // Process extras that were sent to the app and were intended for the timer // fragment public void processIntent(Intent intent) { // switch to timer setup view if (intent.getBooleanExtra(GOTO_SETUP_VIEW, false)) { gotoSetupView(); } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (prefs.equals(mPrefs)) { if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false)) || (key.equals(Timers.FROM_NOTIFICATION) && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) { // The data-changed flag was set in the alert or notification so the adapter needs // to re-sync with the database SharedPreferences.Editor editor = mPrefs.edit(); editor.putBoolean(key, false); editor.apply(); mAdapter = createAdapter(getActivity(), mPrefs); mAdapter.onRestoreInstanceState(null); mTimersList.setAdapter(mAdapter); } } } @Override public void onFabClick(View view) { if (mLastVisibleView != mTimersListPage) { // New timer create if timer length is not zero // Create a new timer object to track the timer and // switch to the timers view. int timerLength = mTimerSetup.getTime(); if (timerLength == 0) { return; } TimerObj t = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS, getActivity()); t.mState = TimerObj.STATE_RUNNING; mAdapter.addTimer(t); updateTimersState(t, Timers.START_TIMER); gotoTimersView(); mTimerSetup.reset(); // Make sure the setup is cleared for next time mTimersList.setFirstPositionAndOffsets( mAdapter.findTimerPositionById(t.mTimerId), 0); } else { mTimerSetup.reset(); gotoSetupView(); } } }