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
public void onDownloadSucceeded(
DownloadInfo downloadInfo, final long downloadId, boolean canBeResolved) {
if (getSnackbarManager() == null) return;
Snackbar snackbar = Snackbar.make(mContext.getString(
R.string.download_succeeded_message, downloadInfo.getFileName()), this);
Snackbar snackbar = Snackbar.make(
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.
snackbar.setDuration(SNACKBAR_DURATION_IN_MILLISECONDS).setSingleLine(false);
Pair<DownloadInfo, Long> actionData = null;
......@@ -75,7 +76,7 @@ public class DownloadSnackbarController implements SnackbarManager.SnackbarContr
public void onDownloadFailed(String errorMessage, boolean showAllDownloads) {
if (getSnackbarManager() == null) return;
// 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)
.setDuration(SNACKBAR_DURATION_IN_MILLISECONDS);
if (showAllDownloads) {
......
......@@ -83,12 +83,13 @@ public class EnhancedBookmarkUndoController extends BookmarkModelObserver implem
if (!isUndoable) return;
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))
.setAction(mContext.getString(R.string.undo_bar_button_text), null));
} else {
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))
.setAction(mContext.getString(R.string.undo_bar_button_text), null));
}
......
......@@ -119,9 +119,9 @@ public class EnhancedBookmarkUtils {
bookmarkModel, activity, bookmarkId);
if (getLastUsedParent(activity) == null) {
snackbar = Snackbar.make(activity.getString(R.string.enhanced_bookmark_page_saved),
snackbarController);
snackbarController, Snackbar.TYPE_ACTION);
} else {
snackbar = Snackbar.make(folderName, snackbarController)
snackbar = Snackbar.make(folderName, snackbarController, Snackbar.TYPE_ACTION)
.setTemplateText(activity.getString(
R.string.enhanced_bookmark_page_saved_folder));
}
......@@ -164,7 +164,9 @@ public class EnhancedBookmarkUtils {
snackbarController = createSnackbarControllerForEditButton(
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);
}
......
......@@ -608,7 +608,7 @@ public class NewTabPage
}
Context context = mNewTabPageView.getContext();
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);
mTab.getSnackbarManager().showSnackbar(snackbar);
}
......
......@@ -59,7 +59,7 @@ public class OfflinePageFreeUpSpaceDialog
public void onDismissNoAction(Object actionData) {}
@Override
public void onAction(Object actionData) {}
});
}, Snackbar.TYPE_ACTION);
}
@Override
......
......@@ -308,8 +308,9 @@ public class OfflinePageUtils {
}
}
};
Snackbar snackbar = Snackbar.make(context.getString(snackbarTextId), snackbarController)
.setAction(context.getString(actionTextId), buttonType);
Snackbar snackbar = Snackbar
.make(context.getString(snackbarTextId), snackbarController, Snackbar.TYPE_ACTION)
.setAction(context.getString(actionTextId), buttonType);
activity.getSnackbarManager().showSnackbar(snackbar);
}
}
......@@ -83,7 +83,7 @@ public class GeolocationSnackbarController implements SnackbarController {
int durationMs = DeviceClassManager.isAccessibilityModeEnabled(view.getContext())
? ACCESSIBILITY_SNACKBAR_DURATION_MS : SNACKBAR_DURATION_MS;
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)
.setSingleLine(false)
.setDuration(durationMs);
......
......@@ -39,20 +39,20 @@ public class DataUseSnackbarController implements SnackbarManager.SnackbarContro
}
public void showDataUseTrackingStartedBar() {
mSnackbarManager.showSnackbar(Snackbar.make(
mContext.getString(R.string.data_use_tracking_started_snackbar_message), this)
mSnackbarManager.showSnackbar(Snackbar
.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),
STARTED_SNACKBAR)
.setForceDisplay());
STARTED_SNACKBAR));
DataUseTabUIManager.recordDataUseUIAction(DataUsageUIAction.STARTED_SNACKBAR_SHOWN);
}
public void showDataUseTrackingEndedBar() {
mSnackbarManager.showSnackbar(Snackbar.make(
mContext.getString(R.string.data_use_tracking_ended_snackbar_message), this)
mSnackbarManager.showSnackbar(Snackbar
.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),
ENDED_SNACKBAR)
.setForceDisplay());
ENDED_SNACKBAR));
DataUseTabUIManager.recordDataUseUIAction(DataUsageUIAction.ENDED_SNACKBAR_SHOWN);
}
......
......@@ -95,10 +95,9 @@ public class LoFiBarPopupController implements SnackbarManager.SnackbarControlle
String buttonText = mContext
.getString(isPreview ? R.string.data_reduction_lo_fi_preview_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())
.setDuration(DEFAULT_LO_FI_SNACKBAR_SHOW_DURATION_MS)
.setForceDisplay());
.setDuration(DEFAULT_LO_FI_SNACKBAR_SHOW_DURATION_MS));
DataReductionProxySettings.getInstance().incrementLoFiSnackbarShown();
DataReductionProxyUma.dataReductionProxyLoFiUIAction(
DataReductionProxyUma.ACTION_LOAD_IMAGES_SNACKBAR_SHOWN);
......
......@@ -18,6 +18,18 @@ import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
* .setAction("undo", actionData));
*/
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 CharSequence mText;
......@@ -28,20 +40,23 @@ public class Snackbar {
private boolean mSingleLine = true;
private int mDurationMs;
private Bitmap mProfileImage;
private boolean mForceDisplay = false;
private int mType;
// Prevent instantiation.
private Snackbar() {}
/**
* Creates and returns a snackbar to display the given text.
*
* @param text The text to show on the snackbar.
* @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();
s.mText = text;
s.mController = controller;
s.mType = type;
return s;
}
......@@ -102,27 +117,6 @@ public class Snackbar {
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() {
return mController;
}
......@@ -164,4 +158,11 @@ public class Snackbar {
Bitmap getProfileImage() {
return mProfileImage;
}
/**
* @return Whether the snackbar is of {@link #TYPE_ACTION}.
*/
boolean isTypeAction() {
return mType == TYPE_ACTION;
}
}
......@@ -27,6 +27,7 @@ class SnackbarPopupWindow extends PopupWindow {
private final TextView mActionButtonView;
private final ImageView mProfileImageView;
private final int mAnimationDuration;
private Snackbar mSnackbar;
/**
* Creates an instance of the {@link SnackbarPopupWindow}.
......@@ -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 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.setTemplate(snackbar.getTemplateText());
setViewText(mMessageView, snackbar.getText(), animate);
......@@ -104,6 +111,7 @@ class SnackbarPopupWindow extends PopupWindow {
} else {
((ViewGroup) view).removeView(mProfileImageView);
}
return true;
}
private void setViewText(TextView view, CharSequence text, boolean animate) {
......
......@@ -38,7 +38,7 @@ public class AutoSigninSnackbarController
if (snackbarManager == null) return;
AutoSigninSnackbarController snackbarController =
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();
int backgroundColor = ApiCompatibilityUtils.getColor(resources,
R.color.smart_lock_auto_signin_snackbar_background_color);
......
......@@ -127,7 +127,7 @@ public class UndoBarPopupController implements SnackbarManager.SnackbarControlle
mSnackbarManager.isShowing() ? TAB_CLOSE_UNDO_TOAST_SHOWN_WARM
: TAB_CLOSE_UNDO_TOAST_SHOWN_COLD,
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))
.setAction(mContext.getString(R.string.undo_bar_button_text), tabId));
}
......@@ -142,7 +142,7 @@ public class UndoBarPopupController implements SnackbarManager.SnackbarControlle
*/
private void showUndoCloseAllBar(List<Integer> closedTabIds) {
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))
.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