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;
}
}
......@@ -15,25 +15,23 @@ import android.view.Window;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.ui.UiUtils;
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/>
* There should be only one SnackbarManager and one snackbar in the activity. The manager maintains
* a stack to store all entries that should be displayed. When showing a new snackbar, old entry
* will be pushed to stack and text/button will be updated to the newest entry.
* <p/>
* 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.
* 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, OnGlobalLayoutListener {
......@@ -78,17 +76,18 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
private View mDecor;
private final Handler mUIThreadHandler;
private Stack<Snackbar> mStack = new Stack<Snackbar>();
private SnackbarCollection mSnackbars = new SnackbarCollection();
private SnackbarPopupWindow mPopup;
private boolean mActivityInForeground;
private final Runnable mHideRunnable = new Runnable() {
@Override
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 Rect mTempVisibleDisplayFrame = new Rect();
......@@ -112,95 +111,31 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
* Notifies the snackbar manager that the activity has been pushed to background.
*/
public void onStop() {
dismissAllSnackbars(false);
mSnackbars.clear();
updatePopup();
mActivityInForeground = false;
}
/**
* 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
* the next to be displayed on the stack.
* visible.
*/
public void showSnackbar(Snackbar snackbar) {
if (!mActivityInForeground) return;
if (mPopup != null && !mStack.empty() && mStack.peek().getForceDisplay()) {
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);
}
mSnackbars.add(snackbar);
updatePopup();
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}.
*
* @param controller Only snackbars with this controller will be removed.
*/
public void dismissSnackbars(SnackbarController controller) {
boolean isFound = false;
Snackbar[] snackbars = new Snackbar[mStack.size()];
mStack.toArray(snackbars);
for (Snackbar snackbar : snackbars) {
if (snackbar.getController() == controller) {
mStack.remove(snackbar);
isFound = true;
}
if (mSnackbars.removeMatchingSnackbars(controller)) {
updatePopup();
}
if (!isFound) return;
finishSnackbarRemoval();
}
/**
......@@ -210,26 +145,8 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
* @param actionData Only snackbars whose action data is equal to actionData will be removed.
*/
public void dismissSnackbars(SnackbarController controller, Object actionData) {
boolean isFound = false;
for (Snackbar snackbar : mStack) {
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());
if (mSnackbars.removeMatchingSnackbars(controller, actionData)) {
updatePopup();
}
}
......@@ -238,34 +155,15 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener
*/
@Override
public void onClick(View v) {
assert !mStack.isEmpty();
Snackbar snackbar = mStack.pop();
snackbar.getController().onAction(snackbar.getActionData());
if (!mStack.isEmpty()) {
showSnackbar(mStack.pop());
} else {
dismissAllSnackbars(false);
}
mSnackbars.removeCurrent(true);
updatePopup();
}
private void showPopupAtBottom() {
// 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);
/**
* @return Whether there is a snackbar on screen.
*/
public boolean isShowing() {
return mPopup != null && mPopup.isShowing();
}
/**
......@@ -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() {
if (mPopup == null) return false;
return mPopup.isShowing();
private void updatePopup() {
if (!mActivityInForeground) return;
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
sSnackbarDurationMs = 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 {
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