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