// Copyright 2019 The Chromium Authors
// 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.tasks.tab_management;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.base.Callback;
import org.chromium.base.TraceEvent;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tab_ui.TabContentManagerThumbnailProvider;
import org.chromium.chrome.browser.tab_ui.ThumbnailProvider;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.tasks.tab_management.TabListMediator.GridCardOnClickListenerProvider;
import org.chromium.chrome.browser.tasks.tab_management.TabProperties.TabActionState;
import org.chromium.chrome.browser.tasks.tab_management.TabProperties.UiType;
import org.chromium.chrome.browser.tasks.tab_management.TabUiMetricsHelper.TabListEditorExitMetricGroups;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import java.util.List;
/**
* This class is a coordinator for TabListEditor component. It manages the communication with
* {@link TabListCoordinator} as well as the life-cycle of shared component.
*/
class TabListEditorCoordinator {
static final String COMPONENT_NAME = "TabListEditor";
// TODO(crbug.com/41467140): Unify similar interfaces in other components that used the
// TabListCoordinator.
/** Interface for resetting the selectable tab grid. */
interface ResetHandler {
/**
* Handles the reset event.
*
* @param tabs List of {@link Tab}s to reset.
* @param recyclerViewPosition The state to preserve scroll position of the recycler view.
* @param quickMode whether to use quick mode.
*/
void resetWithListOfTabs(
@Nullable List<Tab> tabs,
@Nullable RecyclerViewPosition recyclerViewPosition,
boolean quickMode);
/** Handles syncing the position of the outer {@link TabListCoordinator}'s RecyclerView. */
void syncRecyclerViewPosition();
/** Handles cleanup. */
void postHiding();
}
/** An interface to control the TabListEditor. */
interface TabListEditorController extends BackPressHandler {
/**
* Shows the TabListEditor with the given {@Link Tab}s.
*
* @param tabs List of {@link Tab}s to show.
* @param recyclerViewPosition The state to preserve scroll position of the recycler view.
*/
void show(List<Tab> tabs, @Nullable RecyclerViewPosition recyclerViewPosition);
/** Hides the TabListEditor. */
void hide();
/**
* @return Whether or not the TabListEditor consumed the event.
*/
boolean handleBackPressed();
/**
* Configure the Toolbar for TabListEditor with multiple actions.
*
* @param actions The {@link TabListEditorAction} to make available.
*/
void configureToolbarWithMenuItems(List<TabListEditorAction> actions);
/**
* @return Whether the TabListEditor is visible.
*/
boolean isVisible();
/** Sets the toolbar title when no items are selected. */
void setToolbarTitle(String title);
/** Sets a custom {@link NavigationProvider} to handle "back" actions. */
void setNavigationProvider(@NonNull NavigationProvider navigationProvider);
/** Sets the {@link TabActionState} for the TabListEditor. */
void setTabActionState(@TabActionState int tabActionState);
/** Sets the {@link LifecycleObserver} for this TabListEditor. */
void setLifecycleObserver(LifecycleObserver lifecycleObserver);
}
/** An interface for embedders to provide navigation. */
public interface NavigationProvider {
/** Defines what to do to handle "back" actions. */
void goBack();
}
/** Allows an embedder to observe the lifecycle of the TabListEditor. */
public interface LifecycleObserver {
/** Called when the TabListEditor is about to be hidden. */
void willHide();
/** Called after the TabListEditor is hidden. */
void didHide();
}
/** Provider of action for the navigation button in {@link TabListEditorMediator}. */
public static class TabListEditorNavigationProvider implements NavigationProvider {
private final TabListEditorCoordinator.TabListEditorController mTabListEditorController;
private final Context mContext;
public TabListEditorNavigationProvider(
Context context,
TabListEditorCoordinator.TabListEditorController tabListEditorController) {
mContext = context;
mTabListEditorController = tabListEditorController;
}
@Override
public void goBack() {
TabUiMetricsHelper.recordSelectionEditorExitMetrics(
TabListEditorExitMetricGroups.CLOSED_BY_USER, mContext);
mTabListEditorController.hide();
}
}
private final TabListEditorController mTabListEditorController =
new TabListEditorController() {
@Override
public void show(
List<Tab> tabs, @Nullable RecyclerViewPosition recyclerViewPosition) {
if (mTabListCoordinator == null) {
createTabListCoordinator();
}
mTabListEditorMediator.show(tabs, recyclerViewPosition);
}
@Override
public void hide() {
mTabListEditorMediator.hide();
}
@Override
public void configureToolbarWithMenuItems(List<TabListEditorAction> actions) {
assert mTabListCoordinator != null
: "Must call #show before #configureToolbarWithMenuItems";
mTabListEditorMediator.configureToolbarWithMenuItems(actions);
}
@Override
public boolean isVisible() {
return mTabListEditorMediator.isVisible();
}
@Override
public void setToolbarTitle(String title) {
mTabListEditorMediator.setToolbarTitle(title);
}
@Override
public void setNavigationProvider(NavigationProvider navigationProvider) {
mTabListEditorMediator.setNavigationProvider(navigationProvider);
}
@Override
public boolean handleBackPressed() {
return mTabListEditorMediator.handleBackPressed();
}
@Override
public @BackPressResult int handleBackPress() {
return mTabListEditorMediator.handleBackPress();
}
@Override
public ObservableSupplier<Boolean> getHandleBackPressChangedSupplier() {
return mTabListEditorMediator.getHandleBackPressChangedSupplier();
}
@Override
public void setTabActionState(@TabActionState int tabActionState) {
mTabActionState = tabActionState;
mTabListEditorMediator.setTabActionState(tabActionState);
}
@Override
public void setLifecycleObserver(LifecycleObserver lifecycleObserver) {
mTabListEditorMediator.setLifecycleObserver(lifecycleObserver);
}
};
private final Context mContext;
private final ViewGroup mRootView;
private final ViewGroup mParentView;
private final BrowserControlsStateProvider mBrowserControlsStateProvider;
private final @NonNull ObservableSupplier<TabModelFilter> mCurrentTabModelFilterSupplier;
private final TabListEditorLayout mTabListEditorLayout;
private final SelectionDelegate<Integer> mSelectionDelegate = new SelectionDelegate<>();
private final PropertyModel mModel;
private final TabListEditorMediator mTabListEditorMediator;
private final Callback<RecyclerViewPosition> mClientTabListRecyclerViewPositionSetter;
private final @TabListMode int mTabListMode;
private final boolean mDisplayGroups;
private final TabContentManager mTabContentManager;
private final @Nullable GridCardOnClickListenerProvider mGridCardOnClickListenerProvider;
private final @NonNull ModalDialogManager mModalDialogManager;
private MultiThumbnailCardProvider mMultiThumbnailCardProvider;
private TabListCoordinator mTabListCoordinator;
private PropertyModelChangeProcessor mTabListEditorLayoutChangeProcessor;
private @TabActionState int mTabActionState;
/**
* @param context The Android context to use.
* @param rootView The top ViewGroup which has parentView attached to it, or the same if no
* custom parentView is present.
* @param parentView The ViewGroup which the TabListEditor will attach itself to it may be
* rootView if no custom view is being used, or a sub-view which is then attached to
* rootView.
* @param browserControlsStateProvider Provides the browser controls state.
* @param currentTabModelFilterSupplier Supplies the current TabModelFilter.
* @param tabContentManager Provides thumbnails for tabs.
* @param clientTabListRecyclerViewPositionSetter Allows setting the recycler view position.
* @param mode Modes of showing the list of tabs. Can be used in GRID or STRIP.
* @param displayGroups Whether groups should be displayed.
* @param snackbarManager Used to display snackbar messages.
* @param bottomSheetController Used to display bottom sheets.
* @param initialTabActionState The initial TabActionState to use.
* @param modalDialogManager Used for managing the modal dialogs.
*/
public TabListEditorCoordinator(
Context context,
ViewGroup rootView,
ViewGroup parentView,
BrowserControlsStateProvider browserControlsStateProvider,
@NonNull ObservableSupplier<TabModelFilter> currentTabModelFilterSupplier,
TabContentManager tabContentManager,
Callback<RecyclerViewPosition> clientTabListRecyclerViewPositionSetter,
@TabListMode int mode,
boolean displayGroups,
SnackbarManager snackbarManager,
BottomSheetController bottomSheetController,
@TabActionState int initialTabActionState,
@Nullable
TabListMediator.GridCardOnClickListenerProvider gridCardOnClickListenerProvider,
@NonNull ModalDialogManager modalDialogManager) {
try (TraceEvent e = TraceEvent.scoped("TabListEditorCoordinator.constructor")) {
mContext = context;
mRootView = rootView;
mParentView = parentView;
mBrowserControlsStateProvider = browserControlsStateProvider;
mCurrentTabModelFilterSupplier = currentTabModelFilterSupplier;
mClientTabListRecyclerViewPositionSetter = clientTabListRecyclerViewPositionSetter;
mTabListMode = mode;
mDisplayGroups = displayGroups;
mTabActionState = initialTabActionState;
mTabContentManager = tabContentManager;
assert mode == TabListCoordinator.TabListMode.GRID
|| mode == TabListCoordinator.TabListMode.LIST;
mGridCardOnClickListenerProvider = gridCardOnClickListenerProvider;
mModalDialogManager = modalDialogManager;
// The change processor isn't created until TabListCoordinator is created (lazily).
mTabListEditorLayout =
LayoutInflater.from(context)
.inflate(R.layout.tab_list_editor_layout, parentView, false)
.findViewById(R.id.selectable_list);
mModel = new PropertyModel.Builder(TabListEditorProperties.ALL_KEYS).build();
// TODO(crbug.com/40881091): Refactor SnackbarManager to support multiple overridden
// parentViews in a stack to avoid contention and using new snackbar managers.
mTabListEditorMediator =
new TabListEditorMediator(
mContext,
mCurrentTabModelFilterSupplier,
mModel,
mSelectionDelegate,
displayGroups,
snackbarManager,
bottomSheetController,
mTabListEditorLayout,
mTabActionState);
mTabListEditorMediator.setNavigationProvider(
new TabListEditorNavigationProvider(mContext, mTabListEditorController));
}
}
/**
* @return The {@link SelectionDelegate} that is used in this component.
*/
SelectionDelegate<Integer> getSelectionDelegate() {
return mSelectionDelegate;
}
/**
* Resets {@link TabListCoordinator} with the provided list.
*
* @param tabs List of {@link Tab}s to reset.
* @param quickMode whether to use quick mode.
*/
void resetWithListOfTabs(@Nullable List<Tab> tabs, boolean quickMode) {
mTabListCoordinator.resetWithListOfTabs(tabs, quickMode);
}
/**
* @return {@link TabListEditorController} that can control the TabListEditor.
*/
TabListEditorController getController() {
return mTabListEditorController;
}
/** Destroy any members that needs clean up. */
public void destroy() {
if (mTabListCoordinator != null) {
mTabListCoordinator.onDestroy();
mTabListCoordinator = null;
}
if (mTabListEditorLayoutChangeProcessor != null) {
mTabListEditorLayoutChangeProcessor.destroy();
mTabListEditorLayoutChangeProcessor = null;
}
mTabListEditorLayout.destroy();
mTabListEditorMediator.destroy();
if (mMultiThumbnailCardProvider != null) {
mMultiThumbnailCardProvider.destroy();
}
}
/**
* Register a new view type for the underlying TabListCoordinator.
*
* @see MVCListAdapter#registerType(int, MVCListAdapter.ViewBuilder,
* PropertyModelChangeProcessor.ViewBinder).
*/
public <T extends View> void registerItemType(
@UiType int typeId,
MVCListAdapter.ViewBuilder<T> builder,
PropertyModelChangeProcessor.ViewBinder<PropertyModel, T, PropertyKey> binder) {
assert mTabListCoordinator != null;
mTabListCoordinator.registerItemType(typeId, builder, binder);
}
/**
* Inserts a special item into the underlying TabListCoordinator.
*
* @see TabListCoordinator#addSpecialItemToModel(int, int, PropertyModel).
*/
public void addSpecialListItem(int index, @UiType int uiType, PropertyModel model) {
assert mTabListCoordinator != null;
mTabListCoordinator.addSpecialListItem(index, uiType, model);
}
/**
* Removes a special {@link org.chromium.ui.modelutil.MVCListAdapter.ListItem} that has the
* given {@code uiType} and/or its {@link PropertyModel} has the given {@code itemIdentifier}.
*
* @param uiType The uiType to match.
* @param itemIdentifier The itemIdentifier to match. This can be obsoleted if the {@link
* org.chromium.ui.modelutil.MVCListAdapter.ListItem} does not need additional identifier.
*/
public void removeSpecialListItem(@UiType int uiType, int itemIdentifier) {
assert mTabListCoordinator != null;
mTabListCoordinator.removeSpecialListItem(uiType, itemIdentifier);
}
/**
* Override the content descriptions of the top-level layout and back button.
*
* @param containerContentDescription The content description for the top-level layout.
* @param backButtonContentDescription The content description for the back button.
*/
public void overrideContentDescriptions(
@StringRes int containerContentDescription,
@StringRes int backButtonContentDescription) {
mTabListEditorLayout.overrideContentDescriptions(
containerContentDescription, backButtonContentDescription);
}
private void createTabListCoordinator() {
Profile regularProfile =
mCurrentTabModelFilterSupplier
.get()
.getTabModel()
.getProfile()
.getOriginalProfile();
ResetHandler resetHandler =
new ResetHandler() {
@Override
public void resetWithListOfTabs(
@Nullable List<Tab> tabs,
@Nullable RecyclerViewPosition recyclerViewPosition,
boolean quickMode) {
TabListEditorCoordinator.this.resetWithListOfTabs(tabs, quickMode);
if (recyclerViewPosition == null) {
return;
}
mTabListCoordinator.setRecyclerViewPosition(recyclerViewPosition);
}
@Override
public void syncRecyclerViewPosition() {
if (mClientTabListRecyclerViewPositionSetter == null) {
return;
}
mClientTabListRecyclerViewPositionSetter.onResult(
mTabListCoordinator.getRecyclerViewPosition());
}
@Override
public void postHiding() {
mTabListCoordinator.postHiding();
mTabListCoordinator.softCleanup();
mTabListCoordinator.resetWithListOfTabs(null, /* quickMode= */ false);
}
};
ThumbnailProvider thumbnailProvider =
initMultiThumbnailCardProvider(mDisplayGroups, mTabContentManager);
if (mMultiThumbnailCardProvider != null) {
mMultiThumbnailCardProvider.initWithNative(regularProfile);
}
mTabListCoordinator =
new TabListCoordinator(
mTabListMode,
mContext,
mBrowserControlsStateProvider,
mModalDialogManager,
mCurrentTabModelFilterSupplier,
thumbnailProvider,
mDisplayGroups,
mGridCardOnClickListenerProvider,
/* dialogHandler= */ null,
mTabActionState,
this::getSelectionDelegate,
/* priceWelcomeMessageControllerSupplier= */ null,
mTabListEditorLayout,
/* attachToParent= */ false,
COMPONENT_NAME,
null,
/* allowDragAndDrop= */ false);
// Note: The TabListEditorCoordinator is always created after native is initialized.
mTabListCoordinator.initWithNative(regularProfile);
RecyclerView.LayoutManager layoutManager =
mTabListCoordinator.getContainerView().getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
((GridLayoutManager) layoutManager)
.setSpanSizeLookup(
new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int i) {
return 1;
}
});
}
mTabListEditorLayout.initialize(
mRootView,
mParentView,
mTabListCoordinator.getContainerView(),
mTabListCoordinator.getContainerView().getAdapter(),
mSelectionDelegate);
mSelectionDelegate.setSelectionModeEnabledForZeroItems(true);
mTabListEditorMediator.initializeWithTabListCoordinator(mTabListCoordinator, resetHandler);
mTabListEditorLayoutChangeProcessor =
PropertyModelChangeProcessor.create(
mModel, mTabListEditorLayout, TabListEditorLayoutBinder::bind);
}
private ThumbnailProvider initMultiThumbnailCardProvider(
boolean displayGroups, TabContentManager tabContentManager) {
if (displayGroups) {
mMultiThumbnailCardProvider =
new MultiThumbnailCardProvider(
mContext,
mBrowserControlsStateProvider,
tabContentManager,
mCurrentTabModelFilterSupplier);
return mMultiThumbnailCardProvider;
}
return new TabContentManagerThumbnailProvider(tabContentManager);
}
// Testing-specific methods
/**
* @return The {@link TabListEditorLayout} for testing.
*/
TabListEditorLayout getTabListEditorLayoutForTesting() {
return mTabListEditorLayout;
}
/**
* @return The {@link TabListRecyclerView} for testing.
*/
TabListRecyclerView getTabListRecyclerViewForTesting() {
return mTabListCoordinator.getContainerView();
}
}