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

// 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 static org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsCardViewProperties.ARCHIVE_TIME_DELTA_DAYS;
import static org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsCardViewProperties.CLICK_HANDLER;
import static org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsCardViewProperties.NUMBER_OF_ARCHIVED_TABS;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.ViewParent;

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

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
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.tab.TabArchiveSettings;
import org.chromium.chrome.browser.tab_ui.OnTabSelectingListener;
import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tasks.tab_management.MessageCardViewProperties.MessageCardScope;
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.TabListModel.CardProperties.ModelType;
import org.chromium.chrome.browser.tasks.tab_management.TabSwitcherMessageManager.MessageUpdateObserver;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter;
import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter.HighlightParams;
import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter.HighlightShape;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

/** A message service to surface information about archived tabs. */
public class ArchivedTabsMessageService extends MessageService
        implements CustomMessageCardProvider, MessageUpdateObserver {

    static class ArchivedTabsMessageData implements MessageService.CustomMessageData {
        private CustomMessageCardProvider mProvider;

        public ArchivedTabsMessageData(CustomMessageCardProvider provider) {
            mProvider = provider;
        }

        @Override
        public CustomMessageCardProvider getProvider() {
            return mProvider;
        }
    }

    private final ArchivedTabModelOrchestrator.Observer mArchivedTabModelOrchestratorObserver =
            new ArchivedTabModelOrchestrator.Observer() {
                @Override
                public void onTabModelCreated(TabModel archivedTabModel) {
                    mArchivedTabModelOrchestrator.removeObserver(this);
                    mTabArchiveSettings = mArchivedTabModelOrchestrator.getTabArchiveSettings();
                    mTabArchiveSettings.addObserver(mTabArchiveSettingsObserver);
                    assert mTabArchiveSettings != null;

                    mArchivedTabModel = archivedTabModel;
                    mArchivedTabModel.getTabCountSupplier().addObserver(mTabCountObserver);

                    mCustomCardView =
                            LayoutInflater.from(mContext)
                                    .inflate(R.layout.archived_tabs_message_card_view, null);
                    if (mShowTwoStepIPH) {
                        mCustomCardView.addOnAttachStateChangeListener(
                                new OnAttachStateChangeListener() {
                                    @Override
                                    public void onViewAttachedToWindow(@NonNull View view) {
                                        HighlightParams params =
                                                new HighlightParams(HighlightShape.CIRCLE);
                                        params.setBoundsRespectPadding(false);
                                        ViewHighlighter.turnOnHighlight(mEndIconView, params);
                                        mCustomCardView.removeOnAttachStateChangeListener(this);
                                    }

                                    @Override
                                    public void onViewDetachedFromWindow(@NonNull View view) {}
                                });
                    }
                    mEndIconView = mCustomCardView.findViewById(R.id.end_image);
                    mCustomCardModel =
                            new PropertyModel.Builder(ArchivedTabsCardViewProperties.ALL_KEYS)
                                    .with(
                                            CLICK_HANDLER,
                                            ArchivedTabsMessageService.this::openArchivedTabsDialog)
                                    .build();
                    PropertyModelChangeProcessor.create(
                            mCustomCardModel, mCustomCardView, ArchivedTabsCardViewBinder::bind);
                }
            };

    private final Callback<Integer> mTabCountObserver =
            (count) -> {
                updateModelProperties();
                if (count > 0) {
                    maybeSendMessageToQueue();
                } else {
                    maybeInvalidatePreviouslySentMessage();
                }
            };

    /** When the settings change, the message subtitle may need to be updated. */
    private final TabArchiveSettings.Observer mTabArchiveSettingsObserver =
            new TabArchiveSettings.Observer() {
                @Override
                public void onSettingChanged() {
                    updateModelProperties();
                }
            };

    private final @NonNull Context mContext;
    private final @NonNull ArchivedTabModelOrchestrator mArchivedTabModelOrchestrator;
    private final @NonNull BrowserControlsStateProvider mBrowserControlsStateProvider;
    private final @NonNull TabContentManager mTabContentManager;
    private final @TabListMode int mTabListMode;
    private final @NonNull ViewGroup mRootView;
    private final @NonNull SnackbarManager mSnackbarManager;
    private final @NonNull TabCreator mRegularTabCreator;
    private final @NonNull BackPressManager mBackPressManager;
    private final @NonNull ModalDialogManager mModalDialogManager;
    private final @NonNull Tracker mTracker;
    private final @NonNull Runnable mAppendMessageRunnable;
    private final @NonNull Supplier<TabListCoordinator> mTabListCoordinatorSupplier;

    private TabArchiveSettings mTabArchiveSettings;
    private ArchivedTabsDialogCoordinator mArchivedTabsDialogCoordinator;
    private TabModel mArchivedTabModel;
    private View mCustomCardView;
    private View mEndIconView;
    private PropertyModel mCustomCardModel;
    private boolean mMessageSentToQueue;
    private OnTabSelectingListener mOnTabSelectingListener;
    private boolean mShowTwoStepIPH;

    ArchivedTabsMessageService(
            @NonNull Context context,
            @NonNull ArchivedTabModelOrchestrator archivedTabModelOrchestrator,
            @NonNull BrowserControlsStateProvider browserControlStateProvider,
            @NonNull TabContentManager tabContentManager,
            @TabListMode int tabListMode,
            @NonNull ViewGroup rootView,
            @NonNull SnackbarManager snackbarManager,
            @NonNull TabCreator regularTabCreator,
            @NonNull BackPressManager backPressManager,
            @NonNull ModalDialogManager modalDialogManager,
            @NonNull Tracker tracker,
            @NonNull Runnable appendMessageRunnable,
            @NonNull Supplier<TabListCoordinator> tabListCoordinatorSupplier) {
        super(MessageType.ARCHIVED_TABS_MESSAGE);
        mContext = context;
        mArchivedTabModelOrchestrator = archivedTabModelOrchestrator;
        mBrowserControlsStateProvider = browserControlStateProvider;
        mTabContentManager = tabContentManager;
        mTabListMode = tabListMode;
        mRootView = rootView;
        mSnackbarManager = snackbarManager;
        mRegularTabCreator = regularTabCreator;
        mBackPressManager = backPressManager;
        mModalDialogManager = modalDialogManager;
        mTracker = tracker;
        mAppendMessageRunnable = appendMessageRunnable;
        mTabListCoordinatorSupplier = tabListCoordinatorSupplier;
        // Capture this value immediately before it expires when the IPH is dismissed, which will
        // happen regardless of user behavior. The TabArchiveSettings tracks whether the main IPH
        // was followed. When that's true, the archived tabs message should be highlighted as part
        // of the 2-step IPH.
        mShowTwoStepIPH = TabArchiveSettings.getIphShownThisSession();

        if (mArchivedTabModelOrchestrator.isTabModelInitialized()) {
            mArchivedTabModelOrchestratorObserver.onTabModelCreated(
                    mArchivedTabModelOrchestrator
                            .getTabModelSelector()
                            .getModel(/* incognito= */ false));
        } else {
            mArchivedTabModelOrchestrator.addObserver(mArchivedTabModelOrchestratorObserver);
        }
    }

    public void destroy() {
        if (mTabArchiveSettings != null) {
            mTabArchiveSettings.removeObserver(mTabArchiveSettingsObserver);
        }

        if (mArchivedTabsDialogCoordinator != null) {
            mArchivedTabsDialogCoordinator.destroy();
        }
    }

    // CustomMessageCardViewProvider implementation.

    @Override
    public int getMessageType() {
        return MessageService.MessageType.ARCHIVED_TABS_MESSAGE;
    }

    @Override
    public View getCustomView() {
        return mCustomCardView;
    }

    @Override
    public @MessageCardScope int getMessageCardVisibilityControl() {
        return MessageCardViewProperties.MessageCardScope.REGULAR;
    }

    @Override
    public @ModelType int getCardType() {
        return TabListModel.CardProperties.ModelType.MESSAGE;
    }

    @Override
    public void setIsIncognito(boolean isIncognito) {
        // No-op
    }

    // MessageUpdateObserver implementation.

    @Override
    public void onAppendedMessage() {
        // When the two-step IPH is active, highlight the end icon.
        if (mShowTwoStepIPH) {
            mShowTwoStepIPH = false;
            // Reset this manually, in case the IPH wasn't dismissed for some reason.
            TabArchiveSettings.setIphShownThisSession(false);
            // Scrolling the recycler view only works when posted.
            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    () -> {
                        if (!mTabListCoordinatorSupplier.hasValue()) return;
                        TabListCoordinator tabListCoordiantor = mTabListCoordinatorSupplier.get();
                        if (tabListCoordiantor.specialItemExists(
                                MessageType.ARCHIVED_TABS_MESSAGE)) {
                            mTabListCoordinatorSupplier
                                    .get()
                                    .setRecyclerViewPosition(new RecyclerViewPosition(0, 0));
                        }
                    });
        }
    }

    @Override
    public void onRemoveAllAppendedMessage() {
        if (mCustomCardView == null) return;
        // When messages are removed, detach the custom view.
        ViewParent parent = mCustomCardView.getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(mCustomCardView);
        }
    }

    @Override
    public void addObserver(MessageService.MessageObserver obs) {
        super.addObserver(obs);
        maybeSendMessageToQueue();
    }

    public void setOnTabSelectingListener(OnTabSelectingListener onTabSelectingListener) {
        mOnTabSelectingListener = onTabSelectingListener;
    }

    // Private methods.

    @VisibleForTesting
    void maybeSendMessageToQueue() {
        if (mMessageSentToQueue) return;
        if (mArchivedTabModel == null) return;
        if (mArchivedTabModel.getCount() <= 0) return;
        updateModelProperties();
        sendAvailabilityNotification(new ArchivedTabsMessageData(this));
        mMessageSentToQueue = true;
        mAppendMessageRunnable.run();
    }

    @VisibleForTesting
    void maybeInvalidatePreviouslySentMessage() {
        if (!mMessageSentToQueue) return;
        sendInvalidNotification();
        mMessageSentToQueue = false;
    }

    private void openArchivedTabsDialog() {
        if (mArchivedTabsDialogCoordinator == null) {
            createArchivedTabsDialogCoordinator();
        }
        mTracker.notifyEvent("android_tab_declutter_button_clicked");
        mArchivedTabsDialogCoordinator.show(mOnTabSelectingListener);
        ViewHighlighter.turnOffHighlight(mEndIconView);
    }

    private void createArchivedTabsDialogCoordinator() {
        mArchivedTabsDialogCoordinator =
                new ArchivedTabsDialogCoordinator(
                        mContext,
                        mArchivedTabModelOrchestrator,
                        mBrowserControlsStateProvider,
                        mTabContentManager,
                        mTabListMode,
                        mRootView,
                        mRootView.findViewById(R.id.tab_switcher_view_holder),
                        mSnackbarManager,
                        mRegularTabCreator,
                        mBackPressManager,
                        mTabArchiveSettings,
                        mModalDialogManager);
    }

    private void updateModelProperties() {
        mCustomCardModel.set(NUMBER_OF_ARCHIVED_TABS, mArchivedTabModel.getCount());
        mCustomCardModel.set(
                ARCHIVE_TIME_DELTA_DAYS, mTabArchiveSettings.getArchiveTimeDeltaDays());
    }

    // Testing methods.

    PropertyModel getCustomCardModelForTesting() {
        return mCustomCardModel;
    }

    ArchivedTabModelOrchestrator.Observer getArchivedTabModelOrchestratorObserverForTesting() {
        return mArchivedTabModelOrchestratorObserver;
    }

    void setArchivedTabsDialogCoordiantorForTesting(
            ArchivedTabsDialogCoordinator archivedTabsDialogCoordinator) {
        mArchivedTabsDialogCoordinator = archivedTabsDialogCoordinator;
    }

    Callback<Integer> getTabCountObserverForTesting() {
        return mTabCountObserver;
    }
}