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