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