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