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