1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock.timer; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 24 import android.app.Activity; 25 import android.app.Fragment; 26 import android.app.FragmentTransaction; 27 import android.app.NotificationManager; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.SharedPreferences; 31 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.preference.PreferenceManager; 35 import android.text.format.DateUtils; 36 import android.util.Log; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.View.OnClickListener; 40 import android.view.ViewAnimationUtils; 41 import android.view.ViewGroup; 42 import android.view.ViewGroup.LayoutParams; 43 import android.view.ViewGroupOverlay; 44 import android.view.animation.AccelerateInterpolator; 45 import android.view.animation.DecelerateInterpolator; 46 import android.view.animation.Interpolator; 47 import android.view.animation.PathInterpolator; 48 import android.widget.FrameLayout; 49 import android.widget.ImageButton; 50 import android.widget.TextView; 51 52 import com.android.deskclock.CircleButtonsLayout; 53 import com.android.deskclock.DeskClock; 54 import com.android.deskclock.DeskClock.OnTapListener; 55 import com.android.deskclock.DeskClockFragment; 56 import com.android.deskclock.LabelDialogFragment; 57 import com.android.deskclock.LogUtils; 58 import com.android.deskclock.R; 59 import com.android.deskclock.TimerSetupView; 60 import com.android.deskclock.Utils; 61 import com.android.deskclock.widget.sgv.GridAdapter; 62 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn; 63 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut; 64 import com.android.deskclock.widget.sgv.StaggeredGridView; 65 66 import java.util.ArrayList; 67 import java.util.Collections; 68 import java.util.Comparator; 69 import java.util.LinkedList; 70 71 // TODO: This class is renamed from TimerFragment to TimerFullScreenFragment with no change. It 72 // is responsible for the timer list in full screen timer alert and should be deprecated shortly. 73 public class TimerFullScreenFragment extends DeskClockFragment 74 implements OnClickListener, OnSharedPreferenceChangeListener { 75 76 private static final String TAG = "TimerFragment1"; 77 private static final String KEY_ENTRY_STATE = "entry_state"; 78 private static final Interpolator REVEAL_INTERPOLATOR = 79 new PathInterpolator(0.0f, 0.0f, 0.2f, 1.0f); 80 public static final String GOTO_SETUP_VIEW = "deskclock.timers.gotosetup"; 81 82 private Bundle mViewState; 83 private StaggeredGridView mTimersList; 84 private View mTimersListPage; 85 private int mColumnCount; 86 private ImageButton mFab; 87 private TimerSetupView mTimerSetup; 88 private TimersListAdapter mAdapter; 89 private boolean mTicking = false; 90 private SharedPreferences mPrefs; 91 private NotificationManager mNotificationManager; 92 private OnEmptyListListener mOnEmptyListListener; 93 private View mLastVisibleView = null; // used to decide if to set the view or animate to it. 94 95 class ClickAction { 96 public static final int ACTION_STOP = 1; 97 public static final int ACTION_PLUS_ONE = 2; 98 public static final int ACTION_DELETE = 3; 99 100 public int mAction; 101 public TimerObj mTimer; 102 ClickAction(int action, TimerObj t)103 public ClickAction(int action, TimerObj t) { 104 mAction = action; 105 mTimer = t; 106 } 107 } 108 109 // Container Activity that requests TIMESUP_MODE must implement this interface 110 public interface OnEmptyListListener { onEmptyList()111 public void onEmptyList(); 112 onListChanged()113 public void onListChanged(); 114 } 115 createAdapter(Context context, SharedPreferences prefs)116 TimersListAdapter createAdapter(Context context, SharedPreferences prefs) { 117 if (mOnEmptyListListener == null) { 118 return new TimersListAdapter(context, prefs); 119 } else { 120 return new TimesUpListAdapter(context, prefs); 121 } 122 } 123 124 private class TimersListAdapter extends GridAdapter { 125 126 ArrayList<TimerObj> mTimers = new ArrayList<TimerObj>(); 127 Context mContext; 128 SharedPreferences mmPrefs; 129 clear()130 private void clear() { 131 mTimers.clear(); 132 notifyDataSetChanged(); 133 } 134 TimersListAdapter(Context context, SharedPreferences prefs)135 public TimersListAdapter(Context context, SharedPreferences prefs) { 136 mContext = context; 137 mmPrefs = prefs; 138 } 139 140 @Override getCount()141 public int getCount() { 142 return mTimers.size(); 143 } 144 145 @Override hasStableIds()146 public boolean hasStableIds() { 147 return true; 148 } 149 150 @Override getItem(int p)151 public TimerObj getItem(int p) { 152 return mTimers.get(p); 153 } 154 155 @Override getItemId(int p)156 public long getItemId(int p) { 157 if (p >= 0 && p < mTimers.size()) { 158 return mTimers.get(p).mTimerId; 159 } 160 return 0; 161 } 162 deleteTimer(int id)163 public void deleteTimer(int id) { 164 for (int i = 0; i < mTimers.size(); i++) { 165 TimerObj t = mTimers.get(i); 166 167 if (t.mTimerId == id) { 168 if (t.mView != null) { 169 ((TimerListItem) t.mView).stop(); 170 } 171 t.deleteFromSharedPref(mmPrefs); 172 mTimers.remove(i); 173 if (mTimers.size() == 1 && mColumnCount > 1) { 174 // If we're going from two timers to one (in the same row), we don't want to 175 // animate the translation because we're changing the layout params span 176 // from 1 to 2, and the animation doesn't handle that very well. So instead, 177 // just fade out and in. 178 mTimersList.setAnimationMode(AnimationIn.FADE, AnimationOut.FADE); 179 } else { 180 mTimersList.setAnimationMode( 181 AnimationIn.FLY_IN_NEW_VIEWS, AnimationOut.FADE); 182 } 183 notifyDataSetChanged(); 184 return; 185 } 186 } 187 } 188 findTimerPositionById(int id)189 protected int findTimerPositionById(int id) { 190 for (int i = 0; i < mTimers.size(); i++) { 191 TimerObj t = mTimers.get(i); 192 if (t.mTimerId == id) { 193 return i; 194 } 195 } 196 return -1; 197 } 198 removeTimer(TimerObj timerObj)199 public void removeTimer(TimerObj timerObj) { 200 int position = findTimerPositionById(timerObj.mTimerId); 201 if (position >= 0) { 202 mTimers.remove(position); 203 notifyDataSetChanged(); 204 } 205 } 206 207 @Override getView(int position, View convertView, ViewGroup parent)208 public View getView(int position, View convertView, ViewGroup parent) { 209 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 210 Context.LAYOUT_INFLATER_SERVICE); 211 final TimerListItem v = (TimerListItem) inflater.inflate(R.layout.timer_list_item, 212 null); 213 final TimerObj o = (TimerObj) getItem(position); 214 o.mView = v; 215 long timeLeft = o.updateTimeLeft(false); 216 boolean drawRed = o.mState != TimerObj.STATE_RESTART; 217 v.set(o.mOriginalLength, timeLeft, drawRed); 218 v.setTime(timeLeft, true); 219 switch (o.mState) { 220 case TimerObj.STATE_RUNNING: 221 v.start(); 222 break; 223 case TimerObj.STATE_TIMESUP: 224 v.timesUp(); 225 break; 226 case TimerObj.STATE_DONE: 227 v.done(); 228 break; 229 default: 230 break; 231 } 232 233 // Timer text serves as a virtual start/stop button. 234 final CountingTimerView countingTimerView = (CountingTimerView) 235 v.findViewById(R.id.timer_time_text); 236 countingTimerView.registerVirtualButtonAction(new Runnable() { 237 @Override 238 public void run() { 239 TimerFullScreenFragment.this.onClickHelper( 240 new ClickAction(ClickAction.ACTION_STOP, o)); 241 } 242 }); 243 244 CircleButtonsLayout circleLayout = 245 (CircleButtonsLayout) v.findViewById(R.id.timer_circle); 246 circleLayout.setCircleTimerViewIds(R.id.timer_time, R.id.reset_add, R.id.timer_label, 247 R.id.timer_label_text); 248 249 ImageButton resetAddButton = (ImageButton) v.findViewById(R.id.reset_add); 250 resetAddButton.setTag(new ClickAction(ClickAction.ACTION_PLUS_ONE, o)); 251 v.setResetAddButton(true, TimerFullScreenFragment.this); 252 FrameLayout label = (FrameLayout) v.findViewById(R.id.timer_label); 253 TextView labelIcon = (TextView) v.findViewById(R.id.timer_label_placeholder); 254 TextView labelText = (TextView) v.findViewById(R.id.timer_label_text); 255 if (o.mLabel.equals("")) { 256 labelText.setVisibility(View.GONE); 257 labelIcon.setVisibility(View.VISIBLE); 258 } else { 259 labelText.setText(o.mLabel); 260 labelText.setVisibility(View.VISIBLE); 261 labelIcon.setVisibility(View.GONE); 262 } 263 if (getActivity() instanceof DeskClock) { 264 label.setOnTouchListener(new OnTapListener(getActivity(), labelText) { 265 @Override 266 protected void processClick(View v) { 267 onLabelPressed(o); 268 } 269 }); 270 } else { 271 labelIcon.setVisibility(View.INVISIBLE); 272 } 273 return v; 274 } 275 276 @Override getItemColumnSpan(Object item, int position)277 public int getItemColumnSpan(Object item, int position) { 278 // This returns the width for a specified position. If we only have one item, have it 279 // span all columns so that it's centered. Otherwise, all timers should just span one. 280 if (getCount() == 1) { 281 return mColumnCount; 282 } else { 283 return 1; 284 } 285 } 286 addTimer(TimerObj t)287 public void addTimer(TimerObj t) { 288 mTimers.add(0, t); 289 sort(); 290 } 291 onSaveInstanceState(Bundle outState)292 public void onSaveInstanceState(Bundle outState) { 293 TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers); 294 } 295 onRestoreInstanceState(Bundle outState)296 public void onRestoreInstanceState(Bundle outState) { 297 TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers); 298 sort(); 299 } 300 saveGlobalState()301 public void saveGlobalState() { 302 TimerObj.putTimersInSharedPrefs(mmPrefs, mTimers); 303 } 304 sort()305 public void sort() { 306 if (getCount() > 0) { 307 Collections.sort(mTimers, mTimersCompare); 308 notifyDataSetChanged(); 309 } 310 } 311 312 private final Comparator<TimerObj> mTimersCompare = new Comparator<TimerObj>() { 313 static final int BUZZING = 0; 314 static final int IN_USE = 1; 315 static final int NOT_USED = 2; 316 317 protected int getSection(TimerObj timerObj) { 318 switch (timerObj.mState) { 319 case TimerObj.STATE_TIMESUP: 320 return BUZZING; 321 case TimerObj.STATE_RUNNING: 322 case TimerObj.STATE_STOPPED: 323 return IN_USE; 324 default: 325 return NOT_USED; 326 } 327 } 328 329 @Override 330 public int compare(TimerObj o1, TimerObj o2) { 331 int section1 = getSection(o1); 332 int section2 = getSection(o2); 333 if (section1 != section2) { 334 return (section1 < section2) ? -1 : 1; 335 } else if (section1 == BUZZING || section1 == IN_USE) { 336 return (o1.mTimeLeft < o2.mTimeLeft) ? -1 : 1; 337 } else { 338 return (o1.mSetupLength < o2.mSetupLength) ? -1 : 1; 339 } 340 } 341 }; 342 } 343 344 private class TimesUpListAdapter extends TimersListAdapter { 345 TimesUpListAdapter(Context context, SharedPreferences prefs)346 public TimesUpListAdapter(Context context, SharedPreferences prefs) { 347 super(context, prefs); 348 } 349 350 @Override onSaveInstanceState(Bundle outState)351 public void onSaveInstanceState(Bundle outState) { 352 // This adapter has a data subset and never updates entire database 353 // Individual timers are updated in button handlers. 354 } 355 356 @Override saveGlobalState()357 public void saveGlobalState() { 358 // This adapter has a data subset and never updates entire database 359 // Individual timers are updated in button handlers. 360 } 361 362 @Override onRestoreInstanceState(Bundle outState)363 public void onRestoreInstanceState(Bundle outState) { 364 // This adapter loads a subset 365 TimerObj.getTimersFromSharedPrefs(mmPrefs, mTimers, TimerObj.STATE_TIMESUP); 366 367 if (getCount() == 0) { 368 mOnEmptyListListener.onEmptyList(); 369 } else { 370 Collections.sort(mTimers, new Comparator<TimerObj>() { 371 @Override 372 public int compare(TimerObj o1, TimerObj o2) { 373 return (o1.mTimeLeft < o2.mTimeLeft) ? -1 : 1; 374 } 375 }); 376 } 377 } 378 } 379 380 private final Runnable mClockTick = new Runnable() { 381 boolean mVisible = true; 382 final static int TIME_PERIOD_MS = 1000; 383 final static int SPLIT = TIME_PERIOD_MS / 2; 384 385 @Override 386 public void run() { 387 // Setup for blinking 388 boolean visible = Utils.getTimeNow() % TIME_PERIOD_MS < SPLIT; 389 boolean toggle = mVisible != visible; 390 mVisible = visible; 391 for (int i = 0; i < mAdapter.getCount(); i++) { 392 TimerObj t = mAdapter.getItem(i); 393 if (t.mState == TimerObj.STATE_RUNNING || t.mState == TimerObj.STATE_TIMESUP) { 394 long timeLeft = t.updateTimeLeft(false); 395 if (t.mView != null) { 396 ((TimerListItem) (t.mView)).setTime(timeLeft, false); 397 } 398 } 399 if (t.mTimeLeft <= 0 && t.mState != TimerObj.STATE_DONE 400 && t.mState != TimerObj.STATE_RESTART) { 401 t.mState = TimerObj.STATE_TIMESUP; 402 if (t.mView != null) { 403 ((TimerListItem) (t.mView)).timesUp(); 404 } 405 } 406 407 // The blinking 408 if (toggle && t.mView != null) { 409 if (t.mState == TimerObj.STATE_TIMESUP) { 410 ((TimerListItem) (t.mView)).setCircleBlink(mVisible); 411 } 412 if (t.mState == TimerObj.STATE_STOPPED) { 413 ((TimerListItem) (t.mView)).setTextBlink(mVisible); 414 } 415 } 416 } 417 mTimersList.postDelayed(mClockTick, 20); 418 } 419 }; 420 421 @Override onCreate(Bundle savedInstanceState)422 public void onCreate(Bundle savedInstanceState) { 423 // Cache instance data and consume in first call to setupPage() 424 if (savedInstanceState != null) { 425 mViewState = savedInstanceState; 426 } 427 428 super.onCreate(savedInstanceState); 429 } 430 431 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)432 public View onCreateView(LayoutInflater inflater, ViewGroup container, 433 Bundle savedInstanceState) { 434 // Inflate the layout for this fragment 435 View v = inflater.inflate(R.layout.timer_full_screen_fragment, container, false); 436 437 // Handle arguments from parent 438 Bundle bundle = getArguments(); 439 if (bundle != null && bundle.containsKey(Timers.TIMESUP_MODE)) { 440 if (bundle.getBoolean(Timers.TIMESUP_MODE, false)) { 441 try { 442 mOnEmptyListListener = (OnEmptyListListener) getActivity(); 443 } catch (ClassCastException e) { 444 Log.wtf(TAG, getActivity().toString() + " must implement OnEmptyListListener"); 445 } 446 } 447 } 448 449 mFab = (ImageButton) v.findViewById(R.id.fab); 450 mTimersList = (StaggeredGridView) v.findViewById(R.id.timers_list); 451 // For tablets in landscape, the count will be 2. All else will be 1. 452 mColumnCount = getResources().getInteger(R.integer.timer_column_count); 453 mTimersList.setColumnCount(mColumnCount); 454 // Set this to true; otherwise adding new views to the end of the list won't cause 455 // everything above it to be filled in correctly. 456 mTimersList.setGuardAgainstJaggedEdges(true); 457 458 mTimersListPage = v.findViewById(R.id.timers_list_page); 459 mTimerSetup = (TimerSetupView) v.findViewById(R.id.timer_setup); 460 461 mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); 462 mNotificationManager = (NotificationManager) 463 getActivity().getSystemService(Context.NOTIFICATION_SERVICE); 464 465 return v; 466 } 467 468 @Override onDestroyView()469 public void onDestroyView() { 470 mViewState = new Bundle(); 471 saveViewState(mViewState); 472 super.onDestroyView(); 473 } 474 475 @Override onResume()476 public void onResume() { 477 Intent newIntent = null; 478 479 if (getActivity() instanceof DeskClock) { 480 DeskClock activity = (DeskClock) getActivity(); 481 activity.registerPageChangedListener(this); 482 newIntent = activity.getIntent(); 483 } 484 super.onResume(); 485 mPrefs.registerOnSharedPreferenceChangeListener(this); 486 487 mAdapter = createAdapter(getActivity(), mPrefs); 488 mAdapter.onRestoreInstanceState(null); 489 490 LayoutParams params; 491 float dividerHeight = getResources().getDimension(R.dimen.timer_divider_height); 492 if (getActivity() instanceof DeskClock) { 493 // If this is a DeskClock fragment (i.e. not a FullScreenTimerAlert), add a footer to 494 // the bottom of the list so that it can scroll underneath the bottom button bar. 495 // StaggeredGridView doesn't support a footer view, but GridAdapter does, so this 496 // can't happen until the Adapter itself is instantiated. 497 View footerView = getActivity().getLayoutInflater().inflate( 498 R.layout.blank_footer_view, mTimersList, false); 499 params = footerView.getLayoutParams(); 500 params.height -= dividerHeight; 501 footerView.setLayoutParams(params); 502 mAdapter.setFooterView(footerView); 503 } 504 505 if (mPrefs.getBoolean(Timers.FROM_NOTIFICATION, false)) { 506 // Clear the flag set in the notification because the adapter was just 507 // created and is thus in sync with the database 508 SharedPreferences.Editor editor = mPrefs.edit(); 509 editor.putBoolean(Timers.FROM_NOTIFICATION, false); 510 editor.apply(); 511 } 512 if (mPrefs.getBoolean(Timers.FROM_ALERT, false)) { 513 // Clear the flag set in the alert because the adapter was just 514 // created and is thus in sync with the database 515 SharedPreferences.Editor editor = mPrefs.edit(); 516 editor.putBoolean(Timers.FROM_ALERT, false); 517 editor.apply(); 518 } 519 520 mTimersList.setAdapter(mAdapter); 521 mLastVisibleView = null; // Force a non animation setting of the view 522 setPage(); 523 // View was hidden in onPause, make sure it is visible now. 524 View v = getView(); 525 if (v != null) { 526 getView().setVisibility(View.VISIBLE); 527 } 528 529 if (newIntent != null) { 530 processIntent(newIntent); 531 } 532 533 mFab.setOnClickListener(new OnClickListener() { 534 @Override 535 public void onClick(View view) { 536 revealAnimation(mFab, getActivity().getResources().getColor(R.color.clock_white)); 537 new Handler().postDelayed(new Runnable() { 538 @Override 539 public void run() { 540 updateAllTimesUpTimers(false /* stop */); 541 } 542 }, TimerFragment.ANIMATION_TIME_MILLIS); 543 } 544 }); 545 } 546 revealAnimation(final View centerView, int color)547 private void revealAnimation(final View centerView, int color) { 548 final Activity activity = getActivity(); 549 final View decorView = activity.getWindow().getDecorView(); 550 final ViewGroupOverlay overlay = (ViewGroupOverlay) decorView.getOverlay(); 551 552 // Create a transient view for performing the reveal animation. 553 final View revealView = new View(activity); 554 revealView.setRight(decorView.getWidth()); 555 revealView.setBottom(decorView.getHeight()); 556 revealView.setBackgroundColor(color); 557 overlay.add(revealView); 558 559 final int[] clearLocation = new int[2]; 560 centerView.getLocationInWindow(clearLocation); 561 clearLocation[0] += centerView.getWidth() / 2; 562 clearLocation[1] += centerView.getHeight() / 2; 563 final int revealCenterX = clearLocation[0] - revealView.getLeft(); 564 final int revealCenterY = clearLocation[1] - revealView.getTop(); 565 566 final int xMax = Math.max(revealCenterX, decorView.getWidth() - revealCenterX); 567 final int yMax = Math.max(revealCenterY, decorView.getHeight() - revealCenterY); 568 final float revealRadius = (float) Math.sqrt(Math.pow(xMax, 2.0) + Math.pow(yMax, 2.0)); 569 570 final Animator revealAnimator = ViewAnimationUtils.createCircularReveal( 571 revealView, revealCenterX, revealCenterY, 0.0f, revealRadius); 572 revealAnimator.setInterpolator(REVEAL_INTERPOLATOR); 573 574 final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 1.0f); 575 fadeAnimator.addListener(new AnimatorListenerAdapter() { 576 @Override 577 public void onAnimationEnd(Animator animation) { 578 overlay.remove(revealView); 579 } 580 }); 581 582 final AnimatorSet alertAnimator = new AnimatorSet(); 583 alertAnimator.setDuration(TimerFragment.ANIMATION_TIME_MILLIS); 584 alertAnimator.play(revealAnimator).before(fadeAnimator); 585 alertAnimator.start(); 586 } 587 588 @Override onPause()589 public void onPause() { 590 if (getActivity() instanceof DeskClock) { 591 ((DeskClock) getActivity()).unregisterPageChangedListener(this); 592 } 593 super.onPause(); 594 stopClockTicks(); 595 if (mAdapter != null) { 596 mAdapter.saveGlobalState(); 597 } 598 mPrefs.unregisterOnSharedPreferenceChangeListener(this); 599 // This is called because the lock screen was activated, the window stay 600 // active under it and when we unlock the screen, we see the old time for 601 // a fraction of a second. 602 View v = getView(); 603 if (v != null) { 604 v.setVisibility(View.INVISIBLE); 605 } 606 } 607 608 @Override onPageChanged(int page)609 public void onPageChanged(int page) { 610 if (page == DeskClock.TIMER_TAB_INDEX && mAdapter != null) { 611 mAdapter.sort(); 612 } 613 } 614 615 @Override onSaveInstanceState(Bundle outState)616 public void onSaveInstanceState(Bundle outState) { 617 super.onSaveInstanceState(outState); 618 if (mAdapter != null) { 619 mAdapter.onSaveInstanceState(outState); 620 } 621 if (mTimerSetup != null) { 622 saveViewState(outState); 623 } else if (mViewState != null) { 624 outState.putAll(mViewState); 625 } 626 } 627 saveViewState(Bundle outState)628 private void saveViewState(Bundle outState) { 629 mTimerSetup.saveEntryState(outState, KEY_ENTRY_STATE); 630 } 631 setPage()632 public void setPage() { 633 boolean switchToSetupView; 634 if (mViewState != null) { 635 switchToSetupView = false; 636 mTimerSetup.restoreEntryState(mViewState, KEY_ENTRY_STATE); 637 mViewState = null; 638 } else { 639 switchToSetupView = mAdapter.getCount() == 0; 640 } 641 if (switchToSetupView) { 642 gotoSetupView(); 643 } else { 644 gotoTimersView(); 645 } 646 } 647 resetTimer(TimerObj t)648 private void resetTimer(TimerObj t) { 649 t.mState = TimerObj.STATE_RESTART; 650 t.mTimeLeft = t.mOriginalLength = t.mSetupLength; 651 652 // when multiple timers are firing, some timers will be off-screen and they will not 653 // have Fragment instances unless user scrolls down further. t.mView is null in this case. 654 if (t.mView != null) { 655 t.mView.stop(); 656 t.mView.setTime(t.mTimeLeft, false); 657 t.mView.set(t.mOriginalLength, t.mTimeLeft, false); 658 } 659 updateTimersState(t, Timers.TIMER_RESET); 660 } 661 updateAllTimesUpTimers(boolean stop)662 public void updateAllTimesUpTimers(boolean stop) { 663 boolean notifyChange = false; 664 // To avoid race conditions where a timer was dismissed and it is still in the timers list 665 // and can be picked again, create a temporary list of timers to be removed first and 666 // then removed them one by one 667 LinkedList<TimerObj> timesupTimers = new LinkedList<TimerObj>(); 668 for (int i = 0; i < mAdapter.getCount(); i++) { 669 TimerObj timerObj = mAdapter.getItem(i); 670 if (timerObj.mState == TimerObj.STATE_TIMESUP) { 671 timesupTimers.addFirst(timerObj); 672 notifyChange = true; 673 } 674 } 675 676 while (timesupTimers.size() > 0) { 677 final TimerObj t = timesupTimers.remove(); 678 if (stop) { 679 onStopButtonPressed(t); 680 } else { 681 resetTimer(t); 682 } 683 } 684 685 if (notifyChange) { 686 SharedPreferences.Editor editor = mPrefs.edit(); 687 editor.putBoolean(Timers.FROM_ALERT, true); 688 editor.apply(); 689 } 690 } 691 gotoSetupView()692 private void gotoSetupView() { 693 if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timer_setup) { 694 mTimerSetup.setVisibility(View.VISIBLE); 695 mTimerSetup.setScaleX(1f); 696 mTimersListPage.setVisibility(View.GONE); 697 } else { 698 // Animate 699 ObjectAnimator a = ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 1f, 0f); 700 a.setInterpolator(new AccelerateInterpolator()); 701 a.setDuration(125); 702 a.addListener(new AnimatorListenerAdapter() { 703 @Override 704 public void onAnimationEnd(Animator animation) { 705 mTimersListPage.setVisibility(View.GONE); 706 mTimerSetup.setScaleX(0); 707 mTimerSetup.setVisibility(View.VISIBLE); 708 ObjectAnimator b = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 0f, 1f); 709 b.setInterpolator(new DecelerateInterpolator()); 710 b.setDuration(225); 711 b.start(); 712 } 713 }); 714 a.start(); 715 716 } 717 stopClockTicks(); 718 mTimerSetup.updateDeleteButtonAndDivider(); 719 mLastVisibleView = mTimerSetup; 720 } 721 gotoTimersView()722 private void gotoTimersView() { 723 if (mLastVisibleView == null || mLastVisibleView.getId() == R.id.timers_list_page) { 724 mTimerSetup.setVisibility(View.GONE); 725 mTimersListPage.setVisibility(View.VISIBLE); 726 mTimersListPage.setScaleX(1f); 727 } else { 728 // Animate 729 ObjectAnimator a = ObjectAnimator.ofFloat(mTimerSetup, View.SCALE_X, 1f, 0f); 730 a.setInterpolator(new AccelerateInterpolator()); 731 a.setDuration(125); 732 a.addListener(new AnimatorListenerAdapter() { 733 @Override 734 public void onAnimationEnd(Animator animation) { 735 mTimerSetup.setVisibility(View.GONE); 736 mTimersListPage.setScaleX(0); 737 mTimersListPage.setVisibility(View.VISIBLE); 738 ObjectAnimator b = 739 ObjectAnimator.ofFloat(mTimersListPage, View.SCALE_X, 0f, 1f); 740 b.setInterpolator(new DecelerateInterpolator()); 741 b.setDuration(225); 742 b.start(); 743 } 744 }); 745 a.start(); 746 } 747 startClockTicks(); 748 mLastVisibleView = mTimersListPage; 749 } 750 751 @Override onClick(View v)752 public void onClick(View v) { 753 ClickAction tag = (ClickAction) v.getTag(); 754 onClickHelper(tag); 755 } 756 onClickHelper(ClickAction clickAction)757 private void onClickHelper(ClickAction clickAction) { 758 switch (clickAction.mAction) { 759 case ClickAction.ACTION_DELETE: 760 final TimerObj t = clickAction.mTimer; 761 if (t.mState == TimerObj.STATE_TIMESUP) { 762 cancelTimerNotification(t.mTimerId); 763 } 764 // Tell receiver the timer was deleted. 765 // It will stop all activity related to the 766 // timer 767 t.mState = TimerObj.STATE_DELETED; 768 updateTimersState(t, Timers.DELETE_TIMER); 769 break; 770 case ClickAction.ACTION_PLUS_ONE: 771 onPlusOneButtonPressed(clickAction.mTimer); 772 break; 773 case ClickAction.ACTION_STOP: 774 onStopButtonPressed(clickAction.mTimer); 775 break; 776 default: 777 break; 778 } 779 } 780 onPlusOneButtonPressed(TimerObj t)781 private void onPlusOneButtonPressed(TimerObj t) { 782 switch (t.mState) { 783 case TimerObj.STATE_RUNNING: 784 t.addTime(TimerObj.MINUTE_IN_MILLIS); 785 long timeLeft = t.updateTimeLeft(false); 786 ((TimerListItem) (t.mView)).setTime(timeLeft, false); 787 ((TimerListItem) (t.mView)).setLength(timeLeft); 788 mAdapter.notifyDataSetChanged(); 789 updateTimersState(t, Timers.TIMER_UPDATE); 790 break; 791 case TimerObj.STATE_TIMESUP: 792 // +1 min when the time is up will restart the timer with 1 minute left. 793 t.mState = TimerObj.STATE_RUNNING; 794 t.mStartTime = Utils.getTimeNow(); 795 t.mTimeLeft = t.mOriginalLength = TimerObj.MINUTE_IN_MILLIS; 796 updateTimersState(t, Timers.TIMER_RESET); 797 updateTimersState(t, Timers.START_TIMER); 798 updateTimesUpMode(t); 799 cancelTimerNotification(t.mTimerId); 800 break; 801 case TimerObj.STATE_STOPPED: 802 case TimerObj.STATE_DONE: 803 t.mState = TimerObj.STATE_RESTART; 804 t.mTimeLeft = t.mOriginalLength = t.mSetupLength; 805 ((TimerListItem) t.mView).stop(); 806 ((TimerListItem) t.mView).setTime(t.mTimeLeft, false); 807 ((TimerListItem) t.mView).set(t.mOriginalLength, t.mTimeLeft, false); 808 updateTimersState(t, Timers.TIMER_RESET); 809 break; 810 default: 811 break; 812 } 813 } 814 onStopButtonPressed(TimerObj t)815 private void onStopButtonPressed(TimerObj t) { 816 switch (t.mState) { 817 case TimerObj.STATE_RUNNING: 818 // Stop timer and save the remaining time of the timer 819 t.mState = TimerObj.STATE_STOPPED; 820 ((TimerListItem) t.mView).pause(); 821 t.updateTimeLeft(true); 822 updateTimersState(t, Timers.TIMER_STOP); 823 break; 824 case TimerObj.STATE_STOPPED: 825 // Reset the remaining time and continue timer 826 t.mState = TimerObj.STATE_RUNNING; 827 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft); 828 ((TimerListItem) t.mView).start(); 829 updateTimersState(t, Timers.START_TIMER); 830 break; 831 case TimerObj.STATE_TIMESUP: 832 if (t.mDeleteAfterUse) { 833 cancelTimerNotification(t.mTimerId); 834 // Tell receiver the timer was deleted. 835 // It will stop all activity related to the 836 // timer 837 t.mState = TimerObj.STATE_DELETED; 838 updateTimersState(t, Timers.DELETE_TIMER); 839 } else { 840 t.mState = TimerObj.STATE_DONE; 841 // Used in a context where the timer could be off-screen and without a view 842 if (t.mView != null) { 843 ((TimerListItem) t.mView).done(); 844 } 845 updateTimersState(t, Timers.TIMER_DONE); 846 cancelTimerNotification(t.mTimerId); 847 updateTimesUpMode(t); 848 } 849 break; 850 case TimerObj.STATE_DONE: 851 break; 852 case TimerObj.STATE_RESTART: 853 t.mState = TimerObj.STATE_RUNNING; 854 t.mStartTime = Utils.getTimeNow() - (t.mOriginalLength - t.mTimeLeft); 855 ((TimerListItem) t.mView).start(); 856 updateTimersState(t, Timers.START_TIMER); 857 break; 858 default: 859 break; 860 } 861 } 862 onLabelPressed(TimerObj t)863 private void onLabelPressed(TimerObj t) { 864 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 865 final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog"); 866 if (prev != null) { 867 ft.remove(prev); 868 } 869 ft.addToBackStack(null); 870 871 // Create and show the dialog. 872 final LabelDialogFragment newFragment = 873 LabelDialogFragment.newInstance(t, t.mLabel, getTag()); 874 newFragment.show(ft, "label_dialog"); 875 } 876 877 // Starts the ticks that animate the timers. startClockTicks()878 private void startClockTicks() { 879 mTimersList.postDelayed(mClockTick, 20); 880 mTicking = true; 881 } 882 883 // Stops the ticks that animate the timers. stopClockTicks()884 private void stopClockTicks() { 885 if (mTicking) { 886 mTimersList.removeCallbacks(mClockTick); 887 mTicking = false; 888 } 889 } 890 updateTimersState(TimerObj t, String action)891 private void updateTimersState(TimerObj t, String action) { 892 if (Timers.DELETE_TIMER.equals(action)) { 893 LogUtils.e("~~ update timer state"); 894 t.deleteFromSharedPref(mPrefs); 895 } else { 896 t.writeToSharedPref(mPrefs); 897 } 898 Intent i = new Intent(); 899 i.setAction(action); 900 i.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId); 901 // Make sure the receiver is getting the intent ASAP. 902 i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 903 getActivity().sendBroadcast(i); 904 } 905 cancelTimerNotification(int timerId)906 private void cancelTimerNotification(int timerId) { 907 mNotificationManager.cancel(timerId); 908 } 909 updateTimesUpMode(TimerObj timerObj)910 private void updateTimesUpMode(TimerObj timerObj) { 911 if (mOnEmptyListListener != null && timerObj.mState != TimerObj.STATE_TIMESUP) { 912 mAdapter.removeTimer(timerObj); 913 if (mAdapter.getCount() == 0) { 914 mOnEmptyListListener.onEmptyList(); 915 } else { 916 mOnEmptyListListener.onListChanged(); 917 } 918 } 919 } 920 restartAdapter()921 public void restartAdapter() { 922 mAdapter = createAdapter(getActivity(), mPrefs); 923 mAdapter.onRestoreInstanceState(null); 924 } 925 926 // Process extras that were sent to the app and were intended for the timer 927 // fragment processIntent(Intent intent)928 public void processIntent(Intent intent) { 929 // switch to timer setup view 930 if (intent.getBooleanExtra(GOTO_SETUP_VIEW, false)) { 931 gotoSetupView(); 932 } 933 } 934 935 @Override onSharedPreferenceChanged(SharedPreferences prefs, String key)936 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 937 if (prefs.equals(mPrefs)) { 938 if ((key.equals(Timers.FROM_ALERT) && prefs.getBoolean(Timers.FROM_ALERT, false)) 939 || (key.equals(Timers.FROM_NOTIFICATION) 940 && prefs.getBoolean(Timers.FROM_NOTIFICATION, false))) { 941 // The data-changed flag was set in the alert or notification so the adapter needs 942 // to re-sync with the database 943 SharedPreferences.Editor editor = mPrefs.edit(); 944 editor.putBoolean(key, false); 945 editor.apply(); 946 mAdapter = createAdapter(getActivity(), mPrefs); 947 mAdapter.onRestoreInstanceState(null); 948 mTimersList.setAdapter(mAdapter); 949 } 950 } 951 } 952 953 @Override onFabClick(View view)954 public void onFabClick(View view) { 955 if (mLastVisibleView != mTimersListPage) { 956 // New timer create if timer length is not zero 957 // Create a new timer object to track the timer and 958 // switch to the timers view. 959 int timerLength = mTimerSetup.getTime(); 960 if (timerLength == 0) { 961 return; 962 } 963 TimerObj t = new TimerObj(timerLength * DateUtils.SECOND_IN_MILLIS, getActivity()); 964 t.mState = TimerObj.STATE_RUNNING; 965 mAdapter.addTimer(t); 966 updateTimersState(t, Timers.START_TIMER); 967 gotoTimersView(); 968 mTimerSetup.reset(); // Make sure the setup is cleared for next time 969 970 mTimersList.setFirstPositionAndOffsets( 971 mAdapter.findTimerPositionById(t.mTimerId), 0); 972 } else { 973 mTimerSetup.reset(); 974 gotoSetupView(); 975 } 976 } 977 } 978