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