// 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.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ValueChangedCallback;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.build.BuildConfig;
import org.chromium.chrome.browser.data_sharing.DataSharingServiceFactory;
import org.chromium.chrome.browser.data_sharing.DataSharingTabManager;
import org.chromium.chrome.browser.data_sharing.ui.shared_image_tiles.SharedImageTilesCoordinator;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncFeatures;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncUtils;
import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition;
import org.chromium.chrome.browser.tab_ui.TabUiThemeUtils;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilterObserver;
import org.chromium.chrome.browser.tasks.tab_management.MessageService.MessageType;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.ButtonType;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.IconPosition;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.ShowMode;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator.TabListEditorController;
import org.chromium.chrome.browser.tasks.tab_management.TabUiMetricsHelper.TabGroupColorChangeActionType;
import org.chromium.chrome.browser.tasks.tab_management.TabUiMetricsHelper.TabListEditorOpenMetricGroups;
import org.chromium.chrome.browser.tinker_tank.TinkerTankDelegateImpl;
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.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.data_sharing.DataSharingService;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.tab_group_sync.LocalTabGroupId;
import org.chromium.components.tab_group_sync.TabGroupSyncService;
import org.chromium.components.tab_groups.TabGroupColorId;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.text.EmptyTextWatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
* A mediator for the TabGridDialog component, responsible for communicating with the components'
* coordinator as well as managing the business logic for dialog show/hide.
*/
public class TabGridDialogMediator
implements SnackbarManager.SnackbarController,
TabGridDialogView.VisibilityListener,
TabGridItemTouchHelperCallback.OnLongPressTabItemEventListener {
/** Defines an interface for a {@link TabGridDialogMediator} to control dialog. */
interface DialogController extends BackPressHandler {
/**
* Handles a reset event originated from {@link TabGridDialogMediator} and {@link
* TabSwitcherMediator}.
*
* @param tabs List of Tabs to reset.
*/
void resetWithListOfTabs(@Nullable List<Tab> tabs);
/**
* Hide the TabGridDialog
* @param showAnimation Whether to show an animation when hiding the dialog.
*/
void hideDialog(boolean showAnimation);
/** Prepare the TabGridDialog before show. */
void prepareDialog();
/** Cleanup post hiding dialog. */
void postHiding();
/**
* @return Whether or not the TabGridDialog consumed the event.
*/
boolean handleBackPressed();
/**
* @return Whether the TabGridDialog is visible.
*/
boolean isVisible();
/** A supplier that returns if the dialog is currently showing or animating. */
ObservableSupplier<Boolean> getShowingOrAnimationSupplier();
/**
* Adds a message card to the UI.
*
* @param index The index to insert the card at.
* @param messageCardModel The {@link PropertyModel} using {@link MessageCardViewProperties}
* keys.
*/
void addMessageCardItem(int position, PropertyModel messageCardModel);
/**
* Removes a message card from the UI.
*
* @param messageType The type of message to remove.
*/
void removeMessageCardItem(@MessageType int messageType);
/**
* Checks whether a message card exists.
*
* @param messageType The type of message to look for.
*/
boolean messageCardExists(@MessageType int messageType);
}
/**
* Defines an interface for a {@link TabGridDialogMediator} to get the source {@link View} in
* order to prepare show/hide animation.
*/
interface AnimationSourceViewProvider {
/**
* Provide {@link View} of the source item to setup the animation.
*
* @param tabId The id of the tab whose position is requested.
* @return The source {@link View} used to setup the animation.
*/
View getAnimationSourceViewForTab(int tabId);
}
private final Activity mActivity;
private final PropertyModel mModel;
private final ObservableSupplier<TabModelFilter> mCurrentTabModelFilterSupplier;
private final ValueChangedCallback<TabModelFilter> mOnTabModelFilterChanged =
new ValueChangedCallback<>(this::onTabModelFilterChanged);
private final TabModelObserver mTabModelObserver;
private final TabGroupModelFilterObserver mTabGroupModelFilterObserver;
private final TabCreatorManager mTabCreatorManager;
private final DialogController mDialogController;
private final @Nullable TabSwitcherResetHandler mTabSwitcherResetHandler;
private final Supplier<RecyclerViewPosition> mRecyclerViewPositionSupplier;
private final AnimationSourceViewProvider mAnimationSourceViewProvider;
private final DialogHandler mTabGridDialogHandler;
private final Runnable mScrimClickRunnable;
private final @Nullable SnackbarManager mSnackbarManager;
private @Nullable SharedImageTilesCoordinator mSharedImageTilesCoordinator;
private final String mComponentName;
private final Runnable mShowColorPickerPopupRunnable;
private final ActionConfirmationManager mActionConfirmationManager;
private final @Nullable TabGroupSyncService mTabGroupSyncService;
private final DataSharingTabManager mDataSharingTabManager;
private TabGridDialogMenuCoordinator mTabGridDialogMenuCoordinator;
private TabGroupTitleEditor mTabGroupTitleEditor;
private Supplier<TabListEditorController> mTabListEditorControllerSupplier;
private boolean mTabListEditorSetup;
private KeyboardVisibilityDelegate.KeyboardVisibilityListener mKeyboardVisibilityListener;
private @Nullable String mCurrentCollaborationId;
private int mCurrentTabId = Tab.INVALID_TAB_ID;
private boolean mIsUpdatingTitle;
private String mCurrentGroupModifiedTitle;
private Profile mOriginalProfile;
private @Nullable CollaborationActivityMessageCardViewModel mCollaborationActivityPropertyModel;
TabGridDialogMediator(
Activity activity,
DialogController dialogController,
PropertyModel model,
ObservableSupplier<TabModelFilter> currentTabModelFilterSupplier,
TabCreatorManager tabCreatorManager,
@Nullable TabSwitcherResetHandler tabSwitcherResetHandler,
Supplier<RecyclerViewPosition> recyclerViewPositionSupplier,
AnimationSourceViewProvider animationSourceViewProvider,
@Nullable SnackbarManager snackbarManager,
@Nullable SharedImageTilesCoordinator sharedImageTilesCoordinator,
@NonNull DataSharingTabManager dataSharingTabManager,
String componentName,
Runnable showColorPickerPopupRunnable,
@Nullable ActionConfirmationManager actionConfirmationManager) {
mModel = model;
mCurrentTabModelFilterSupplier = currentTabModelFilterSupplier;
mTabCreatorManager = tabCreatorManager;
mDialogController = dialogController;
mTabSwitcherResetHandler = tabSwitcherResetHandler;
mRecyclerViewPositionSupplier = recyclerViewPositionSupplier;
mAnimationSourceViewProvider = animationSourceViewProvider;
mTabGridDialogHandler = new DialogHandler();
mSnackbarManager = snackbarManager;
mComponentName = componentName;
mActivity = activity;
mSharedImageTilesCoordinator = sharedImageTilesCoordinator;
mShowColorPickerPopupRunnable = showColorPickerPopupRunnable;
mActionConfirmationManager = actionConfirmationManager;
mDataSharingTabManager = dataSharingTabManager;
mOriginalProfile =
mCurrentTabModelFilterSupplier
.get()
.getTabModel()
.getProfile()
.getOriginalProfile();
if (TabGroupSyncFeatures.isTabGroupSyncEnabled(mOriginalProfile)) {
mTabGroupSyncService = TabGroupSyncServiceFactory.getForProfile(mOriginalProfile);
} else {
mTabGroupSyncService = null;
}
mTabModelObserver =
new TabModelObserver() {
@Override
public void didAddTab(
Tab tab,
@TabLaunchType int type,
@TabCreationState int creationState,
boolean markedForSelection) {
if (!isVisible()) return;
TabModelFilter filter = mCurrentTabModelFilterSupplier.get();
if (filter == null || !filter.isTabModelRestored()) {
return;
}
// For tab group sync a tab can be added without needing to close the tab
// grid dialog. The UI updates are driven from TabListMediator's
// TabGroupModelFilterObserver's didMergeTabToGroup implementation.
if (type == TabLaunchType.FROM_SYNC_BACKGROUND) {
return;
}
hideDialog(false);
}
@Override
public void tabClosureUndone(Tab tab) {
// Allow this to update when invisible so the undo bar is handled correctly.
updateDialog();
updateGridTabSwitcher();
dismissSingleTabSnackbar(tab.getId());
}
@Override
public void didSelectTab(Tab tab, int type, int lastId) {
if (!isVisible()) return;
// When this grid dialog is opened via the tab switcher there is a
// `mTabSwitcherResetHandler`.
boolean isTabSwitcherContext = mTabSwitcherResetHandler != null;
if (type == TabSelectionType.FROM_USER && !isTabSwitcherContext) {
// Hide the dialog from the strip context only.
hideDialog(false);
} else if (getRelatedTabs(mCurrentTabId).contains(tab)) {
mCurrentTabId = tab.getId();
}
}
@Override
public void willCloseTab(Tab tab, boolean didCloseAlone) {
if (!isVisible()) return;
// Ignore updates to tabs in other tab groups.
boolean closingTabIsCurrentTab = tab.getId() == mCurrentTabId;
if (!closingTabIsCurrentTab
&& !currentTabRootIdMatchesRootId(tab.getRootId())) {
return;
}
List<Tab> relatedTabs = getRelatedTabs(tab.getId());
// If the group is empty, update the animation and hide the dialog.
if (relatedTabs.size() == 0) {
hideDialog(false);
return;
}
// If current tab is closed and tab group is not empty, hand over ID of the
// next tab in the group to mCurrentTabId.
if (closingTabIsCurrentTab) {
mCurrentTabId = relatedTabs.get(0).getId();
}
updateDialog();
updateGridTabSwitcher();
}
@Override
public void tabPendingClosure(Tab tab) {
if (!isVisible()) return;
// TODO(b/338447134): This shouldn't show a snackbar if the tab isn't in
// this group. However, background closures are currently not-undoable so
// this is fine for now...
showSingleTabClosureSnackbar(tab);
}
@Override
public void multipleTabsPendingClosure(
List<Tab> closedTabs, boolean isAllTabs) {
if (!isVisible() || mSnackbarManager == null) return;
// TODO(b/338447134): This shouldn't show a snackbar if the tabs aren't in
// this group. However, background closures are currently not-undoable so
// this is fine for now...
if (closedTabs.size() == 1) {
showSingleTabClosureSnackbar(closedTabs.get(0));
return;
}
assert !isAllTabs;
String content =
String.format(Locale.getDefault(), "%d", closedTabs.size());
mSnackbarManager.showSnackbar(
Snackbar.make(
content,
TabGridDialogMediator.this,
Snackbar.TYPE_ACTION,
Snackbar.UMA_TAB_CLOSE_MULTIPLE_UNDO)
.setTemplateText(
mActivity.getString(
R.string.undo_bar_close_all_message))
.setAction(mActivity.getString(R.string.undo), closedTabs));
}
@Override
public void tabClosureCommitted(Tab tab) {
// Allow this to update while invisible so the snackbar updates correctly.
dismissSingleTabSnackbar(tab.getId());
}
@Override
public void onFinishingMultipleTabClosure(List<Tab> tabs, boolean canRestore) {
// Allow this to update while invisible so the snackbar updates correctly.
if (tabs.size() == 1) {
dismissSingleTabSnackbar(tabs.get(0).getId());
return;
}
dismissMultipleTabSnackbar(tabs);
}
@Override
public void allTabsClosureCommitted(boolean isIncognito) {
// Allow this to update while invisible so the snackbar updates correctly.
dismissAllSnackbars();
}
private void showSingleTabClosureSnackbar(Tab tab) {
if (mSnackbarManager == null) return;
mSnackbarManager.showSnackbar(
Snackbar.make(
tab.getTitle(),
TabGridDialogMediator.this,
Snackbar.TYPE_ACTION,
Snackbar.UMA_TAB_CLOSE_UNDO)
.setTemplateText(
mActivity.getString(
R.string.undo_bar_close_message))
.setAction(
mActivity.getString(R.string.undo), tab.getId()));
}
private void dismissMultipleTabSnackbar(List<Tab> tabs) {
if (mSnackbarManager == null) return;
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
mSnackbarManager.dismissSnackbars(
TabGridDialogMediator.this, tabs);
});
}
private void dismissSingleTabSnackbar(int tabId) {
if (mSnackbarManager == null) return;
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
mSnackbarManager.dismissSnackbars(
TabGridDialogMediator.this, tabId);
});
}
private void dismissAllSnackbars() {
if (mSnackbarManager == null) return;
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
mSnackbarManager.dismissSnackbars(TabGridDialogMediator.this);
});
}
};
mTabGroupModelFilterObserver =
new TabGroupModelFilterObserver() {
@Override
public void didChangeTabGroupTitle(int rootId, String newTitle) {
if (currentTabRootIdMatchesRootId(rootId)
&& !Objects.equals(
mModel.get(TabGridDialogProperties.HEADER_TITLE),
newTitle)) {
int tabsCount = getRelatedTabs(mCurrentTabId).size();
updateTitle(tabsCount);
}
}
@Override
public void didChangeTabGroupColor(int rootId, @TabGroupColorId int newColor) {
if (!ChromeFeatureList.sTabGroupParityAndroid.isEnabled()) return;
if (currentTabRootIdMatchesRootId(rootId)) {
mModel.set(TabGridDialogProperties.TAB_GROUP_COLOR_ID, newColor);
}
}
};
mOnTabModelFilterChanged.onResult(
mCurrentTabModelFilterSupplier.addObserver(mOnTabModelFilterChanged));
// Setup ScrimView click Runnable.
mScrimClickRunnable =
() -> {
hideDialog(true);
RecordUserAction.record("TabGridDialog.Exit");
};
mModel.set(TabGridDialogProperties.VISIBILITY_LISTENER, this);
mModel.set(TabGridDialogProperties.IS_DIALOG_VISIBLE, false);
mModel.set(
TabGridDialogProperties.UNGROUP_BAR_STATUS,
TabGridDialogView.UngroupBarStatus.HIDE);
}
public void initWithNative(
@NonNull Supplier<TabListEditorController> tabListEditorControllerSupplier,
TabGroupTitleEditor tabGroupTitleEditor) {
mTabListEditorControllerSupplier = tabListEditorControllerSupplier;
mTabGroupTitleEditor = tabGroupTitleEditor;
assert mCurrentTabModelFilterSupplier.get() instanceof TabGroupModelFilter;
setupToolbarClickHandlers();
setupToolbarEditText();
mModel.set(TabGridDialogProperties.MENU_CLICK_LISTENER, getMenuButtonClickListener());
// TODO(b/325082444): Only a subset should be visible at a time. Only set the listeners that
// can be seen and used.
mModel.set(TabGridDialogProperties.SHARE_BUTTON_CLICK_LISTENER, getShareBarClickListener());
mModel.set(
TabGridDialogProperties.SHARE_IMAGE_TILES_CLICK_LISTENER,
getShareBarClickListener());
}
void hideDialog(boolean showAnimation) {
if (!mModel.get(TabGridDialogProperties.IS_DIALOG_VISIBLE)) {
if (!showAnimation) {
// Forcibly finish any pending animations.
mModel.set(TabGridDialogProperties.FORCE_ANIMATION_TO_FINISH, true);
mModel.set(TabGridDialogProperties.FORCE_ANIMATION_TO_FINISH, false);
}
return;
}
if (mModel.get(TabGridDialogProperties.IS_SHARE_SHEET_VISIBLE)) {
// TODO(b/333776074): Close the ShareSheet without causing a crash at accessibility
// important restoration.
}
if (mSnackbarManager != null) {
mSnackbarManager.dismissSnackbars(TabGridDialogMediator.this);
}
// Save the title first so that the animation has the correct title.
saveCurrentGroupModifiedTitle();
mModel.set(TabGridDialogProperties.IS_TITLE_TEXT_FOCUSED, false);
if (!showAnimation) {
mModel.set(TabGridDialogProperties.ANIMATION_SOURCE_VIEW, null);
} else {
if (mAnimationSourceViewProvider != null && mCurrentTabId != Tab.INVALID_TAB_ID) {
mModel.set(
TabGridDialogProperties.ANIMATION_SOURCE_VIEW,
mAnimationSourceViewProvider.getAnimationSourceViewForTab(mCurrentTabId));
}
}
if (mTabListEditorControllerSupplier != null
&& mTabListEditorControllerSupplier.hasValue()) {
mTabListEditorControllerSupplier.get().hide();
}
// Hide view first. Listener will reset tabs on #finishedHiding.
mModel.set(TabGridDialogProperties.IS_DIALOG_VISIBLE, false);
}
/**
* @return a boolean indicating if the result of handling the backpress was successful.
*/
public boolean handleBackPress() {
if (mTabListEditorControllerSupplier != null
&& mTabListEditorControllerSupplier.hasValue()
&& mTabListEditorControllerSupplier.get().isVisible()) {
mTabListEditorControllerSupplier.get().hide();
return !mTabListEditorControllerSupplier.get().isVisible();
}
hideDialog(true);
RecordUserAction.record("TabGridDialog.Exit");
return !isVisible();
}
// @TabGridDialogView.VisibilityListener
@Override
public void finishedHidingDialogView() {
removeCollaborationActivityMessageCard();
mDialogController.resetWithListOfTabs(null);
mDialogController.postHiding();
// Purge the bitmap reference in the animation.
mModel.set(TabGridDialogProperties.ANIMATION_SOURCE_VIEW, null);
mModel.set(TabGridDialogProperties.BINDING_TOKEN, null);
}
void onReset(@Nullable List<Tab> tabs) {
TabModelFilter filter = mCurrentTabModelFilterSupplier.get();
if (tabs == null) {
mCurrentTabId = Tab.INVALID_TAB_ID;
} else {
mCurrentTabId = filter.getTabAt(filter.indexOf(tabs.get(0))).getId();
}
updateShareData();
if (mCurrentTabId != Tab.INVALID_TAB_ID) {
if (mAnimationSourceViewProvider != null) {
mModel.set(
TabGridDialogProperties.ANIMATION_SOURCE_VIEW,
mAnimationSourceViewProvider.getAnimationSourceViewForTab(mCurrentTabId));
} else {
mModel.set(TabGridDialogProperties.ANIMATION_SOURCE_VIEW, null);
}
updateDialog();
mModel.set(TabGridDialogProperties.SCRIMVIEW_CLICK_RUNNABLE, mScrimClickRunnable);
updateDialogScrollPosition();
// Do this after the dialog is updated so most attributes are not set with stale values
// when the binding token is set.
mModel.set(TabGridDialogProperties.BINDING_TOKEN, hashCode());
mDialogController.prepareDialog();
mModel.set(TabGridDialogProperties.IS_DIALOG_VISIBLE, true);
} else if (isVisible()) {
mModel.set(TabGridDialogProperties.IS_DIALOG_VISIBLE, false);
}
}
/** Destroy any members that needs clean up. */
public void destroy() {
removeTabModelFilterObserver(mCurrentTabModelFilterSupplier.get());
mCurrentTabModelFilterSupplier.removeObserver(mOnTabModelFilterChanged);
KeyboardVisibilityDelegate.getInstance()
.removeKeyboardVisibilityListener(mKeyboardVisibilityListener);
}
boolean isVisible() {
return mModel.get(TabGridDialogProperties.IS_DIALOG_VISIBLE);
}
void setSelectedTabGroupColor(int selectedColor) {
mModel.set(TabGridDialogProperties.TAB_GROUP_COLOR_ID, selectedColor);
TabGroupModelFilter filter = (TabGroupModelFilter) mCurrentTabModelFilterSupplier.get();
Tab currentTab = filter.getTabModel().getTabById(mCurrentTabId);
if (currentTab != null) {
filter.setTabGroupColor(currentTab.getRootId(), selectedColor);
}
}
private void updateGridTabSwitcher() {
if (!isVisible() || mTabSwitcherResetHandler == null) return;
mTabSwitcherResetHandler.resetWithTabList(mCurrentTabModelFilterSupplier.get(), false);
}
private void updateDialog() {
final int tabsCount = getRelatedTabs(mCurrentTabId).size();
if (tabsCount == 0) {
hideDialog(true);
return;
}
Resources res = mActivity.getResources();
// Change the ungroup bar text if the tab being ungrouped is the last tab in the group.
final @StringRes int ungroupBarTextId =
tabsCount == 1
? R.string.remove_last_tab_action
: R.string.tab_grid_dialog_remove_from_group;
mModel.set(
TabGridDialogProperties.DIALOG_UNGROUP_BAR_TEXT, res.getString(ungroupBarTextId));
if (ChromeFeatureList.sTabGroupParityAndroid.isEnabled()) {
TabGroupModelFilter filter = (TabGroupModelFilter) mCurrentTabModelFilterSupplier.get();
Tab currentTab = filter.getTabModel().getTabById(mCurrentTabId);
final @TabGroupColorId int color =
filter.getTabGroupColorWithFallback(currentTab.getRootId());
mModel.set(TabGridDialogProperties.TAB_GROUP_COLOR_ID, color);
}
updateTitle(tabsCount);
}
private void updateTitle(int tabsCount) {
Resources res = mActivity.getResources();
TabGroupModelFilter filter = (TabGroupModelFilter) mCurrentTabModelFilterSupplier.get();
Tab currentTab = filter.getTabModel().getTabById(mCurrentTabId);
if (mTabGroupTitleEditor != null) {
String storedTitle = mTabGroupTitleEditor.getTabGroupTitle(currentTab.getRootId());
if (storedTitle != null && filter.isTabInTabGroup(currentTab)) {
mModel.set(
TabGridDialogProperties.COLLAPSE_BUTTON_CONTENT_DESCRIPTION,
res.getQuantityString(
R.plurals.accessibility_dialog_back_button_with_group_name,
tabsCount,
storedTitle,
tabsCount));
mModel.set(TabGridDialogProperties.HEADER_TITLE, storedTitle);
return;
}
}
mModel.set(
TabGridDialogProperties.COLLAPSE_BUTTON_CONTENT_DESCRIPTION,
res.getQuantityString(
R.plurals.accessibility_dialog_back_button, tabsCount, tabsCount));
mModel.set(
TabGridDialogProperties.HEADER_TITLE,
TabGroupTitleEditor.getDefaultTitle(mActivity, tabsCount));
}
private void updateColorProperties(Context context, boolean isIncognito) {
@ColorInt
int dialogBackgroundColor =
TabUiThemeProvider.getTabGridDialogBackgroundColor(context, isIncognito);
ColorStateList tintList =
isIncognito
? AppCompatResources.getColorStateList(
mActivity, R.color.default_icon_color_light_tint_list)
: AppCompatResources.getColorStateList(
mActivity, R.color.default_icon_color_tint_list);
@ColorInt
int ungroupBarBackgroundColor =
TabUiThemeProvider.getTabGridDialogUngroupBarBackgroundColor(context, isIncognito);
@ColorInt
int ungroupBarHoveredBackgroundColor =
TabUiThemeProvider.getTabGridDialogUngroupBarHoveredBackgroundColor(
context, isIncognito);
@ColorInt
int ungroupBarTextColor =
TabUiThemeProvider.getTabGridDialogUngroupBarTextColor(context, isIncognito);
@ColorInt
int ungroupBarHoveredTextColor =
TabUiThemeProvider.getTabGridDialogUngroupBarHoveredTextColor(context, isIncognito);
@ColorInt
int hairlineColor =
isIncognito
? ContextCompat.getColor(context, R.color.divider_line_bg_color_light)
: SemanticColorUtils.getDividerLineBgColor(context);
mModel.set(TabGridDialogProperties.DIALOG_BACKGROUND_COLOR, dialogBackgroundColor);
mModel.set(TabGridDialogProperties.HAIRLINE_COLOR, hairlineColor);
mModel.set(TabGridDialogProperties.TINT, tintList);
mModel.set(
TabGridDialogProperties.DIALOG_UNGROUP_BAR_BACKGROUND_COLOR,
ungroupBarBackgroundColor);
mModel.set(
TabGridDialogProperties.DIALOG_UNGROUP_BAR_HOVERED_BACKGROUND_COLOR,
ungroupBarHoveredBackgroundColor);
mModel.set(TabGridDialogProperties.DIALOG_UNGROUP_BAR_TEXT_COLOR, ungroupBarTextColor);
mModel.set(
TabGridDialogProperties.DIALOG_UNGROUP_BAR_HOVERED_TEXT_COLOR,
ungroupBarHoveredTextColor);
mModel.set(TabGridDialogProperties.IS_INCOGNITO, isIncognito);
if (TabUiFeatureUtilities.shouldUseListMode()) {
int animationBackgroundColor =
TabUiThemeUtils.getCardViewBackgroundColor(
mActivity, isIncognito, /* isSelected= */ false);
mModel.set(
TabGridDialogProperties.ANIMATION_BACKGROUND_COLOR, animationBackgroundColor);
}
}
private int getIdForTab(@Nullable Tab tab) {
return tab == null ? Tab.INVALID_TAB_ID : tab.getId();
}
private void updateDialogScrollPosition() {
// If current selected tab is not within this dialog, always scroll to the top.
Tab currentTab = TabModelUtils.getCurrentTab(mCurrentTabModelFilterSupplier.get());
if (mCurrentTabId != getIdForTab(currentTab)) {
mModel.set(TabGridDialogProperties.INITIAL_SCROLL_INDEX, 0);
return;
}
List<Tab> relatedTabs = getRelatedTabs(mCurrentTabId);
int initialPosition = relatedTabs.indexOf(currentTab);
mModel.set(TabGridDialogProperties.INITIAL_SCROLL_INDEX, initialPosition);
}
private void setupToolbarClickHandlers() {
mModel.set(
TabGridDialogProperties.COLLAPSE_CLICK_LISTENER, getCollapseButtonClickListener());
mModel.set(TabGridDialogProperties.ADD_CLICK_LISTENER, getAddButtonClickListener());
}
private void configureTabListEditorMenu() {
assert mTabListEditorControllerSupplier != null;
if (mTabListEditorSetup) {
return;
}
mTabListEditorSetup = true;
List<TabListEditorAction> actions = new ArrayList<>();
actions.add(
TabListEditorSelectionAction.createAction(
mActivity, ShowMode.MENU_ONLY, ButtonType.ICON_AND_TEXT, IconPosition.END));
actions.add(
TabListEditorCloseAction.createAction(
mActivity,
ShowMode.MENU_ONLY,
ButtonType.ICON_AND_TEXT,
IconPosition.START,
mActionConfirmationManager));
actions.add(
TabListEditorUngroupAction.createAction(
mActivity,
ShowMode.MENU_ONLY,
ButtonType.ICON_AND_TEXT,
IconPosition.START,
mActionConfirmationManager));
actions.add(
TabListEditorBookmarkAction.createAction(
mActivity,
ShowMode.MENU_ONLY,
ButtonType.ICON_AND_TEXT,
IconPosition.START));
if (TinkerTankDelegateImpl.enabled()) {
actions.add(
TabListEditorTinkerTankAction.createAction(
mActivity,
ShowMode.MENU_ONLY,
ButtonType.ICON_AND_TEXT,
IconPosition.START));
}
actions.add(
TabListEditorShareAction.createAction(
mActivity,
ShowMode.MENU_ONLY,
ButtonType.ICON_AND_TEXT,
IconPosition.START));
mTabListEditorControllerSupplier.get().configureToolbarWithMenuItems(actions);
}
private void setupToolbarEditText() {
mKeyboardVisibilityListener =
isShowing -> {
mModel.set(TabGridDialogProperties.TITLE_CURSOR_VISIBILITY, isShowing);
if (!isShowing) {
mModel.set(TabGridDialogProperties.IS_TITLE_TEXT_FOCUSED, false);
saveCurrentGroupModifiedTitle();
}
};
KeyboardVisibilityDelegate.getInstance()
.addKeyboardVisibilityListener(mKeyboardVisibilityListener);
TextWatcher textWatcher =
new EmptyTextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (!mIsUpdatingTitle) return;
mCurrentGroupModifiedTitle = s.toString();
}
};
mModel.set(TabGridDialogProperties.TITLE_TEXT_WATCHER, textWatcher);
View.OnFocusChangeListener onFocusChangeListener =
(v, hasFocus) -> {
mIsUpdatingTitle = hasFocus;
mModel.set(TabGridDialogProperties.IS_KEYBOARD_VISIBLE, hasFocus);
mModel.set(TabGridDialogProperties.IS_TITLE_TEXT_FOCUSED, hasFocus);
};
mModel.set(TabGridDialogProperties.TITLE_TEXT_ON_FOCUS_LISTENER, onFocusChangeListener);
}
private View.OnClickListener getCollapseButtonClickListener() {
return view -> {
hideDialog(true);
RecordUserAction.record("TabGridDialog.Exit");
};
}
private View.OnClickListener getAddButtonClickListener() {
return view -> {
// Get the current Tab first since hideDialog causes mCurrentTabId to be
// Tab.INVALID_TAB_ID.
TabModelFilter filter = mCurrentTabModelFilterSupplier.get();
Tab currentTab = filter.getTabModel().getTabById(mCurrentTabId);
hideDialog(false);
// Reset the list of tabs so the new tab doesn't appear on the dialog before the
// animation.
if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) {
mDialogController.resetWithListOfTabs(null);
}
if (currentTab == null) {
mTabCreatorManager.getTabCreator(filter.isIncognito()).launchNtp();
return;
}
TabUiUtils.openNtpInGroup(
(TabGroupModelFilter) mCurrentTabModelFilterSupplier.get(),
mTabCreatorManager.getTabCreator(filter.isIncognito()),
currentTab.getId(),
TabLaunchType.FROM_TAB_GROUP_UI);
RecordUserAction.record("MobileNewTabOpened." + mComponentName);
};
}
@VisibleForTesting
public void onToolbarMenuItemClick(int menuId, int tabId, String collaborationId) {
assert tabId == mCurrentTabId;
assert Objects.equals(collaborationId, mCurrentCollaborationId);
if (menuId == R.id.ungroup_tab || menuId == R.id.select_tabs) {
RecordUserAction.record("TabGridDialogMenu.SelectTabs");
mModel.set(TabGridDialogProperties.IS_TITLE_TEXT_FOCUSED, false);
if (setupAndShowTabListEditor(tabId)) {
TabUiMetricsHelper.recordSelectionEditorOpenMetrics(
TabListEditorOpenMetricGroups.OPEN_FROM_DIALOG, mActivity);
}
} else if (menuId == R.id.edit_group_name) {
RecordUserAction.record("TabGridDialogMenu.Rename");
mModel.set(TabGridDialogProperties.IS_TITLE_TEXT_FOCUSED, true);
} else if (menuId == R.id.edit_group_color) {
RecordUserAction.record("TabGridDialogMenu.EditColor");
mShowColorPickerPopupRunnable.run();
TabUiMetricsHelper.recordTabGroupColorChangeActionMetrics(
TabGroupColorChangeActionType.VIA_OVERFLOW_MENU);
} else if (menuId == R.id.manage_sharing) {
RecordUserAction.record("TabGridDialogMenu.ManageSharing");
mDataSharingTabManager.showManageSharing(mActivity, collaborationId);
} else if (menuId == R.id.recent_activity) {
RecordUserAction.record("TabGridDialogMenu.RecentActivity");
mDataSharingTabManager.showRecentActivity(collaborationId);
} else if (menuId == R.id.close_tab || menuId == R.id.delete_tab) {
boolean hideTabGroups = menuId == R.id.close_tab;
if (hideTabGroups) {
RecordUserAction.record("TabGridDialogMenu.Close");
} else {
RecordUserAction.record("TabGridDialogMenu.Delete");
}
TabUiUtils.closeTabGroup(
(TabGroupModelFilter) mCurrentTabModelFilterSupplier.get(),
mActionConfirmationManager,
tabId,
hideTabGroups,
mTabGroupSyncService != null,
/* didCloseCallback= */ null);
} else if (menuId == R.id.delete_shared_group) {
RecordUserAction.record("TabGridDialogMenu.DeleteShared");
TabUiUtils.deleteSharedTabGroup(
(TabGroupModelFilter) mCurrentTabModelFilterSupplier.get(),
mActionConfirmationManager,
tabId);
} else if (menuId == R.id.leave_group) {
RecordUserAction.record("TabGridDialogMenu.LeaveShared");
TabUiUtils.leaveTabGroup(
(TabGroupModelFilter) mCurrentTabModelFilterSupplier.get(),
mActionConfirmationManager,
tabId);
}
}
private View.OnClickListener getMenuButtonClickListener() {
assert mTabListEditorControllerSupplier != null;
boolean isTabGroupSyncEnabled = mTabGroupSyncService != null;
IdentityManager identityManager = null;
DataSharingService dataSharingService = null;
if (isTabGroupSyncEnabled && ChromeFeatureList.isEnabled(ChromeFeatureList.DATA_SHARING)) {
identityManager = IdentityServicesProvider.get().getIdentityManager(mOriginalProfile);
dataSharingService = DataSharingServiceFactory.getForProfile(mOriginalProfile);
}
if (mTabGridDialogMenuCoordinator == null) {
mTabGridDialogMenuCoordinator =
new TabGridDialogMenuCoordinator(
this::onToolbarMenuItemClick,
() -> mCurrentTabModelFilterSupplier.get().getTabModel(),
() -> mCurrentTabId,
isTabGroupSyncEnabled,
identityManager,
mTabGroupSyncService,
dataSharingService);
}
return mTabGridDialogMenuCoordinator.getOnClickListener();
}
private View.OnClickListener getShareBarClickListener() {
return view -> {
handleShareClick();
};
}
private void handleShareClick() {
assert ChromeFeatureList.isEnabled(ChromeFeatureList.DATA_SHARING);
mModel.set(TabGridDialogProperties.IS_SHARE_SHEET_VISIBLE, true);
String tabGroupDisplayName = mModel.get(TabGridDialogProperties.HEADER_TITLE);
TabGroupModelFilter filter = (TabGroupModelFilter) mCurrentTabModelFilterSupplier.get();
Tab tab = filter.getTabModel().getTabById(mCurrentTabId);
LocalTabGroupId localTabGroupId = TabGroupSyncUtils.getLocalTabGroupId(tab);
mDataSharingTabManager.createGroupFlow(
mActivity,
tabGroupDisplayName,
localTabGroupId,
(groupCreated) -> {
mModel.set(TabGridDialogProperties.IS_SHARE_SHEET_VISIBLE, false);
if (groupCreated) {
updateShareData();
}
});
}
private void updateShareData() {
boolean isIncognitoBranded = mCurrentTabModelFilterSupplier.get().isIncognitoBranded();
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.DATA_SHARING)
|| isIncognitoBranded
|| mCurrentTabId == Tab.INVALID_TAB_ID) {
clearCollaborationId(/* showShareButton= */ false);
return;
}
assert mSharedImageTilesCoordinator != null;
@Nullable
String collaborationId =
TabShareUtils.getCollaborationIdOrNull(
mCurrentTabId,
mCurrentTabModelFilterSupplier.get().getTabModel(),
mTabGroupSyncService);
if (TabShareUtils.isCollaborationIdValid(collaborationId)) {
mCurrentCollaborationId = collaborationId;
mSharedImageTilesCoordinator.updateCollaborationId(mCurrentCollaborationId);
// TODO(crbug.com/363043430): Per UX spec the share button should remain visible until
// the image tiles contains at least one avatar. Fix this.
mModel.set(TabGridDialogProperties.SHOW_SHARE_BUTTON, false);
mModel.set(TabGridDialogProperties.SHOW_IMAGE_TILES, true);
showOrUpdateCollaborationActivityMessageCard();
} else {
clearCollaborationId(/* showShareButton= */ true);
}
}
private void clearCollaborationId(boolean showShareButton) {
mCurrentCollaborationId = null;
mModel.set(TabGridDialogProperties.SHOW_SHARE_BUTTON, showShareButton);
mModel.set(TabGridDialogProperties.SHOW_IMAGE_TILES, false);
if (mSharedImageTilesCoordinator != null) {
// Remove any images left in the shared image tiles component so they don't waste
// memory.
mSharedImageTilesCoordinator.updateCollaborationId(/* collaborationId= */ null);
}
removeCollaborationActivityMessageCard();
}
private List<Tab> getRelatedTabs(int tabId) {
return mCurrentTabModelFilterSupplier.get().getRelatedTabList(tabId);
}
private void saveCurrentGroupModifiedTitle() {
TabModelFilter filter = mCurrentTabModelFilterSupplier.get();
Tab currentTab = filter.getTabModel().getTabById(mCurrentTabId);
// When current group no longer exists, skip saving the title.
if (currentTab == null || !filter.isTabInTabGroup(currentTab)) {
mCurrentGroupModifiedTitle = null;
}
if (mCurrentGroupModifiedTitle == null) {
return;
}
assert mTabGroupTitleEditor != null;
int tabsCount = getRelatedTabs(mCurrentTabId).size();
if (mCurrentGroupModifiedTitle.length() == 0
|| mTabGroupTitleEditor.isDefaultTitle(mCurrentGroupModifiedTitle, tabsCount)) {
// When dialog title is empty or was unchanged, delete previously stored title and
// restore default title.
mTabGroupTitleEditor.deleteTabGroupTitle(currentTab.getRootId());
String originalTitle = TabGroupTitleEditor.getDefaultTitle(mActivity, tabsCount);
mModel.set(
TabGridDialogProperties.COLLAPSE_BUTTON_CONTENT_DESCRIPTION,
mActivity.getResources()
.getQuantityString(
R.plurals.accessibility_dialog_back_button,
tabsCount,
tabsCount));
mModel.set(TabGridDialogProperties.HEADER_TITLE, originalTitle);
mTabGroupTitleEditor.updateTabGroupTitle(currentTab, originalTitle);
mCurrentGroupModifiedTitle = null;
RecordUserAction.record("TabGridDialog.ResetTabGroupName");
return;
}
mTabGroupTitleEditor.storeTabGroupTitle(currentTab.getRootId(), mCurrentGroupModifiedTitle);
mTabGroupTitleEditor.updateTabGroupTitle(currentTab, mCurrentGroupModifiedTitle);
int relatedTabsCount = getRelatedTabs(mCurrentTabId).size();
mModel.set(
TabGridDialogProperties.COLLAPSE_BUTTON_CONTENT_DESCRIPTION,
mActivity.getResources()
.getQuantityString(
R.plurals.accessibility_dialog_back_button_with_group_name,
relatedTabsCount,
mCurrentGroupModifiedTitle,
relatedTabsCount));
mModel.set(TabGridDialogProperties.HEADER_TITLE, mCurrentGroupModifiedTitle);
RecordUserAction.record("TabGridDialog.TabGroupNamedInDialog");
mCurrentGroupModifiedTitle = null;
}
TabListMediator.TabGridDialogHandler getTabGridDialogHandler() {
return mTabGridDialogHandler;
}
// SnackbarManager.SnackbarController implementation.
@Override
public void onAction(Object actionData) {
if (actionData instanceof Integer) {
int tabId = (Integer) actionData;
TabModel model = mCurrentTabModelFilterSupplier.get().getTabModel();
model.cancelTabClosure(tabId);
} else {
List<Tab> tabs = (List<Tab>) actionData;
if (tabs.isEmpty()) return;
TabModel model = mCurrentTabModelFilterSupplier.get().getTabModel();
for (Tab tab : tabs) {
model.cancelTabClosure(tab.getId());
}
}
}
@Override
public void onDismissNoAction(Object actionData) {
if (actionData instanceof Integer) {
int tabId = (Integer) actionData;
TabModel model = mCurrentTabModelFilterSupplier.get().getTabModel();
model.commitTabClosure(tabId);
} else {
List<Tab> tabs = (List<Tab>) actionData;
if (tabs.isEmpty()) return;
TabModel model = mCurrentTabModelFilterSupplier.get().getTabModel();
for (Tab tab : tabs) {
model.commitTabClosure(tab.getId());
}
}
}
// OnLongPressTabItemEventListener implementation
@Override
public void onLongPressEvent(int tabId) {
if (setupAndShowTabListEditor(tabId)) {
RecordUserAction.record("TabMultiSelectV2.OpenLongPressInDialog");
}
}
private boolean setupAndShowTabListEditor(int currentTabId) {
if (mTabListEditorControllerSupplier == null) return false;
List<Tab> tabs = getRelatedTabs(currentTabId);
// Setup dialog selection editor.
mTabListEditorControllerSupplier.get().show(tabs, mRecyclerViewPositionSupplier.get());
configureTabListEditorMenu();
return true;
}
private void onTabModelFilterChanged(
@Nullable TabModelFilter newFilter, @Nullable TabModelFilter oldFilter) {
removeTabModelFilterObserver(oldFilter);
if (newFilter != null) {
boolean isIncognito = newFilter.isIncognito();
updateColorProperties(mActivity, isIncognito);
newFilter.addObserver(mTabModelObserver);
((TabGroupModelFilter) newFilter).addTabGroupObserver(mTabGroupModelFilterObserver);
}
}
private void removeTabModelFilterObserver(@Nullable TabModelFilter filter) {
if (filter != null) {
filter.removeObserver(mTabModelObserver);
((TabGroupModelFilter) filter).removeTabGroupObserver(mTabGroupModelFilterObserver);
}
}
private boolean currentTabRootIdMatchesRootId(int rootId) {
Tab tab = mCurrentTabModelFilterSupplier.get().getTabModel().getTabById(mCurrentTabId);
return tab != null && tab.getRootId() == rootId;
}
/**
* A handler that handles TabGridDialog related changes originated from {@link TabListMediator}
* and {@link TabGridItemTouchHelperCallback}.
*/
class DialogHandler implements TabListMediator.TabGridDialogHandler {
@Override
public void updateUngroupBarStatus(@TabGridDialogView.UngroupBarStatus int status) {
mModel.set(TabGridDialogProperties.UNGROUP_BAR_STATUS, status);
}
@Override
public void updateDialogContent(int tabId) {
mCurrentTabId = tabId;
updateDialog();
}
}
int getCurrentTabIdForTesting() {
return mCurrentTabId;
}
void setCurrentTabIdForTesting(int tabId) {
var oldValue = mCurrentTabId;
mCurrentTabId = tabId;
ResettersForTesting.register(() -> mCurrentTabId = oldValue);
}
KeyboardVisibilityDelegate.KeyboardVisibilityListener
getKeyboardVisibilityListenerForTesting() {
return mKeyboardVisibilityListener;
}
boolean getIsUpdatingTitleForTesting() {
return mIsUpdatingTitle;
}
String getCurrentGroupModifiedTitleForTesting() {
return mCurrentGroupModifiedTitle;
}
Runnable getScrimClickRunnableForTesting() {
return mScrimClickRunnable;
}
private void removeCollaborationActivityMessageCard() {
mDialogController.removeMessageCardItem(MessageType.COLLABORATION_ACTIVITY);
mCollaborationActivityPropertyModel = null;
}
private void showOrUpdateCollaborationActivityMessageCard() {
if (mCurrentCollaborationId == null) {
assert mCollaborationActivityPropertyModel == null;
return;
}
// TODO(crbug.com/348731400): Fetch these numbers from the activity backend.
int tabsAdded = BuildConfig.IS_FOR_TEST ? 1 : 0;
int tabsChanged = BuildConfig.IS_FOR_TEST ? 2 : 0;
int tabsClosed = BuildConfig.IS_FOR_TEST ? 3 : 0;
if (tabsAdded == 0 && tabsChanged == 0 && tabsClosed == 0) {
removeCollaborationActivityMessageCard();
return;
}
if (mCollaborationActivityPropertyModel == null) {
mCollaborationActivityPropertyModel =
new CollaborationActivityMessageCardViewModel(
mActivity,
this::showRecentActivityOrDismissActivityMessageCard,
(unused) -> {
removeCollaborationActivityMessageCard();
});
}
mCollaborationActivityPropertyModel.updateDescriptionText(
mActivity, tabsAdded, tabsChanged, tabsClosed);
if (!mDialogController.messageCardExists(MessageType.COLLABORATION_ACTIVITY)) {
mDialogController.addMessageCardItem(
/* position= */ 0, mCollaborationActivityPropertyModel.getPropertyModel());
}
}
private void showRecentActivityOrDismissActivityMessageCard() {
if (mCurrentCollaborationId != null) {
mDataSharingTabManager.showRecentActivity(mCurrentCollaborationId);
} else {
removeCollaborationActivityMessageCard();
}
}
void setCurrentCollaborationIdForTesting(@Nullable String collaborationId) {
@Nullable String oldValue = mCurrentCollaborationId;
mCurrentCollaborationId = collaborationId;
ResettersForTesting.register(() -> mCurrentCollaborationId = oldValue);
}
}