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