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.AnimatorInflater;
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.res.Configuration;
32import android.content.res.Resources;
33import android.database.Cursor;
34import android.database.DataSetObserver;
35import android.graphics.Rect;
36import android.graphics.Typeface;
37import android.media.Ringtone;
38import android.media.RingtoneManager;
39import android.net.Uri;
40import android.os.AsyncTask;
41import android.os.Bundle;
42import android.os.Vibrator;
43import android.text.format.DateFormat;
44import android.view.Gravity;
45import android.view.LayoutInflater;
46import android.view.MotionEvent;
47import android.view.View;
48import android.view.View.OnClickListener;
49import android.view.ViewGroup;
50import android.view.ViewGroup.LayoutParams;
51import android.view.ViewTreeObserver;
52import android.view.animation.DecelerateInterpolator;
53import android.view.animation.Interpolator;
54import android.widget.CheckBox;
55import android.widget.CompoundButton;
56import android.widget.CursorAdapter;
57import android.widget.FrameLayout;
58import android.widget.ImageButton;
59import android.widget.ImageView;
60import android.widget.LinearLayout;
61import android.widget.ListView;
62import android.widget.Switch;
63import android.widget.TextView;
64import android.widget.Toast;
65import android.widget.ToggleButton;
66
67import com.android.datetimepicker.time.RadialPickerLayout;
68import com.android.datetimepicker.time.TimePickerDialog;
69import com.android.deskclock.alarms.AlarmStateManager;
70import com.android.deskclock.provider.Alarm;
71import com.android.deskclock.provider.AlarmInstance;
72import com.android.deskclock.provider.DaysOfWeek;
73import com.android.deskclock.widget.ActionableToastBar;
74import com.android.deskclock.widget.TextTime;
75
76import java.text.DateFormatSymbols;
77import java.util.Calendar;
78import java.util.HashSet;
79import java.util.concurrent.ConcurrentHashMap;
80
81/**
82 * AlarmClock application.
83 */
84public class AlarmClockFragment extends DeskClockFragment implements
85        LoaderManager.LoaderCallbacks<Cursor>,
86        TimePickerDialog.OnTimeSetListener,
87        View.OnTouchListener
88        {
89    private static final float EXPAND_DECELERATION = 1f;
90    private static final float COLLAPSE_DECELERATION = 0.7f;
91    private static final int ANIMATION_DURATION = 300;
92    private static final String KEY_EXPANDED_IDS = "expandedIds";
93    private static final String KEY_REPEAT_CHECKED_IDS = "repeatCheckedIds";
94    private static final String KEY_RINGTONE_TITLE_CACHE = "ringtoneTitleCache";
95    private static final String KEY_SELECTED_ALARMS = "selectedAlarms";
96    private static final String KEY_DELETED_ALARM = "deletedAlarm";
97    private static final String KEY_UNDO_SHOWING = "undoShowing";
98    private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap";
99    private static final String KEY_SELECTED_ALARM = "selectedAlarm";
100    private static final String KEY_DELETE_CONFIRMATION = "deleteConfirmation";
101
102    private static final int REQUEST_CODE_RINGTONE = 1;
103
104    // This extra is used when receiving an intent to create an alarm, but no alarm details
105    // have been passed in, so the alarm page should start the process of creating a new alarm.
106    public static final String ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new";
107
108    // This extra is used when receiving an intent to scroll to specific alarm. If alarm
109    // can not be found, and toast message will pop up that the alarm has be deleted.
110    public static final String SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm";
111
112    private ListView mAlarmsList;
113    private AlarmItemAdapter mAdapter;
114    private View mEmptyView;
115    private ImageView mAddAlarmButton;
116    private View mAlarmsView;
117    private View mTimelineLayout;
118    private AlarmTimelineView mTimelineView;
119    private View mFooterView;
120
121    private Bundle mRingtoneTitleCache; // Key: ringtone uri, value: ringtone title
122    private ActionableToastBar mUndoBar;
123    private View mUndoFrame;
124
125    private Alarm mSelectedAlarm;
126    private long mScrollToAlarmId = -1;
127
128    private Loader mCursorLoader = null;
129
130    // Saved states for undo
131    private Alarm mDeletedAlarm;
132    private Alarm mAddedAlarm;
133    private boolean mUndoShowing = false;
134
135    private Animator mFadeIn;
136    private Animator mFadeOut;
137
138    private Interpolator mExpandInterpolator;
139    private Interpolator mCollapseInterpolator;
140
141    private int mTimelineViewWidth;
142    private int mUndoBarInitialMargin;
143
144    // Cached layout positions of items in listview prior to add/removal of alarm item
145    private ConcurrentHashMap<Long, Integer> mItemIdTopMap = new ConcurrentHashMap<Long, Integer>();
146
147    public AlarmClockFragment() {
148        // Basic provider required by Fragment.java
149    }
150
151    @Override
152    public void onCreate(Bundle savedState) {
153        super.onCreate(savedState);
154        mCursorLoader = getLoaderManager().initLoader(0, null, this);
155    }
156
157    @Override
158    public View onCreateView(LayoutInflater inflater, ViewGroup container,
159            Bundle savedState) {
160        // Inflate the layout for this fragment
161        final View v = inflater.inflate(R.layout.alarm_clock, container, false);
162
163        long[] expandedIds = null;
164        long[] repeatCheckedIds = null;
165        long[] selectedAlarms = null;
166        Bundle previousDayMap = null;
167        if (savedState != null) {
168            expandedIds = savedState.getLongArray(KEY_EXPANDED_IDS);
169            repeatCheckedIds = savedState.getLongArray(KEY_REPEAT_CHECKED_IDS);
170            mRingtoneTitleCache = savedState.getBundle(KEY_RINGTONE_TITLE_CACHE);
171            mDeletedAlarm = savedState.getParcelable(KEY_DELETED_ALARM);
172            mUndoShowing = savedState.getBoolean(KEY_UNDO_SHOWING);
173            selectedAlarms = savedState.getLongArray(KEY_SELECTED_ALARMS);
174            previousDayMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP);
175            mSelectedAlarm = savedState.getParcelable(KEY_SELECTED_ALARM);
176        }
177
178        mExpandInterpolator = new DecelerateInterpolator(EXPAND_DECELERATION);
179        mCollapseInterpolator = new DecelerateInterpolator(COLLAPSE_DECELERATION);
180
181        mAddAlarmButton = (ImageButton) v.findViewById(R.id.alarm_add_alarm);
182        mAddAlarmButton.setOnClickListener(new OnClickListener() {
183            @Override
184            public void onClick(View v) {
185                hideUndoBar(true, null);
186                startCreatingAlarm();
187            }
188        });
189        // For landscape, put the add button on the right and the menu in the actionbar.
190        FrameLayout.LayoutParams layoutParams =
191                (FrameLayout.LayoutParams) mAddAlarmButton.getLayoutParams();
192        boolean isLandscape = getResources().getConfiguration().orientation
193                == Configuration.ORIENTATION_LANDSCAPE;
194        if (isLandscape) {
195            layoutParams.gravity = Gravity.END;
196        } else {
197            layoutParams.gravity = Gravity.CENTER;
198        }
199        mAddAlarmButton.setLayoutParams(layoutParams);
200
201        View menuButton = v.findViewById(R.id.menu_button);
202        if (menuButton != null) {
203            if (isLandscape) {
204                menuButton.setVisibility(View.GONE);
205            } else {
206                menuButton.setVisibility(View.VISIBLE);
207                setupFakeOverflowMenuButton(menuButton);
208            }
209        }
210
211        mEmptyView = v.findViewById(R.id.alarms_empty_view);
212        mEmptyView.setOnClickListener(new OnClickListener() {
213            @Override
214            public void onClick(View v) {
215                startCreatingAlarm();
216            }
217        });
218        mAlarmsList = (ListView) v.findViewById(R.id.alarms_list);
219
220        mFadeIn = AnimatorInflater.loadAnimator(getActivity(), R.anim.fade_in);
221        mFadeIn.setDuration(ANIMATION_DURATION);
222        mFadeIn.addListener(new AnimatorListener() {
223
224            @Override
225            public void onAnimationStart(Animator animation) {
226                mEmptyView.setVisibility(View.VISIBLE);
227            }
228
229            @Override
230            public void onAnimationCancel(Animator animation) {
231                // Do nothing.
232            }
233
234            @Override
235            public void onAnimationEnd(Animator animation) {
236                // Do nothing.
237            }
238
239            @Override
240            public void onAnimationRepeat(Animator animation) {
241                // Do nothing.
242            }
243        });
244        mFadeIn.setTarget(mEmptyView);
245        mFadeOut = AnimatorInflater.loadAnimator(getActivity(), R.anim.fade_out);
246        mFadeOut.setDuration(ANIMATION_DURATION);
247        mFadeOut.addListener(new AnimatorListener() {
248
249            @Override
250            public void onAnimationStart(Animator arg0) {
251                mEmptyView.setVisibility(View.VISIBLE);
252            }
253
254            @Override
255            public void onAnimationCancel(Animator arg0) {
256                // Do nothing.
257            }
258
259            @Override
260            public void onAnimationEnd(Animator arg0) {
261                mEmptyView.setVisibility(View.GONE);
262            }
263
264            @Override
265            public void onAnimationRepeat(Animator arg0) {
266                // Do nothing.
267            }
268        });
269        mFadeOut.setTarget(mEmptyView);
270        mAlarmsView = v.findViewById(R.id.alarm_layout);
271        mTimelineLayout = v.findViewById(R.id.timeline_layout);
272
273        mUndoBar = (ActionableToastBar) v.findViewById(R.id.undo_bar);
274        mUndoBarInitialMargin = getActivity().getResources()
275                .getDimensionPixelOffset(R.dimen.alarm_undo_bar_horizontal_margin);
276        mUndoFrame = v.findViewById(R.id.undo_frame);
277        mUndoFrame.setOnTouchListener(this);
278
279        mFooterView = v.findViewById(R.id.alarms_footer_view);
280        mFooterView.setOnTouchListener(this);
281
282        // Timeline layout only exists in tablet landscape mode for now.
283        if (mTimelineLayout != null) {
284            mTimelineView = (AlarmTimelineView) v.findViewById(R.id.alarm_timeline_view);
285            mTimelineViewWidth = getActivity().getResources()
286                    .getDimensionPixelOffset(R.dimen.alarm_timeline_layout_width);
287        }
288
289        mAdapter = new AlarmItemAdapter(getActivity(),
290                expandedIds, repeatCheckedIds, selectedAlarms, previousDayMap, mAlarmsList);
291        mAdapter.registerDataSetObserver(new DataSetObserver() {
292
293            private int prevAdapterCount = -1;
294
295            @Override
296            public void onChanged() {
297
298                final int count = mAdapter.getCount();
299                if (mDeletedAlarm != null && prevAdapterCount > count) {
300                    showUndoBar();
301                }
302
303                // If there are no alarms in the adapter...
304                if (count == 0) {
305                    mAddAlarmButton.setBackgroundResource(R.drawable.main_button_red);
306
307                    // ...and if there exists a timeline view (currently only in tablet landscape)
308                    if (mTimelineLayout != null && mAlarmsView != null) {
309
310                        // ...and if the previous adapter had alarms (indicating a removal)...
311                        if (prevAdapterCount > 0) {
312
313                            // Then animate in the "no alarms" icon...
314                            mFadeIn.start();
315
316                            // and animate out the alarm timeline view, expanding the width of the
317                            // alarms list / undo bar.
318                            mTimelineLayout.setVisibility(View.VISIBLE);
319                            ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f)
320                                    .setDuration(ANIMATION_DURATION);
321                            animator.setInterpolator(mCollapseInterpolator);
322                            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
323                                @Override
324                                public void onAnimationUpdate(ValueAnimator animator) {
325                                    Float value = (Float) animator.getAnimatedValue();
326                                    int currentTimelineWidth = (int) (value * mTimelineViewWidth);
327                                    float rightOffset = mTimelineViewWidth * (1 - value);
328                                    mTimelineLayout.setTranslationX(rightOffset);
329                                    mTimelineLayout.setAlpha(value);
330                                    mTimelineLayout.requestLayout();
331                                    setUndoBarRightMargin(currentTimelineWidth
332                                            + mUndoBarInitialMargin);
333                                }
334                            });
335                            animator.addListener(new AnimatorListener() {
336
337                                @Override
338                                public void onAnimationCancel(Animator animation) {
339                                    // Do nothing.
340                                }
341
342                                @Override
343                                public void onAnimationEnd(Animator animation) {
344                                    mTimelineView.setIsAnimatingOut(false);
345                                }
346
347                                @Override
348                                public void onAnimationRepeat(Animator animation) {
349                                    // Do nothing.
350                                }
351
352                                @Override
353                                public void onAnimationStart(Animator animation) {
354                                    mTimelineView.setIsAnimatingOut(true);
355                                }
356
357                            });
358                            animator.start();
359                        } else {
360                            // If the previous adapter did not have alarms, no animation needed,
361                            // just hide the timeline view and show the "no alarms" icon.
362                            mTimelineLayout.setVisibility(View.GONE);
363                            mEmptyView.setVisibility(View.VISIBLE);
364                            setUndoBarRightMargin(mUndoBarInitialMargin);
365                        }
366                    } else {
367
368                        // If there is no timeline view, just show the "no alarms" icon.
369                        mEmptyView.setVisibility(View.VISIBLE);
370                    }
371                } else {
372
373                    // Otherwise, if the adapter DOES contain alarms...
374                    mAddAlarmButton.setBackgroundResource(R.drawable.main_button_normal);
375
376                    // ...and if there exists a timeline view (currently in tablet landscape mode)
377                    if (mTimelineLayout != null && mAlarmsView != null) {
378
379                        mTimelineLayout.setVisibility(View.VISIBLE);
380                        // ...and if the previous adapter did not have alarms (indicating an add)
381                        if (prevAdapterCount == 0) {
382
383                            // Then, animate to hide the "no alarms" icon...
384                            mFadeOut.start();
385
386                            // and animate to show the timeline view, reducing the width of the
387                            // alarms list / undo bar.
388                            ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
389                                    .setDuration(ANIMATION_DURATION);
390                            animator.setInterpolator(mExpandInterpolator);
391                            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
392                                @Override
393                                public void onAnimationUpdate(ValueAnimator animator) {
394                                    Float value = (Float) animator.getAnimatedValue();
395                                    int currentTimelineWidth = (int) (value * mTimelineViewWidth);
396                                    float rightOffset = mTimelineViewWidth * (1 - value);
397                                    mTimelineLayout.setTranslationX(rightOffset);
398                                    mTimelineLayout.setAlpha(value);
399                                    mTimelineLayout.requestLayout();
400                                    ((FrameLayout.LayoutParams) mAlarmsView.getLayoutParams())
401                                        .setMargins(0, 0, (int) -rightOffset, 0);
402                                    mAlarmsView.requestLayout();
403                                    setUndoBarRightMargin(currentTimelineWidth
404                                            + mUndoBarInitialMargin);
405                                }
406                            });
407                            animator.start();
408                        } else {
409                            mTimelineLayout.setVisibility(View.VISIBLE);
410                            mEmptyView.setVisibility(View.GONE);
411                            setUndoBarRightMargin(mUndoBarInitialMargin + mTimelineViewWidth);
412                        }
413                    } else {
414
415                        // If there is no timeline view, just hide the "no alarms" icon.
416                        mEmptyView.setVisibility(View.GONE);
417                    }
418                }
419
420                // Cache this adapter's count for when the adapter changes.
421                prevAdapterCount = count;
422                super.onChanged();
423            }
424        });
425
426        if (mRingtoneTitleCache == null) {
427            mRingtoneTitleCache = new Bundle();
428        }
429
430        mAlarmsList.setAdapter(mAdapter);
431        mAlarmsList.setVerticalScrollBarEnabled(true);
432        mAlarmsList.setOnCreateContextMenuListener(this);
433
434        if (mUndoShowing) {
435            showUndoBar();
436        }
437        return v;
438    }
439
440    private void setUndoBarRightMargin(int margin) {
441        FrameLayout.LayoutParams params =
442                (FrameLayout.LayoutParams) mUndoBar.getLayoutParams();
443        ((FrameLayout.LayoutParams) mUndoBar.getLayoutParams())
444            .setMargins(params.leftMargin, params.topMargin, margin, params.bottomMargin);
445        mUndoBar.requestLayout();
446    }
447
448    @Override
449    public void onResume() {
450        super.onResume();
451        // Check if another app asked us to create a blank new alarm.
452        final Intent intent = getActivity().getIntent();
453        if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
454            if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
455                // An external app asked us to create a blank alarm.
456                startCreatingAlarm();
457            }
458
459            // Remove the CREATE_NEW extra now that we've processed it.
460            intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA);
461        } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
462            long alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID);
463            if (alarmId != Alarm.INVALID_ID) {
464                mScrollToAlarmId = alarmId;
465                if (mCursorLoader != null && mCursorLoader.isStarted()) {
466                    // We need to force a reload here to make sure we have the latest view
467                    // of the data to scroll to.
468                    mCursorLoader.forceLoad();
469                }
470            }
471
472            // Remove the SCROLL_TO_ALARM extra now that we've processed it.
473            intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA);
474        }
475
476        // Make sure to use the child FragmentManager. We have to use that one for the
477        // case where an intent comes in telling the activity to load the timepicker,
478        // which means we have to use that one everywhere so that the fragment can get
479        // correctly picked up here if it's open.
480        TimePickerDialog tpd = (TimePickerDialog) getChildFragmentManager().
481                findFragmentByTag(AlarmUtils.FRAG_TAG_TIME_PICKER);
482        if (tpd != null) {
483            // The dialog is already open so we need to set the listener again.
484            tpd.setOnTimeSetListener(this);
485        }
486    }
487
488    private void hideUndoBar(boolean animate, MotionEvent event) {
489        if (mUndoBar != null) {
490            mUndoFrame.setVisibility(View.GONE);
491            if (event != null && mUndoBar.isEventInToastBar(event)) {
492                // Avoid touches inside the undo bar.
493                return;
494            }
495            mUndoBar.hide(animate);
496        }
497        mDeletedAlarm = null;
498        mUndoShowing = false;
499    }
500
501    private void showUndoBar() {
502        mUndoFrame.setVisibility(View.VISIBLE);
503        mUndoBar.show(new ActionableToastBar.ActionClickedListener() {
504            @Override
505            public void onActionClicked() {
506                asyncAddAlarm(mDeletedAlarm);
507                mDeletedAlarm = null;
508                mUndoShowing = false;
509            }
510        }, 0, getResources().getString(R.string.alarm_deleted), true, R.string.alarm_undo, true);
511    }
512
513    @Override
514    public void onSaveInstanceState(Bundle outState) {
515        super.onSaveInstanceState(outState);
516        outState.putLongArray(KEY_EXPANDED_IDS, mAdapter.getExpandedArray());
517        outState.putLongArray(KEY_REPEAT_CHECKED_IDS, mAdapter.getRepeatArray());
518        outState.putLongArray(KEY_SELECTED_ALARMS, mAdapter.getSelectedAlarmsArray());
519        outState.putBundle(KEY_RINGTONE_TITLE_CACHE, mRingtoneTitleCache);
520        outState.putParcelable(KEY_DELETED_ALARM, mDeletedAlarm);
521        outState.putBoolean(KEY_UNDO_SHOWING, mUndoShowing);
522        outState.putBundle(KEY_PREVIOUS_DAY_MAP, mAdapter.getPreviousDaysOfWeekMap());
523        outState.putParcelable(KEY_SELECTED_ALARM, mSelectedAlarm);
524    }
525
526    @Override
527    public void onDestroy() {
528        super.onDestroy();
529        ToastMaster.cancelToast();
530    }
531
532    @Override
533    public void onPause() {
534        super.onPause();
535        // When the user places the app in the background by pressing "home",
536        // dismiss the toast bar. However, since there is no way to determine if
537        // home was pressed, just dismiss any existing toast bar when restarting
538        // the app.
539        hideUndoBar(false, null);
540    }
541
542    // Callback used by TimePickerDialog
543    @Override
544    public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute) {
545        if (mSelectedAlarm == null) {
546            // If mSelectedAlarm is null then we're creating a new alarm.
547            Alarm a = new Alarm();
548            a.alert = RingtoneManager.getActualDefaultRingtoneUri(getActivity(),
549                    RingtoneManager.TYPE_ALARM);
550            if (a.alert == null) {
551                a.alert = Uri.parse("content://settings/system/alarm_alert");
552            }
553            a.hour = hourOfDay;
554            a.minutes = minute;
555            a.enabled = true;
556            asyncAddAlarm(a);
557        } else {
558            mSelectedAlarm.hour = hourOfDay;
559            mSelectedAlarm.minutes = minute;
560            mSelectedAlarm.enabled = true;
561            mScrollToAlarmId = mSelectedAlarm.id;
562            asyncUpdateAlarm(mSelectedAlarm, true);
563            mSelectedAlarm = null;
564        }
565    }
566
567    private void showLabelDialog(final Alarm alarm) {
568        final FragmentTransaction ft = getFragmentManager().beginTransaction();
569        final Fragment prev = getFragmentManager().findFragmentByTag("label_dialog");
570        if (prev != null) {
571            ft.remove(prev);
572        }
573        ft.addToBackStack(null);
574
575        // Create and show the dialog.
576        final LabelDialogFragment newFragment =
577                LabelDialogFragment.newInstance(alarm, alarm.label, getTag());
578        newFragment.show(ft, "label_dialog");
579    }
580
581    public void setLabel(Alarm alarm, String label) {
582        alarm.label = label;
583        asyncUpdateAlarm(alarm, false);
584    }
585
586    @Override
587    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
588        return Alarm.getAlarmsCursorLoader(getActivity());
589    }
590
591    @Override
592    public void onLoadFinished(Loader<Cursor> cursorLoader, final Cursor data) {
593        mAdapter.swapCursor(data);
594        if (mScrollToAlarmId != -1) {
595            scrollToAlarm(mScrollToAlarmId);
596            mScrollToAlarmId = -1;
597        }
598    }
599
600    /**
601     * Scroll to alarm with given alarm id.
602     *
603     * @param alarmId The alarm id to scroll to.
604     */
605    private void scrollToAlarm(long alarmId) {
606        int alarmPosition = -1;
607        for (int i = 0; i < mAdapter.getCount(); i++) {
608            long id = mAdapter.getItemId(i);
609            if (id == alarmId) {
610                alarmPosition = i;
611                break;
612            }
613        }
614
615        if (alarmPosition >= 0) {
616            mAdapter.setNewAlarm(alarmId);
617            mAlarmsList.smoothScrollToPositionFromTop(alarmPosition, 0);
618        } else {
619            // Trying to display a deleted alarm should only happen from a missed notification for
620            // an alarm that has been marked deleted after use.
621            Context context = getActivity().getApplicationContext();
622            Toast toast = Toast.makeText(context, R.string.missed_alarm_has_been_deleted,
623                    Toast.LENGTH_LONG);
624            ToastMaster.setToast(toast);
625            toast.show();
626        }
627    }
628
629    @Override
630    public void onLoaderReset(Loader<Cursor> cursorLoader) {
631        mAdapter.swapCursor(null);
632    }
633
634    private void launchRingTonePicker(Alarm alarm) {
635        mSelectedAlarm = alarm;
636        Uri oldRingtone = Alarm.NO_RINGTONE_URI.equals(alarm.alert) ? null : alarm.alert;
637        final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
638        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, oldRingtone);
639        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM);
640        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
641        startActivityForResult(intent, REQUEST_CODE_RINGTONE);
642    }
643
644    private void saveRingtoneUri(Intent intent) {
645        Uri uri = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
646        if (uri == null) {
647            uri = Alarm.NO_RINGTONE_URI;
648        }
649        mSelectedAlarm.alert = uri;
650
651        // Save the last selected ringtone as the default for new alarms
652        if (!Alarm.NO_RINGTONE_URI.equals(uri)) {
653            RingtoneManager.setActualDefaultRingtoneUri(
654                    getActivity(), RingtoneManager.TYPE_ALARM, uri);
655        }
656        asyncUpdateAlarm(mSelectedAlarm, false);
657    }
658
659    @Override
660    public void onActivityResult(int requestCode, int resultCode, Intent data) {
661        if (resultCode == Activity.RESULT_OK) {
662            switch (requestCode) {
663                case REQUEST_CODE_RINGTONE:
664                    saveRingtoneUri(data);
665                    break;
666                default:
667                    Log.w("Unhandled request code in onActivityResult: " + requestCode);
668            }
669        }
670    }
671
672    public class AlarmItemAdapter extends CursorAdapter {
673        private static final int EXPAND_DURATION = 300;
674        private static final int COLLAPSE_DURATION = 250;
675
676        private final Context mContext;
677        private final LayoutInflater mFactory;
678        private final String[] mShortWeekDayStrings;
679        private final String[] mLongWeekDayStrings;
680        private final int mColorLit;
681        private final int mColorDim;
682        private final int mBackgroundColorExpanded;
683        private final int mBackgroundColor;
684        private final Typeface mRobotoNormal;
685        private final Typeface mRobotoBold;
686        private final ListView mList;
687
688        private final HashSet<Long> mExpanded = new HashSet<Long>();
689        private final HashSet<Long> mRepeatChecked = new HashSet<Long>();
690        private final HashSet<Long> mSelectedAlarms = new HashSet<Long>();
691        private Bundle mPreviousDaysOfWeekMap = new Bundle();
692
693        private final boolean mHasVibrator;
694        private final int mCollapseExpandHeight;
695
696        // This determines the order in which it is shown and processed in the UI.
697        private final int[] DAY_ORDER = new int[] {
698                Calendar.SUNDAY,
699                Calendar.MONDAY,
700                Calendar.TUESDAY,
701                Calendar.WEDNESDAY,
702                Calendar.THURSDAY,
703                Calendar.FRIDAY,
704                Calendar.SATURDAY,
705        };
706
707        public class ItemHolder {
708
709            // views for optimization
710            LinearLayout alarmItem;
711            TextTime clock;
712            Switch onoff;
713            TextView daysOfWeek;
714            TextView label;
715            ImageView delete;
716            View expandArea;
717            View summary;
718            TextView clickableLabel;
719            CheckBox repeat;
720            LinearLayout repeatDays;
721            ViewGroup[] dayButtonParents = new ViewGroup[7];
722            ToggleButton[] dayButtons = new ToggleButton[7];
723            CheckBox vibrate;
724            TextView ringtone;
725            View hairLine;
726            View arrow;
727            View collapseExpandArea;
728            View footerFiller;
729
730            // Other states
731            Alarm alarm;
732        }
733
734        // Used for scrolling an expanded item in the list to make sure it is fully visible.
735        private long mScrollAlarmId = -1;
736        private final Runnable mScrollRunnable = new Runnable() {
737            @Override
738            public void run() {
739                if (mScrollAlarmId != -1) {
740                    View v = getViewById(mScrollAlarmId);
741                    if (v != null) {
742                        Rect rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
743                        mList.requestChildRectangleOnScreen(v, rect, false);
744                    }
745                    mScrollAlarmId = -1;
746                }
747            }
748        };
749
750        public AlarmItemAdapter(Context context, long[] expandedIds, long[] repeatCheckedIds,
751                long[] selectedAlarms, Bundle previousDaysOfWeekMap, ListView list) {
752            super(context, null, 0);
753            mContext = context;
754            mFactory = LayoutInflater.from(context);
755            mList = list;
756
757            DateFormatSymbols dfs = new DateFormatSymbols();
758            mShortWeekDayStrings = dfs.getShortWeekdays();
759            mLongWeekDayStrings = dfs.getWeekdays();
760
761            Resources res = mContext.getResources();
762            mColorLit = res.getColor(R.color.clock_white);
763            mColorDim = res.getColor(R.color.clock_gray);
764            mBackgroundColorExpanded = res.getColor(R.color.alarm_whiteish);
765            mBackgroundColor = R.drawable.alarm_background_normal;
766
767            mRobotoBold = Typeface.create("sans-serif-condensed", Typeface.BOLD);
768            mRobotoNormal = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
769
770            if (expandedIds != null) {
771                buildHashSetFromArray(expandedIds, mExpanded);
772            }
773            if (repeatCheckedIds != null) {
774                buildHashSetFromArray(repeatCheckedIds, mRepeatChecked);
775            }
776            if (previousDaysOfWeekMap != null) {
777                mPreviousDaysOfWeekMap = previousDaysOfWeekMap;
778            }
779            if (selectedAlarms != null) {
780                buildHashSetFromArray(selectedAlarms, mSelectedAlarms);
781            }
782
783            mHasVibrator = ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE))
784                    .hasVibrator();
785
786            mCollapseExpandHeight = (int) res.getDimension(R.dimen.collapse_expand_height);
787        }
788
789        public void removeSelectedId(int id) {
790            mSelectedAlarms.remove(id);
791        }
792
793        @Override
794        public View getView(int position, View convertView, ViewGroup parent) {
795            if (!getCursor().moveToPosition(position)) {
796                // May happen if the last alarm was deleted and the cursor refreshed while the
797                // list is updated.
798                Log.v("couldn't move cursor to position " + position);
799                return null;
800            }
801            View v;
802            if (convertView == null) {
803                v = newView(mContext, getCursor(), parent);
804            } else {
805                // TODO temporary hack to prevent the convertView from not having stuff we need.
806                boolean badConvertView = convertView.findViewById(R.id.digital_clock) == null;
807                // Do a translation check to test for animation. Change this to something more
808                // reliable and robust in the future.
809                if (convertView.getTranslationX() != 0 || convertView.getTranslationY() != 0 ||
810                        badConvertView) {
811                    // view was animated, reset
812                    v = newView(mContext, getCursor(), parent);
813                } else {
814                    v = convertView;
815                }
816            }
817            bindView(v, mContext, getCursor());
818            ItemHolder holder = (ItemHolder) v.getTag();
819
820            // We need the footer for the last element of the array to allow the user to scroll
821            // the item beyond the bottom button bar, which obscures the view.
822            holder.footerFiller.setVisibility(position < getCount() - 1 ? View.GONE : View.VISIBLE);
823            return v;
824        }
825
826        @Override
827        public View newView(Context context, Cursor cursor, ViewGroup parent) {
828            final View view = mFactory.inflate(R.layout.alarm_time, parent, false);
829            setNewHolder(view);
830            return view;
831        }
832
833        /**
834         * In addition to changing the data set for the alarm list, swapCursor is now also
835         * responsible for preparing the list view's pre-draw operation for any animations that
836         * need to occur if an alarm was removed or added.
837         */
838        @Override
839        public synchronized Cursor swapCursor(Cursor cursor) {
840            Cursor c = super.swapCursor(cursor);
841
842            if (mItemIdTopMap.isEmpty() && mAddedAlarm == null) {
843                return c;
844            }
845
846            final ListView list = mAlarmsList;
847            final ViewTreeObserver observer = list.getViewTreeObserver();
848
849            /*
850             * Add a pre-draw listener to the observer to prepare for any possible animations to
851             * the alarms within the list view.  The animations will occur if an alarm has been
852             * removed or added.
853             *
854             * For alarm removal, the remaining children should all retain their initial starting
855             * positions, and transition to their new positions.
856             *
857             * For alarm addition, the other children should all retain their initial starting
858             * positions, transition to their new positions, and at the end of that transition, the
859             * newly added alarm should appear in the designated space.
860             */
861            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
862
863                private View mAddedView;
864
865                @Override
866                public boolean onPreDraw() {
867                    // Remove the pre-draw listener, as this only needs to occur once.
868                    if (observer.isAlive()) {
869                        observer.removeOnPreDrawListener(this);
870                    }
871                    boolean firstAnimation = true;
872                    int firstVisiblePosition = list.getFirstVisiblePosition();
873
874                    // Iterate through the children to prepare the add/remove animation.
875                    for (int i = 0; i< list.getChildCount(); i++) {
876                        final View child = list.getChildAt(i);
877
878                        int position = firstVisiblePosition + i;
879                        long itemId = mAdapter.getItemId(position);
880
881                        // If this is the added alarm, set it invisible for now, and animate later.
882                        if (mAddedAlarm != null && itemId == mAddedAlarm.id) {
883                            mAddedView = child;
884                            mAddedView.setAlpha(0.0f);
885                            continue;
886                        }
887
888                        // The cached starting position of the child view.
889                        Integer startTop = mItemIdTopMap.get(itemId);
890                        // The new starting position of the child view.
891                        int top = child.getTop();
892
893                        // If there is no cached starting position, determine whether the item has
894                        // come from the top of bottom of the list view.
895                        if (startTop == null) {
896                            int childHeight = child.getHeight() + list.getDividerHeight();
897                            startTop = top + (i > 0 ? childHeight : -childHeight);
898                        }
899
900                        Log.d("Start Top: " + startTop + ", Top: " + top);
901                        // If the starting position of the child view is different from the
902                        // current position, animate the child.
903                        if (startTop != top) {
904                            int delta = startTop - top;
905                            child.setTranslationY(delta);
906                            child.animate().setDuration(ANIMATION_DURATION).translationY(0);
907                            final View addedView = mAddedView;
908                            if (firstAnimation) {
909
910                                // If this is the first child being animated, then after the
911                                // animation is complete, and animate in the added alarm (if one
912                                // exists).
913                                child.animate().withEndAction(new Runnable() {
914
915                                    @Override
916                                    public void run() {
917
918
919                                        // If there was an added view, animate it in after
920                                        // the other views have animated.
921                                        if (addedView != null) {
922                                            addedView.animate().alpha(1.0f)
923                                                .setDuration(ANIMATION_DURATION)
924                                                .withEndAction(new Runnable() {
925
926                                                    @Override
927                                                    public void run() {
928                                                        // Re-enable the list after the add
929                                                        // animation is complete.
930                                                        list.setEnabled(true);
931                                                    }
932
933                                                });
934                                        } else {
935                                            // Re-enable the list after animations are complete.
936                                            list.setEnabled(true);
937                                        }
938                                    }
939
940                                });
941                                firstAnimation = false;
942                            }
943                        }
944                    }
945
946                    // If there were no child views (outside of a possible added view)
947                    // that require animation...
948                    if (firstAnimation) {
949                        if (mAddedView != null) {
950                            // If there is an added view, prepare animation for the added view.
951                            Log.d("Animating added view...");
952                            mAddedView.animate().alpha(1.0f)
953                                .setDuration(ANIMATION_DURATION)
954                                .withEndAction(new Runnable() {
955                                    @Override
956                                    public void run() {
957                                        // Re-enable the list after animations are complete.
958                                        list.setEnabled(true);
959                                    }
960                                });
961                        } else {
962                            // Re-enable the list after animations are complete.
963                            list.setEnabled(true);
964                        }
965                    }
966
967                    mAddedAlarm = null;
968                    mItemIdTopMap.clear();
969                    return true;
970                }
971            });
972            return c;
973        }
974
975        private void setNewHolder(View view) {
976            // standard view holder optimization
977            final ItemHolder holder = new ItemHolder();
978            holder.alarmItem = (LinearLayout) view.findViewById(R.id.alarm_item);
979            holder.clock = (TextTime) view.findViewById(R.id.digital_clock);
980            holder.onoff = (Switch) view.findViewById(R.id.onoff);
981            holder.onoff.setTypeface(mRobotoNormal);
982            holder.daysOfWeek = (TextView) view.findViewById(R.id.daysOfWeek);
983            holder.label = (TextView) view.findViewById(R.id.label);
984            holder.delete = (ImageView) view.findViewById(R.id.delete);
985            holder.summary = view.findViewById(R.id.summary);
986            holder.expandArea = view.findViewById(R.id.expand_area);
987            holder.hairLine = view.findViewById(R.id.hairline);
988            holder.arrow = view.findViewById(R.id.arrow);
989            holder.repeat = (CheckBox) view.findViewById(R.id.repeat_onoff);
990            holder.clickableLabel = (TextView) view.findViewById(R.id.edit_label);
991            holder.repeatDays = (LinearLayout) view.findViewById(R.id.repeat_days);
992            holder.collapseExpandArea = view.findViewById(R.id.collapse_expand);
993            holder.footerFiller = view.findViewById(R.id.alarm_footer_filler);
994            holder.footerFiller.setOnClickListener(new OnClickListener() {
995
996                @Override
997                public void onClick(View v) {
998                    // Do nothing.
999                }
1000            });
1001
1002            // Build button for each day.
1003            for (int i = 0; i < 7; i++) {
1004                final ViewGroup viewgroup = (ViewGroup) mFactory.inflate(R.layout.day_button,
1005                        holder.repeatDays, false);
1006                final ToggleButton button = (ToggleButton) viewgroup.getChildAt(0);
1007                final int dayToShowIndex = DAY_ORDER[i];
1008                button.setText(mShortWeekDayStrings[dayToShowIndex]);
1009                button.setTextOn(mShortWeekDayStrings[dayToShowIndex]);
1010                button.setTextOff(mShortWeekDayStrings[dayToShowIndex]);
1011                button.setContentDescription(mLongWeekDayStrings[dayToShowIndex]);
1012                holder.repeatDays.addView(viewgroup);
1013                holder.dayButtons[i] = button;
1014                holder.dayButtonParents[i] = viewgroup;
1015            }
1016            holder.vibrate = (CheckBox) view.findViewById(R.id.vibrate_onoff);
1017            holder.ringtone = (TextView) view.findViewById(R.id.choose_ringtone);
1018
1019            view.setTag(holder);
1020        }
1021
1022        @Override
1023        public void bindView(final View view, Context context, final Cursor cursor) {
1024            final Alarm alarm = new Alarm(cursor);
1025            Object tag = view.getTag();
1026            if (tag == null) {
1027                // The view was converted but somehow lost its tag.
1028                setNewHolder(view);
1029            }
1030            final ItemHolder itemHolder = (ItemHolder) tag;
1031            itemHolder.alarm = alarm;
1032
1033            // We must unset the listener first because this maybe a recycled view so changing the
1034            // state would affect the wrong alarm.
1035            itemHolder.onoff.setOnCheckedChangeListener(null);
1036            itemHolder.onoff.setChecked(alarm.enabled);
1037
1038            if (mSelectedAlarms.contains(itemHolder.alarm.id)) {
1039                itemHolder.alarmItem.setBackgroundColor(mBackgroundColorExpanded);
1040                setItemAlpha(itemHolder, true);
1041                itemHolder.onoff.setEnabled(false);
1042            } else {
1043                itemHolder.onoff.setEnabled(true);
1044                itemHolder.alarmItem.setBackgroundResource(mBackgroundColor);
1045                setItemAlpha(itemHolder, itemHolder.onoff.isChecked());
1046            }
1047            itemHolder.clock.setFormat(
1048                    (int)mContext.getResources().getDimension(R.dimen.alarm_label_size));
1049            itemHolder.clock.setTime(alarm.hour, alarm.minutes);
1050            itemHolder.clock.setClickable(true);
1051            itemHolder.clock.setOnClickListener(new View.OnClickListener() {
1052                @Override
1053                public void onClick(View view) {
1054                    mSelectedAlarm = itemHolder.alarm;
1055                    AlarmUtils.showTimeEditDialog(getChildFragmentManager(),
1056                            alarm, AlarmClockFragment.this
1057                            , DateFormat.is24HourFormat(getActivity()));
1058                    expandAlarm(itemHolder, true);
1059                    itemHolder.alarmItem.post(mScrollRunnable);
1060                }
1061            });
1062
1063            final CompoundButton.OnCheckedChangeListener onOffListener =
1064                    new CompoundButton.OnCheckedChangeListener() {
1065                        @Override
1066                        public void onCheckedChanged(CompoundButton compoundButton,
1067                                boolean checked) {
1068                            if (checked != alarm.enabled) {
1069                                setItemAlpha(itemHolder, checked);
1070                                alarm.enabled = checked;
1071                                asyncUpdateAlarm(alarm, alarm.enabled);
1072                            }
1073                        }
1074                    };
1075
1076            itemHolder.onoff.setOnCheckedChangeListener(onOffListener);
1077
1078            boolean expanded = isAlarmExpanded(alarm);
1079            itemHolder.expandArea.setVisibility(expanded? View.VISIBLE : View.GONE);
1080            itemHolder.summary.setVisibility(expanded? View.GONE : View.VISIBLE);
1081
1082            String labelSpace = "";
1083            // Set the repeat text or leave it blank if it does not repeat.
1084            final String daysOfWeekStr =
1085                    alarm.daysOfWeek.toString(AlarmClockFragment.this.getActivity(), false);
1086            if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) {
1087                itemHolder.daysOfWeek.setText(daysOfWeekStr);
1088                itemHolder.daysOfWeek.setContentDescription(alarm.daysOfWeek.toAccessibilityString(
1089                        AlarmClockFragment.this.getActivity()));
1090                itemHolder.daysOfWeek.setVisibility(View.VISIBLE);
1091                labelSpace = "  ";
1092                itemHolder.daysOfWeek.setOnClickListener(new View.OnClickListener() {
1093                    @Override
1094                    public void onClick(View view) {
1095                        expandAlarm(itemHolder, true);
1096                        itemHolder.alarmItem.post(mScrollRunnable);
1097                    }
1098                });
1099
1100            } else {
1101                itemHolder.daysOfWeek.setVisibility(View.GONE);
1102            }
1103
1104            if (alarm.label != null && alarm.label.length() != 0) {
1105                itemHolder.label.setText(alarm.label + labelSpace);
1106                itemHolder.label.setVisibility(View.VISIBLE);
1107                itemHolder.label.setContentDescription(
1108                        mContext.getResources().getString(R.string.label_description) + " "
1109                        + alarm.label);
1110                itemHolder.label.setOnClickListener(new View.OnClickListener() {
1111                    @Override
1112                    public void onClick(View view) {
1113                        expandAlarm(itemHolder, true);
1114                        itemHolder.alarmItem.post(mScrollRunnable);
1115                    }
1116                });
1117            } else {
1118                itemHolder.label.setVisibility(View.GONE);
1119            }
1120
1121            itemHolder.delete.setOnClickListener(new View.OnClickListener() {
1122                @Override
1123                public void onClick(View v) {
1124                    mDeletedAlarm = alarm;
1125
1126                    view.animate().setDuration(ANIMATION_DURATION).alpha(0).translationY(-1)
1127                    .withEndAction(new Runnable() {
1128
1129                        @Override
1130                        public void run() {
1131                            asyncDeleteAlarm(mDeletedAlarm, view);
1132                        }
1133                    });
1134                }
1135            });
1136
1137            if (expanded) {
1138                expandAlarm(itemHolder, false);
1139            } else {
1140                collapseAlarm(itemHolder, false);
1141            }
1142
1143            itemHolder.alarmItem.setOnClickListener(new View.OnClickListener() {
1144                @Override
1145                public void onClick(View view) {
1146                    if (isAlarmExpanded(alarm)) {
1147                        collapseAlarm(itemHolder, true);
1148                    } else {
1149                        expandAlarm(itemHolder, true);
1150                    }
1151                }
1152            });
1153        }
1154
1155        private void bindExpandArea(final ItemHolder itemHolder, final Alarm alarm) {
1156            // Views in here are not bound until the item is expanded.
1157
1158            if (alarm.label != null && alarm.label.length() > 0) {
1159                itemHolder.clickableLabel.setText(alarm.label);
1160                itemHolder.clickableLabel.setTextColor(mColorLit);
1161            } else {
1162                itemHolder.clickableLabel.setText(R.string.label);
1163                itemHolder.clickableLabel.setTextColor(mColorDim);
1164            }
1165            itemHolder.clickableLabel.setOnClickListener(new View.OnClickListener() {
1166                @Override
1167                public void onClick(View view) {
1168                    showLabelDialog(alarm);
1169                }
1170            });
1171
1172            if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeating()) {
1173                itemHolder.repeat.setChecked(true);
1174                itemHolder.repeatDays.setVisibility(View.VISIBLE);
1175            } else {
1176                itemHolder.repeat.setChecked(false);
1177                itemHolder.repeatDays.setVisibility(View.GONE);
1178            }
1179            itemHolder.repeat.setOnClickListener(new View.OnClickListener() {
1180                @Override
1181                public void onClick(View view) {
1182                    final boolean checked = ((CheckBox) view).isChecked();
1183                    if (checked) {
1184                        // Show days
1185                        itemHolder.repeatDays.setVisibility(View.VISIBLE);
1186                        mRepeatChecked.add(alarm.id);
1187
1188                        // Set all previously set days
1189                        // or
1190                        // Set all days if no previous.
1191                        final int bitSet = mPreviousDaysOfWeekMap.getInt("" + alarm.id);
1192                        alarm.daysOfWeek.setBitSet(bitSet);
1193                        if (!alarm.daysOfWeek.isRepeating()) {
1194                            alarm.daysOfWeek.setDaysOfWeek(true, DAY_ORDER);
1195                        }
1196                        updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek);
1197                    } else {
1198                        itemHolder.repeatDays.setVisibility(View.GONE);
1199                        mRepeatChecked.remove(alarm.id);
1200
1201                        // Remember the set days in case the user wants it back.
1202                        final int bitSet = alarm.daysOfWeek.getBitSet();
1203                        mPreviousDaysOfWeekMap.putInt("" + alarm.id, bitSet);
1204
1205                        // Remove all repeat days
1206                        alarm.daysOfWeek.clearAllDays();
1207                    }
1208                    asyncUpdateAlarm(alarm, false);
1209                }
1210            });
1211
1212            updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek);
1213            for (int i = 0; i < 7; i++) {
1214                final int buttonIndex = i;
1215
1216                itemHolder.dayButtonParents[i].setOnClickListener(new View.OnClickListener() {
1217                    @Override
1218                    public void onClick(View view) {
1219                        itemHolder.dayButtons[buttonIndex].toggle();
1220                        final boolean checked = itemHolder.dayButtons[buttonIndex].isChecked();
1221                        int day = DAY_ORDER[buttonIndex];
1222                        alarm.daysOfWeek.setDaysOfWeek(checked, day);
1223                        if (checked) {
1224                            turnOnDayOfWeek(itemHolder, buttonIndex);
1225                        } else {
1226                            turnOffDayOfWeek(itemHolder, buttonIndex);
1227
1228                            // See if this was the last day, if so, un-check the repeat box.
1229                            if (!alarm.daysOfWeek.isRepeating()) {
1230                                itemHolder.repeatDays.setVisibility(View.GONE);
1231                                itemHolder.repeat.setTextColor(mColorDim);
1232                                mRepeatChecked.remove(alarm.id);
1233
1234                                // Set history to no days, so it will be everyday when repeat is
1235                                // turned back on
1236                                mPreviousDaysOfWeekMap.putInt("" + alarm.id,
1237                                        DaysOfWeek.NO_DAYS_SET);
1238                            }
1239                        }
1240                        asyncUpdateAlarm(alarm, false);
1241                    }
1242                });
1243            }
1244
1245
1246            if (!mHasVibrator) {
1247                itemHolder.vibrate.setVisibility(View.INVISIBLE);
1248            } else {
1249                itemHolder.vibrate.setVisibility(View.VISIBLE);
1250                if (!alarm.vibrate) {
1251                    itemHolder.vibrate.setChecked(false);
1252                    itemHolder.vibrate.setTextColor(mColorDim);
1253                } else {
1254                    itemHolder.vibrate.setChecked(true);
1255                    itemHolder.vibrate.setTextColor(mColorLit);
1256                }
1257            }
1258
1259            itemHolder.vibrate.setOnClickListener(new View.OnClickListener() {
1260                @Override
1261                public void onClick(View v) {
1262                    final boolean checked = ((CheckBox) v).isChecked();
1263                    if (checked) {
1264                        itemHolder.vibrate.setTextColor(mColorLit);
1265                    } else {
1266                        itemHolder.vibrate.setTextColor(mColorDim);
1267                    }
1268                    alarm.vibrate = checked;
1269                    asyncUpdateAlarm(alarm, false);
1270                }
1271            });
1272
1273            final String ringtone;
1274            if (Alarm.NO_RINGTONE_URI.equals(alarm.alert)) {
1275                ringtone = mContext.getResources().getString(R.string.silent_alarm_summary);
1276            } else {
1277                ringtone = getRingToneTitle(alarm.alert);
1278            }
1279            itemHolder.ringtone.setText(ringtone);
1280            itemHolder.ringtone.setContentDescription(
1281                    mContext.getResources().getString(R.string.ringtone_description) + " "
1282                            + ringtone);
1283            itemHolder.ringtone.setOnClickListener(new View.OnClickListener() {
1284                @Override
1285                public void onClick(View view) {
1286                    launchRingTonePicker(alarm);
1287                }
1288            });
1289        }
1290
1291        // Sets the alpha of the item except the on/off switch. This gives a visual effect
1292        // for enabled/disabled alarm while leaving the on/off switch more visible
1293        private void setItemAlpha(ItemHolder holder, boolean enabled) {
1294            float alpha = enabled ? 1f : 0.5f;
1295            holder.clock.setAlpha(alpha);
1296            holder.summary.setAlpha(alpha);
1297            holder.expandArea.setAlpha(alpha);
1298            holder.delete.setAlpha(alpha);
1299            holder.daysOfWeek.setAlpha(alpha);
1300        }
1301
1302        private void updateDaysOfWeekButtons(ItemHolder holder, DaysOfWeek daysOfWeek) {
1303            HashSet<Integer> setDays = daysOfWeek.getSetDays();
1304            for (int i = 0; i < 7; i++) {
1305                if (setDays.contains(DAY_ORDER[i])) {
1306                    turnOnDayOfWeek(holder, i);
1307                } else {
1308                    turnOffDayOfWeek(holder, i);
1309                }
1310            }
1311        }
1312
1313        public void toggleSelectState(View v) {
1314            // long press could be on the parent view or one of its childs, so find the parent view
1315            v = getTopParent(v);
1316            if (v != null) {
1317                long id = ((ItemHolder)v.getTag()).alarm.id;
1318                if (mSelectedAlarms.contains(id)) {
1319                    mSelectedAlarms.remove(id);
1320                } else {
1321                    mSelectedAlarms.add(id);
1322                }
1323            }
1324        }
1325
1326        private View getTopParent(View v) {
1327            while (v != null && v.getId() != R.id.alarm_item) {
1328                v = (View) v.getParent();
1329            }
1330            return v;
1331        }
1332
1333        public int getSelectedItemsNum() {
1334            return mSelectedAlarms.size();
1335        }
1336
1337        private void turnOffDayOfWeek(ItemHolder holder, int dayIndex) {
1338            holder.dayButtons[dayIndex].setChecked(false);
1339            holder.dayButtons[dayIndex].setTextColor(mColorDim);
1340            holder.dayButtons[dayIndex].setTypeface(mRobotoNormal);
1341        }
1342
1343        private void turnOnDayOfWeek(ItemHolder holder, int dayIndex) {
1344            holder.dayButtons[dayIndex].setChecked(true);
1345            holder.dayButtons[dayIndex].setTextColor(mColorLit);
1346            holder.dayButtons[dayIndex].setTypeface(mRobotoBold);
1347        }
1348
1349
1350        /**
1351         * Does a read-through cache for ringtone titles.
1352         *
1353         * @param uri The uri of the ringtone.
1354         * @return The ringtone title. {@literal null} if no matching ringtone found.
1355         */
1356        private String getRingToneTitle(Uri uri) {
1357            // Try the cache first
1358            String title = mRingtoneTitleCache.getString(uri.toString());
1359            if (title == null) {
1360                // This is slow because a media player is created during Ringtone object creation.
1361                Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri);
1362                title = ringTone.getTitle(mContext);
1363                if (title != null) {
1364                    mRingtoneTitleCache.putString(uri.toString(), title);
1365                }
1366            }
1367            return title;
1368        }
1369
1370        public void setNewAlarm(long alarmId) {
1371            mExpanded.add(alarmId);
1372        }
1373
1374        /**
1375         * Expands the alarm for editing.
1376         *
1377         * @param itemHolder The item holder instance.
1378         */
1379        private void expandAlarm(final ItemHolder itemHolder, boolean animate) {
1380            mExpanded.add(itemHolder.alarm.id);
1381            bindExpandArea(itemHolder, itemHolder.alarm);
1382            // Scroll the view to make sure it is fully viewed
1383            mScrollAlarmId = itemHolder.alarm.id;
1384
1385            // Save the starting height so we can animate from this value.
1386            final int startingHeight = itemHolder.alarmItem.getHeight();
1387
1388            // Set the expand area to visible so we can measure the height to animate to.
1389            itemHolder.alarmItem.setBackgroundColor(mBackgroundColorExpanded);
1390            itemHolder.expandArea.setVisibility(View.VISIBLE);
1391
1392            if (!animate) {
1393                // Set the "end" layout and don't do the animation.
1394                itemHolder.arrow.setRotation(180);
1395                // We need to translate the hairline up, so the height of the collapseArea
1396                // needs to be measured to know how high to translate it.
1397                final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver();
1398                observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1399                    @Override
1400                    public boolean onPreDraw() {
1401                        // We don't want to continue getting called for every listview drawing.
1402                        if (observer.isAlive()) {
1403                            observer.removeOnPreDrawListener(this);
1404                        }
1405                        int hairlineHeight = itemHolder.hairLine.getHeight();
1406                        int collapseHeight =
1407                                itemHolder.collapseExpandArea.getHeight() - hairlineHeight;
1408                        itemHolder.hairLine.setTranslationY(-collapseHeight);
1409                        return true;
1410                    }
1411                });
1412                return;
1413            }
1414
1415            // Add an onPreDrawListener, which gets called after measurement but before the draw.
1416            // This way we can check the height we need to animate to before any drawing.
1417            // Note the series of events:
1418            //  * expandArea is set to VISIBLE, which causes a layout pass
1419            //  * the view is measured, and our onPreDrawListener is called
1420            //  * we set up the animation using the start and end values.
1421            //  * the height is set back to the starting point so it can be animated down.
1422            //  * request another layout pass.
1423            //  * return false so that onDraw() is not called for the single frame before
1424            //    the animations have started.
1425            final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver();
1426            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1427                @Override
1428                public boolean onPreDraw() {
1429                    // We don't want to continue getting called for every listview drawing.
1430                    if (observer.isAlive()) {
1431                        observer.removeOnPreDrawListener(this);
1432                    }
1433                    // Calculate some values to help with the animation.
1434                    final int endingHeight = itemHolder.alarmItem.getHeight();
1435                    final int distance = endingHeight - startingHeight;
1436                    final int collapseHeight = itemHolder.collapseExpandArea.getHeight();
1437                    int hairlineHeight = itemHolder.hairLine.getHeight();
1438                    final int hairlineDistance = collapseHeight - hairlineHeight;
1439
1440                    // Set the height back to the start state of the animation.
1441                    itemHolder.alarmItem.getLayoutParams().height = startingHeight;
1442                    // To allow the expandArea to glide in with the expansion animation, set a
1443                    // negative top margin, which will animate down to a margin of 0 as the height
1444                    // is increased.
1445                    // Note that we need to maintain the bottom margin as a fixed value (instead of
1446                    // just using a listview, to allow for a flatter hierarchy) to fit the bottom
1447                    // bar underneath.
1448                    FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1449                            itemHolder.expandArea.getLayoutParams();
1450                    expandParams.setMargins(0, -distance, 0, collapseHeight);
1451                    itemHolder.alarmItem.requestLayout();
1452
1453                    // Set up the animator to animate the expansion.
1454                    ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
1455                            .setDuration(EXPAND_DURATION);
1456                    animator.setInterpolator(mExpandInterpolator);
1457                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1458                        @Override
1459                        public void onAnimationUpdate(ValueAnimator animator) {
1460                            Float value = (Float) animator.getAnimatedValue();
1461
1462                            // For each value from 0 to 1, animate the various parts of the layout.
1463                            itemHolder.alarmItem.getLayoutParams().height =
1464                                    (int) (value * distance + startingHeight);
1465                            FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1466                                    itemHolder.expandArea.getLayoutParams();
1467                            expandParams.setMargins(
1468                                    0, (int) -((1 - value) * distance), 0, collapseHeight);
1469                            itemHolder.arrow.setRotation(180 * value);
1470                            itemHolder.hairLine.setTranslationY(-hairlineDistance * value);
1471                            itemHolder.summary.setAlpha(1 - value);
1472
1473                            itemHolder.alarmItem.requestLayout();
1474                        }
1475                    });
1476                    // Set everything to their final values when the animation's done.
1477                    animator.addListener(new AnimatorListener() {
1478                        @Override
1479                        public void onAnimationEnd(Animator animation) {
1480                            // Set it back to wrap content since we'd explicitly set the height.
1481                            itemHolder.alarmItem.getLayoutParams().height =
1482                                    LayoutParams.WRAP_CONTENT;
1483                            itemHolder.arrow.setRotation(180);
1484                            itemHolder.hairLine.setTranslationY(-hairlineDistance);
1485                            itemHolder.summary.setVisibility(View.GONE);
1486                        }
1487
1488                        @Override
1489                        public void onAnimationCancel(Animator animation) {
1490                            // TODO we may have to deal with cancelations of the animation.
1491                        }
1492
1493                        @Override
1494                        public void onAnimationRepeat(Animator animation) { }
1495                        @Override
1496                        public void onAnimationStart(Animator animation) { }
1497                    });
1498                    animator.start();
1499
1500                    // Return false so this draw does not occur to prevent the final frame from
1501                    // being drawn for the single frame before the animations start.
1502                    return false;
1503                }
1504            });
1505        }
1506
1507        private boolean isAlarmExpanded(Alarm alarm) {
1508            return mExpanded.contains(alarm.id);
1509        }
1510
1511        private void collapseAlarm(final ItemHolder itemHolder, boolean animate) {
1512            mExpanded.remove(itemHolder.alarm.id);
1513
1514            // Save the starting height so we can animate from this value.
1515            final int startingHeight = itemHolder.alarmItem.getHeight();
1516
1517            // Set the expand area to gone so we can measure the height to animate to.
1518            itemHolder.alarmItem.setBackgroundResource(mBackgroundColor);
1519            itemHolder.expandArea.setVisibility(View.GONE);
1520
1521            if (!animate) {
1522                // Set the "end" layout and don't do the animation.
1523                itemHolder.arrow.setRotation(0);
1524                itemHolder.hairLine.setTranslationY(0);
1525                return;
1526            }
1527
1528            // Add an onPreDrawListener, which gets called after measurement but before the draw.
1529            // This way we can check the height we need to animate to before any drawing.
1530            // Note the series of events:
1531            //  * expandArea is set to GONE, which causes a layout pass
1532            //  * the view is measured, and our onPreDrawListener is called
1533            //  * we set up the animation using the start and end values.
1534            //  * expandArea is set to VISIBLE again so it can be shown animating.
1535            //  * request another layout pass.
1536            //  * return false so that onDraw() is not called for the single frame before
1537            //    the animations have started.
1538            final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver();
1539            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1540                @Override
1541                public boolean onPreDraw() {
1542                    if (observer.isAlive()) {
1543                        observer.removeOnPreDrawListener(this);
1544                    }
1545
1546                    // Calculate some values to help with the animation.
1547                    final int endingHeight = itemHolder.alarmItem.getHeight();
1548                    final int distance = endingHeight - startingHeight;
1549                    int hairlineHeight = itemHolder.hairLine.getHeight();
1550                    final int hairlineDistance = mCollapseExpandHeight - hairlineHeight;
1551
1552                    // Re-set the visibilities for the start state of the animation.
1553                    itemHolder.expandArea.setVisibility(View.VISIBLE);
1554                    itemHolder.summary.setVisibility(View.VISIBLE);
1555                    itemHolder.summary.setAlpha(1);
1556
1557                    // Set up the animator to animate the expansion.
1558                    ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
1559                            .setDuration(COLLAPSE_DURATION);
1560                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1561                        @Override
1562                        public void onAnimationUpdate(ValueAnimator animator) {
1563                            Float value = (Float) animator.getAnimatedValue();
1564
1565                            // For each value from 0 to 1, animate the various parts of the layout.
1566                            itemHolder.alarmItem.getLayoutParams().height =
1567                                    (int) (value * distance + startingHeight);
1568                            FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1569                                    itemHolder.expandArea.getLayoutParams();
1570                            expandParams.setMargins(
1571                                    0, (int) (value * distance), 0, mCollapseExpandHeight);
1572                            itemHolder.arrow.setRotation(180 * (1 - value));
1573                            itemHolder.hairLine.setTranslationY(-hairlineDistance * (1 - value));
1574                            itemHolder.summary.setAlpha(value);
1575
1576                            itemHolder.alarmItem.requestLayout();
1577                        }
1578                    });
1579                    animator.setInterpolator(mCollapseInterpolator);
1580                    // Set everything to their final values when the animation's done.
1581                    animator.addListener(new AnimatorListener() {
1582                        @Override
1583                        public void onAnimationEnd(Animator animation) {
1584                            // Set it back to wrap content since we'd explicitly set the height.
1585                            itemHolder.alarmItem.getLayoutParams().height =
1586                                    LayoutParams.WRAP_CONTENT;
1587
1588                            FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1589                                    itemHolder.expandArea.getLayoutParams();
1590                            expandParams.setMargins(0, 0, 0, mCollapseExpandHeight);
1591
1592                            itemHolder.expandArea.setVisibility(View.GONE);
1593                            itemHolder.arrow.setRotation(0);
1594                            itemHolder.hairLine.setTranslationY(0);
1595                        }
1596
1597                        @Override
1598                        public void onAnimationCancel(Animator animation) {
1599                            // TODO we may have to deal with cancelations of the animation.
1600                        }
1601
1602                        @Override
1603                        public void onAnimationRepeat(Animator animation) { }
1604                        @Override
1605                        public void onAnimationStart(Animator animation) { }
1606                    });
1607                    animator.start();
1608
1609                    return false;
1610                }
1611            });
1612        }
1613
1614        @Override
1615        public int getViewTypeCount() {
1616            return 1;
1617        }
1618
1619        private View getViewById(long id) {
1620            for (int i = 0; i < mList.getCount(); i++) {
1621                View v = mList.getChildAt(i);
1622                if (v != null) {
1623                    ItemHolder h = (ItemHolder)(v.getTag());
1624                    if (h != null && h.alarm.id == id) {
1625                        return v;
1626                    }
1627                }
1628            }
1629            return null;
1630        }
1631
1632        public long[] getExpandedArray() {
1633            int index = 0;
1634            long[] ids = new long[mExpanded.size()];
1635            for (long id : mExpanded) {
1636                ids[index] = id;
1637                index++;
1638            }
1639            return ids;
1640        }
1641
1642        public long[] getSelectedAlarmsArray() {
1643            int index = 0;
1644            long[] ids = new long[mSelectedAlarms.size()];
1645            for (long id : mSelectedAlarms) {
1646                ids[index] = id;
1647                index++;
1648            }
1649            return ids;
1650        }
1651
1652        public long[] getRepeatArray() {
1653            int index = 0;
1654            long[] ids = new long[mRepeatChecked.size()];
1655            for (long id : mRepeatChecked) {
1656                ids[index] = id;
1657                index++;
1658            }
1659            return ids;
1660        }
1661
1662        public Bundle getPreviousDaysOfWeekMap() {
1663            return mPreviousDaysOfWeekMap;
1664        }
1665
1666        private void buildHashSetFromArray(long[] ids, HashSet<Long> set) {
1667            for (long id : ids) {
1668                set.add(id);
1669            }
1670        }
1671    }
1672
1673    private void startCreatingAlarm() {
1674        // Set the "selected" alarm as null, and we'll create the new one when the timepicker
1675        // comes back.
1676        mSelectedAlarm = null;
1677        AlarmUtils.showTimeEditDialog(getChildFragmentManager(),
1678                null, AlarmClockFragment.this, DateFormat.is24HourFormat(getActivity()));
1679    }
1680
1681    private static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) {
1682        ContentResolver cr = context.getContentResolver();
1683        AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
1684        newInstance = AlarmInstance.addInstance(cr, newInstance);
1685        // Register instance to state manager
1686        AlarmStateManager.registerInstance(context, newInstance, true);
1687        return newInstance;
1688    }
1689
1690    private void asyncDeleteAlarm(final Alarm alarm, final View viewToRemove) {
1691        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
1692        final AsyncTask<Void, Void, Void> deleteTask = new AsyncTask<Void, Void, Void>() {
1693            @Override
1694            public synchronized void onPreExecute() {
1695                if (viewToRemove == null) {
1696                    return;
1697                }
1698                // The alarm list needs to be disabled until the animation finishes to prevent
1699                // possible concurrency issues.  It becomes re-enabled after the animations have
1700                // completed.
1701                mAlarmsList.setEnabled(false);
1702
1703                // Store all of the current list view item positions in memory for animation.
1704                final ListView list = mAlarmsList;
1705                int firstVisiblePosition = list.getFirstVisiblePosition();
1706                for (int i=0; i<list.getChildCount(); i++) {
1707                    View child = list.getChildAt(i);
1708                    if (child != viewToRemove) {
1709                        int position = firstVisiblePosition + i;
1710                        long itemId = mAdapter.getItemId(position);
1711                        mItemIdTopMap.put(itemId, child.getTop());
1712                    }
1713                }
1714            }
1715
1716            @Override
1717            protected Void doInBackground(Void... parameters) {
1718                // Activity may be closed at this point , make sure data is still valid
1719                if (context != null && alarm != null) {
1720                    ContentResolver cr = context.getContentResolver();
1721                    AlarmStateManager.deleteAllInstances(context, alarm.id);
1722                    Alarm.deleteAlarm(cr, alarm.id);
1723                }
1724                return null;
1725            }
1726        };
1727        mUndoShowing = true;
1728        deleteTask.execute();
1729    }
1730
1731    private void asyncAddAlarm(final Alarm alarm) {
1732        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
1733        final AsyncTask<Void, Void, AlarmInstance> updateTask =
1734                new AsyncTask<Void, Void, AlarmInstance>() {
1735            @Override
1736            public synchronized void onPreExecute() {
1737                final ListView list = mAlarmsList;
1738                // The alarm list needs to be disabled until the animation finishes to prevent
1739                // possible concurrency issues.  It becomes re-enabled after the animations have
1740                // completed.
1741                mAlarmsList.setEnabled(false);
1742
1743                // Store all of the current list view item positions in memory for animation.
1744                int firstVisiblePosition = list.getFirstVisiblePosition();
1745                for (int i=0; i<list.getChildCount(); i++) {
1746                    View child = list.getChildAt(i);
1747                    int position = firstVisiblePosition + i;
1748                    long itemId = mAdapter.getItemId(position);
1749                    mItemIdTopMap.put(itemId, child.getTop());
1750                }
1751            }
1752
1753            @Override
1754            protected AlarmInstance doInBackground(Void... parameters) {
1755                if (context != null && alarm != null) {
1756                    ContentResolver cr = context.getContentResolver();
1757
1758                    // Add alarm to db
1759                    Alarm newAlarm = Alarm.addAlarm(cr, alarm);
1760                    mScrollToAlarmId = newAlarm.id;
1761
1762                    // Create and add instance to db
1763                    if (newAlarm.enabled) {
1764                        return setupAlarmInstance(context, newAlarm);
1765                    }
1766                }
1767                return null;
1768            }
1769
1770            @Override
1771            protected void onPostExecute(AlarmInstance instance) {
1772                if (instance != null) {
1773                    AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis());
1774                }
1775            }
1776        };
1777        updateTask.execute();
1778    }
1779
1780    private void asyncUpdateAlarm(final Alarm alarm, final boolean popToast) {
1781        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
1782        final AsyncTask<Void, Void, AlarmInstance> updateTask =
1783                new AsyncTask<Void, Void, AlarmInstance>() {
1784            @Override
1785            protected AlarmInstance doInBackground(Void ... parameters) {
1786                ContentResolver cr = context.getContentResolver();
1787
1788                // Dismiss all old instances
1789                AlarmStateManager.deleteAllInstances(context, alarm.id);
1790
1791                // Update alarm
1792                Alarm.updateAlarm(cr, alarm);
1793                if (alarm.enabled) {
1794                    return setupAlarmInstance(context, alarm);
1795                }
1796
1797                return null;
1798            }
1799
1800            @Override
1801            protected void onPostExecute(AlarmInstance instance) {
1802                if (popToast && instance != null) {
1803                    AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis());
1804                }
1805            }
1806        };
1807        updateTask.execute();
1808    }
1809
1810    @Override
1811    public boolean onTouch(View v, MotionEvent event) {
1812        hideUndoBar(true, event);
1813        return false;
1814    }
1815}
1816