Home | History | Annotate | Download | only in deskclock
      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 android.support.annotation.StringRes;
     28 import android.support.design.widget.Snackbar;
     29 import android.support.design.widget.TabLayout;
     30 import android.support.v4.view.ViewPager;
     31 import android.support.v4.view.ViewPager.OnPageChangeListener;
     32 import android.support.v7.app.ActionBar;
     33 import android.support.v7.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 android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
     57 import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
     58 import static android.support.v4.view.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
    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 }