• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.content.Context
20 import android.database.Cursor
21 import android.graphics.drawable.Drawable
22 import android.os.Bundle
23 import android.os.SystemClock
24 import android.view.LayoutInflater
25 import android.view.View
26 import android.view.View.OnLayoutChangeListener
27 import android.view.ViewGroup
28 import android.widget.Button
29 import android.widget.ImageView
30 import android.widget.TextView
31 import androidx.loader.app.LoaderManager.LoaderCallbacks
32 import androidx.loader.content.Loader
33 import androidx.recyclerview.widget.LinearLayoutManager
34 import androidx.recyclerview.widget.RecyclerView
35 
36 import com.android.deskclock.ItemAdapter.ItemHolder
37 import com.android.deskclock.ItemAdapter.OnItemChangedListener
38 import com.android.deskclock.alarms.AlarmTimeClickHandler
39 import com.android.deskclock.alarms.AlarmUpdateHandler
40 import com.android.deskclock.alarms.ScrollHandler
41 import com.android.deskclock.alarms.TimePickerDialogFragment
42 import com.android.deskclock.alarms.dataadapter.AlarmItemHolder
43 import com.android.deskclock.alarms.dataadapter.CollapsedAlarmViewHolder
44 import com.android.deskclock.alarms.dataadapter.ExpandedAlarmViewHolder
45 import com.android.deskclock.provider.Alarm
46 import com.android.deskclock.provider.AlarmInstance
47 import com.android.deskclock.uidata.UiDataModel
48 import com.android.deskclock.widget.EmptyViewController
49 import com.android.deskclock.widget.toast.SnackbarManager
50 import com.android.deskclock.widget.toast.ToastManager
51 
52 import com.google.android.material.snackbar.Snackbar
53 
54 import kotlin.math.max
55 
56 /**
57  * A fragment that displays a list of alarm time and allows interaction with them.
58  */
59 class AlarmClockFragment : DeskClockFragment(UiDataModel.Tab.ALARMS),
60         LoaderCallbacks<Cursor>, ScrollHandler, TimePickerDialogFragment.OnTimeSetListener {
61     // Updates "Today/Tomorrow" in the UI when midnight passes.
62     private val mMidnightUpdater: Runnable = MidnightRunnable()
63 
64     // Views
65     private lateinit var mMainLayout: ViewGroup
66     private lateinit var mRecyclerView: RecyclerView
67 
68     // Data
69     private var mCursorLoader: Loader<*>? = null
70     private var mScrollToAlarmId = Alarm.INVALID_ID
71     private var mExpandedAlarmId = Alarm.INVALID_ID
72     private var mCurrentUpdateToken: Long = 0
73 
74     // Controllers
75     private lateinit var mItemAdapter: ItemAdapter<AlarmItemHolder>
76     private lateinit var mAlarmUpdateHandler: AlarmUpdateHandler
77     private lateinit var mEmptyViewController: EmptyViewController
78     private lateinit var mAlarmTimeClickHandler: AlarmTimeClickHandler
79     private lateinit var mLayoutManager: LinearLayoutManager
80 
onCreatenull81     override fun onCreate(savedState: Bundle?) {
82         super.onCreate(savedState)
83         mCursorLoader = loaderManager.initLoader(0, Bundle.EMPTY, this)
84         savedState?.let {
85             mExpandedAlarmId = it.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID)
86         }
87     }
88 
onCreateViewnull89     override fun onCreateView(
90         inflater: LayoutInflater,
91         container: ViewGroup?,
92         savedState: Bundle?
93     ): View? {
94         // Inflate the layout for this fragment
95         val v = inflater.inflate(R.layout.alarm_clock, container, false)
96         val context: Context = requireActivity()
97 
98         mRecyclerView = v.findViewById<View>(R.id.alarms_recycler_view) as RecyclerView
99         mLayoutManager = object : LinearLayoutManager(context) {
100             override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
101                 val extraSpace: Int = super.getExtraLayoutSpace(state)
102                 return if (state.willRunPredictiveAnimations()) {
103                     max(getHeight(), extraSpace)
104                 } else extraSpace
105             }
106         }
107         mRecyclerView.setLayoutManager(mLayoutManager)
108         mMainLayout = v.findViewById<View>(R.id.main) as ViewGroup
109         mAlarmUpdateHandler = AlarmUpdateHandler(context, this, mMainLayout)
110         val emptyView = v.findViewById<View>(R.id.alarms_empty_view) as TextView
111         val noAlarms: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_noalarms)
112         emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null)
113         mEmptyViewController = EmptyViewController(mMainLayout, mRecyclerView, emptyView)
114         mAlarmTimeClickHandler = AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler, this)
115 
116         mItemAdapter = ItemAdapter()
117         mItemAdapter.setHasStableIds()
118         mItemAdapter.withViewTypes(CollapsedAlarmViewHolder.Factory(inflater),
119                 null, CollapsedAlarmViewHolder.VIEW_TYPE)
120         mItemAdapter.withViewTypes(ExpandedAlarmViewHolder.Factory(context),
121                 null, ExpandedAlarmViewHolder.VIEW_TYPE)
122         mItemAdapter.setOnItemChangedListener(object : OnItemChangedListener {
123             override fun onItemChanged(holder: ItemHolder<*>) {
124                 if ((holder as AlarmItemHolder).isExpanded) {
125                     if (mExpandedAlarmId != holder.itemId) {
126                         // Collapse the prior expanded alarm.
127                         val aih = mItemAdapter.findItemById(mExpandedAlarmId)
128                         aih?.collapse()
129                         // Record the freshly expanded alarm.
130                         mExpandedAlarmId = holder.itemId
131                         val viewHolder: RecyclerView.ViewHolder? =
132                                 mRecyclerView.findViewHolderForItemId(mExpandedAlarmId)
133                         viewHolder?.let {
134                             smoothScrollTo(viewHolder.getAdapterPosition())
135                         }
136                     }
137                 } else if (mExpandedAlarmId == holder.itemId) {
138                     // The expanded alarm is now collapsed so update the tracking id.
139                     mExpandedAlarmId = Alarm.INVALID_ID
140                 }
141             }
142 
143             override fun onItemChanged(holder: ItemHolder<*>, payload: Any) {
144                 /* No additional work to do */
145             }
146         })
147         val scrollPositionWatcher = ScrollPositionWatcher()
148         mRecyclerView.addOnLayoutChangeListener(scrollPositionWatcher)
149         mRecyclerView.addOnScrollListener(scrollPositionWatcher)
150         mRecyclerView.setAdapter(mItemAdapter)
151         val itemAnimator = ItemAnimator()
152         itemAnimator.setChangeDuration(300L)
153         itemAnimator.setMoveDuration(300L)
154         mRecyclerView.setItemAnimator(itemAnimator)
155         return v
156     }
157 
onStartnull158     override fun onStart() {
159         super.onStart()
160 
161         if (!isTabSelected) {
162             TimePickerDialogFragment.removeTimeEditDialog(parentFragmentManager)
163         }
164     }
165 
onResumenull166     override fun onResume() {
167         super.onResume()
168 
169         // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
170         // alarms when midnight passes.
171         UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
172 
173         // Check if another app asked us to create a blank new alarm.
174         val intent = requireActivity().intent ?: return
175 
176         if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
177             UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
178             if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
179                 // An external app asked us to create a blank alarm.
180                 startCreatingAlarm()
181             }
182 
183             // Remove the CREATE_NEW extra now that we've processed it.
184             intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA)
185         } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
186             UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
187 
188             val alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID)
189             if (alarmId != Alarm.INVALID_ID) {
190                 setSmoothScrollStableId(alarmId)
191                 if (mCursorLoader != null && mCursorLoader!!.isStarted) {
192                     // We need to force a reload here to make sure we have the latest view
193                     // of the data to scroll to.
194                     mCursorLoader!!.forceLoad()
195                 }
196             }
197 
198             // Remove the SCROLL_TO_ALARM extra now that we've processed it.
199             intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA)
200         }
201     }
202 
onPausenull203     override fun onPause() {
204         super.onPause()
205         UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
206 
207         // When the user places the app in the background by pressing "home",
208         // dismiss the toast bar. However, since there is no way to determine if
209         // home was pressed, just dismiss any existing toast bar when restarting
210         // the app.
211         mAlarmUpdateHandler.hideUndoBar()
212     }
213 
smoothScrollTonull214     override fun smoothScrollTo(position: Int) {
215         mLayoutManager.scrollToPositionWithOffset(position, 0)
216     }
217 
onSaveInstanceStatenull218     override fun onSaveInstanceState(outState: Bundle) {
219         super.onSaveInstanceState(outState)
220         mAlarmTimeClickHandler.saveInstance(outState)
221         outState.putLong(KEY_EXPANDED_ID, mExpandedAlarmId)
222     }
223 
onDestroynull224     override fun onDestroy() {
225         super.onDestroy()
226         ToastManager.cancelToast()
227     }
228 
setLabelnull229     fun setLabel(alarm: Alarm, label: String?) {
230         alarm.label = label
231         mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
232     }
233 
onCreateLoadernull234     override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
235         return Alarm.getAlarmsCursorLoader(requireActivity())
236     }
237 
onLoadFinishednull238     override fun onLoadFinished(cursorLoader: Loader<Cursor>, data: Cursor) {
239         val itemHolders: MutableList<AlarmItemHolder> = ArrayList(data.count)
240         data.moveToFirst()
241         while (!data.isAfterLast) {
242             val alarm = Alarm(data)
243             val alarmInstance = if (alarm.canPreemptivelyDismiss()) {
244                 AlarmInstance(data, joinedTable = true)
245             } else {
246                 null
247             }
248             val itemHolder = AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler)
249             itemHolders.add(itemHolder)
250             data.moveToNext()
251         }
252         setAdapterItems(itemHolders, SystemClock.elapsedRealtime())
253     }
254 
255     /**
256      * Updates the adapters items, deferring the update until the current animation is finished or
257      * if no animation is running then the listener will be automatically be invoked immediately.
258      *
259      * @param items the new list of [AlarmItemHolder] to use
260      * @param updateToken a monotonically increasing value used to preserve ordering of deferred
261      * updates
262      */
setAdapterItemsnull263     private fun setAdapterItems(items: List<AlarmItemHolder>, updateToken: Long) {
264         if (updateToken < mCurrentUpdateToken) {
265             LogUtils.v("Ignoring adapter update: %d < %d", updateToken, mCurrentUpdateToken)
266             return
267         }
268 
269         if (mRecyclerView.getItemAnimator()!!.isRunning()) {
270             // RecyclerView is currently animating -> defer update.
271             mRecyclerView.getItemAnimator()!!.isRunning(
272                     object : RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {
273                         override fun onAnimationsFinished() {
274                             setAdapterItems(items, updateToken)
275                         }
276                     })
277         } else if (mRecyclerView.isComputingLayout()) {
278             // RecyclerView is currently computing a layout -> defer update.
279             mRecyclerView.post(Runnable { setAdapterItems(items, updateToken) })
280         } else {
281             mCurrentUpdateToken = updateToken
282             mItemAdapter.setItems(items)
283 
284             // Show or hide the empty view as appropriate.
285             val noAlarms = items.isEmpty()
286             mEmptyViewController.setEmpty(noAlarms)
287             if (noAlarms) {
288                 // Ensure the drop shadow is hidden when no alarms exist.
289                 setTabScrolledToTop(true)
290             }
291 
292             // Expand the correct alarm.
293             if (mExpandedAlarmId != Alarm.INVALID_ID) {
294                 val aih = mItemAdapter.findItemById(mExpandedAlarmId)
295                 if (aih != null) {
296                     mAlarmTimeClickHandler.setSelectedAlarm(aih.item)
297                     aih.expand()
298                 } else {
299                     mAlarmTimeClickHandler.setSelectedAlarm(null)
300                     mExpandedAlarmId = Alarm.INVALID_ID
301                 }
302             }
303 
304             // Scroll to the selected alarm.
305             if (mScrollToAlarmId != Alarm.INVALID_ID) {
306                 scrollToAlarm(mScrollToAlarmId)
307                 setSmoothScrollStableId(Alarm.INVALID_ID)
308             }
309         }
310     }
311 
312     /**
313      * @param alarmId identifies the alarm to be displayed
314      */
scrollToAlarmnull315     private fun scrollToAlarm(alarmId: Long) {
316         val alarmCount = mItemAdapter.itemCount
317         var alarmPosition = -1
318         for (i in 0 until alarmCount) {
319             val id = mItemAdapter.getItemId(i)
320             if (id == alarmId) {
321                 alarmPosition = i
322                 break
323             }
324         }
325 
326         if (alarmPosition >= 0) {
327             mItemAdapter.findItemById(alarmId)?.expand()
328             smoothScrollTo(alarmPosition)
329         } else {
330             // Trying to display a deleted alarm should only happen from a missed notification for
331             // an alarm that has been marked deleted after use.
332             SnackbarManager.show(Snackbar.make(mMainLayout, R.string.missed_alarm_has_been_deleted,
333                     Snackbar.LENGTH_LONG))
334         }
335     }
336 
onLoaderResetnull337     override fun onLoaderReset(cursorLoader: Loader<Cursor>) {
338     }
339 
setSmoothScrollStableIdnull340     override fun setSmoothScrollStableId(stableId: Long) {
341         mScrollToAlarmId = stableId
342     }
343 
onFabClicknull344     override fun onFabClick(fab: ImageView) {
345         mAlarmUpdateHandler.hideUndoBar()
346         startCreatingAlarm()
347     }
348 
onUpdateFabnull349     override fun onUpdateFab(fab: ImageView) {
350         fab.visibility = View.VISIBLE
351         fab.setImageResource(R.drawable.ic_add_white_24dp)
352         fab.contentDescription = fab.resources.getString(R.string.button_alarms)
353     }
354 
onUpdateFabButtonsnull355     override fun onUpdateFabButtons(left: Button, right: Button) {
356         left.visibility = View.INVISIBLE
357         right.visibility = View.INVISIBLE
358     }
359 
startCreatingAlarmnull360     private fun startCreatingAlarm() {
361         // Clear the currently selected alarm.
362         mAlarmTimeClickHandler.setSelectedAlarm(null)
363         TimePickerDialogFragment.show(this)
364     }
365 
onTimeSetnull366     override fun onTimeSet(fragment: TimePickerDialogFragment?, hourOfDay: Int, minute: Int) {
367         mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute)
368     }
369 
removeItemnull370     fun removeItem(itemHolder: AlarmItemHolder) {
371         mItemAdapter.removeItem(itemHolder)
372     }
373 
374     /**
375      * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
376      * the recyclerview or when the size/position of elements within the recyclerview changes.
377      */
378     private inner class ScrollPositionWatcher
379         : RecyclerView.OnScrollListener(), OnLayoutChangeListener {
onScrollednull380         override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
381             setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
382         }
383 
onLayoutChangenull384         override fun onLayoutChange(
385             v: View,
386             left: Int,
387             top: Int,
388             right: Int,
389             bottom: Int,
390             oldLeft: Int,
391             oldTop: Int,
392             oldRight: Int,
393             oldBottom: Int
394         ) {
395             setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
396         }
397     }
398 
399     /**
400      * This runnable executes at midnight and refreshes the display of all alarms. Collapsed alarms
401      * that do no repeat will have their "Tomorrow" strings updated to say "Today".
402      */
403     private inner class MidnightRunnable : Runnable {
runnull404         override fun run() {
405             mItemAdapter.notifyDataSetChanged()
406         }
407     }
408 
409     companion object {
410         // This extra is used when receiving an intent to create an alarm, but no alarm details
411         // have been passed in, so the alarm page should start the process of creating a new alarm.
412         const val ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new"
413 
414         // This extra is used when receiving an intent to scroll to specific alarm. If alarm
415         // can not be found, and toast message will pop up that the alarm has be deleted.
416         const val SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm"
417 
418         private const val KEY_EXPANDED_ID = "expandedId"
419     }
420 }