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