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
17package com.android.deskclock;
18
19import android.app.Activity;
20import android.app.LoaderManager;
21import android.content.Intent;
22import android.content.Loader;
23import android.database.Cursor;
24import android.media.RingtoneManager;
25import android.net.Uri;
26import android.os.Bundle;
27import android.support.design.widget.Snackbar;
28import android.support.v7.widget.LinearLayoutManager;
29import android.support.v7.widget.RecyclerView;
30import android.text.format.DateFormat;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34
35import com.android.deskclock.alarms.AlarmTimeClickHandler;
36import com.android.deskclock.alarms.AlarmUpdateHandler;
37import com.android.deskclock.alarms.ScrollHandler;
38import com.android.deskclock.alarms.TimePickerCompat;
39import com.android.deskclock.alarms.dataadapter.AlarmTimeAdapter;
40import com.android.deskclock.data.DataModel;
41import com.android.deskclock.provider.Alarm;
42import com.android.deskclock.widget.EmptyViewController;
43import com.android.deskclock.widget.toast.SnackbarManager;
44import com.android.deskclock.widget.toast.ToastManager;
45
46/**
47 * A fragment that displays a list of alarm time and allows interaction with them.
48 */
49public 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
76    public void processTimeSet(int hourOfDay, int minute) {
77        mAlarmTimeClickHandler.processTimeSet(hourOfDay, minute);
78    }
79
80    @Override
81    public void onCreate(Bundle savedState) {
82        super.onCreate(savedState);
83        mCursorLoader = getLoaderManager().initLoader(0, null, this);
84    }
85
86    @Override
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
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
144    public void smoothScrollTo(int position) {
145        mLayoutManager.scrollToPositionWithOffset(position, 20);
146    }
147
148    @Override
149    public void onSaveInstanceState(Bundle outState) {
150        super.onSaveInstanceState(outState);
151        mAlarmTimeAdapter.saveInstance(outState);
152        mAlarmTimeClickHandler.saveInstance(outState);
153    }
154
155    @Override
156    public void onDestroy() {
157        super.onDestroy();
158        ToastManager.cancelToast();
159    }
160
161    @Override
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
171    public void setLabel(Alarm alarm, String label) {
172        alarm.label = label;
173        mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
174    }
175
176    @Override
177    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
178        return Alarm.getAlarmsCursorLoader(getActivity());
179    }
180
181    @Override
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     */
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
218    public void onLoaderReset(Loader<Cursor> cursorLoader) {
219        mAlarmTimeAdapter.swapCursor(null);
220    }
221
222    @Override
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
257    public void setSmoothScrollStableId(long stableId) {
258        mScrollToAlarmId = stableId;
259    }
260
261    @Override
262    public void onFabClick(View view) {
263        mAlarmUpdateHandler.hideUndoBar();
264        startCreatingAlarm();
265    }
266
267    @Override
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
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
287    private void startCreatingAlarm() {
288        mAlarmTimeClickHandler.clearSelectedAlarm();
289        TimePickerCompat.showTimeEditDialog(this, null /* alarm */,
290                DateFormat.is24HourFormat(getActivity()));
291    }
292}
293