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.app.ActionBar; 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.app.Fragment; 23import android.app.FragmentTransaction; 24import android.app.LoaderManager; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.Intent; 28import android.content.Loader; 29import android.content.res.Resources; 30import android.database.Cursor; 31import android.graphics.Rect; 32import android.graphics.Typeface; 33import android.media.Ringtone; 34import android.media.RingtoneManager; 35import android.net.Uri; 36import android.os.AsyncTask; 37import android.os.Bundle; 38import android.os.Vibrator; 39import android.view.ActionMode; 40import android.view.ActionMode.Callback; 41import android.view.LayoutInflater; 42import android.view.Menu; 43import android.view.MenuItem; 44import android.view.MotionEvent; 45import android.view.View; 46import android.view.View.OnLongClickListener; 47import android.view.ViewGroup; 48import android.widget.CheckBox; 49import android.widget.CompoundButton; 50import android.widget.CursorAdapter; 51import android.widget.LinearLayout; 52import android.widget.ListView; 53import android.widget.Switch; 54import android.widget.TextView; 55import android.widget.ToggleButton; 56 57import com.android.deskclock.widget.ActionableToastBar; 58import com.android.deskclock.widget.swipeablelistview.SwipeableListView; 59 60import java.text.DateFormatSymbols; 61import java.util.Calendar; 62import java.util.HashSet; 63 64/** 65 * AlarmClock application. 66 */ 67public class AlarmClock extends Activity implements LoaderManager.LoaderCallbacks<Cursor>, 68 AlarmTimePickerDialogFragment.AlarmTimePickerDialogHandler, 69 LabelDialogFragment.AlarmLabelDialogHandler, 70 OnLongClickListener, Callback, DialogInterface.OnClickListener, 71 DialogInterface.OnCancelListener { 72 73 private static final String KEY_EXPANDED_IDS = "expandedIds"; 74 private static final String KEY_REPEAT_CHECKED_IDS = "repeatCheckedIds"; 75 private static final String KEY_RINGTONE_TITLE_CACHE = "ringtoneTitleCache"; 76 private static final String KEY_SELECTED_ALARMS = "selectedAlarms"; 77 private static final String KEY_DELETED_ALARM = "deletedAlarm"; 78 private static final String KEY_UNDO_SHOWING = "undoShowing"; 79 private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap"; 80 private static final String KEY_SELECTED_ALARM = "selectedAlarm"; 81 private static final String KEY_DELETE_CONFIRMATION = "deleteConfirmation"; 82 83 private static final int REQUEST_CODE_RINGTONE = 1; 84 85 private SwipeableListView mAlarmsList; 86 private AlarmItemAdapter mAdapter; 87 private Bundle mRingtoneTitleCache; // Key: ringtone uri, value: ringtone title 88 private ActionableToastBar mUndoBar; 89 private ActionMode mActionMode; 90 91 private Alarm mSelectedAlarm; 92 private int mScrollToAlarmId = -1; 93 private boolean mInDeleteConfirmation = false; 94 95 // This flag relies on the activity having a "standard" launchMode and a new instance of this 96 // activity being created when launched. 97 private boolean mFirstLoad = true; 98 99 // Saved states for undo 100 private Alarm mDeletedAlarm; 101 private boolean mUndoShowing = false; 102 103 @Override 104 protected void onCreate(Bundle savedState) { 105 super.onCreate(savedState); 106 initialize(savedState); 107 updateLayout(); 108 getLoaderManager().initLoader(0, null, this); 109 } 110 111 private void initialize(Bundle savedState) { 112 setContentView(R.layout.alarm_clock); 113 int[] expandedIds = null; 114 int[] repeatCheckedIds = null; 115 int[] selectedAlarms = null; 116 Bundle previousDayMap = null; 117 if (savedState != null) { 118 expandedIds = savedState.getIntArray(KEY_EXPANDED_IDS); 119 repeatCheckedIds = savedState.getIntArray(KEY_REPEAT_CHECKED_IDS); 120 mRingtoneTitleCache = savedState.getBundle(KEY_RINGTONE_TITLE_CACHE); 121 mDeletedAlarm = savedState.getParcelable(KEY_DELETED_ALARM); 122 mUndoShowing = savedState.getBoolean(KEY_UNDO_SHOWING); 123 selectedAlarms = savedState.getIntArray(KEY_SELECTED_ALARMS); 124 previousDayMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP); 125 mSelectedAlarm = savedState.getParcelable(KEY_SELECTED_ALARM); 126 mInDeleteConfirmation = savedState.getBoolean(KEY_DELETE_CONFIRMATION, false); 127 } 128 129 mAlarmsList = (SwipeableListView) findViewById(R.id.alarms_list); 130 mAdapter = new AlarmItemAdapter( 131 this, expandedIds, repeatCheckedIds, selectedAlarms, previousDayMap, mAlarmsList); 132 mAdapter.setLongClickListener(this); 133 134 if (mRingtoneTitleCache == null) { 135 mRingtoneTitleCache = new Bundle(); 136 } 137 138 mAlarmsList.setAdapter(mAdapter); 139 mAlarmsList.setVerticalScrollBarEnabled(true); 140 mAlarmsList.enableSwipe(true); 141 mAlarmsList.setOnCreateContextMenuListener(this); 142 mAlarmsList.setOnItemSwipeListener(new SwipeableListView.OnItemSwipeListener() { 143 @Override 144 public void onSwipe(View view) { 145 final AlarmItemAdapter.ItemHolder itemHolder = 146 (AlarmItemAdapter.ItemHolder) view.getTag(); 147 mAdapter.removeSelectedId(itemHolder.alarm.id); 148 updateActionMode(); 149 asyncDeleteAlarm(itemHolder.alarm); 150 } 151 }); 152 mAlarmsList.setOnTouchListener(new View.OnTouchListener() { 153 @Override 154 public boolean onTouch(View view, MotionEvent event) { 155 hideUndoBar(true, event); 156 return false; 157 } 158 }); 159 160 mUndoBar = (ActionableToastBar) findViewById(R.id.undo_bar); 161 162 if (mUndoShowing) { 163 mUndoBar.show(new ActionableToastBar.ActionClickedListener() { 164 @Override 165 public void onActionClicked() { 166 asyncAddAlarm(mDeletedAlarm, false); 167 mDeletedAlarm = null; 168 mUndoShowing = false; 169 } 170 }, 0, getResources().getString(R.string.alarm_deleted), true, R.string.alarm_undo, 171 true); 172 } 173 174 // Show action mode if needed 175 int selectedNum = mAdapter.getSelectedItemsNum(); 176 if (selectedNum > 0) { 177 mActionMode = startActionMode(this); 178 setActionModeTitle(selectedNum); 179 } 180 181 } 182 183 @Override 184 public void onResume() { 185 super.onResume(); 186 if (mInDeleteConfirmation) { 187 showConfirmationDialog(); 188 } 189 } 190 private void hideUndoBar(boolean animate, MotionEvent event) { 191 if (mUndoBar != null) { 192 if (event != null && mUndoBar.isEventInToastBar(event)) { 193 // Avoid touches inside the undo bar. 194 return; 195 } 196 mUndoBar.hide(animate); 197 } 198 mDeletedAlarm = null; 199 mUndoShowing = false; 200 } 201 202 @Override 203 protected void onSaveInstanceState(Bundle outState) { 204 super.onSaveInstanceState(outState); 205 outState.putIntArray(KEY_EXPANDED_IDS, mAdapter.getExpandedArray()); 206 outState.putIntArray(KEY_REPEAT_CHECKED_IDS, mAdapter.getRepeatArray()); 207 outState.putIntArray(KEY_SELECTED_ALARMS, mAdapter.getSelectedAlarmsArray()); 208 outState.putBundle(KEY_RINGTONE_TITLE_CACHE, mRingtoneTitleCache); 209 outState.putParcelable(KEY_DELETED_ALARM, mDeletedAlarm); 210 outState.putBoolean(KEY_UNDO_SHOWING, mUndoShowing); 211 outState.putBundle(KEY_PREVIOUS_DAY_MAP, mAdapter.getPreviousDaysOfWeekMap()); 212 outState.putParcelable(KEY_SELECTED_ALARM, mSelectedAlarm); 213 outState.putBoolean(KEY_DELETE_CONFIRMATION, mInDeleteConfirmation); 214 } 215 216 private void updateLayout() { 217 final ActionBar actionBar = getActionBar(); 218 if (actionBar != null) { 219 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP); 220 } 221 } 222 223 @Override 224 protected void onDestroy() { 225 super.onDestroy(); 226 ToastMaster.cancelToast(); 227 } 228 229 @Override 230 public boolean onOptionsItemSelected(MenuItem item) { 231 hideUndoBar(true, null); 232 switch (item.getItemId()) { 233 case R.id.menu_item_settings: 234 startActivity(new Intent(this, SettingsActivity.class)); 235 return true; 236 case R.id.menu_item_add_alarm: 237 asyncAddAlarm(); 238 return true; 239 case R.id.menu_item_delete_alarm: 240 if (mAdapter != null) { 241 mAdapter.deleteSelectedAlarms(); 242 } 243 return true; 244 case android.R.id.home: 245 Intent intent = new Intent(this, DeskClock.class); 246 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 247 startActivity(intent); 248 return true; 249 default: 250 251 break; 252 } 253 return super.onOptionsItemSelected(item); 254 } 255 256 @Override 257 public boolean onCreateOptionsMenu(Menu menu) { 258 getMenuInflater().inflate(R.menu.alarm_list_menu, menu); 259 MenuItem help = menu.findItem(R.id.menu_item_help); 260 if (help != null) { 261 Utils.prepareHelpMenuItem(this, help); 262 } 263 return super.onCreateOptionsMenu(menu); 264 } 265 266 @Override 267 protected void onRestart() { 268 super.onRestart(); 269 // When the user places the app in the background by pressing "home", 270 // dismiss the toast bar. However, since there is no way to determine if 271 // home was pressed, just dismiss any existing toast bar when restarting 272 // the app. 273 if (mUndoBar != null) { 274 hideUndoBar(false, null); 275 } 276 } 277 278 // Callback used by AlarmTimePickerDialogFragment 279 @Override 280 public void onDialogTimeSet(Alarm alarm, int hourOfDay, int minute) { 281 alarm.hour = hourOfDay; 282 alarm.minutes = minute; 283 alarm.enabled = true; 284 mScrollToAlarmId = alarm.id; 285 asyncUpdateAlarm(alarm, true); 286 } 287 288 private void showLabelDialog(final Alarm alarm) { 289 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 290 final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog"); 291 if (prev != null) { 292 ft.remove(prev); 293 } 294 ft.addToBackStack(null); 295 296 // Create and show the dialog. 297 final LabelDialogFragment newFragment = LabelDialogFragment.newInstance(alarm, alarm.label); 298 newFragment.show(ft, "label_dialog"); 299 } 300 301 // Callback used by AlarmLabelDialogFragment. 302 @Override 303 public void onDialogLabelSet(Alarm alarm, String label) { 304 alarm.label = label; 305 asyncUpdateAlarm(alarm, false); 306 } 307 308 @Override 309 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 310 return Alarms.getAlarmsCursorLoader(this); 311 } 312 313 @Override 314 public void onLoadFinished(Loader<Cursor> cursorLoader, final Cursor data) { 315 mAdapter.swapCursor(data); 316 gotoAlarmIfSpecified(); 317 } 318 319 /** If an alarm was passed in via intent and goes to that particular alarm in the list. */ 320 private void gotoAlarmIfSpecified() { 321 final Intent intent = getIntent(); 322 if (mFirstLoad && intent != null) { 323 final Alarm alarm = (Alarm) intent.getParcelableExtra(Alarms.ALARM_INTENT_EXTRA); 324 if (alarm != null) { 325 scrollToAlarm(alarm.id); 326 } 327 } else if (mScrollToAlarmId != -1) { 328 scrollToAlarm(mScrollToAlarmId); 329 mScrollToAlarmId = -1; 330 } 331 mFirstLoad = false; 332 } 333 334 /** 335 * Scroll to alarm with given alarm id. 336 * 337 * @param alarmId The alarm id to scroll to. 338 */ 339 private void scrollToAlarm(int alarmId) { 340 for (int i = 0; i < mAdapter.getCount(); i++) { 341 long id = mAdapter.getItemId(i); 342 if (id == alarmId) { 343 mAdapter.setNewAlarm(alarmId); 344 mAlarmsList.smoothScrollToPositionFromTop(i, 0); 345 346 final int firstPositionId = mAlarmsList.getFirstVisiblePosition(); 347 final int childId = i - firstPositionId; 348 349 final View view = mAlarmsList.getChildAt(childId); 350 mAdapter.getView(i, view, mAlarmsList); 351 break; 352 } 353 } 354 } 355 356 @Override 357 public void onLoaderReset(Loader<Cursor> cursorLoader) { 358 mAdapter.swapCursor(null); 359 } 360 361 private void launchRingTonePicker(Alarm alarm) { 362 mSelectedAlarm = alarm; 363 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 364 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarm.alert); 365 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM); 366 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); 367 startActivityForResult(intent, REQUEST_CODE_RINGTONE); 368 } 369 370 private void saveRingtoneUri(Intent intent) { 371 final Uri uri = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 372 mSelectedAlarm.alert = uri; 373 // Save the last selected ringtone as the default for new alarms 374 if (uri != null) { 375 RingtoneManager.setActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM, uri); 376 } 377 asyncUpdateAlarm(mSelectedAlarm, false); 378 } 379 380 @Override 381 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 382 if (resultCode == RESULT_OK) { 383 switch (requestCode) { 384 case REQUEST_CODE_RINGTONE: 385 saveRingtoneUri(data); 386 break; 387 default: 388 Log.w("Unhandled request code in onActivityResult: " + requestCode); 389 } 390 } 391 } 392 393 /*** 394 * On long click, mark/unmark the selected view and activate/deactivate action mode 395 */ 396 @Override 397 public boolean onLongClick(View v) { 398 mAdapter.toggleSelectState(v); 399 mAdapter.notifyDataSetChanged(); 400 updateActionMode(); 401 return false; 402 } 403 404 /*** 405 * Activate/update/close action mode according to the number of selected views. 406 */ 407 private void updateActionMode() { 408 int selectedNum = mAdapter.getSelectedItemsNum(); 409 if (mActionMode == null && selectedNum > 0) { 410 // Start the action mode 411 mActionMode = startActionMode(this); 412 setActionModeTitle(selectedNum); 413 } else if (mActionMode != null) { 414 if (selectedNum > 0) { 415 // Update the number of selected items in the title 416 setActionModeTitle(selectedNum); 417 } else { 418 // No selected items. close the action mode 419 mActionMode.finish(); 420 mActionMode = null; 421 } 422 } 423 } 424 425 /*** 426 * Display the number of selected items on the action bar in action mode 427 * @param items - number of selected items 428 */ 429 private void setActionModeTitle(int items) { 430 mActionMode.setTitle(String.format(getString(R.string.alarms_selected), items)); 431 } 432 433 public class AlarmItemAdapter extends CursorAdapter { 434 435 private final Context mContext; 436 private final LayoutInflater mFactory; 437 private final String[] mShortWeekDayStrings; 438 private final String[] mLongWeekDayStrings; 439 private final int mColorLit; 440 private final int mColorDim; 441 private final int mBackgroundColorSelected; 442 private final int mBackgroundColor; 443 private final Typeface mRobotoNormal; 444 private final Typeface mRobotoBold; 445 private OnLongClickListener mLongClickListener; 446 private final ListView mList; 447 448 private final HashSet<Integer> mExpanded = new HashSet<Integer>(); 449 private final HashSet<Integer> mRepeatChecked = new HashSet<Integer>(); 450 private final HashSet<Integer> mSelectedAlarms = new HashSet<Integer>(); 451 private Bundle mPreviousDaysOfWeekMap = new Bundle(); 452 453 private final boolean mHasVibrator; 454 455 // This determines the order in which it is shown and processed in the UI. 456 private final int[] DAY_ORDER = new int[] { 457 Calendar.SUNDAY, 458 Calendar.MONDAY, 459 Calendar.TUESDAY, 460 Calendar.WEDNESDAY, 461 Calendar.THURSDAY, 462 Calendar.FRIDAY, 463 Calendar.SATURDAY, 464 }; 465 466 public class ItemHolder { 467 468 // views for optimization 469 LinearLayout alarmItem; 470 DigitalClock clock; 471 Switch onoff; 472 TextView daysOfWeek; 473 TextView label; 474 View expandArea; 475 View infoArea; 476 TextView clickableLabel; 477 CheckBox repeat; 478 LinearLayout repeatDays; 479 ViewGroup[] dayButtonParents = new ViewGroup[7]; 480 ToggleButton[] dayButtons = new ToggleButton[7]; 481 CheckBox vibrate; 482 ViewGroup collapse; 483 TextView ringtone; 484 View hairLine; 485 486 // Other states 487 Alarm alarm; 488 } 489 490 // Used for scrolling an expanded item in the list to make sure it is fully visible. 491 private int mScrollAlarmId = -1; 492 private final Runnable mScrollRunnable = new Runnable() { 493 @Override 494 public void run() { 495 if (mScrollAlarmId != -1) { 496 View v = getViewById(mScrollAlarmId); 497 if (v != null) { 498 Rect rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 499 mList.requestChildRectangleOnScreen(v, rect, false); 500 } 501 mScrollAlarmId = -1; 502 } 503 } 504 }; 505 506 public AlarmItemAdapter(Context context, int[] expandedIds, int[] repeatCheckedIds, 507 int[] selectedAlarms, Bundle previousDaysOfWeekMap, ListView list) { 508 super(context, null, 0); 509 mContext = context; 510 mFactory = LayoutInflater.from(context); 511 mList = list; 512 513 DateFormatSymbols dfs = new DateFormatSymbols(); 514 mShortWeekDayStrings = dfs.getShortWeekdays(); 515 mLongWeekDayStrings = dfs.getWeekdays(); 516 517 Resources res = mContext.getResources(); 518 mColorLit = res.getColor(R.color.clock_white); 519 mColorDim = res.getColor(R.color.clock_gray); 520 mBackgroundColorSelected = res.getColor(R.color.alarm_selected_color); 521 mBackgroundColor = res.getColor(R.color.alarm_whiteish); 522 523 524 mRobotoBold = Typeface.create("sans-serif-condensed", Typeface.BOLD); 525 mRobotoNormal = Typeface.create("sans-serif-condensed", Typeface.NORMAL); 526 527 if (expandedIds != null) { 528 buildHashSetFromArray(expandedIds, mExpanded); 529 } 530 if (repeatCheckedIds != null) { 531 buildHashSetFromArray(repeatCheckedIds, mRepeatChecked); 532 } 533 if (previousDaysOfWeekMap != null) { 534 mPreviousDaysOfWeekMap = previousDaysOfWeekMap; 535 } 536 if (selectedAlarms != null) { 537 buildHashSetFromArray(selectedAlarms, mSelectedAlarms); 538 } 539 540 mHasVibrator = ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)) 541 .hasVibrator(); 542 } 543 544 public void removeSelectedId(int id) { 545 mSelectedAlarms.remove(id); 546 } 547 548 public void setLongClickListener(OnLongClickListener l) { 549 mLongClickListener = l; 550 } 551 552 @Override 553 public View getView(int position, View convertView, ViewGroup parent) { 554 if (!getCursor().moveToPosition(position)) { 555 // May happen if the last alarm was deleted and the cursor refreshed while the 556 // list is updated. 557 Log.v("couldn't move cursor to position " + position); 558 return null; 559 } 560 View v; 561 if (convertView == null) { 562 v = newView(mContext, getCursor(), parent); 563 } else { 564 // Do a translation check to test for animation. Change this to something more 565 // reliable and robust in the future. 566 if (convertView.getTranslationX() != 0 || convertView.getTranslationY() != 0) { 567 // view was animated, reset 568 v = newView(mContext, getCursor(), parent); 569 } else { 570 v = convertView; 571 } 572 } 573 bindView(v, mContext, getCursor()); 574 return v; 575 } 576 577 @Override 578 public View newView(Context context, Cursor cursor, ViewGroup parent) { 579 final View view = mFactory.inflate(R.layout.alarm_time, parent, false); 580 581 // standard view holder optimization 582 final ItemHolder holder = new ItemHolder(); 583 holder.alarmItem = (LinearLayout) view.findViewById(R.id.alarm_item); 584 holder.clock = (DigitalClock) view.findViewById(R.id.digital_clock); 585 holder.clock.setLive(false); 586 holder.onoff = (Switch) view.findViewById(R.id.onoff); 587 holder.onoff.setTypeface(mRobotoNormal); 588 holder.daysOfWeek = (TextView) view.findViewById(R.id.daysOfWeek); 589 holder.label = (TextView) view.findViewById(R.id.label); 590 holder.expandArea = view.findViewById(R.id.expand_area); 591 holder.infoArea = view.findViewById(R.id.info_area); 592 holder.repeat = (CheckBox) view.findViewById(R.id.repeat_onoff); 593 holder.clickableLabel = (TextView) view.findViewById(R.id.edit_label); 594 holder.hairLine = view.findViewById(R.id.hairline); 595 holder.repeatDays = (LinearLayout) view.findViewById(R.id.repeat_days); 596 597 // Build button for each day. 598 for (int i = 0; i < 7; i++) { 599 final ViewGroup viewgroup = (ViewGroup) mFactory.inflate(R.layout.day_button, 600 holder.repeatDays, false); 601 final ToggleButton button = (ToggleButton) viewgroup.getChildAt(0); 602 final int dayToShowIndex = DAY_ORDER[i]; 603 button.setText(mShortWeekDayStrings[dayToShowIndex]); 604 button.setTextOn(mShortWeekDayStrings[dayToShowIndex]); 605 button.setTextOff(mShortWeekDayStrings[dayToShowIndex]); 606 button.setContentDescription(mLongWeekDayStrings[dayToShowIndex]); 607 holder.repeatDays.addView(viewgroup); 608 holder.dayButtons[i] = button; 609 holder.dayButtonParents[i] = viewgroup; 610 } 611 holder.vibrate = (CheckBox) view.findViewById(R.id.vibrate_onoff); 612 holder.collapse = (ViewGroup) view.findViewById(R.id.collapse); 613 holder.ringtone = (TextView) view.findViewById(R.id.choose_ringtone); 614 615 view.setTag(holder); 616 return view; 617 } 618 619 @Override 620 public void bindView(View view, Context context, final Cursor cursor) { 621 final Alarm alarm = new Alarm(cursor); 622 final ItemHolder itemHolder = (ItemHolder) view.getTag(); 623 itemHolder.alarm = alarm; 624 625 // We must unset the listener first because this maybe a recycled view so changing the 626 // state would affect the wrong alarm. 627 itemHolder.onoff.setOnCheckedChangeListener(null); 628 itemHolder.onoff.setChecked(alarm.enabled); 629 if (mSelectedAlarms.contains(itemHolder.alarm.id)) { 630 itemHolder.alarmItem.setBackgroundColor(mBackgroundColorSelected); 631 setItemAlpha(itemHolder, true); 632 itemHolder.onoff.setEnabled(false); 633 } else { 634 itemHolder.onoff.setEnabled(true); 635 itemHolder.alarmItem.setBackgroundColor(mBackgroundColor); 636 setItemAlpha(itemHolder, itemHolder.onoff.isChecked()); 637 } 638 final CompoundButton.OnCheckedChangeListener onOffListener = 639 new CompoundButton.OnCheckedChangeListener() { 640 @Override 641 public void onCheckedChanged(CompoundButton compoundButton, 642 boolean checked) { 643 //When action mode is on - simulate long click 644 if (doLongClick(compoundButton)) { 645 return; 646 } 647 if (checked != alarm.enabled) { 648 setItemAlpha(itemHolder, checked); 649 alarm.enabled = checked; 650 asyncUpdateAlarm(alarm, alarm.enabled); 651 } 652 } 653 }; 654 655 itemHolder.onoff.setOnCheckedChangeListener(onOffListener); 656 itemHolder.onoff.setOnLongClickListener(mLongClickListener); 657 658 itemHolder.clock.updateTime(alarm.hour, alarm.minutes); 659 itemHolder.clock.setClickable(true); 660 itemHolder.clock.setOnClickListener(new View.OnClickListener() { 661 @Override 662 public void onClick(View view) { 663 //When action mode is on - simulate long click 664 if (doLongClick(view)) { 665 return; 666 } 667 AlarmUtils.showTimeEditDialog(AlarmClock.this.getFragmentManager(), alarm); 668 expandAlarm(itemHolder); 669 itemHolder.alarmItem.post(mScrollRunnable); 670 } 671 }); 672 itemHolder.clock.setOnLongClickListener(mLongClickListener); 673 674 itemHolder.expandArea.setVisibility(isAlarmExpanded(alarm) ? View.VISIBLE : View.GONE); 675 itemHolder.expandArea.setOnLongClickListener(mLongClickListener); 676 itemHolder.infoArea.setVisibility(!isAlarmExpanded(alarm) ? View.VISIBLE : View.GONE); 677 itemHolder.infoArea.setOnClickListener(new View.OnClickListener() { 678 @Override 679 public void onClick(View view) { 680 //When action mode is on - simulate long click 681 if (doLongClick(view)) { 682 return; 683 } 684 expandAlarm(itemHolder); 685 itemHolder.alarmItem.post(mScrollRunnable); 686 } 687 }); 688 itemHolder.infoArea.setOnLongClickListener(mLongClickListener); 689 690 String colons = ""; 691 // Set the repeat text or leave it blank if it does not repeat. 692 final String daysOfWeekStr = alarm.daysOfWeek.toString(AlarmClock.this, false); 693 if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) { 694 itemHolder.daysOfWeek.setText(daysOfWeekStr); 695 itemHolder.daysOfWeek.setContentDescription( 696 alarm.daysOfWeek.toAccessibilityString(AlarmClock.this)); 697 itemHolder.daysOfWeek.setVisibility(View.VISIBLE); 698 colons = ": "; 699 itemHolder.daysOfWeek.setOnClickListener(new View.OnClickListener() { 700 @Override 701 public void onClick(View view) { 702 //When action mode is on - simulate long click 703 if (doLongClick(view)) { 704 return; 705 } 706 expandAlarm(itemHolder); 707 itemHolder.alarmItem.post(mScrollRunnable); 708 } 709 }); 710 itemHolder.daysOfWeek.setOnLongClickListener(mLongClickListener); 711 712 } else { 713 itemHolder.daysOfWeek.setVisibility(View.GONE); 714 } 715 716 if (alarm.label != null && alarm.label.length() != 0) { 717 itemHolder.label.setText(alarm.label + colons); 718 itemHolder.label.setVisibility(View.VISIBLE); 719 itemHolder.label.setContentDescription( 720 mContext.getResources().getString(R.string.label_description) + " " 721 + alarm.label); 722 itemHolder.label.setOnClickListener(new View.OnClickListener() { 723 @Override 724 public void onClick(View view) { 725 //When action mode is on - simulate long click 726 if (doLongClick(view)) { 727 return; 728 } 729 expandAlarm(itemHolder); 730 itemHolder.alarmItem.post(mScrollRunnable); 731 } 732 }); 733 itemHolder.label.setOnLongClickListener(mLongClickListener); 734 } else { 735 itemHolder.label.setVisibility(View.GONE); 736 } 737 738 if (isAlarmExpanded(alarm)) { 739 expandAlarm(itemHolder); 740 } 741 view.setOnLongClickListener(mLongClickListener); 742 view.setOnClickListener(new View.OnClickListener() { 743 @Override 744 public void onClick(View view) { 745 //When action mode is on - simulate long click 746 doLongClick(view); 747 } 748 }); 749 } 750 751 private void bindExpandArea(final ItemHolder itemHolder, final Alarm alarm) { 752 // Views in here are not bound until the item is expanded. 753 754 if (alarm.label != null && alarm.label.length() > 0) { 755 itemHolder.clickableLabel.setText(alarm.label); 756 itemHolder.clickableLabel.setTextColor(mColorLit); 757 } else { 758 itemHolder.clickableLabel.setText(R.string.label); 759 itemHolder.clickableLabel.setTextColor(mColorDim); 760 } 761 itemHolder.clickableLabel.setOnClickListener(new View.OnClickListener() { 762 @Override 763 public void onClick(View view) { 764 //When action mode is on - simulate long click 765 if (doLongClick(view)) { 766 return; 767 } 768 showLabelDialog(alarm); 769 } 770 }); 771 itemHolder.clickableLabel.setOnLongClickListener(mLongClickListener); 772 773 if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeatSet()) { 774 itemHolder.repeat.setChecked(true); 775 itemHolder.repeatDays.setVisibility(View.VISIBLE); 776 itemHolder.repeatDays.setOnLongClickListener(mLongClickListener); 777 } else { 778 itemHolder.repeat.setChecked(false); 779 itemHolder.repeatDays.setVisibility(View.GONE); 780 } 781 itemHolder.repeat.setOnClickListener(new View.OnClickListener() { 782 @Override 783 public void onClick(View view) { 784 //When action mode is on - simulate long click 785 if (doLongClick(view)) { 786 return; 787 } 788 final boolean checked = ((CheckBox) view).isChecked(); 789 if (checked) { 790 // Show days 791 itemHolder.repeatDays.setVisibility(View.VISIBLE); 792 mRepeatChecked.add(alarm.id); 793 794 // Set all previously set days 795 // or 796 // Set all days if no previous. 797 final int daysOfWeekCoded = mPreviousDaysOfWeekMap.getInt("" + alarm.id); 798 if (daysOfWeekCoded == 0) { 799 for (int day : DAY_ORDER) { 800 alarm.daysOfWeek.setDayOfWeek(day, true); 801 } 802 } else { 803 alarm.daysOfWeek.set(new Alarm.DaysOfWeek(daysOfWeekCoded)); 804 } 805 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 806 } else { 807 itemHolder.repeatDays.setVisibility(View.GONE); 808 mRepeatChecked.remove(alarm.id); 809 810 // Remember the set days in case the user wants it back. 811 final int daysOfWeekCoded = alarm.daysOfWeek.getCoded(); 812 mPreviousDaysOfWeekMap.putInt("" + alarm.id, daysOfWeekCoded); 813 814 // Remove all repeat days 815 alarm.daysOfWeek.set(new Alarm.DaysOfWeek(0)); 816 } 817 asyncUpdateAlarm(alarm, false); 818 } 819 }); 820 itemHolder.repeat.setOnLongClickListener(mLongClickListener); 821 822 updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek); 823 for (int i = 0; i < 7; i++) { 824 final int buttonIndex = i; 825 826 itemHolder.dayButtonParents[i].setOnClickListener(new View.OnClickListener() { 827 @Override 828 public void onClick(View view) { 829 //When action mode is on - simulate long click 830 if (doLongClick(view)) { 831 return; 832 } 833 itemHolder.dayButtons[buttonIndex].toggle(); 834 final boolean checked = itemHolder.dayButtons[buttonIndex].isChecked(); 835 int day = DAY_ORDER[buttonIndex]; 836 alarm.daysOfWeek.setDayOfWeek(day, checked); 837 if (checked) { 838 turnOnDayOfWeek(itemHolder, buttonIndex); 839 } else { 840 turnOffDayOfWeek(itemHolder, buttonIndex); 841 842 // See if this was the last day, if so, un-check the repeat box. 843 if (alarm.daysOfWeek.getCoded() == 0) { 844 itemHolder.repeatDays.setVisibility(View.GONE); 845 itemHolder.repeat.setTextColor(mColorDim); 846 mRepeatChecked.remove(alarm.id); 847 848 // Remember the set days in case the user wants it back. 849 mPreviousDaysOfWeekMap.putInt("" + alarm.id, 0); 850 } 851 } 852 asyncUpdateAlarm(alarm, false); 853 } 854 }); 855 } 856 857 858 if (!mHasVibrator) { 859 itemHolder.vibrate.setVisibility(View.INVISIBLE); 860 } else { 861 itemHolder.vibrate.setVisibility(View.VISIBLE); 862 if (!alarm.vibrate) { 863 itemHolder.vibrate.setChecked(false); 864 itemHolder.vibrate.setTextColor(mColorDim); 865 } else { 866 itemHolder.vibrate.setChecked(true); 867 itemHolder.vibrate.setTextColor(mColorLit); 868 } 869 itemHolder.vibrate.setOnLongClickListener(mLongClickListener); 870 } 871 872 itemHolder.vibrate.setOnClickListener(new View.OnClickListener() { 873 @Override 874 public void onClick(View v) { 875 final boolean checked = ((CheckBox) v).isChecked(); 876 //When action mode is on - simulate long click 877 if (doLongClick(v)) { 878 return; 879 } 880 if (checked) { 881 itemHolder.vibrate.setTextColor(mColorLit); 882 } else { 883 itemHolder.vibrate.setTextColor(mColorDim); 884 } 885 alarm.vibrate = checked; 886 asyncUpdateAlarm(alarm, false); 887 } 888 }); 889 890 itemHolder.collapse.setOnClickListener(new View.OnClickListener() { 891 @Override 892 public void onClick(View v) { 893 //When action mode is on - simulate long click 894 if (doLongClick(v)) { 895 return; 896 } 897 itemHolder.expandArea.setVisibility(LinearLayout.GONE); 898 itemHolder.infoArea.setVisibility(View.VISIBLE); 899 collapseAlarm(alarm); 900 } 901 }); 902 itemHolder.collapse.setOnLongClickListener(mLongClickListener); 903 904 final String ringtone; 905 if (alarm.alert == null) { 906 ringtone = mContext.getResources().getString(R.string.silent_alarm_summary); 907 } else { 908 ringtone = getRingToneTitle(alarm.alert); 909 } 910 itemHolder.ringtone.setText(ringtone); 911 itemHolder.ringtone.setContentDescription( 912 mContext.getResources().getString(R.string.ringtone_description) + " " 913 + ringtone); 914 itemHolder.ringtone.setOnClickListener(new View.OnClickListener() { 915 @Override 916 public void onClick(View view) { 917 //When action mode is on - simulate long click 918 if (doLongClick(view)) { 919 return; 920 } 921 launchRingTonePicker(alarm); 922 } 923 }); 924 itemHolder.ringtone.setOnLongClickListener(mLongClickListener); 925 } 926 927 // Sets the alpha of the item except the on/off switch. This gives a visual effect 928 // for enabled/disabled alarm while leaving the on/off switch more visible 929 private void setItemAlpha(ItemHolder holder, boolean enabled) { 930 float alpha = enabled ? 1f : 0.5f; 931 holder.clock.setAlpha(alpha); 932 holder.infoArea.setAlpha(alpha); 933 holder.expandArea.setAlpha(alpha); 934 holder.hairLine.setAlpha(alpha); 935 } 936 937 private void updateDaysOfWeekButtons(ItemHolder holder, Alarm.DaysOfWeek daysOfWeek) { 938 HashSet<Integer> setDays = daysOfWeek.getSetDays(); 939 for (int i = 0; i < 7; i++) { 940 if (setDays.contains(DAY_ORDER[i])) { 941 turnOnDayOfWeek(holder, i); 942 } else { 943 turnOffDayOfWeek(holder, i); 944 } 945 } 946 } 947 948 /*** 949 * Simulate a long click to override clicks on view when ActionMode is on 950 * Returns true if handled a long click, false if not 951 */ 952 private boolean doLongClick(View v) { 953 if (mActionMode == null) { 954 return false; 955 } 956 v = getTopParent(v); 957 if (v != null) { 958 toggleSelectState(v); 959 notifyDataSetChanged(); 960 updateActionMode(); 961 } 962 return true; 963 } 964 965 public void toggleSelectState(View v) { 966 // long press could be on the parent view or one of its childs, so find the parent view 967 v = getTopParent(v); 968 if (v != null) { 969 int id = ((ItemHolder)v.getTag()).alarm.id; 970 if (mSelectedAlarms.contains(id)) { 971 mSelectedAlarms.remove(id); 972 } else { 973 mSelectedAlarms.add(id); 974 } 975 } 976 } 977 978 private View getTopParent(View v) { 979 while (v != null && v.getId() != R.id.alarm_item) { 980 v = (View) v.getParent(); 981 } 982 return v; 983 } 984 985 public int getSelectedItemsNum() { 986 return mSelectedAlarms.size(); 987 } 988 989 private void turnOffDayOfWeek(ItemHolder holder, int dayIndex) { 990 holder.dayButtons[dayIndex].setChecked(false); 991 holder.dayButtons[dayIndex].setTextColor(mColorDim); 992 holder.dayButtons[dayIndex].setTypeface(mRobotoNormal); 993 } 994 995 private void turnOnDayOfWeek(ItemHolder holder, int dayIndex) { 996 holder.dayButtons[dayIndex].setChecked(true); 997 holder.dayButtons[dayIndex].setTextColor(mColorLit); 998 holder.dayButtons[dayIndex].setTypeface(mRobotoBold); 999 } 1000 1001 1002 /** 1003 * Does a read-through cache for ringtone titles. 1004 * 1005 * @param uri The uri of the ringtone. 1006 * @return The ringtone title. {@literal null} if no matching ringtone found. 1007 */ 1008 private String getRingToneTitle(Uri uri) { 1009 // Try the cache first 1010 String title = mRingtoneTitleCache.getString(uri.toString()); 1011 if (title == null) { 1012 // This is slow because a media player is created during Ringtone object creation. 1013 Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri); 1014 title = ringTone.getTitle(mContext); 1015 if (title != null) { 1016 mRingtoneTitleCache.putString(uri.toString(), title); 1017 } 1018 } 1019 return title; 1020 } 1021 1022 public void setNewAlarm(int alarmId) { 1023 mExpanded.add(alarmId); 1024 } 1025 1026 /** 1027 * Expands the alarm for editing. 1028 * 1029 * @param itemHolder The item holder instance. 1030 */ 1031 private void expandAlarm(ItemHolder itemHolder) { 1032 itemHolder.expandArea.setVisibility(View.VISIBLE); 1033 itemHolder.expandArea.setOnClickListener(new View.OnClickListener() { 1034 @Override 1035 public void onClick(View view) { 1036 //When action mode is on - simulate long click 1037 doLongClick(view); 1038 } 1039 }); 1040 itemHolder.infoArea.setVisibility(View.GONE); 1041 1042 mExpanded.add(itemHolder.alarm.id); 1043 bindExpandArea(itemHolder, itemHolder.alarm); 1044 // Scroll the view to make sure it is fully viewed 1045 mScrollAlarmId = itemHolder.alarm.id; 1046 } 1047 1048 private boolean isAlarmExpanded(Alarm alarm) { 1049 return mExpanded.contains(alarm.id); 1050 } 1051 1052 private void collapseAlarm(Alarm alarm) { 1053 mExpanded.remove(alarm.id); 1054 } 1055 1056 @Override 1057 public int getViewTypeCount() { 1058 return 1; 1059 } 1060 1061 private View getViewById(int id) { 1062 for (int i = 0; i < mList.getCount(); i++) { 1063 View v = mList.getChildAt(i); 1064 if (v != null) { 1065 ItemHolder h = (ItemHolder)(v.getTag()); 1066 if (h != null && h.alarm.id == id) { 1067 return v; 1068 } 1069 } 1070 } 1071 return null; 1072 } 1073 1074 public int[] getExpandedArray() { 1075 final int[] ids = new int[mExpanded.size()]; 1076 int index = 0; 1077 for (int id : mExpanded) { 1078 ids[index] = id; 1079 index++; 1080 } 1081 return ids; 1082 } 1083 1084 public int[] getSelectedAlarmsArray() { 1085 final int[] ids = new int[mSelectedAlarms.size()]; 1086 int index = 0; 1087 for (int id : mSelectedAlarms) { 1088 ids[index] = id; 1089 index++; 1090 } 1091 return ids; 1092 } 1093 1094 public int[] getRepeatArray() { 1095 final int[] ids = new int[mRepeatChecked.size()]; 1096 int index = 0; 1097 for (int id : mRepeatChecked) { 1098 ids[index] = id; 1099 index++; 1100 } 1101 return ids; 1102 } 1103 1104 public Bundle getPreviousDaysOfWeekMap() { 1105 return mPreviousDaysOfWeekMap; 1106 } 1107 1108 private void buildHashSetFromArray(int[] ids, HashSet<Integer> set) { 1109 for (int id : ids) { 1110 set.add(id); 1111 } 1112 } 1113 1114 public void deleteSelectedAlarms() { 1115 Integer ids [] = new Integer[mSelectedAlarms.size()]; 1116 int index = 0; 1117 for (int id : mSelectedAlarms) { 1118 ids[index] = id; 1119 index ++; 1120 } 1121 asyncDeleteAlarm(ids); 1122 clearSelectedAlarms(); 1123 } 1124 1125 public void clearSelectedAlarms() { 1126 mSelectedAlarms.clear(); 1127 notifyDataSetChanged(); 1128 } 1129 } 1130 1131 private void asyncAddAlarm() { 1132 Alarm a = new Alarm(); 1133 a.alert = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); 1134 if (a.alert == null) { 1135 a.alert = Uri.parse("content://settings/system/alarm_alert"); 1136 } 1137 asyncAddAlarm(a, true); 1138 } 1139 1140 private void asyncDeleteAlarm(final Integer [] alarmIds) { 1141 final AsyncTask<Integer, Void, Void> deleteTask = new AsyncTask<Integer, Void, Void>() { 1142 @Override 1143 protected Void doInBackground(Integer... ids) { 1144 for (final int id : ids) { 1145 Alarms.deleteAlarm(AlarmClock.this, id); 1146 } 1147 return null; 1148 } 1149 }; 1150 deleteTask.execute(alarmIds); 1151 } 1152 1153 private void asyncDeleteAlarm(final Alarm alarm) { 1154 final AsyncTask<Alarm, Void, Void> deleteTask = new AsyncTask<Alarm, Void, Void>() { 1155 1156 @Override 1157 protected Void doInBackground(Alarm... alarms) { 1158 for (final Alarm alarm : alarms) { 1159 Alarms.deleteAlarm(AlarmClock.this, alarm.id); 1160 } 1161 return null; 1162 } 1163 }; 1164 mDeletedAlarm = alarm; 1165 mUndoShowing = true; 1166 deleteTask.execute(alarm); 1167 mUndoBar.show(new ActionableToastBar.ActionClickedListener() { 1168 @Override 1169 public void onActionClicked() { 1170 asyncAddAlarm(alarm, false); 1171 mDeletedAlarm = null; 1172 mUndoShowing = false; 1173 } 1174 }, 0, getResources().getString(R.string.alarm_deleted), true, R.string.alarm_undo, true); 1175 } 1176 1177 private void asyncAddAlarm(final Alarm alarm, final boolean showTimePicker) { 1178 final AsyncTask<Void, Void, Void> updateTask = new AsyncTask<Void, Void, Void>() { 1179 @Override 1180 protected Void doInBackground(Void... aVoid) { 1181 Alarms.addAlarm(AlarmClock.this, alarm); 1182 return null; 1183 } 1184 1185 @Override 1186 protected void onPostExecute(Void aVoid) { 1187 if (alarm.enabled) { 1188 popToast(alarm); 1189 } 1190 mAdapter.setNewAlarm(alarm.id); 1191 scrollToAlarm(alarm.id); 1192 1193 // We need to refresh the first view item because bindView may have been called 1194 // before setNewAlarm took effect. In that case, the newly created alarm will not be 1195 // expanded. 1196 View view = mAlarmsList.getChildAt(0); 1197 mAdapter.getView(0, view, mAlarmsList); 1198 if (showTimePicker) { 1199 AlarmUtils.showTimeEditDialog(AlarmClock.this.getFragmentManager(), alarm); 1200 } 1201 } 1202 }; 1203 updateTask.execute(); 1204 } 1205 1206 private void asyncUpdateAlarm(final Alarm alarm, final boolean popToast) { 1207 final AsyncTask<Alarm, Void, Void> updateTask = new AsyncTask<Alarm, Void, Void>() { 1208 @Override 1209 protected Void doInBackground(Alarm... alarms) { 1210 for (final Alarm alarm : alarms) { 1211 Alarms.setAlarm(AlarmClock.this, alarm); 1212 } 1213 return null; 1214 } 1215 1216 @Override 1217 protected void onPostExecute(Void aVoid) { 1218 if (popToast) { 1219 popToast(alarm); 1220 } 1221 } 1222 }; 1223 updateTask.execute(alarm); 1224 } 1225 1226 private void popToast(Alarm alarm) { 1227 AlarmUtils.popAlarmSetToast(this, alarm.hour, alarm.minutes, alarm.daysOfWeek); 1228 } 1229 1230 /*** 1231 * Support for action mode when the user long presses an item in the alarms list 1232 */ 1233 1234 @Override 1235 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 1236 switch (item.getItemId()) { 1237 // Delete selected items and close CAB. 1238 case R.id.menu_item_delete_alarm: 1239 showConfirmationDialog(); 1240 break; 1241 default: 1242 break; 1243 } 1244 return false; 1245 } 1246 1247 @Override 1248 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 1249 getMenuInflater().inflate(R.menu.alarm_cab_menu, menu); 1250 return true; 1251 } 1252 1253 @Override 1254 public void onDestroyActionMode(ActionMode arg0) { 1255 if(mAdapter != null) { 1256 mAdapter.clearSelectedAlarms(); 1257 } 1258 mActionMode = null; 1259 } 1260 1261 @Override 1262 public boolean onPrepareActionMode(ActionMode arg0, Menu arg1) { 1263 return false; 1264 } 1265 1266 /*** 1267 * Handle the delete alarms confirmation dialog 1268 */ 1269 1270 private void showConfirmationDialog() { 1271 AlertDialog.Builder b = new AlertDialog.Builder(this); 1272 Resources res = getResources(); 1273 String msg = String.format(res.getQuantityText(R.plurals.alarm_delete_confirmation, 1274 mAdapter.getSelectedItemsNum()).toString()); 1275 b.setCancelable(true).setMessage(msg) 1276 .setOnCancelListener(this) 1277 .setNegativeButton(res.getString(android.R.string.cancel), this) 1278 .setPositiveButton(res.getString(android.R.string.ok), this).show(); 1279 mInDeleteConfirmation = true; 1280 } 1281 @Override 1282 public void onClick(DialogInterface dialog, int which) { 1283 if (which == -1) { 1284 if (mAdapter != null) { 1285 mAdapter.deleteSelectedAlarms(); 1286 mActionMode.finish(); 1287 } 1288 } 1289 dialog.dismiss(); 1290 mInDeleteConfirmation = false; 1291 } 1292 1293 @Override 1294 public void onCancel(DialogInterface dialog) { 1295 mInDeleteConfirmation = false; 1296 } 1297} 1298