1 /* 2 * Copyright (C) 2007 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; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorInflater; 22 import android.animation.ValueAnimator; 23 import android.app.Activity; 24 import android.app.Fragment; 25 import android.app.FragmentTransaction; 26 import android.app.LoaderManager; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.Loader; 31 import android.content.res.Configuration; 32 import android.content.res.Resources; 33 import android.database.Cursor; 34 import android.database.DataSetObserver; 35 import android.graphics.Rect; 36 import android.graphics.Typeface; 37 import android.media.Ringtone; 38 import android.media.RingtoneManager; 39 import android.net.Uri; 40 import android.os.AsyncTask; 41 import android.os.Bundle; 42 import android.os.Vibrator; 43 import android.text.format.DateFormat; 44 import android.view.Gravity; 45 import android.view.LayoutInflater; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.View.OnClickListener; 49 import android.view.ViewGroup; 50 import android.view.ViewGroup.LayoutParams; 51 import android.view.ViewTreeObserver; 52 import android.view.animation.DecelerateInterpolator; 53 import android.view.animation.Interpolator; 54 import android.widget.CheckBox; 55 import android.widget.CompoundButton; 56 import android.widget.CursorAdapter; 57 import android.widget.FrameLayout; 58 import android.widget.ImageButton; 59 import android.widget.ImageView; 60 import android.widget.LinearLayout; 61 import android.widget.ListView; 62 import android.widget.Switch; 63 import android.widget.TextView; 64 import android.widget.Toast; 65 import android.widget.ToggleButton; 66 67 import com.android.datetimepicker.time.RadialPickerLayout; 68 import com.android.datetimepicker.time.TimePickerDialog; 69 import com.android.deskclock.alarms.AlarmStateManager; 70 import com.android.deskclock.provider.Alarm; 71 import com.android.deskclock.provider.AlarmInstance; 72 import com.android.deskclock.provider.DaysOfWeek; 73 import com.android.deskclock.widget.ActionableToastBar; 74 import com.android.deskclock.widget.TextTime; 75 76 import java.text.DateFormatSymbols; 77 import java.util.Calendar; 78 import java.util.HashSet; 79 import java.util.concurrent.ConcurrentHashMap; 80 81 /** 82 * AlarmClock application. 83 */ 84 public class AlarmClockFragment extends DeskClockFragment implements 85 LoaderManager.LoaderCallbacks<Cursor>, 86 TimePickerDialog.OnTimeSetListener, 87 View.OnTouchListener 88 { 89 private static final float EXPAND_DECELERATION = 1f; 90 private static final float COLLAPSE_DECELERATION = 0.7f; 91 private static final int ANIMATION_DURATION = 300; 92 private static final String KEY_EXPANDED_IDS = "expandedIds"; 93 private static final String KEY_REPEAT_CHECKED_IDS = "repeatCheckedIds"; 94 private static final String KEY_RINGTONE_TITLE_CACHE = "ringtoneTitleCache"; 95 private static final String KEY_SELECTED_ALARMS = "selectedAlarms"; 96 private static final String KEY_DELETED_ALARM = "deletedAlarm"; 97 private static final String KEY_UNDO_SHOWING = "undoShowing"; 98 private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap"; 99 private static final String KEY_SELECTED_ALARM = "selectedAlarm"; 100 private static final String KEY_DELETE_CONFIRMATION = "deleteConfirmation"; 101 102 private static final int REQUEST_CODE_RINGTONE = 1; 103 104 // This extra is used when receiving an intent to create an alarm, but no alarm details 105 // have been passed in, so the alarm page should start the process of creating a new alarm. 106 public static final String ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new"; 107 108 // This extra is used when receiving an intent to scroll to specific alarm. If alarm 109 // can not be found, and toast message will pop up that the alarm has be deleted. 110 public static final String SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm"; 111 112 private ListView mAlarmsList; 113 private AlarmItemAdapter mAdapter; 114 private View mEmptyView; 115 private ImageView mAddAlarmButton; 116 private View mAlarmsView; 117 private View mTimelineLayout; 118 private AlarmTimelineView mTimelineView; 119 private View mFooterView; 120 121 private Bundle mRingtoneTitleCache; // Key: ringtone uri, value: ringtone title 122 private ActionableToastBar mUndoBar; 123 private View mUndoFrame; 124 125 private Alarm mSelectedAlarm; 126 private long mScrollToAlarmId = -1; 127 128 private Loader mCursorLoader = null; 129 130 // Saved states for undo 131 private Alarm mDeletedAlarm; 132 private Alarm mAddedAlarm; 133 private boolean mUndoShowing = false; 134 135 private Animator mFadeIn; 136 private Animator mFadeOut; 137 138 private Interpolator mExpandInterpolator; 139 private Interpolator mCollapseInterpolator; 140 141 private int mTimelineViewWidth; 142 private int mUndoBarInitialMargin; 143 144 // Cached layout positions of items in listview prior to add/removal of alarm item 145 private ConcurrentHashMap<Long, Integer> mItemIdTopMap = new ConcurrentHashMap<Long, Integer>(); 146 147 public AlarmClockFragment() { 148 // Basic provider required by Fragment.java 149 } 150 151 @Override 152 public void onCreate(Bundle savedState) { 153 super.onCreate(savedState); 154 mCursorLoader = getLoaderManager().initLoader(0, null, this); 155 } 156 157 @Override 158 public View onCreateView(LayoutInflater inflater, ViewGroup container, 159 Bundle savedState) { 160 // Inflate the layout for this fragment 161 final View v = inflater.inflate(R.layout.alarm_clock, container, false); 162 163 long[] expandedIds = null; 164 long[] repeatCheckedIds = null; 165 long[] selectedAlarms = null; 166 Bundle previousDayMap = null; 167 if (savedState != null) { 168 expandedIds = savedState.getLongArray(KEY_EXPANDED_IDS); 169 repeatCheckedIds = savedState.getLongArray(KEY_REPEAT_CHECKED_IDS); 170 mRingtoneTitleCache = savedState.getBundle(KEY_RINGTONE_TITLE_CACHE); 171 mDeletedAlarm = savedState.getParcelable(KEY_DELETED_ALARM); 172 mUndoShowing = savedState.getBoolean(KEY_UNDO_SHOWING); 173 selectedAlarms = savedState.getLongArray(KEY_SELECTED_ALARMS); 174 previousDayMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP); 175 mSelectedAlarm = savedState.getParcelable(KEY_SELECTED_ALARM); 176 } 177 178 mExpandInterpolator = new DecelerateInterpolator(EXPAND_DECELERATION); 179 mCollapseInterpolator = new DecelerateInterpolator(COLLAPSE_DECELERATION); 180 181 mAddAlarmButton = (ImageButton) v.findViewById(R.id.alarm_add_alarm); 182 mAddAlarmButton.setOnClickListener(new OnClickListener() { 183 @Override 184 public void onClick(View v) { 185 hideUndoBar(true, null); 186 startCreatingAlarm(); 187 } 188 }); 189 // For landscape, put the add button on the right and the menu in the actionbar. 190 FrameLayout.LayoutParams layoutParams = 191 (FrameLayout.LayoutParams) mAddAlarmButton.getLayoutParams(); 192 boolean isLandscape = getResources().getConfiguration().orientation 193 == Configuration.ORIENTATION_LANDSCAPE; 194 if (isLandscape) { 195 layoutParams.gravity = Gravity.END; 196 } else { 197 layoutParams.gravity = Gravity.CENTER; 198 } 199 mAddAlarmButton.setLayoutParams(layoutParams); 200 201 View menuButton = v.findViewById(R.id.menu_button); 202 if (menuButton != null) { 203 if (isLandscape) { 204 menuButton.setVisibility(View.GONE); 205 } else { 206 menuButton.setVisibility(View.VISIBLE); 207 setupFakeOverflowMenuButton(menuButton); 208 } 209 } 210 211 mEmptyView = v.findViewById(R.id.alarms_empty_view); 212 mEmptyView.setOnClickListener(new OnClickListener() { 213 @Override 214 public void onClick(View v) { 215 startCreatingAlarm(); 216 } 217 }); 218 mAlarmsList = (ListView) v.findViewById(R.id.alarms_list); 219 220 mFadeIn = AnimatorInflater.loadAnimator(getActivity(), R.anim.fade_in); 221 mFadeIn.setDuration(ANIMATION_DURATION); 222 mFadeIn.addListener(new AnimatorListener() { 223 224 @Override 225 public void onAnimationStart(Animator animation) { 226 mEmptyView.setVisibility(View.VISIBLE); 227 } 228 229 @Override 230 public void onAnimationCancel(Animator animation) { 231 // Do nothing. 232 } 233 234 @Override 235 public void onAnimationEnd(Animator animation) { 236 // Do nothing. 237 } 238 239 @Override 240 public void onAnimationRepeat(Animator animation) { 241 // Do nothing. 242 } 243 }); 244 mFadeIn.setTarget(mEmptyView); 245 mFadeOut = AnimatorInflater.loadAnimator(getActivity(), R.anim.fade_out); 246 mFadeOut.setDuration(ANIMATION_DURATION); 247 mFadeOut.addListener(new AnimatorListener() { 248 249 @Override 250 public void onAnimationStart(Animator arg0) { 251 mEmptyView.setVisibility(View.VISIBLE); 252 } 253 254 @Override 255 public void onAnimationCancel(Animator arg0) { 256 // Do nothing. 257 } 258 259 @Override 260 public void onAnimationEnd(Animator arg0) { 261 mEmptyView.setVisibility(View.GONE); 262 } 263 264 @Override 265 public void onAnimationRepeat(Animator arg0) { 266 // Do nothing. 267 } 268 }); 269 mFadeOut.setTarget(mEmptyView); 270 mAlarmsView = v.findViewById(R.id.alarm_layout); 271 mTimelineLayout = v.findViewById(R.id.timeline_layout); 272 273 mUndoBar = (ActionableToastBar) v.findViewById(R.id.undo_bar); 274 mUndoBarInitialMargin = getActivity().getResources() 275 .getDimensionPixelOffset(R.dimen.alarm_undo_bar_horizontal_margin); 276 mUndoFrame = v.findViewById(R.id.undo_frame); 277 mUndoFrame.setOnTouchListener(this); 278 279 mFooterView = v.findViewById(R.id.alarms_footer_view); 280 mFooterView.setOnTouchListener(this); 281 282 // Timeline layout only exists in tablet landscape mode for now. 283 if (mTimelineLayout != null) { 284 mTimelineView = (AlarmTimelineView) v.findViewById(R.id.alarm_timeline_view); 285 mTimelineViewWidth = getActivity().getResources() 286 .getDimensionPixelOffset(R.dimen.alarm_timeline_layout_width); 287 } 288 289 mAdapter = new AlarmItemAdapter(getActivity(), 290 expandedIds, repeatCheckedIds, selectedAlarms, previousDayMap, mAlarmsList); 291 mAdapter.registerDataSetObserver(new DataSetObserver() { 292 293 private int prevAdapterCount = -1; 294 295 @Override 296 public void onChanged() { 297 298 final int count = mAdapter.getCount(); 299 if (mDeletedAlarm != null && prevAdapterCount > count) { 300 showUndoBar(); 301 } 302 303 // If there are no alarms in the adapter... 304 if (count == 0) { 305 mAddAlarmButton.setBackgroundResource(R.drawable.main_button_red); 306 307 // ...and if there exists a timeline view (currently only in tablet landscape) 308 if (mTimelineLayout != null && mAlarmsView != null) { 309 310 // ...and if the previous adapter had alarms (indicating a removal)... 311 if (prevAdapterCount > 0) { 312 313 // Then animate in the "no alarms" icon... 314 mFadeIn.start(); 315 316 // and animate out the alarm timeline view, expanding the width of the 317 // alarms list / undo bar. 318 mTimelineLayout.setVisibility(View.VISIBLE); 319 ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f) 320 .setDuration(ANIMATION_DURATION); 321 animator.setInterpolator(mCollapseInterpolator); 322 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 323 @Override 324 public void onAnimationUpdate(ValueAnimator animator) { 325 Float value = (Float) animator.getAnimatedValue(); 326 int currentTimelineWidth = (int) (value * mTimelineViewWidth); 327 float rightOffset = mTimelineViewWidth * (1 - value); 328 mTimelineLayout.setTranslationX(rightOffset); 329 mTimelineLayout.setAlpha(value); 330 mTimelineLayout.requestLayout(); 331 setUndoBarRightMargin(currentTimelineWidth 332 + mUndoBarInitialMargin); 333 } 334 }); 335 animator.addListener(new AnimatorListener() { 336 337 @Override 338 public void onAnimationCancel(Animator animation) { 339 // Do nothing. 340 } 341 342 @Override 343 public void onAnimationEnd(Animator animation) { 344 mTimelineView.setIsAnimatingOut(false); 345 } 346 347 @Override 348 public void onAnimationRepeat(Animator animation) { 349 // Do nothing. 350 } 351 352 @Override 353 public void onAnimationStart(Animator animation) { 354 mTimelineView.setIsAnimatingOut(true); 355 } 356 357 }); 358 animator.start(); 359 } else { 360 // If the previous adapter did not have alarms, no animation needed, 361 // just hide the timeline view and show the "no alarms" icon. 362 mTimelineLayout.setVisibility(View.GONE); 363 mEmptyView.setVisibility(View.VISIBLE); 364 setUndoBarRightMargin(mUndoBarInitialMargin); 365 } 366 } else { 367 368 // If there is no timeline view, just show the "no alarms" icon. 369 mEmptyView.setVisibility(View.VISIBLE); 370 } 371 } else { 372 373 // Otherwise, if the adapter DOES contain alarms... 374 mAddAlarmButton.setBackgroundResource(R.drawable.main_button_normal); 375 376 // ...and if there exists a timeline view (currently in tablet landscape mode) 377 if (mTimelineLayout != null && mAlarmsView != null) { 378 379 mTimelineLayout.setVisibility(View.VISIBLE); 380 // ...and if the previous adapter did not have alarms (indicating an add) 381 if (prevAdapterCount == 0) { 382 383 // Then, animate to hide the "no alarms" icon... 384 mFadeOut.start(); 385 386 // and animate to show the timeline view, reducing the width of the 387 // alarms list / undo bar. 388 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 389 .setDuration(ANIMATION_DURATION); 390 animator.setInterpolator(mExpandInterpolator); 391 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 392 @Override 393 public void onAnimationUpdate(ValueAnimator animator) { 394 Float value = (Float) animator.getAnimatedValue(); 395 int currentTimelineWidth = (int) (value * mTimelineViewWidth); 396 float rightOffset = mTimelineViewWidth * (1 - value); 397 mTimelineLayout.setTranslationX(rightOffset); 398 mTimelineLayout.setAlpha(value); 399 mTimelineLayout.requestLayout(); 400 ((FrameLayout.LayoutParams) mAlarmsView.getLayoutParams()) 401 .setMargins(0, 0, (int) -rightOffset, 0); 402 mAlarmsView.requestLayout(); 403 setUndoBarRightMargin(currentTimelineWidth 404 + mUndoBarInitialMargin); 405 } 406 }); 407 animator.start(); 408 } else { 409 mTimelineLayout.setVisibility(View.VISIBLE); 410 mEmptyView.setVisibility(View.GONE); 411 setUndoBarRightMargin(mUndoBarInitialMargin + mTimelineViewWidth); 412 } 413 } else { 414 415 // If there is no timeline view, just hide the "no alarms" icon. 416 mEmptyView.setVisibility(View.GONE); 417 } 418 } 419 420 // Cache this adapter's count for when the adapter changes. 421 prevAdapterCount = count; 422 super.onChanged(); 423 } 424 }); 425 426 if (mRingtoneTitleCache == null) { 427 mRingtoneTitleCache = new Bundle(); 428 } 429 430 mAlarmsList.setAdapter(mAdapter); 431 mAlarmsList.setVerticalScrollBarEnabled(true); 432 mAlarmsList.setOnCreateContextMenuListener(this); 433 434 if (mUndoShowing) { 435 showUndoBar(); 436 } 437 return v; 438 } 439 440 private void setUndoBarRightMargin(int margin) { 441 FrameLayout.LayoutParams params = 442 (FrameLayout.LayoutParams) mUndoBar.getLayoutParams(); 443 ((FrameLayout.LayoutParams) mUndoBar.getLayoutParams()) 444 .setMargins(params.leftMargin, params.topMargin, margin, params.bottomMargin); 445 mUndoBar.requestLayout(); 446 } 447 448 @Override 449 public void onResume() { 450 super.onResume(); 451 // Check if another app asked us to create a blank new alarm. 452 final Intent intent = getActivity().getIntent(); 453 if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) { 454 if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) { 455 // An external app asked us to create a blank alarm. 456 startCreatingAlarm(); 457 } 458 459 // Remove the CREATE_NEW extra now that we've processed it. 460 intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA); 461 } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) { 462 long alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID); 463 if (alarmId != Alarm.INVALID_ID) { 464 mScrollToAlarmId = alarmId; 465 if (mCursorLoader != null && mCursorLoader.isStarted()) { 466 // We need to force a reload here to make sure we have the latest view 467 // of the data to scroll to. 468 mCursorLoader.forceLoad(); 469 } 470 } 471 472 // Remove the SCROLL_TO_ALARM extra now that we've processed it. 473 intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA); 474 } 475 476 // Make sure to use the child FragmentManager. We have to use that one for the 477 // case where an intent comes in telling the activity to load the timepicker, 478 // which means we have to use that one everywhere so that the fragment can get 479 // correctly picked up here if it's open. 480 TimePickerDialog tpd = (TimePickerDialog) getChildFragmentManager(). 481 findFragmentByTag(AlarmUtils.FRAG_TAG_TIME_PICKER); 482 if (tpd != null) { 483 // The dialog is already open so we need to set the listener again. 484 tpd.setOnTimeSetListener(this); 485 } 486 } 487 488 private void hideUndoBar(boolean animate, MotionEvent event) { 489 if (mUndoBar != null) { 490 mUndoFrame.setVisibility(View.GONE); 491 if (event != null && mUndoBar.isEventInToastBar(event)) { 492 // Avoid touches inside the undo bar. 493 return; 494 } 495 mUndoBar.hide(animate); 496 } 497 mDeletedAlarm = null; 498 mUndoShowing = false; 499 } 500 501 private void showUndoBar() { 502 mUndoFrame.setVisibility(View.VISIBLE); 503 mUndoBar.show(new ActionableToastBar.ActionClickedListener() { 504 @Override 505 public void onActionClicked() { 506 asyncAddAlarm(mDeletedAlarm); 507 mDeletedAlarm = null; 508 mUndoShowing = false; 509 } 510 }, 0, getResources().getString(R.string.alarm_deleted), true, R.string.alarm_undo, true); 511 } 512 513 @Override 514 public void onSaveInstanceState(Bundle outState) { 515 super.onSaveInstanceState(outState); 516 outState.putLongArray(KEY_EXPANDED_IDS, mAdapter.getExpandedArray()); 517 outState.putLongArray(KEY_REPEAT_CHECKED_IDS, mAdapter.getRepeatArray()); 518 outState.putLongArray(KEY_SELECTED_ALARMS, mAdapter.getSelectedAlarmsArray()); 519 outState.putBundle(KEY_RINGTONE_TITLE_CACHE, mRingtoneTitleCache); 520 outState.putParcelable(KEY_DELETED_ALARM, mDeletedAlarm); 521 outState.putBoolean(KEY_UNDO_SHOWING, mUndoShowing); 522 outState.putBundle(KEY_PREVIOUS_DAY_MAP, mAdapter.getPreviousDaysOfWeekMap()); 523 outState.putParcelable(KEY_SELECTED_ALARM, mSelectedAlarm); 524 } 525 526 @Override 527 public void onDestroy() { 528 super.onDestroy(); 529 ToastMaster.cancelToast(); 530 } 531 532 @Override 533 public void onPause() { 534 super.onPause(); 535 // When the user places the app in the background by pressing "home", 536 // dismiss the toast bar. However, since there is no way to determine if 537 // home was pressed, just dismiss any existing toast bar when restarting 538 // the app. 539 hideUndoBar(false, null); 540 } 541 542 // Callback used by TimePickerDialog 543 @Override 544 public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute) { 545 if (mSelectedAlarm == null) { 546 // If mSelectedAlarm is null then we're creating a new alarm. 547 Alarm a = new Alarm(); 548 a.alert = RingtoneManager.getActualDefaultRingtoneUri(getActivity(), 549 RingtoneManager.TYPE_ALARM); 550 if (a.alert == null) { 551 a.alert = Uri.parse("content://settings/system/alarm_alert"); 552 } 553 a.hour = hourOfDay; 554 a.minutes = minute; 555 a.enabled = true; 556 asyncAddAlarm(a); 557 } else { 558 mSelectedAlarm.hour = hourOfDay; 559 mSelectedAlarm.minutes = minute; 560 mSelectedAlarm.enabled = true; 561 mScrollToAlarmId = mSelectedAlarm.id; 562 asyncUpdateAlarm(mSelectedAlarm, true); 563 mSelectedAlarm = null; 564 } 565 } 566 567 private void showLabelDialog(final Alarm alarm) { 568 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 569 final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog"); 570 if (prev != null) { 571 ft.remove(prev); 572 } 573 ft.addToBackStack(null); 574 575 // Create and show the dialog. 576 final LabelDialogFragment newFragment = 577 LabelDialogFragment.newInstance(alarm, alarm.label, getTag()); 578 newFragment.show(ft, "label_dialog"); 579 } 580 581 public void setLabel(Alarm alarm, String label) { 582 alarm.label = label; 583 asyncUpdateAlarm(alarm, false); 584 } 585 586 @Override 587 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 588 return Alarm.getAlarmsCursorLoader(getActivity()); 589 } 590 591 @Override 592 public void onLoadFinished(Loader<Cursor> cursorLoader, final Cursor data) { 593 mAdapter.swapCursor(data); 594 if (mScrollToAlarmId != -1) { 595 scrollToAlarm(mScrollToAlarmId); 596 mScrollToAlarmId = -1; 597 } 598 } 599 600 /** 601 * Scroll to alarm with given alarm id. 602 * 603 * @param alarmId The alarm id to scroll to. 604 */ 605 private void scrollToAlarm(long alarmId) { 606 int alarmPosition = -1; 607 for (int i = 0; i < mAdapter.getCount(); i++) { 608 long id = mAdapter.getItemId(i); 609 if (id == alarmId) { 610 alarmPosition = i; 611 break; 612 } 613 } 614 615 if (alarmPosition >= 0) { 616 mAdapter.setNewAlarm(alarmId); 617 mAlarmsList.smoothScrollToPositionFromTop(alarmPosition, 0); 618 } else { 619 // Trying to display a deleted alarm should only happen from a missed notification for 620 // an alarm that has been marked deleted after use. 621 Context context = getActivity().getApplicationContext(); 622 Toast toast = Toast.makeText(context, R.string.missed_alarm_has_been_deleted, 623 Toast.LENGTH_LONG); 624 ToastMaster.setToast(toast); 625 toast.show(); 626 } 627 } 628 629 @Override 630 public void onLoaderReset(Loader<Cursor> cursorLoader) { 631 mAdapter.swapCursor(null); 632 } 633 634 private void launchRingTonePicker(Alarm alarm) { 635 mSelectedAlarm = alarm; 636 Uri oldRingtone = Alarm.NO_RINGTONE_URI.equals(alarm.alert) ? null : alarm.alert; 637 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 638 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, oldRingtone); 639 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM); 640 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); 641 startActivityForResult(intent, REQUEST_CODE_RINGTONE); 642 } 643 644 private void saveRingtoneUri(Intent intent) { 645 Uri uri = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 646 if (uri == null) { 647 uri = Alarm.NO_RINGTONE_URI; 648 } 649 mSelectedAlarm.alert = uri; 650 651 // Save the last selected ringtone as the default for new alarms 652 if (!Alarm.NO_RINGTONE_URI.equals(uri)) { 653 RingtoneManager.setActualDefaultRingtoneUri( 654 getActivity(), RingtoneManager.TYPE_ALARM, uri); 655 } 656 asyncUpdateAlarm(mSelectedAlarm, false); 657 } 658 659 @Override 660 public void onActivityResult(int requestCode, int resultCode, Intent data) { 661 if (resultCode == Activity.RESULT_OK) { 662 switch (requestCode) { 663 case REQUEST_CODE_RINGTONE: 664 saveRingtoneUri(data); 665 break; 666 default: 667 Log.w("Unhandled request code in onActivityResult: " + requestCode); 668 } 669 } 670 } 671 672 public class AlarmItemAdapter extends CursorAdapter { 673 private static final int EXPAND_DURATION = 300; 674 private static final int COLLAPSE_DURATION = 250; 675 676 private final Context mContext; 677 private final LayoutInflater mFactory; 678 private final String[] mShortWeekDayStrings; 679 private final String[] mLongWeekDayStrings; 680 private final int mColorLit; 681 private final int mColorDim; 682 private final int mBackgroundColorExpanded; 683 private final int mBackgroundColor; 684 private final Typeface mRobotoNormal; 685 private final Typeface mRobotoBold; 686 private final ListView mList; 687 688 private final HashSet<Long> mExpanded = new HashSet<Long>(); 689 private final HashSet<Long> mRepeatChecked = new HashSet<Long>(); 690 private final HashSet<Long> mSelectedAlarms = new HashSet<Long>(); 691 private Bundle mPreviousDaysOfWeekMap = new Bundle(); 692 693 private final boolean mHasVibrator; 694 private final int mCollapseExpandHeight; 695 696 // This determines the order in which it is shown and processed in the UI. 697 private final int[] DAY_ORDER = new int[] { 698 Calendar.SUNDAY, 699 Calendar.MONDAY, 700 Calendar.TUESDAY, 701 Calendar.WEDNESDAY, 702 Calendar.THURSDAY, 703 Calendar.FRIDAY, 704 Calendar.SATURDAY, 705 }; 706 707 public class ItemHolder { 708 709 // views for optimization 710 LinearLayout alarmItem; 711 TextTime clock; 712 Switch onoff; 713 TextView daysOfWeek; 714 TextView label; 715 ImageView delete; 716 View expandArea; 717 View summary; 718 TextView clickableLabel; 719 CheckBox repeat; 720 LinearLayout repeatDays; 721 ViewGroup[] dayButtonParents = new ViewGroup[7]; 722 ToggleButton[] dayButtons = new ToggleButton[7]; 723 CheckBox vibrate; 724 TextView ringtone; 725 View hairLine; 726 View arrow; 727 View collapseExpandArea; 728 View footerFiller; 729 730 // Other states 731 Alarm alarm; 732 } 733 734 // Used for scrolling an expanded item in the list to make sure it is fully visible. 735 private long mScrollAlarmId = -1; 736 private final Runnable mScrollRunnable = new Runnable() { 737 @Override 738 public void run() { 739 if (mScrollAlarmId != -1) { 740 View v = getViewById(mScrollAlarmId); 741 if (v != null) { 742 Rect rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 743 mList.requestChildRectangleOnScreen(v, rect, false); 744 } 745 mScrollAlarmId = -1; 746 } 747 } 748 }; 749 750 public AlarmItemAdapter(Context context, long[] expandedIds, long[] repeatCheckedIds, 751 long[] selectedAlarms, Bundle previousDaysOfWeekMap, ListView list) { 752 super(context, null, 0); 753 mContext = context; 754 mFactory = LayoutInflater.from(context); 755 mList = list; 756 757 DateFormatSymbols dfs = new DateFormatSymbols(); 758 mShortWeekDayStrings = dfs.getShortWeekdays(); 759 mLongWeekDayStrings = dfs.getWeekdays(); 760 761 Resources res = mContext.getResources(); 762 mColorLit = res.getColor(R.color.clock_white); 763 mColorDim = res.getColor(R.color.clock_gray); 764 mBackgroundColorExpanded = res.getColor(R.color.alarm_whiteish); 765 mBackgroundColor = R.drawable.alarm_background_normal; 766 767 mRobotoBold = Typeface.create("sans-serif-condensed", Typeface.BOLD); 768 mRobotoNormal = Typeface.create("sans-serif-condensed", Typeface.NORMAL); 769 770 if (expandedIds != null) { 771 buildHashSetFromArray(expandedIds, mExpanded); 772 } 773 if (repeatCheckedIds != null) { 774 buildHashSetFromArray(repeatCheckedIds, mRepeatChecked); 775 } 776 if (previousDaysOfWeekMap != null) { 777 mPreviousDaysOfWeekMap = previousDaysOfWeekMap; 778 } 779 if (selectedAlarms != null) { 780 buildHashSetFromArray(selectedAlarms, mSelectedAlarms); 781 } 782 783 mHasVibrator = ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)) 784 .hasVibrator(); 785 786 mCollapseExpandHeight = (int) res.getDimension(R.dimen.collapse_expand_height); 787 } 788 789 public void removeSelectedId(int id) { 790 mSelectedAlarms.remove(id); 791 } 792 793 @Override 794 public View getView(int position, View convertView, ViewGroup parent) { 795 if (!getCursor().moveToPosition(position)) { 796 // May happen if the last alarm was deleted and the cursor refreshed while the 797 // list is updated. 798 Log.v("couldn't move cursor to position " + position); 799 return null; 800 } 801 View v; 802 if (convertView == null) { 803 v = newView(mContext, getCursor(), parent); 804 } else { 805 // TODO temporary hack to prevent the convertView from not having stuff we need. 806 boolean badConvertView = convertView.findViewById(R.id.digital_clock) == null; 807 // Do a translation check to test for animation. Change this to something more 808 // reliable and robust in the future. 809 if (convertView.getTranslationX() != 0 || convertView.getTranslationY() != 0 || 810 badConvertView) { 811 // view was animated, reset 812 v = newView(mContext, getCursor(), parent); 813 } else { 814 v = convertView; 815 } 816 } 817 bindView(v, mContext, getCursor()); 818 ItemHolder holder = (ItemHolder) v.getTag(); 819 820 // We need the footer for the last element of the array to allow the user to scroll 821 // the item beyond the bottom button bar, which obscures the view. 822 holder.footerFiller.setVisibility(position < getCount() - 1 ? View.GONE : View.VISIBLE); 823 return v; 824 } 825 826 @Override 827 public View newView(Context context, Cursor cursor, ViewGroup parent) { 828 final View view = mFactory.inflate(R.layout.alarm_time, parent, false); 829 setNewHolder(view); 830 return view; 831 } 832 833 /** 834 * In addition to changing the data set for the alarm list, swapCursor is now also 835 * responsible for preparing the list view's pre-draw operation for any animations that 836 * need to occur if an alarm was removed or added. 837 */ 838 @Override 839 public synchronized Cursor swapCursor(Cursor cursor) { 840 Cursor c = super.swapCursor(cursor); 841 842 if (mItemIdTopMap.isEmpty() && mAddedAlarm == null) { 843 return c; 844 } 845 846 final ListView list = mAlarmsList; 847 final ViewTreeObserver observer = list.getViewTreeObserver(); 848 849 /* 850 * Add a pre-draw listener to the observer to prepare for any possible animations to 851 * the alarms within the list view. The animations will occur if an alarm has been 852 * removed or added. 853 * 854 * For alarm removal, the remaining children should all retain their initial starting 855 * positions, and transition to their new positions. 856 * 857 * For alarm addition, the other children should all retain their initial starting 858 * positions, transition to their new positions, and at the end of that transition, the 859 * newly added alarm should appear in the designated space. 860 */ 861 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 862 863 private View mAddedView; 864 865 @Override 866 public boolean onPreDraw() { 867 // Remove the pre-draw listener, as this only needs to occur once. 868 if (observer.isAlive()) { 869 observer.removeOnPreDrawListener(this); 870 } 871 boolean firstAnimation = true; 872 int firstVisiblePosition = list.getFirstVisiblePosition(); 873 874 // Iterate through the children to prepare the add/remove animation. 875 for (int i = 0; i< list.getChildCount(); i++) { 876 final View child = list.getChildAt(i); 877 878 int position = firstVisiblePosition + i; 879 long itemId = mAdapter.getItemId(position); 880 881 // If this is the added alarm, set it invisible for now, and animate later. 882 if (mAddedAlarm != null && itemId == mAddedAlarm.id) { 883 mAddedView = child; 884 mAddedView.setAlpha(0.0f); 885 continue; 886 } 887 888 // The cached starting position of the child view. 889 Integer startTop = mItemIdTopMap.get(itemId); 890 // The new starting position of the child view. 891 int top = child.getTop(); 892 893 // If there is no cached starting position, determine whether the item has 894 // come from the top of bottom of the list view. 895 if (startTop == null) { 896 int childHeight = child.getHeight() + list.getDividerHeight(); 897 startTop = top + (i > 0 ? childHeight : -childHeight); 898 } 899 900 Log.d("Start Top: " + startTop + ", Top: " + top); 901 // If the starting position of the child view is different from the 902 // current position, animate the child. 903 if (startTop != top) { 904 int delta = startTop - top; 905 child.setTranslationY(delta); 906 child.animate().setDuration(ANIMATION_DURATION).translationY(0); 907 final View addedView = mAddedView; 908 if (firstAnimation) { 909 910 // If this is the first child being animated, then after the 911 // animation is complete, and animate in the added alarm (if one 912 // exists). 913 child.animate().withEndAction(new Runnable() { 914 915 @Override 916 public void run() { 917 918 919 // If there was an added view, animate it in after 920 // the other views have animated. 921 if (addedView != null) { 922 addedView.animate().alpha(1.0f) 923 .setDuration(ANIMATION_DURATION) 924 .withEndAction(new Runnable() { 925 926 @Override 927 public void run() { 928 // Re-enable the list after the add 929 // animation is complete. 930 list.setEnabled(true); 931 } 932 933 }); 934 } else { 935 // Re-enable the list after animations are complete. 936 list.setEnabled(true); 937 } 938 } 939 940 }); 941 firstAnimation = false; 942 } 943 } 944 } 945 946 // If there were no child views (outside of a possible added view) 947 // that require animation... 948 if (firstAnimation) { 949 if (mAddedView != null) { 950 // If there is an added view, prepare animation for the added view. 951 Log.d("Animating added view..."); 952 mAddedView.animate().alpha(1.0f) 953 .setDuration(ANIMATION_DURATION) 954 .withEndAction(new Runnable() { 955 @Override 956 public void run() { 957 // Re-enable the list after animations are complete. 958 list.setEnabled(true); 959 } 960 }); 961 } else { 962 // Re-enable the list after animations are complete. 963 list.setEnabled(true); 964 } 965 } 966 967 mAddedAlarm = null; 968 mItemIdTopMap.clear(); 969 return true; 970 } 971 }); 972 return c; 973 } 974 975 private void setNewHolder(View view) { 976 // standard view holder optimization 977 final ItemHolder holder = new ItemHolder(); 978 holder.alarmItem = (LinearLayout) view.findViewById(R.id.alarm_item); 979 holder.clock = (TextTime) view.findViewById(R.id.digital_clock); 980 holder.onoff = (Switch) view.findViewById(R.id.onoff); 981 holder.onoff.setTypeface(mRobotoNormal); 982 holder.daysOfWeek = (TextView) view.findViewById(R.id.daysOfWeek); 983 holder.label = (TextView) view.findViewById(R.id.label); 984 holder.delete = (ImageView) view.findViewById(R.id.delete); 985 holder.summary = view.findViewById(R.id.summary); 986 holder.expandArea = view.findViewById(R.id.expand_area); 987 holder.hairLine = view.findViewById(R.id.hairline); 988 holder.arrow = view.findViewById(R.id.arrow); 989 holder.repeat = (CheckBox) view.findViewById(R.id.repeat_onoff); 990 holder.clickableLabel = (TextView) view.findViewById(R.id.edit_label); 991 holder.repeatDays = (LinearLayout) view.findViewById(R.id.repeat_days); 992 holder.collapseExpandArea = view.findViewById(R.id.collapse_expand); 993 holder.footerFiller = view.findViewById(R.id.alarm_footer_filler); 994 holder.footerFiller.setOnClickListener(new OnClickListener() { 995 996 @Override 997 public void onClick(View v) { 998 // Do nothing. 999 } 1000 }); 1001 1002 // Build button for each day. 1003 for (int i = 0; i < 7; i++) { 1004 final ViewGroup viewgroup = (ViewGroup) mFactory.inflate(R.layout.day_button, 1005 holder.repeatDays, false); 1006 final ToggleButton button = (ToggleButton) viewgroup.getChildAt(0); 1007 final int dayToShowIndex = DAY_ORDER[i]; 1008 button.setText(mShortWeekDayStrings[dayToShowIndex]); 1009 button.setTextOn(mShortWeekDayStrings[dayToShowIndex]); 1010 button.setTextOff(mShortWeekDayStrings[dayToShowIndex]); 1011 button.setContentDescription(mLongWeekDayStrings[dayToShowIndex]); 1012 holder.repeatDays.addView(viewgroup); 1013 holder.dayButtons[i] = button; 1014 holder.dayButtonParents[i] = viewgroup; 1015 } 1016 holder.vibrate = (CheckBox) view.findViewById(R.id.vibrate_onoff); 1017 holder.ringtone = (TextView) view.findViewById(R.id.choose_ringtone); 1018 1019 view.setTag(holder); 1020 } 1021 1022 @Override 1023 public void bindView(final View view, Context context, final Cursor cursor) { 1024 final Alarm alarm = new Alarm(cursor); 1025 Object tag = view.getTag(); 1026 if (tag == null) { 1027 // The view was converted but somehow lost its tag. 1028 setNewHolder(view); 1029 } 1030 final ItemHolder itemHolder = (ItemHolder) tag; 1031 itemHolder.alarm = alarm; 1032 1033 // We must unset the listener first because this maybe a recycled view so changing the 1034 // state would affect the wrong alarm. 1035 itemHolder.onoff.setOnCheckedChangeListener(null); 1036 itemHolder.onoff.setChecked(alarm.enabled); 1037 1038 if (mSelectedAlarms.contains(itemHolder.alarm.id)) { 1039 itemHolder.alarmItem.setBackgroundColor(mBackgroundColorExpanded); 1040 setItemAlpha(itemHolder, true); 1041 itemHolder.onoff.setEnabled(false); 1042 } else { 1043 itemHolder.onoff.setEnabled(true); 1044 itemHolder.alarmItem.setBackgroundResource(mBackgroundColor); 1045 setItemAlpha(itemHolder, itemHolder.onoff.isChecked()); 1046 } 1047 itemHolder.clock.setFormat( 1048 (int)mContext.getResources().getDimension(R.dimen.alarm_label_size)); 1049 itemHolder.clock.setTime(alarm.hour, alarm.minutes); 1050 itemHolder.clock.setClickable(true); 1051 itemHolder.clock.setOnClickListener(new View.OnClickListener() { 1052 @Override 1053 public void onClick(View view) { 1054 mSelectedAlarm = itemHolder.alarm; 1055 AlarmUtils.showTimeEditDialog(getChildFragmentManager(), 1056 alarm, AlarmClockFragment.this 1057 , DateFormat.is24HourFormat(getActivity())); 1058 expandAlarm(itemHolder, true); 1059 itemHolder.alarmItem.post(mScrollRunnable); 1060 } 1061 }); 1062 1063 final CompoundButton.OnCheckedChangeListener onOffListener = 1064 new CompoundButton.OnCheckedChangeListener() { 1065 @Override 1066 public void onCheckedChanged(CompoundButton compoundButton, 1067 boolean checked) { 1068 if (checked != alarm.enabled) { 1069 setItemAlpha(itemHolder, checked); 1070 alarm.enabled = checked; 1071 asyncUpdateAlarm(alarm, alarm.enabled); 1072 } 1073 } 1074 }; 1075 1076 itemHolder.onoff.setOnCheckedChangeListener(onOffListener); 1077 1078 boolean expanded = isAlarmExpanded(alarm); 1079 itemHolder.expandArea.setVisibility(expanded? View.VISIBLE : View.GONE); 1080 itemHolder.summary.setVisibility(expanded? View.GONE : View.VISIBLE); 1081 1082 String labelSpace = ""; 1083 // Set the repeat text or leave it blank if it does not repeat. 1084 final String daysOfWeekStr = 1085 alarm.daysOfWeek.toString(AlarmClockFragment.this.getActivity(), false); 1086 if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) { 1087 itemHolder.daysOfWeek.setText(daysOfWeekStr); 1088 itemHolder.daysOfWeek.setContentDescription(alarm.daysOfWeek.toAccessibilityString( 1089 AlarmClockFragment.this.getActivity())); 1090 itemHolder.daysOfWeek.setVisibility(View.VISIBLE); 1091 labelSpace = " "; 1092 itemHolder.daysOfWeek.setOnClickListener(new View.OnClickListener() { 1093 @Override 1094 public void onClick(View view) { 1095 expandAlarm(itemHolder, true); 1096 itemHolder.alarmItem.post(mScrollRunnable); 1097 } 1098 }); 1099 1100 } else { 1101 itemHolder.daysOfWeek.setVisibility(View.GONE); 1102 } 1103 1104 if (alarm.label != null && alarm.label.length() != 0) { 1105 itemHolder.label.setText(alarm.label + labelSpace); 1106 itemHolder.label.setVisibility(View.VISIBLE); 1107 itemHolder.label.setContentDescription( 1108 mContext.getResources().getString(R.string.label_description) + " " 1109 + alarm.label); 1110 itemHolder.label.setOnClickListener(new View.OnClickListener() { 1111 @Override 1112 public void onClick(View view) { 1113 expandAlarm(itemHolder, true); 1114 itemHolder.alarmItem.post(mScrollRunnable); 1115 } 1116 }); 1117 } else { 1118 itemHolder.label.setVisibility(View.GONE); 1119 } 1120 1121 itemHolder.delete.setOnClickListener(new View.OnClickListener() { 1122 @Override 1123 public void onClick(View v) { 1124 mDeletedAlarm = alarm; 1125 1126 view.animate().setDuration(ANIMATION_DURATION).alpha(0).translationY(-1) 1127 .withEndAction(new Runnable() { 1128 1129 @Override 1130 public void run() { 1131 asyncDeleteAlarm(mDeletedAlarm, view); 1132 } 1133 }); 1134 } 1135 }); 1136 1137 if (expanded) { 1138 expandAlarm(itemHolder, false); 1139 } else { 1140 collapseAlarm(itemHolder, false); 1141 } 1142 1143 itemHolder.alarmItem.setOnClickListener(new View.OnClickListener() { 1144 @Override 1145 public void onClick(View view) { 1146 if (isAlarmExpanded(alarm)) { 1147 collapseAlarm(itemHolder, true); 1148 } else { 1149 expandAlarm(itemHolder, true); 1150 } 1151 } 1152 }); 1153 } 1154 1155 private void bindExpandArea(final ItemHolder itemHolder, final Alarm alarm) { 1156 // Views in here are not bound until the item is expanded. 1157 1158 if (alarm.label != null && alarm.label.length() > 0) { 1159 itemHolder.clickableLabel.setText(alarm.label); 1160 itemHolder.clickableLabel.setTextColor(mColorLit); 1161 } else { 1162 itemHolder.clickableLabel.setText(R.string.label); 1163 itemHolder.clickableLabel.setTextColor(mColorDim); 1164 } 1165 itemHolder.clickableLabel.setOnClickListener(new View.OnClickListener() { 1166 @Override 1167 public void onClick(View view) { 1168 showLabelDialog(alarm); 1169 } 1170 }); 1171 1172 if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeating()) { 1173 itemHolder.repeat.setChecked(true); 1174 itemHolder.repeatDays.setVisibility(View.VISIBLE); 1175 } else { 1176 itemHolder.repeat.setChecked(false); 1177 itemHolder.repeatDays.setVisibility(View.GONE); 1178 } 1179 itemHolder.repeat.setOnClickListener(new View.OnClickListener() { 1180 @Override 1181 public void onClick(View view) { 1182 final boolean checked = ((CheckBox) view).isChecked(); 1183 if (checked) { 1184 // Show days 1185 itemHolder.repeatDays.setVisibility(View.VISIBLE); 1186 mRepeatChecked.add(alarm.id); 1187 1188 // Set all previously set days 1189 // or 1190 // Set all days if no previous. 1191 final int bitSet = mPreviousDaysOfWeekMap.getInt("" + alarm.id); 1192 alarm.daysOfWeek.setBitSet(bitSet); 1193 if (!alarm.daysOfWeek.isRepeating()) { 1194 alarm.daysOfWeek.setDaysOfWeek(true, DAY_ORDER); 1195 } 1196 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 1197 } else { 1198 itemHolder.repeatDays.setVisibility(View.GONE); 1199 mRepeatChecked.remove(alarm.id); 1200 1201 // Remember the set days in case the user wants it back. 1202 final int bitSet = alarm.daysOfWeek.getBitSet(); 1203 mPreviousDaysOfWeekMap.putInt("" + alarm.id, bitSet); 1204 1205 // Remove all repeat days 1206 alarm.daysOfWeek.clearAllDays(); 1207 } 1208 asyncUpdateAlarm(alarm, false); 1209 } 1210 }); 1211 1212 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 1213 for (int i = 0; i < 7; i++) { 1214 final int buttonIndex = i; 1215 1216 itemHolder.dayButtonParents[i].setOnClickListener(new View.OnClickListener() { 1217 @Override 1218 public void onClick(View view) { 1219 itemHolder.dayButtons[buttonIndex].toggle(); 1220 final boolean checked = itemHolder.dayButtons[buttonIndex].isChecked(); 1221 int day = DAY_ORDER[buttonIndex]; 1222 alarm.daysOfWeek.setDaysOfWeek(checked, day); 1223 if (checked) { 1224 turnOnDayOfWeek(itemHolder, buttonIndex); 1225 } else { 1226 turnOffDayOfWeek(itemHolder, buttonIndex); 1227 1228 // See if this was the last day, if so, un-check the repeat box. 1229 if (!alarm.daysOfWeek.isRepeating()) { 1230 itemHolder.repeatDays.setVisibility(View.GONE); 1231 itemHolder.repeat.setTextColor(mColorDim); 1232 mRepeatChecked.remove(alarm.id); 1233 1234 // Set history to no days, so it will be everyday when repeat is 1235 // turned back on 1236 mPreviousDaysOfWeekMap.putInt("" + alarm.id, 1237 DaysOfWeek.NO_DAYS_SET); 1238 } 1239 } 1240 asyncUpdateAlarm(alarm, false); 1241 } 1242 }); 1243 } 1244 1245 1246 if (!mHasVibrator) { 1247 itemHolder.vibrate.setVisibility(View.INVISIBLE); 1248 } else { 1249 itemHolder.vibrate.setVisibility(View.VISIBLE); 1250 if (!alarm.vibrate) { 1251 itemHolder.vibrate.setChecked(false); 1252 itemHolder.vibrate.setTextColor(mColorDim); 1253 } else { 1254 itemHolder.vibrate.setChecked(true); 1255 itemHolder.vibrate.setTextColor(mColorLit); 1256 } 1257 } 1258 1259 itemHolder.vibrate.setOnClickListener(new View.OnClickListener() { 1260 @Override 1261 public void onClick(View v) { 1262 final boolean checked = ((CheckBox) v).isChecked(); 1263 if (checked) { 1264 itemHolder.vibrate.setTextColor(mColorLit); 1265 } else { 1266 itemHolder.vibrate.setTextColor(mColorDim); 1267 } 1268 alarm.vibrate = checked; 1269 asyncUpdateAlarm(alarm, false); 1270 } 1271 }); 1272 1273 final String ringtone; 1274 if (Alarm.NO_RINGTONE_URI.equals(alarm.alert)) { 1275 ringtone = mContext.getResources().getString(R.string.silent_alarm_summary); 1276 } else { 1277 ringtone = getRingToneTitle(alarm.alert); 1278 } 1279 itemHolder.ringtone.setText(ringtone); 1280 itemHolder.ringtone.setContentDescription( 1281 mContext.getResources().getString(R.string.ringtone_description) + " " 1282 + ringtone); 1283 itemHolder.ringtone.setOnClickListener(new View.OnClickListener() { 1284 @Override 1285 public void onClick(View view) { 1286 launchRingTonePicker(alarm); 1287 } 1288 }); 1289 } 1290 1291 // Sets the alpha of the item except the on/off switch. This gives a visual effect 1292 // for enabled/disabled alarm while leaving the on/off switch more visible 1293 private void setItemAlpha(ItemHolder holder, boolean enabled) { 1294 float alpha = enabled ? 1f : 0.5f; 1295 holder.clock.setAlpha(alpha); 1296 holder.summary.setAlpha(alpha); 1297 holder.expandArea.setAlpha(alpha); 1298 holder.delete.setAlpha(alpha); 1299 holder.daysOfWeek.setAlpha(alpha); 1300 } 1301 1302 private void updateDaysOfWeekButtons(ItemHolder holder, DaysOfWeek daysOfWeek) { 1303 HashSet<Integer> setDays = daysOfWeek.getSetDays(); 1304 for (int i = 0; i < 7; i++) { 1305 if (setDays.contains(DAY_ORDER[i])) { 1306 turnOnDayOfWeek(holder, i); 1307 } else { 1308 turnOffDayOfWeek(holder, i); 1309 } 1310 } 1311 } 1312 1313 public void toggleSelectState(View v) { 1314 // long press could be on the parent view or one of its childs, so find the parent view 1315 v = getTopParent(v); 1316 if (v != null) { 1317 long id = ((ItemHolder)v.getTag()).alarm.id; 1318 if (mSelectedAlarms.contains(id)) { 1319 mSelectedAlarms.remove(id); 1320 } else { 1321 mSelectedAlarms.add(id); 1322 } 1323 } 1324 } 1325 1326 private View getTopParent(View v) { 1327 while (v != null && v.getId() != R.id.alarm_item) { 1328 v = (View) v.getParent(); 1329 } 1330 return v; 1331 } 1332 1333 public int getSelectedItemsNum() { 1334 return mSelectedAlarms.size(); 1335 } 1336 1337 private void turnOffDayOfWeek(ItemHolder holder, int dayIndex) { 1338 holder.dayButtons[dayIndex].setChecked(false); 1339 holder.dayButtons[dayIndex].setTextColor(mColorDim); 1340 holder.dayButtons[dayIndex].setTypeface(mRobotoNormal); 1341 } 1342 1343 private void turnOnDayOfWeek(ItemHolder holder, int dayIndex) { 1344 holder.dayButtons[dayIndex].setChecked(true); 1345 holder.dayButtons[dayIndex].setTextColor(mColorLit); 1346 holder.dayButtons[dayIndex].setTypeface(mRobotoBold); 1347 } 1348 1349 1350 /** 1351 * Does a read-through cache for ringtone titles. 1352 * 1353 * @param uri The uri of the ringtone. 1354 * @return The ringtone title. {@literal null} if no matching ringtone found. 1355 */ 1356 private String getRingToneTitle(Uri uri) { 1357 // Try the cache first 1358 String title = mRingtoneTitleCache.getString(uri.toString()); 1359 if (title == null) { 1360 // This is slow because a media player is created during Ringtone object creation. 1361 Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri); 1362 title = ringTone.getTitle(mContext); 1363 if (title != null) { 1364 mRingtoneTitleCache.putString(uri.toString(), title); 1365 } 1366 } 1367 return title; 1368 } 1369 1370 public void setNewAlarm(long alarmId) { 1371 mExpanded.add(alarmId); 1372 } 1373 1374 /** 1375 * Expands the alarm for editing. 1376 * 1377 * @param itemHolder The item holder instance. 1378 */ 1379 private void expandAlarm(final ItemHolder itemHolder, boolean animate) { 1380 mExpanded.add(itemHolder.alarm.id); 1381 bindExpandArea(itemHolder, itemHolder.alarm); 1382 // Scroll the view to make sure it is fully viewed 1383 mScrollAlarmId = itemHolder.alarm.id; 1384 1385 // Save the starting height so we can animate from this value. 1386 final int startingHeight = itemHolder.alarmItem.getHeight(); 1387 1388 // Set the expand area to visible so we can measure the height to animate to. 1389 itemHolder.alarmItem.setBackgroundColor(mBackgroundColorExpanded); 1390 itemHolder.expandArea.setVisibility(View.VISIBLE); 1391 1392 if (!animate) { 1393 // Set the "end" layout and don't do the animation. 1394 itemHolder.arrow.setRotation(180); 1395 // We need to translate the hairline up, so the height of the collapseArea 1396 // needs to be measured to know how high to translate it. 1397 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1398 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1399 @Override 1400 public boolean onPreDraw() { 1401 // We don't want to continue getting called for every listview drawing. 1402 if (observer.isAlive()) { 1403 observer.removeOnPreDrawListener(this); 1404 } 1405 int hairlineHeight = itemHolder.hairLine.getHeight(); 1406 int collapseHeight = 1407 itemHolder.collapseExpandArea.getHeight() - hairlineHeight; 1408 itemHolder.hairLine.setTranslationY(-collapseHeight); 1409 return true; 1410 } 1411 }); 1412 return; 1413 } 1414 1415 // Add an onPreDrawListener, which gets called after measurement but before the draw. 1416 // This way we can check the height we need to animate to before any drawing. 1417 // Note the series of events: 1418 // * expandArea is set to VISIBLE, which causes a layout pass 1419 // * the view is measured, and our onPreDrawListener is called 1420 // * we set up the animation using the start and end values. 1421 // * the height is set back to the starting point so it can be animated down. 1422 // * request another layout pass. 1423 // * return false so that onDraw() is not called for the single frame before 1424 // the animations have started. 1425 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1426 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1427 @Override 1428 public boolean onPreDraw() { 1429 // We don't want to continue getting called for every listview drawing. 1430 if (observer.isAlive()) { 1431 observer.removeOnPreDrawListener(this); 1432 } 1433 // Calculate some values to help with the animation. 1434 final int endingHeight = itemHolder.alarmItem.getHeight(); 1435 final int distance = endingHeight - startingHeight; 1436 final int collapseHeight = itemHolder.collapseExpandArea.getHeight(); 1437 int hairlineHeight = itemHolder.hairLine.getHeight(); 1438 final int hairlineDistance = collapseHeight - hairlineHeight; 1439 1440 // Set the height back to the start state of the animation. 1441 itemHolder.alarmItem.getLayoutParams().height = startingHeight; 1442 // To allow the expandArea to glide in with the expansion animation, set a 1443 // negative top margin, which will animate down to a margin of 0 as the height 1444 // is increased. 1445 // Note that we need to maintain the bottom margin as a fixed value (instead of 1446 // just using a listview, to allow for a flatter hierarchy) to fit the bottom 1447 // bar underneath. 1448 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1449 itemHolder.expandArea.getLayoutParams(); 1450 expandParams.setMargins(0, -distance, 0, collapseHeight); 1451 itemHolder.alarmItem.requestLayout(); 1452 1453 // Set up the animator to animate the expansion. 1454 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 1455 .setDuration(EXPAND_DURATION); 1456 animator.setInterpolator(mExpandInterpolator); 1457 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1458 @Override 1459 public void onAnimationUpdate(ValueAnimator animator) { 1460 Float value = (Float) animator.getAnimatedValue(); 1461 1462 // For each value from 0 to 1, animate the various parts of the layout. 1463 itemHolder.alarmItem.getLayoutParams().height = 1464 (int) (value * distance + startingHeight); 1465 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1466 itemHolder.expandArea.getLayoutParams(); 1467 expandParams.setMargins( 1468 0, (int) -((1 - value) * distance), 0, collapseHeight); 1469 itemHolder.arrow.setRotation(180 * value); 1470 itemHolder.hairLine.setTranslationY(-hairlineDistance * value); 1471 itemHolder.summary.setAlpha(1 - value); 1472 1473 itemHolder.alarmItem.requestLayout(); 1474 } 1475 }); 1476 // Set everything to their final values when the animation's done. 1477 animator.addListener(new AnimatorListener() { 1478 @Override 1479 public void onAnimationEnd(Animator animation) { 1480 // Set it back to wrap content since we'd explicitly set the height. 1481 itemHolder.alarmItem.getLayoutParams().height = 1482 LayoutParams.WRAP_CONTENT; 1483 itemHolder.arrow.setRotation(180); 1484 itemHolder.hairLine.setTranslationY(-hairlineDistance); 1485 itemHolder.summary.setVisibility(View.GONE); 1486 } 1487 1488 @Override 1489 public void onAnimationCancel(Animator animation) { 1490 // TODO we may have to deal with cancelations of the animation. 1491 } 1492 1493 @Override 1494 public void onAnimationRepeat(Animator animation) { } 1495 @Override 1496 public void onAnimationStart(Animator animation) { } 1497 }); 1498 animator.start(); 1499 1500 // Return false so this draw does not occur to prevent the final frame from 1501 // being drawn for the single frame before the animations start. 1502 return false; 1503 } 1504 }); 1505 } 1506 1507 private boolean isAlarmExpanded(Alarm alarm) { 1508 return mExpanded.contains(alarm.id); 1509 } 1510 1511 private void collapseAlarm(final ItemHolder itemHolder, boolean animate) { 1512 mExpanded.remove(itemHolder.alarm.id); 1513 1514 // Save the starting height so we can animate from this value. 1515 final int startingHeight = itemHolder.alarmItem.getHeight(); 1516 1517 // Set the expand area to gone so we can measure the height to animate to. 1518 itemHolder.alarmItem.setBackgroundResource(mBackgroundColor); 1519 itemHolder.expandArea.setVisibility(View.GONE); 1520 1521 if (!animate) { 1522 // Set the "end" layout and don't do the animation. 1523 itemHolder.arrow.setRotation(0); 1524 itemHolder.hairLine.setTranslationY(0); 1525 return; 1526 } 1527 1528 // Add an onPreDrawListener, which gets called after measurement but before the draw. 1529 // This way we can check the height we need to animate to before any drawing. 1530 // Note the series of events: 1531 // * expandArea is set to GONE, which causes a layout pass 1532 // * the view is measured, and our onPreDrawListener is called 1533 // * we set up the animation using the start and end values. 1534 // * expandArea is set to VISIBLE again so it can be shown animating. 1535 // * request another layout pass. 1536 // * return false so that onDraw() is not called for the single frame before 1537 // the animations have started. 1538 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1539 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1540 @Override 1541 public boolean onPreDraw() { 1542 if (observer.isAlive()) { 1543 observer.removeOnPreDrawListener(this); 1544 } 1545 1546 // Calculate some values to help with the animation. 1547 final int endingHeight = itemHolder.alarmItem.getHeight(); 1548 final int distance = endingHeight - startingHeight; 1549 int hairlineHeight = itemHolder.hairLine.getHeight(); 1550 final int hairlineDistance = mCollapseExpandHeight - hairlineHeight; 1551 1552 // Re-set the visibilities for the start state of the animation. 1553 itemHolder.expandArea.setVisibility(View.VISIBLE); 1554 itemHolder.summary.setVisibility(View.VISIBLE); 1555 itemHolder.summary.setAlpha(1); 1556 1557 // Set up the animator to animate the expansion. 1558 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 1559 .setDuration(COLLAPSE_DURATION); 1560 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1561 @Override 1562 public void onAnimationUpdate(ValueAnimator animator) { 1563 Float value = (Float) animator.getAnimatedValue(); 1564 1565 // For each value from 0 to 1, animate the various parts of the layout. 1566 itemHolder.alarmItem.getLayoutParams().height = 1567 (int) (value * distance + startingHeight); 1568 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1569 itemHolder.expandArea.getLayoutParams(); 1570 expandParams.setMargins( 1571 0, (int) (value * distance), 0, mCollapseExpandHeight); 1572 itemHolder.arrow.setRotation(180 * (1 - value)); 1573 itemHolder.hairLine.setTranslationY(-hairlineDistance * (1 - value)); 1574 itemHolder.summary.setAlpha(value); 1575 1576 itemHolder.alarmItem.requestLayout(); 1577 } 1578 }); 1579 animator.setInterpolator(mCollapseInterpolator); 1580 // Set everything to their final values when the animation's done. 1581 animator.addListener(new AnimatorListener() { 1582 @Override 1583 public void onAnimationEnd(Animator animation) { 1584 // Set it back to wrap content since we'd explicitly set the height. 1585 itemHolder.alarmItem.getLayoutParams().height = 1586 LayoutParams.WRAP_CONTENT; 1587 1588 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1589 itemHolder.expandArea.getLayoutParams(); 1590 expandParams.setMargins(0, 0, 0, mCollapseExpandHeight); 1591 1592 itemHolder.expandArea.setVisibility(View.GONE); 1593 itemHolder.arrow.setRotation(0); 1594 itemHolder.hairLine.setTranslationY(0); 1595 } 1596 1597 @Override 1598 public void onAnimationCancel(Animator animation) { 1599 // TODO we may have to deal with cancelations of the animation. 1600 } 1601 1602 @Override 1603 public void onAnimationRepeat(Animator animation) { } 1604 @Override 1605 public void onAnimationStart(Animator animation) { } 1606 }); 1607 animator.start(); 1608 1609 return false; 1610 } 1611 }); 1612 } 1613 1614 @Override 1615 public int getViewTypeCount() { 1616 return 1; 1617 } 1618 1619 private View getViewById(long id) { 1620 for (int i = 0; i < mList.getCount(); i++) { 1621 View v = mList.getChildAt(i); 1622 if (v != null) { 1623 ItemHolder h = (ItemHolder)(v.getTag()); 1624 if (h != null && h.alarm.id == id) { 1625 return v; 1626 } 1627 } 1628 } 1629 return null; 1630 } 1631 1632 public long[] getExpandedArray() { 1633 int index = 0; 1634 long[] ids = new long[mExpanded.size()]; 1635 for (long id : mExpanded) { 1636 ids[index] = id; 1637 index++; 1638 } 1639 return ids; 1640 } 1641 1642 public long[] getSelectedAlarmsArray() { 1643 int index = 0; 1644 long[] ids = new long[mSelectedAlarms.size()]; 1645 for (long id : mSelectedAlarms) { 1646 ids[index] = id; 1647 index++; 1648 } 1649 return ids; 1650 } 1651 1652 public long[] getRepeatArray() { 1653 int index = 0; 1654 long[] ids = new long[mRepeatChecked.size()]; 1655 for (long id : mRepeatChecked) { 1656 ids[index] = id; 1657 index++; 1658 } 1659 return ids; 1660 } 1661 1662 public Bundle getPreviousDaysOfWeekMap() { 1663 return mPreviousDaysOfWeekMap; 1664 } 1665 1666 private void buildHashSetFromArray(long[] ids, HashSet<Long> set) { 1667 for (long id : ids) { 1668 set.add(id); 1669 } 1670 } 1671 } 1672 1673 private void startCreatingAlarm() { 1674 // Set the "selected" alarm as null, and we'll create the new one when the timepicker 1675 // comes back. 1676 mSelectedAlarm = null; 1677 AlarmUtils.showTimeEditDialog(getChildFragmentManager(), 1678 null, AlarmClockFragment.this, DateFormat.is24HourFormat(getActivity())); 1679 } 1680 1681 private static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) { 1682 ContentResolver cr = context.getContentResolver(); 1683 AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance()); 1684 newInstance = AlarmInstance.addInstance(cr, newInstance); 1685 // Register instance to state manager 1686 AlarmStateManager.registerInstance(context, newInstance, true); 1687 return newInstance; 1688 } 1689 1690 private void asyncDeleteAlarm(final Alarm alarm, final View viewToRemove) { 1691 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1692 final AsyncTask<Void, Void, Void> deleteTask = new AsyncTask<Void, Void, Void>() { 1693 @Override 1694 public synchronized void onPreExecute() { 1695 if (viewToRemove == null) { 1696 return; 1697 } 1698 // The alarm list needs to be disabled until the animation finishes to prevent 1699 // possible concurrency issues. It becomes re-enabled after the animations have 1700 // completed. 1701 mAlarmsList.setEnabled(false); 1702 1703 // Store all of the current list view item positions in memory for animation. 1704 final ListView list = mAlarmsList; 1705 int firstVisiblePosition = list.getFirstVisiblePosition(); 1706 for (int i=0; i<list.getChildCount(); i++) { 1707 View child = list.getChildAt(i); 1708 if (child != viewToRemove) { 1709 int position = firstVisiblePosition + i; 1710 long itemId = mAdapter.getItemId(position); 1711 mItemIdTopMap.put(itemId, child.getTop()); 1712 } 1713 } 1714 } 1715 1716 @Override 1717 protected Void doInBackground(Void... parameters) { 1718 // Activity may be closed at this point , make sure data is still valid 1719 if (context != null && alarm != null) { 1720 ContentResolver cr = context.getContentResolver(); 1721 AlarmStateManager.deleteAllInstances(context, alarm.id); 1722 Alarm.deleteAlarm(cr, alarm.id); 1723 } 1724 return null; 1725 } 1726 }; 1727 mUndoShowing = true; 1728 deleteTask.execute(); 1729 } 1730 1731 private void asyncAddAlarm(final Alarm alarm) { 1732 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1733 final AsyncTask<Void, Void, AlarmInstance> updateTask = 1734 new AsyncTask<Void, Void, AlarmInstance>() { 1735 @Override 1736 public synchronized void onPreExecute() { 1737 final ListView list = mAlarmsList; 1738 // The alarm list needs to be disabled until the animation finishes to prevent 1739 // possible concurrency issues. It becomes re-enabled after the animations have 1740 // completed. 1741 mAlarmsList.setEnabled(false); 1742 1743 // Store all of the current list view item positions in memory for animation. 1744 int firstVisiblePosition = list.getFirstVisiblePosition(); 1745 for (int i=0; i<list.getChildCount(); i++) { 1746 View child = list.getChildAt(i); 1747 int position = firstVisiblePosition + i; 1748 long itemId = mAdapter.getItemId(position); 1749 mItemIdTopMap.put(itemId, child.getTop()); 1750 } 1751 } 1752 1753 @Override 1754 protected AlarmInstance doInBackground(Void... parameters) { 1755 if (context != null && alarm != null) { 1756 ContentResolver cr = context.getContentResolver(); 1757 1758 // Add alarm to db 1759 Alarm newAlarm = Alarm.addAlarm(cr, alarm); 1760 mScrollToAlarmId = newAlarm.id; 1761 1762 // Create and add instance to db 1763 if (newAlarm.enabled) { 1764 return setupAlarmInstance(context, newAlarm); 1765 } 1766 } 1767 return null; 1768 } 1769 1770 @Override 1771 protected void onPostExecute(AlarmInstance instance) { 1772 if (instance != null) { 1773 AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis()); 1774 } 1775 } 1776 }; 1777 updateTask.execute(); 1778 } 1779 1780 private void asyncUpdateAlarm(final Alarm alarm, final boolean popToast) { 1781 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1782 final AsyncTask<Void, Void, AlarmInstance> updateTask = 1783 new AsyncTask<Void, Void, AlarmInstance>() { 1784 @Override 1785 protected AlarmInstance doInBackground(Void ... parameters) { 1786 ContentResolver cr = context.getContentResolver(); 1787 1788 // Dismiss all old instances 1789 AlarmStateManager.deleteAllInstances(context, alarm.id); 1790 1791 // Update alarm 1792 Alarm.updateAlarm(cr, alarm); 1793 if (alarm.enabled) { 1794 return setupAlarmInstance(context, alarm); 1795 } 1796 1797 return null; 1798 } 1799 1800 @Override 1801 protected void onPostExecute(AlarmInstance instance) { 1802 if (popToast && instance != null) { 1803 AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis()); 1804 } 1805 } 1806 }; 1807 updateTask.execute(); 1808 } 1809 1810 @Override 1811 public boolean onTouch(View v, MotionEvent event) { 1812 hideUndoBar(true, event); 1813 return false; 1814 } 1815 } 1816