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 }