Commit d117eb1c authored by ianwen's avatar ianwen Committed by Commit bot

Introduce Queue-Based Notification Snackbars

Notification snackbars are low-priority snackbars that are stored in a
queue, contrary to tranditional "action" snackbars that ought to be
shown immediately and are stored in a stack.

When a queue snackbar is showing, a stack snackbar can always kick in
and override the snackbar; however when a stack snackbar is showing, a
queue snackbar will wait till the stack is cleared.

Queue snackbars are not dismissed in batch; instead, they will be shown
one by one, in FIFO order.

BUG=579347

Review URL: https://codereview.chromium.org/1635753002

Cr-Commit-Position: refs/heads/master@{#371899}
parent 1f3c2431
...@@ -52,8 +52,9 @@ public class DownloadSnackbarController implements SnackbarManager.SnackbarContr ...@@ -52,8 +52,9 @@ public class DownloadSnackbarController implements SnackbarManager.SnackbarContr
public void onDownloadSucceeded( public void onDownloadSucceeded(
DownloadInfo downloadInfo, final long downloadId, boolean canBeResolved) { DownloadInfo downloadInfo, final long downloadId, boolean canBeResolved) {
if (getSnackbarManager() == null) return; if (getSnackbarManager() == null) return;
Snackbar snackbar = Snackbar.make(mContext.getString( Snackbar snackbar = Snackbar.make(
R.string.download_succeeded_message, downloadInfo.getFileName()), this); mContext.getString(R.string.download_succeeded_message, downloadInfo.getFileName()),
this, Snackbar.TYPE_NOTIFICATION);
// TODO(qinmin): Coalesce snackbars if multiple downloads finish at the same time. // TODO(qinmin): Coalesce snackbars if multiple downloads finish at the same time.
snackbar.setDuration(SNACKBAR_DURATION_IN_MILLISECONDS).setSingleLine(false); snackbar.setDuration(SNACKBAR_DURATION_IN_MILLISECONDS).setSingleLine(false);
Pair<DownloadInfo, Long> actionData = null; Pair<DownloadInfo, Long> actionData = null;
...@@ -75,7 +76,7 @@ public class DownloadSnackbarController implements SnackbarManager.SnackbarContr ...@@ -75,7 +76,7 @@ public class DownloadSnackbarController implements SnackbarManager.SnackbarContr
public void onDownloadFailed(String errorMessage, boolean showAllDownloads) { public void onDownloadFailed(String errorMessage, boolean showAllDownloads) {
if (getSnackbarManager() == null) return; if (getSnackbarManager() == null) return;
// TODO(qinmin): Coalesce snackbars if multiple downloads finish at the same time. // TODO(qinmin): Coalesce snackbars if multiple downloads finish at the same time.
Snackbar snackbar = Snackbar.make(errorMessage, this) Snackbar snackbar = Snackbar.make(errorMessage, this, Snackbar.TYPE_NOTIFICATION)
.setSingleLine(false) .setSingleLine(false)
.setDuration(SNACKBAR_DURATION_IN_MILLISECONDS); .setDuration(SNACKBAR_DURATION_IN_MILLISECONDS);
if (showAllDownloads) { if (showAllDownloads) {
......
...@@ -83,12 +83,13 @@ public class EnhancedBookmarkUndoController extends BookmarkModelObserver implem ...@@ -83,12 +83,13 @@ public class EnhancedBookmarkUndoController extends BookmarkModelObserver implem
if (!isUndoable) return; if (!isUndoable) return;
if (titles.length == 1) { if (titles.length == 1) {
mSnackbarManager.showSnackbar(Snackbar.make(titles[0], this) mSnackbarManager.showSnackbar(Snackbar.make(titles[0], this, Snackbar.TYPE_ACTION)
.setTemplateText(mContext.getString(R.string.undo_bar_delete_message)) .setTemplateText(mContext.getString(R.string.undo_bar_delete_message))
.setAction(mContext.getString(R.string.undo_bar_button_text), null)); .setAction(mContext.getString(R.string.undo_bar_button_text), null));
} else { } else {
mSnackbarManager.showSnackbar( mSnackbarManager.showSnackbar(
Snackbar.make(String.format(Locale.getDefault(), "%d", titles.length), this) Snackbar.make(String.format(Locale.getDefault(), "%d", titles.length), this,
Snackbar.TYPE_ACTION)
.setTemplateText(mContext.getString(R.string.undo_bar_multiple_delete_message)) .setTemplateText(mContext.getString(R.string.undo_bar_multiple_delete_message))
.setAction(mContext.getString(R.string.undo_bar_button_text), null)); .setAction(mContext.getString(R.string.undo_bar_button_text), null));
} }
......
...@@ -119,9 +119,9 @@ public class EnhancedBookmarkUtils { ...@@ -119,9 +119,9 @@ public class EnhancedBookmarkUtils {
bookmarkModel, activity, bookmarkId); bookmarkModel, activity, bookmarkId);
if (getLastUsedParent(activity) == null) { if (getLastUsedParent(activity) == null) {
snackbar = Snackbar.make(activity.getString(R.string.enhanced_bookmark_page_saved), snackbar = Snackbar.make(activity.getString(R.string.enhanced_bookmark_page_saved),
snackbarController); snackbarController, Snackbar.TYPE_ACTION);
} else { } else {
snackbar = Snackbar.make(folderName, snackbarController) snackbar = Snackbar.make(folderName, snackbarController, Snackbar.TYPE_ACTION)
.setTemplateText(activity.getString( .setTemplateText(activity.getString(
R.string.enhanced_bookmark_page_saved_folder)); R.string.enhanced_bookmark_page_saved_folder));
} }
...@@ -164,7 +164,9 @@ public class EnhancedBookmarkUtils { ...@@ -164,7 +164,9 @@ public class EnhancedBookmarkUtils {
snackbarController = createSnackbarControllerForEditButton( snackbarController = createSnackbarControllerForEditButton(
bookmarkModel, activity, bookmarkId); bookmarkModel, activity, bookmarkId);
} }
snackbar = Snackbar.make(activity.getString(messageId, suffix), snackbarController) snackbar = Snackbar
.make(activity.getString(messageId, suffix), snackbarController,
Snackbar.TYPE_ACTION)
.setAction(activity.getString(buttonId), null).setSingleLine(false); .setAction(activity.getString(buttonId), null).setSingleLine(false);
} }
......
...@@ -608,7 +608,7 @@ public class NewTabPage ...@@ -608,7 +608,7 @@ public class NewTabPage
} }
Context context = mNewTabPageView.getContext(); Context context = mNewTabPageView.getContext();
Snackbar snackbar = Snackbar.make(context.getString(R.string.most_visited_item_removed), Snackbar snackbar = Snackbar.make(context.getString(R.string.most_visited_item_removed),
mMostVisitedItemRemovedController) mMostVisitedItemRemovedController, Snackbar.TYPE_ACTION)
.setAction(context.getString(R.string.undo_bar_button_text), url); .setAction(context.getString(R.string.undo_bar_button_text), url);
mTab.getSnackbarManager().showSnackbar(snackbar); mTab.getSnackbarManager().showSnackbar(snackbar);
} }
......
...@@ -59,7 +59,7 @@ public class OfflinePageFreeUpSpaceDialog ...@@ -59,7 +59,7 @@ public class OfflinePageFreeUpSpaceDialog
public void onDismissNoAction(Object actionData) {} public void onDismissNoAction(Object actionData) {}
@Override @Override
public void onAction(Object actionData) {} public void onAction(Object actionData) {}
}); }, Snackbar.TYPE_ACTION);
} }
@Override @Override
......
...@@ -308,8 +308,9 @@ public class OfflinePageUtils { ...@@ -308,8 +308,9 @@ public class OfflinePageUtils {
} }
} }
}; };
Snackbar snackbar = Snackbar.make(context.getString(snackbarTextId), snackbarController) Snackbar snackbar = Snackbar
.setAction(context.getString(actionTextId), buttonType); .make(context.getString(snackbarTextId), snackbarController, Snackbar.TYPE_ACTION)
.setAction(context.getString(actionTextId), buttonType);
activity.getSnackbarManager().showSnackbar(snackbar); activity.getSnackbarManager().showSnackbar(snackbar);
} }
} }
...@@ -83,7 +83,7 @@ public class GeolocationSnackbarController implements SnackbarController { ...@@ -83,7 +83,7 @@ public class GeolocationSnackbarController implements SnackbarController {
int durationMs = DeviceClassManager.isAccessibilityModeEnabled(view.getContext()) int durationMs = DeviceClassManager.isAccessibilityModeEnabled(view.getContext())
? ACCESSIBILITY_SNACKBAR_DURATION_MS : SNACKBAR_DURATION_MS; ? ACCESSIBILITY_SNACKBAR_DURATION_MS : SNACKBAR_DURATION_MS;
final GeolocationSnackbarController controller = new GeolocationSnackbarController(); final GeolocationSnackbarController controller = new GeolocationSnackbarController();
final Snackbar snackbar = Snackbar.make(message, controller) final Snackbar snackbar = Snackbar.make(message, controller, Snackbar.TYPE_ACTION)
.setAction(settings, view) .setAction(settings, view)
.setSingleLine(false) .setSingleLine(false)
.setDuration(durationMs); .setDuration(durationMs);
......
...@@ -39,20 +39,20 @@ public class DataUseSnackbarController implements SnackbarManager.SnackbarContro ...@@ -39,20 +39,20 @@ public class DataUseSnackbarController implements SnackbarManager.SnackbarContro
} }
public void showDataUseTrackingStartedBar() { public void showDataUseTrackingStartedBar() {
mSnackbarManager.showSnackbar(Snackbar.make( mSnackbarManager.showSnackbar(Snackbar
mContext.getString(R.string.data_use_tracking_started_snackbar_message), this) .make(mContext.getString(R.string.data_use_tracking_started_snackbar_message), this,
Snackbar.TYPE_NOTIFICATION)
.setAction(mContext.getString(R.string.data_use_tracking_snackbar_action), .setAction(mContext.getString(R.string.data_use_tracking_snackbar_action),
STARTED_SNACKBAR) STARTED_SNACKBAR));
.setForceDisplay());
DataUseTabUIManager.recordDataUseUIAction(DataUsageUIAction.STARTED_SNACKBAR_SHOWN); DataUseTabUIManager.recordDataUseUIAction(DataUsageUIAction.STARTED_SNACKBAR_SHOWN);
} }
public void showDataUseTrackingEndedBar() { public void showDataUseTrackingEndedBar() {
mSnackbarManager.showSnackbar(Snackbar.make( mSnackbarManager.showSnackbar(Snackbar
mContext.getString(R.string.data_use_tracking_ended_snackbar_message), this) .make(mContext.getString(R.string.data_use_tracking_ended_snackbar_message), this,
Snackbar.TYPE_NOTIFICATION)
.setAction(mContext.getString(R.string.data_use_tracking_snackbar_action), .setAction(mContext.getString(R.string.data_use_tracking_snackbar_action),
ENDED_SNACKBAR) ENDED_SNACKBAR));
.setForceDisplay());
DataUseTabUIManager.recordDataUseUIAction(DataUsageUIAction.ENDED_SNACKBAR_SHOWN); DataUseTabUIManager.recordDataUseUIAction(DataUsageUIAction.ENDED_SNACKBAR_SHOWN);
} }
......
...@@ -95,10 +95,9 @@ public class LoFiBarPopupController implements SnackbarManager.SnackbarControlle ...@@ -95,10 +95,9 @@ public class LoFiBarPopupController implements SnackbarManager.SnackbarControlle
String buttonText = mContext String buttonText = mContext
.getString(isPreview ? R.string.data_reduction_lo_fi_preview_snackbar_action .getString(isPreview ? R.string.data_reduction_lo_fi_preview_snackbar_action
: R.string.data_reduction_lo_fi_snackbar_action); : R.string.data_reduction_lo_fi_snackbar_action);
mSnackbarManager.showSnackbar(Snackbar.make(message, this) mSnackbarManager.showSnackbar(Snackbar.make(message, this, Snackbar.TYPE_NOTIFICATION)
.setAction(buttonText, tab.getId()) .setAction(buttonText, tab.getId())
.setDuration(DEFAULT_LO_FI_SNACKBAR_SHOW_DURATION_MS) .setDuration(DEFAULT_LO_FI_SNACKBAR_SHOW_DURATION_MS));
.setForceDisplay());
DataReductionProxySettings.getInstance().incrementLoFiSnackbarShown(); DataReductionProxySettings.getInstance().incrementLoFiSnackbarShown();
DataReductionProxyUma.dataReductionProxyLoFiUIAction( DataReductionProxyUma.dataReductionProxyLoFiUIAction(
DataReductionProxyUma.ACTION_LOAD_IMAGES_SNACKBAR_SHOWN); DataReductionProxyUma.ACTION_LOAD_IMAGES_SNACKBAR_SHOWN);
......
...@@ -18,6 +18,18 @@ import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController; ...@@ -18,6 +18,18 @@ import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
* .setAction("undo", actionData)); * .setAction("undo", actionData));
*/ */
public class Snackbar { public class Snackbar {
/**
* Snackbars that are created as an immediate response to user's action. These snackbars are
* managed in a stack and will be swiped away altogether after timeout.
*/
public static final int TYPE_ACTION = 0;
/**
* Snackbars that are for notification purposes. These snackbars are stored in a queue and thus
* are of lower priority, compared to {@link #TYPE_ACTION}. Notification snackbars are dismissed
* one by one.
*/
public static final int TYPE_NOTIFICATION = 1;
private SnackbarController mController; private SnackbarController mController;
private CharSequence mText; private CharSequence mText;
...@@ -28,20 +40,23 @@ public class Snackbar { ...@@ -28,20 +40,23 @@ public class Snackbar {
private boolean mSingleLine = true; private boolean mSingleLine = true;
private int mDurationMs; private int mDurationMs;
private Bitmap mProfileImage; private Bitmap mProfileImage;
private boolean mForceDisplay = false; private int mType;
// Prevent instantiation. // Prevent instantiation.
private Snackbar() {} private Snackbar() {}
/** /**
* Creates and returns a snackbar to display the given text. * Creates and returns a snackbar to display the given text.
*
* @param text The text to show on the snackbar. * @param text The text to show on the snackbar.
* @param controller The SnackbarController to receive callbacks about the snackbar's state. * @param controller The SnackbarController to receive callbacks about the snackbar's state.
* @param type Type of the snackbar. Either {@link #TYPE_ACTION} or {@link #TYPE_NOTIFICATION}.
*/ */
public static Snackbar make(CharSequence text, SnackbarController controller) { public static Snackbar make(CharSequence text, SnackbarController controller, int type) {
Snackbar s = new Snackbar(); Snackbar s = new Snackbar();
s.mText = text; s.mText = text;
s.mController = controller; s.mController = controller;
s.mType = type;
return s; return s;
} }
...@@ -102,27 +117,6 @@ public class Snackbar { ...@@ -102,27 +117,6 @@ public class Snackbar {
return this; return this;
} }
/**
* Forces this snackbar to be shown when {@link #dismissAllSnackbars(SnackbarManager)} is called
* from a timeout. If {@link #showSnackbar(SnackbarManager)} is called while this snackbar is
* showing, the new snackbar will be added to the stack and this snackbar will not be
* overwritten.
*/
public Snackbar setForceDisplay() {
mForceDisplay = true;
return this;
}
/**
* Returns true if this snackbar should still be shown when @link
* #dismissAllSnackbars(SnackbarManager)} is called from a timeout. If
* {@link #showSnackbar(SnackbarManager)} is called while this snackbar is showing, the new
* snackbar will be added to the stack and this snackbar will not be overwritten.
*/
public boolean getForceDisplay() {
return mForceDisplay;
}
SnackbarController getController() { SnackbarController getController() {
return mController; return mController;
} }
...@@ -164,4 +158,11 @@ public class Snackbar { ...@@ -164,4 +158,11 @@ public class Snackbar {
Bitmap getProfileImage() { Bitmap getProfileImage() {
return mProfileImage; return mProfileImage;
} }
/**
* @return Whether the snackbar is of {@link #TYPE_ACTION}.
*/
boolean isTypeAction() {
return mType == TYPE_ACTION;
}
} }
...@@ -15,25 +15,23 @@ import android.view.Window; ...@@ -15,25 +15,23 @@ import android.view.Window;
import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.device.DeviceClassManager; import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.ui.UiUtils; import org.chromium.ui.UiUtils;
import org.chromium.ui.base.DeviceFormFactor; import org.chromium.ui.base.DeviceFormFactor;
import java.util.Stack; import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
/** /**
* Manager for the snackbar showing at the bottom of activity. * Manager for the snackbar showing at the bottom of activity. There should be only one
* SnackbarManager and one snackbar in the activity.
* <p/> * <p/>
* There should be only one SnackbarManager and one snackbar in the activity. The manager maintains * When action button is clicked, this manager will call {@link SnackbarController#onAction(Object)}
* a stack to store all entries that should be displayed. When showing a new snackbar, old entry * in corresponding listener, and show the next entry. Otherwise if no action is taken by user
* will be pushed to stack and text/button will be updated to the newest entry. * during {@link #DEFAULT_SNACKBAR_DURATION_MS} milliseconds, it will call
* <p/> * {@link SnackbarController#onDismissNoAction(Object)}.
* When action button is clicked, this manager will call
* {@link SnackbarController#onAction(Object)} in corresponding listener, and show the next
* entry in stack. Otherwise if no action is taken by user during
* {@link #DEFAULT_SNACKBAR_DURATION_MS} milliseconds, it will clear the stack and call
* {@link SnackbarController#onDismissNoAction(Object)} to all listeners.
*/ */
public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener { public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener {
...@@ -78,17 +76,18 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener ...@@ -78,17 +76,18 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
private View mDecor; private View mDecor;
private final Handler mUIThreadHandler; private final Handler mUIThreadHandler;
private Stack<Snackbar> mStack = new Stack<Snackbar>(); private SnackbarCollection mSnackbars = new SnackbarCollection();
private SnackbarPopupWindow mPopup; private SnackbarPopupWindow mPopup;
private boolean mActivityInForeground; private boolean mActivityInForeground;
private final Runnable mHideRunnable = new Runnable() { private final Runnable mHideRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
dismissAllSnackbars(true); mSnackbars.removeCurrentDueToTimeout();
updatePopup();
} }
}; };
// Variables used and reused in local calculations. // Variables used and reused in popup position calculations.
private int[] mTempDecorPosition = new int[2]; private int[] mTempDecorPosition = new int[2];
private Rect mTempVisibleDisplayFrame = new Rect(); private Rect mTempVisibleDisplayFrame = new Rect();
...@@ -112,95 +111,31 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener ...@@ -112,95 +111,31 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
* Notifies the snackbar manager that the activity has been pushed to background. * Notifies the snackbar manager that the activity has been pushed to background.
*/ */
public void onStop() { public void onStop() {
dismissAllSnackbars(false); mSnackbars.clear();
updatePopup();
mActivityInForeground = false; mActivityInForeground = false;
} }
/** /**
* Shows a snackbar at the bottom of the screen, or above the keyboard if the keyboard is * Shows a snackbar at the bottom of the screen, or above the keyboard if the keyboard is
* visible. If the currently displayed snackbar is forcing display, the new snackbar is added as * visible.
* the next to be displayed on the stack.
*/ */
public void showSnackbar(Snackbar snackbar) { public void showSnackbar(Snackbar snackbar) {
if (!mActivityInForeground) return; if (!mActivityInForeground) return;
mSnackbars.add(snackbar);
if (mPopup != null && !mStack.empty() && mStack.peek().getForceDisplay()) { updatePopup();
mStack.add(mStack.size() - 1, snackbar);
return;
}
int durationMs = snackbar.getDuration();
if (durationMs == 0) {
durationMs = DeviceClassManager.isAccessibilityModeEnabled(mDecor.getContext())
? sAccessibilitySnackbarDurationMs : sSnackbarDurationMs;
}
mUIThreadHandler.removeCallbacks(mHideRunnable);
mUIThreadHandler.postDelayed(mHideRunnable, durationMs);
mStack.push(snackbar);
if (mPopup == null) {
mPopup = new SnackbarPopupWindow(mDecor, this, snackbar);
showPopupAtBottom();
mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
} else {
mPopup.update(snackbar, true);
}
mPopup.announceforAccessibility(); mPopup.announceforAccessibility();
} }
/**
* Warning: Calling this method might cause cascading destroy loop, because you might trigger
* callbacks for other {@link SnackbarController}. This method is only meant to be used during
* {@link ChromeActivity}'s destruction routine. For other purposes, use
* {@link #dismissSnackbars(SnackbarController)} instead.
* <p>
* Dismisses all snackbars in stack. This will call
* {@link SnackbarController#onDismissNoAction(Object)} for every closing snackbar.
*
* @param isTimeout Whether dismissal was triggered by timeout.
*/
public void dismissAllSnackbars(boolean isTimeout) {
mUIThreadHandler.removeCallbacks(mHideRunnable);
if (!mActivityInForeground) return;
if (mPopup != null) {
mPopup.dismiss();
mPopup = null;
}
while (!mStack.isEmpty()) {
Snackbar snackbar = mStack.pop();
snackbar.getController().onDismissNoAction(snackbar.getActionData());
if (isTimeout && !mStack.isEmpty() && mStack.peek().getForceDisplay()) {
showSnackbar(mStack.pop());
return;
}
}
mDecor.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
/** /**
* Dismisses snackbars that are associated with the given {@link SnackbarController}. * Dismisses snackbars that are associated with the given {@link SnackbarController}.
* *
* @param controller Only snackbars with this controller will be removed. * @param controller Only snackbars with this controller will be removed.
*/ */
public void dismissSnackbars(SnackbarController controller) { public void dismissSnackbars(SnackbarController controller) {
boolean isFound = false; if (mSnackbars.removeMatchingSnackbars(controller)) {
Snackbar[] snackbars = new Snackbar[mStack.size()]; updatePopup();
mStack.toArray(snackbars);
for (Snackbar snackbar : snackbars) {
if (snackbar.getController() == controller) {
mStack.remove(snackbar);
isFound = true;
}
} }
if (!isFound) return;
finishSnackbarRemoval();
} }
/** /**
...@@ -210,26 +145,8 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener ...@@ -210,26 +145,8 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
* @param actionData Only snackbars whose action data is equal to actionData will be removed. * @param actionData Only snackbars whose action data is equal to actionData will be removed.
*/ */
public void dismissSnackbars(SnackbarController controller, Object actionData) { public void dismissSnackbars(SnackbarController controller, Object actionData) {
boolean isFound = false; if (mSnackbars.removeMatchingSnackbars(controller, actionData)) {
for (Snackbar snackbar : mStack) { updatePopup();
if (snackbar.getActionData() != null && snackbar.getActionData().equals(actionData)
&& snackbar.getController() == controller) {
mStack.remove(snackbar);
isFound = true;
break;
}
}
if (!isFound) return;
finishSnackbarRemoval();
}
private void finishSnackbarRemoval() {
if (mStack.isEmpty()) {
dismissAllSnackbars(false);
} else {
// Refresh the snackbar to let it show top of stack and have full timeout.
showSnackbar(mStack.pop());
} }
} }
...@@ -238,34 +155,15 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener ...@@ -238,34 +155,15 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
*/ */
@Override @Override
public void onClick(View v) { public void onClick(View v) {
assert !mStack.isEmpty(); mSnackbars.removeCurrent(true);
updatePopup();
Snackbar snackbar = mStack.pop();
snackbar.getController().onAction(snackbar.getActionData());
if (!mStack.isEmpty()) {
showSnackbar(mStack.pop());
} else {
dismissAllSnackbars(false);
}
} }
private void showPopupAtBottom() { /**
// When the keyboard is showing, translating the snackbar upwards looks bad because it * @return Whether there is a snackbar on screen.
// overlaps the keyboard. In this case, use an alternative animation without translation. */
boolean isKeyboardShowing = UiUtils.isKeyboardShowing(mDecor.getContext(), mDecor); public boolean isShowing() {
mPopup.setAnimationStyle(isKeyboardShowing ? R.style.SnackbarAnimationWithKeyboard return mPopup != null && mPopup.isShowing();
: R.style.SnackbarAnimation);
mDecor.getLocationInWindow(mTempDecorPosition);
mDecor.getWindowVisibleDisplayFrame(mTempVisibleDisplayFrame);
int decorBottom = mTempDecorPosition[1] + mDecor.getHeight();
int visibleBottom = Math.min(mTempVisibleDisplayFrame.bottom, decorBottom);
int margin = mIsTablet ? mDecor.getResources().getDimensionPixelSize(
R.dimen.snackbar_tablet_margin) : 0;
mPopup.showAtLocation(mDecor, Gravity.START | Gravity.BOTTOM, margin,
decorBottom - visibleBottom + margin);
} }
/** /**
...@@ -296,11 +194,61 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener ...@@ -296,11 +194,61 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
} }
/** /**
* @return Whether there is a snackbar on screen. * Updates the snackbar popup window to reflect the value of mSnackbars.currentSnackbar(), which
* may be null. This might show, change, or hide the popup.
*/ */
public boolean isShowing() { private void updatePopup() {
if (mPopup == null) return false; if (!mActivityInForeground) return;
return mPopup.isShowing(); Snackbar currentSnackbar = mSnackbars.getCurrent();
if (currentSnackbar == null) {
mUIThreadHandler.removeCallbacks(mHideRunnable);
if (mPopup != null) {
mPopup.dismiss();
mPopup = null;
}
mDecor.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
boolean popupChanged = true;
if (mPopup == null) {
mPopup = new SnackbarPopupWindow(mDecor, this, currentSnackbar);
// When the keyboard is showing, translating the snackbar upwards looks bad because
// it overlaps the keyboard. In this case, use an alternative animation without
// translation.
boolean isKeyboardShowing = UiUtils.isKeyboardShowing(mDecor.getContext(), mDecor);
mPopup.setAnimationStyle(isKeyboardShowing ? R.style.SnackbarAnimationWithKeyboard
: R.style.SnackbarAnimation);
mDecor.getLocationInWindow(mTempDecorPosition);
mDecor.getWindowVisibleDisplayFrame(mTempVisibleDisplayFrame);
int decorBottom = mTempDecorPosition[1] + mDecor.getHeight();
int visibleBottom = Math.min(mTempVisibleDisplayFrame.bottom, decorBottom);
int margin = mIsTablet ? mDecor.getResources().getDimensionPixelSize(
R.dimen.snackbar_tablet_margin) : 0;
mPopup.showAtLocation(mDecor, Gravity.START | Gravity.BOTTOM, margin,
decorBottom - visibleBottom + margin);
mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
} else {
popupChanged = mPopup.update(currentSnackbar);
}
if (popupChanged) {
int durationMs = getDuration(currentSnackbar);
mUIThreadHandler.removeCallbacks(mHideRunnable);
mUIThreadHandler.postDelayed(mHideRunnable, durationMs);
mPopup.announceforAccessibility();
}
}
}
private int getDuration(Snackbar snackbar) {
int durationMs = snackbar.getDuration();
if (durationMs == 0) {
durationMs = DeviceClassManager.isAccessibilityModeEnabled(mDecor.getContext())
? sAccessibilitySnackbarDurationMs : sSnackbarDurationMs;
}
return durationMs;
} }
/** /**
...@@ -312,4 +260,105 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener ...@@ -312,4 +260,105 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
sSnackbarDurationMs = durationMs; sSnackbarDurationMs = durationMs;
sAccessibilitySnackbarDurationMs = durationMs; sAccessibilitySnackbarDurationMs = durationMs;
} }
/**
* @return The currently showing snackbar. For testing only.
*/
@VisibleForTesting
Snackbar getCurrentSnackbarForTesting() {
return mSnackbars.getCurrent();
}
private static class SnackbarCollection {
private Deque<Snackbar> mStack = new LinkedList<>();
private Queue<Snackbar> mQueue = new LinkedList<>();
/**
* Adds a new snackbar to the collection. If the new snackbar is of
* {@link Snackbar#TYPE_ACTION} and current snackbar is of
* {@link Snackbar#TYPE_NOTIFICATION}, the current snackbar will be removed from the
* collection immediately.
*/
public void add(Snackbar snackbar) {
if (snackbar.isTypeAction()) {
if (getCurrent() != null && !getCurrent().isTypeAction()) {
removeCurrent(false);
}
mStack.push(snackbar);
} else {
mQueue.offer(snackbar);
}
}
/**
* Removes the current snackbar from the collection.
* @param isAction Whether the removal is triggered by user clicking the action button.
*/
public void removeCurrent(boolean isAction) {
Snackbar current = !mStack.isEmpty() ? mStack.pop() : mQueue.poll();
if (current != null) {
SnackbarController controller = current.getController();
if (isAction) controller.onAction(current.getActionData());
else controller.onDismissNoAction(current.getActionData());
}
}
/**
* @return The snackbar that is currently displayed.
*/
public Snackbar getCurrent() {
return !mStack.isEmpty() ? mStack.peek() : mQueue.peek();
}
public boolean isEmpty() {
return mStack.isEmpty() && mQueue.isEmpty();
}
public void clear() {
while (!isEmpty()) {
removeCurrent(false);
}
}
public void removeCurrentDueToTimeout() {
removeCurrent(false);
Snackbar current;
while ((current = getCurrent()) != null && current.isTypeAction()) {
removeCurrent(false);
}
}
public boolean removeMatchingSnackbars(SnackbarController controller) {
boolean snackbarRemoved = false;
Iterator<Snackbar> iter = mStack.iterator();
while (iter.hasNext()) {
Snackbar snackbar = iter.next();
if (snackbar.getController() == controller) {
iter.remove();
snackbarRemoved = true;
}
}
return snackbarRemoved;
}
public boolean removeMatchingSnackbars(SnackbarController controller, Object data) {
boolean snackbarRemoved = false;
Iterator<Snackbar> iter = mStack.iterator();
while (iter.hasNext()) {
Snackbar snackbar = iter.next();
if (snackbar.getController() == controller
&& objectsAreEqual(snackbar.getActionData(), data)) {
iter.remove();
snackbarRemoved = true;
}
}
return snackbarRemoved;
}
private static boolean objectsAreEqual(Object a, Object b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
return a.equals(b);
}
}
} }
...@@ -27,6 +27,7 @@ class SnackbarPopupWindow extends PopupWindow { ...@@ -27,6 +27,7 @@ class SnackbarPopupWindow extends PopupWindow {
private final TextView mActionButtonView; private final TextView mActionButtonView;
private final ImageView mProfileImageView; private final ImageView mProfileImageView;
private final int mAnimationDuration; private final int mAnimationDuration;
private Snackbar mSnackbar;
/** /**
* Creates an instance of the {@link SnackbarPopupWindow}. * Creates an instance of the {@link SnackbarPopupWindow}.
...@@ -63,12 +64,18 @@ class SnackbarPopupWindow extends PopupWindow { ...@@ -63,12 +64,18 @@ class SnackbarPopupWindow extends PopupWindow {
} }
/** /**
* Updates the view to display data from the given snackbar. * Updates the view to display data from the given snackbar. No-op if the popup is already
* * showing the given snackbar.
* @param snackbar The snackbar to display * @param snackbar The snackbar to display
* @param animate Whether or not to animate the text in or set it immediately * @return Whether update has actually been executed.
*/ */
void update(Snackbar snackbar, boolean animate) { boolean update(Snackbar snackbar) {
return update(snackbar, true);
}
private boolean update(Snackbar snackbar, boolean animate) {
if (mSnackbar == snackbar) return false;
mSnackbar = snackbar;
mMessageView.setMaxLines(snackbar.getSingleLine() ? 1 : Integer.MAX_VALUE); mMessageView.setMaxLines(snackbar.getSingleLine() ? 1 : Integer.MAX_VALUE);
mMessageView.setTemplate(snackbar.getTemplateText()); mMessageView.setTemplate(snackbar.getTemplateText());
setViewText(mMessageView, snackbar.getText(), animate); setViewText(mMessageView, snackbar.getText(), animate);
...@@ -104,6 +111,7 @@ class SnackbarPopupWindow extends PopupWindow { ...@@ -104,6 +111,7 @@ class SnackbarPopupWindow extends PopupWindow {
} else { } else {
((ViewGroup) view).removeView(mProfileImageView); ((ViewGroup) view).removeView(mProfileImageView);
} }
return true;
} }
private void setViewText(TextView view, CharSequence text, boolean animate) { private void setViewText(TextView view, CharSequence text, boolean animate) {
......
...@@ -38,7 +38,7 @@ public class AutoSigninSnackbarController ...@@ -38,7 +38,7 @@ public class AutoSigninSnackbarController
if (snackbarManager == null) return; if (snackbarManager == null) return;
AutoSigninSnackbarController snackbarController = AutoSigninSnackbarController snackbarController =
new AutoSigninSnackbarController(snackbarManager, tab); new AutoSigninSnackbarController(snackbarManager, tab);
Snackbar snackbar = Snackbar.make(text, snackbarController); Snackbar snackbar = Snackbar.make(text, snackbarController, Snackbar.TYPE_NOTIFICATION);
Resources resources = tab.getWindowAndroid().getActivity().get().getResources(); Resources resources = tab.getWindowAndroid().getActivity().get().getResources();
int backgroundColor = ApiCompatibilityUtils.getColor(resources, int backgroundColor = ApiCompatibilityUtils.getColor(resources,
R.color.smart_lock_auto_signin_snackbar_background_color); R.color.smart_lock_auto_signin_snackbar_background_color);
......
...@@ -127,7 +127,7 @@ public class UndoBarPopupController implements SnackbarManager.SnackbarControlle ...@@ -127,7 +127,7 @@ public class UndoBarPopupController implements SnackbarManager.SnackbarControlle
mSnackbarManager.isShowing() ? TAB_CLOSE_UNDO_TOAST_SHOWN_WARM mSnackbarManager.isShowing() ? TAB_CLOSE_UNDO_TOAST_SHOWN_WARM
: TAB_CLOSE_UNDO_TOAST_SHOWN_COLD, : TAB_CLOSE_UNDO_TOAST_SHOWN_COLD,
TAB_CLOSE_UNDO_TOAST_COUNT); TAB_CLOSE_UNDO_TOAST_COUNT);
mSnackbarManager.showSnackbar(Snackbar.make(content, this) mSnackbarManager.showSnackbar(Snackbar.make(content, this, Snackbar.TYPE_ACTION)
.setTemplateText(mContext.getString(R.string.undo_bar_close_message)) .setTemplateText(mContext.getString(R.string.undo_bar_close_message))
.setAction(mContext.getString(R.string.undo_bar_button_text), tabId)); .setAction(mContext.getString(R.string.undo_bar_button_text), tabId));
} }
...@@ -142,7 +142,7 @@ public class UndoBarPopupController implements SnackbarManager.SnackbarControlle ...@@ -142,7 +142,7 @@ public class UndoBarPopupController implements SnackbarManager.SnackbarControlle
*/ */
private void showUndoCloseAllBar(List<Integer> closedTabIds) { private void showUndoCloseAllBar(List<Integer> closedTabIds) {
String content = String.format(Locale.getDefault(), "%d", closedTabIds.size()); String content = String.format(Locale.getDefault(), "%d", closedTabIds.size());
mSnackbarManager.showSnackbar(Snackbar.make(content, this) mSnackbarManager.showSnackbar(Snackbar.make(content, this, Snackbar.TYPE_ACTION)
.setTemplateText(mContext.getString(R.string.undo_bar_close_all_message)) .setTemplateText(mContext.getString(R.string.undo_bar_close_all_message))
.setAction(mContext.getString(R.string.undo_bar_button_text), closedTabIds)); .setAction(mContext.getString(R.string.undo_bar_button_text), closedTabIds));
......
// Copyright 2016 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.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.test.ChromeTabbedActivityTestBase;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
/**
* Tests for {@link SnackbarManager}.
*/
public class SnackbarTest extends ChromeTabbedActivityTestBase {
SnackbarManager mManager;
SnackbarController mDefaultController = new SnackbarController() {
@Override
public void onDismissNoAction(Object actionData) {
}
@Override
public void onAction(Object actionData) {
}
};
@Override
public void startMainActivity() throws InterruptedException {
SnackbarManager.setDurationForTesting(1000);
startMainActivityOnBlankPage();
mManager = getActivity().getSnackbarManager();
}
@MediumTest
public void testStackQueueOrder() throws InterruptedException {
final Snackbar stackbar = Snackbar.make("stack", mDefaultController,
Snackbar.TYPE_ACTION);
final Snackbar queuebar = Snackbar.make("queue", mDefaultController,
Snackbar.TYPE_NOTIFICATION);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mManager.showSnackbar(stackbar);
}
});
CriteriaHelper.pollForUIThreadCriteria(new Criteria("First snackbar not shown") {
@Override
public boolean isSatisfied() {
return mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == stackbar;
}
});
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mManager.showSnackbar(queuebar);
assertTrue("Snackbar not showing", mManager.isShowing());
assertEquals("Snackbars on stack should not be cancled by snackbars on queue",
stackbar, mManager.getCurrentSnackbarForTesting());
}
});
CriteriaHelper.pollForUIThreadCriteria(new Criteria("Snackbar on queue not shown") {
@Override
public boolean isSatisfied() {
return mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == queuebar;
}
});
CriteriaHelper.pollForUIThreadCriteria(new Criteria("Snackbar did not time out") {
@Override
public boolean isSatisfied() {
return !mManager.isShowing();
}
});
}
@SmallTest
public void testQueueStackOrder() throws InterruptedException {
final Snackbar stackbar = Snackbar.make("stack", mDefaultController,
Snackbar.TYPE_ACTION);
final Snackbar queuebar = Snackbar.make("queue", mDefaultController,
Snackbar.TYPE_NOTIFICATION);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mManager.showSnackbar(queuebar);
}
});
CriteriaHelper.pollForUIThreadCriteria(new Criteria("First snackbar not shown") {
@Override
public boolean isSatisfied() {
return mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == queuebar;
}
});
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mManager.showSnackbar(stackbar);
}
});
CriteriaHelper.pollForUIThreadCriteria(
new Criteria("Snackbar on queue was not cleared by snackbar stack.") {
@Override
public boolean isSatisfied() {
return mManager.isShowing()
&& mManager.getCurrentSnackbarForTesting() == stackbar;
}
});
CriteriaHelper.pollForUIThreadCriteria(new Criteria("Snackbar did not time out") {
@Override
public boolean isSatisfied() {
return !mManager.isShowing();
}
});
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment