Java tutorial
/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.wizardsofm.deskclock; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Fragment; import android.app.FragmentManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.media.AudioManager; import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.Settings; import android.support.annotation.ColorInt; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout.ViewPagerOnTabSelectedListener; import android.support.v13.app.ActivityCompat; import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.Toast; import com.wizardsofm.deskclock.actionbarmenu.MenuItemControllerFactory; import com.wizardsofm.deskclock.actionbarmenu.NightModeMenuItemController; import com.wizardsofm.deskclock.actionbarmenu.OptionsMenuManager; import com.wizardsofm.deskclock.actionbarmenu.SettingsMenuItemController; import com.wizardsofm.deskclock.alarms.AlarmStateManager; import com.wizardsofm.deskclock.data.DataModel; import com.wizardsofm.deskclock.events.Events; import com.wizardsofm.deskclock.provider.Alarm; import com.wizardsofm.deskclock.uidata.TabListener; import com.wizardsofm.deskclock.uidata.UiDataModel; import com.wizardsofm.deskclock.widget.RtlViewPager; import com.wizardsofm.deskclock.widget.toast.SnackbarManager; import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED; import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; import static android.media.AudioManager.FLAG_SHOW_UI; import static android.media.AudioManager.STREAM_ALARM; import static android.media.RingtoneManager.TYPE_ALARM; import static android.provider.Settings.System.CONTENT_URI; import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI; import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING; import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE; import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.wizardsofm.deskclock.AnimatorUtils.getAlphaAnimator; import static com.wizardsofm.deskclock.AnimatorUtils.getScaleAnimator; import static com.wizardsofm.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_IMMEDIATE; /** * The main activity of the application which displays 4 different tabs contains alarms, world * clocks, timers and a stopwatch. */ public class DeskClock extends BaseActivity implements FabContainer, LabelDialogFragment.AlarmLabelDialogHandler { /** The Uri to the settings entry that stores alarm stream volume. */ private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker"); /** The intent filter that identifies do-not-disturb change broadcasts. */ @SuppressLint("NewApi") private static final IntentFilter DND_CHANGE_FILTER = new IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED); /** Models the interesting state of display the {@link #mFab} button may inhabit. */ private enum FabState { SHOWING, HIDE_ARMED, HIDING } /** Coordinates handling of context menu items. */ private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager(); /** Shrinks the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to nothing. */ private final AnimatorSet mHideAnimation = new AnimatorSet(); /** Grows the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to natural sizes. */ private final AnimatorSet mShowAnimation = new AnimatorSet(); /** Hides, updates, and shows only the {@link #mFab}; the buttons are untouched. */ private final AnimatorSet mUpdateFabOnlyAnimation = new AnimatorSet(); /** Automatically starts the {@link #mShowAnimation} after {@link #mHideAnimation} ends. */ private final AnimatorListenerAdapter mAutoStartShowListener = new AutoStartShowListener(); /** Updates the user interface to reflect the selected tab from the backing model. */ private final TabListener mTabChangeWatcher = new TabChangeWatcher(); /** Displays a snackbar explaining that the system default alarm ringtone is silent. */ private final Runnable mShowSilentAlarmSnackbarRunnable = new ShowSilentAlarmSnackbarRunnable(); /** Observes default alarm ringtone changes while the app is in the foreground. */ private final ContentObserver mAlarmRingtoneChangeObserver = new AlarmRingtoneChangeObserver(); /** Displays a snackbar explaining that the alarm volume is muted. */ private final Runnable mShowMutedVolumeSnackbarRunnable = new ShowMutedVolumeSnackbarRunnable(); /** Observes alarm volume changes while the app is in the foreground. */ private final ContentObserver mAlarmVolumeChangeObserver = new AlarmVolumeChangeObserver(); /** Displays a snackbar explaining that do-not-disturb is blocking alarms. */ private final Runnable mShowDNDBlockingSnackbarRunnable = new ShowDNDBlockingSnackbarRunnable(); /** Observes do-not-disturb changes while the app is in the foreground. */ private final BroadcastReceiver mDoNotDisturbChangeReceiver = new DoNotDisturbChangeReceiver(); /** Used to query the alarm volume and display the system control to change the alarm volume. */ private AudioManager mAudioManager; /** Used to query the do-not-disturb setting value, also called "interruption filter". */ private NotificationManager mNotificationManager; /** {@code true} permits the muted alarm volume snackbar to show when starting this activity. */ private boolean mShowSilencedAlarmsSnackbar; /** The view to which snackbar items are anchored. */ private View mSnackbarAnchor; /** The current display state of the {@link #mFab}. */ private FabState mFabState = FabState.SHOWING; /** The single floating-action button shared across all tabs in the user interface. */ private ImageView mFab; /** The button left of the {@link #mFab} shared across all tabs in the user interface. */ private ImageButton mLeftButton; /** The button right of the {@link #mFab} shared across all tabs in the user interface. */ private ImageButton mRightButton; /** The controller that shows the drop shadow when content is not scrolled to the top. */ private DropShadowController mDropShadowController; /** The ViewPager that pages through the fragments representing the content of the tabs. */ private RtlViewPager mFragmentTabPager; /** Generates the fragments that are displayed by the {@link #mFragmentTabPager}. */ private TabFragmentAdapter mFragmentTabPagerAdapter; /** The container that stores the tab headers. */ private TabLayout mTabLayout; /** {@code true} when a settings change necessitates recreating this activity. */ private boolean mRecreateActivity; @Override public void onNewIntent(Intent newIntent) { super.onNewIntent(newIntent); // Fragments may query the latest intent for information, so update the intent. setIntent(newIntent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(com.wizardsofm.deskclock.R.layout.desk_clock); mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // Don't show the volume muted snackbar on rotations. mShowSilencedAlarmsSnackbar = savedInstanceState == null; mSnackbarAnchor = findViewById(com.wizardsofm.deskclock.R.id.coordinator); // Configure the toolbar. final Toolbar toolbar = (Toolbar) findViewById(com.wizardsofm.deskclock.R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayShowTitleEnabled(false); // Configure the menu item controllers add behavior to the toolbar. mOptionsMenuManager.addMenuItemController(new NightModeMenuItemController(this)) .addMenuItemController(new SettingsMenuItemController(this)) .addMenuItemController(MenuItemControllerFactory.getInstance().buildMenuItemControllers(this)); // Inflate the menu during creation to avoid a double layout pass. Otherwise, the menu // inflation occurs *after* the initial draw and a second layout pass adds in the menu. onCreateOptionsMenu(toolbar.getMenu()); // Create the tabs that make up the user interface. mTabLayout = (TabLayout) findViewById(com.wizardsofm.deskclock.R.id.sliding_tabs); for (int i = 0; i < UiDataModel.getUiDataModel().getTabCount(); i++) { final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTab(i); mTabLayout.addTab(mTabLayout.newTab().setIcon(tab.getIconId()) .setContentDescription(tab.getContentDescriptionId())); } // Configure the buttons shared by the tabs. mFab = (ImageView) findViewById(com.wizardsofm.deskclock.R.id.fab); mLeftButton = (ImageButton) findViewById(com.wizardsofm.deskclock.R.id.left_button); mRightButton = (ImageButton) findViewById(com.wizardsofm.deskclock.R.id.right_button); mFab.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { getSelectedDeskClockFragment().onFabClick(mFab); } }); mLeftButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { getSelectedDeskClockFragment().onLeftButtonClick(mLeftButton); } }); mRightButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { getSelectedDeskClockFragment().onRightButtonClick(mRightButton); } }); // Build the reusable animations that hide and show the fab and left/right buttons. // These may be used independently or be chained together. final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration(); mHideAnimation.setDuration(duration).play(getScaleAnimator(mFab, 1f, 0f)) .with(getAlphaAnimator(mLeftButton, 1f, 0f)).with(getAlphaAnimator(mRightButton, 1f, 0f)); mShowAnimation.setDuration(duration).play(getScaleAnimator(mFab, 0f, 1f)) .with(getAlphaAnimator(mLeftButton, 0f, 1f)).with(getAlphaAnimator(mRightButton, 0f, 1f)); // Build the reusable animation that hides and shows only the fab. final ValueAnimator hideFabAnimation = getScaleAnimator(mFab, 1f, 0f); hideFabAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { getSelectedDeskClockFragment().onUpdateFab(mFab); } }); final ValueAnimator showFabAnimation = getScaleAnimator(mFab, 0f, 1f); mUpdateFabOnlyAnimation.setDuration(duration).play(showFabAnimation).after(hideFabAnimation); // Customize the view pager. mFragmentTabPagerAdapter = new TabFragmentAdapter(this); mFragmentTabPager = (RtlViewPager) findViewById(com.wizardsofm.deskclock.R.id.desk_clock_pager); // Keep all four tabs to minimize jank. mFragmentTabPager.setOffscreenPageLimit(3); // Set Accessibility Delegate to null so view pager doesn't intercept movements and // prevent the fab from being selected. mFragmentTabPager.setAccessibilityDelegate(null); // Mirror changes made to the selected page of the view pager into UiDataModel. mFragmentTabPager.setOnRTLPageChangeListener(new PageChangeWatcher()); mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter); // Selecting a tab implicitly selects a page in the view pager. mTabLayout.setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(mFragmentTabPager)); // Honor changes to the selected tab from outside entities. UiDataModel.getUiDataModel().addTabListener(mTabChangeWatcher); // Update the next alarm time on app startup because the user might have altered the data. AlarmStateManager.updateNextAlarm(this); if (savedInstanceState == null) { // Set the background color to initially match the theme value so that we can // smoothly transition to the dynamic color. final int backgroundColor = ContextCompat.getColor(this, com.wizardsofm.deskclock.R.color.default_background); adjustAppColor(backgroundColor, false /* animate */); } // request permission to use microphone ActivityCompat.requestPermissions(this, new String[] { android.Manifest.permission.RECORD_AUDIO }, 1); } // handle voice command permission result for microphone usage @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case 1: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { } else { Toast.makeText(this, "Permission denied to snooze by voice command", Toast.LENGTH_SHORT).show(); } return; } } } @Override protected void onStart() { super.onStart(); if (mShowSilencedAlarmsSnackbar) { if (isDoNotDisturbBlockingAlarms()) { mSnackbarAnchor.postDelayed(mShowDNDBlockingSnackbarRunnable, SECOND_IN_MILLIS); } else if (isAlarmStreamMuted()) { mSnackbarAnchor.postDelayed(mShowMutedVolumeSnackbarRunnable, SECOND_IN_MILLIS); } else if (isSystemAlarmRingtoneSilent()) { mSnackbarAnchor.postDelayed(mShowSilentAlarmSnackbarRunnable, SECOND_IN_MILLIS); } } // Subsequent starts of this activity should show the snackbar by default. mShowSilencedAlarmsSnackbar = true; final ContentResolver cr = getContentResolver(); // Watch for system alarm ringtone changes while the app is in the foreground. cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, mAlarmRingtoneChangeObserver); // Watch for alarm volume changes while the app is in the foreground. cr.registerContentObserver(VOLUME_URI, false, mAlarmVolumeChangeObserver); if (Utils.isMOrLater()) { // Watch for do-not-disturb changes while the app is in the foreground. registerReceiver(mDoNotDisturbChangeReceiver, DND_CHANGE_FILTER); } DataModel.getDataModel().setApplicationInForeground(true); } @Override protected void onResume() { super.onResume(); final View dropShadow = findViewById(com.wizardsofm.deskclock.R.id.drop_shadow); mDropShadowController = new DropShadowController(dropShadow, UiDataModel.getUiDataModel()); // Honor the selected tab in case it changed while the app was paused. updateCurrentTab(UiDataModel.getUiDataModel().getSelectedTabIndex()); } @Override protected void onPostResume() { super.onPostResume(); if (mRecreateActivity) { mRecreateActivity = false; // A runnable must be posted here or the new DeskClock activity will be recreated in a // paused state, even though it is the foreground activity. mFragmentTabPager.post(new Runnable() { @Override public void run() { recreate(); } }); } } @Override public void onPause() { mDropShadowController.stop(); mDropShadowController = null; super.onPause(); } @Override protected void onStop() { if (!isChangingConfigurations()) { DataModel.getDataModel().setApplicationInForeground(false); } // Stop watching for system alarm ringtone changes while the app is in the background. getContentResolver().unregisterContentObserver(mAlarmRingtoneChangeObserver); // Stop watching for alarm volume changes while the app is in the background. getContentResolver().unregisterContentObserver(mAlarmVolumeChangeObserver); if (Utils.isMOrLater()) { // Stop watching for do-not-disturb changes while the app is in the background. unregisterReceiver(mDoNotDisturbChangeReceiver); } // Remove any scheduled work to show snackbars; it is no longer relevant. mSnackbarAnchor.removeCallbacks(mShowSilentAlarmSnackbarRunnable); mSnackbarAnchor.removeCallbacks(mShowDNDBlockingSnackbarRunnable); mSnackbarAnchor.removeCallbacks(mShowMutedVolumeSnackbarRunnable); super.onStop(); } @Override protected void onDestroy() { UiDataModel.getUiDataModel().removeTabListener(mTabChangeWatcher); super.onDestroy(); } @Override public boolean onCreateOptionsMenu(Menu menu) { mOptionsMenuManager.onCreateOptionsMenu(menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); mOptionsMenuManager.onPrepareOptionsMenu(menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); } /** * Called by the LabelDialogFormat class after the dialog is finished. */ @Override public void onDialogLabelSet(Alarm alarm, String label, String tag) { final Fragment frag = getFragmentManager().findFragmentByTag(tag); if (frag instanceof AlarmClockFragment) { ((AlarmClockFragment) frag).setLabel(alarm, label); } } /** * Listens for keyboard activity for the tab fragments to handle if necessary. A tab may want to * respond to key presses even if they are not currently focused. */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (getSelectedDeskClockFragment().onKeyDown(keyCode, event)) { return true; } return super.onKeyDown(keyCode, event); } /** * @param color the newly installed window background color */ @Override protected void onAppColorChanged(@ColorInt int color) { super.onAppColorChanged(color); // Notify each fragment of the background color change. for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) { mFragmentTabPagerAdapter.getItem(i).onAppColorChanged(color); } } @Override public void updateFab(UpdateType updateType) { switch (updateType) { case DISABLE_BUTTONS: { mLeftButton.setEnabled(false); mRightButton.setEnabled(false); break; } case FAB_AND_BUTTONS_IMMEDIATE: { final DeskClockFragment f = getSelectedDeskClockFragment(); f.onUpdateFab(mFab); f.onUpdateFabButtons(mLeftButton, mRightButton); break; } case FAB_AND_BUTTONS_MORPH: { final DeskClockFragment f = getSelectedDeskClockFragment(); f.onUpdateFab(mFab); f.onMorphFabButtons(mLeftButton, mRightButton); break; } case FAB_ONLY_SHRINK_AND_EXPAND: { mUpdateFabOnlyAnimation.start(); break; } case FAB_AND_BUTTONS_SHRINK_AND_EXPAND: { // Ensure there is never more than one mAutoStartShowListener registered. mHideAnimation.removeListener(mAutoStartShowListener); mHideAnimation.addListener(mAutoStartShowListener); mHideAnimation.start(); break; } case FAB_REQUESTS_FOCUS: { mFab.requestFocus(); break; } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Recreate the activity if any settings have been changed if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS && resultCode == RESULT_OK) { mRecreateActivity = true; } } /** * Configure the {@link #mFragmentTabPager} and {@link #mTabLayout} to display the tab at the * given {@code index}. * * @param index the index of the page to display */ private void updateCurrentTab(int index) { final TabLayout.Tab tab = mTabLayout.getTabAt(index); if (tab != null && !tab.isSelected()) { tab.select(); } if (mFragmentTabPager.getCurrentItem() != index) { mFragmentTabPager.setCurrentItem(index); } } private DeskClockFragment getSelectedDeskClockFragment() { final int index = UiDataModel.getUiDataModel().getSelectedTabIndex(); return mFragmentTabPagerAdapter.getItem(index); } private boolean isSystemAlarmRingtoneSilent() { return RingtoneManager.getActualDefaultRingtoneUri(this, TYPE_ALARM) == null; } private void showSilentRingtoneSnackbar() { final OnClickListener changeClickListener = new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } }; SnackbarManager.show(createSnackbar(com.wizardsofm.deskclock.R.string.silent_default_alarm_ringtone) .setAction(com.wizardsofm.deskclock.R.string.change_default_alarm_ringtone, changeClickListener)); } private boolean isAlarmStreamMuted() { return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0; } private void showAlarmVolumeMutedSnackbar() { final OnClickListener unmuteClickListener = new OnClickListener() { @Override public void onClick(View v) { // Set the alarm volume to ~30% of max and show the slider UI. final int index = mAudioManager.getStreamMaxVolume(STREAM_ALARM) / 3; mAudioManager.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI); } }; SnackbarManager.show(createSnackbar(com.wizardsofm.deskclock.R.string.alarm_volume_muted) .setAction(com.wizardsofm.deskclock.R.string.unmute_alarm_volume, unmuteClickListener)); } @TargetApi(Build.VERSION_CODES.M) private boolean isDoNotDisturbBlockingAlarms() { if (!Utils.isMOrLater()) { return false; } return mNotificationManager.getCurrentInterruptionFilter() == INTERRUPTION_FILTER_NONE; } private void showDoNotDisturbIsBlockingAlarmsSnackbar() { SnackbarManager.show(createSnackbar(com.wizardsofm.deskclock.R.string.alarms_blocked_by_dnd)); } /** * @return a Snackbar that displays the message with the given id for 5 seconds */ private Snackbar createSnackbar(@StringRes int messageId) { return Snackbar.make(mSnackbarAnchor, messageId, 5000 /* duration */); } /** * As the view pager changes the selected page, update the model to record the new selected tab. */ private final class PageChangeWatcher implements OnPageChangeListener { /** The last reported page scroll state; used to detect exotic state changes. */ private int mPriorState = SCROLL_STATE_IDLE; public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // Only hide the fab when a non-zero drag distance is detected. This prevents // over-scrolling from needlessly hiding the fab. if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) { mFabState = FabState.HIDING; mHideAnimation.start(); } } @Override public void onPageScrollStateChanged(int state) { if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) { // The user has tapped a tab button; play the hide and show animations linearly. mHideAnimation.addListener(mAutoStartShowListener); mHideAnimation.start(); mFabState = FabState.HIDING; } else if (mPriorState == SCROLL_STATE_SETTLING && state == SCROLL_STATE_DRAGGING) { // The user has interrupted settling on a tab and the fab button must be re-hidden. if (mShowAnimation.isStarted()) { mShowAnimation.cancel(); } if (mHideAnimation.isStarted()) { // Let the hide animation finish naturally; don't auto show when it ends. mHideAnimation.removeListener(mAutoStartShowListener); } else { // Start and immediately end the hide animation to jump to the hidden state. mHideAnimation.start(); mHideAnimation.end(); } mFabState = FabState.HIDING; } else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) { // The user has lifted their finger; show the buttons now or after hide ends. if (mHideAnimation.isStarted()) { // Finish the hide animation and then start the show animation. mHideAnimation.addListener(mAutoStartShowListener); } else { updateFab(FAB_AND_BUTTONS_IMMEDIATE); mShowAnimation.start(); // The animation to show the fab has begun; update the state to showing. mFabState = FabState.SHOWING; } } else if (state == SCROLL_STATE_DRAGGING) { // The user has started a drag so arm the hide animation. mFabState = FabState.HIDE_ARMED; } // Update the last known state. mPriorState = state; } @Override public void onPageSelected(int position) { UiDataModel.getUiDataModel().setSelectedTabIndex(position); } } /** * If this listener is attached to {@link #mHideAnimation} when it ends, the corresponding * {@link #mShowAnimation} is automatically started. */ private final class AutoStartShowListener extends AnimatorListenerAdapter { @Override public void onAnimationEnd(Animator animation) { // Prepare the hide animation for its next use; by default do not auto-show after hide. mHideAnimation.removeListener(mAutoStartShowListener); // Update the buttons now that they are no longer visible. updateFab(FAB_AND_BUTTONS_IMMEDIATE); // Automatically start the grow animation now that shrinking is complete. mShowAnimation.start(); // The animation to show the fab has begun; update the state to showing. mFabState = FabState.SHOWING; } } /** * Displays a snackbar that indicates the system default alarm ringtone currently silent and * offers an action that displays the system alarm ringtone setting to adjust it. */ private final class ShowSilentAlarmSnackbarRunnable implements Runnable { @Override public void run() { showSilentRingtoneSnackbar(); } } /** * Displays a snackbar that indicates the alarm volume is currently muted and offers an action * that displays the system volume control to adjust it. */ private final class ShowMutedVolumeSnackbarRunnable implements Runnable { @Override public void run() { showAlarmVolumeMutedSnackbar(); } } /** * Displays a snackbar that indicates the do-not-disturb setting is currently blocking alarms. */ private final class ShowDNDBlockingSnackbarRunnable implements Runnable { @Override public void run() { showDoNotDisturbIsBlockingAlarmsSnackbar(); } } /** * Observe changes to the system default alarm ringtone while the application is in the * foreground and show/hide the snackbar that warns when the ringtone is silent. */ private final class AlarmRingtoneChangeObserver extends ContentObserver { private AlarmRingtoneChangeObserver() { super(new Handler()); } @Override public void onChange(boolean selfChange) { if (isSystemAlarmRingtoneSilent()) { showSilentRingtoneSnackbar(); } else { SnackbarManager.dismiss(); } } } /** * Observe changes to the alarm stream volume while the application is in the foreground and * show/hide the snackbar that warns when the alarm volume is muted. */ private final class AlarmVolumeChangeObserver extends ContentObserver { private AlarmVolumeChangeObserver() { super(new Handler()); } @Override public void onChange(boolean selfChange) { if (isAlarmStreamMuted()) { showAlarmVolumeMutedSnackbar(); } else { SnackbarManager.dismiss(); } } } /** * Observe changes to the do-not-disturb setting while the application is in the foreground * and show/hide the snackbar that warns when the setting is blocking alarms. */ private final class DoNotDisturbChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (isDoNotDisturbBlockingAlarms()) { showDoNotDisturbIsBlockingAlarmsSnackbar(); } else { SnackbarManager.dismiss(); } } } /** * As the model reports changes to the selected tab, update the user interface. */ private final class TabChangeWatcher implements TabListener { @Override public void selectedTabChanged(UiDataModel.Tab oldSelectedTab, UiDataModel.Tab newSelectedTab) { final int index = newSelectedTab.ordinal(); // Update the view pager and tab layout to agree with the model. updateCurrentTab(index); // Avoid sending events for the initial tab selection on launch and re-selecting a tab // after a configuration change. if (DataModel.getDataModel().isApplicationInForeground()) { switch (newSelectedTab) { case ALARMS: Events.sendAlarmEvent(com.wizardsofm.deskclock.R.string.action_show, com.wizardsofm.deskclock.R.string.label_deskclock); break; case CLOCKS: Events.sendClockEvent(com.wizardsofm.deskclock.R.string.action_show, com.wizardsofm.deskclock.R.string.label_deskclock); break; case TIMERS: Events.sendTimerEvent(com.wizardsofm.deskclock.R.string.action_show, com.wizardsofm.deskclock.R.string.label_deskclock); break; case STOPWATCH: Events.sendStopwatchEvent(com.wizardsofm.deskclock.R.string.action_show, com.wizardsofm.deskclock.R.string.label_deskclock); break; } } // If the hide animation has already completed, the buttons must be updated now when the // new tab is known. Otherwise they are updated at the end of the hide animation. if (!mHideAnimation.isStarted()) { updateFab(FAB_AND_BUTTONS_IMMEDIATE); } } } /** * This adapter produces the DeskClockFragments that are the contents of the tabs. */ private static final class TabFragmentAdapter extends FragmentPagerAdapter { private final FragmentManager mFragmentManager; private final Context mContext; public TabFragmentAdapter(AppCompatActivity activity) { super(activity.getFragmentManager()); mContext = activity; mFragmentManager = activity.getFragmentManager(); } @Override public Object instantiateItem(ViewGroup container, int position) { position = UiDataModel.getUiDataModel().getTabLayoutIndex(position); return super.instantiateItem(container, position); } @Override public DeskClockFragment getItem(int position) { final String tag = makeFragmentName(com.wizardsofm.deskclock.R.id.desk_clock_pager, position); Fragment fragment = mFragmentManager.findFragmentByTag(tag); if (fragment == null) { final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTab(position); final String fragmentClassName = tab.getFragmentClassName(); fragment = Fragment.instantiate(mContext, fragmentClassName); } return (DeskClockFragment) fragment; } @Override public int getCount() { return UiDataModel.getUiDataModel().getTabCount(); } /** This implementation duplicated from {@link FragmentPagerAdapter#makeFragmentName}. */ private String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; } } }