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