blob: 02a65ea623a2ed42f79829fc3b15663af7f60161 [file] [log] [blame]
/*
* 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.android.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.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.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 com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
import com.android.deskclock.actionbarmenu.OptionsMenuManager;
import com.android.deskclock.actionbarmenu.SettingsMenuItemController;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.uidata.TabListener;
import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.uidata.UiDataModel.Tab;
import com.android.deskclock.widget.RtlViewPager;
import com.android.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.android.deskclock.AnimatorUtils.getAlphaAnimator;
import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
import static com.android.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(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(R.id.coordinator);
// Configure the toolbar.
final Toolbar toolbar = (Toolbar) findViewById(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(R.id.sliding_tabs);
for (int i = 0; i < UiDataModel.getUiDataModel().getTabCount(); i++) {
final 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(R.id.fab);
mLeftButton = (ImageButton) findViewById(R.id.left_button);
mRightButton = (ImageButton) findViewById(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(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);
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, R.color.default_background);
adjustAppColor(backgroundColor, false /* animate */);
}
}
@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(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() {
if (mDropShadowController != null) {
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() {
try {
return RingtoneManager.getActualDefaultRingtoneUri(this, TYPE_ALARM) == null;
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
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(R.string.silent_default_alarm_ringtone)
.setAction(R.string.change_default_alarm_ringtone, changeClickListener)
);
}
private boolean isAlarmStreamMuted() {
try {
return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0;
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
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(R.string.alarm_volume_muted)
.setAction(R.string.unmute_alarm_volume, unmuteClickListener)
);
}
@TargetApi(Build.VERSION_CODES.M)
private boolean isDoNotDisturbBlockingAlarms() {
if (!Utils.isMOrLater()) {
return false;
}
try {
return mNotificationManager.getCurrentInterruptionFilter() == INTERRUPTION_FILTER_NONE;
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
private void showDoNotDisturbIsBlockingAlarmsSnackbar() {
SnackbarManager.show(createSnackbar(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(Tab oldSelectedTab, 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(R.string.action_show, R.string.label_deskclock);
break;
case CLOCKS:
Events.sendClockEvent(R.string.action_show, R.string.label_deskclock);
break;
case TIMERS:
Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock);
break;
case STOPWATCH:
Events.sendStopwatchEvent(R.string.action_show, 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;
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(R.id.desk_clock_pager, position);
Fragment fragment = mFragmentManager.findFragmentByTag(tag);
if (fragment == null) {
final 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;
}
}
}