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
17package com.android.deskclock;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.app.Fragment;
24import android.content.Intent;
25import android.graphics.drawable.Drawable;
26import android.os.Bundle;
27import android.support.annotation.StringRes;
28import android.support.design.widget.Snackbar;
29import android.support.design.widget.TabLayout;
30import android.support.v4.view.ViewPager;
31import android.support.v4.view.ViewPager.OnPageChangeListener;
32import android.support.v7.app.ActionBar;
33import android.support.v7.widget.Toolbar;
34import android.view.KeyEvent;
35import android.view.Menu;
36import android.view.MenuItem;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.widget.Button;
40import android.widget.ImageView;
41import android.widget.TextView;
42
43import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
44import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
45import com.android.deskclock.actionbarmenu.OptionsMenuManager;
46import com.android.deskclock.actionbarmenu.SettingsMenuItemController;
47import com.android.deskclock.data.DataModel;
48import com.android.deskclock.data.DataModel.SilentSetting;
49import com.android.deskclock.data.OnSilentSettingsListener;
50import com.android.deskclock.events.Events;
51import com.android.deskclock.provider.Alarm;
52import com.android.deskclock.uidata.TabListener;
53import com.android.deskclock.uidata.UiDataModel;
54import com.android.deskclock.widget.toast.SnackbarManager;
55
56import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
57import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
58import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
59import static android.text.format.DateUtils.SECOND_IN_MILLIS;
60import 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 */
66public 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
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
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
310    protected void onStart() {
311        super.onStart();
312        DataModel.getDataModel().addSilentSettingsListener(mSilentSettingChangeWatcher);
313        DataModel.getDataModel().setApplicationInForeground(true);
314    }
315
316    @Override
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
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
347    public void onPause() {
348        if (mDropShadowController != null) {
349            mDropShadowController.stop();
350            mDropShadowController = null;
351        }
352
353        super.onPause();
354    }
355
356    @Override
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
367    protected void onDestroy() {
368        UiDataModel.getUiDataModel().removeTabListener(mTabChangeWatcher);
369        super.onDestroy();
370    }
371
372    @Override
373    public boolean onCreateOptionsMenu(Menu menu) {
374        mOptionsMenuManager.onCreateOptionsMenu(menu);
375        return true;
376    }
377
378    @Override
379    public boolean onPrepareOptionsMenu(Menu menu) {
380        super.onPrepareOptionsMenu(menu);
381        mOptionsMenuManager.onPrepareOptionsMenu(menu);
382        return true;
383    }
384
385    @Override
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
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
406    public boolean onKeyDown(int keyCode, KeyEvent event) {
407        return getSelectedDeskClockFragment().onKeyDown(keyCode,event)
408                || super.onKeyDown(keyCode, event);
409    }
410
411    @Override
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
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     */
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     */
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     */
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
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
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
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
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
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
626        private ShowSilentSettingSnackbarRunnable(SilentSetting silentSetting) {
627            mSilentSetting = silentSetting;
628        }
629
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
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}