Commit fb08e564 authored by Yue Zhang's avatar Yue Zhang Committed by Commit Bot

Fix a crash in pop-up tab strip

In the popup TabStrip used by Duet-TabStrip integration, there is a
crash in RecycleView when user very intensively clicks on the TabStrip
favicon to close multiple tabs. The stack indicates that the
recyclerView is trying to remove the same ViewHolder in two consecutive
remove operations, and the second call would crash. The fix introduced
in this CL is to early end the animation if we know for sure that this
item is going to be removed. This should help with better identifying
the ViewHolder to remove.

This fix is inspired by the discussion here:
https://b.corp.google.com/issues/34184109
and the CL here:
https://critique.corp.google.com/#review/188055979/depot/google3/java/com/google/android/apps/play/music/app/ui/common/MediaListRecyclerFragment.java
Even though we early end the animation, there should be no visible
animation change from the UI perspective.

Bug: 1045944
Change-Id: I0d8a7b098e17cdc86c4ae3227cd9dc1ba37b2adc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2036874
Commit-Queue: Yue Zhang <yuezhanggg@chromium.org>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738392}
parent dfa14881
...@@ -221,7 +221,8 @@ public class TabListCoordinator implements Destroyable { ...@@ -221,7 +221,8 @@ public class TabListCoordinator implements Destroyable {
mMediator = new TabListMediator(context, modelList, tabModelSelector, thumbnailProvider, mMediator = new TabListMediator(context, modelList, tabModelSelector, thumbnailProvider,
titleProvider, tabListFaviconProvider, actionOnRelatedTabs, titleProvider, tabListFaviconProvider, actionOnRelatedTabs,
createGroupButtonProvider, selectionDelegateProvider, createGroupButtonProvider, selectionDelegateProvider,
gridCardOnClickListenerProvider, dialogHandler, componentName, itemType); gridCardOnClickListenerProvider, dialogHandler, this::endItemAnimationForPosition,
componentName, itemType);
if (mMode == TabListMode.GRID) { if (mMode == TabListMode.GRID) {
GridLayoutManager gridLayoutManager = GridLayoutManager gridLayoutManager =
...@@ -399,4 +400,12 @@ public class TabListCoordinator implements Destroyable { ...@@ -399,4 +400,12 @@ public class TabListCoordinator implements Destroyable {
void removeSpecialListItem(@UiType int uiType, int itemIdentifier) { void removeSpecialListItem(@UiType int uiType, int itemIdentifier) {
mMediator.removeSpecialItemFromModel(uiType, itemIdentifier); mMediator.removeSpecialItemFromModel(uiType, itemIdentifier);
} }
private void endItemAnimationForPosition(int pos) {
RecyclerView.ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(pos);
if (viewHolder == null || mRecyclerView.getItemAnimator() == null) {
return;
}
mRecyclerView.getItemAnimator().endAnimation(viewHolder);
}
} }
...@@ -21,6 +21,7 @@ import android.os.Bundle; ...@@ -21,6 +21,7 @@ import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.v7.content.res.AppCompatResources; import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Pair; import android.util.Pair;
import android.view.View; import android.view.View;
...@@ -182,6 +183,19 @@ class TabListMediator { ...@@ -182,6 +183,19 @@ class TabListMediator {
} }
} }
/**
* An interface used to end current item animation in {@link RecyclerView} at certain position.
*/
interface ItemAnimationStopper {
/**
* This method stops the item animation from {@link RecyclerView.ItemAnimator} for item in
* {@code position}.
*
* @param position The position of the item whose animation will end.
*/
void endItemAnimationForPosition(int position);
}
/** /**
* An interface to show IPH for a tab. * An interface to show IPH for a tab.
*/ */
...@@ -432,8 +446,8 @@ class TabListMediator { ...@@ -432,8 +446,8 @@ class TabListMediator {
@Nullable CreateGroupButtonProvider createGroupButtonProvider, @Nullable CreateGroupButtonProvider createGroupButtonProvider,
@Nullable SelectionDelegateProvider selectionDelegateProvider, @Nullable SelectionDelegateProvider selectionDelegateProvider,
@Nullable GridCardOnClickListenerProvider gridCardOnClickListenerProvider, @Nullable GridCardOnClickListenerProvider gridCardOnClickListenerProvider,
@Nullable TabGridDialogHandler dialogHandler, String componentName, @Nullable TabGridDialogHandler dialogHandler, ItemAnimationStopper itemAnimationStopper,
@UiType int uiType) { String componentName, @UiType int uiType) {
mContext = context; mContext = context;
mTabModelSelector = tabModelSelector; mTabModelSelector = tabModelSelector;
mThumbnailProvider = thumbnailProvider; mThumbnailProvider = thumbnailProvider;
...@@ -533,9 +547,14 @@ class TabListMediator { ...@@ -533,9 +547,14 @@ class TabListMediator {
@Override @Override
public void willCloseTab(Tab tab, boolean animate) { public void willCloseTab(Tab tab, boolean animate) {
if (mModel.indexFromId(tab.getId()) == TabModel.INVALID_TAB_INDEX) return; int index = mModel.indexFromId(tab.getId());
if (index == TabModel.INVALID_TAB_INDEX) return;
tab.removeObserver(mTabObserver); tab.removeObserver(mTabObserver);
mModel.removeAt(mModel.indexFromId(tab.getId())); // TODO(crbug.com/1045944): This line is for a specific crash, it should not be
// needed. Investigate why the crash is happening only in pop-up tab strip. Maybe
// related to how RecyclerView layouts in a wrap_content PopupWindow.
if (uiType == UiType.STRIP) itemAnimationStopper.endItemAnimationForPosition(index);
mModel.removeAt(index);
} }
@Override @Override
......
...@@ -210,6 +210,8 @@ public class TabListMediatorUnitTest { ...@@ -210,6 +210,8 @@ public class TabListMediatorUnitTest {
UrlUtilities.Natives mUrlUtilitiesJniMock; UrlUtilities.Natives mUrlUtilitiesJniMock;
@Mock @Mock
TabListMediator.TabGridAccessibilityHelper mTabGridAccessibilityHelper; TabListMediator.TabGridAccessibilityHelper mTabGridAccessibilityHelper;
@Mock
TabListMediator.ItemAnimationStopper mItemAnimationStopper;
@Captor @Captor
ArgumentCaptor<TabModelObserver> mTabModelObserverCaptor; ArgumentCaptor<TabModelObserver> mTabModelObserverCaptor;
...@@ -317,7 +319,7 @@ public class TabListMediatorUnitTest { ...@@ -317,7 +319,7 @@ public class TabListMediatorUnitTest {
mMediator = new TabListMediator(mContext, mModel, mTabModelSelector, mMediator = new TabListMediator(mContext, mModel, mTabModelSelector,
mTabContentManager::getTabThumbnailWithCallback, mTitleProvider, mTabContentManager::getTabThumbnailWithCallback, mTitleProvider,
mTabListFaviconProvider, false, null, null, mGridCardOnClickListenerProvider, null, mTabListFaviconProvider, false, null, null, mGridCardOnClickListenerProvider, null,
getClass().getSimpleName(), 0); mItemAnimationStopper, getClass().getSimpleName(), 0);
mMediator.registerOrientationListener(mGridLayoutManager); mMediator.registerOrientationListener(mGridLayoutManager);
TrackerFactory.setTrackerForTests(mTracker); TrackerFactory.setTrackerForTests(mTracker);
ContextUtils.initApplicationContextForTests(mContext); ContextUtils.initApplicationContextForTests(mContext);
...@@ -604,6 +606,20 @@ public class TabListMediatorUnitTest { ...@@ -604,6 +606,20 @@ public class TabListMediatorUnitTest {
assertThat(mModel.size(), equalTo(2)); assertThat(mModel.size(), equalTo(2));
} }
@Test
@Features.EnableFeatures({ChromeFeatureList.TAB_GROUPS_ANDROID,
ChromeFeatureList.TAB_GROUPS_UI_IMPROVEMENTS_ANDROID})
// clang-format off
public void tabClosure_ForceStopItemAnimationForStrip() {
// clang-format on
setUpForTabGroupOperation(TabListMediatorType.TAB_STRIP);
mMediatorTabModelObserver.willCloseTab(mTab2, false);
assertThat(mModel.size(), equalTo(1));
verify(mItemAnimationStopper).endItemAnimationForPosition(eq(1));
}
@Test @Test
public void tabAddition_RestoreNotComplete() { public void tabAddition_RestoreNotComplete() {
initAndAssertAllProperties(); initAndAssertAllProperties();
...@@ -1861,12 +1877,19 @@ public class TabListMediatorUnitTest { ...@@ -1861,12 +1877,19 @@ public class TabListMediatorUnitTest {
TabListMediator.TabGridDialogHandler handler = TabListMediator.TabGridDialogHandler handler =
type == TabListMediatorType.TAB_GRID_DIALOG ? mTabGridDialogHandler : null; type == TabListMediatorType.TAB_GRID_DIALOG ? mTabGridDialogHandler : null;
boolean actionOnRelatedTabs = type == TabListMediatorType.TAB_SWITCHER; boolean actionOnRelatedTabs = type == TabListMediatorType.TAB_SWITCHER;
int uiType = 0;
if (type == TabListMediatorType.TAB_SWITCHER
|| type == TabListMediatorType.TAB_GRID_DIALOG) {
uiType = TabProperties.UiType.CLOSABLE;
} else if (type == TabListMediatorType.TAB_STRIP) {
uiType = TabProperties.UiType.STRIP;
}
CachedFeatureFlags.setTabGroupsAndroidEnabledForTesting(true); CachedFeatureFlags.setTabGroupsAndroidEnabledForTesting(true);
mMediator = new TabListMediator(mContext, mModel, mTabModelSelector, mMediator = new TabListMediator(mContext, mModel, mTabModelSelector,
mTabContentManager::getTabThumbnailWithCallback, mTitleProvider, mTabContentManager::getTabThumbnailWithCallback, mTitleProvider,
mTabListFaviconProvider, actionOnRelatedTabs, null, null, null, handler, mTabListFaviconProvider, actionOnRelatedTabs, null, null, null, handler,
getClass().getSimpleName(), 0); mItemAnimationStopper, getClass().getSimpleName(), uiType);
// There are two TabModelObserver and two TabGroupModelFilter.Observer added when // There are two TabModelObserver and two TabGroupModelFilter.Observer added when
// initializing TabListMediator, one set from TabListMediator and the other from // initializing TabListMediator, one set from TabListMediator and the other from
......
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