1 /*
2  * Copyright (C) 2015 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.app.Activity;
20 import android.app.LoaderManager;
21 import android.content.Intent;
22 import android.content.Loader;
23 import android.database.Cursor;
24 import android.media.RingtoneManager;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.support.design.widget.Snackbar;
28 import android.support.v7.widget.LinearLayoutManager;
29 import android.support.v7.widget.RecyclerView;
30 import android.text.format.DateFormat;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 
35 import com.android.deskclock.alarms.AlarmTimeClickHandler;
36 import com.android.deskclock.alarms.AlarmUpdateHandler;
37 import com.android.deskclock.alarms.ScrollHandler;
38 import com.android.deskclock.alarms.TimePickerCompat;
39 import com.android.deskclock.alarms.dataadapter.AlarmTimeAdapter;
40 import com.android.deskclock.data.DataModel;
41 import com.android.deskclock.provider.Alarm;
42 import com.android.deskclock.widget.EmptyViewController;
43 import com.android.deskclock.widget.toast.SnackbarManager;
44 import com.android.deskclock.widget.toast.ToastManager;
45 
46 /**
47  * A fragment that displays a list of alarm time and allows interaction with them.
48  */
49 public final class AlarmClockFragment extends DeskClockFragment implements
50         LoaderManager.LoaderCallbacks<Cursor>, ScrollHandler, TimePickerCompat.OnTimeSetListener {
51 
52     // This extra is used when receiving an intent to create an alarm, but no alarm details
53     // have been passed in, so the alarm page should start the process of creating a new alarm.
54     public static final String ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new";
55 
56     // This extra is used when receiving an intent to scroll to specific alarm. If alarm
57     // can not be found, and toast message will pop up that the alarm has be deleted.
58     public static final String SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm";
59 
60     // Views
61     private ViewGroup mMainLayout;
62     private RecyclerView mRecyclerView;
63 
64     // Data
65     private long mScrollToAlarmId = Alarm.INVALID_ID;
66     private Loader mCursorLoader = null;
67 
68     // Controllers
69     private AlarmTimeAdapter mAlarmTimeAdapter;
70     private AlarmUpdateHandler mAlarmUpdateHandler;
71     private EmptyViewController mEmptyViewController;
72     private AlarmTimeClickHandler mAlarmTimeClickHandler;
73     private LinearLayoutManager mLayoutManager;
74 
75     @Override
processTimeSet(int hourOfDay, int minute)76     public void processTimeSet(int hourOfDay, int minute) {
77         mAlarmTimeClickHandler.processTimeSet(hourOfDay, minute);
78     }
79 
80     @Override
onCreate(Bundle savedState)81     public void onCreate(Bundle savedState) {
82         super.onCreate(savedState);
83         mCursorLoader = getLoaderManager().initLoader(0, null, this);
84     }
85 
86     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)87     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
88         // Inflate the layout for this fragment
89         final View v = inflater.inflate(R.layout.alarm_clock, container, false);
90 
91         mRecyclerView = (RecyclerView) v.findViewById(R.id.alarms_recycler_view);
92         mLayoutManager = new LinearLayoutManager(getActivity());
93         mRecyclerView.setLayoutManager(mLayoutManager);
94         mMainLayout = (ViewGroup) v.findViewById(R.id.main);
95         mAlarmUpdateHandler = new AlarmUpdateHandler(getActivity(), this, mMainLayout);
96         mEmptyViewController = new EmptyViewController(mMainLayout, mRecyclerView,
97                 v.findViewById(R.id.alarms_empty_view));
98         mAlarmTimeClickHandler = new AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler,
99                 this);
100         mAlarmTimeAdapter = new AlarmTimeAdapter(getActivity(), savedState,
101                 mAlarmTimeClickHandler, this);
102         mRecyclerView.setAdapter(mAlarmTimeAdapter);
103 
104         return v;
105     }
106 
107     @Override
onResume()108     public void onResume() {
109         super.onResume();
110 
111         final DeskClock activity = (DeskClock) getActivity();
112         if (activity.getSelectedTab() == DeskClock.ALARM_TAB_INDEX) {
113             setFabAppearance();
114             setLeftRightButtonAppearance();
115         }
116 
117         // Check if another app asked us to create a blank new alarm.
118         final Intent intent = getActivity().getIntent();
119         if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
120             if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
121                 // An external app asked us to create a blank alarm.
122                 startCreatingAlarm();
123             }
124 
125             // Remove the CREATE_NEW extra now that we've processed it.
126             intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA);
127         } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
128             long alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID);
129             if (alarmId != Alarm.INVALID_ID) {
130                 setSmoothScrollStableId(alarmId);
131                 if (mCursorLoader != null && mCursorLoader.isStarted()) {
132                     // We need to force a reload here to make sure we have the latest view
133                     // of the data to scroll to.
134                     mCursorLoader.forceLoad();
135                 }
136             }
137 
138             // Remove the SCROLL_TO_ALARM extra now that we've processed it.
139             intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA);
140         }
141     }
142 
143     @Override
smoothScrollTo(int position)144     public void smoothScrollTo(int position) {
145         mLayoutManager.scrollToPositionWithOffset(position, 20);
146     }
147 
148     @Override
onSaveInstanceState(Bundle outState)149     public void onSaveInstanceState(Bundle outState) {
150         super.onSaveInstanceState(outState);
151         mAlarmTimeAdapter.saveInstance(outState);
152         mAlarmTimeClickHandler.saveInstance(outState);
153     }
154 
155     @Override
onDestroy()156     public void onDestroy() {
157         super.onDestroy();
158         ToastManager.cancelToast();
159     }
160 
161     @Override
onPause()162     public void onPause() {
163         super.onPause();
164         // When the user places the app in the background by pressing "home",
165         // dismiss the toast bar. However, since there is no way to determine if
166         // home was pressed, just dismiss any existing toast bar when restarting
167         // the app.
168         mAlarmUpdateHandler.hideUndoBar();
169     }
170 
setLabel(Alarm alarm, String label)171     public void setLabel(Alarm alarm, String label) {
172         alarm.label = label;
173         mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
174     }
175 
176     @Override
onCreateLoader(int id, Bundle args)177     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
178         return Alarm.getAlarmsCursorLoader(getActivity());
179     }
180 
181     @Override
onLoadFinished(Loader<Cursor> cursorLoader, final Cursor data)182     public void onLoadFinished(Loader<Cursor> cursorLoader, final Cursor data) {
183         mEmptyViewController.setEmpty(data.getCount() == 0);
184         mAlarmTimeAdapter.swapCursor(data);
185         if (mScrollToAlarmId != Alarm.INVALID_ID) {
186             scrollToAlarm(mScrollToAlarmId);
187             setSmoothScrollStableId(Alarm.INVALID_ID);
188         }
189     }
190 
191     /**
192      * Scroll to alarm with given alarm id.
193      *
194      * @param alarmId The alarm id to scroll to.
195      */
scrollToAlarm(long alarmId)196     private void scrollToAlarm(long alarmId) {
197         final int alarmCount = mAlarmTimeAdapter.getItemCount();
198         int alarmPosition = -1;
199         for (int i = 0; i < alarmCount; i++) {
200             long id = mAlarmTimeAdapter.getItemId(i);
201             if (id == alarmId) {
202                 alarmPosition = i;
203                 break;
204             }
205         }
206 
207         if (alarmPosition >= 0) {
208             mAlarmTimeAdapter.expand(alarmPosition);
209         } else {
210             // Trying to display a deleted alarm should only happen from a missed notification for
211             // an alarm that has been marked deleted after use.
212             SnackbarManager.show(Snackbar.make(mMainLayout, R.string
213                     .missed_alarm_has_been_deleted, Snackbar.LENGTH_LONG));
214         }
215     }
216 
217     @Override
onLoaderReset(Loader<Cursor> cursorLoader)218     public void onLoaderReset(Loader<Cursor> cursorLoader) {
219         mAlarmTimeAdapter.swapCursor(null);
220     }
221 
222     @Override
onActivityResult(int requestCode, int resultCode, Intent data)223     public void onActivityResult(int requestCode, int resultCode, Intent data) {
224         if (resultCode != Activity.RESULT_OK) {
225             return;
226         }
227 
228         switch (requestCode) {
229             case R.id.request_code_ringtone:
230                 // Extract the selected ringtone uri.
231                 Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
232                 if (uri == null) {
233                     uri = Alarm.NO_RINGTONE_URI;
234                 }
235 
236                 // Update the default ringtone for future new alarms.
237                 DataModel.getDataModel().setDefaultAlarmRingtoneUri(uri);
238 
239                 // Set the ringtone uri on the alarm.
240                 final Alarm alarm = mAlarmTimeClickHandler.getSelectedAlarm();
241                 if (alarm == null) {
242                     LogUtils.e("Could not get selected alarm to set ringtone");
243                     return;
244                 }
245                 alarm.alert = uri;
246 
247                 // Save the change to alarm.
248                 mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false /* popToast */,
249                         true /* minorUpdate */);
250                 break;
251             default:
252                 LogUtils.w("Unhandled request code in onActivityResult: " + requestCode);
253         }
254     }
255 
256     @Override
setSmoothScrollStableId(long stableId)257     public void setSmoothScrollStableId(long stableId) {
258         mScrollToAlarmId = stableId;
259     }
260 
261     @Override
onFabClick(View view)262     public void onFabClick(View view) {
263         mAlarmUpdateHandler.hideUndoBar();
264         startCreatingAlarm();
265     }
266 
267     @Override
setFabAppearance()268     public void setFabAppearance() {
269         if (mFab == null || getDeskClock().getSelectedTab() != DeskClock.ALARM_TAB_INDEX) {
270             return;
271         }
272         mFab.setVisibility(View.VISIBLE);
273         mFab.setImageResource(R.drawable.ic_add_white_24dp);
274         mFab.setContentDescription(getString(R.string.button_alarms));
275     }
276 
277     @Override
setLeftRightButtonAppearance()278     public void setLeftRightButtonAppearance() {
279         if (mLeftButton == null || mRightButton == null ||
280                 getDeskClock().getSelectedTab() != DeskClock.ALARM_TAB_INDEX) {
281             return;
282         }
283         mLeftButton.setVisibility(View.INVISIBLE);
284         mRightButton.setVisibility(View.INVISIBLE);
285     }
286 
startCreatingAlarm()287     private void startCreatingAlarm() {
288         mAlarmTimeClickHandler.clearSelectedAlarm();
289         TimePickerCompat.showTimeEditDialog(this, null /* alarm */,
290                 DateFormat.is24HourFormat(getActivity()));
291     }
292 }
293