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 // TODO(b/157255731) Replace the deprecated Fragment related calls
60 class TimerFragment : DeskClockFragment(UiDataModel.Tab.TIMERS) {
61     /** Notified when the user swipes vertically to change the visible timer.  */
62     private val mTimerPageChangeListener = TimerPageChangeListener()
63 
64     /** Scheduled to update the timers while at least one is running.  */
65     private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
66 
67     /** Updates the [.mPageIndicators] in response to timers being added or removed.  */
68     private val mTimerWatcher: TimerListener = TimerWatcher()
69 
70     private lateinit var mCreateTimerView: TimerSetupView
71     private lateinit var mViewPager: ViewPager
72     private lateinit var mAdapter: TimerPagerAdapter
73     private var mTimersView: View? = null
74     private var mCurrentView: View? = null
75     private lateinit var mPageIndicators: Array<ImageView>
76 
77     private var mTimerSetupState: Serializable? = null
78 
79     /** `true` while this fragment is creating a new timer; `false` otherwise.  */
80     private var mCreatingTimer = false
81 
onCreateViewnull82     override fun onCreateView(
83         inflater: LayoutInflater,
84         container: ViewGroup?,
85         savedInstanceState: Bundle?
86     ): View? {
87         val view = inflater.inflate(R.layout.timer_fragment, container, false)
88 
89         mAdapter = TimerPagerAdapter(fragmentManager)
90         mViewPager = view.findViewById<View>(R.id.vertical_view_pager) as ViewPager
91         mViewPager.setAdapter(mAdapter)
92         mViewPager.addOnPageChangeListener(mTimerPageChangeListener)
93 
94         mTimersView = view.findViewById(R.id.timer_view)
95         mCreateTimerView = view.findViewById<View>(R.id.timer_setup) as TimerSetupView
96         mCreateTimerView.setFabContainer(this)
97         mPageIndicators = arrayOf(
98                 view.findViewById<View>(R.id.page_indicator0) as ImageView,
99                 view.findViewById<View>(R.id.page_indicator1) as ImageView,
100                 view.findViewById<View>(R.id.page_indicator2) as ImageView,
101                 view.findViewById<View>(R.id.page_indicator3) as ImageView
102         )
103 
104         DataModel.dataModel.addTimerListener(mAdapter)
105         DataModel.dataModel.addTimerListener(mTimerWatcher)
106 
107         // If timer setup state is present, retrieve it to be later honored.
108         savedInstanceState?.let {
109             mTimerSetupState = it.getSerializable(KEY_TIMER_SETUP_STATE)
110         }
111 
112         return view
113     }
114 
onStartnull115     override fun onStart() {
116         super.onStart()
117 
118         // Initialize the page indicators.
119         updatePageIndicators()
120         var createTimer = false
121         var showTimerId = -1
122 
123         // Examine the intent of the parent activity to determine which view to display.
124         val intent = activity.intent
125         intent?.let {
126             // These extras are single-use; remove them after honoring them.
127             createTimer = it.getBooleanExtra(EXTRA_TIMER_SETUP, false)
128             it.removeExtra(EXTRA_TIMER_SETUP)
129 
130             showTimerId = it.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
131             it.removeExtra(TimerService.EXTRA_TIMER_ID)
132         }
133 
134         // Choose the view to display in this fragment.
135         if (showTimerId != -1) {
136             // A specific timer must be shown; show the list of timers.
137             showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
138         } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
139             // No timers exist, a timer is being created, or the last view was timer setup;
140             // show the timer setup view.
141             showCreateTimerView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
142 
143             if (mTimerSetupState != null) {
144                 mCreateTimerView.state = mTimerSetupState
145                 mTimerSetupState = null
146             }
147         } else {
148             // Otherwise, default to showing the list of timers.
149             showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
150         }
151 
152         // If the intent did not specify a timer to show, show the last timer that expired.
153         if (showTimerId == -1) {
154             val timer: Timer? = DataModel.dataModel.mostRecentExpiredTimer
155             showTimerId = timer?.id ?: -1
156         }
157 
158         // If a specific timer should be displayed, display the corresponding timer tab.
159         if (showTimerId != -1) {
160             val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
161             timer?.let {
162                 val index: Int = DataModel.dataModel.timers.indexOf(it)
163                 mViewPager.setCurrentItem(index)
164             }
165         }
166     }
167 
onResumenull168     override fun onResume() {
169         super.onResume()
170 
171         // We may have received a new intent while paused.
172         val intent = activity.intent
173         if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
174             // This extra is single-use; remove after honoring it.
175             val showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
176             intent.removeExtra(TimerService.EXTRA_TIMER_ID)
177 
178             val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
179             timer?.let {
180                 // A specific timer must be shown; show the list of timers.
181                 val index: Int = DataModel.dataModel.timers.indexOf(it)
182                 mViewPager.setCurrentItem(index)
183 
184                 animateToView(mTimersView, null, false)
185             }
186         }
187     }
188 
onStopnull189     override fun onStop() {
190         super.onStop()
191 
192         // Stop updating the timers when this fragment is no longer visible.
193         stopUpdatingTime()
194     }
195 
onDestroyViewnull196     override fun onDestroyView() {
197         super.onDestroyView()
198 
199         DataModel.dataModel.removeTimerListener(mAdapter)
200         DataModel.dataModel.removeTimerListener(mTimerWatcher)
201     }
202 
onSaveInstanceStatenull203     override fun onSaveInstanceState(outState: Bundle) {
204         super.onSaveInstanceState(outState)
205 
206         // If the timer creation view is visible, store the input for later restoration.
207         if (mCurrentView === mCreateTimerView) {
208             mTimerSetupState = mCreateTimerView.state
209             outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState)
210         }
211     }
212 
updateFabnull213     private fun updateFab(fab: ImageView, animate: Boolean) {
214         if (mCurrentView === mTimersView) {
215             val timer = timer
216             if (timer == null) {
217                 fab.visibility = View.INVISIBLE
218                 return
219             }
220 
221             fab.visibility = View.VISIBLE
222             when (timer.state) {
223                 Timer.State.RUNNING -> {
224                     if (animate) {
225                         fab.setImageResource(R.drawable.ic_play_pause_animation)
226                     } else {
227                         fab.setImageResource(R.drawable.ic_play_pause)
228                     }
229                     fab.contentDescription = fab.resources.getString(R.string.timer_stop)
230                 }
231                 Timer.State.RESET -> {
232                     if (animate) {
233                         fab.setImageResource(R.drawable.ic_stop_play_animation)
234                     } else {
235                         fab.setImageResource(R.drawable.ic_pause_play)
236                     }
237                     fab.contentDescription = fab.resources.getString(R.string.timer_start)
238                 }
239                 Timer.State.PAUSED -> {
240                     if (animate) {
241                         fab.setImageResource(R.drawable.ic_pause_play_animation)
242                     } else {
243                         fab.setImageResource(R.drawable.ic_pause_play)
244                     }
245                     fab.contentDescription = fab.resources.getString(R.string.timer_start)
246                 }
247                 Timer.State.MISSED, Timer.State.EXPIRED -> {
248                     fab.setImageResource(R.drawable.ic_stop_white_24dp)
249                     fab.contentDescription = fab.resources.getString(R.string.timer_stop)
250                 }
251             }
252         } else if (mCurrentView === mCreateTimerView) {
253             if (mCreateTimerView.hasValidInput()) {
254                 fab.setImageResource(R.drawable.ic_start_white_24dp)
255                 fab.contentDescription = fab.resources.getString(R.string.timer_start)
256                 fab.visibility = View.VISIBLE
257             } else {
258                 fab.contentDescription = null
259                 fab.visibility = View.INVISIBLE
260             }
261         }
262     }
263 
onUpdateFabnull264     override fun onUpdateFab(fab: ImageView) {
265         updateFab(fab, false)
266     }
267 
onMorphFabnull268     override fun onMorphFab(fab: ImageView) {
269         // Update the fab's drawable to match the current timer state.
270         updateFab(fab, Utils.isNOrLater())
271         // Animate the drawable.
272         AnimatorUtils.startDrawableAnimation(fab)
273     }
274 
onUpdateFabButtonsnull275     override fun onUpdateFabButtons(left: Button, right: Button) {
276         if (mCurrentView === mTimersView) {
277             left.isClickable = true
278             left.setText(R.string.timer_delete)
279             left.contentDescription = left.resources.getString(R.string.timer_delete)
280             left.visibility = View.VISIBLE
281 
282             right.isClickable = true
283             right.setText(R.string.timer_add_timer)
284             right.contentDescription = right.resources.getString(R.string.timer_add_timer)
285             right.visibility = View.VISIBLE
286         } else if (mCurrentView === mCreateTimerView) {
287             left.isClickable = true
288             left.setText(R.string.timer_cancel)
289             left.contentDescription = left.resources.getString(R.string.timer_cancel)
290             // If no timers yet exist, the user is forced to create the first one.
291             left.visibility = if (hasTimers()) View.VISIBLE else View.INVISIBLE
292 
293             right.visibility = View.INVISIBLE
294         }
295     }
296 
onFabClicknull297     override fun onFabClick(fab: ImageView) {
298         if (mCurrentView === mTimersView) {
299             // If no timer is currently showing a fab action is meaningless.
300             val timer = timer ?: return
301 
302             val context = fab.context
303             val currentTime: Long = timer.remainingTime
304 
305             when (timer.state) {
306                 Timer.State.RUNNING -> {
307                     DataModel.dataModel.pauseTimer(timer)
308                     Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock)
309                     if (currentTime > 0) {
310                         mTimersView?.announceForAccessibility(TimerStringFormatter.formatString(
311                                 context, R.string.timer_accessibility_stopped, currentTime, true))
312                     }
313                 }
314                 Timer.State.PAUSED, Timer.State.RESET -> {
315                     DataModel.dataModel.startTimer(timer)
316                     Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
317                     if (currentTime > 0) {
318                         mTimersView?.announceForAccessibility(TimerStringFormatter.formatString(
319                                 context, R.string.timer_accessibility_started, currentTime, true))
320                     }
321                 }
322                 Timer.State.MISSED, Timer.State.EXPIRED -> {
323                     DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock)
324                 }
325             }
326         } else if (mCurrentView === mCreateTimerView) {
327             mCreatingTimer = true
328             try {
329                 // Create the new timer.
330                 val timerLength: Long = mCreateTimerView.timeInMillis
331                 val timer: Timer = DataModel.dataModel.addTimer(timerLength, "", false)
332                 Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock)
333 
334                 // Start the new timer.
335                 DataModel.dataModel.startTimer(timer)
336                 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
337 
338                 // Display the freshly created timer view.
339                 mViewPager.setCurrentItem(0)
340             } finally {
341                 mCreatingTimer = false
342             }
343 
344             // Return to the list of timers.
345             animateToView(mTimersView, null, true)
346         }
347     }
348 
onLeftButtonClicknull349     override fun onLeftButtonClick(left: Button) {
350         if (mCurrentView === mTimersView) {
351             // Clicking the "delete" button.
352             val timer = timer ?: return
353 
354             if (mAdapter.getCount() > 1) {
355                 animateTimerRemove(timer)
356             } else {
357                 animateToView(mCreateTimerView, timer, false)
358             }
359 
360             left.announceForAccessibility(activity.getString(R.string.timer_deleted))
361         } else if (mCurrentView === mCreateTimerView) {
362             // Clicking the "cancel" button on the timer creation page returns to the timers list.
363             mCreateTimerView.reset()
364 
365             animateToView(mTimersView, null, false)
366 
367             left.announceForAccessibility(activity.getString(R.string.timer_canceled))
368         }
369     }
370 
onRightButtonClicknull371     override fun onRightButtonClick(right: Button) {
372         if (mCurrentView !== mCreateTimerView) {
373             animateToView(mCreateTimerView, null, true)
374         }
375     }
376 
onKeyDownnull377     override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
378         return if (mCurrentView === mCreateTimerView) {
379             mCreateTimerView.onKeyDown(keyCode, event)
380         } else super.onKeyDown(keyCode, event)
381     }
382 
383     /**
384      * Updates the state of the page indicators so they reflect the selected page in the context of
385      * all pages.
386      */
updatePageIndicatorsnull387     private fun updatePageIndicators() {
388         val page: Int = mViewPager.getCurrentItem()
389         val pageIndicatorCount = mPageIndicators.size
390         val pageCount = mAdapter.getCount()
391 
392         val states = computePageIndicatorStates(page, pageIndicatorCount, pageCount)
393         for (i in states.indices) {
394             val state = states[i]
395             val pageIndicator = mPageIndicators[i]
396             if (state == 0) {
397                 pageIndicator.visibility = View.GONE
398             } else {
399                 pageIndicator.visibility = View.VISIBLE
400                 pageIndicator.setImageResource(state)
401             }
402         }
403     }
404 
405     /**
406      * Display the view that creates a new timer.
407      */
showCreateTimerViewnull408     private fun showCreateTimerView(updateTypes: Int) {
409         // Stop animating the timers.
410         stopUpdatingTime()
411 
412         // Show the creation view; hide the timer view.
413         mTimersView?.visibility = View.GONE
414         mCreateTimerView.visibility = View.VISIBLE
415 
416         // Record the fact that the create view is visible.
417         mCurrentView = mCreateTimerView
418 
419         // Update the fab and buttons.
420         updateFab(updateTypes)
421     }
422 
423     /**
424      * Display the view that lists all existing timers.
425      */
showTimersViewnull426     private fun showTimersView(updateTypes: Int) {
427         // Clear any defunct timer creation state; the next timer creation starts fresh.
428         mTimerSetupState = null
429 
430         // Show the timer view; hide the creation view.
431         mTimersView?.visibility = View.VISIBLE
432         mCreateTimerView.visibility = View.GONE
433 
434         // Record the fact that the create view is visible.
435         mCurrentView = mTimersView
436 
437         // Update the fab and buttons.
438         updateFab(updateTypes)
439 
440         // Start animating the timers.
441         startUpdatingTime()
442     }
443 
444     /**
445      * @param timerToRemove the timer to be removed during the animation
446      */
animateTimerRemovenull447     private fun animateTimerRemove(timerToRemove: Timer) {
448         val duration = UiDataModel.uiDataModel.shortAnimationDuration
449 
450         val fadeOut: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 1f, 0f)
451         fadeOut.duration = duration
452         fadeOut.interpolator = DecelerateInterpolator()
453         fadeOut.addListener(object : AnimatorListenerAdapter() {
454             override fun onAnimationEnd(animation: Animator) {
455                 DataModel.dataModel.removeTimer(timerToRemove)
456                 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
457             }
458         })
459 
460         val fadeIn: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 0f, 1f)
461         fadeIn.duration = duration
462         fadeIn.interpolator = AccelerateInterpolator()
463 
464         val animatorSet = AnimatorSet()
465         animatorSet.play(fadeOut).before(fadeIn)
466         animatorSet.start()
467     }
468 
469     /**
470      * @param toView one of [.mTimersView] or [.mCreateTimerView]
471      * @param timerToRemove the timer to be removed during the animation; `null` if no timer
472      * should be removed
473      * @param animateDown `true` if the views should animate upwards, otherwise downwards
474      */
animateToViewnull475     private fun animateToView(
476         toView: View?,
477         timerToRemove: Timer?,
478         animateDown: Boolean
479     ) {
480         if (mCurrentView === toView) {
481             return
482         }
483 
484         val toTimers = toView === mTimersView
485         if (toTimers) {
486             mTimersView?.visibility = View.VISIBLE
487         } else {
488             mCreateTimerView.visibility = View.VISIBLE
489         }
490         // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
491         updateFab(FabContainer.BUTTONS_DISABLE)
492 
493         val animationDuration = UiDataModel.uiDataModel.longAnimationDuration
494 
495         val viewTreeObserver = toView!!.viewTreeObserver
496         viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
497             override fun onPreDraw(): Boolean {
498                 if (viewTreeObserver.isAlive) {
499                     viewTreeObserver.removeOnPreDrawListener(this)
500                 }
501 
502                 val view = mTimersView?.findViewById<View>(R.id.timer_time)
503                 val distanceY: Float = if (view != null) view.height + view.y else 0f
504                 val translationDistance = if (animateDown) distanceY else -distanceY
505 
506                 toView.translationY = -translationDistance
507                 mCurrentView?.translationY = 0f
508                 toView.alpha = 0f
509                 mCurrentView?.alpha = 1f
510 
511                 val translateCurrent: Animator = ObjectAnimator.ofFloat(mCurrentView,
512                         View.TRANSLATION_Y, translationDistance)
513                 val translateNew: Animator = ObjectAnimator.ofFloat(toView, View.TRANSLATION_Y, 0f)
514                 val translationAnimatorSet = AnimatorSet()
515                 translationAnimatorSet.playTogether(translateCurrent, translateNew)
516                 translationAnimatorSet.duration = animationDuration
517                 translationAnimatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
518 
519                 val fadeOutAnimator: Animator = ObjectAnimator.ofFloat(mCurrentView, View.ALPHA, 0f)
520                 fadeOutAnimator.duration = animationDuration / 2
521                 fadeOutAnimator.addListener(object : AnimatorListenerAdapter() {
522                     override fun onAnimationStart(animation: Animator) {
523                         super.onAnimationStart(animation)
524 
525                         // The fade-out animation and fab-shrinking animation should run together.
526                         updateFab(FabContainer.FAB_AND_BUTTONS_SHRINK)
527                     }
528 
529                     override fun onAnimationEnd(animation: Animator) {
530                         super.onAnimationEnd(animation)
531                         if (toTimers) {
532                             showTimersView(FabContainer.FAB_AND_BUTTONS_EXPAND)
533 
534                             // Reset the state of the create view.
535                             mCreateTimerView.reset()
536                         } else {
537                             showCreateTimerView(FabContainer.FAB_AND_BUTTONS_EXPAND)
538                         }
539                         if (timerToRemove != null) {
540                             DataModel.dataModel.removeTimer(timerToRemove)
541                             Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
542                         }
543 
544                         // Update the fab and button states now that the correct view is visible and
545                         // before the animation to expand the fab and buttons starts.
546                         updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
547                     }
548                 })
549 
550                 val fadeInAnimator: Animator = ObjectAnimator.ofFloat(toView, View.ALPHA, 1f)
551                 fadeInAnimator.duration = animationDuration / 2
552                 fadeInAnimator.startDelay = animationDuration / 2
553 
554                 val animatorSet = AnimatorSet()
555                 animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet)
556                 animatorSet.addListener(object : AnimatorListenerAdapter() {
557                     override fun onAnimationEnd(animation: Animator) {
558                         super.onAnimationEnd(animation)
559                         mTimersView?.translationY = 0f
560                         mCreateTimerView.translationY = 0f
561                         mTimersView?.alpha = 1f
562                         mCreateTimerView.alpha = 1f
563                     }
564                 })
565                 animatorSet.start()
566 
567                 return true
568             }
569         })
570     }
571 
hasTimersnull572     private fun hasTimers(): Boolean {
573         return mAdapter.getCount() > 0
574     }
575 
576     private val timer: Timer?
577         get() {
578             if (!::mViewPager.isInitialized) {
579                 return null
580             }
581 
582             return if (mAdapter.getCount() == 0) {
583                 null
584             } else {
585                 mAdapter.getTimer(mViewPager.getCurrentItem())
586             }
587         }
588 
startUpdatingTimenull589     private fun startUpdatingTime() {
590         // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
591         stopUpdatingTime()
592         mViewPager.post(mTimeUpdateRunnable)
593     }
594 
stopUpdatingTimenull595     private fun stopUpdatingTime() {
596         mViewPager.removeCallbacks(mTimeUpdateRunnable)
597     }
598 
599     /**
600      * Periodically refreshes the state of each timer.
601      */
602     private inner class TimeUpdateRunnable : Runnable {
runnull603         override fun run() {
604             val startTime = SystemClock.elapsedRealtime()
605             // If no timers require continuous updates, avoid scheduling the next update.
606             if (!mAdapter.updateTime()) {
607                 return
608             }
609             val endTime = SystemClock.elapsedRealtime()
610 
611             // Try to maintain a consistent period of time between redraws.
612             val delay = max(0, startTime + 20 - endTime)
613             mTimersView?.postDelayed(this, delay)
614         }
615     }
616 
617     /**
618      * Update the page indicators and fab in response to a new timer becoming visible.
619      */
620     private inner class TimerPageChangeListener : ViewPager.SimpleOnPageChangeListener() {
onPageSelectednull621         override fun onPageSelected(position: Int) {
622             updatePageIndicators()
623             updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
624 
625             // Showing a new timer page may introduce a timer requiring continuous updates.
626             startUpdatingTime()
627         }
628 
onPageScrollStateChangednull629         override fun onPageScrollStateChanged(state: Int) {
630             // Teasing a neighboring timer may introduce a timer requiring continuous updates.
631             if (state == ViewPager.SCROLL_STATE_DRAGGING) {
632                 startUpdatingTime()
633             }
634         }
635     }
636 
637     /**
638      * Update the page indicators in response to timers being added or removed.
639      * Update the fab in response to the visible timer changing.
640      */
641     private inner class TimerWatcher : TimerListener {
timerAddednull642         override fun timerAdded(timer: Timer) {
643             updatePageIndicators()
644             // If the timer is being created via this fragment avoid adjusting the fab.
645             // Timer setup view is about to be animated away in response to this timer creation.
646             // Changes to the fab immediately preceding that animation are jarring.
647             if (!mCreatingTimer) {
648                 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
649             }
650         }
651 
timerUpdatednull652         override fun timerUpdated(before: Timer, after: Timer) {
653             // If the timer started, animate the timers.
654             if (before.isReset && !after.isReset) {
655                 startUpdatingTime()
656             }
657 
658             // Fetch the index of the change.
659             val index: Int = DataModel.dataModel.timers.indexOf(after)
660 
661             // If the timer just expired but is not displayed, display it now.
662             if (!before.isExpired && after.isExpired && index != mViewPager.getCurrentItem()) {
663                 mViewPager.setCurrentItem(index, true)
664             } else if (mCurrentView === mTimersView && index == mViewPager.getCurrentItem()) {
665                 // Morph the fab from its old state to new state if necessary.
666                 if (before.state != after.state &&
667                         !(before.isPaused && after.isReset)) {
668                     updateFab(FabContainer.FAB_MORPH)
669                 }
670             }
671         }
672 
timerRemovednull673         override fun timerRemoved(timer: Timer) {
674             updatePageIndicators()
675             updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
676 
677             if (mCurrentView === mTimersView && mAdapter.getCount() == 0) {
678                 animateToView(mCreateTimerView, null, false)
679             }
680         }
681     }
682 
683     companion object {
684         private const val EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP"
685 
686         private const val KEY_TIMER_SETUP_STATE = "timer_setup_input"
687 
688         /**
689          * @return an Intent that selects the timers tab with the
690          * setup screen for a new timer in place.
691          */
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 }