AlarmClockFragment.java revision c677dfde1d276531c10efd8008b342318b04782e
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
1048            itemHolder.clock.setTime(alarm.hour, alarm.minutes);
1049            itemHolder.clock.setClickable(true);
1050            itemHolder.clock.setOnClickListener(new View.OnClickListener() {
1051                @Override
1052                public void onClick(View view) {
1053                    mSelectedAlarm = itemHolder.alarm;
1054                    AlarmUtils.showTimeEditDialog(getChildFragmentManager(),
1055                            alarm, AlarmClockFragment.this
1056                            , DateFormat.is24HourFormat(getActivity()));
1057                    expandAlarm(itemHolder, true);
1058                    itemHolder.alarmItem.post(mScrollRunnable);
1059                }
1060            });
1061
1062            final CompoundButton.OnCheckedChangeListener onOffListener =
1063                    new CompoundButton.OnCheckedChangeListener() {
1064                        @Override
1065                        public void onCheckedChanged(CompoundButton compoundButton,
1066                                boolean checked) {
1067                            if (checked != alarm.enabled) {
1068                                setItemAlpha(itemHolder, checked);
1069                                alarm.enabled = checked;
1070                                asyncUpdateAlarm(alarm, alarm.enabled);
1071                            }
1072                        }
1073                    };
1074
1075            itemHolder.onoff.setOnCheckedChangeListener(onOffListener);
1076
1077            boolean expanded = isAlarmExpanded(alarm);
1078            itemHolder.expandArea.setVisibility(expanded? View.VISIBLE : View.GONE);
1079            itemHolder.summary.setVisibility(expanded? View.GONE : View.VISIBLE);
1080
1081            String labelSpace = "";
1082            // Set the repeat text or leave it blank if it does not repeat.
1083            final String daysOfWeekStr =
1084                    alarm.daysOfWeek.toString(AlarmClockFragment.this.getActivity(), false);
1085            if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) {
1086                itemHolder.daysOfWeek.setText(daysOfWeekStr);
1087                itemHolder.daysOfWeek.setContentDescription(alarm.daysOfWeek.toAccessibilityString(
1088                        AlarmClockFragment.this.getActivity()));
1089                itemHolder.daysOfWeek.setVisibility(View.VISIBLE);
1090                labelSpace = "  ";
1091                itemHolder.daysOfWeek.setOnClickListener(new View.OnClickListener() {
1092                    @Override
1093                    public void onClick(View view) {
1094                        expandAlarm(itemHolder, true);
1095                        itemHolder.alarmItem.post(mScrollRunnable);
1096                    }
1097                });
1098
1099            } else {
1100                itemHolder.daysOfWeek.setVisibility(View.GONE);
1101            }
1102
1103            if (alarm.label != null && alarm.label.length() != 0) {
1104                itemHolder.label.setText(alarm.label + labelSpace);
1105                itemHolder.label.setVisibility(View.VISIBLE);
1106                itemHolder.label.setContentDescription(
1107                        mContext.getResources().getString(R.string.label_description) + " "
1108                        + alarm.label);
1109                itemHolder.label.setOnClickListener(new View.OnClickListener() {
1110                    @Override
1111                    public void onClick(View view) {
1112                        expandAlarm(itemHolder, true);
1113                        itemHolder.alarmItem.post(mScrollRunnable);
1114                    }
1115                });
1116            } else {
1117                itemHolder.label.setVisibility(View.GONE);
1118            }
1119
1120            itemHolder.delete.setOnClickListener(new View.OnClickListener() {
1121                @Override
1122                public void onClick(View v) {
1123                    mDeletedAlarm = alarm;
1124
1125                    view.animate().setDuration(ANIMATION_DURATION).alpha(0).translationY(-1)
1126                    .withEndAction(new Runnable() {
1127
1128                        @Override
1129                        public void run() {
1130                            asyncDeleteAlarm(mDeletedAlarm, view);
1131                        }
1132                    });
1133                }
1134            });
1135
1136            if (expanded) {
1137                expandAlarm(itemHolder, false);
1138            } else {
1139                collapseAlarm(itemHolder, false);
1140            }
1141
1142            itemHolder.alarmItem.setOnClickListener(new View.OnClickListener() {
1143                @Override
1144                public void onClick(View view) {
1145                    if (isAlarmExpanded(alarm)) {
1146                        collapseAlarm(itemHolder, true);
1147                    } else {
1148                        expandAlarm(itemHolder, true);
1149                    }
1150                }
1151            });
1152        }
1153
1154        private void bindExpandArea(final ItemHolder itemHolder, final Alarm alarm) {
1155            // Views in here are not bound until the item is expanded.
1156
1157            if (alarm.label != null && alarm.label.length() > 0) {
1158                itemHolder.clickableLabel.setText(alarm.label);
1159                itemHolder.clickableLabel.setTextColor(mColorLit);
1160            } else {
1161                itemHolder.clickableLabel.setText(R.string.label);
1162                itemHolder.clickableLabel.setTextColor(mColorDim);
1163            }
1164            itemHolder.clickableLabel.setOnClickListener(new View.OnClickListener() {
1165                @Override
1166                public void onClick(View view) {
1167                    showLabelDialog(alarm);
1168                }
1169            });
1170
1171            if (mRepeatChecked.contains(alarm.id) || itemHolder.alarm.daysOfWeek.isRepeating()) {
1172                itemHolder.repeat.setChecked(true);
1173                itemHolder.repeatDays.setVisibility(View.VISIBLE);
1174            } else {
1175                itemHolder.repeat.setChecked(false);
1176                itemHolder.repeatDays.setVisibility(View.GONE);
1177            }
1178            itemHolder.repeat.setOnClickListener(new View.OnClickListener() {
1179                @Override
1180                public void onClick(View view) {
1181                    final boolean checked = ((CheckBox) view).isChecked();
1182                    if (checked) {
1183                        // Show days
1184                        itemHolder.repeatDays.setVisibility(View.VISIBLE);
1185                        mRepeatChecked.add(alarm.id);
1186
1187                        // Set all previously set days
1188                        // or
1189                        // Set all days if no previous.
1190                        final int bitSet = mPreviousDaysOfWeekMap.getInt("" + alarm.id);
1191                        alarm.daysOfWeek.setBitSet(bitSet);
1192                        if (!alarm.daysOfWeek.isRepeating()) {
1193                            alarm.daysOfWeek.setDaysOfWeek(true, DAY_ORDER);
1194                        }
1195                        updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek);
1196                    } else {
1197                        itemHolder.repeatDays.setVisibility(View.GONE);
1198                        mRepeatChecked.remove(alarm.id);
1199
1200                        // Remember the set days in case the user wants it back.
1201                        final int bitSet = alarm.daysOfWeek.getBitSet();
1202                        mPreviousDaysOfWeekMap.putInt("" + alarm.id, bitSet);
1203
1204                        // Remove all repeat days
1205                        alarm.daysOfWeek.clearAllDays();
1206                    }
1207                    asyncUpdateAlarm(alarm, false);
1208                }
1209            });
1210
1211            updateDaysOfWeekButtons(itemHolder, alarm.daysOfWeek);
1212            for (int i = 0; i < 7; i++) {
1213                final int buttonIndex = i;
1214
1215                itemHolder.dayButtonParents[i].setOnClickListener(new View.OnClickListener() {
1216                    @Override
1217                    public void onClick(View view) {
1218                        itemHolder.dayButtons[buttonIndex].toggle();
1219                        final boolean checked = itemHolder.dayButtons[buttonIndex].isChecked();
1220                        int day = DAY_ORDER[buttonIndex];
1221                        alarm.daysOfWeek.setDaysOfWeek(checked, day);
1222                        if (checked) {
1223                            turnOnDayOfWeek(itemHolder, buttonIndex);
1224                        } else {
1225                            turnOffDayOfWeek(itemHolder, buttonIndex);
1226
1227                            // See if this was the last day, if so, un-check the repeat box.
1228                            if (!alarm.daysOfWeek.isRepeating()) {
1229                                itemHolder.repeatDays.setVisibility(View.GONE);
1230                                itemHolder.repeat.setTextColor(mColorDim);
1231                                mRepeatChecked.remove(alarm.id);
1232
1233                                // Set history to no days, so it will be everyday when repeat is
1234                                // turned back on
1235                                mPreviousDaysOfWeekMap.putInt("" + alarm.id,
1236                                        DaysOfWeek.NO_DAYS_SET);
1237                            }
1238                        }
1239                        asyncUpdateAlarm(alarm, false);
1240                    }
1241                });
1242            }
1243
1244
1245            if (!mHasVibrator) {
1246                itemHolder.vibrate.setVisibility(View.INVISIBLE);
1247            } else {
1248                itemHolder.vibrate.setVisibility(View.VISIBLE);
1249                if (!alarm.vibrate) {
1250                    itemHolder.vibrate.setChecked(false);
1251                    itemHolder.vibrate.setTextColor(mColorDim);
1252                } else {
1253                    itemHolder.vibrate.setChecked(true);
1254                    itemHolder.vibrate.setTextColor(mColorLit);
1255                }
1256            }
1257
1258            itemHolder.vibrate.setOnClickListener(new View.OnClickListener() {
1259                @Override
1260                public void onClick(View v) {
1261                    final boolean checked = ((CheckBox) v).isChecked();
1262                    if (checked) {
1263                        itemHolder.vibrate.setTextColor(mColorLit);
1264                    } else {
1265                        itemHolder.vibrate.setTextColor(mColorDim);
1266                    }
1267                    alarm.vibrate = checked;
1268                    asyncUpdateAlarm(alarm, false);
1269                }
1270            });
1271
1272            final String ringtone;
1273            if (Alarm.NO_RINGTONE_URI.equals(alarm.alert)) {
1274                ringtone = mContext.getResources().getString(R.string.silent_alarm_summary);
1275            } else {
1276                ringtone = getRingToneTitle(alarm.alert);
1277            }
1278            itemHolder.ringtone.setText(ringtone);
1279            itemHolder.ringtone.setContentDescription(
1280                    mContext.getResources().getString(R.string.ringtone_description) + " "
1281                            + ringtone);
1282            itemHolder.ringtone.setOnClickListener(new View.OnClickListener() {
1283                @Override
1284                public void onClick(View view) {
1285                    launchRingTonePicker(alarm);
1286                }
1287            });
1288        }
1289
1290        // Sets the alpha of the item except the on/off switch. This gives a visual effect
1291        // for enabled/disabled alarm while leaving the on/off switch more visible
1292        private void setItemAlpha(ItemHolder holder, boolean enabled) {
1293            float alpha = enabled ? 1f : 0.5f;
1294            holder.clock.setAlpha(alpha);
1295            holder.summary.setAlpha(alpha);
1296            holder.expandArea.setAlpha(alpha);
1297            holder.delete.setAlpha(alpha);
1298            holder.daysOfWeek.setAlpha(alpha);
1299        }
1300
1301        private void updateDaysOfWeekButtons(ItemHolder holder, DaysOfWeek daysOfWeek) {
1302            HashSet<Integer> setDays = daysOfWeek.getSetDays();
1303            for (int i = 0; i < 7; i++) {
1304                if (setDays.contains(DAY_ORDER[i])) {
1305                    turnOnDayOfWeek(holder, i);
1306                } else {
1307                    turnOffDayOfWeek(holder, i);
1308                }
1309            }
1310        }
1311
1312        public void toggleSelectState(View v) {
1313            // long press could be on the parent view or one of its childs, so find the parent view
1314            v = getTopParent(v);
1315            if (v != null) {
1316                long id = ((ItemHolder)v.getTag()).alarm.id;
1317                if (mSelectedAlarms.contains(id)) {
1318                    mSelectedAlarms.remove(id);
1319                } else {
1320                    mSelectedAlarms.add(id);
1321                }
1322            }
1323        }
1324
1325        private View getTopParent(View v) {
1326            while (v != null && v.getId() != R.id.alarm_item) {
1327                v = (View) v.getParent();
1328            }
1329            return v;
1330        }
1331
1332        public int getSelectedItemsNum() {
1333            return mSelectedAlarms.size();
1334        }
1335
1336        private void turnOffDayOfWeek(ItemHolder holder, int dayIndex) {
1337            holder.dayButtons[dayIndex].setChecked(false);
1338            holder.dayButtons[dayIndex].setTextColor(mColorDim);
1339            holder.dayButtons[dayIndex].setTypeface(mRobotoNormal);
1340        }
1341
1342        private void turnOnDayOfWeek(ItemHolder holder, int dayIndex) {
1343            holder.dayButtons[dayIndex].setChecked(true);
1344            holder.dayButtons[dayIndex].setTextColor(mColorLit);
1345            holder.dayButtons[dayIndex].setTypeface(mRobotoBold);
1346        }
1347
1348
1349        /**
1350         * Does a read-through cache for ringtone titles.
1351         *
1352         * @param uri The uri of the ringtone.
1353         * @return The ringtone title. {@literal null} if no matching ringtone found.
1354         */
1355        private String getRingToneTitle(Uri uri) {
1356            // Try the cache first
1357            String title = mRingtoneTitleCache.getString(uri.toString());
1358            if (title == null) {
1359                // This is slow because a media player is created during Ringtone object creation.
1360                Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri);
1361                title = ringTone.getTitle(mContext);
1362                if (title != null) {
1363                    mRingtoneTitleCache.putString(uri.toString(), title);
1364                }
1365            }
1366            return title;
1367        }
1368
1369        public void setNewAlarm(long alarmId) {
1370            mExpanded.add(alarmId);
1371        }
1372
1373        /**
1374         * Expands the alarm for editing.
1375         *
1376         * @param itemHolder The item holder instance.
1377         */
1378        private void expandAlarm(final ItemHolder itemHolder, boolean animate) {
1379            mExpanded.add(itemHolder.alarm.id);
1380            bindExpandArea(itemHolder, itemHolder.alarm);
1381            // Scroll the view to make sure it is fully viewed
1382            mScrollAlarmId = itemHolder.alarm.id;
1383
1384            // Save the starting height so we can animate from this value.
1385            final int startingHeight = itemHolder.alarmItem.getHeight();
1386
1387            // Set the expand area to visible so we can measure the height to animate to.
1388            itemHolder.alarmItem.setBackgroundColor(mBackgroundColorExpanded);
1389            itemHolder.expandArea.setVisibility(View.VISIBLE);
1390
1391            if (!animate) {
1392                // Set the "end" layout and don't do the animation.
1393                itemHolder.arrow.setRotation(180);
1394                // We need to translate the hairline up, so the height of the collapseArea
1395                // needs to be measured to know how high to translate it.
1396                final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver();
1397                observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1398                    @Override
1399                    public boolean onPreDraw() {
1400                        // We don't want to continue getting called for every listview drawing.
1401                        if (observer.isAlive()) {
1402                            observer.removeOnPreDrawListener(this);
1403                        }
1404                        int hairlineHeight = itemHolder.hairLine.getHeight();
1405                        int collapseHeight =
1406                                itemHolder.collapseExpandArea.getHeight() - hairlineHeight;
1407                        itemHolder.hairLine.setTranslationY(-collapseHeight);
1408                        return true;
1409                    }
1410                });
1411                return;
1412            }
1413
1414            // Add an onPreDrawListener, which gets called after measurement but before the draw.
1415            // This way we can check the height we need to animate to before any drawing.
1416            // Note the series of events:
1417            //  * expandArea is set to VISIBLE, which causes a layout pass
1418            //  * the view is measured, and our onPreDrawListener is called
1419            //  * we set up the animation using the start and end values.
1420            //  * the height is set back to the starting point so it can be animated down.
1421            //  * request another layout pass.
1422            //  * return false so that onDraw() is not called for the single frame before
1423            //    the animations have started.
1424            final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver();
1425            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1426                @Override
1427                public boolean onPreDraw() {
1428                    // We don't want to continue getting called for every listview drawing.
1429                    if (observer.isAlive()) {
1430                        observer.removeOnPreDrawListener(this);
1431                    }
1432                    // Calculate some values to help with the animation.
1433                    final int endingHeight = itemHolder.alarmItem.getHeight();
1434                    final int distance = endingHeight - startingHeight;
1435                    final int collapseHeight = itemHolder.collapseExpandArea.getHeight();
1436                    int hairlineHeight = itemHolder.hairLine.getHeight();
1437                    final int hairlineDistance = collapseHeight - hairlineHeight;
1438
1439                    // Set the height back to the start state of the animation.
1440                    itemHolder.alarmItem.getLayoutParams().height = startingHeight;
1441                    // To allow the expandArea to glide in with the expansion animation, set a
1442                    // negative top margin, which will animate down to a margin of 0 as the height
1443                    // is increased.
1444                    // Note that we need to maintain the bottom margin as a fixed value (instead of
1445                    // just using a listview, to allow for a flatter hierarchy) to fit the bottom
1446                    // bar underneath.
1447                    FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1448                            itemHolder.expandArea.getLayoutParams();
1449                    expandParams.setMargins(0, -distance, 0, collapseHeight);
1450                    itemHolder.alarmItem.requestLayout();
1451
1452                    // Set up the animator to animate the expansion.
1453                    ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
1454                            .setDuration(EXPAND_DURATION);
1455                    animator.setInterpolator(mExpandInterpolator);
1456                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1457                        @Override
1458                        public void onAnimationUpdate(ValueAnimator animator) {
1459                            Float value = (Float) animator.getAnimatedValue();
1460
1461                            // For each value from 0 to 1, animate the various parts of the layout.
1462                            itemHolder.alarmItem.getLayoutParams().height =
1463                                    (int) (value * distance + startingHeight);
1464                            FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1465                                    itemHolder.expandArea.getLayoutParams();
1466                            expandParams.setMargins(
1467                                    0, (int) -((1 - value) * distance), 0, collapseHeight);
1468                            itemHolder.arrow.setRotation(180 * value);
1469                            itemHolder.hairLine.setTranslationY(-hairlineDistance * value);
1470                            itemHolder.summary.setAlpha(1 - value);
1471
1472                            itemHolder.alarmItem.requestLayout();
1473                        }
1474                    });
1475                    // Set everything to their final values when the animation's done.
1476                    animator.addListener(new AnimatorListener() {
1477                        @Override
1478                        public void onAnimationEnd(Animator animation) {
1479                            // Set it back to wrap content since we'd explicitly set the height.
1480                            itemHolder.alarmItem.getLayoutParams().height =
1481                                    LayoutParams.WRAP_CONTENT;
1482                            itemHolder.arrow.setRotation(180);
1483                            itemHolder.hairLine.setTranslationY(-hairlineDistance);
1484                            itemHolder.summary.setVisibility(View.GONE);
1485                        }
1486
1487                        @Override
1488                        public void onAnimationCancel(Animator animation) {
1489                            // TODO we may have to deal with cancelations of the animation.
1490                        }
1491
1492                        @Override
1493                        public void onAnimationRepeat(Animator animation) { }
1494                        @Override
1495                        public void onAnimationStart(Animator animation) { }
1496                    });
1497                    animator.start();
1498
1499                    // Return false so this draw does not occur to prevent the final frame from
1500                    // being drawn for the single frame before the animations start.
1501                    return false;
1502                }
1503            });
1504        }
1505
1506        private boolean isAlarmExpanded(Alarm alarm) {
1507            return mExpanded.contains(alarm.id);
1508        }
1509
1510        private void collapseAlarm(final ItemHolder itemHolder, boolean animate) {
1511            mExpanded.remove(itemHolder.alarm.id);
1512
1513            // Save the starting height so we can animate from this value.
1514            final int startingHeight = itemHolder.alarmItem.getHeight();
1515
1516            // Set the expand area to gone so we can measure the height to animate to.
1517            itemHolder.alarmItem.setBackgroundResource(mBackgroundColor);
1518            itemHolder.expandArea.setVisibility(View.GONE);
1519
1520            if (!animate) {
1521                // Set the "end" layout and don't do the animation.
1522                itemHolder.arrow.setRotation(0);
1523                itemHolder.hairLine.setTranslationY(0);
1524                return;
1525            }
1526
1527            // Add an onPreDrawListener, which gets called after measurement but before the draw.
1528            // This way we can check the height we need to animate to before any drawing.
1529            // Note the series of events:
1530            //  * expandArea is set to GONE, which causes a layout pass
1531            //  * the view is measured, and our onPreDrawListener is called
1532            //  * we set up the animation using the start and end values.
1533            //  * expandArea is set to VISIBLE again so it can be shown animating.
1534            //  * request another layout pass.
1535            //  * return false so that onDraw() is not called for the single frame before
1536            //    the animations have started.
1537            final ViewTreeObserver observer = mAlarmsList.getViewTreeObserver();
1538            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1539                @Override
1540                public boolean onPreDraw() {
1541                    if (observer.isAlive()) {
1542                        observer.removeOnPreDrawListener(this);
1543                    }
1544
1545                    // Calculate some values to help with the animation.
1546                    final int endingHeight = itemHolder.alarmItem.getHeight();
1547                    final int distance = endingHeight - startingHeight;
1548                    int hairlineHeight = itemHolder.hairLine.getHeight();
1549                    final int hairlineDistance = mCollapseExpandHeight - hairlineHeight;
1550
1551                    // Re-set the visibilities for the start state of the animation.
1552                    itemHolder.expandArea.setVisibility(View.VISIBLE);
1553                    itemHolder.summary.setVisibility(View.VISIBLE);
1554                    itemHolder.summary.setAlpha(1);
1555
1556                    // Set up the animator to animate the expansion.
1557                    ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
1558                            .setDuration(COLLAPSE_DURATION);
1559                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1560                        @Override
1561                        public void onAnimationUpdate(ValueAnimator animator) {
1562                            Float value = (Float) animator.getAnimatedValue();
1563
1564                            // For each value from 0 to 1, animate the various parts of the layout.
1565                            itemHolder.alarmItem.getLayoutParams().height =
1566                                    (int) (value * distance + startingHeight);
1567                            FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1568                                    itemHolder.expandArea.getLayoutParams();
1569                            expandParams.setMargins(
1570                                    0, (int) (value * distance), 0, mCollapseExpandHeight);
1571                            itemHolder.arrow.setRotation(180 * (1 - value));
1572                            itemHolder.hairLine.setTranslationY(-hairlineDistance * (1 - value));
1573                            itemHolder.summary.setAlpha(value);
1574
1575                            itemHolder.alarmItem.requestLayout();
1576                        }
1577                    });
1578                    animator.setInterpolator(mCollapseInterpolator);
1579                    // Set everything to their final values when the animation's done.
1580                    animator.addListener(new AnimatorListener() {
1581                        @Override
1582                        public void onAnimationEnd(Animator animation) {
1583                            // Set it back to wrap content since we'd explicitly set the height.
1584                            itemHolder.alarmItem.getLayoutParams().height =
1585                                    LayoutParams.WRAP_CONTENT;
1586
1587                            FrameLayout.LayoutParams expandParams = (FrameLayout.LayoutParams)
1588                                    itemHolder.expandArea.getLayoutParams();
1589                            expandParams.setMargins(0, 0, 0, mCollapseExpandHeight);
1590
1591                            itemHolder.expandArea.setVisibility(View.GONE);
1592                            itemHolder.arrow.setRotation(0);
1593                            itemHolder.hairLine.setTranslationY(0);
1594                        }
1595
1596                        @Override
1597                        public void onAnimationCancel(Animator animation) {
1598                            // TODO we may have to deal with cancelations of the animation.
1599                        }
1600
1601                        @Override
1602                        public void onAnimationRepeat(Animator animation) { }
1603                        @Override
1604                        public void onAnimationStart(Animator animation) { }
1605                    });
1606                    animator.start();
1607
1608                    return false;
1609                }
1610            });
1611        }
1612
1613        @Override
1614        public int getViewTypeCount() {
1615            return 1;
1616        }
1617
1618        private View getViewById(long id) {
1619            for (int i = 0; i < mList.getCount(); i++) {
1620                View v = mList.getChildAt(i);
1621                if (v != null) {
1622                    ItemHolder h = (ItemHolder)(v.getTag());
1623                    if (h != null && h.alarm.id == id) {
1624                        return v;
1625                    }
1626                }
1627            }
1628            return null;
1629        }
1630
1631        public long[] getExpandedArray() {
1632            int index = 0;
1633            long[] ids = new long[mExpanded.size()];
1634            for (long id : mExpanded) {
1635                ids[index] = id;
1636                index++;
1637            }
1638            return ids;
1639        }
1640
1641        public long[] getSelectedAlarmsArray() {
1642            int index = 0;
1643            long[] ids = new long[mSelectedAlarms.size()];
1644            for (long id : mSelectedAlarms) {
1645                ids[index] = id;
1646                index++;
1647            }
1648            return ids;
1649        }
1650
1651        public long[] getRepeatArray() {
1652            int index = 0;
1653            long[] ids = new long[mRepeatChecked.size()];
1654            for (long id : mRepeatChecked) {
1655                ids[index] = id;
1656                index++;
1657            }
1658            return ids;
1659        }
1660
1661        public Bundle getPreviousDaysOfWeekMap() {
1662            return mPreviousDaysOfWeekMap;
1663        }
1664
1665        private void buildHashSetFromArray(long[] ids, HashSet<Long> set) {
1666            for (long id : ids) {
1667                set.add(id);
1668            }
1669        }
1670    }
1671
1672    private void startCreatingAlarm() {
1673        // Set the "selected" alarm as null, and we'll create the new one when the timepicker
1674        // comes back.
1675        mSelectedAlarm = null;
1676        AlarmUtils.showTimeEditDialog(getChildFragmentManager(),
1677                null, AlarmClockFragment.this, DateFormat.is24HourFormat(getActivity()));
1678    }
1679
1680    private static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) {
1681        ContentResolver cr = context.getContentResolver();
1682        AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
1683        newInstance = AlarmInstance.addInstance(cr, newInstance);
1684        // Register instance to state manager
1685        AlarmStateManager.registerInstance(context, newInstance, true);
1686        return newInstance;
1687    }
1688
1689    private void asyncDeleteAlarm(final Alarm alarm, final View viewToRemove) {
1690        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
1691        final AsyncTask<Void, Void, Void> deleteTask = new AsyncTask<Void, Void, Void>() {
1692            @Override
1693            public synchronized void onPreExecute() {
1694                if (viewToRemove == null) {
1695                    return;
1696                }
1697                // The alarm list needs to be disabled until the animation finishes to prevent
1698                // possible concurrency issues.  It becomes re-enabled after the animations have
1699                // completed.
1700                mAlarmsList.setEnabled(false);
1701
1702                // Store all of the current list view item positions in memory for animation.
1703                final ListView list = mAlarmsList;
1704                int firstVisiblePosition = list.getFirstVisiblePosition();
1705                for (int i=0; i<list.getChildCount(); i++) {
1706                    View child = list.getChildAt(i);
1707                    if (child != viewToRemove) {
1708                        int position = firstVisiblePosition + i;
1709                        long itemId = mAdapter.getItemId(position);
1710                        mItemIdTopMap.put(itemId, child.getTop());
1711                    }
1712                }
1713            }
1714
1715            @Override
1716            protected Void doInBackground(Void... parameters) {
1717                // Activity may be closed at this point , make sure data is still valid
1718                if (context != null && alarm != null) {
1719                    ContentResolver cr = context.getContentResolver();
1720                    AlarmStateManager.deleteAllInstances(context, alarm.id);
1721                    Alarm.deleteAlarm(cr, alarm.id);
1722                }
1723                return null;
1724            }
1725        };
1726        mUndoShowing = true;
1727        deleteTask.execute();
1728    }
1729
1730    private void asyncAddAlarm(final Alarm alarm) {
1731        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
1732        final AsyncTask<Void, Void, AlarmInstance> updateTask =
1733                new AsyncTask<Void, Void, AlarmInstance>() {
1734            @Override
1735            public synchronized void onPreExecute() {
1736                final ListView list = mAlarmsList;
1737                // The alarm list needs to be disabled until the animation finishes to prevent
1738                // possible concurrency issues.  It becomes re-enabled after the animations have
1739                // completed.
1740                mAlarmsList.setEnabled(false);
1741
1742                // Store all of the current list view item positions in memory for animation.
1743                int firstVisiblePosition = list.getFirstVisiblePosition();
1744                for (int i=0; i<list.getChildCount(); i++) {
1745                    View child = list.getChildAt(i);
1746                    int position = firstVisiblePosition + i;
1747                    long itemId = mAdapter.getItemId(position);
1748                    mItemIdTopMap.put(itemId, child.getTop());
1749                }
1750            }
1751
1752            @Override
1753            protected AlarmInstance doInBackground(Void... parameters) {
1754                if (context != null && alarm != null) {
1755                    ContentResolver cr = context.getContentResolver();
1756
1757                    // Add alarm to db
1758                    Alarm newAlarm = Alarm.addAlarm(cr, alarm);
1759                    mScrollToAlarmId = newAlarm.id;
1760
1761                    // Create and add instance to db
1762                    if (newAlarm.enabled) {
1763                        return setupAlarmInstance(context, newAlarm);
1764                    }
1765                }
1766                return null;
1767            }
1768
1769            @Override
1770            protected void onPostExecute(AlarmInstance instance) {
1771                if (instance != null) {
1772                    AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis());
1773                }
1774            }
1775        };
1776        updateTask.execute();
1777    }
1778
1779    private void asyncUpdateAlarm(final Alarm alarm, final boolean popToast) {
1780        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
1781        final AsyncTask<Void, Void, AlarmInstance> updateTask =
1782                new AsyncTask<Void, Void, AlarmInstance>() {
1783            @Override
1784            protected AlarmInstance doInBackground(Void ... parameters) {
1785                ContentResolver cr = context.getContentResolver();
1786
1787                // Dismiss all old instances
1788                AlarmStateManager.deleteAllInstances(context, alarm.id);
1789
1790                // Update alarm
1791                Alarm.updateAlarm(cr, alarm);
1792                if (alarm.enabled) {
1793                    return setupAlarmInstance(context, alarm);
1794                }
1795
1796                return null;
1797            }
1798
1799            @Override
1800            protected void onPostExecute(AlarmInstance instance) {
1801                if (popToast && instance != null) {
1802                    AlarmUtils.popAlarmSetToast(context, instance.getAlarmTime().getTimeInMillis());
1803                }
1804            }
1805        };
1806        updateTask.execute();
1807    }
1808
1809    @Override
1810    public boolean onTouch(View v, MotionEvent event) {
1811        hideUndoBar(true, event);
1812        return false;
1813    }
1814}
1815