// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.snackbar; import android.app.Activity; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.device.DeviceClassManager; /** * Manager for the snackbar showing at the bottom of activity. There should be only one * SnackbarManager and one snackbar in the activity. * <p/> * When action button is clicked, this manager will call {@link SnackbarController#onAction(Object)} * in corresponding listener, and show the next entry. Otherwise if no action is taken by user * during {@link #DEFAULT_SNACKBAR_DURATION_MS} milliseconds, it will call * {@link SnackbarController#onDismissNoAction(Object)}. */ public class SnackbarManager implements OnClickListener { /** * Interface that shows the ability to provide a snackbar manager. Activities implementing this * interface must call {@link SnackbarManager#onStart()} and {@link SnackbarManager#onStop()} in * corresponding lifecycle events. */ public interface SnackbarManageable { /** * @return The snackbar manager that has a proper anchor view. */ SnackbarManager getSnackbarManager(); } /** * Controller that post entries to snackbar manager and interact with snackbar manager during * dismissal and action click event. */ public interface SnackbarController { /** * Called when the user clicks the action button on the snackbar. * @param actionData Data object passed when showing this specific snackbar. */ void onAction(Object actionData); /** * Called when the snackbar is dismissed by tiemout or UI enviroment change. * @param actionData Data object associated with the dismissed snackbar entry. */ void onDismissNoAction(Object actionData); } private static final int DEFAULT_SNACKBAR_DURATION_MS = 3000; private static final int ACCESSIBILITY_MODE_SNACKBAR_DURATION_MS = 6000; // Used instead of the constant so tests can override the value. private static int sSnackbarDurationMs = DEFAULT_SNACKBAR_DURATION_MS; private static int sAccessibilitySnackbarDurationMs = ACCESSIBILITY_MODE_SNACKBAR_DURATION_MS; private Activity mActivity; private SnackbarView mView; private final Handler mUIThreadHandler; private SnackbarCollection mSnackbars = new SnackbarCollection(); private boolean mActivityInForeground; private boolean mIsDisabledForTesting; private final Runnable mHideRunnable = new Runnable() { @Override public void run() { mSnackbars.removeCurrentDueToTimeout(); updateView(); } }; /** * Constructs a SnackbarManager to show snackbars in the given window. * @param activity The embedding activity. */ public SnackbarManager(Activity activity) { mActivity = activity; mUIThreadHandler = new Handler(); } /** * Notifies the snackbar manager that the activity is running in foreground now. */ public void onStart() { mActivityInForeground = true; } /** * Notifies the snackbar manager that the activity has been pushed to background. */ public void onStop() { mSnackbars.clear(); updateView(); mActivityInForeground = false; } /** * Shows a snackbar at the bottom of the screen, or above the keyboard if the keyboard is * visible. */ public void showSnackbar(Snackbar snackbar) { if (!mActivityInForeground || mIsDisabledForTesting) return; RecordHistogram.recordSparseSlowlyHistogram("Snackbar.Shown", snackbar.getIdentifier()); mSnackbars.add(snackbar); updateView(); mView.announceforAccessibility(); } /** * Dismisses snackbars that are associated with the given {@link SnackbarController}. * * @param controller Only snackbars with this controller will be removed. */ public void dismissSnackbars(SnackbarController controller) { if (mSnackbars.removeMatchingSnackbars(controller)) { updateView(); } } /** * Dismisses snackbars that have a certain controller and action data. * * @param controller Only snackbars with this controller will be removed. * @param actionData Only snackbars whose action data is equal to actionData will be removed. */ public void dismissSnackbars(SnackbarController controller, Object actionData) { if (mSnackbars.removeMatchingSnackbars(controller, actionData)) { updateView(); } } /** * Handles click event for action button at end of snackbar. */ @Override public void onClick(View v) { mSnackbars.removeCurrentDueToAction(); updateView(); } /** * Temporarily changes the parent {@link ViewGroup} of the snackbar. If a snackbar is currently * showing, this method removes the snackbar from its original parent, and attaches it to the * given parent. If <code>null</code> is given, the snackbar will be reattached to its original * parent. * * @param overridingParent The temporary parent of the snackbar. If null, previous calls of this * method will be reverted. */ public void overrideParent(ViewGroup overridingParent) { if (mView != null) mView.overrideParent(overridingParent); } /** * @return Whether there is a snackbar on screen. */ public boolean isShowing() { return mView != null && mView.isShowing(); } /** * Updates the {@link SnackbarView} to reflect the value of mSnackbars.currentSnackbar(), which * may be null. This might show, change, or hide the view. */ private void updateView() { if (!mActivityInForeground) return; Snackbar currentSnackbar = mSnackbars.getCurrent(); if (currentSnackbar == null) { mUIThreadHandler.removeCallbacks(mHideRunnable); if (mView != null) { mView.dismiss(); mView = null; } } else { boolean viewChanged = true; if (mView == null) { mView = new SnackbarView(mActivity, this, currentSnackbar); mView.show(); } else { viewChanged = mView.update(currentSnackbar); } if (viewChanged) { int durationMs = getDuration(currentSnackbar); mUIThreadHandler.removeCallbacks(mHideRunnable); mUIThreadHandler.postDelayed(mHideRunnable, durationMs); mView.announceforAccessibility(); } } } private int getDuration(Snackbar snackbar) { int durationMs = snackbar.getDuration(); if (durationMs == 0) { durationMs = DeviceClassManager.isAccessibilityModeEnabled(mActivity) ? sAccessibilitySnackbarDurationMs : sSnackbarDurationMs; } return durationMs; } /** * Disables the snackbar manager. This is only intented for testing purposes. */ @VisibleForTesting public void disableForTesting() { mIsDisabledForTesting = true; } /** * Overrides the default snackbar duration with a custom value for testing. * @param durationMs The duration to use in ms. */ @VisibleForTesting public static void setDurationForTesting(int durationMs) { sSnackbarDurationMs = durationMs; sAccessibilitySnackbarDurationMs = durationMs; } /** * @return The currently showing snackbar. For testing only. */ @VisibleForTesting Snackbar getCurrentSnackbarForTesting() { return mSnackbars.getCurrent(); } }