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.timer
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.animation.ObjectAnimator
23 import android.content.Context
24 import android.content.Intent
25 import android.os.Bundle
26 import android.os.SystemClock
27 import android.view.KeyEvent
28 import android.view.LayoutInflater
29 import android.view.View
30 import android.view.ViewGroup
31 import android.view.ViewTreeObserver.OnPreDrawListener
32 import android.view.animation.AccelerateInterpolator
33 import android.view.animation.DecelerateInterpolator
34 import android.widget.Button
35 import android.widget.ImageView
36 import androidx.annotation.VisibleForTesting
37 import androidx.viewpager.widget.ViewPager
38 
39 import com.android.deskclock.data.DataModel
40 import com.android.deskclock.data.Timer
41 import com.android.deskclock.data.TimerListener
42 import com.android.deskclock.data.TimerStringFormatter
43 import com.android.deskclock.events.Events
44 import com.android.deskclock.uidata.UiDataModel
45 import com.android.deskclock.AnimatorUtils
46 import com.android.deskclock.DeskClock
47 import com.android.deskclock.DeskClockFragment
48 import com.android.deskclock.FabContainer
49 import com.android.deskclock.R
50 import com.android.deskclock.Utils
51 
52 import java.io.Serializable
53 import kotlin.math.max
54 import kotlin.math.min
55 
56 /**
57  * Displays a vertical list of timers in all states.
58  */
59 class TimerFragment : DeskClockFragment(UiDataModel.Tab.TIMERS) {
60     /** Notified when the user swipes vertically to change the visible timer.  */
61     private val mTimerPageChangeListener = TimerPageChangeListener()
62 
63     /** Scheduled to update the timers while at least one is running.  */
64     private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
65 
66     /** Updates the [.mPageIndicators] in response to timers being added or removed.  */
67     private val mTimerWatcher: TimerListener = TimerWatcher()
68 
69     private lateinit var mCreateTimerView: TimerSetupView
70     private lateinit var mViewPager: ViewPager
71     private lateinit var mAdapter: TimerPagerAdapter
72     private var mTimersView: View? = null
73     private var mCurrentView: View? = null
74     private lateinit var mPageIndicators: Array<ImageView>
75 
76     private var mTimerSetupState: Serializable? = null
77 
78     /** `true` while this fragment is creating a new timer; `false` otherwise.  */
79     private var mCreatingTimer = false
80 
onCreateViewnull81     override fun onCreateView(
82         inflater: LayoutInflater,
83         container: ViewGroup?,
84         savedInstanceState: Bundle?
85     ): View? {
86         val view = inflater.inflate(R.layout.timer_fragment, container, false)
87 
88         mAdapter = TimerPagerAdapter(parentFragmentManager)
89         mViewPager = view.findViewById<View>(R.id.vertical_view_pager) as ViewPager
90         mViewPager.setAdapter(mAdapter)
91         mViewPager.addOnPageChangeListener(mTimerPageChangeListener)
92 
93         mTimersView = view.findViewById(R.id.timer_view)
94         mCreateTimerView = view.findViewById<View>(R.id.timer_setup) as TimerSetupView
95         mCreateTimerView.setFabContainer(this)
96         mPageIndicators = arrayOf(
97                 view.findViewById<View>(R.id.page_indicator0) as ImageView,
98                 view.findViewById<View>(R.id.page_indicator1) as ImageView,
99                 view.findViewById<View>(R.id.page_indicator2) as ImageView,
100                 view.findViewById<View>(R.id.page_indicator3) as ImageView
101         )
102 
103         DataModel.dataModel.addTimerListener(mAdapter)
104         DataModel.dataModel.addTimerListener(mTimerWatcher)
105 
106         // If timer setup state is present, retrieve it to be later honored.
107         savedInstanceState?.let {
108             mTimerSetupState = it.getSerializable(KEY_TIMER_SETUP_STATE)
109         }
110 
111         return view
112     }
113 
onStartnull114     override fun onStart() {
115         super.onStart()
116 
117         // Initialize the page indicators.
118         updatePageIndicators()
119         var createTimer = false
120         var showTimerId = -1
121 
122         // Examine the intent of the parent activity to determine which view to display.
123         val intent = requireActivity().intent
124         intent?.let {
125             // These extras are single-use; remove them after honoring them.
126             createTimer = it.getBooleanExtra(EXTRA_TIMER_SETUP, false)
127             it.removeExtra(EXTRA_TIMER_SETUP)
128 
129             showTimerId = it.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
130             it.removeExtra(TimerService.EXTRA_TIMER_ID)
131         }
132 
133         // Choose the view to display in this fragment.
134         if (showTimerId != -1) {
135             // A specific timer must be shown; show the list of timers.
136             showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
137         } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
138             // No timers exist, a timer is being created, or the last view was timer setup;
139             // show the timer setup view.
140             showCreateTimerView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
141 
142             if (mTimerSetupState != null) {
143                 mCreateTimerView.state = mTimerSetupState
144                 mTimerSetupState = null
145             }
146         } else {
147             // Otherwise, default to showing the list of timers.
148             showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
149         }
150 
151         // If the intent did not specify a timer to show, show the last timer that expired.
152         if (showTimerId == -1) {
153             val timer: Timer? = DataModel.dataModel.mostRecentExpiredTimer
154             showTimerId = timer?.id ?: -1
155         }
156 
157         // If a specific timer should be displayed, display the corresponding timer tab.
158         if (showTimerId != -1) {
159             val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
160             timer?.let {
161                 val index: Int = DataModel.dataModel.timers.indexOf(it)
162                 mViewPager.setCurrentItem(index)
163             }
164         }
165     }
166 
onResumenull167     override fun onResume() {
168         super.onResume()
169 
170         // We may have received a new intent while paused.
171         val intent = requireActivity().intent
172         if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
173             // This extra is single-use; remove after honoring it.
174             val showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
175             intent.removeExtra(TimerService.EXTRA_TIMER_ID)
176 
177             val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
178             timer?.let {
179                 // A specific timer must be shown; show the list of timers.
180                 val index: Int = DataModel.dataModel.timers.indexOf(it)
181                 mViewPager.setCurrentItem(index)
182 
183                 animateToView(mTimersView, null, false)
184             }
185         }
186     }
187 
onStopnull188     override fun onStop() {
189         super.onStop()
190 
191         // Stop updating the timers when this fragment is no longer visible.
192         stopUpdatingTime()
193     }
194 
onDestroyViewnull195     override fun onDestroyView() {
196         super.onDestroyView()
197 
198         DataModel.dataModel.removeTimerListener(mAdapter)
199         DataModel.dataModel.removeTimerListener(mTimerWatcher)
200     }
201 
onSaveInstanceStatenull202     override fun onSaveInstanceState(outState: Bundle) {
203         super.onSaveInstanceState(outState)
204 
205         // If the timer creation view is visible, store the input for later restoration.
206         if (mCurrentView === mCreateTimerView) {
207             mTimerSetupState = mCreateTimerView.state
208             outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState)
209         }
210     }
211 
updateFabnull212     private fun updateFab(fab: ImageView, animate: Boolean) {
213         if (mCurrentView === mTimersView) {
214             val timer = timer
215             if (timer == null) {
216                 fab.visibility = View.INVISIBLE
217                 return
218             }
219 
220             fab.visibility = View.VISIBLE
221             when (timer.state) {
222                 Timer.State.RUNNING -> {
223                     if (animate) {
224                         fab.setImageResource(R.drawable.ic_play_pause_animation)
225                     } else {
226                         fab.setImageResource(R.drawable.ic_play_pause)
227                     }
228                     fab.contentDescription = fab.resources.getString(R.string.timer_stop)
229                 }
230                 Timer.State.RESET -> {
231                     if (animate) {
232                         fab.setImageResource(R.drawable.ic_stop_play_animation)
233                     } else {
234                         fab.setImageResource(R.drawable.ic_pause_play)
235                     }
236                     fab.contentDescription = fab.resources.getString(R.string.timer_start)
237                 }
238                 Timer.State.PAUSED -> {
239                     if (animate) {
240                         fab.setImageResource(R.drawable.ic_pause_play_animation)
241                     } else {
242                         fab.setImageResource(R.drawable.ic_pause_play)
243                     }
244                     fab.contentDescription = fab.resources.getString(R.string.timer_start)
245                 }
246                 Timer.State.MISSED, Timer.State.EXPIRED -> {
247                     fab.setImageResource(R.drawable.ic_stop_white_24dp)
248                     fab.contentDescription = fab.resources.getString(R.string.timer_stop)
249                 }
250             }
251         } else if (mCurrentView === mCreateTimerView) {
252             if (mCreateTimerView.hasValidInput()) {
253                 fab.setImageResource(R.drawable.ic_start_white_24dp)
254                 fab.contentDescription = fab.resources.getString(R.string.timer_start)
255                 fab.visibility = View.VISIBLE
256             } else {
257                 fab.contentDescription = null
258                 fab.visibility = View.INVISIBLE
259             }
260         }
261     }
262 
onUpdateFabnull263     override fun onUpdateFab(fab: ImageView) {
264         updateFab(fab, false)
265     }
266 
onMorphFabnull267     override fun onMorphFab(fab: ImageView) {
268         // Update the fab's drawable to match the current timer state.
269         updateFab(fab, Utils.isNOrLater)
270         // Animate the drawable.
271         AnimatorUtils.startDrawableAnimation(fab)
272     }
273 
onUpdateFabButtonsnull274     override fun onUpdateFabButtons(left: Button, right: Button) {
275         if (mCurrentView === mTimersView) {
276             left.isClickable = true
277             left.setText(R.string.timer_delete)
278             left.contentDescription = left.resources.getString(R.string.timer_delete)
279             left.visibility = View.VISIBLE
280 
281             right.isClickable = true
282             right.setText(R.string.timer_add_timer)
283             right.contentDescription = right.resources.getString(R.string.timer_add_timer)
284             right.visibility = View.VISIBLE
285         } else if (mCurrentView === mCreateTimerView) {
286             left.isClickable = true
287             left.setText(R.string.timer_cancel)
288             left.contentDescription = left.resources.getString(R.string.timer_cancel)
289             // If no timers yet exist, the user is forced to create the first one.
290             left.visibility = if (hasTimers()) View.VISIBLE else View.INVISIBLE
291 
292             right.visibility = View.INVISIBLE
293         }
294     }
295 
onFabClicknull296     override fun onFabClick(fab: ImageView) {
297         if (mCurrentView === mTimersView) {
298             // If no timer is currently showing a fab action is meaningless.
299             val timer = timer ?: return
300 
301             val context = fab.context
302             val currentTime: Long = timer.remainingTime
303 
304             when (timer.state) {
305                 Timer.State.RUNNING -> {
306                     DataModel.dataModel.pauseTimer(timer)
307                     Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock)
308                     if (currentTime > 0) {
309                         mTimersView?.announceForAccessibility(TimerStringFormatter.formatString(
310                                 context, R.string.timer_accessibility_stopped, currentTime, true))
311                     }
312                 }
313                 Timer.State.PAUSED, Timer.State.RESET -> {
314                     DataModel.dataModel.startTimer(timer)
315                     Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
316                     if (currentTime > 0) {
317                         mTimersView?.announceForAccessibility(TimerStringFormatter.formatString(
318                                 context, R.string.timer_accessibility_started, currentTime, true))
319                     }
320                 }
321                 Timer.State.MISSED, Timer.State.EXPIRED -> {
322                     DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock)
323                 }
324             }
325         } else if (mCurrentView === mCreateTimerView) {
326             mCreatingTimer = true
327             try {
328                 // Create the new timer.
329                 val timerLength: Long = mCreateTimerView.timeInMillis
330                 val timer: Timer = DataModel.dataModel.addTimer(timerLength, "", false)
331                 Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock)
332 
333                 // Start the new timer.
334                 DataModel.dataModel.startTimer(timer)
335                 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
336 
337                 // Display the freshly created timer view.
338                 mViewPager.setCurrentItem(0)
339             } finally {
340                 mCreatingTimer = false
341             }
342 
343             // Return to the list of timers.
344             animateToView(mTimersView, null, true)
345         }
346     }
347 
onLeftButtonClicknull348     override fun onLeftButtonClick(left: Button) {
349         if (mCurrentView === mTimersView) {
350             // Clicking the "delete" button.
351             val timer = timer ?: return
352 
353             if (mAdapter.getCount() > 1) {
354                 animateTimerRemove(timer)
355             } else {
356                 animateToView(mCreateTimerView, timer, false)
357             }
358 
359             left.announceForAccessibility(requireActivity().getString(R.string.timer_deleted))
360         } else if (mCurrentView === mCreateTimerView) {
361             // Clicking the "cancel" button on the timer creation page returns to the timers list.
362             mCreateTimerView.reset()
363 
364             animateToView(mTimersView, null, false)
365 
366             left.announceForAccessibility(requireActivity().getString(R.string.timer_canceled))
367         }
368     }
369 
onRightButtonClicknull370     override fun onRightButtonClick(right: Button) {
371         if (mCurrentView !== mCreateTimerView) {
372             animateToView(mCreateTimerView, null, true)
373         }
374     }
375 
onKeyDownnull376     override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
377         return if (mCurrentView === mCreateTimerView) {
378             mCreateTimerView.onKeyDown(keyCode, event)
379         } else super.onKeyDown(keyCode, event)
380     }
381 
382     /**
383      * Updates the state of the page indicators so they reflect the selected page in the context of
384      * all pages.
385      */
updatePageIndicatorsnull386     private fun updatePageIndicators() {
387         val page: Int = mViewPager.getCurrentItem()
388         val pageIndicatorCount = mPageIndicators.size
389         val pageCount = mAdapter.getCount()
390 
391         val states = computePageIndicatorStates(page, pageIndicatorCount, pageCount)
392         for (i in states.indices) {
393             val state = states[i]
394             val pageIndicator = mPageIndicators[i]
395             if (state == 0) {
396                 pageIndicator.visibility = View.GONE
397             } else {
398                 pageIndicator.visibility = View.VISIBLE
399                 pageIndicator.setImageResource(state)
400             }
401         }
402     }
403 
404     /**
405      * Display the view that creates a new timer.
406      */
showCreateTimerViewnull407     private fun showCreateTimerView(updateTypes: Int) {
408         // Stop animating the timers.
409         stopUpdatingTime()
410 
411         // Show the creation view; hide the timer view.
412         mTimersView?.visibility = View.GONE
413         mCreateTimerView.visibility = View.VISIBLE
414 
415         // Record the fact that the create view is visible.
416         mCurrentView = mCreateTimerView
417 
418         // Update the fab and buttons.
419         updateFab(updateTypes)
420     }
421 
422     /**
423      * Display the view that lists all existing timers.
424      */
showTimersViewnull425     private fun showTimersView(updateTypes: Int) {
426         // Clear any defunct timer creation state; the next timer creation starts fresh.
427         mTimerSetupState = null
428 
429         // Show the timer view; hide the creation view.
430         mTimersView?.visibility = View.VISIBLE
431         mCreateTimerView.visibility = View.GONE
432 
433         // Record the fact that the create view is visible.
434         mCurrentView = mTimersView
435 
436         // Update the fab and buttons.
437         updateFab(updateTypes)
438 
439         // Start animating the timers.
440         startUpdatingTime()
441     }
442 
443     /**
444      * @param timerToRemove the timer to be removed during the animation
445      */
animateTimerRemovenull446     private fun animateTimerRemove(timerToRemove: Timer) {
447         val duration = UiDataModel.uiDataModel.shortAnimationDuration
448 
449         val fadeOut: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 1f, 0f)
450         fadeOut.duration = duration
451         fadeOut.interpolator = DecelerateInterpolator()
452         fadeOut.addListener(object : AnimatorListenerAdapter() {
453             override fun onAnimationEnd(animation: Animator) {
454                 DataModel.dataModel.removeTimer(timerToRemove)
455                 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
456             }
457         })
458 
459         val fadeIn: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 0f, 1f)
460         fadeIn.duration = duration
461         fadeIn.interpolator = AccelerateInterpolator()
462 
463         val animatorSet = AnimatorSet()
464         animatorSet.play(fadeOut).before(fadeIn)
465         animatorSet.start()
466     }
467 
468     /**
469      * @param toView one of [.mTimersView] or [.mCreateTimerView]
470      * @param timerToRemove the timer to be removed during the animation; `null` if no timer
471      * should be removed
472      * @param animateDown `true` if the views should animate upwards, otherwise downwards
473      */
animateToViewnull474     private fun animateToView(
475         toView: View?,
476         timerToRemove: Timer?,
477         animateDown: Boolean
478     ) {
479         if (mCurrentView === toView) {
480             return
481         }
482 
483         val toTimers = toView === mTimersView
484         if (toTimers) {
485             mTimersView?.visibility = View.VISIBLE
486         } else {
487             mCreateTimerView.visibility = View.VISIBLE
488         }
489         // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
490         updateFab(FabContainer.BUTTONS_DISABLE)
491 
492         val animationDuration = UiDataModel.uiDataModel.longAnimationDuration
493 
494         val viewTreeObserver = toView!!.viewTreeObserver
495         viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
496             override fun onPreDraw(): Boolean {
497                 if (viewTreeObserver.isAlive) {
498                     viewTreeObserver.removeOnPreDrawListener(this)
499                 }
500 
501                 val view = mTimersView?.findViewById<View>(R.id.timer_time)
502                 val distanceY: Float = if (view != null) view.height + view.y else 0f
503                 val translationDistance = if (animateDown) distanceY else -distanceY
504 
505                 toView.translationY = -translationDistance
506                 mCurrentView?.translationY = 0f
507                 toView.alpha = 0f
508                 mCurrentView?.alpha = 1f
509 
510                 val translateCurrent: Animator = ObjectAnimator.ofFloat(mCurrentView,
511                         View.TRANSLATION_Y, translationDistance)
512                 val translateNew: Animator = ObjectAnimator.ofFloat(toView, View.TRANSLATION_Y, 0f)
513                 val translationAnimatorSet = AnimatorSet()
514                 translationAnimatorSet.playTogether(translateCurrent, translateNew)
515                 translationAnimatorSet.duration = animationDuration
516                 translationAnimatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
517 
518                 val fadeOutAnimator: Animator = ObjectAnimator.ofFloat(mCurrentView, View.ALPHA, 0f)
519                 fadeOutAnimator.duration = animationDuration / 2
520                 fadeOutAnimator.addListener(object : AnimatorListenerAdapter() {
521                     override fun onAnimationStart(animation: Animator) {
522                         super.onAnimationStart(animation)
523 
524                         // The fade-out animation and fab-shrinking animation should run together.
525                         updateFab(FabContainer.FAB_AND_BUTTONS_SHRINK)
526                     }
527 
528                     override fun onAnimationEnd(animation: Animator) {
529                         super.onAnimationEnd(animation)
530                         if (toTimers) {
531                             showTimersView(FabContainer.FAB_AND_BUTTONS_EXPAND)
532 
533                             // Reset the state of the create view.
534                             mCreateTimerView.reset()
535                         } else {
536                             showCreateTimerView(FabContainer.FAB_AND_BUTTONS_EXPAND)
537                         }
538                         if (timerToRemove != null) {
539                             DataModel.dataModel.removeTimer(timerToRemove)
540                             Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
541                         }
542 
543                         // Update the fab and button states now that the correct view is visible and
544                         // before the animation to expand the fab and buttons starts.
545                         updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
546                     }
547                 })
548 
549                 val fadeInAnimator: Animator = ObjectAnimator.ofFloat(toView, View.ALPHA, 1f)
550                 fadeInAnimator.duration = animationDuration / 2
551                 fadeInAnimator.startDelay = animationDuration / 2
552 
553                 val animatorSet = AnimatorSet()
554                 animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet)
555                 animatorSet.addListener(object : AnimatorListenerAdapter() {
556                     override fun onAnimationEnd(animation: Animator) {
557                         super.onAnimationEnd(animation)
558                         mTimersView?.translationY = 0f
559                         mCreateTimerView.translationY = 0f
560                         mTimersView?.alpha = 1f
561                         mCreateTimerView.alpha = 1f
562                     }
563                 })
564                 animatorSet.start()
565 
566                 return true
567             }
568         })
569     }
570 
hasTimersnull571     private fun hasTimers(): Boolean {
572         return mAdapter.getCount() > 0
573     }
574 
575     private val timer: Timer?
576         get() {
577             if (!::mViewPager.isInitialized) {
578                 return null
579             }
580 
581             return if (mAdapter.getCount() == 0) {
582                 null
583             } else {
584                 mAdapter.getTimer(mViewPager.getCurrentItem())
585             }
586         }
587 
startUpdatingTimenull588     private fun startUpdatingTime() {
589         // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
590         stopUpdatingTime()
591         mViewPager.post(mTimeUpdateRunnable)
592     }
593 
stopUpdatingTimenull594     private fun stopUpdatingTime() {
595         mViewPager.removeCallbacks(mTimeUpdateRunnable)
596     }
597 
598     /**
599      * Periodically refreshes the state of each timer.
600      */
601     private inner class TimeUpdateRunnable : Runnable {
runnull602         override fun run() {
603             val startTime = SystemClock.elapsedRealtime()
604             // If no timers require continuous updates, avoid scheduling the next update.
605             if (!mAdapter.updateTime()) {
606                 return
607             }
608             val endTime = SystemClock.elapsedRealtime()
609 
610             // Try to maintain a consistent period of time between redraws.
611             val delay = max(0, startTime + 20 - endTime)
612             mTimersView?.postDelayed(this, delay)
613         }
614     }
615 
616     /**
617      * Update the page indicators and fab in response to a new timer becoming visible.
618      */
619     private inner class TimerPageChangeListener : ViewPager.SimpleOnPageChangeListener() {
onPageSelectednull620         override fun onPageSelected(position: Int) {
621             updatePageIndicators()
622             updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
623 
624             // Showing a new timer page may introduce a timer requiring continuous updates.
625             startUpdatingTime()
626         }
627 
onPageScrollStateChangednull628         override fun onPageScrollStateChanged(state: Int) {
629             // Teasing a neighboring timer may introduce a timer requiring continuous updates.
630             if (state == ViewPager.SCROLL_STATE_DRAGGING) {
631                 startUpdatingTime()
632             }
633         }
634     }
635 
636     /**
637      * Update the page indicators in response to timers being added or removed.
638      * Update the fab in response to the visible timer changing.
639      */
640     private inner class TimerWatcher : TimerListener {
timerAddednull641         override fun timerAdded(timer: Timer) {
642             updatePageIndicators()
643             // If the timer is being created via this fragment avoid adjusting the fab.
644             // Timer setup view is about to be animated away in response to this timer creation.
645             // Changes to the fab immediately preceding that animation are jarring.
646             if (!mCreatingTimer) {
647                 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
648             }
649         }
650 
timerUpdatednull651         override fun timerUpdated(before: Timer, after: Timer) {
652             // If the timer started, animate the timers.
653             if (before.isReset && !after.isReset) {
654                 startUpdatingTime()
655             }
656 
657             // Fetch the index of the change.
658             val index: Int = DataModel.dataModel.timers.indexOf(after)
659 
660             // If the timer just expired but is not displayed, display it now.
661             if (!before.isExpired && after.isExpired && index != mViewPager.getCurrentItem()) {
662                 mViewPager.setCurrentItem(index, true)
663             } else if (mCurrentView === mTimersView && index == mViewPager.getCurrentItem()) {
664                 // Morph the fab from its old state to new state if necessary.
665                 if (before.state != after.state &&
666                         !(before.isPaused && after.isReset)) {
667                     updateFab(FabContainer.FAB_MORPH)
668                 }
669             }
670         }
671 
timerRemovednull672         override fun timerRemoved(timer: Timer) {
673             updatePageIndicators()
674             updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
675 
676             if (mCurrentView === mTimersView && mAdapter.getCount() == 0) {
677                 animateToView(mCreateTimerView, null, false)
678             }
679         }
680     }
681 
682     companion object {
683         private const val EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP"
684 
685         private const val KEY_TIMER_SETUP_STATE = "timer_setup_input"
686 
687         /**
688          * @return an Intent that selects the timers tab with the
689          * setup screen for a new timer in place.
690          */
691         @VisibleForTesting
692         @JvmStatic
createTimerSetupIntentnull693         fun createTimerSetupIntent(context: Context): Intent {
694             return Intent(context, DeskClock::class.java).putExtra(EXTRA_TIMER_SETUP, true)
695         }
696 
697         /**
698          * @param page the selected page; value between 0 and `pageCount`
699          * @param pageIndicatorCount the number of indicators displaying the `page` location
700          * @param pageCount the number of pages that exist
701          * @return an array of length `pageIndicatorCount` specifying which image to display for
702          * each page indicator or 0 if the page indicator should be hidden
703          */
704         @VisibleForTesting
705         @JvmStatic
computePageIndicatorStatesnull706         fun computePageIndicatorStates(
707             page: Int,
708             pageIndicatorCount: Int,
709             pageCount: Int
710         ): IntArray {
711             // Compute the number of page indicators that will be visible.
712             val rangeSize = min(pageIndicatorCount, pageCount)
713 
714             // Compute the inclusive range of pages to indicate centered around the selected page.
715             var rangeStart = page - rangeSize / 2
716             var rangeEnd = rangeStart + rangeSize - 1
717 
718             // Clamp the range of pages if they extend beyond the last page.
719             if (rangeEnd >= pageCount) {
720                 rangeEnd = pageCount - 1
721                 rangeStart = rangeEnd - rangeSize + 1
722             }
723 
724             // Clamp the range of pages if they extend beyond the first page.
725             if (rangeStart < 0) {
726                 rangeStart = 0
727                 rangeEnd = rangeSize - 1
728             }
729 
730             // Build the result with all page indicators initially hidden.
731             val states = IntArray(pageIndicatorCount)
732             states.fill(0)
733 
734             // If 0 or 1 total pages exist, all page indicators must remain hidden.
735             if (rangeSize < 2) {
736                 return states
737             }
738 
739             // Initialize the visible page indicators to be dark.
740             states.fill(R.drawable.ic_swipe_circle_dark, 0, rangeSize)
741 
742             // If more pages exist before the first page indicator, make it a fade-in gradient.
743             if (rangeStart > 0) {
744                 states[0] = R.drawable.ic_swipe_circle_top
745             }
746 
747             // If more pages exist after the last page indicator, make it a fade-out gradient.
748             if (rangeEnd < pageCount - 1) {
749                 states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom
750             }
751 
752             // Set the indicator of the selected page to be light.
753             states[page - rangeStart] = R.drawable.ic_swipe_circle_light
754             return states
755         }
756     }
757 }