Commit 114425f8 authored by Yue Zhang's avatar Yue Zhang Committed by Commit Bot

Add an undo closure snack bar in TabGridDialog

This CL adds a snack bar to TabGridDialog that handles closure within
dialog. Note that this change doesn't prevent the original snack bar
from showing when closure happens, so there will be two snack bars
showing when a closure happens within dialog. This issue will be
addressed by later CLs.

Bug: 1119899
Change-Id: If0e5b010996a80c9a111c8bf2cb8af44feb63124
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2368236
Commit-Queue: Yue Zhang <yuezhanggg@chromium.org>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#801037}
parent caf7c607
......@@ -17,10 +17,6 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:background="@drawable/tab_grid_dialog_background">
<!-- Ignore useless parents here for two reasons:
1. Content recyclerView and toolbar view will be added programmatically later.
2. We need to keep the LinearLayout so that we can animate the ungroup textView and shadow
imageView together. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
......@@ -28,8 +24,7 @@
android:orientation="vertical"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:visibility="invisible"
tools:ignore="UselessParent">
android:visibility="invisible">
<ImageView
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_shadow_height"
......@@ -46,6 +41,13 @@
android:background="@drawable/ungroup_bar_background"
android:gravity="center" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/dialog_snack_bar_container_view"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true">
</FrameLayout>
</RelativeLayout>
<View
android:layout_width="match_parent"
......
......@@ -4,6 +4,7 @@
package org.chromium.chrome.browser.tasks.tab_management;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.view.LayoutInflater;
......@@ -20,6 +21,7 @@ import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.ui.modelutil.PropertyModel;
......@@ -63,10 +65,13 @@ public class TabGridDialogCoordinator implements TabGridDialogMediator.DialogCon
mDialogView = containerView.findViewById(R.id.dialog_parent_view);
mDialogView.setupScrimCoordinator(scrimCoordinator);
}
Activity activity = (Activity) context;
SnackbarManager snackbarManager =
new SnackbarManager(activity, mDialogView.getSnackBarContainer(), null);
mMediator = new TabGridDialogMediator(context, this, mModel, tabModelSelector,
tabCreatorManager, resetHandler, animationSourceViewProvider, shareDelegateSupplier,
mComponentName);
snackbarManager, mComponentName);
// TODO(crbug.com/1031349) : Remove the inline mode logic here, make the constructor to take
// in a mode parameter instead.
......
......@@ -35,6 +35,8 @@ import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.components.embedder_support.util.UrlConstants;
......@@ -49,7 +51,7 @@ import java.util.List;
* with the components' coordinator as well as managing the business logic
* for dialog show/hide.
*/
public class TabGridDialogMediator {
public class TabGridDialogMediator implements SnackbarManager.SnackbarController {
/**
* Defines an interface for a {@link TabGridDialogMediator} to control dialog.
*/
......@@ -115,7 +117,8 @@ public class TabGridDialogMediator {
TabModelSelector tabModelSelector, TabCreatorManager tabCreatorManager,
TabSwitcherMediator.ResetHandler tabSwitcherResetHandler,
AnimationSourceViewProvider animationSourceViewProvider,
ObservableSupplier<ShareDelegate> shareDelegateSupplier, String componentName) {
ObservableSupplier<ShareDelegate> shareDelegateSupplier,
SnackbarManager snackbarManager, String componentName) {
mContext = context;
mModel = model;
mTabModelSelector = tabModelSelector;
......@@ -142,6 +145,7 @@ public class TabGridDialogMediator {
public void tabClosureUndone(Tab tab) {
updateDialog();
updateGridTabSwitcher();
snackbarManager.dismissSnackbars(TabGridDialogMediator.this, tab.getId());
}
@Override
......@@ -168,6 +172,22 @@ public class TabGridDialogMediator {
updateDialog();
updateGridTabSwitcher();
}
@Override
public void tabPendingClosure(Tab tab) {
if (!mModel.get(TabGridPanelProperties.IS_DIALOG_VISIBLE)) return;
snackbarManager.showSnackbar(
Snackbar.make(tab.getTitle(), TabGridDialogMediator.this,
Snackbar.TYPE_ACTION, Snackbar.UMA_TAB_CLOSE_UNDO)
.setTemplateText(
mContext.getString(R.string.undo_bar_close_message))
.setAction(mContext.getString(R.string.undo), tab.getId()));
}
@Override
public void tabClosureCommitted(Tab tab) {
snackbarManager.dismissSnackbars(TabGridDialogMediator.this, tab.getId());
}
};
mTabModelSelectorObserver = new EmptyTabModelSelectorObserver() {
......@@ -504,6 +524,25 @@ public class TabGridDialogMediator {
return mTabGridDialogHandler;
}
// SnackbarManager.SnackbarController implementation.
@Override
public void onAction(Object actionData) {
int tabId = (int) actionData;
TabModel model = mTabModelSelector.getModelForTabId(tabId);
if (model != null) {
model.cancelTabClosure(tabId);
}
}
@Override
public void onDismissNoAction(Object actionData) {
int tabId = (int) actionData;
TabModel model = mTabModelSelector.getModelForTabId(tabId);
if (model != null) {
model.commitTabClosure(tabId);
}
}
/**
* A handler that handles TabGridDialog related changes originated from {@link TabListMediator}
* and {@link TabGridItemTouchHelperCallback}.
......
......@@ -74,6 +74,7 @@ public class TabGridDialogView extends FrameLayout
private View mAnimationCardView;
private View mItemView;
private View mUngroupBar;
private ViewGroup mSnackBarContainer;
private ViewGroup mParent;
private TextView mUngroupBarTextView;
private RelativeLayout mDialogContainerView;
......@@ -157,6 +158,7 @@ public class TabGridDialogView extends FrameLayout
mBackgroundFrame = findViewById(R.id.dialog_frame);
mBackgroundFrame.setLayoutParams(mContainerParams);
mAnimationCardView = findViewById(R.id.dialog_animation_card_view);
mSnackBarContainer = findViewById(R.id.dialog_snack_bar_container_view);
updateDialogWithOrientation(mContext.getResources().getConfiguration().orientation);
prepareAnimation();
......@@ -674,6 +676,7 @@ public class TabGridDialogView extends FrameLayout
mDialogContainerView.addView(toolbarView);
mDialogContainerView.addView(recyclerView);
mDialogContainerView.addView(mUngroupBar);
mDialogContainerView.addView(mSnackBarContainer);
RelativeLayout.LayoutParams params =
(RelativeLayout.LayoutParams) recyclerView.getLayoutParams();
params.setMargins(0, mToolbarHeight, 0, 0);
......@@ -804,6 +807,13 @@ public class TabGridDialogView extends FrameLayout
mUngroupBarTextAppearance = textAppearance;
}
/**
* Return the container view for undo closure snack bar.
*/
ViewGroup getSnackBarContainer() {
return mSnackBarContainer;
}
@VisibleForTesting
Animator getCurrentDialogAnimatorForTesting() {
return mCurrentDialogAnimator;
......
......@@ -52,6 +52,7 @@ import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.v
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabSwitcherCardCount;
import static org.chromium.chrome.features.start_surface.InstantStartTest.createTabStateFile;
import static org.chromium.chrome.features.start_surface.InstantStartTest.createThumbnailBitmapAndWriteToFile;
import static org.chromium.chrome.test.util.ViewUtils.onViewWaiting;
import static org.chromium.chrome.test.util.ViewUtils.waitForView;
import android.content.Intent;
......@@ -352,7 +353,10 @@ public class TabGridDialogTest {
// Exit dialog, wait for the undo bar showing and undo the closure.
clickScrimToExitDialog(cta);
waitForDialogHidingAnimationInTabSwitcher(cta);
CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
onViewWaiting(
allOf(withId(R.id.snackbar_button), isDescendantOfA(withId(R.id.bottom_container)),
isCompletelyDisplayed()))
.perform(click());
// Verify the undo has happened.
verifyFirstCardTitle("2 tabs");
......@@ -386,13 +390,50 @@ public class TabGridDialogTest {
// Exit dialog, wait for the undo bar showing and undo the closure.
clickScrimToExitDialog(cta);
waitForDialogHidingAnimation(cta);
CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
onViewWaiting(
allOf(withId(R.id.snackbar_button), isDescendantOfA(withId(R.id.bottom_container)),
isCompletelyDisplayed()))
.perform(click());
// Verify the undo has happened.
verifyTabStripFaviconCount(cta, 2);
openDialogFromStripAndVerify(cta, 2, null);
}
@Test
@MediumTest
public void testUndoClosureInDialog_DialogUndoBar() throws ExecutionException {
final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
createTabs(cta, false, 2);
enterTabSwitcher(cta);
verifyTabSwitcherCardCount(cta, 2);
mergeAllNormalTabsToAGroup(cta);
verifyTabSwitcherCardCount(cta, 1);
openDialogFromTabSwitcherAndVerify(cta, 2, null);
// Verify close and undo in dialog from tab switcher.
closeFirstTabInDialog();
verifyShowingDialog(cta, 1, null);
onViewWaiting(allOf(withId(R.id.snackbar_button),
isDescendantOfA(withId(R.id.dialog_snack_bar_container_view)),
isCompletelyDisplayed()))
.perform(click());
verifyShowingDialog(cta, 2, null);
// Verify close and undo in dialog from tab strip.
clickFirstTabInDialog(cta);
openDialogFromStripAndVerify(cta, 2, null);
closeFirstTabInDialog();
verifyShowingDialog(cta, 1, null);
onViewWaiting(allOf(withId(R.id.snackbar_button),
isDescendantOfA(withId(R.id.dialog_snack_bar_container_view)),
isCompletelyDisplayed()))
.perform(click());
verifyShowingDialog(cta, 2, null);
clickScrimToExitDialog(cta);
verifyTabStripFaviconCount(cta, 2);
}
@Test
@MediumTest
@Features.EnableFeatures(ChromeFeatureList.TAB_GROUPS_CONTINUATION_ANDROID)
......
......@@ -131,8 +131,9 @@ public class TabGridDialogViewTest extends DummyUiActivityTestCase {
mTabGridDialogView.resetDialog(toolbarView, recyclerView);
// It should contain three child views: top tool bar, recyclerview and ungroup bar.
Assert.assertEquals(3, mTabGridDialogContainer.getChildCount());
// It should contain four child views: top tool bar, recyclerview, ungroup bar and undo bar
// container.
Assert.assertEquals(4, mTabGridDialogContainer.getChildCount());
Assert.assertEquals(View.VISIBLE, recyclerView.getVisibility());
RelativeLayout.LayoutParams params =
(RelativeLayout.LayoutParams) recyclerView.getLayoutParams();
......
......@@ -57,6 +57,8 @@ import org.chromium.chrome.browser.tabmodel.TabModelFilterProvider;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.content_public.browser.LoadUrlParams;
......@@ -128,6 +130,8 @@ public class TabGridDialogMediatorUnitTest {
Editable mEditable;
@Mock
ObservableSupplier<ShareDelegate> mShareDelegateSupplier;
@Mock
SnackbarManager mSnackbarManager;
@Captor
ArgumentCaptor<TabModelObserver> mTabModelObserverCaptor;
......@@ -192,7 +196,7 @@ public class TabGridDialogMediatorUnitTest {
mModel = new PropertyModel(TabGridPanelProperties.ALL_KEYS);
mMediator = new TabGridDialogMediator(mContext, mDialogController, mModel,
mTabModelSelector, mTabCreatorManager, mTabSwitcherResetHandler,
mAnimationSourceViewProvider, mShareDelegateSupplier, "");
mAnimationSourceViewProvider, mShareDelegateSupplier, mSnackbarManager, "");
// TabModelObserver is registered when native is ready.
assertThat(mTabModelObserverCaptor.getAllValues().isEmpty(), equalTo(true));
......@@ -601,6 +605,7 @@ public class TabGridDialogMediatorUnitTest {
assertThat(mModel.get(TabGridPanelProperties.HEADER_TITLE), equalTo(DIALOG_TITLE1));
verify(mTabSwitcherResetHandler).resetWithTabList(mTabGroupModelFilter, false, false);
verify(mSnackbarManager).dismissSnackbars(eq(mMediator), eq(TAB1_ID));
}
@Test
......@@ -623,6 +628,7 @@ public class TabGridDialogMediatorUnitTest {
assertThat(
mModel.get(TabGridPanelProperties.HEADER_TITLE), equalTo(CUSTOMIZED_DIALOG_TITLE));
verify(mTabSwitcherResetHandler).resetWithTabList(mTabGroupModelFilter, false, false);
verify(mSnackbarManager).dismissSnackbars(eq(mMediator), eq(TAB2_ID));
}
@Test
......@@ -639,6 +645,32 @@ public class TabGridDialogMediatorUnitTest {
assertThat(mModel.get(TabGridPanelProperties.IS_DIALOG_VISIBLE), equalTo(false));
verify(mTabSwitcherResetHandler, never())
.resetWithTabList(mTabGroupModelFilter, false, false);
verify(mSnackbarManager).dismissSnackbars(eq(mMediator), eq(TAB1_ID));
}
@Test
public void tabClosureCommitted() {
mTabModelObserverCaptor.getValue().tabClosureCommitted(mTab1);
verify(mSnackbarManager).dismissSnackbars(eq(mMediator), eq(TAB1_ID));
}
@Test
public void tabPendingClosure_DialogVisible() {
mModel.set(TabGridPanelProperties.IS_DIALOG_VISIBLE, true);
mTabModelObserverCaptor.getValue().tabPendingClosure(mTab1);
verify(mSnackbarManager).showSnackbar(any(Snackbar.class));
}
@Test
public void tabPendingClosure_DialogInVisible() {
mModel.set(TabGridPanelProperties.IS_DIALOG_VISIBLE, false);
mTabModelObserverCaptor.getValue().tabPendingClosure(mTab1);
verify(mSnackbarManager, never()).showSnackbar(any(Snackbar.class));
}
@Test
......@@ -882,7 +914,7 @@ public class TabGridDialogMediatorUnitTest {
// the animationParamsProvider is null.
mMediator = new TabGridDialogMediator(mContext, mDialogController, mModel,
mTabModelSelector, mTabCreatorManager, mTabSwitcherResetHandler, null,
mShareDelegateSupplier, "");
mShareDelegateSupplier, mSnackbarManager, "");
mMediator.initWithNative(mTabSelectionEditorController, mTabGroupTitleEditor);
// Mock that the dialog is hidden and animation source view, header title and scrim click
......@@ -913,7 +945,7 @@ public class TabGridDialogMediatorUnitTest {
// the animationParamsProvider is null.
mMediator = new TabGridDialogMediator(mContext, mDialogController, mModel,
mTabModelSelector, mTabCreatorManager, mTabSwitcherResetHandler, null,
mShareDelegateSupplier, "");
mShareDelegateSupplier, mSnackbarManager, "");
mMediator.initWithNative(mTabSelectionEditorController, mTabGroupTitleEditor);
// Mock that the dialog is hidden and animation source view, header title and scrim click
// runnable are all null.
......@@ -947,7 +979,7 @@ public class TabGridDialogMediatorUnitTest {
// the animationParamsProvider is null.
mMediator = new TabGridDialogMediator(mContext, mDialogController, mModel,
mTabModelSelector, mTabCreatorManager, mTabSwitcherResetHandler, null,
mShareDelegateSupplier, "");
mShareDelegateSupplier, mSnackbarManager, "");
mMediator.initWithNative(mTabSelectionEditorController, mTabGroupTitleEditor);
// Mock that the dialog is hidden and animation source view is set to some mock view for
// testing purpose.
......@@ -1011,6 +1043,24 @@ public class TabGridDialogMediatorUnitTest {
assertThat(shareString2, equalTo(mMediator.getTabGroupStringForSharingForTesting()));
}
@Test
public void testSnackbarController_onAction() {
doReturn(mTabModel).when(mTabModelSelector).getModelForTabId(TAB1_ID);
mMediator.onAction(TAB1_ID);
verify(mTabModel).cancelTabClosure(eq(TAB1_ID));
}
@Test
public void testSnackbarController_onDismissNoAction() {
doReturn(mTabModel).when(mTabModelSelector).getModelForTabId(TAB1_ID);
mMediator.onDismissNoAction(TAB1_ID);
verify(mTabModel).commitTabClosure(eq(TAB1_ID));
}
@Test
public void destroy() {
mMediator.destroy();
......
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