chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherMessageManager.java

// Copyright 2023 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.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.ValueChangedCallback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
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.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthManager;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.price_tracking.PriceDropNotificationManager;
import org.chromium.chrome.browser.price_tracking.PriceDropNotificationManagerFactory;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.price_tracking.PriceTrackingUtilities;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_ui.OnTabSelectingListener;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tab_ui.TabGridIphDialogCoordinator;
import org.chromium.chrome.browser.tab_ui.TabSwitcher;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tasks.tab_management.MessageService.MessageType;
import org.chromium.chrome.browser.tasks.tab_management.PriceMessageService.PriceMessageType;
import org.chromium.chrome.browser.tasks.tab_management.PriceMessageService.PriceWelcomeMessageProvider;
import org.chromium.chrome.browser.tasks.tab_management.PriceMessageService.PriceWelcomeMessageReviewActionProvider;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.LayoutViewBuilder;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.List;

/** Manages message related glue for the {@link TabSwitcher}. */
public class TabSwitcherMessageManager implements PriceWelcomeMessageController {
    /** Used to observe updates to message cards. */
    public interface MessageUpdateObserver {
        /** Invoked when messages are added. */
        default void onAppendedMessage() {}

        /** Invoked when a message is removed. */
        default void onRemovedMessage() {}

        /** Invoked when messages are removed. */
        default void onRemoveAllAppendedMessage() {}

        /** Invoked when messages are restored. */
        default void onRestoreAllAppendedMessage() {}

        /** Invoked when price message is shown. */
        default void onShowPriceWelcomeMessage() {}

        /** Invoked when price message is removed. */
        default void onRemovePriceWelcomeMessage() {}

        /** Invoked when price message is restored. */
        default void onRestorePriceWelcomeMessage() {}
    }

    private final MultiWindowModeStateDispatcher.MultiWindowModeObserver mMultiWindowModeObserver =
            isInMultiWindowMode -> {
                if (isInMultiWindowMode) {
                    removeAllAppendedMessage();
                } else {
                    restoreAllAppendedMessage();
                }
            };

    private final TabModelObserver mTabModelObserver =
            new TabModelObserver() {
                @Override
                public void willCloseTab(Tab tab, boolean didCloseAlone) {
                    if (mCurrentTabModelFilterSupplier.get().getTabModel().getCount() == 1) {
                        removeAllAppendedMessagePostAnimation();
                    } else if (mPriceMessageService != null
                            && mPriceMessageService.getBindingTabId() == tab.getId()) {
                        removePriceWelcomeMessage();
                    }
                }

                @Override
                public void tabClosureUndone(Tab tab) {
                    if (mCurrentTabModelFilterSupplier.get().getTabModel().getCount() == 1) {
                        restoreAllAppendedMessage();
                    }
                    if (mPriceMessageService != null
                            && mPriceMessageService.getBindingTabId() == tab.getId()) {
                        restorePriceWelcomeMessage();
                    }
                }

                @Override
                public void tabClosureCommitted(Tab tab) {
                    // TODO(crbug.com/40160889): Auto update the PriceMessageService instead of
                    // updating it based on the client caller.
                    if (mPriceMessageService != null
                            && mPriceMessageService.getBindingTabId() == tab.getId()) {
                        mPriceMessageService.invalidateMessage();
                    }
                }
            };

    private static boolean sAppendedMessagesForTesting;

    private final @NonNull ObserverList<MessageUpdateObserver> mObservers = new ObserverList<>();
    private final @NonNull Context mContext;
    private final @NonNull ActivityLifecycleDispatcher mLifecylceDispatcher;
    private final @NonNull ObservableSupplier<TabModelFilter> mCurrentTabModelFilterSupplier;
    private final @NonNull TabGridIphDialogCoordinator mTabGridIphDialogCoordinator;
    private final @NonNull MultiWindowModeStateDispatcher mMultiWindowModeStateDispatcher;
    private final @NonNull SnackbarManager mSnackbarManager;
    private final @NonNull ModalDialogManager mModalDialogManager;
    private final @NonNull MessageCardProviderCoordinator mMessageCardProviderCoordinator;
    private final @NonNull ValueChangedCallback<TabModelFilter> mOnTabModelFilterChanged =
            new ValueChangedCallback<>(this::onTabModelFilterChanged);
    private final @NonNull ObservableSupplierImpl<PriceWelcomeMessageReviewActionProvider>
            mPriceWelcomeMessageReviewActionProviderSupplier = new ObservableSupplierImpl<>();
    private final @NonNull ObservableSupplierImpl<TabListCoordinator> mTabListCoordinatorSupplier =
            new ObservableSupplierImpl<>();
    private final @NonNull BrowserControlsStateProvider mBrowserControlsStateProvider;
    private final @NonNull TabContentManager mTabContentManager;
    private final @TabListMode int mTabListMode;
    private final @NonNull ViewGroup mRootView;
    private final @NonNull TabCreator mRegularTabCreator;
    private final @NonNull BackPressManager mBackPressManager;

    private @Nullable Profile mProfile;
    private @Nullable PriceMessageService mPriceMessageService;
    private @Nullable IncognitoReauthPromoMessageService mIncognitoReauthPromoMessageService;
    private @Nullable ArchivedTabsMessageService mArchivedTabsMessageService;

    /**
     * @param context The Android activity context.
     * @param lifecycleDispatcher The {@link ActivityLifecycleDispatcher} for the activity.
     * @param currentTabModelFilterSupplier The supplier of the current {@link TabModelFilter}.
     * @param multiWindowModeStateDispatcher The {@link MultiWindowModeStateDispatcher} to observe
     *     for multi-window related changes.
     * @param snackbarManager The {@link SnackbarManager} for the activity.
     * @param modalDialogManager The {@link ModalDialogManager} for the activity.
     * @param browserControlStateProvider Provides the state of browser controls.
     * @param tabContentManager Serves tab content to UI components.
     * @param tabListMode The {@link TabListMode} determining how the TabList will be displayed.
     * @param rootView The root {@link ViewGroup} to attach dialogs to.
     * @param regularTabCreator Manages the creation of regular tabs.
     * @param backPressManager Manages the different back press handlers in the app.
     */
    public TabSwitcherMessageManager(
            @NonNull Context context,
            @NonNull ActivityLifecycleDispatcher lifecycleDispatcher,
            @NonNull ObservableSupplier<TabModelFilter> currentTabModelFilterSupplier,
            @NonNull MultiWindowModeStateDispatcher multiWindowModeStateDispatcher,
            @NonNull SnackbarManager snackbarManager,
            @NonNull ModalDialogManager modalDialogManager,
            @NonNull BrowserControlsStateProvider browserControlStateProvider,
            @NonNull TabContentManager tabContentManager,
            @TabListMode int tabListMode,
            @NonNull ViewGroup rootView,
            @NonNull TabCreator regularTabCreator,
            @NonNull BackPressManager backPressManager) {
        mContext = context;
        mLifecylceDispatcher = lifecycleDispatcher;
        mCurrentTabModelFilterSupplier = currentTabModelFilterSupplier;
        mMultiWindowModeStateDispatcher = multiWindowModeStateDispatcher;
        mSnackbarManager = snackbarManager;
        mModalDialogManager = modalDialogManager;
        mBrowserControlsStateProvider = browserControlStateProvider;
        mTabContentManager = tabContentManager;
        mTabListMode = tabListMode;
        mRootView = rootView;
        mRegularTabCreator = regularTabCreator;
        mBackPressManager = backPressManager;

        mMessageCardProviderCoordinator =
                new MessageCardProviderCoordinator(
                        context,
                        () -> currentTabModelFilterSupplier.get().getTabModel().getProfile(),
                        this::dismissHandler);

        mTabGridIphDialogCoordinator =
                new TabGridIphDialogCoordinator(mContext, mModalDialogManager);

        mMultiWindowModeStateDispatcher.addObserver(mMultiWindowModeObserver);
        mOnTabModelFilterChanged.onResult(
                currentTabModelFilterSupplier.addObserver(mOnTabModelFilterChanged));
    }

    /**
     * Bind the message manager to emit messages on a specific {@link TabListCoordinator}. If
     * already bound to a coordinator messages are removed from the previous coordinator.
     *
     * @param tabListCoordinator The {@link TabListCoordinator} to show messages on.
     * @param container The {@link ViewGroup} of the container view.
     * @param priceWelcomeMessageReviewActionProvider The review action provider for price welcome.
     * @param onTabSelectingListener The {@link OnTabSelectingListener} for the parent tab switcher.
     */
    public void bind(
            @NonNull TabListCoordinator tabListCoordinator,
            @NonNull ViewGroup container,
            @NonNull
                    PriceWelcomeMessageReviewActionProvider priceWelcomeMessageReviewActionProvider,
            @NonNull OnTabSelectingListener onTabSelectingListener) {
        TabListCoordinator oldTabListCoordinator = mTabListCoordinatorSupplier.get();
        if (oldTabListCoordinator != null) {
            if (oldTabListCoordinator != tabListCoordinator) {
                unbind(oldTabListCoordinator);
            }
        }
        mTabListCoordinatorSupplier.set(tabListCoordinator);
        mPriceWelcomeMessageReviewActionProviderSupplier.set(
                priceWelcomeMessageReviewActionProvider);

        mTabGridIphDialogCoordinator.setParentView(container);
        if (mArchivedTabsMessageService != null) {
            mArchivedTabsMessageService.setOnTabSelectingListener(onTabSelectingListener);
        }

        // Don't add any messages. Wait for the next time tabs are loaded into the coordinator.
    }

    /**
     * Unbinds a {@link TabListCoordinator} and related objects from receiving updates.
     *
     * @param tabListCoordinator The {@link TabListCoordinator} to remove messages from.
     */
    public void unbind(TabListCoordinator tabListCoordinator) {
        TabListCoordinator currentTabListCoordinator = mTabListCoordinatorSupplier.get();
        if (currentTabListCoordinator != tabListCoordinator) return;

        removeAllAppendedMessage();

        mTabListCoordinatorSupplier.set(null);
        mPriceWelcomeMessageReviewActionProviderSupplier.set(null);

        mTabGridIphDialogCoordinator.setParentView(null);
    }

    /**
     * Register messages for a particular {@link TabListCoordinator}. Should only be called once per
     * coordinator.
     */
    public void registerMessages(@NonNull TabListCoordinator tabListCoordinator) {
        tabListCoordinator.registerItemType(
                TabProperties.UiType.MESSAGE,
                new LayoutViewBuilder(R.layout.tab_grid_message_card_item),
                MessageCardViewBinder::bind);

        tabListCoordinator.registerItemType(
                TabProperties.UiType.LARGE_MESSAGE,
                new LayoutViewBuilder(R.layout.large_message_card_item),
                LargeMessageCardViewBinder::bind);

        tabListCoordinator.registerItemType(
                TabProperties.UiType.CUSTOM_MESSAGE,
                new LayoutViewBuilder(R.layout.custom_message_card_item),
                CustomMessageCardViewBinder::bind);
    }

    /** Post-native initialization. */
    public void initWithNative(@NonNull Profile profile, @TabListMode int mode) {
        assert profile != null;
        mProfile = profile;

        if (ChromeFeatureList.sAndroidTabDeclutter.isEnabled()) {
            mArchivedTabsMessageService =
                    new ArchivedTabsMessageService(
                            mContext,
                            ArchivedTabModelOrchestrator.getForProfile(mProfile),
                            mBrowserControlsStateProvider,
                            mTabContentManager,
                            mTabListMode,
                            mRootView,
                            mSnackbarManager,
                            mRegularTabCreator,
                            mBackPressManager,
                            mModalDialogManager,
                            TrackerFactory.getTrackerForProfile(profile),
                            () ->
                                    appendNextMessage(
                                            MessageService.MessageType.ARCHIVED_TABS_MESSAGE),
                            mTabListCoordinatorSupplier);
            addObserver(mArchivedTabsMessageService);
            mMessageCardProviderCoordinator.subscribeMessageService(mArchivedTabsMessageService);
        }

        IphMessageService iphMessageService =
                new IphMessageService(profile, mTabGridIphDialogCoordinator);
        mMessageCardProviderCoordinator.subscribeMessageService(iphMessageService);

        if (IncognitoReauthManager.isIncognitoReauthFeatureAvailable()
                && mIncognitoReauthPromoMessageService == null) {
            IncognitoReauthManager incognitoReauthManager =
                    new IncognitoReauthManager(ContextUtils.activityFromContext(mContext), profile);
            mIncognitoReauthPromoMessageService =
                    new IncognitoReauthPromoMessageService(
                            MessageService.MessageType.INCOGNITO_REAUTH_PROMO_MESSAGE,
                            profile,
                            mContext,
                            ChromeSharedPreferences.getInstance(),
                            incognitoReauthManager,
                            mSnackbarManager,
                            mLifecylceDispatcher);
            mMessageCardProviderCoordinator.subscribeMessageService(
                    mIncognitoReauthPromoMessageService);
        }
        setUpPriceTracking();
    }

    /**
     * @param observer The {@link MessageUpdateObserver} to notify.
     */
    public void addObserver(MessageUpdateObserver observer) {
        mObservers.addObserver(observer);
    }

    /**
     * @param observer The {@link MessageUpdateObserver} to remove.
     */
    public void removeObserver(MessageUpdateObserver observer) {
        mObservers.removeObserver(observer);
    }

    /** Called before resetting the list of tabs. */
    public void beforeReset() {
        // Unregister and re-register the TabModelObserver along with
        // TabListCoordinator#resetWithListOfTabs to ensure that the TabList gets observer
        // calls before the TabSwitcherMessageManager.
        removeTabModelFilterObservers(mCurrentTabModelFilterSupplier.get());
        // Invalidate price welcome message for every reset so that the stale message won't be
        // restored by mistake (e.g. from tabClosureUndone in TabSwitcherMediator).
        if (mPriceMessageService != null) {
            mPriceMessageService.invalidateMessage();
        }
    }

    /** Called after resetting the list of tabs. */
    public void afterReset(int tabCount) {
        onTabModelFilterChanged(mCurrentTabModelFilterSupplier.get(), null);
        removeAllAppendedMessage();
        if (tabCount > 0) {
            if (mPriceMessageService != null
                    && PriceTrackingUtilities.isPriceAlertsMessageCardEnabled(mProfile)) {
                mPriceMessageService.preparePriceMessage(PriceMessageType.PRICE_ALERTS, null);
            }
            appendMessagesTo(tabCount);
        }
    }

    /** Destroys the module and unregisters observers. */
    public void destroy() {
        mMultiWindowModeStateDispatcher.removeObserver(mMultiWindowModeObserver);
        removeTabModelFilterObservers(mCurrentTabModelFilterSupplier.get());
        mCurrentTabModelFilterSupplier.removeObserver(mOnTabModelFilterChanged);

        mMessageCardProviderCoordinator.destroy();
        mTabGridIphDialogCoordinator.destroy();
        if (mIncognitoReauthPromoMessageService != null) {
            mIncognitoReauthPromoMessageService.destroy();
        }
        if (mArchivedTabsMessageService != null) {
            mArchivedTabsMessageService.destroy();
        }
    }

    @Override
    public void showPriceWelcomeMessage(PriceMessageService.PriceTabData priceTabData) {
        assert mPriceWelcomeMessageReviewActionProviderSupplier.get() != null;

        if (mPriceMessageService == null
                || !PriceTrackingUtilities.isPriceWelcomeMessageCardEnabled(mProfile)
                || mMessageCardProviderCoordinator.isMessageShown(
                        MessageService.MessageType.PRICE_MESSAGE, PriceMessageType.PRICE_WELCOME)) {
            return;
        }
        if (mPriceMessageService.preparePriceMessage(
                PriceMessageType.PRICE_WELCOME, priceTabData)) {
            appendNextMessage(MessageService.MessageType.PRICE_MESSAGE);
            // To make the message card in view when user enters tab switcher, we should scroll to
            // current tab with 0 offset. See {@link
            // TabSwitcherMediator#setInitialScrollIndexOffset} for more details.
            mPriceWelcomeMessageReviewActionProviderSupplier
                    .get()
                    .scrollToTab(mCurrentTabModelFilterSupplier.get().index());
        }
        for (MessageUpdateObserver observer : mObservers) {
            observer.onShowPriceWelcomeMessage();
        }
    }

    @Override
    public void removePriceWelcomeMessage() {
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        if (tabListCoordinator != null) {
            tabListCoordinator.removeSpecialListItem(
                    TabProperties.UiType.LARGE_MESSAGE, MessageService.MessageType.PRICE_MESSAGE);
        }

        for (MessageUpdateObserver observer : mObservers) {
            observer.onRemovePriceWelcomeMessage();
        }
    }

    @Override
    public void restorePriceWelcomeMessage() {
        appendNextMessage(MessageService.MessageType.PRICE_MESSAGE);
        for (MessageUpdateObserver observer : mObservers) {
            observer.onRestorePriceWelcomeMessage();
        }
    }

    private void appendNextMessage(@MessageService.MessageType int messageType) {
        assert mMessageCardProviderCoordinator != null;
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        if (tabListCoordinator == null) return;

        MessageCardProviderMediator.Message nextMessage =
                mMessageCardProviderCoordinator.getNextMessageItemForType(messageType);
        if (nextMessage == null || !shouldAppendMessage(nextMessage)) return;
        if (messageType == MessageService.MessageType.PRICE_MESSAGE) {
            tabListCoordinator.addSpecialListItem(
                    tabListCoordinator.getPriceWelcomeMessageInsertionIndex(),
                    TabProperties.UiType.LARGE_MESSAGE,
                    nextMessage.model);
        } else if (messageType == MessageService.MessageType.ARCHIVED_TABS_MESSAGE) {
            tabListCoordinator.addSpecialListItem(
                    0, TabProperties.UiType.CUSTOM_MESSAGE, nextMessage.model);
        } else {
            tabListCoordinator.addSpecialListItemToEnd(
                    TabProperties.UiType.MESSAGE, nextMessage.model);
        }
        for (MessageUpdateObserver observer : mObservers) {
            observer.onAppendedMessage();
        }
    }

    private void appendMessagesTo(int index) {
        if (!shouldShowMessages()) return;

        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        assert tabListCoordinator != null;

        sAppendedMessagesForTesting = false;
        List<MessageCardProviderMediator.Message> messages =
                mMessageCardProviderCoordinator.getMessageItems();
        for (int i = 0; i < messages.size(); i++) {
            if (!shouldAppendMessage(messages.get(i))) continue;
            if (messages.get(i).type == MessageService.MessageType.PRICE_MESSAGE) {
                tabListCoordinator.addSpecialListItem(
                        index, TabProperties.UiType.LARGE_MESSAGE, messages.get(i).model);
            } else if (messages.get(i).type
                    == MessageService.MessageType.INCOGNITO_REAUTH_PROMO_MESSAGE) {
                if (!mayAddIncognitoReauthPromoCard(messages.get(i).model)) {
                    // Skip incrementing index if the message was not added.
                    continue;
                }
            } else if (messages.get(i).type == MessageService.MessageType.ARCHIVED_TABS_MESSAGE) {
                // Always add the archived tabs message to the start.
                tabListCoordinator.addSpecialListItem(
                        0, TabProperties.UiType.CUSTOM_MESSAGE, messages.get(i).model);
            } else {
                tabListCoordinator.addSpecialListItem(
                        index, TabProperties.UiType.MESSAGE, messages.get(i).model);
            }
            index++;
        }
        if (messages.size() > 0) sAppendedMessagesForTesting = true;
        for (MessageUpdateObserver observer : mObservers) {
            observer.onAppendedMessage();
        }
    }

    private boolean mayAddIncognitoReauthPromoCard(PropertyModel model) {
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        assert tabListCoordinator != null;
        if (mIncognitoReauthPromoMessageService.isIncognitoReauthPromoMessageEnabled(mProfile)) {
            tabListCoordinator.addSpecialListItemToEnd(TabProperties.UiType.LARGE_MESSAGE, model);
            mIncognitoReauthPromoMessageService.increasePromoShowCountAndMayDisableIfCountExceeds();
            return true;
        }
        return false;
    }

    private boolean shouldAppendMessage(MessageCardProviderMediator.Message message) {
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        assert tabListCoordinator != null;
        if (tabListCoordinator.specialItemExists(message.type)) return false;
        PropertyModel messageModel = message.model;

        Integer messageCardVisibilityControlValue =
                messageModel.get(
                        MessageCardViewProperties
                                .MESSAGE_CARD_VISIBILITY_CONTROL_IN_REGULAR_AND_INCOGNITO_MODE);

        @MessageCardViewProperties.MessageCardScope
        int scope =
                (messageCardVisibilityControlValue != null)
                        ? messageCardVisibilityControlValue
                        : MessageCardViewProperties.MessageCardScope.REGULAR;

        if (scope == MessageCardViewProperties.MessageCardScope.BOTH) return true;
        return mCurrentTabModelFilterSupplier.get().isIncognito()
                ? scope == MessageCardViewProperties.MessageCardScope.INCOGNITO
                : scope == MessageCardViewProperties.MessageCardScope.REGULAR;
    }

    /**
     * Remove all the message items in the model list. Right now this is used when all tabs are
     * closed in the grid tab switcher.
     */
    private void removeAllAppendedMessage() {
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        if (tabListCoordinator == null) return;

        tabListCoordinator.removeSpecialListItem(
                TabProperties.UiType.MESSAGE, MessageService.MessageType.ALL);
        tabListCoordinator.removeSpecialListItem(
                TabProperties.UiType.LARGE_MESSAGE,
                MessageService.MessageType.INCOGNITO_REAUTH_PROMO_MESSAGE);
        tabListCoordinator.removeSpecialListItem(
                TabProperties.UiType.CUSTOM_MESSAGE,
                MessageService.MessageType.ARCHIVED_TABS_MESSAGE);

        sAppendedMessagesForTesting = false;
        for (MessageUpdateObserver observer : mObservers) {
            observer.onRemoveAllAppendedMessage();
        }
    }

    private void removeAllAppendedMessagePostAnimation() {
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        if (tabListCoordinator == null) return;

        tabListCoordinator.runOnItemAnimatorFinished(
                () -> {
                    if (mTabListCoordinatorSupplier.get() != tabListCoordinator) {
                        return;
                    }
                    removeAllAppendedMessage();
                });
    }

    /**
     * Restore all the message items that should show. Right now this is only used to restore
     * message items when the closure of the last tab in tab switcher is undone.
     */
    private void restoreAllAppendedMessage() {
        if (!shouldShowMessages()) return;
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        assert tabListCoordinator != null;

        assert mCurrentTabModelFilterSupplier.get().getTabModel().getProfile() != null;

        sAppendedMessagesForTesting = false;
        List<MessageCardProviderMediator.Message> messages =
                mMessageCardProviderCoordinator.getMessageItems();
        for (int i = 0; i < messages.size(); i++) {
            if (!shouldAppendMessage(messages.get(i))) continue;
            // The restore of PRICE_MESSAGE is handled in the restorePriceWelcomeMessage() below.
            if (messages.get(i).type == MessageService.MessageType.PRICE_MESSAGE) {
                continue;
            } else if (messages.get(i).type
                    == MessageService.MessageType.INCOGNITO_REAUTH_PROMO_MESSAGE) {
                tabListCoordinator.addSpecialListItemToEnd(
                        TabProperties.UiType.LARGE_MESSAGE, messages.get(i).model);
            } else if (messages.get(i).type == MessageService.MessageType.ARCHIVED_TABS_MESSAGE) {
                tabListCoordinator.addSpecialListItem(
                        0, TabProperties.UiType.CUSTOM_MESSAGE, messages.get(i).model);
            } else {
                tabListCoordinator.addSpecialListItemToEnd(
                        TabProperties.UiType.MESSAGE, messages.get(i).model);
            }
        }
        sAppendedMessagesForTesting = messages.size() > 0;
        for (MessageUpdateObserver observer : mObservers) {
            observer.onRestoreAllAppendedMessage();
        }
    }

    private void setUpPriceTracking() {
        assert mProfile != null;
        if (PriceTrackingFeatures.isPriceTrackingEnabled(mProfile)) {
            PriceDropNotificationManager notificationManager =
                    PriceDropNotificationManagerFactory.create(mProfile);
            if (mPriceMessageService == null) {
                mPriceMessageService =
                        new PriceMessageService(
                                mProfile,
                                (Supplier<PriceWelcomeMessageProvider>)
                                        ((Supplier<? extends PriceWelcomeMessageProvider>)
                                                mTabListCoordinatorSupplier),
                                mPriceWelcomeMessageReviewActionProviderSupplier,
                                notificationManager);
            }
            mMessageCardProviderCoordinator.subscribeMessageService(mPriceMessageService);
        }
    }

    @VisibleForTesting
    void dismissHandler(@MessageType int messageType) {
        // `bind` and `unbind` to attach to a `TabListCoordinator` are independent of whether the
        // `MessageCardProviderCoordinator`, has access to the `dismissHandler`. That means this
        // may be called while unbound by one of the message services.
        TabListCoordinator tabListCoordinator = mTabListCoordinatorSupplier.get();
        if (tabListCoordinator == null) return;

        if (messageType == MessageService.MessageType.PRICE_MESSAGE
                || messageType == MessageService.MessageType.INCOGNITO_REAUTH_PROMO_MESSAGE) {
            tabListCoordinator.removeSpecialListItem(
                    TabProperties.UiType.LARGE_MESSAGE, messageType);
        } else if (messageType == MessageService.MessageType.ARCHIVED_TABS_MESSAGE) {
            tabListCoordinator.removeSpecialListItem(
                    TabProperties.UiType.CUSTOM_MESSAGE, messageType);
        } else {
            tabListCoordinator.removeSpecialListItem(TabProperties.UiType.MESSAGE, messageType);
            appendNextMessage(messageType);
        }
        for (MessageUpdateObserver observer : mObservers) {
            observer.onRemovedMessage();
        }
    }

    private void onTabModelFilterChanged(
            @Nullable TabModelFilter newFilter, @Nullable TabModelFilter oldFilter) {
        removeTabModelFilterObservers(oldFilter);

        if (newFilter != null) {
            newFilter.addObserver(mTabModelObserver);
        }
    }

    private void removeTabModelFilterObservers(@Nullable TabModelFilter filter) {
        if (filter != null) {
            filter.removeObserver(mTabModelObserver);
        }
    }

    private boolean shouldShowMessages() {
        return !mMultiWindowModeStateDispatcher.isInMultiWindowMode()
                && mTabListCoordinatorSupplier.get() != null;
    }

    /** Returns whether this manager has appended any messages. */
    public static boolean hasAppendedMessagesForTesting() {
        return sAppendedMessagesForTesting;
    }

    /** Resets whether this manager has appended any messages. */
    public static void resetHasAppendedMessagesForTesting() {
        sAppendedMessagesForTesting = false;
    }

    boolean isNativeInitializedForTesting() {
        return mProfile != null;
    }

    void setPriceMessageServiceForTesting(@NonNull PriceMessageService priceMessageService) {
        assert mPriceMessageService == null
                : "setPriceMessageServiceForTesting() must be before initWithNative().";
        mPriceMessageService = priceMessageService;
    }
}