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