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