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