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.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.content.Intent
23 import android.graphics.drawable.Drawable
24 import android.os.Bundle
25 import android.text.format.DateUtils
26 import android.view.KeyEvent
27 import android.view.Menu
28 import android.view.MenuItem
29 import android.view.View
30 import android.widget.Button
31 import android.widget.ImageView
32 import android.widget.TextView
33 import androidx.annotation.StringRes
34 import androidx.appcompat.app.ActionBar
35 import androidx.appcompat.widget.Toolbar
36 import androidx.fragment.app.Fragment
37 import androidx.viewpager.widget.ViewPager
38 import androidx.viewpager.widget.ViewPager.OnPageChangeListener
39 import androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING
40 import androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE
41 import androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING
42 
43 import com.android.deskclock.FabContainer.UpdateFabFlag
44 import com.android.deskclock.LabelDialogFragment.AlarmLabelDialogHandler
45 import com.android.deskclock.actionbarmenu.MenuItemControllerFactory
46 import com.android.deskclock.actionbarmenu.NightModeMenuItemController
47 import com.android.deskclock.actionbarmenu.OptionsMenuManager
48 import com.android.deskclock.actionbarmenu.SettingsMenuItemController
49 import com.android.deskclock.data.DataModel
50 import com.android.deskclock.data.OnSilentSettingsListener
51 import com.android.deskclock.events.Events
52 import com.android.deskclock.provider.Alarm
53 import com.android.deskclock.uidata.TabListener
54 import com.android.deskclock.uidata.UiDataModel
55 import com.android.deskclock.widget.toast.SnackbarManager
56 
57 import com.google.android.material.snackbar.Snackbar
58 import com.google.android.material.tabs.TabLayout
59 
60 /**
61  * The main activity of the application which displays 4 different tabs contains alarms, world
62  * clocks, timers and a stopwatch.
63  */
64 class DeskClock : BaseActivity(), FabContainer, AlarmLabelDialogHandler {
65     /** Models the interesting state of display the [.mFab] button may inhabit.  */
66     private enum class FabState {
67         SHOWING, HIDE_ARMED, HIDING
68     }
69 
70     /** Coordinates handling of context menu items.  */
71     private val mOptionsMenuManager = OptionsMenuManager()
72 
73     /** Shrinks the [.mFab], [.mLeftButton] and [.mRightButton] to nothing.  */
74     private val mHideAnimation = AnimatorSet()
75 
76     /** Grows the [.mFab], [.mLeftButton] and [.mRightButton] to natural sizes.  */
77     private val mShowAnimation = AnimatorSet()
78 
79     /** Hides, updates, and shows only the [.mFab]; the buttons are untouched.  */
80     private val mUpdateFabOnlyAnimation = AnimatorSet()
81 
82     /** Hides, updates, and shows only the [.mLeftButton] and [.mRightButton].  */
83     private val mUpdateButtonsOnlyAnimation = AnimatorSet()
84 
85     /** Automatically starts the [.mShowAnimation] after [.mHideAnimation] ends.  */
86     private val mAutoStartShowListener: AnimatorListenerAdapter = AutoStartShowListener()
87 
88     /** Updates the user interface to reflect the selected tab from the backing model.  */
89     private val mTabChangeWatcher: TabListener = TabChangeWatcher()
90 
91     /** Shows/hides a snackbar explaining which setting is suppressing alarms from firing.  */
92     private val mSilentSettingChangeWatcher: OnSilentSettingsListener = SilentSettingChangeWatcher()
93 
94     /** Displays a snackbar explaining why alarms may not fire or may fire silently.  */
95     private var mShowSilentSettingSnackbarRunnable: Runnable? = null
96 
97     /** The view to which snackbar items are anchored.  */
98     private lateinit var mSnackbarAnchor: View
99 
100     /** The current display state of the [.mFab].  */
101     private var mFabState = FabState.SHOWING
102 
103     /** The single floating-action button shared across all tabs in the user interface.  */
104     private lateinit var mFab: ImageView
105 
106     /** The button left of the [.mFab] shared across all tabs in the user interface.  */
107     private lateinit var mLeftButton: Button
108 
109     /** The button right of the [.mFab] shared across all tabs in the user interface.  */
110     private lateinit var mRightButton: Button
111 
112     /** The controller that shows the drop shadow when content is not scrolled to the top.  */
113     private var mDropShadowController: DropShadowController? = null
114 
115     /** The ViewPager that pages through the fragments representing the content of the tabs.  */
116     private lateinit var mFragmentTabPager: ViewPager
117 
118     /** Generates the fragments that are displayed by the [.mFragmentTabPager].  */
119     private lateinit var mFragmentTabPagerAdapter: FragmentTabPagerAdapter
120 
121     /** The container that stores the tab headers.  */
122     private lateinit var mTabLayout: TabLayout
123 
124     /** `true` when a settings change necessitates recreating this activity.  */
125     private var mRecreateActivity = false
126 
onNewIntentnull127     override fun onNewIntent(newIntent: Intent) {
128         super.onNewIntent(newIntent)
129 
130         // Fragments may query the latest intent for information, so update the intent.
131         setIntent(newIntent)
132     }
133 
onCreatenull134     protected override fun onCreate(savedInstanceState: Bundle?) {
135         super.onCreate(savedInstanceState)
136 
137         setContentView(R.layout.desk_clock)
138         mSnackbarAnchor = findViewById(R.id.content)
139 
140         // Configure the toolbar.
141         val toolbar: Toolbar = findViewById(R.id.toolbar) as Toolbar
142         setSupportActionBar(toolbar)
143 
144         val actionBar: ActionBar? = getSupportActionBar()
145         actionBar?.setDisplayShowTitleEnabled(false)
146 
147         // Configure the menu item controllers add behavior to the toolbar.
148         mOptionsMenuManager.addMenuItemController(
149                 NightModeMenuItemController(this), SettingsMenuItemController(this))
150         mOptionsMenuManager.addMenuItemController(
151                 *MenuItemControllerFactory.buildMenuItemControllers(this))
152 
153         // Inflate the menu during creation to avoid a double layout pass. Otherwise, the menu
154         // inflation occurs *after* the initial draw and a second layout pass adds in the menu.
155         onCreateOptionsMenu(toolbar.getMenu())
156 
157         // Create the tabs that make up the user interface.
158         mTabLayout = findViewById(R.id.tabs) as TabLayout
159         val tabCount: Int = UiDataModel.uiDataModel.tabCount
160         val showTabLabel: Boolean = getResources().getBoolean(R.bool.showTabLabel)
161         val showTabHorizontally: Boolean = getResources().getBoolean(R.bool.showTabHorizontally)
162         for (i in 0 until tabCount) {
163             val tabModel: UiDataModel.Tab = UiDataModel.uiDataModel.getTab(i)
164             @StringRes val labelResId: Int = tabModel.labelResId
165 
166             val tab: TabLayout.Tab = mTabLayout.newTab()
167                     .setTag(tabModel)
168                     .setIcon(tabModel.iconResId)
169                     .setContentDescription(labelResId)
170 
171             if (showTabLabel) {
172                 tab.setText(labelResId)
173                 tab.setCustomView(R.layout.tab_item)
174 
175                 val text = tab.getCustomView()!!.findViewById(android.R.id.text1) as TextView
176                 text.setTextColor(mTabLayout.getTabTextColors())
177 
178                 // Bind the icon to the TextView.
179                 val icon: Drawable? = tab.getIcon()
180                 if (showTabHorizontally) {
181                     // Remove the icon so it doesn't affect the minimum TabLayout height.
182                     tab.setIcon(null)
183                     text.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
184                 } else {
185                     text.setCompoundDrawablesRelativeWithIntrinsicBounds(null, icon, null, null)
186                 }
187             }
188 
189             mTabLayout.addTab(tab)
190         }
191 
192         // Configure the buttons shared by the tabs.
193         mFab = findViewById(R.id.fab) as ImageView
194         mLeftButton = findViewById(R.id.left_button) as Button
195         mRightButton = findViewById(R.id.right_button) as Button
196 
197         mFab.setOnClickListener { selectedDeskClockFragment.onFabClick(mFab) }
198         mLeftButton.setOnClickListener {
199             selectedDeskClockFragment.onLeftButtonClick(mLeftButton)
200         }
201         mRightButton.setOnClickListener {
202             selectedDeskClockFragment.onRightButtonClick(mRightButton)
203         }
204 
205         val duration: Long = UiDataModel.uiDataModel.shortAnimationDuration
206 
207         val hideFabAnimation = AnimatorUtils.getScaleAnimator(mFab, 1f, 0f)
208         val showFabAnimation = AnimatorUtils.getScaleAnimator(mFab, 0f, 1f)
209 
210         val leftHideAnimation = AnimatorUtils.getScaleAnimator(mLeftButton, 1f, 0f)
211         val rightHideAnimation = AnimatorUtils.getScaleAnimator(mRightButton, 1f, 0f)
212         val leftShowAnimation = AnimatorUtils.getScaleAnimator(mLeftButton, 0f, 1f)
213         val rightShowAnimation = AnimatorUtils.getScaleAnimator(mRightButton, 0f, 1f)
214 
215         hideFabAnimation.addListener(object : AnimatorListenerAdapter() {
216             override fun onAnimationEnd(animation: Animator) {
217                 selectedDeskClockFragment.onUpdateFab(mFab)
218             }
219         })
220 
221         leftHideAnimation.addListener(object : AnimatorListenerAdapter() {
222             override fun onAnimationEnd(animation: Animator) {
223                 selectedDeskClockFragment.onUpdateFabButtons(mLeftButton, mRightButton)
224             }
225         })
226 
227         // Build the reusable animations that hide and show the fab and left/right buttons.
228         // These may be used independently or be chained together.
229         mHideAnimation
230                 .setDuration(duration)
231                 .play(hideFabAnimation)
232                 .with(leftHideAnimation)
233                 .with(rightHideAnimation)
234 
235         mShowAnimation
236                 .setDuration(duration)
237                 .play(showFabAnimation)
238                 .with(leftShowAnimation)
239                 .with(rightShowAnimation)
240 
241         // Build the reusable animation that hides and shows only the fab.
242         mUpdateFabOnlyAnimation
243                 .setDuration(duration)
244                 .play(showFabAnimation)
245                 .after(hideFabAnimation)
246 
247         // Build the reusable animation that hides and shows only the buttons.
248         mUpdateButtonsOnlyAnimation
249                 .setDuration(duration)
250                 .play(leftShowAnimation)
251                 .with(rightShowAnimation)
252                 .after(leftHideAnimation)
253                 .after(rightHideAnimation)
254 
255         // Customize the view pager.
256         mFragmentTabPagerAdapter = FragmentTabPagerAdapter(this)
257         mFragmentTabPager = findViewById(R.id.desk_clock_pager) as ViewPager
258         // Keep all four tabs to minimize jank.
259         mFragmentTabPager.setOffscreenPageLimit(3)
260         // Set Accessibility Delegate to null so view pager doesn't intercept movements and
261         // prevent the fab from being selected.
262         mFragmentTabPager.setAccessibilityDelegate(null)
263         // Mirror changes made to the selected page of the view pager into UiDataModel.
264         mFragmentTabPager.addOnPageChangeListener(PageChangeWatcher())
265         mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter)
266 
267         // Mirror changes made to the selected tab into UiDataModel.
268         mTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
269             override fun onTabSelected(tab: TabLayout.Tab) {
270                 UiDataModel.uiDataModel.selectedTab = tab.getTag() as UiDataModel.Tab
271             }
272 
273             override fun onTabUnselected(tab: TabLayout.Tab) {
274             }
275 
276             override fun onTabReselected(tab: TabLayout.Tab) {
277             }
278         })
279 
280         // Honor changes to the selected tab from outside entities.
281         UiDataModel.uiDataModel.addTabListener(mTabChangeWatcher)
282     }
283 
onStartnull284     override fun onStart() {
285         super.onStart()
286         DataModel.dataModel.addSilentSettingsListener(mSilentSettingChangeWatcher)
287         DataModel.dataModel.isApplicationInForeground = true
288     }
289 
onResumenull290     override fun onResume() {
291         super.onResume()
292 
293         val dropShadow: View = findViewById(R.id.drop_shadow)
294         mDropShadowController = DropShadowController(dropShadow, UiDataModel.uiDataModel,
295                 mSnackbarAnchor.findViewById(R.id.tab_hairline))
296 
297         // ViewPager does not save state; this honors the selected tab in the user interface.
298         updateCurrentTab()
299     }
300 
onPostResumenull301     override fun onPostResume() {
302         super.onPostResume()
303 
304         if (mRecreateActivity) {
305             mRecreateActivity = false
306 
307             // A runnable must be posted here or the new DeskClock activity will be recreated in a
308             // paused state, even though it is the foreground activity.
309             mFragmentTabPager.post(Runnable { recreate() })
310         }
311     }
312 
onPausenull313     override fun onPause() {
314         if (mDropShadowController != null) {
315             mDropShadowController!!.stop()
316             mDropShadowController = null
317         }
318 
319         super.onPause()
320     }
321 
onStopnull322     override fun onStop() {
323         DataModel.dataModel.removeSilentSettingsListener(mSilentSettingChangeWatcher)
324         if (!isChangingConfigurations()) {
325             DataModel.dataModel.isApplicationInForeground = false
326         }
327 
328         super.onStop()
329     }
330 
onDestroynull331     override fun onDestroy() {
332         UiDataModel.uiDataModel.removeTabListener(mTabChangeWatcher)
333         super.onDestroy()
334     }
335 
onCreateOptionsMenunull336     override fun onCreateOptionsMenu(menu: Menu): Boolean {
337         mOptionsMenuManager.onCreateOptionsMenu(menu)
338         return true
339     }
340 
onPrepareOptionsMenunull341     override fun onPrepareOptionsMenu(menu: Menu): Boolean {
342         super.onPrepareOptionsMenu(menu)
343         mOptionsMenuManager.onPrepareOptionsMenu(menu)
344         return true
345     }
346 
onOptionsItemSelectednull347     override fun onOptionsItemSelected(item: MenuItem): Boolean {
348         return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item)
349     }
350 
351     /**
352      * Called by the LabelDialogFormat class after the dialog is finished.
353      */
onDialogLabelSetnull354     override fun onDialogLabelSet(alarm: Alarm, label: String, tag: String) {
355         val frag: Fragment? = supportFragmentManager.findFragmentByTag(tag)
356         if (frag is AlarmClockFragment) {
357             frag.setLabel(alarm, label)
358         }
359     }
360 
361     /**
362      * Listens for keyboard activity for the tab fragments to handle if necessary. A tab may want to
363      * respond to key presses even if they are not currently focused.
364      */
onKeyDownnull365     override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
366         return (selectedDeskClockFragment.onKeyDown(keyCode, event) ||
367                 super.onKeyDown(keyCode, event))
368     }
369 
updateFabnull370     override fun updateFab(@UpdateFabFlag updateType: Int) {
371         val f = selectedDeskClockFragment
372 
373         when (updateType and FabContainer.FAB_ANIMATION_MASK) {
374             FabContainer.FAB_SHRINK_AND_EXPAND -> mUpdateFabOnlyAnimation.start()
375             FabContainer.FAB_IMMEDIATE -> f.onUpdateFab(mFab)
376             FabContainer.FAB_MORPH -> f.onMorphFab(mFab)
377         }
378         when (updateType and FabContainer.FAB_REQUEST_FOCUS_MASK) {
379             FabContainer.FAB_REQUEST_FOCUS -> mFab.requestFocus()
380         }
381         when (updateType and FabContainer.BUTTONS_ANIMATION_MASK) {
382             FabContainer.BUTTONS_IMMEDIATE -> f.onUpdateFabButtons(mLeftButton, mRightButton)
383             FabContainer.BUTTONS_SHRINK_AND_EXPAND -> mUpdateButtonsOnlyAnimation.start()
384         }
385         when (updateType and FabContainer.BUTTONS_DISABLE_MASK) {
386             FabContainer.BUTTONS_DISABLE -> {
387                 mLeftButton.isClickable = false
388                 mRightButton.isClickable = false
389             }
390         }
391         when (updateType and FabContainer.FAB_AND_BUTTONS_SHRINK_EXPAND_MASK) {
392             FabContainer.FAB_AND_BUTTONS_SHRINK -> mHideAnimation.start()
393             FabContainer.FAB_AND_BUTTONS_EXPAND -> mShowAnimation.start()
394         }
395     }
396 
onActivityResultnull397     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
398         // Recreate the activity if any settings have been changed
399         if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS &&
400                 resultCode == RESULT_OK) {
401             mRecreateActivity = true
402         }
403     }
404 
405     /**
406      * Configure the [.mFragmentTabPager] and [.mTabLayout] to display UiDataModel's
407      * selected tab.
408      */
updateCurrentTabnull409     private fun updateCurrentTab() {
410         // Fetch the selected tab from the source of truth: UiDataModel.
411         val selectedTab: UiDataModel.Tab = UiDataModel.uiDataModel.selectedTab
412 
413         // Update the selected tab in the tablayout if it does not agree with UiDataModel.
414         for (i in 0 until mTabLayout.getTabCount()) {
415             val tab: TabLayout.Tab? = mTabLayout.getTabAt(i)
416             if (tab?.getTag() == selectedTab && !tab.isSelected()) {
417                 tab.select()
418                 break
419             }
420         }
421 
422         // Update the selected fragment in the viewpager if it does not agree with UiDataModel.
423         for (i in 0 until mFragmentTabPagerAdapter.count) {
424             val fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i)
425             if (fragment.isTabSelected && mFragmentTabPager.getCurrentItem() != i) {
426                 mFragmentTabPager.setCurrentItem(i)
427                 break
428             }
429         }
430     }
431 
432     /**
433      * @return the DeskClockFragment that is currently selected according to UiDataModel
434      */
435     private val selectedDeskClockFragment: DeskClockFragment
436         get() {
437             for (i in 0 until mFragmentTabPagerAdapter.count) {
438                 val fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i)
439                 if (fragment.isTabSelected) {
440                     return fragment
441                 }
442             }
443             val selectedTab: UiDataModel.Tab = UiDataModel.uiDataModel.selectedTab
444             throw IllegalStateException("Unable to locate selected fragment ($selectedTab)")
445         }
446 
447     /**
448      * @return a Snackbar that displays the message with the given id for 5 seconds
449      */
createSnackbarnull450     private fun createSnackbar(@StringRes messageId: Int): Snackbar {
451         return Snackbar.make(mSnackbarAnchor, messageId, 5000)
452     }
453 
454     /**
455      * As the view pager changes the selected page, update the model to record the new selected tab.
456      */
457     private inner class PageChangeWatcher : OnPageChangeListener {
458         /** The last reported page scroll state; used to detect exotic state changes.  */
459         private var mPriorState: Int = SCROLL_STATE_IDLE
460 
onPageScrollednull461         override fun onPageScrolled(
462             position: Int,
463             positionOffset: Float,
464             positionOffsetPixels: Int
465         ) {
466             // Only hide the fab when a non-zero drag distance is detected. This prevents
467             // over-scrolling from needlessly hiding the fab.
468             if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) {
469                 mFabState = FabState.HIDING
470                 mHideAnimation.start()
471             }
472         }
473 
onPageScrollStateChangednull474         override fun onPageScrollStateChanged(state: Int) {
475             if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) {
476                 // The user has tapped a tab button; play the hide and show animations linearly.
477                 mHideAnimation.addListener(mAutoStartShowListener)
478                 mHideAnimation.start()
479                 mFabState = FabState.HIDING
480             } else if (mPriorState == SCROLL_STATE_SETTLING && state == SCROLL_STATE_DRAGGING) {
481                 // The user has interrupted settling on a tab and the fab button must be re-hidden.
482                 if (mShowAnimation.isStarted) {
483                     mShowAnimation.cancel()
484                 }
485                 if (mHideAnimation.isStarted) {
486                     // Let the hide animation finish naturally; don't auto show when it ends.
487                     mHideAnimation.removeListener(mAutoStartShowListener)
488                 } else {
489                     // Start and immediately end the hide animation to jump to the hidden state.
490                     mHideAnimation.start()
491                     mHideAnimation.end()
492                 }
493                 mFabState = FabState.HIDING
494             } else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) {
495                 // The user has lifted their finger; show the buttons now or after hide ends.
496                 if (mHideAnimation.isStarted) {
497                     // Finish the hide animation and then start the show animation.
498                     mHideAnimation.addListener(mAutoStartShowListener)
499                 } else {
500                     updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
501                     mShowAnimation.start()
502 
503                     // The animation to show the fab has begun; update the state to showing.
504                     mFabState = FabState.SHOWING
505                 }
506             } else if (state == SCROLL_STATE_DRAGGING) {
507                 // The user has started a drag so arm the hide animation.
508                 mFabState = FabState.HIDE_ARMED
509             }
510 
511             // Update the last known state.
512             mPriorState = state
513         }
514 
onPageSelectednull515         override fun onPageSelected(position: Int) {
516             mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab()
517         }
518     }
519 
520     /**
521      * If this listener is attached to [.mHideAnimation] when it ends, the corresponding
522      * [.mShowAnimation] is automatically started.
523      */
524     private inner class AutoStartShowListener : AnimatorListenerAdapter() {
onAnimationEndnull525         override fun onAnimationEnd(animation: Animator) {
526             // Prepare the hide animation for its next use; by default do not auto-show after hide.
527             mHideAnimation.removeListener(mAutoStartShowListener)
528 
529             // Update the buttons now that they are no longer visible.
530             updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
531 
532             // Automatically start the grow animation now that shrinking is complete.
533             mShowAnimation.start()
534 
535             // The animation to show the fab has begun; update the state to showing.
536             mFabState = FabState.SHOWING
537         }
538     }
539 
540     /**
541      * Shows/hides a snackbar as silencing settings are enabled/disabled.
542      */
543     private inner class SilentSettingChangeWatcher : OnSilentSettingsListener {
onSilentSettingsChangenull544         override fun onSilentSettingsChange(
545             before: DataModel.SilentSetting?,
546             after: DataModel.SilentSetting?
547         ) {
548             if (mShowSilentSettingSnackbarRunnable != null) {
549                 mSnackbarAnchor.removeCallbacks(mShowSilentSettingSnackbarRunnable)
550                 mShowSilentSettingSnackbarRunnable = null
551             }
552 
553             if (after == null) {
554                 SnackbarManager.dismiss()
555             } else {
556                 mShowSilentSettingSnackbarRunnable = ShowSilentSettingSnackbarRunnable(after)
557                 mSnackbarAnchor.postDelayed(mShowSilentSettingSnackbarRunnable,
558                         DateUtils.SECOND_IN_MILLIS)
559             }
560         }
561     }
562 
563     /**
564      * Displays a snackbar that indicates a system setting is currently silencing alarms.
565      */
566     private inner class ShowSilentSettingSnackbarRunnable(
567         private val mSilentSetting: DataModel.SilentSetting
568     ) : Runnable {
runnull569         override fun run() {
570             // Create a snackbar with a message explaining the setting that is silencing alarms.
571             val snackbar: Snackbar = createSnackbar(mSilentSetting.labelResId)
572 
573             // Set the associated corrective action if one exists.
574             if (mSilentSetting.isActionEnabled(this@DeskClock)) {
575                 val actionResId: Int = mSilentSetting.actionResId
576                 snackbar.setAction(actionResId, mSilentSetting.actionListener)
577             }
578 
579             SnackbarManager.show(snackbar)
580         }
581     }
582 
583     /**
584      * As the model reports changes to the selected tab, update the user interface.
585      */
586     private inner class TabChangeWatcher : TabListener {
selectedTabChangednull587         override fun selectedTabChanged(
588             oldSelectedTab: UiDataModel.Tab,
589             newSelectedTab: UiDataModel.Tab
590         ) {
591             // Update the view pager and tab layout to agree with the model.
592             updateCurrentTab()
593 
594             // Avoid sending events for the initial tab selection on launch and re-selecting a tab
595             // after a configuration change.
596             if (DataModel.dataModel.isApplicationInForeground) {
597                 when (newSelectedTab) {
598                     UiDataModel.Tab.ALARMS -> {
599                         Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock)
600                     }
601                     UiDataModel.Tab.CLOCKS -> {
602                         Events.sendClockEvent(R.string.action_show, R.string.label_deskclock)
603                     }
604                     UiDataModel.Tab.TIMERS -> {
605                         Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock)
606                     }
607                     UiDataModel.Tab.STOPWATCH -> {
608                         Events.sendStopwatchEvent(R.string.action_show, R.string.label_deskclock)
609                     }
610                 }
611             }
612 
613             // If the hide animation has already completed, the buttons must be updated now when the
614             // new tab is known. Otherwise they are updated at the end of the hide animation.
615             if (!mHideAnimation.isStarted) {
616                 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
617             }
618         }
619     }
620 }