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.AnimatorListenerAdapter; 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.app.TimePickerDialog.OnTimeSetListener; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.Loader; 32 import android.content.res.Configuration; 33 import android.content.res.Resources; 34 import android.database.Cursor; 35 import android.database.DataSetObserver; 36 import android.graphics.Color; 37 import android.graphics.Rect; 38 import android.graphics.Typeface; 39 import android.media.Ringtone; 40 import android.media.RingtoneManager; 41 import android.net.Uri; 42 import android.os.AsyncTask; 43 import android.os.Bundle; 44 import android.os.Vibrator; 45 import android.transition.AutoTransition; 46 import android.transition.Fade; 47 import android.transition.Transition; 48 import android.transition.TransitionManager; 49 import android.transition.TransitionSet; 50 import android.view.LayoutInflater; 51 import android.view.MotionEvent; 52 import android.view.View; 53 import android.view.View.OnClickListener; 54 import android.view.ViewGroup; 55 import android.view.ViewGroup.LayoutParams; 56 import android.view.ViewTreeObserver; 57 import android.view.animation.AccelerateDecelerateInterpolator; 58 import android.view.animation.DecelerateInterpolator; 59 import android.view.animation.Interpolator; 60 import android.widget.Button; 61 import android.widget.CheckBox; 62 import android.widget.CompoundButton; 63 import android.widget.CursorAdapter; 64 import android.widget.FrameLayout; 65 import android.widget.ImageButton; 66 import android.widget.LinearLayout; 67 import android.widget.ListView; 68 import android.widget.Switch; 69 import android.widget.TextView; 70 import android.widget.TimePicker; 71 import android.widget.Toast; 72 73 import com.android.deskclock.alarms.AlarmStateManager; 74 import com.android.deskclock.provider.Alarm; 75 import com.android.deskclock.provider.AlarmInstance; 76 import com.android.deskclock.provider.DaysOfWeek; 77 import com.android.deskclock.widget.ActionableToastBar; 78 import com.android.deskclock.widget.TextTime; 79 80 import java.text.DateFormatSymbols; 81 import java.util.Calendar; 82 import java.util.HashSet; 83 84 /** 85 * AlarmClock application. 86 */ 87 public class AlarmClockFragment extends DeskClockFragment implements 88 LoaderManager.LoaderCallbacks<Cursor>, OnTimeSetListener, View.OnTouchListener { 89 private static final float EXPAND_DECELERATION = 1f; 90 private static final float COLLAPSE_DECELERATION = 0.7f; 91 92 private static final int ANIMATION_DURATION = 300; 93 private static final int EXPAND_DURATION = 300; 94 private static final int COLLAPSE_DURATION = 250; 95 96 private static final int ROTATE_180_DEGREE = 180; 97 private static final float ALARM_ELEVATION = 8f; 98 private static final float TINTED_LEVEL = 0.09f; 99 100 private static final String KEY_EXPANDED_ID = "expandedId"; 101 private static final String KEY_REPEAT_CHECKED_IDS = "repeatCheckedIds"; 102 private static final String KEY_RINGTONE_TITLE_CACHE = "ringtoneTitleCache"; 103 private static final String KEY_SELECTED_ALARMS = "selectedAlarms"; 104 private static final String KEY_DELETED_ALARM = "deletedAlarm"; 105 private static final String KEY_UNDO_SHOWING = "undoShowing"; 106 private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap"; 107 private static final String KEY_SELECTED_ALARM = "selectedAlarm"; 108 private static final DeskClockExtensions sDeskClockExtensions = ExtensionsFactory 109 .getDeskClockExtensions(); 110 111 private static final int REQUEST_CODE_RINGTONE = 1; 112 private static final long INVALID_ID = -1; 113 114 // This extra is used when receiving an intent to create an alarm, but no alarm details 115 // have been passed in, so the alarm page should start the process of creating a new alarm. 116 public static final String ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new"; 117 118 // This extra is used when receiving an intent to scroll to specific alarm. If alarm 119 // can not be found, and toast message will pop up that the alarm has be deleted. 120 public static final String SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm"; 121 122 private FrameLayout mMainLayout; 123 private ListView mAlarmsList; 124 private AlarmItemAdapter mAdapter; 125 private View mEmptyView; 126 private View mFooterView; 127 128 private Bundle mRingtoneTitleCache; // Key: ringtone uri, value: ringtone title 129 private ActionableToastBar mUndoBar; 130 private View mUndoFrame; 131 132 private Alarm mSelectedAlarm; 133 private long mScrollToAlarmId = INVALID_ID; 134 135 private Loader mCursorLoader = null; 136 137 // Saved states for undo 138 private Alarm mDeletedAlarm; 139 private Alarm mAddedAlarm; 140 private boolean mUndoShowing; 141 142 private Interpolator mExpandInterpolator; 143 private Interpolator mCollapseInterpolator; 144 145 private Transition mAddRemoveTransition; 146 private Transition mRepeatTransition; 147 private Transition mEmptyViewTransition; 148 149 public AlarmClockFragment() { 150 // Basic provider required by Fragment.java 151 } 152 153 @Override 154 public void onCreate(Bundle savedState) { 155 super.onCreate(savedState); 156 mCursorLoader = getLoaderManager().initLoader(0, null, this); 157 } 158 159 @Override 160 public View onCreateView(LayoutInflater inflater, ViewGroup container, 161 Bundle savedState) { 162 // Inflate the layout for this fragment 163 final View v = inflater.inflate(R.layout.alarm_clock, container, false); 164 165 long expandedId = INVALID_ID; 166 long[] repeatCheckedIds = null; 167 long[] selectedAlarms = null; 168 Bundle previousDayMap = null; 169 if (savedState != null) { 170 expandedId = savedState.getLong(KEY_EXPANDED_ID); 171 repeatCheckedIds = savedState.getLongArray(KEY_REPEAT_CHECKED_IDS); 172 mRingtoneTitleCache = savedState.getBundle(KEY_RINGTONE_TITLE_CACHE); 173 mDeletedAlarm = savedState.getParcelable(KEY_DELETED_ALARM); 174 mUndoShowing = savedState.getBoolean(KEY_UNDO_SHOWING); 175 selectedAlarms = savedState.getLongArray(KEY_SELECTED_ALARMS); 176 previousDayMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP); 177 mSelectedAlarm = savedState.getParcelable(KEY_SELECTED_ALARM); 178 } 179 180 mExpandInterpolator = new DecelerateInterpolator(EXPAND_DECELERATION); 181 mCollapseInterpolator = new DecelerateInterpolator(COLLAPSE_DECELERATION); 182 183 mAddRemoveTransition = new AutoTransition(); 184 mAddRemoveTransition.setDuration(ANIMATION_DURATION); 185 186 mRepeatTransition = new AutoTransition(); 187 mRepeatTransition.setDuration(ANIMATION_DURATION / 2); 188 mRepeatTransition.setInterpolator(new AccelerateDecelerateInterpolator()); 189 190 mEmptyViewTransition = new TransitionSet() 191 .setOrdering(TransitionSet.ORDERING_SEQUENTIAL) 192 .addTransition(new Fade(Fade.OUT)) 193 .addTransition(new Fade(Fade.IN)) 194 .setDuration(ANIMATION_DURATION); 195 196 boolean isLandscape = getResources().getConfiguration().orientation 197 == Configuration.ORIENTATION_LANDSCAPE; 198 View menuButton = v.findViewById(R.id.menu_button); 199 if (menuButton != null) { 200 if (isLandscape) { 201 menuButton.setVisibility(View.GONE); 202 } else { 203 menuButton.setVisibility(View.VISIBLE); 204 setupFakeOverflowMenuButton(menuButton); 205 } 206 } 207 208 mEmptyView = v.findViewById(R.id.alarms_empty_view); 209 210 mMainLayout = (FrameLayout) v.findViewById(R.id.main); 211 mAlarmsList = (ListView) v.findViewById(R.id.alarms_list); 212 213 mUndoBar = (ActionableToastBar) v.findViewById(R.id.undo_bar); 214 mUndoFrame = v.findViewById(R.id.undo_frame); 215 mUndoFrame.setOnTouchListener(this); 216 217 mFooterView = v.findViewById(R.id.alarms_footer_view); 218 mFooterView.setOnTouchListener(this); 219 220 mAdapter = new AlarmItemAdapter(getActivity(), 221 expandedId, repeatCheckedIds, selectedAlarms, previousDayMap, mAlarmsList); 222 mAdapter.registerDataSetObserver(new DataSetObserver() { 223 224 private int prevAdapterCount = -1; 225 226 @Override 227 public void onChanged() { 228 229 final int count = mAdapter.getCount(); 230 if (mDeletedAlarm != null && prevAdapterCount > count) { 231 showUndoBar(); 232 } 233 234 if ((count == 0 && prevAdapterCount > 0) || /* should fade in */ 235 (count > 0 && prevAdapterCount == 0) /* should fade out */) { 236 TransitionManager.beginDelayedTransition(mMainLayout, mEmptyViewTransition); 237 } 238 mEmptyView.setVisibility(count == 0 ? View.VISIBLE : View.GONE); 239 240 // Cache this adapter's count for when the adapter changes. 241 prevAdapterCount = count; 242 super.onChanged(); 243 } 244 }); 245 246 if (mRingtoneTitleCache == null) { 247 mRingtoneTitleCache = new Bundle(); 248 } 249 250 mAlarmsList.setAdapter(mAdapter); 251 mAlarmsList.setVerticalScrollBarEnabled(true); 252 mAlarmsList.setOnCreateContextMenuListener(this); 253 254 if (mUndoShowing) { 255 showUndoBar(); 256 } 257 return v; 258 } 259 260 private void setUndoBarRightMargin(int margin) { 261 FrameLayout.LayoutParams params = 262 (FrameLayout.LayoutParams) mUndoBar.getLayoutParams(); 263 ((FrameLayout.LayoutParams) mUndoBar.getLayoutParams()) 264 .setMargins(params.leftMargin, params.topMargin, margin, params.bottomMargin); 265 mUndoBar.requestLayout(); 266 } 267 268 @Override 269 public void onResume() { 270 super.onResume(); 271 272 final DeskClock activity = (DeskClock) getActivity(); 273 if (activity.getSelectedTab() == DeskClock.ALARM_TAB_INDEX) { 274 setFabAppearance(); 275 setLeftRightButtonAppearance(); 276 } 277 278 if (mAdapter != null) { 279 mAdapter.notifyDataSetChanged(); 280 } 281 // Check if another app asked us to create a blank new alarm. 282 final Intent intent = getActivity().getIntent(); 283 if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) { 284 if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) { 285 // An external app asked us to create a blank alarm. 286 startCreatingAlarm(); 287 } 288 289 // Remove the CREATE_NEW extra now that we've processed it. 290 intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA); 291 } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) { 292 long alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID); 293 if (alarmId != Alarm.INVALID_ID) { 294 mScrollToAlarmId = alarmId; 295 if (mCursorLoader != null && mCursorLoader.isStarted()) { 296 // We need to force a reload here to make sure we have the latest view 297 // of the data to scroll to. 298 mCursorLoader.forceLoad(); 299 } 300 } 301 302 // Remove the SCROLL_TO_ALARM extra now that we've processed it. 303 intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA); 304 } 305 } 306 307 private void hideUndoBar(boolean animate, MotionEvent event) { 308 if (mUndoBar != null) { 309 mUndoFrame.setVisibility(View.GONE); 310 if (event != null && mUndoBar.isEventInToastBar(event)) { 311 // Avoid touches inside the undo bar. 312 return; 313 } 314 mUndoBar.hide(animate); 315 } 316 mDeletedAlarm = null; 317 mUndoShowing = false; 318 } 319 320 private void showUndoBar() { 321 final Alarm deletedAlarm = mDeletedAlarm; 322 mUndoFrame.setVisibility(View.VISIBLE); 323 mUndoBar.show(new ActionableToastBar.ActionClickedListener() { 324 @Override 325 public void onActionClicked() { 326 mAddedAlarm = deletedAlarm; 327 mDeletedAlarm = null; 328 mUndoShowing = false; 329 330 asyncAddAlarm(deletedAlarm); 331 } 332 }, 0, getResources().getString(R.string.alarm_deleted), true, R.string.alarm_undo, true); 333 } 334 335 @Override 336 public void onSaveInstanceState(Bundle outState) { 337 super.onSaveInstanceState(outState); 338 outState.putLong(KEY_EXPANDED_ID, mAdapter.getExpandedId()); 339 outState.putLongArray(KEY_REPEAT_CHECKED_IDS, mAdapter.getRepeatArray()); 340 outState.putLongArray(KEY_SELECTED_ALARMS, mAdapter.getSelectedAlarmsArray()); 341 outState.putBundle(KEY_RINGTONE_TITLE_CACHE, mRingtoneTitleCache); 342 outState.putParcelable(KEY_DELETED_ALARM, mDeletedAlarm); 343 outState.putBoolean(KEY_UNDO_SHOWING, mUndoShowing); 344 outState.putBundle(KEY_PREVIOUS_DAY_MAP, mAdapter.getPreviousDaysOfWeekMap()); 345 outState.putParcelable(KEY_SELECTED_ALARM, mSelectedAlarm); 346 } 347 348 @Override 349 public void onDestroy() { 350 super.onDestroy(); 351 ToastMaster.cancelToast(); 352 } 353 354 @Override 355 public void onPause() { 356 super.onPause(); 357 // When the user places the app in the background by pressing "home", 358 // dismiss the toast bar. However, since there is no way to determine if 359 // home was pressed, just dismiss any existing toast bar when restarting 360 // the app. 361 hideUndoBar(false, null); 362 } 363 364 // Callback used by TimePickerDialog 365 @Override 366 public void onTimeSet(TimePicker timePicker, int hourOfDay, int minute) { 367 if (mSelectedAlarm == null) { 368 // If mSelectedAlarm is null then we're creating a new alarm. 369 Alarm a = new Alarm(); 370 a.alert = RingtoneManager.getActualDefaultRingtoneUri(getActivity(), 371 RingtoneManager.TYPE_ALARM); 372 if (a.alert == null) { 373 a.alert = Uri.parse("content://settings/system/alarm_alert"); 374 } 375 a.hour = hourOfDay; 376 a.minutes = minute; 377 a.enabled = true; 378 mAddedAlarm = a; 379 asyncAddAlarm(a); 380 } else { 381 mSelectedAlarm.hour = hourOfDay; 382 mSelectedAlarm.minutes = minute; 383 mSelectedAlarm.enabled = true; 384 mScrollToAlarmId = mSelectedAlarm.id; 385 asyncUpdateAlarm(mSelectedAlarm, true); 386 mSelectedAlarm = null; 387 } 388 } 389 390 private void showLabelDialog(final Alarm alarm) { 391 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 392 final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog"); 393 if (prev != null) { 394 ft.remove(prev); 395 } 396 ft.addToBackStack(null); 397 398 // Create and show the dialog. 399 final LabelDialogFragment newFragment = 400 LabelDialogFragment.newInstance(alarm, alarm.label, getTag()); 401 newFragment.show(ft, "label_dialog"); 402 } 403 404 public void setLabel(Alarm alarm, String label) { 405 alarm.label = label; 406 asyncUpdateAlarm(alarm, false); 407 } 408 409 @Override 410 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 411 return Alarm.getAlarmsCursorLoader(getActivity()); 412 } 413 414 @Override 415 public void onLoadFinished(Loader<Cursor> cursorLoader, final Cursor data) { 416 mAdapter.swapCursor(data); 417 if (mScrollToAlarmId != INVALID_ID) { 418 scrollToAlarm(mScrollToAlarmId); 419 mScrollToAlarmId = INVALID_ID; 420 } 421 } 422 423 /** 424 * Scroll to alarm with given alarm id. 425 * 426 * @param alarmId The alarm id to scroll to. 427 */ 428 private void scrollToAlarm(long alarmId) { 429 int alarmPosition = -1; 430 for (int i = 0; i < mAdapter.getCount(); i++) { 431 long id = mAdapter.getItemId(i); 432 if (id == alarmId) { 433 alarmPosition = i; 434 break; 435 } 436 } 437 438 if (alarmPosition >= 0) { 439 mAdapter.setNewAlarm(alarmId); 440 mAlarmsList.smoothScrollToPositionFromTop(alarmPosition, 0); 441 } else { 442 // Trying to display a deleted alarm should only happen from a missed notification for 443 // an alarm that has been marked deleted after use. 444 Context context = getActivity().getApplicationContext(); 445 Toast toast = Toast.makeText(context, R.string.missed_alarm_has_been_deleted, 446 Toast.LENGTH_LONG); 447 ToastMaster.setToast(toast); 448 toast.show(); 449 } 450 } 451 452 @Override 453 public void onLoaderReset(Loader<Cursor> cursorLoader) { 454 mAdapter.swapCursor(null); 455 } 456 457 private void launchRingTonePicker(Alarm alarm) { 458 mSelectedAlarm = alarm; 459 Uri oldRingtone = Alarm.NO_RINGTONE_URI.equals(alarm.alert) ? null : alarm.alert; 460 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 461 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, oldRingtone); 462 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM); 463 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); 464 startActivityForResult(intent, REQUEST_CODE_RINGTONE); 465 } 466 467 private void saveRingtoneUri(Intent intent) { 468 Uri uri = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 469 if (uri == null) { 470 uri = Alarm.NO_RINGTONE_URI; 471 } 472 mSelectedAlarm.alert = uri; 473 474 // Save the last selected ringtone as the default for new alarms 475 if (!Alarm.NO_RINGTONE_URI.equals(uri)) { 476 RingtoneManager.setActualDefaultRingtoneUri( 477 getActivity(), RingtoneManager.TYPE_ALARM, uri); 478 } 479 asyncUpdateAlarm(mSelectedAlarm, false); 480 } 481 482 @Override 483 public void onActivityResult(int requestCode, int resultCode, Intent data) { 484 if (resultCode == Activity.RESULT_OK) { 485 switch (requestCode) { 486 case REQUEST_CODE_RINGTONE: 487 saveRingtoneUri(data); 488 break; 489 default: 490 LogUtils.w("Unhandled request code in onActivityResult: " + requestCode); 491 } 492 } 493 } 494 495 public class AlarmItemAdapter extends CursorAdapter { 496 private final Context mContext; 497 private final LayoutInflater mFactory; 498 private final String[] mShortWeekDayStrings; 499 private final String[] mLongWeekDayStrings; 500 private final int mColorLit; 501 private final int mColorDim; 502 private final Typeface mRobotoNormal; 503 private final ListView mList; 504 505 private long mExpandedId; 506 private ItemHolder mExpandedItemHolder; 507 private final HashSet<Long> mRepeatChecked = new HashSet<Long>(); 508 private final HashSet<Long> mSelectedAlarms = new HashSet<Long>(); 509 private Bundle mPreviousDaysOfWeekMap = new Bundle(); 510 511 private final boolean mHasVibrator; 512 private final int mCollapseExpandHeight; 513 514 // This determines the order in which it is shown and processed in the UI. 515 private final int[] DAY_ORDER = new int[] { 516 Calendar.SUNDAY, 517 Calendar.MONDAY, 518 Calendar.TUESDAY, 519 Calendar.WEDNESDAY, 520 Calendar.THURSDAY, 521 Calendar.FRIDAY, 522 Calendar.SATURDAY, 523 }; 524 525 public class ItemHolder { 526 527 // views for optimization 528 LinearLayout alarmItem; 529 TextTime clock; 530 TextView tomorrowLabel; 531 Switch onoff; 532 TextView daysOfWeek; 533 TextView label; 534 ImageButton delete; 535 View expandArea; 536 View summary; 537 TextView clickableLabel; 538 CheckBox repeat; 539 LinearLayout repeatDays; 540 Button[] dayButtons = new Button[7]; 541 CheckBox vibrate; 542 TextView ringtone; 543 View hairLine; 544 View arrow; 545 View collapseExpandArea; 546 547 // Other states 548 Alarm alarm; 549 } 550 551 // Used for scrolling an expanded item in the list to make sure it is fully visible. 552 private long mScrollAlarmId = AlarmClockFragment.INVALID_ID; 553 private final Runnable mScrollRunnable = new Runnable() { 554 @Override 555 public void run() { 556 if (mScrollAlarmId != AlarmClockFragment.INVALID_ID) { 557 View v = getViewById(mScrollAlarmId); 558 if (v != null) { 559 Rect rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 560 mList.requestChildRectangleOnScreen(v, rect, false); 561 } 562 mScrollAlarmId = AlarmClockFragment.INVALID_ID; 563 } 564 } 565 }; 566 567 public AlarmItemAdapter(Context context, long expandedId, long[] repeatCheckedIds, 568 long[] selectedAlarms, Bundle previousDaysOfWeekMap, ListView list) { 569 super(context, null, 0); 570 mContext = context; 571 mFactory = LayoutInflater.from(context); 572 mList = list; 573 574 DateFormatSymbols dfs = new DateFormatSymbols(); 575 mShortWeekDayStrings = Utils.getShortWeekdays(); 576 mLongWeekDayStrings = dfs.getWeekdays(); 577 578 Resources res = mContext.getResources(); 579 mColorLit = res.getColor(R.color.clock_white); 580 mColorDim = res.getColor(R.color.clock_gray); 581 582 mRobotoNormal = Typeface.create("sans-serif", Typeface.NORMAL); 583 584 mExpandedId = expandedId; 585 if (repeatCheckedIds != null) { 586 buildHashSetFromArray(repeatCheckedIds, mRepeatChecked); 587 } 588 if (previousDaysOfWeekMap != null) { 589 mPreviousDaysOfWeekMap = previousDaysOfWeekMap; 590 } 591 if (selectedAlarms != null) { 592 buildHashSetFromArray(selectedAlarms, mSelectedAlarms); 593 } 594 595 mHasVibrator = ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)) 596 .hasVibrator(); 597 598 mCollapseExpandHeight = (int) res.getDimension(R.dimen.collapse_expand_height); 599 } 600 601 public void removeSelectedId(int id) { 602 mSelectedAlarms.remove(id); 603 } 604 605 @Override 606 public View getView(int position, View convertView, ViewGroup parent) { 607 if (!getCursor().moveToPosition(position)) { 608 // May happen if the last alarm was deleted and the cursor refreshed while the 609 // list is updated. 610 LogUtils.v("couldn't move cursor to position " + position); 611 return null; 612 } 613 View v; 614 if (convertView == null) { 615 v = newView(mContext, getCursor(), parent); 616 } else { 617 v = convertView; 618 } 619 bindView(v, mContext, getCursor()); 620 return v; 621 } 622 623 @Override 624 public View newView(Context context, Cursor cursor, ViewGroup parent) { 625 final View view = mFactory.inflate(R.layout.alarm_time, parent, false); 626 setNewHolder(view); 627 return view; 628 } 629 630 /** 631 * In addition to changing the data set for the alarm list, swapCursor is now also 632 * responsible for preparing the transition for any added/removed items. 633 */ 634 @Override 635 public synchronized Cursor swapCursor(Cursor cursor) { 636 if (mAddedAlarm != null || mDeletedAlarm != null) { 637 TransitionManager.beginDelayedTransition(mAlarmsList, mAddRemoveTransition); 638 } 639 640 final Cursor c = super.swapCursor(cursor); 641 642 mAddedAlarm = null; 643 mDeletedAlarm = null; 644 645 return c; 646 } 647 648 private void setNewHolder(View view) { 649 // standard view holder optimization 650 final ItemHolder holder = new ItemHolder(); 651 holder.alarmItem = (LinearLayout) view.findViewById(R.id.alarm_item); 652 holder.tomorrowLabel = (TextView) view.findViewById(R.id.tomorrowLabel); 653 holder.clock = (TextTime) view.findViewById(R.id.digital_clock); 654 holder.onoff = (Switch) view.findViewById(R.id.onoff); 655 holder.onoff.setTypeface(mRobotoNormal); 656 holder.daysOfWeek = (TextView) view.findViewById(R.id.daysOfWeek); 657 holder.label = (TextView) view.findViewById(R.id.label); 658 holder.delete = (ImageButton) view.findViewById(R.id.delete); 659 holder.summary = view.findViewById(R.id.summary); 660 holder.expandArea = view.findViewById(R.id.expand_area); 661 holder.hairLine = view.findViewById(R.id.hairline); 662 holder.arrow = view.findViewById(R.id.arrow); 663 holder.repeat = (CheckBox) view.findViewById(R.id.repeat_onoff); 664 holder.clickableLabel = (TextView) view.findViewById(R.id.edit_label); 665 holder.repeatDays = (LinearLayout) view.findViewById(R.id.repeat_days); 666 holder.collapseExpandArea = view.findViewById(R.id.collapse_expand); 667 668 // Build button for each day. 669 for (int i = 0; i < 7; i++) { 670 final Button dayButton = (Button) mFactory.inflate( 671 R.layout.day_button, holder.repeatDays, false /* attachToRoot */); 672 dayButton.setText(mShortWeekDayStrings[i]); 673 dayButton.setContentDescription(mLongWeekDayStrings[DAY_ORDER[i]]); 674 holder.repeatDays.addView(dayButton); 675 holder.dayButtons[i] = dayButton; 676 } 677 holder.vibrate = (CheckBox) view.findViewById(R.id.vibrate_onoff); 678 holder.ringtone = (TextView) view.findViewById(R.id.choose_ringtone); 679 680 view.setTag(holder); 681 } 682 683 @Override 684 public void bindView(final View view, Context context, final Cursor cursor) { 685 final Alarm alarm = new Alarm(cursor); 686 Object tag = view.getTag(); 687 if (tag == null) { 688 // The view was converted but somehow lost its tag. 689 setNewHolder(view); 690 } 691 final ItemHolder itemHolder = (ItemHolder) tag; 692 itemHolder.alarm = alarm; 693 694 // We must unset the listener first because this maybe a recycled view so changing the 695 // state would affect the wrong alarm. 696 itemHolder.onoff.setOnCheckedChangeListener(null); 697 itemHolder.onoff.setChecked(alarm.enabled); 698 699 if (mSelectedAlarms.contains(itemHolder.alarm.id)) { 700 setAlarmItemBackgroundAndElevation(itemHolder.alarmItem, true /* expanded */); 701 setDigitalTimeAlpha(itemHolder, true); 702 itemHolder.onoff.setEnabled(false); 703 } else { 704 itemHolder.onoff.setEnabled(true); 705 setAlarmItemBackgroundAndElevation(itemHolder.alarmItem, false /* expanded */); 706 setDigitalTimeAlpha(itemHolder, itemHolder.onoff.isChecked()); 707 } 708 itemHolder.clock.setFormat( 709 (int)mContext.getResources().getDimension(R.dimen.alarm_label_size)); 710 itemHolder.clock.setTime(alarm.hour, alarm.minutes); 711 itemHolder.clock.setClickable(true); 712 itemHolder.clock.setOnClickListener(new View.OnClickListener() { 713 @Override 714 public void onClick(View view) { 715 mSelectedAlarm = itemHolder.alarm; 716 AlarmUtils.showTimeEditDialog(AlarmClockFragment.this, alarm); 717 expandAlarm(itemHolder, true); 718 itemHolder.alarmItem.post(mScrollRunnable); 719 } 720 }); 721 722 final CompoundButton.OnCheckedChangeListener onOffListener = 723 new CompoundButton.OnCheckedChangeListener() { 724 @Override 725 public void onCheckedChanged(CompoundButton compoundButton, 726 boolean checked) { 727 if (checked != alarm.enabled) { 728 setDigitalTimeAlpha(itemHolder, checked); 729 alarm.enabled = checked; 730 asyncUpdateAlarm(alarm, alarm.enabled); 731 } 732 } 733 }; 734 735 if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeating()) { 736 itemHolder.tomorrowLabel.setVisibility(View.GONE); 737 } else { 738 itemHolder.tomorrowLabel.setVisibility(View.VISIBLE); 739 final Resources resources = getResources(); 740 final String labelText = isTomorrow(alarm) ? 741 resources.getString(R.string.alarm_tomorrow) : 742 resources.getString(R.string.alarm_today); 743 itemHolder.tomorrowLabel.setText(labelText); 744 } 745 itemHolder.onoff.setOnCheckedChangeListener(onOffListener); 746 747 boolean expanded = isAlarmExpanded(alarm); 748 if (expanded) { 749 mExpandedItemHolder = itemHolder; 750 } 751 itemHolder.expandArea.setVisibility(expanded? View.VISIBLE : View.GONE); 752 itemHolder.delete.setVisibility(expanded ? View.VISIBLE : View.GONE); 753 itemHolder.summary.setVisibility(expanded? View.GONE : View.VISIBLE); 754 itemHolder.hairLine.setVisibility(expanded ? View.GONE : View.VISIBLE); 755 itemHolder.arrow.setRotation(expanded ? ROTATE_180_DEGREE : 0); 756 757 // Set the repeat text or leave it blank if it does not repeat. 758 final String daysOfWeekStr = 759 alarm.daysOfWeek.toString(AlarmClockFragment.this.getActivity(), false); 760 if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) { 761 itemHolder.daysOfWeek.setText(daysOfWeekStr); 762 itemHolder.daysOfWeek.setContentDescription(alarm.daysOfWeek.toAccessibilityString( 763 AlarmClockFragment.this.getActivity())); 764 itemHolder.daysOfWeek.setVisibility(View.VISIBLE); 765 itemHolder.daysOfWeek.setOnClickListener(new View.OnClickListener() { 766 @Override 767 public void onClick(View view) { 768 expandAlarm(itemHolder, true); 769 itemHolder.alarmItem.post(mScrollRunnable); 770 } 771 }); 772 773 } else { 774 itemHolder.daysOfWeek.setVisibility(View.GONE); 775 } 776 777 if (alarm.label != null && alarm.label.length() != 0) { 778 itemHolder.label.setText(alarm.label + " "); 779 itemHolder.label.setVisibility(View.VISIBLE); 780 itemHolder.label.setContentDescription( 781 mContext.getResources().getString(R.string.label_description) + " " 782 + alarm.label); 783 itemHolder.label.setOnClickListener(new View.OnClickListener() { 784 @Override 785 public void onClick(View view) { 786 expandAlarm(itemHolder, true); 787 itemHolder.alarmItem.post(mScrollRunnable); 788 } 789 }); 790 } else { 791 itemHolder.label.setVisibility(View.GONE); 792 } 793 794 itemHolder.delete.setOnClickListener(new View.OnClickListener() { 795 @Override 796 public void onClick(View v) { 797 mDeletedAlarm = alarm; 798 mRepeatChecked.remove(alarm.id); 799 asyncDeleteAlarm(alarm); 800 } 801 }); 802 803 if (expanded) { 804 expandAlarm(itemHolder, false); 805 } 806 807 itemHolder.alarmItem.setOnClickListener(new View.OnClickListener() { 808 @Override 809 public void onClick(View view) { 810 if (isAlarmExpanded(alarm)) { 811 collapseAlarm(itemHolder, true); 812 } else { 813 expandAlarm(itemHolder, true); 814 } 815 } 816 }); 817 } 818 819 private void setAlarmItemBackgroundAndElevation(LinearLayout layout, boolean expanded) { 820 if (expanded) { 821 layout.setBackgroundColor(getTintedBackgroundColor()); 822 layout.setElevation(ALARM_ELEVATION); 823 } else { 824 layout.setBackgroundResource(R.drawable.alarm_background_normal); 825 layout.setElevation(0); 826 } 827 } 828 829 private int getTintedBackgroundColor() { 830 final int c = Utils.getCurrentHourColor(); 831 final int red = Color.red(c) + (int) (TINTED_LEVEL * (255 - Color.red(c))); 832 final int green = Color.green(c) + (int) (TINTED_LEVEL * (255 - Color.green(c))); 833 final int blue = Color.blue(c) + (int) (TINTED_LEVEL * (255 - Color.blue(c))); 834 return Color.rgb(red, green, blue); 835 } 836 837 private boolean isTomorrow(Alarm alarm) { 838 final Calendar now = Calendar.getInstance(); 839 final int alarmHour = alarm.hour; 840 final int currHour = now.get(Calendar.HOUR_OF_DAY); 841 return alarmHour < currHour || 842 (alarmHour == currHour && alarm.minutes < now.get(Calendar.MINUTE)); 843 } 844 845 private void bindExpandArea(final ItemHolder itemHolder, final Alarm alarm) { 846 // Views in here are not bound until the item is expanded. 847 848 if (alarm.label != null && alarm.label.length() > 0) { 849 itemHolder.clickableLabel.setText(alarm.label); 850 } else { 851 itemHolder.clickableLabel.setText(R.string.label); 852 } 853 854 itemHolder.clickableLabel.setOnClickListener(new View.OnClickListener() { 855 @Override 856 public void onClick(View view) { 857 showLabelDialog(alarm); 858 } 859 }); 860 861 if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeating()) { 862 itemHolder.repeat.setChecked(true); 863 itemHolder.repeatDays.setVisibility(View.VISIBLE); 864 } else { 865 itemHolder.repeat.setChecked(false); 866 itemHolder.repeatDays.setVisibility(View.GONE); 867 } 868 itemHolder.repeat.setOnClickListener(new View.OnClickListener() { 869 @Override 870 public void onClick(View view) { 871 // Animate the resulting layout changes. 872 TransitionManager.beginDelayedTransition(mList, mRepeatTransition); 873 874 final boolean checked = ((CheckBox) view).isChecked(); 875 if (checked) { 876 // Show days 877 itemHolder.repeatDays.setVisibility(View.VISIBLE); 878 mRepeatChecked.add(alarm.id); 879 880 // Set all previously set days 881 // or 882 // Set all days if no previous. 883 final int bitSet = mPreviousDaysOfWeekMap.getInt("" + alarm.id); 884 alarm.daysOfWeek.setBitSet(bitSet); 885 if (!alarm.daysOfWeek.isRepeating()) { 886 alarm.daysOfWeek.setDaysOfWeek(true, DAY_ORDER); 887 } 888 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 889 } else { 890 // Hide days 891 itemHolder.repeatDays.setVisibility(View.GONE); 892 mRepeatChecked.remove(alarm.id); 893 894 // Remember the set days in case the user wants it back. 895 final int bitSet = alarm.daysOfWeek.getBitSet(); 896 mPreviousDaysOfWeekMap.putInt("" + alarm.id, bitSet); 897 898 // Remove all repeat days 899 alarm.daysOfWeek.clearAllDays(); 900 } 901 902 asyncUpdateAlarm(alarm, false); 903 } 904 }); 905 906 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 907 for (int i = 0; i < 7; i++) { 908 final int buttonIndex = i; 909 910 itemHolder.dayButtons[i].setOnClickListener(new View.OnClickListener() { 911 @Override 912 public void onClick(View view) { 913 final boolean isActivated = 914 itemHolder.dayButtons[buttonIndex].isActivated(); 915 alarm.daysOfWeek.setDaysOfWeek(!isActivated, DAY_ORDER[buttonIndex]); 916 if (!isActivated) { 917 turnOnDayOfWeek(itemHolder, buttonIndex); 918 } else { 919 turnOffDayOfWeek(itemHolder, buttonIndex); 920 921 // See if this was the last day, if so, un-check the repeat box. 922 if (!alarm.daysOfWeek.isRepeating()) { 923 // Animate the resulting layout changes. 924 TransitionManager.beginDelayedTransition(mList, mRepeatTransition); 925 926 itemHolder.repeat.setChecked(false); 927 itemHolder.repeatDays.setVisibility(View.GONE); 928 mRepeatChecked.remove(alarm.id); 929 930 // Set history to no days, so it will be everyday when repeat is 931 // turned back on 932 mPreviousDaysOfWeekMap.putInt("" + alarm.id, 933 DaysOfWeek.NO_DAYS_SET); 934 } 935 } 936 asyncUpdateAlarm(alarm, false); 937 } 938 }); 939 } 940 941 if (!mHasVibrator) { 942 itemHolder.vibrate.setVisibility(View.INVISIBLE); 943 } else { 944 itemHolder.vibrate.setVisibility(View.VISIBLE); 945 if (!alarm.vibrate) { 946 itemHolder.vibrate.setChecked(false); 947 } else { 948 itemHolder.vibrate.setChecked(true); 949 } 950 } 951 952 itemHolder.vibrate.setOnClickListener(new View.OnClickListener() { 953 @Override 954 public void onClick(View v) { 955 final boolean checked = ((CheckBox) v).isChecked(); 956 alarm.vibrate = checked; 957 asyncUpdateAlarm(alarm, false); 958 } 959 }); 960 961 final String ringtone; 962 if (Alarm.NO_RINGTONE_URI.equals(alarm.alert)) { 963 ringtone = mContext.getResources().getString(R.string.silent_alarm_summary); 964 } else { 965 ringtone = getRingToneTitle(alarm.alert); 966 } 967 itemHolder.ringtone.setText(ringtone); 968 itemHolder.ringtone.setContentDescription( 969 mContext.getResources().getString(R.string.ringtone_description) + " " 970 + ringtone); 971 itemHolder.ringtone.setOnClickListener(new View.OnClickListener() { 972 @Override 973 public void onClick(View view) { 974 launchRingTonePicker(alarm); 975 } 976 }); 977 } 978 979 // Sets the alpha of the digital time display. This gives a visual effect 980 // for enabled/disabled alarm while leaving the on/off switch more visible 981 private void setDigitalTimeAlpha(ItemHolder holder, boolean enabled) { 982 float alpha = enabled ? 1f : 0.69f; 983 holder.clock.setAlpha(alpha); 984 } 985 986 private void updateDaysOfWeekButtons(ItemHolder holder, DaysOfWeek daysOfWeek) { 987 HashSet<Integer> setDays = daysOfWeek.getSetDays(); 988 for (int i = 0; i < 7; i++) { 989 if (setDays.contains(DAY_ORDER[i])) { 990 turnOnDayOfWeek(holder, i); 991 } else { 992 turnOffDayOfWeek(holder, i); 993 } 994 } 995 } 996 997 public void toggleSelectState(View v) { 998 // long press could be on the parent view or one of its childs, so find the parent view 999 v = getTopParent(v); 1000 if (v != null) { 1001 long id = ((ItemHolder)v.getTag()).alarm.id; 1002 if (mSelectedAlarms.contains(id)) { 1003 mSelectedAlarms.remove(id); 1004 } else { 1005 mSelectedAlarms.add(id); 1006 } 1007 } 1008 } 1009 1010 private View getTopParent(View v) { 1011 while (v != null && v.getId() != R.id.alarm_item) { 1012 v = (View) v.getParent(); 1013 } 1014 return v; 1015 } 1016 1017 public int getSelectedItemsNum() { 1018 return mSelectedAlarms.size(); 1019 } 1020 1021 private void turnOffDayOfWeek(ItemHolder holder, int dayIndex) { 1022 final Button dayButton = holder.dayButtons[dayIndex]; 1023 dayButton.setActivated(false); 1024 dayButton.setTextColor(getResources().getColor(R.color.clock_white)); 1025 } 1026 1027 private void turnOnDayOfWeek(ItemHolder holder, int dayIndex) { 1028 final Button dayButton = holder.dayButtons[dayIndex]; 1029 dayButton.setActivated(true); 1030 dayButton.setTextColor(Utils.getCurrentHourColor()); 1031 } 1032 1033 1034 /** 1035 * Does a read-through cache for ringtone titles. 1036 * 1037 * @param uri The uri of the ringtone. 1038 * @return The ringtone title. {@literal null} if no matching ringtone found. 1039 */ 1040 private String getRingToneTitle(Uri uri) { 1041 // Try the cache first 1042 String title = mRingtoneTitleCache.getString(uri.toString()); 1043 if (title == null) { 1044 // This is slow because a media player is created during Ringtone object creation. 1045 Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri); 1046 title = ringTone.getTitle(mContext); 1047 if (title != null) { 1048 mRingtoneTitleCache.putString(uri.toString(), title); 1049 } 1050 } 1051 return title; 1052 } 1053 1054 public void setNewAlarm(long alarmId) { 1055 mExpandedId = alarmId; 1056 } 1057 1058 /** 1059 * Expands the alarm for editing. 1060 * 1061 * @param itemHolder The item holder instance. 1062 */ 1063 private void expandAlarm(final ItemHolder itemHolder, boolean animate) { 1064 // Skip animation later if item is already expanded 1065 animate &= mExpandedId != itemHolder.alarm.id; 1066 1067 if (mExpandedItemHolder != null 1068 && mExpandedItemHolder != itemHolder 1069 && mExpandedId != itemHolder.alarm.id) { 1070 // Only allow one alarm to expand at a time. 1071 collapseAlarm(mExpandedItemHolder, animate); 1072 } 1073 1074 bindExpandArea(itemHolder, itemHolder.alarm); 1075 1076 mExpandedId = itemHolder.alarm.id; 1077 mExpandedItemHolder = itemHolder; 1078 1079 // Scroll the view to make sure it is fully viewed 1080 mScrollAlarmId = itemHolder.alarm.id; 1081 1082 // Save the starting height so we can animate from this value. 1083 final int startingHeight = itemHolder.alarmItem.getHeight(); 1084 1085 // Set the expand area to visible so we can measure the height to animate to. 1086 setAlarmItemBackgroundAndElevation(itemHolder.alarmItem, true /* expanded */); 1087 itemHolder.expandArea.setVisibility(View.VISIBLE); 1088 itemHolder.delete.setVisibility(View.VISIBLE); 1089 1090 if (!animate) { 1091 // Set the "end" layout and don't do the animation. 1092 itemHolder.arrow.setRotation(ROTATE_180_DEGREE); 1093 return; 1094 } 1095 1096 // Add an onPreDrawListener, which gets called after measurement but before the draw. 1097 // This way we can check the height we need to animate to before any drawing. 1098 // Note the series of events: 1099 // * expandArea is set to VISIBLE, which causes a layout pass 1100 // * the view is measured, and our onPreDrawListener is called 1101 // * we set up the animation using the start and end values. 1102 // * the height is set back to the starting point so it can be animated down. 1103 // * request another layout pass. 1104 // * return false so that onDraw() is not called for the single frame before 1105 // the animations have started. 1106 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1107 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1108 @Override 1109 public boolean onPreDraw() { 1110 // We don't want to continue getting called for every listview drawing. 1111 if (observer.isAlive()) { 1112 observer.removeOnPreDrawListener(this); 1113 } 1114 // Calculate some values to help with the animation. 1115 final int endingHeight = itemHolder.alarmItem.getHeight(); 1116 final int distance = endingHeight - startingHeight; 1117 final int collapseHeight = itemHolder.collapseExpandArea.getHeight(); 1118 1119 // Set the height back to the start state of the animation. 1120 itemHolder.alarmItem.getLayoutParams().height = startingHeight; 1121 // To allow the expandArea to glide in with the expansion animation, set a 1122 // negative top margin, which will animate down to a margin of 0 as the height 1123 // is increased. 1124 // Note that we need to maintain the bottom margin as a fixed value (instead of 1125 // just using a listview, to allow for a flatter hierarchy) to fit the bottom 1126 // bar underneath. 1127 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1128 itemHolder.expandArea.getLayoutParams(); 1129 expandParams.setMargins(0, -distance, 0, collapseHeight); 1130 itemHolder.alarmItem.requestLayout(); 1131 1132 // Set up the animator to animate the expansion. 1133 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 1134 .setDuration(EXPAND_DURATION); 1135 animator.setInterpolator(mExpandInterpolator); 1136 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1137 @Override 1138 public void onAnimationUpdate(ValueAnimator animator) { 1139 Float value = (Float) animator.getAnimatedValue(); 1140 1141 // For each value from 0 to 1, animate the various parts of the layout. 1142 itemHolder.alarmItem.getLayoutParams().height = 1143 (int) (value * distance + startingHeight); 1144 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1145 itemHolder.expandArea.getLayoutParams(); 1146 expandParams.setMargins( 1147 0, (int) -((1 - value) * distance), 0, collapseHeight); 1148 itemHolder.arrow.setRotation(ROTATE_180_DEGREE * value); 1149 itemHolder.summary.setAlpha(1 - value); 1150 itemHolder.hairLine.setAlpha(1 - value); 1151 1152 itemHolder.alarmItem.requestLayout(); 1153 } 1154 }); 1155 // Set everything to their final values when the animation's done. 1156 animator.addListener(new AnimatorListener() { 1157 @Override 1158 public void onAnimationEnd(Animator animation) { 1159 // Set it back to wrap content since we'd explicitly set the height. 1160 itemHolder.alarmItem.getLayoutParams().height = 1161 LayoutParams.WRAP_CONTENT; 1162 itemHolder.arrow.setRotation(ROTATE_180_DEGREE); 1163 itemHolder.summary.setVisibility(View.GONE); 1164 itemHolder.hairLine.setVisibility(View.GONE); 1165 itemHolder.delete.setVisibility(View.VISIBLE); 1166 } 1167 1168 @Override 1169 public void onAnimationCancel(Animator animation) { 1170 // TODO we may have to deal with cancelations of the animation. 1171 } 1172 1173 @Override 1174 public void onAnimationRepeat(Animator animation) { } 1175 @Override 1176 public void onAnimationStart(Animator animation) { } 1177 }); 1178 animator.start(); 1179 1180 // Return false so this draw does not occur to prevent the final frame from 1181 // being drawn for the single frame before the animations start. 1182 return false; 1183 } 1184 }); 1185 } 1186 1187 private boolean isAlarmExpanded(Alarm alarm) { 1188 return mExpandedId == alarm.id; 1189 } 1190 1191 private void collapseAlarm(final ItemHolder itemHolder, boolean animate) { 1192 mExpandedId = AlarmClockFragment.INVALID_ID; 1193 mExpandedItemHolder = null; 1194 1195 // Save the starting height so we can animate from this value. 1196 final int startingHeight = itemHolder.alarmItem.getHeight(); 1197 1198 // Set the expand area to gone so we can measure the height to animate to. 1199 setAlarmItemBackgroundAndElevation(itemHolder.alarmItem, false /* expanded */); 1200 itemHolder.expandArea.setVisibility(View.GONE); 1201 1202 if (!animate) { 1203 // Set the "end" layout and don't do the animation. 1204 itemHolder.arrow.setRotation(0); 1205 itemHolder.hairLine.setTranslationY(0); 1206 return; 1207 } 1208 1209 // Add an onPreDrawListener, which gets called after measurement but before the draw. 1210 // This way we can check the height we need to animate to before any drawing. 1211 // Note the series of events: 1212 // * expandArea is set to GONE, which causes a layout pass 1213 // * the view is measured, and our onPreDrawListener is called 1214 // * we set up the animation using the start and end values. 1215 // * expandArea is set to VISIBLE again so it can be shown animating. 1216 // * request another layout pass. 1217 // * return false so that onDraw() is not called for the single frame before 1218 // the animations have started. 1219 final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver(); 1220 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1221 @Override 1222 public boolean onPreDraw() { 1223 if (observer.isAlive()) { 1224 observer.removeOnPreDrawListener(this); 1225 } 1226 1227 // Calculate some values to help with the animation. 1228 final int endingHeight = itemHolder.alarmItem.getHeight(); 1229 final int distance = endingHeight - startingHeight; 1230 1231 // Re-set the visibilities for the start state of the animation. 1232 itemHolder.expandArea.setVisibility(View.VISIBLE); 1233 itemHolder.delete.setVisibility(View.GONE); 1234 itemHolder.summary.setVisibility(View.VISIBLE); 1235 itemHolder.hairLine.setVisibility(View.VISIBLE); 1236 itemHolder.summary.setAlpha(1); 1237 1238 // Set up the animator to animate the expansion. 1239 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f) 1240 .setDuration(COLLAPSE_DURATION); 1241 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1242 @Override 1243 public void onAnimationUpdate(ValueAnimator animator) { 1244 Float value = (Float) animator.getAnimatedValue(); 1245 1246 // For each value from 0 to 1, animate the various parts of the layout. 1247 itemHolder.alarmItem.getLayoutParams().height = 1248 (int) (value * distance + startingHeight); 1249 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1250 itemHolder.expandArea.getLayoutParams(); 1251 expandParams.setMargins( 1252 0, (int) (value * distance), 0, mCollapseExpandHeight); 1253 itemHolder.arrow.setRotation(ROTATE_180_DEGREE * (1 - value)); 1254 itemHolder.delete.setAlpha(value); 1255 itemHolder.summary.setAlpha(value); 1256 itemHolder.hairLine.setAlpha(value); 1257 1258 itemHolder.alarmItem.requestLayout(); 1259 } 1260 }); 1261 animator.setInterpolator(mCollapseInterpolator); 1262 // Set everything to their final values when the animation's done. 1263 animator.addListener(new AnimatorListenerAdapter() { 1264 @Override 1265 public void onAnimationEnd(Animator animation) { 1266 // Set it back to wrap content since we'd explicitly set the height. 1267 itemHolder.alarmItem.getLayoutParams().height = 1268 LayoutParams.WRAP_CONTENT; 1269 1270 FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams) 1271 itemHolder.expandArea.getLayoutParams(); 1272 expandParams.setMargins(0, 0, 0, mCollapseExpandHeight); 1273 1274 itemHolder.expandArea.setVisibility(View.GONE); 1275 itemHolder.arrow.setRotation(0); 1276 } 1277 }); 1278 animator.start(); 1279 1280 return false; 1281 } 1282 }); 1283 } 1284 1285 @Override 1286 public int getViewTypeCount() { 1287 return 1; 1288 } 1289 1290 private View getViewById(long id) { 1291 for (int i = 0; i < mList.getCount(); i++) { 1292 View v = mList.getChildAt(i); 1293 if (v != null) { 1294 ItemHolder h = (ItemHolder)(v.getTag()); 1295 if (h != null && h.alarm.id == id) { 1296 return v; 1297 } 1298 } 1299 } 1300 return null; 1301 } 1302 1303 public long getExpandedId() { 1304 return mExpandedId; 1305 } 1306 1307 public long[] getSelectedAlarmsArray() { 1308 int index = 0; 1309 long[] ids = new long[mSelectedAlarms.size()]; 1310 for (long id : mSelectedAlarms) { 1311 ids[index] = id; 1312 index++; 1313 } 1314 return ids; 1315 } 1316 1317 public long[] getRepeatArray() { 1318 int index = 0; 1319 long[] ids = new long[mRepeatChecked.size()]; 1320 for (long id : mRepeatChecked) { 1321 ids[index] = id; 1322 index++; 1323 } 1324 return ids; 1325 } 1326 1327 public Bundle getPreviousDaysOfWeekMap() { 1328 return mPreviousDaysOfWeekMap; 1329 } 1330 1331 private void buildHashSetFromArray(long[] ids, HashSet<Long> set) { 1332 for (long id : ids) { 1333 set.add(id); 1334 } 1335 } 1336 } 1337 1338 private void startCreatingAlarm() { 1339 // Set the "selected" alarm as null, and we'll create the new one when the timepicker 1340 // comes back. 1341 mSelectedAlarm = null; 1342 AlarmUtils.showTimeEditDialog(this, null); 1343 } 1344 1345 private static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) { 1346 ContentResolver cr = context.getContentResolver(); 1347 AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance()); 1348 newInstance = AlarmInstance.addInstance(cr, newInstance); 1349 // Register instance to state manager 1350 AlarmStateManager.registerInstance(context, newInstance, true); 1351 return newInstance; 1352 } 1353 1354 private void asyncDeleteAlarm(final Alarm alarm) { 1355 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1356 final AsyncTask<Void, Void, Void> deleteTask = new AsyncTask<Void, Void, Void>() { 1357 @Override 1358 protected Void doInBackground(Void... parameters) { 1359 // Activity may be closed at this point , make sure data is still valid 1360 if (context != null && alarm != null) { 1361 ContentResolver cr = context.getContentResolver(); 1362 AlarmStateManager.deleteAllInstances(context, alarm.id); 1363 Alarm.deleteAlarm(cr, alarm.id); 1364 sDeskClockExtensions.deleteAlarm( 1365 AlarmClockFragment.this.getActivity().getApplicationContext(), alarm.id); 1366 } 1367 return null; 1368 } 1369 }; 1370 mUndoShowing = true; 1371 deleteTask.execute(); 1372 } 1373 1374 private void asyncAddAlarm(final Alarm alarm) { 1375 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1376 final AsyncTask<Void, Void, AlarmInstance> updateTask = 1377 new AsyncTask<Void, Void, AlarmInstance>() { 1378 @Override 1379 protected AlarmInstance doInBackground(Void... parameters) { 1380 if (context != null && alarm != null) { 1381 ContentResolver cr = context.getContentResolver(); 1382 1383 // Add alarm to db 1384 Alarm newAlarm = Alarm.addAlarm(cr, alarm); 1385 mScrollToAlarmId = newAlarm.id; 1386 1387 // Create and add instance to db 1388 if (newAlarm.enabled) { 1389 sDeskClockExtensions.addAlarm( 1390 AlarmClockFragment.this.getActivity().getApplicationContext(), 1391 newAlarm); 1392 return setupAlarmInstance(context, newAlarm); 1393 } 1394 } 1395 return null; 1396 } 1397 1398 @Override 1399 protected void onPostExecute(AlarmInstance instance) { 1400 if (instance != null) { 1401 AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis()); 1402 } 1403 } 1404 }; 1405 updateTask.execute(); 1406 } 1407 1408 private void asyncUpdateAlarm(final Alarm alarm, final boolean popToast) { 1409 final Context context = AlarmClockFragment.this.getActivity().getApplicationContext(); 1410 final AsyncTask<Void, Void, AlarmInstance> updateTask = 1411 new AsyncTask<Void, Void, AlarmInstance>() { 1412 @Override 1413 protected AlarmInstance doInBackground(Void ... parameters) { 1414 ContentResolver cr = context.getContentResolver(); 1415 1416 // Dismiss all old instances 1417 AlarmStateManager.deleteAllInstances(context, alarm.id); 1418 1419 // Update alarm 1420 Alarm.updateAlarm(cr, alarm); 1421 if (alarm.enabled) { 1422 return setupAlarmInstance(context, alarm); 1423 } 1424 1425 return null; 1426 } 1427 1428 @Override 1429 protected void onPostExecute(AlarmInstance instance) { 1430 if (popToast && instance != null) { 1431 AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis()); 1432 } 1433 } 1434 }; 1435 updateTask.execute(); 1436 } 1437 1438 @Override 1439 public boolean onTouch(View v, MotionEvent event) { 1440 hideUndoBar(true, event); 1441 return false; 1442 } 1443 1444 @Override 1445 public void onFabClick(View view){ 1446 hideUndoBar(true, null); 1447 startCreatingAlarm(); 1448 } 1449 1450 @Override 1451 public void setFabAppearance() { 1452 final DeskClock activity = (DeskClock) getActivity(); 1453 if (mFab == null || activity.getSelectedTab() != DeskClock.ALARM_TAB_INDEX) { 1454 return; 1455 } 1456 mFab.setVisibility(View.VISIBLE); 1457 mFab.setImageResource(R.drawable.ic_fab_plus); 1458 mFab.setContentDescription(getString(R.string.button_alarms)); 1459 } 1460 1461 @Override 1462 public void setLeftRightButtonAppearance() { 1463 final DeskClock activity = (DeskClock) getActivity(); 1464 if (mLeftButton == null || mRightButton == null || 1465 activity.getSelectedTab() != DeskClock.ALARM_TAB_INDEX) { 1466 return; 1467 } 1468 mLeftButton.setVisibility(View.INVISIBLE); 1469 mRightButton.setVisibility(View.INVISIBLE); 1470 } 1471 } 1472