// Copyright 2024 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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Resources;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Function;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.app.tabmodel.ArchivedTabModelOrchestrator;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabArchiveSettings;
import org.chromium.chrome.browser.tab_ui.OnTabSelectingListener;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.tab_management.MessageService.MessageType;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator.NavigationProvider;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator.TabListEditorController;
import org.chromium.chrome.browser.tasks.tab_management.TabListMediator.GridCardOnClickListenerProvider;
import org.chromium.chrome.browser.tasks.tab_management.TabListMediator.TabActionListener;
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.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.undo_tab_close_snackbar.UndoBarController;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.FadingShadow;
import org.chromium.components.browser_ui.widget.FadingShadowView;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.ui.interpolators.Interpolators;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.LayoutViewBuilder;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.util.TokenHolder;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class ArchivedTabsDialogCoordinator implements SnackbarManager.SnackbarManageable {
private static final int ANIM_DURATION_MS = 250;
/** Interface exposing functionality to the menu items for the archived tabs dialog */
public interface ArchiveDelegate {
/** Restore all tabs from the archived tab model. */
void restoreAllArchivedTabs();
/** Open the archive settings page. */
void openArchiveSettings();
/** Start tab selection process. */
void startTabSelection();
/** Restore the given list of tabs. */
void restoreArchivedTabs(List<Tab> tabs);
/** Close the given list of tabs. */
void closeArchivedTabs(List<Tab> tabs);
}
private final ArchiveDelegate mArchiveDelegate =
new ArchiveDelegate() {
@Override
public void restoreAllArchivedTabs() {
List<Tab> tabs = TabModelUtils.convertTabListToListOfTabs(mArchivedTabModel);
int tabCount = tabs.size();
ArchivedTabsDialogCoordinator.this.restoreArchivedTabs(tabs);
RecordHistogram.recordCount1000Histogram(
"Tabs.RestoreAllArchivedTabsMenuItem.TabCount", tabCount);
RecordUserAction.record("Tabs.RestoreAllArchivedTabsMenuItem");
}
@Override
public void openArchiveSettings() {
SettingsLauncherFactory.createSettingsLauncher()
.launchSettingsActivity(mContext, TabArchiveSettingsFragment.class);
RecordUserAction.record("Tabs.OpenArchivedTabsSettingsMenuItem");
}
@Override
public void startTabSelection() {
moveToState(TabActionState.SELECTABLE);
RecordUserAction.record("Tabs.SelectArchivedTabsMenuItem");
}
@Override
public void restoreArchivedTabs(List<Tab> tabs) {
int tabCount = tabs.size();
ArchivedTabsDialogCoordinator.this.restoreArchivedTabs(tabs);
moveToState(TabActionState.CLOSABLE);
RecordHistogram.recordCount1000Histogram(
"Tabs.RestoreArchivedTabsMenuItem.TabCount", tabCount);
RecordUserAction.record("Tabs.RestoreArchivedTabsMenuItem");
}
@Override
public void closeArchivedTabs(List<Tab> tabs) {
mArchivedTabModel.closeTabs(TabClosureParams.closeTabs(tabs).build());
RecordHistogram.recordCount1000Histogram(
"Tabs.CloseArchivedTabsMenuItem.TabCount", tabs.size());
RecordUserAction.record("Tabs.CloseArchivedTabsMenuItem");
}
};
private final NavigationProvider mNavigationProvider =
new NavigationProvider() {
@Override
public void goBack() {
if (mTabActionState == TabActionState.CLOSABLE) {
hide(ANIM_DURATION_MS, /* animationFinishCallback= */ () -> {});
} else {
moveToState(TabActionState.CLOSABLE);
}
}
};
/**
* Observes the tab count in the archived tab model to (1) update the title and (2) hide the
* dialog when no archived tabs remain.
*/
private final Callback<Integer> mTabCountObserver =
(count) -> {
if (count == 0 && !ArchivedTabsDialogCoordinator.this.mIsOpeningLastTab) {
// Post task to allow the last tab to be unregistered.
PostTask.postTask(
TaskTraits.UI_DEFAULT, () -> hide(ANIM_DURATION_MS, () -> {}));
return;
}
updateTitle();
};
/** Used to override the default tab click behavior to restore/open the tab. */
private final GridCardOnClickListenerProvider mGridCardOnCLickListenerProvider =
new GridCardOnClickListenerProvider() {
@Nullable
@Override
public TabActionListener openTabGridDialog(@NonNull Tab tab) {
return null;
}
@Override
public void onTabSelecting(int tabId, boolean fromActionButton) {
mIsOpeningLastTab = mArchivedTabModel.getCount() == 1;
Tab tab = mArchivedTabModel.getTabById(tabId);
mArchivedTabModelOrchestrator
.getTabArchiver()
.unarchiveAndRestoreTab(mRegularTabCreator, tab);
hide(
ANIM_DURATION_MS,
() -> {
// Post task to allow the tab to be unregistered.
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> mOnTabSelectingListener.onTabSelecting(tab.getId()));
RecordUserAction.record("Tabs.RestoreSingleTab");
});
}
};
private final TabArchiveSettings.Observer mTabArchiveSettingsObserver =
new TabArchiveSettings.Observer() {
@Override
public void onSettingChanged() {
updateIphPropertyModel();
}
};
/**
* Observes the TabListEditor lifecycle to remove the view and hide the dialog. This is useful
* for when (1) the TabListEditor is expecting the embedding view to be removed from the
* hierarchy prior to hide completion. (2) If the TabListEditor hides itself outside of the
* dialog control flow, we want to know about it in order to hide the embedding UI.
*/
private final TabListEditorCoordinator.LifecycleObserver mTabListEditorLifecycleObserver =
new TabListEditorCoordinator.LifecycleObserver() {
@Override
public void willHide() {
mDialogRecyclerView.removeOnScrollListener(mRecyclerScrollListener);
mSnackbarManager.popParentViewFromOverrideStack(mSnackbarOverrideToken);
// In case we were hidden by TabListEditor in some other case, force the
// animation to finish.
animateOut(/* duration= */ 0, /* animationFinishCallback= */ () -> {});
mRootView.removeView(mDialogView);
}
@Override
public void didHide() {
ArchivedTabsDialogCoordinator.this.hideInternal();
}
};
private final RecyclerView.OnScrollListener mRecyclerScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mShadowView.setVisibility(
recyclerView.canScrollVertically(1) ? View.VISIBLE : View.GONE);
}
};
private final @NonNull Context mContext;
private final @NonNull ArchivedTabModelOrchestrator mArchivedTabModelOrchestrator;
private final @NonNull TabModel mArchivedTabModel;
private final @NonNull BrowserControlsStateProvider mBrowserControlsStateProvider;
private final @NonNull TabContentManager mTabContentManager;
private final @TabListMode int mMode;
private final @NonNull ViewGroup mRootView;
private final @NonNull SnackbarManager mSnackbarManager;
private final @NonNull TabCreator mRegularTabCreator;
private final @NonNull BackPressManager mBackPressManager;
private final @NonNull TabArchiveSettings mTabArchiveSettings;
private final @NonNull ModalDialogManager mModalDialogManager;
private final @NonNull UndoBarController mUndoBarController;
private final @NonNull ActionConfirmationDialog mActionConfirmationDialog;
private final @NonNull ViewGroup mDialogView;
private final @NonNull ViewGroup mTabSwitcherView;
private final @NonNull FadingShadowView mShadowView;
private TabListRecyclerView mDialogRecyclerView;
private WeakReference<TabListRecyclerView> mTabSwitcherRecyclerView;
private @TabActionState int mTabActionState = TabActionState.CLOSABLE;
private TabListEditorCoordinator mTabListEditorCoordinator;
private OnTabSelectingListener mOnTabSelectingListener;
private PropertyModel mIphMessagePropertyModel;
private int mSnackbarOverrideToken;
private boolean mIsOpeningLastTab;
private boolean mIsShowing;
/**
* @param context The android context.
* @param archivedTabModelOrchestrator The TabModelOrchestrator for archived tabs.
* @param browserControlsStateProvider Used as a dependency to TabListEditorCoordiantor.
* @param tabContentManager Used as a dependency to TabListEditorCoordiantor.
* @param mode Used as a dependency to TabListEditorCoordiantor.
* @param rootView Used as a dependency to TabListEditorCoordiantor.
* @param snackbarManager Manages snackbars shown in the app.
* @param regularTabCreator Handles the creation of regular tabs.
* @param backPressManager Manages the different back press handlers throughout the app.
* @param tabArchiveSettings The settings manager for tab archive.
* @param modalDialogManager Used for managing the modal dialogs.
*/
public ArchivedTabsDialogCoordinator(
@NonNull Context context,
@NonNull ArchivedTabModelOrchestrator archivedTabModelOrchestrator,
@NonNull BrowserControlsStateProvider browserControlsStateProvider,
@NonNull TabContentManager tabContentManager,
@TabListMode int mode,
@NonNull ViewGroup rootView,
@NonNull ViewGroup tabSwitcherView,
@NonNull SnackbarManager snackbarManager,
@NonNull TabCreator regularTabCreator,
@NonNull BackPressManager backPressManager,
@NonNull TabArchiveSettings tabArchiveSettings,
@NonNull ModalDialogManager modalDialogManager) {
mContext = context;
mBrowserControlsStateProvider = browserControlsStateProvider;
mTabContentManager = tabContentManager;
mMode = mode;
mRootView = rootView;
mSnackbarManager = snackbarManager;
mRegularTabCreator = regularTabCreator;
mBackPressManager = backPressManager;
mTabArchiveSettings = tabArchiveSettings;
mModalDialogManager = modalDialogManager;
mArchivedTabModelOrchestrator = archivedTabModelOrchestrator;
mArchivedTabModel =
mArchivedTabModelOrchestrator
.getTabModelSelector()
.getModel(/* incognito= */ false);
mUndoBarController =
new UndoBarController(
mContext,
mArchivedTabModelOrchestrator.getTabModelSelector(),
/* snackbarManageable= */ this,
/* dialogVisibilitySupplier= */ null);
mTabSwitcherView = tabSwitcherView;
// Inflate the dialog view and hook it up
mDialogView =
(ViewGroup)
LayoutInflater.from(mContext)
.inflate(R.layout.archived_tabs_dialog, mRootView, false);
mDialogView
.findViewById(R.id.close_all_tabs_button)
.setOnClickListener(this::onCloseAllInactiveTabsButtonClicked);
// Initialize the shadow for the "Close all inactive tabs" container.
mShadowView = mDialogView.findViewById(R.id.close_all_tabs_button_container_shadow);
mShadowView.init(
mContext.getColor(R.color.toolbar_shadow_color), FadingShadow.POSITION_BOTTOM);
// Initialize the confirmation dialog for when the last archived tab is removed.
mActionConfirmationDialog = new ActionConfirmationDialog(mContext, mModalDialogManager);
}
/** Hides the dialog. */
public void destroy() {
if (mTabListEditorCoordinator != null
&& mTabListEditorCoordinator.getController().isVisible()) {
hide(/* animationDuration= */ 0, /* animationFinishCallback= */ () -> {});
}
}
/**
* Shows the dialog.
*
* @param onTabSelectingListener Allows a tab to be selected in the main tab switcher.
*/
public void show(OnTabSelectingListener onTabSelectingListener) {
if (mIsShowing) return;
mIsShowing = true;
mTabSwitcherRecyclerView =
new WeakReference<>(mTabSwitcherView.findViewById(R.id.tab_list_recycler_view));
mTabSwitcherRecyclerView.get().setBlockTouchInput(true);
boolean tabListFirstShown = false;
if (mTabListEditorCoordinator == null) {
tabListFirstShown = true;
createTabListEditorCoordinator();
}
mOnTabSelectingListener = onTabSelectingListener;
mArchivedTabModel.getTabCountSupplier().addObserver(mTabCountObserver);
mUndoBarController.initialize();
TabListEditorController controller = mTabListEditorCoordinator.getController();
controller.setLifecycleObserver(mTabListEditorLifecycleObserver);
controller.show(TabModelUtils.convertTabListToListOfTabs(mArchivedTabModel), null);
controller.setNavigationProvider(mNavigationProvider);
mTabListEditorCoordinator.overrideContentDescriptions(
R.string.accessibility_archived_tabs_dialog,
R.string.accessibility_archived_tabs_dialog_back_button);
mDialogRecyclerView = mDialogView.findViewById(R.id.tab_list_recycler_view);
mDialogRecyclerView.addOnScrollListener(mRecyclerScrollListener);
mShadowView.setVisibility(
mDialogRecyclerView.canScrollVertically(1) ? View.VISIBLE : View.GONE);
// Register the dialog to handle back press events.
mBackPressManager.addHandler(controller, BackPressHandler.Type.ARCHIVED_TABS_DIALOG);
FrameLayout snackbarContainer = mDialogView.findViewById(R.id.snackbar_container);
mSnackbarOverrideToken = mSnackbarManager.pushParentViewToOverrideStack(snackbarContainer);
// View is obscured by the TabListEditorCoordinator, so it needs to be brought to the front.
mDialogView.findViewById(R.id.close_all_tabs_button_container).bringToFront();
snackbarContainer.bringToFront();
// Add the IPH to the TabListEditor.
if (mTabArchiveSettings.shouldShowDialogIph()) {
if (tabListFirstShown) {
mTabListEditorCoordinator.registerItemType(
TabProperties.UiType.MESSAGE,
new LayoutViewBuilder(R.layout.tab_grid_message_card_item),
MessageCardViewBinder::bind);
}
mIphMessagePropertyModel =
ArchivedTabsIphMessageCardViewModel.create(
mContext, this::onIphReviewClicked, this::onIphDismissClicked);
updateIphPropertyModel();
mTabListEditorCoordinator.addSpecialListItem(
0, UiType.MESSAGE, mIphMessagePropertyModel);
RecordUserAction.record("Tabs.ArchivedTabsDialogIphShown");
}
mTabArchiveSettings.addObserver(mTabArchiveSettingsObserver);
moveToState(TabActionState.CLOSABLE);
animateIn(ANIM_DURATION_MS);
}
private void animateIn(int duration) {
mDialogView.setVisibility(View.INVISIBLE);
mRootView.addView(mDialogView);
mDialogView.post(
() -> {
mDialogView.setTranslationX(mDialogView.getWidth());
mDialogView.setVisibility(View.VISIBLE);
// TODO(crbug.com/358430208): Use AnimatorSet here.
mDialogView
.animate()
.translationX(0f)
.setDuration(duration)
.setInterpolator(Interpolators.ACCELERATE_INTERPOLATOR)
.start();
mTabSwitcherView
.animate()
.translationX(-mTabSwitcherView.getWidth())
.setDuration(duration)
.setInterpolator(Interpolators.ACCELERATE_INTERPOLATOR)
.start();
RecordUserAction.record("Tabs.ArchivedTabsDialogShown");
});
}
private void animateOut(int duration, Runnable animationFinishCallback) {
mDialogRecyclerView.setBlockTouchInput(true);
// TODO(crbug.com/358430208): Use AnimatorSet here.
mDialogView
.animate()
.translationX(mDialogView.getWidth())
.setDuration(duration)
.setInterpolator(Interpolators.ACCELERATE_INTERPOLATOR)
.start();
mTabSwitcherView
.animate()
.translationX(0)
.setDuration(duration)
.setInterpolator(Interpolators.ACCELERATE_INTERPOLATOR)
.setListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(@NonNull Animator animator) {
animationFinishCallback.run();
mDialogRecyclerView.setBlockTouchInput(false);
}
})
.start();
}
/** Hides the dialog. */
public void hide(int animationDuration, Runnable animationFinishCallback) {
animateOut(
animationDuration,
() -> {
TabListEditorController controller = mTabListEditorCoordinator.getController();
controller.hide();
animationFinishCallback.run();
});
}
void hideInternal() {
TabListEditorController controller = mTabListEditorCoordinator.getController();
controller.setLifecycleObserver(null);
mBackPressManager.removeHandler(mTabListEditorCoordinator.getController());
mTabArchiveSettings.removeObserver(mTabArchiveSettingsObserver);
mArchivedTabModel.getTabCountSupplier().removeObserver(mTabCountObserver);
mSnackbarOverrideToken = TokenHolder.INVALID_TOKEN;
mIsShowing = false;
mTabSwitcherRecyclerView.get().setBlockTouchInput(false);
mTabSwitcherRecyclerView.clear();
}
void moveToState(@TabActionState int tabActionState) {
mTabActionState = tabActionState;
mTabListEditorCoordinator.getController().setTabActionState(mTabActionState);
updateTitle();
List<TabListEditorAction> actions = new ArrayList<>();
if (mTabActionState == TabActionState.CLOSABLE) {
actions.add(
TabListEditorRestoreAllArchivedTabsAction.createAction(
mContext, mArchiveDelegate));
actions.add(
TabListEditorSelectArchivedTabsAction.createAction(mContext, mArchiveDelegate));
actions.add(
TabListEditorArchiveSettingsAction.createAction(mContext, mArchiveDelegate));
} else if (mTabActionState == TabActionState.SELECTABLE) {
actions.add(
TabListEditorRestoreArchivedTabsAction.createAction(
mContext, mArchiveDelegate));
actions.add(
TabListEditorCloseArchivedTabsAction.createAction(mContext, mArchiveDelegate));
}
mTabListEditorCoordinator.getController().configureToolbarWithMenuItems(actions);
}
@VisibleForTesting
void updateTitle() {
int numInactiveTabs = mArchivedTabModel.getCount();
String title =
mContext.getResources()
.getQuantityString(
R.plurals.archived_tabs_dialog_title,
numInactiveTabs,
numInactiveTabs);
mTabListEditorCoordinator.getController().setToolbarTitle(title);
}
private void createTabListEditorCoordinator() {
mTabListEditorCoordinator =
new TabListEditorCoordinator(
mContext,
mRootView,
/* parentView= */ mDialogView.findViewById(R.id.tab_list_editor_container),
mBrowserControlsStateProvider,
mArchivedTabModelOrchestrator
.getTabModelSelector()
.getTabModelFilterProvider()
.getCurrentTabModelFilterSupplier(),
mTabContentManager,
/* clientTabListRecyclerViewPositionSetter= */ null,
mMode,
/* displayGroups= */ true,
mSnackbarManager,
/* bottomSheetController= */ null,
TabProperties.TabActionState.CLOSABLE,
mGridCardOnCLickListenerProvider,
mModalDialogManager);
}
@VisibleForTesting
void onCloseAllInactiveTabsButtonClicked(View view) {
int tabCount = mArchivedTabModel.getCount();
showCloseAllArchivedTabsConfirmation(
tabCount,
() -> {
RecordHistogram.recordCount1000Histogram(
"Tabs.CloseAllArchivedTabs.TabCount", tabCount);
RecordUserAction.record("Tabs.CloseAllArchivedTabsMenuItem");
});
}
/**
* Shows a confirmation dialog when the close operation cannot be undone.
*
* @param onConfirmRunnable A runnable which is run if the dialog is confirmed.
*/
private void showCloseAllArchivedTabsConfirmation(int tabCount, Runnable onConfirmRunnable) {
Function<Resources, String> titleResolver =
(res) -> {
return res.getQuantityString(
R.plurals.archive_dialog_close_all_inactive_tabs_confirmation_title,
tabCount,
tabCount);
};
Function<Resources, String> descriptionResolver =
(res) -> {
return res.getString(
R.string
.archive_dialog_close_all_inactive_tabs_confirmation_description);
};
mActionConfirmationDialog.show(
titleResolver,
descriptionResolver,
R.string.archive_dialog_close_all_inactive_tabs_confirmation,
/* supportStopShowing= */ false,
(isPositive, stopShowing) -> {
if (isPositive) {
mArchivedTabModel.closeTabs(
TabClosureParams.closeTabs(
TabModelUtils.convertTabListToListOfTabs(
mArchivedTabModel))
.allowUndo(false)
.build());
onConfirmRunnable.run();
}
});
}
private void restoreArchivedTabs(List<Tab> tabs) {
for (Tab tab : tabs) {
mArchivedTabModelOrchestrator
.getTabArchiver()
.unarchiveAndRestoreTab(mRegularTabCreator, tab);
}
}
private void onIphReviewClicked() {
SettingsLauncherFactory.createSettingsLauncher()
.launchSettingsActivity(mContext, TabArchiveSettingsFragment.class);
RecordUserAction.record("Tabs.ArchivedTabsDialogIphClicked");
}
private void onIphDismissClicked(@MessageType int messageType) {
mTabArchiveSettings.markDialogIphDismissed();
mTabListEditorCoordinator.removeSpecialListItem(
UiType.MESSAGE, MessageService.MessageType.ARCHIVED_TABS_IPH_MESSAGE);
RecordUserAction.record("Tabs.ArchivedTabsDialogIphDismissed");
}
private void updateIphPropertyModel() {
if (mIphMessagePropertyModel == null) return;
int archiveTimeDeltaDays = mTabArchiveSettings.getArchiveTimeDeltaDays();
int autoDeleteTimeDeletaDays = mTabArchiveSettings.getAutoDeleteTimeDeltaDays();
String settingsTitle =
mContext.getString(R.string.archived_tab_iph_card_subtitle_settings_title);
// The auto-delete section is blank when the feature param is disabled.
String autoDeleteTitle =
mTabArchiveSettings.isAutoDeleteEnabled()
? mContext.getString(
R.string.archived_tab_iph_card_subtitle_autodelete_section,
autoDeleteTimeDeletaDays)
: "";
String description =
mContext.getString(
R.string.archived_tab_iph_card_subtitle,
archiveTimeDeltaDays,
autoDeleteTitle,
settingsTitle);
SpannableString ss = new SpannableString(description);
ForegroundColorSpan fcs =
new ForegroundColorSpan(SemanticColorUtils.getDefaultTextColorAccent1(mContext));
ss.setSpan(
fcs,
description.indexOf(settingsTitle),
description.indexOf(settingsTitle) + settingsTitle.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mIphMessagePropertyModel.set(MessageCardViewProperties.DESCRIPTION_TEXT, ss);
}
private boolean shouldShowIph() {
return true;
}
// SnackbarManageable implementation.
@Override
public SnackbarManager getSnackbarManager() {
return mSnackbarManager;
}
// Testing-specific methods.
void setTabListEditorCoordinatorForTesting(TabListEditorCoordinator tabListEditorCoordinator) {
mTabListEditorCoordinator = tabListEditorCoordinator;
}
ArchiveDelegate getArchiveDelegateForTesting() {
return mArchiveDelegate;
}
TabListEditorCoordinator.LifecycleObserver getTabListEditorLifecycleObserver() {
return mTabListEditorLifecycleObserver;
}
View getViewForTesting() {
return mDialogView;
}
}