AlarmClockFragment.java revision c677dfde1d276531c10efd8008b342318b04782e
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 17package com.android.deskclock; 18 19import android.animation.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.AnimatorInflater; 22import android.animation.ValueAnimator; 23import android.app.Activity; 24import android.app.Fragment; 25import android.app.FragmentTransaction; 26import android.app.LoaderManager; 27import android.content.ContentResolver; 28import android.content.Context; 29import android.content.Intent; 30import android.content.Loader; 31import android.content.res.Configuration; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.database.DataSetObserver; 35import android.graphics.Rect; 36import android.graphics.Typeface; 37import android.media.Ringtone; 38import android.media.RingtoneManager; 39import android.net.Uri; 40import android.os.AsyncTask; 41import android.os.Bundle; 42import android.os.Vibrator; 43import android.text.format.DateFormat; 44import android.view.Gravity; 45import android.view.LayoutInflater; 46import android.view.MotionEvent; 47import android.view.View; 48import android.view.View.OnClickListener; 49import android.view.ViewGroup; 50import android.view.ViewGroup.LayoutParams; 51import android.view.ViewTreeObserver; 52import android.view.animation.DecelerateInterpolator; 53import android.view.animation.Interpolator; 54import android.widget.CheckBox; 55import android.widget.CompoundButton; 56import android.widget.CursorAdapter; 57import android.widget.FrameLayout; 58import android.widget.ImageButton; 59import android.widget.ImageView; 60import android.widget.LinearLayout; 61import android.widget.ListView; 62import android.widget.Switch; 63import android.widget.TextView; 64import android.widget.Toast; 65import android.widget.ToggleButton; 66 67import com.android.datetimepicker.time.RadialPickerLayout; 68import com.android.datetimepicker.time.TimePickerDialog; 69import com.android.deskclock.alarms.AlarmStateManager; 70import com.android.deskclock.provider.Alarm; 71import com.android.deskclock.provider.AlarmInstance; 72import com.android.deskclock.provider.DaysOfWeek; 73import com.android.deskclock.widget.ActionableToastBar; 74import com.android.deskclock.widget.TextTime; 75 76import java.text.DateFormatSymbols; 77import java.util.Calendar; 78import java.util.HashSet; 79import java.util.concurrent.ConcurrentHashMap; 80 81/** 82 * AlarmClock application. 83 */ 84public 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 1048 itemHolder.clock.setTime(alarm.hour, alarm.minutes); 1049 itemHolder.clock.setClickable(true); 1050 itemHolder.clock.setOnClickListener(new View.OnClickListener() { 1051 @Override 1052 public void onClick(View view) { 1053 mSelectedAlarm = itemHolder.alarm; 1054 AlarmUtils.showTimeEditDialog(getChildFragmentManager(), 1055 alarm, AlarmClockFragment.this 1056 , DateFormat.is24HourFormat(getActivity())); 1057 expandAlarm(itemHolder, true); 1058 itemHolder.alarmItem.post(mScrollRunnable); 1059 } 1060 }); 1061 1062 final CompoundButton.OnCheckedChangeListener onOffListener = 1063 new CompoundButton.OnCheckedChangeListener() { 1064 @Override 1065 public void onCheckedChanged(CompoundButton compoundButton, 1066 boolean checked) { 1067 if (checked != alarm.enabled) { 1068 setItemAlpha(itemHolder, checked); 1069 alarm.enabled = checked; 1070 asyncUpdateAlarm(alarm, alarm.enabled); 1071 } 1072 } 1073 }; 1074 1075 itemHolder.onoff.setOnCheckedChangeListener(onOffListener); 1076 1077 boolean expanded = isAlarmExpanded(alarm); 1078 itemHolder.expandArea.setVisibility(expanded? View.VISIBLE : View.GONE); 1079 itemHolder.summary.setVisibility(expanded? View.GONE : View.VISIBLE); 1080 1081 String labelSpace = ""; 1082 // Set the repeat text or leave it blank if it does not repeat. 1083 final String daysOfWeekStr = 1084 alarm.daysOfWeek.toString(AlarmClockFragment.this.getActivity(), false); 1085 if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) { 1086 itemHolder.daysOfWeek.setText(daysOfWeekStr); 1087 itemHolder.daysOfWeek.setContentDescription(alarm.daysOfWeek.toAccessibilityString( 1088 AlarmClockFragment.this.getActivity())); 1089 itemHolder.daysOfWeek.setVisibility(View.VISIBLE); 1090 labelSpace = " "; 1091 itemHolder.daysOfWeek.setOnClickListener(new View.OnClickListener() { 1092 @Override 1093 public void onClick(View view) { 1094 expandAlarm(itemHolder, true); 1095 itemHolder.alarmItem.post(mScrollRunnable); 1096 } 1097 }); 1098 1099 } else { 1100 itemHolder.daysOfWeek.setVisibility(View.GONE); 1101 } 1102 1103 if (alarm.label != null && alarm.label.length() != 0) { 1104 itemHolder.label.setText(alarm.label + labelSpace); 1105 itemHolder.label.setVisibility(View.VISIBLE); 1106 itemHolder.label.setContentDescription( 1107 mContext.getResources().getString(R.string.label_description) + " " 1108 + alarm.label); 1109 itemHolder.label.setOnClickListener(new View.OnClickListener() { 1110 @Override 1111 public void onClick(View view) { 1112 expandAlarm(itemHolder, true); 1113 itemHolder.alarmItem.post(mScrollRunnable); 1114 } 1115 }); 1116 } else { 1117 itemHolder.label.setVisibility(View.GONE); 1118 } 1119 1120 itemHolder.delete.setOnClickListener(new View.OnClickListener() { 1121 @Override 1122 public void onClick(View v) { 1123 mDeletedAlarm = alarm; 1124 1125 view.animate().setDuration(ANIMATION_DURATION).alpha(0).translationY(-1) 1126 .withEndAction(new Runnable() { 1127 1128 @Override 1129 public void run() { 1130 asyncDeleteAlarm(mDeletedAlarm, view); 1131 } 1132 }); 1133 } 1134 }); 1135 1136 if (expanded) { 1137 expandAlarm(itemHolder, false); 1138 } else { 1139 collapseAlarm(itemHolder, false); 1140 } 1141 1142 itemHolder.alarmItem.setOnClickListener(new View.OnClickListener() { 1143 @Override 1144 public void onClick(View view) { 1145 if (isAlarmExpanded(alarm)) { 1146 collapseAlarm(itemHolder, true); 1147 } else { 1148 expandAlarm(itemHolder, true); 1149 } 1150 } 1151 }); 1152 } 1153 1154 private void bindExpandArea(final ItemHolder itemHolder, final Alarm alarm) { 1155 // Views in here are not bound until the item is expanded. 1156 1157 if (alarm.label != null && alarm.label.length() > 0) { 1158 itemHolder.clickableLabel.setText(alarm.label); 1159 itemHolder.clickableLabel.setTextColor(mColorLit); 1160 } else { 1161 itemHolder.clickableLabel.setText(R.string.label); 1162 itemHolder.clickableLabel.setTextColor(mColorDim); 1163 } 1164 itemHolder.clickableLabel.setOnClickListener(new View.OnClickListener() { 1165 @Override 1166 public void onClick(View view) { 1167 showLabelDialog(alarm); 1168 } 1169 }); 1170 1171 if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeating()) { 1172 itemHolder.repeat.setChecked(true); 1173 itemHolder.repeatDays.setVisibility(View.VISIBLE); 1174 } else { 1175 itemHolder.repeat.setChecked(false); 1176 itemHolder.repeatDays.setVisibility(View.GONE); 1177 } 1178 itemHolder.repeat.setOnClickListener(new View.OnClickListener() { 1179 @Override 1180 public void onClick(View view) { 1181 final boolean checked = ((CheckBox) view).isChecked(); 1182 if (checked) { 1183 // Show days 1184 itemHolder.repeatDays.setVisibility(View.VISIBLE); 1185 mRepeatChecked.add(alarm.id); 1186 1187 // Set all previously set days 1188 // or 1189 // Set all days if no previous. 1190 final int bitSet = mPreviousDaysOfWeekMap.getInt("" + alarm.id); 1191 alarm.daysOfWeek.setBitSet(bitSet); 1192 if (!alarm.daysOfWeek.isRepeating()) { 1193 alarm.daysOfWeek.setDaysOfWeek(true, DAY_ORDER); 1194 } 1195 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 1196 } else { 1197 itemHolder.repeatDays.setVisibility(View.GONE); 1198 mRepeatChecked.remove(alarm.id); 1199 1200 // Remember the set days in case the user wants it back. 1201 final int bitSet = alarm.daysOfWeek.getBitSet(); 1202 mPreviousDaysOfWeekMap.putInt("" + alarm.id, bitSet); 1203 1204 // Remove all repeat days 1205 alarm.daysOfWeek.clearAllDays(); 1206 } 1207 asyncUpdateAlarm(alarm, false); 1208 } 1209 }); 1210 1211 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 1212 for (int i = 0; i < 7; i++) { 1213 final int buttonIndex = i; 1214 1215 itemHolder.dayButtonParents[i].setOnClickListener(new View.OnClickListener() { 1216 @Override 1217 public void onClick(View view) { 1218 itemHolder.dayButtons[buttonIndex].toggle(); 1219 final boolean checked = itemHolder.dayButtons[buttonIndex].isChecked(); 1220 int day = DAY_ORDER[buttonIndex]; 1221 alarm.daysOfWeek.setDaysOfWeek(checked, day); 1222 if (checked) { 1223 turnOnDayOfWeek(itemHolder, buttonIndex); 1224 } else { 1225 turnOffDayOfWeek(itemHolder, buttonIndex); 1226 1227 // See if this was the last day, if so, un-check the repeat box. 1228 if (!alarm.daysOfWeek.isRepeating()) { 1229 itemHolder.repeatDays.setVisibility(View.GONE); 1230 itemHolder.repeat.setTextColor(mColorDim); 1231 mRepeatChecked.remove(alarm.id); 1232 1233 // Set history to no days, so it will be everyday when repeat is 1234 // turned back on 1235 mPreviousDaysOfWeekMap.putInt("" + alarm.id, 1236 DaysOfWeek.NO_DAYS_SET); 1237 } 1238 } 1239 asyncUpdateAlarm(alarm, false); 1240 } 1241 }); 1242 } 1243 1244 1245 if (!mHasVibrator) { 1246 itemHolder.vibrate.setVisibility(View.INVISIBLE); 1247 } else { 1248 itemHolder.vibrate.setVisibility(View.VISIBLE); 1249 if (!alarm.vibrate) { 1250 itemHolder.vibrate.setChecked(false); 1251 itemHolder.vibrate.setTextColor(mColorDim); 1252 } else { 1253 itemHolder.vibrate.setChecked(true); 1254 itemHolder.vibrate.setTextColor(mColorLit); 1255 } 1256 } 1257 1258 itemHolder.vibrate.setOnClickListener(new View.OnClickListener() { 1259 @Override 1260 public void onClick(View v) { 1261 final boolean checked = ((CheckBox) v).isChecked(); 1262 if (checked) { 1263 itemHolder.vibrate.setTextColor(mColorLit); 1264 } else { 1265 itemHolder.vibrate.setTextColor(mColorDim); 1266 } 1267 alarm.vibrate = checked; 1268 asyncUpdateAlarm(alarm, false); 1269 } 1270 }); 1271 1272 final String ringtone; 1273 if (Alarm.NO_RINGTONE_URI.equals(alarm.alert)) { 1274 ringtone = mContext.getResources().getString(R.string.silent_alarm_summary); 1275 } else { 1276 ringtone = getRingToneTitle(alarm.alert); 1277 } 1278 itemHolder.ringtone.setText(ringtone); 1279 itemHolder.ringtone.setContentDescription( 1280 mContext.getResources().getString(R.string.ringtone_description) + " " 1281 + ringtone); 1282 itemHolder.ringtone.setOnClickListener(new View.OnClickListener() { 1283 @Override 1284 public void onClick(View view) { 1285 launchRingTonePicker(alarm); 1286 } 1287 }); 1288 } 1289 1290 // Sets the alpha of the item except the on/off switch. This gives a visual effect 1291 // for enabled/disabled alarm while leaving the on/off switch more visible 1292 private void setItemAlpha(ItemHolder holder, boolean enabled) { 1293 float alpha = enabled ? 1f : 0.5f; 1294 holder.clock.setAlpha(alpha); 1295 holder.summary.setAlpha(alpha); 1296 holder.expandArea.setAlpha(alpha); 1297 holder.delete.setAlpha(alpha); 1298 holder.daysOfWeek.setAlpha(alpha); 1299 } 1300 1301 private void updateDaysOfWeekButtons(ItemHolder holder, DaysOfWeek daysOfWeek) { 1302 HashSet<Integer> setDays = daysOfWeek.getSetDays(); 1303 for (int i = 0; i < 7; i++) { 1304 if (setDays.contains(DAY_ORDER[i])) { 1305 turnOnDayOfWeek(holder, i); 1306 } else { 1307 turnOffDayOfWeek(holder, i); 1308 } 1309 } 1310 } 1311 1312 public void toggleSelectState(View v) { 1313 // long press could be on the parent view or one of its childs, so find the parent view 1314 v = getTopParent(v); 1315 if (v != null) { 1316 long id = ((ItemHolder)v.getTag()).alarm.id; 1317 if (mSelectedAlarms.contains(id)) { 1318 mSelectedAlarms.remove(id); 1319 } else { 1320 mSelectedAlarms.add(id); 1321 } 1322 } 1323 } 1324 1325 private View getTopParent(View v) { 1326 while (v != null && v.getId() != R.id.alarm_item) { 1327 v = (View) v.getParent(); 1328 } 1329 return v; 1330 } 1331 1332 public int getSelectedItemsNum() { 1333 return mSelectedAlarms.size(); 1334 } 1335 1336 private void turnOffDayOfWeek(ItemHolder holder, int dayIndex) { 1337 holder.dayButtons[dayIndex].setChecked(false); 1338 holder.dayButtons[dayIndex].setTextColor(mColorDim); 1339 holder.dayButtons[dayIndex].setTypeface(mRobotoNormal); 1340 } 1341 1342 private void turnOnDayOfWeek(ItemHolder holder, int dayIndex) { 1343 holder.dayButtons[dayIndex].setChecked(true); 1344 holder.dayButtons[dayIndex].setTextColor(mColorLit); 1345 holder.dayButtons[dayIndex].setTypeface(mRobotoBold); 1346 } 1347 1348 1349 /** 1350 * Does a read-through cache for ringtone titles. 1351 * 1352 * @param uri The uri of the ringtone. 1353 * @return The ringtone title. {@literal null} if no matching ringtone found. 1354 */ 1355 private String getRingToneTitle(Uri uri) { 1356 // Try the cache first 1357 String title = mRingtoneTitleCache.getString(uri.toString()); 1358 if (title == null) { 1359 // This is slow because a media player is created during Ringtone object creation. 1360 Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri); 1361 title = ringTone.getTitle(mContext); 1362 if (title != null) { 1363 mRingtoneTitleCache.putString(uri.toString(), title); 1364 } 1365 } 1366 return title; 1367 } 1368 1369 public void setNewAlarm(long alarmId) { 1370 mExpanded.add(alarmId); 1371 } 1372 1373 /** 1374 * Expands the alarm for editing. 1375 * 1376 * @param itemHolder The item holder instance. 1377 */ 1378 private void expandAlarm(final ItemHolder itemHolder, boolean animate) { 1379 mExpanded.add(itemHolder.alarm.id); 1380 bindExpandArea(itemHolder, itemHolder.alarm); 1381 // Scroll the view to make sure it is fully viewed 1382 mScrollAlarmId = itemHolder.alarm.id; 1383 1384 // Save the starting height so we can animate from this value. 1385 final int startingHeight = itemHolder.alarmItem.getHeight(); 1386 1387 // Set the expand area to visible so we can measure the height to animate to. 1388 itemHolder.alarmItem.setBackgroundColor(mBackgroundColorExpanded); 1389 itemHolder.expandArea.setVisibility(View.VISIBLE); 1390 1391 if (!animate) { 1392 // Set the "end" layout and don't do the animation. 1393 itemHolder.arrow.setRotation(180); 1394 // We need to translate the hairline up, so the height of the collapseArea 1395 // needs to be measured to know how high to translate it. 1396 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1397 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1398 @Override 1399 public boolean onPreDraw() { 1400 // We don't want to continue getting called for every listview drawing. 1401 if (observer.isAlive()) { 1402 observer.removeOnPreDrawListener(this); 1403 } 1404 int hairlineHeight = itemHolder.hairLine.getHeight(); 1405 int collapseHeight = 1406 itemHolder.collapseExpandArea.getHeight() - hairlineHeight; 1407 itemHolder.hairLine.setTranslationY(-collapseHeight); 1408 return true; 1409 } 1410 }); 1411 return; 1412 } 1413 1414 // Add an onPreDrawListener, which gets called after measurement but before the draw. 1415 // This way we can check the height we need to animate to before any drawing. 1416 // Note the series of events: 1417 // * expandArea is set to VISIBLE, which causes a layout pass 1418 // * the view is measured, and our onPreDrawListener is called 1419 // * we set up the animation using the start and end values. 1420 // * the height is set back to the starting point so it can be animated down. 1421 // * request another layout pass. 1422 // * return false so that onDraw() is not called for the single frame before 1423 // the animations have started. 1424 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1425 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1426 @Override 1427 public boolean onPreDraw() { 1428 // We don't want to continue getting called for every listview drawing. 1429 if (observer.isAlive()) { 1430 observer.removeOnPreDrawListener(this); 1431 } 1432 // Calculate some values to help with the animation. 1433 final int endingHeight = itemHolder.alarmItem.getHeight(); 1434 final int distance = endingHeight - startingHeight; 1435 final int collapseHeight = itemHolder.collapseExpandArea.getHeight(); 1436 int hairlineHeight = itemHolder.hairLine.getHeight(); 1437 final int hairlineDistance = collapseHeight - hairlineHeight; 1438 1439 // Set the height back to the start state of the animation. 1440 itemHolder.alarmItem.getLayoutParams().height = startingHeight; 1441 // To allow the expandArea to glide in with the expansion animation, set a 1442 // negative top margin, which will animate down to a margin of 0 as the height 1443 // is increased. 1444 // Note that we need to maintain the bottom margin as a fixed value (instead of 1445 // just using a listview, to allow for a flatter hierarchy) to fit the bottom 1446 // bar underneath. 1447 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1448 itemHolder.expandArea.getLayoutParams(); 1449 expandParams.setMargins(0, -distance, 0, collapseHeight); 1450 itemHolder.alarmItem.requestLayout(); 1451 1452 // Set up the animator to animate the expansion. 1453 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 1454 .setDuration(EXPAND_DURATION); 1455 animator.setInterpolator(mExpandInterpolator); 1456 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1457 @Override 1458 public void onAnimationUpdate(ValueAnimator animator) { 1459 Float value = (Float) animator.getAnimatedValue(); 1460 1461 // For each value from 0 to 1, animate the various parts of the layout. 1462 itemHolder.alarmItem.getLayoutParams().height = 1463 (int) (value * distance + startingHeight); 1464 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1465 itemHolder.expandArea.getLayoutParams(); 1466 expandParams.setMargins( 1467 0, (int) -((1 - value) * distance), 0, collapseHeight); 1468 itemHolder.arrow.setRotation(180 * value); 1469 itemHolder.hairLine.setTranslationY(-hairlineDistance * value); 1470 itemHolder.summary.setAlpha(1 - value); 1471 1472 itemHolder.alarmItem.requestLayout(); 1473 } 1474 }); 1475 // Set everything to their final values when the animation's done. 1476 animator.addListener(new AnimatorListener() { 1477 @Override 1478 public void onAnimationEnd(Animator animation) { 1479 // Set it back to wrap content since we'd explicitly set the height. 1480 itemHolder.alarmItem.getLayoutParams().height = 1481 LayoutParams.WRAP_CONTENT; 1482 itemHolder.arrow.setRotation(180); 1483 itemHolder.hairLine.setTranslationY(-hairlineDistance); 1484 itemHolder.summary.setVisibility(View.GONE); 1485 } 1486 1487 @Override 1488 public void onAnimationCancel(Animator animation) { 1489 // TODO we may have to deal with cancelations of the animation. 1490 } 1491 1492 @Override 1493 public void onAnimationRepeat(Animator animation) { } 1494 @Override 1495 public void onAnimationStart(Animator animation) { } 1496 }); 1497 animator.start(); 1498 1499 // Return false so this draw does not occur to prevent the final frame from 1500 // being drawn for the single frame before the animations start. 1501 return false; 1502 } 1503 }); 1504 } 1505 1506 private boolean isAlarmExpanded(Alarm alarm) { 1507 return mExpanded.contains(alarm.id); 1508 } 1509 1510 private void collapseAlarm(final ItemHolder itemHolder, boolean animate) { 1511 mExpanded.remove(itemHolder.alarm.id); 1512 1513 // Save the starting height so we can animate from this value. 1514 final int startingHeight = itemHolder.alarmItem.getHeight(); 1515 1516 // Set the expand area to gone so we can measure the height to animate to. 1517 itemHolder.alarmItem.setBackgroundResource(mBackgroundColor); 1518 itemHolder.expandArea.setVisibility(View.GONE); 1519 1520 if (!animate) { 1521 // Set the "end" layout and don't do the animation. 1522 itemHolder.arrow.setRotation(0); 1523 itemHolder.hairLine.setTranslationY(0); 1524 return; 1525 } 1526 1527 // Add an onPreDrawListener, which gets called after measurement but before the draw. 1528 // This way we can check the height we need to animate to before any drawing. 1529 // Note the series of events: 1530 // * expandArea is set to GONE, which causes a layout pass 1531 // * the view is measured, and our onPreDrawListener is called 1532 // * we set up the animation using the start and end values. 1533 // * expandArea is set to VISIBLE again so it can be shown animating. 1534 // * request another layout pass. 1535 // * return false so that onDraw() is not called for the single frame before 1536 // the animations have started. 1537 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1538 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1539 @Override 1540 public boolean onPreDraw() { 1541 if (observer.isAlive()) { 1542 observer.removeOnPreDrawListener(this); 1543 } 1544 1545 // Calculate some values to help with the animation. 1546 final int endingHeight = itemHolder.alarmItem.getHeight(); 1547 final int distance = endingHeight - startingHeight; 1548 int hairlineHeight = itemHolder.hairLine.getHeight(); 1549 final int hairlineDistance = mCollapseExpandHeight - hairlineHeight; 1550 1551 // Re-set the visibilities for the start state of the animation. 1552 itemHolder.expandArea.setVisibility(View.VISIBLE); 1553 itemHolder.summary.setVisibility(View.VISIBLE); 1554 itemHolder.summary.setAlpha(1); 1555 1556 // Set up the animator to animate the expansion. 1557 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 1558 .setDuration(COLLAPSE_DURATION); 1559 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1560 @Override 1561 public void onAnimationUpdate(ValueAnimator animator) { 1562 Float value = (Float) animator.getAnimatedValue(); 1563 1564 // For each value from 0 to 1, animate the various parts of the layout. 1565 itemHolder.alarmItem.getLayoutParams().height = 1566 (int) (value * distance + startingHeight); 1567 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1568 itemHolder.expandArea.getLayoutParams(); 1569 expandParams.setMargins( 1570 0, (int) (value * distance), 0, mCollapseExpandHeight); 1571 itemHolder.arrow.setRotation(180 * (1 - value)); 1572 itemHolder.hairLine.setTranslationY(-hairlineDistance * (1 - value)); 1573 itemHolder.summary.setAlpha(value); 1574 1575 itemHolder.alarmItem.requestLayout(); 1576 } 1577 }); 1578 animator.setInterpolator(mCollapseInterpolator); 1579 // Set everything to their final values when the animation's done. 1580 animator.addListener(new AnimatorListener() { 1581 @Override 1582 public void onAnimationEnd(Animator animation) { 1583 // Set it back to wrap content since we'd explicitly set the height. 1584 itemHolder.alarmItem.getLayoutParams().height = 1585 LayoutParams.WRAP_CONTENT; 1586 1587 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1588 itemHolder.expandArea.getLayoutParams(); 1589 expandParams.setMargins(0, 0, 0, mCollapseExpandHeight); 1590 1591 itemHolder.expandArea.setVisibility(View.GONE); 1592 itemHolder.arrow.setRotation(0); 1593 itemHolder.hairLine.setTranslationY(0); 1594 } 1595 1596 @Override 1597 public void onAnimationCancel(Animator animation) { 1598 // TODO we may have to deal with cancelations of the animation. 1599 } 1600 1601 @Override 1602 public void onAnimationRepeat(Animator animation) { } 1603 @Override 1604 public void onAnimationStart(Animator animation) { } 1605 }); 1606 animator.start(); 1607 1608 return false; 1609 } 1610 }); 1611 } 1612 1613 @Override 1614 public int getViewTypeCount() { 1615 return 1; 1616 } 1617 1618 private View getViewById(long id) { 1619 for (int i = 0; i < mList.getCount(); i++) { 1620 View v = mList.getChildAt(i); 1621 if (v != null) { 1622 ItemHolder h = (ItemHolder)(v.getTag()); 1623 if (h != null && h.alarm.id == id) { 1624 return v; 1625 } 1626 } 1627 } 1628 return null; 1629 } 1630 1631 public long[] getExpandedArray() { 1632 int index = 0; 1633 long[] ids = new long[mExpanded.size()]; 1634 for (long id : mExpanded) { 1635 ids[index] = id; 1636 index++; 1637 } 1638 return ids; 1639 } 1640 1641 public long[] getSelectedAlarmsArray() { 1642 int index = 0; 1643 long[] ids = new long[mSelectedAlarms.size()]; 1644 for (long id : mSelectedAlarms) { 1645 ids[index] = id; 1646 index++; 1647 } 1648 return ids; 1649 } 1650 1651 public long[] getRepeatArray() { 1652 int index = 0; 1653 long[] ids = new long[mRepeatChecked.size()]; 1654 for (long id : mRepeatChecked) { 1655 ids[index] = id; 1656 index++; 1657 } 1658 return ids; 1659 } 1660 1661 public Bundle getPreviousDaysOfWeekMap() { 1662 return mPreviousDaysOfWeekMap; 1663 } 1664 1665 private void buildHashSetFromArray(long[] ids, HashSet<Long> set) { 1666 for (long id : ids) { 1667 set.add(id); 1668 } 1669 } 1670 } 1671 1672 private void startCreatingAlarm() { 1673 // Set the "selected" alarm as null, and we'll create the new one when the timepicker 1674 // comes back. 1675 mSelectedAlarm = null; 1676 AlarmUtils.showTimeEditDialog(getChildFragmentManager(), 1677 null, AlarmClockFragment.this, DateFormat.is24HourFormat(getActivity())); 1678 } 1679 1680 private static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) { 1681 ContentResolver cr = context.getContentResolver(); 1682 AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance()); 1683 newInstance = AlarmInstance.addInstance(cr, newInstance); 1684 // Register instance to state manager 1685 AlarmStateManager.registerInstance(context, newInstance, true); 1686 return newInstance; 1687 } 1688 1689 private void asyncDeleteAlarm(final Alarm alarm, final View viewToRemove) { 1690 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1691 final AsyncTask<Void, Void, Void> deleteTask = new AsyncTask<Void, Void, Void>() { 1692 @Override 1693 public synchronized void onPreExecute() { 1694 if (viewToRemove == null) { 1695 return; 1696 } 1697 // The alarm list needs to be disabled until the animation finishes to prevent 1698 // possible concurrency issues. It becomes re-enabled after the animations have 1699 // completed. 1700 mAlarmsList.setEnabled(false); 1701 1702 // Store all of the current list view item positions in memory for animation. 1703 final ListView list = mAlarmsList; 1704 int firstVisiblePosition = list.getFirstVisiblePosition(); 1705 for (int i=0; i<list.getChildCount(); i++) { 1706 View child = list.getChildAt(i); 1707 if (child != viewToRemove) { 1708 int position = firstVisiblePosition + i; 1709 long itemId = mAdapter.getItemId(position); 1710 mItemIdTopMap.put(itemId, child.getTop()); 1711 } 1712 } 1713 } 1714 1715 @Override 1716 protected Void doInBackground(Void... parameters) { 1717 // Activity may be closed at this point , make sure data is still valid 1718 if (context != null && alarm != null) { 1719 ContentResolver cr = context.getContentResolver(); 1720 AlarmStateManager.deleteAllInstances(context, alarm.id); 1721 Alarm.deleteAlarm(cr, alarm.id); 1722 } 1723 return null; 1724 } 1725 }; 1726 mUndoShowing = true; 1727 deleteTask.execute(); 1728 } 1729 1730 private void asyncAddAlarm(final Alarm alarm) { 1731 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1732 final AsyncTask<Void, Void, AlarmInstance> updateTask = 1733 new AsyncTask<Void, Void, AlarmInstance>() { 1734 @Override 1735 public synchronized void onPreExecute() { 1736 final ListView list = mAlarmsList; 1737 // The alarm list needs to be disabled until the animation finishes to prevent 1738 // possible concurrency issues. It becomes re-enabled after the animations have 1739 // completed. 1740 mAlarmsList.setEnabled(false); 1741 1742 // Store all of the current list view item positions in memory for animation. 1743 int firstVisiblePosition = list.getFirstVisiblePosition(); 1744 for (int i=0; i<list.getChildCount(); i++) { 1745 View child = list.getChildAt(i); 1746 int position = firstVisiblePosition + i; 1747 long itemId = mAdapter.getItemId(position); 1748 mItemIdTopMap.put(itemId, child.getTop()); 1749 } 1750 } 1751 1752 @Override 1753 protected AlarmInstance doInBackground(Void... parameters) { 1754 if (context != null && alarm != null) { 1755 ContentResolver cr = context.getContentResolver(); 1756 1757 // Add alarm to db 1758 Alarm newAlarm = Alarm.addAlarm(cr, alarm); 1759 mScrollToAlarmId = newAlarm.id; 1760 1761 // Create and add instance to db 1762 if (newAlarm.enabled) { 1763 return setupAlarmInstance(context, newAlarm); 1764 } 1765 } 1766 return null; 1767 } 1768 1769 @Override 1770 protected void onPostExecute(AlarmInstance instance) { 1771 if (instance != null) { 1772 AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis()); 1773 } 1774 } 1775 }; 1776 updateTask.execute(); 1777 } 1778 1779 private void asyncUpdateAlarm(final Alarm alarm, final boolean popToast) { 1780 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1781 final AsyncTask<Void, Void, AlarmInstance> updateTask = 1782 new AsyncTask<Void, Void, AlarmInstance>() { 1783 @Override 1784 protected AlarmInstance doInBackground(Void ... parameters) { 1785 ContentResolver cr = context.getContentResolver(); 1786 1787 // Dismiss all old instances 1788 AlarmStateManager.deleteAllInstances(context, alarm.id); 1789 1790 // Update alarm 1791 Alarm.updateAlarm(cr, alarm); 1792 if (alarm.enabled) { 1793 return setupAlarmInstance(context, alarm); 1794 } 1795 1796 return null; 1797 } 1798 1799 @Override 1800 protected void onPostExecute(AlarmInstance instance) { 1801 if (popToast && instance != null) { 1802 AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis()); 1803 } 1804 } 1805 }; 1806 updateTask.execute(); 1807 } 1808 1809 @Override 1810 public boolean onTouch(View v, MotionEvent event) { 1811 hideUndoBar(true, event); 1812 return false; 1813 } 1814} 1815