chromium/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java

// Copyright 2018 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.feed;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;

import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.ObserverList;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.TimeUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.jank_tracker.JankScenario;
import org.chromium.base.jank_tracker.JankTracker;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.feed.componentinterfaces.SurfaceCoordinator;
import org.chromium.chrome.browser.feed.sections.SectionHeaderListProperties;
import org.chromium.chrome.browser.feed.sections.SectionHeaderView;
import org.chromium.chrome.browser.feed.sections.SectionHeaderViewBinder;
import org.chromium.chrome.browser.feed.sort_ui.FeedOptionsCoordinator;
import org.chromium.chrome.browser.feed.webfeed.WebFeedBridge;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.ntp.NewTabPageLaunchOrigin;
import org.chromium.chrome.browser.ntp.NewTabPageLayout;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.toolbar.top.Toolbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.native_page.TouchEnabledDelegate;
import org.chromium.chrome.browser.ui.signin.PersonalizedSigninPromoView;
import org.chromium.chrome.browser.user_education.UserEducationHelper;
import org.chromium.chrome.browser.xsurface.HybridListRenderer;
import org.chromium.chrome.browser.xsurface.ProcessScope;
import org.chromium.chrome.browser.xsurface.feed.FeedCardOpeningReliabilityLogger;
import org.chromium.chrome.browser.xsurface.feed.FeedLaunchReliabilityLogger;
import org.chromium.chrome.browser.xsurface.feed.FeedLaunchReliabilityLogger.SurfaceType;
import org.chromium.chrome.browser.xsurface.feed.FeedSurfaceScope;
import org.chromium.chrome.browser.xsurface.feed.FeedUserInteractionReliabilityLogger;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.third_party.android.swiperefresh.SwipeRefreshLayout;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.ListModelChangeProcessor;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.ArrayList;
import java.util.List;

/** Provides a surface that displays an interest feed rendered list of content suggestions. */
public class FeedSurfaceCoordinator
        implements FeedSurfaceProvider,
                FeedBubbleDelegate,
                SwipeRefreshLayout.OnRefreshListener,
                SurfaceCoordinator,
                HasContentListener,
                FeedContentFirstLoadWatcher {
    private static final long DELAY_FEED_HEADER_IPH_MS = 50;

    protected final Activity mActivity;
    private final JankTracker mJankTracker;
    private final SnackbarManager mSnackbarManager;
    @Nullable private final View mNtpHeader;
    private final boolean mShowDarkBackground;
    private final FeedSurfaceDelegate mDelegate;
    private final BottomSheetController mBottomSheetController;
    private final WindowAndroid mWindowAndroid;
    private final Supplier<ShareDelegate> mShareSupplier;
    private final Handler mHandler;
    private final boolean mOverScrollDisabled;
    private final ObserverList<SurfaceCoordinator.Observer> mObservers = new ObserverList<>();
    private final FeedActionDelegate mActionDelegate;
    private final boolean mUseStaggeredLayout;

    // FeedReliabilityLogger params.
    private final long mEmbeddingSurfaceCreatedTimeNs;

    private FeedSurfaceMediator mMediator;

    private UiConfig mUiConfig;
    private FrameLayout mRootView;
    private boolean mIsActive;
    private int mHeaderCount;
    private int mSectionHeaderIndex;
    private int mToolbarHeight;

    // Used when Feed is enabled.
    private @Nullable Profile mProfile;
    private @Nullable FeedSurfaceLifecycleManager mFeedSurfaceLifecycleManager;
    private @Nullable View mSigninPromoView;
    private @Nullable FeedStreamViewResizer mStreamViewResizer;
    // Feed header fields.
    private @Nullable PropertyModel mSectionHeaderModel;
    private @Nullable ViewGroup mViewportView;
    private SectionHeaderView mSectionHeaderView;
    private @Nullable ListModelChangeProcessor<
                    PropertyListModel<PropertyModel, PropertyKey>, SectionHeaderView, PropertyKey>
            mSectionHeaderListModelChangeProcessor;
    private @Nullable PropertyModelChangeProcessor<PropertyModel, SectionHeaderView, PropertyKey>
            mSectionHeaderModelChangeProcessor;
    // Feed RecyclerView/xSurface fields.
    private @Nullable FeedListContentManager mContentManager;
    private @Nullable RecyclerView mRecyclerView;
    private @Nullable FeedSurfaceScope mSurfaceScope;
    private @Nullable FeedSurfaceScopeDependencyProviderImpl mDependencyProvider;
    private @Nullable HybridListRenderer mHybridListRenderer;

    // Used to handle things related to the main scrollable container of NTP surface.
    // In start surface, it does not track scrolling events - only the header offset.
    // In New Tab Page, it does not track the header offset (no header) - instead, it
    // tracks scrolling events.
    private @Nullable ScrollableContainerDelegate mScrollableContainerDelegate;

    private @Nullable HeaderIphScrollListener mHeaderIphScrollListener;
    private @Nullable RefreshIphScrollListener mRefreshIphScrollListener;
    private @Nullable FeedReliabilityLogger mReliabilityLogger;
    private final PrivacyPreferencesManagerImpl mPrivacyPreferencesManager;

    private final Supplier<Toolbar> mToolbarSupplier;

    private FeedSwipeRefreshLayout mSwipeRefreshLayout;

    private boolean mWebFeedHasContent;
    private final ObservableSupplier<Integer> mTabStripHeightSupplier;
    private Callback<Integer> mTabStripHeightChangeCallback;

    /** Provides the additional capabilities needed for the container view. */
    private class RootView extends FrameLayout {
        /**
         * @param context The context of the application.
         */
        public RootView(Context context) {
            super(context);
        }

        @Override
        protected void onConfigurationChanged(Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            mUiConfig.updateDisplayStyle();
        }

        @Override
        protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
            super.onSizeChanged(width, height, oldWidth, oldHeight);
            if (ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_CONTAINMENT)) {
                mRecyclerView.post(mRecyclerView::invalidateItemDecorations);
                updateNtpHeaderMargins();
            }
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (super.onInterceptTouchEvent(ev)) return true;
            if (mMediator != null && !mMediator.getTouchEnabled()) return true;

            return mDelegate.onInterceptTouchEvent(ev);
        }

        @Override
        public void onMeasure(int x, int y) {
            try (TraceEvent e = TraceEvent.scoped("Feed.RootView.onMeasure")) {
                super.onMeasure(x, y);
            }
        }

        @Override
        public void onLayout(boolean a, int b, int c, int d, int e) {
            try (TraceEvent e1 = TraceEvent.scoped("Feed.RootView.onLayout")) {
                super.onLayout(a, b, c, d, e);
            }
        }

        @Override
        public void onDraw(android.graphics.Canvas canvas) {
            try (TraceEvent e = TraceEvent.scoped("Feed.RootView.onDraw")) {
                super.onDraw(canvas);
            }
        }
    }

    private class ScrollableContainerDelegateImpl implements ScrollableContainerDelegate {
        @Override
        public void addScrollListener(ScrollListener listener) {
            if (mRecyclerView == null) return;

            mMediator.addScrollListener(listener);
        }

        @Override
        public void removeScrollListener(ScrollListener listener) {
            if (mRecyclerView == null) return;

            mMediator.removeScrollListener(listener);
        }

        @Override
        public int getVerticalScrollOffset() {
            return mMediator.getVerticalScrollOffset();
        }

        @Override
        public int getRootViewHeight() {
            return mRootView.getHeight();
        }

        @Override
        public int getTopPositionRelativeToContainerView(View childView) {
            int[] pos = new int[2];
            ViewUtils.getRelativeLayoutPosition(mRootView, childView, pos);
            return pos[1];
        }
    }

    // TracingAndPerfScrollListener is explicitly not a ScrollListener due to the fact that the
    // ScrollableContainerDelegate could be null if we are tracking scrolling. However for looking
    // at performance metrics of scrolling we always want to know when feed is scrolling.
    class TracingAndPerfScrollListener extends RecyclerView.OnScrollListener {
        @Override
        public void onScrollStateChanged(RecyclerView view, int newState) {
            switch (mPrevState) {
                case -1:
                    {
                        if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                            startScroll();
                        } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
                            startFling();
                        }
                        // else IDLE
                        break;
                    }
                case RecyclerView.SCROLL_STATE_IDLE:
                    {
                        if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                            startScroll();
                        } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
                            startFling();
                        }
                        break;
                    }
                case RecyclerView.SCROLL_STATE_DRAGGING:
                    {
                        endScroll();
                        if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
                            startFling();
                        } else {
                            finishJankTracking();
                        }
                        break;
                    }
                case RecyclerView.SCROLL_STATE_SETTLING:
                    {
                        endFling();
                        if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                            startScroll();
                        } else {
                            finishJankTracking();
                        }
                        break;
                    }
                default:
                    {
                        mPrevState = -1;
                        break;
                    }
            }
            mPrevState = newState;
        }

        @Override
        public void onScrolled(RecyclerView view, int dx, int dy) {}

        private void finishJankTracking() {
            mJankTracker.finishTrackingScenario(
                    JankScenario.FEED_SCROLLING,
                    TimeUtils.uptimeMillis() * TimeUtils.NANOSECONDS_PER_MILLISECOND);
        }

        private void startScroll() {
            // TODO(nuskos): These next two are just a "hack" to get a nice track name
            // in the UI (it uses the first event it hits). Eventually with the Perfetto
            // SDK we could just explicitly title the track instead.
            TraceEvent.startAsync("Feed.ScrollState", hashCode());
            TraceEvent.finishAsync("Feed.ScrollState", hashCode());
            TraceEvent.startAsync("Feed.TouchScrollStarted", hashCode());
            mJankTracker.startTrackingScenario(JankScenario.FEED_SCROLLING);
        }

        private void endScroll() {
            TraceEvent.finishAsync("Feed.TouchScrollEnded", hashCode());
        }

        private void startFling() {
            TraceEvent.startAsync("Feed.FlingScrollStarted", hashCode());
        }

        private void endFling() {
            TraceEvent.finishAsync("Feed.FlingScrollEnded", hashCode());
        }

        private int mPrevState = -1;
    }

    private class Scroller implements Runnable {
        @Override
        public void run() {
            // The feed header may not be visible for smaller screens or landscape mode. Scroll
            // to show the header after showing the IPH.
            mMediator.scrollToViewIfNecessary(getSectionHeaderPosition());
        }
    }

    // Returns the index of the section header (for you and following tab header).
    int getSectionHeaderPosition() {
        return mSectionHeaderIndex;
    }

    boolean useStaggeredLayout() {
        return mUseStaggeredLayout;
    }

    /**
     * Constructs a new FeedSurfaceCoordinator.
     *
     * @param activity The containing {@link Activity}.
     * @param snackbarManager The {@link SnackbarManager} displaying Snackbar UI.
     * @param windowAndroid The window of the page.
     * @param jankTracker tracks the jank during feed scrolling.
     * @param snapScrollHelper The {@link SnapScrollHelper} for the New Tab Page.
     * @param ntpHeader The extra header on top of the feeds for the New Tab Page.
     * @param toolbarHeight The height of the toolbar which overlaps Feed content at the top of the
     *     view.
     * @param showDarkBackground Whether is shown on dark background.
     * @param delegate The constructing {@link FeedSurfaceDelegate}.
     * @param profile The current user profile.
     * @param bottomSheetController The bottom sheet controller.
     * @param shareDelegateSupplier The supplier for the share delegate used to share articles.
     * @param launchOrigin The origin of what launched the feed.
     * @param privacyPreferencesManager Manages the privacy preferences.
     * @param toolbarSupplier Supplies the {@link Toolbar}.
     * @param embeddingSurfaceCreatedTimeNs Timestamp of creation of the UI surface.
     * @param swipeRefreshLayout The layout to support pull-to-refresh.
     * @param overScrollDisabled Whether the overscroll effect is disabled.
     * @param viewportView The view that should be used as a container for viewport measurement
     *     purposes, or |null| if the view returned by HybridListRenderer is to be used.
     * @param actionDelegate Implements some Feed actions.
     * @param tabStripHeightSupplier Supplier for the tab strip height.
     */
    public FeedSurfaceCoordinator(
            Activity activity,
            SnackbarManager snackbarManager,
            WindowAndroid windowAndroid,
            @Nullable JankTracker jankTracker,
            @Nullable SnapScrollHelper snapScrollHelper,
            @Nullable View ntpHeader,
            @Px int toolbarHeight,
            boolean showDarkBackground,
            FeedSurfaceDelegate delegate,
            Profile profile,
            BottomSheetController bottomSheetController,
            Supplier<ShareDelegate> shareDelegateSupplier,
            @Nullable ScrollableContainerDelegate externalScrollableContainerDelegate,
            @NewTabPageLaunchOrigin int launchOrigin,
            PrivacyPreferencesManagerImpl privacyPreferencesManager,
            @NonNull Supplier<Toolbar> toolbarSupplier,
            long embeddingSurfaceCreatedTimeNs,
            @Nullable FeedSwipeRefreshLayout swipeRefreshLayout,
            boolean overScrollDisabled,
            @Nullable ViewGroup viewportView,
            FeedActionDelegate actionDelegate,
            @NonNull ObservableSupplier<Integer> tabStripHeightSupplier) {
        mActivity = activity;
        mSnackbarManager = snackbarManager;
        mNtpHeader = ntpHeader;
        mShowDarkBackground = showDarkBackground;
        mDelegate = delegate;
        mBottomSheetController = bottomSheetController;
        mProfile = profile;
        mWindowAndroid = windowAndroid;
        mJankTracker = jankTracker;
        mShareSupplier = shareDelegateSupplier;
        mScrollableContainerDelegate = externalScrollableContainerDelegate;
        mPrivacyPreferencesManager = privacyPreferencesManager;
        mToolbarSupplier = toolbarSupplier;
        mSwipeRefreshLayout = swipeRefreshLayout;
        mOverScrollDisabled = overScrollDisabled;
        mViewportView = viewportView;
        mActionDelegate = actionDelegate;
        mEmbeddingSurfaceCreatedTimeNs = embeddingSurfaceCreatedTimeNs;
        mWebFeedHasContent = false;
        mSectionHeaderIndex = 0;
        mToolbarHeight = toolbarHeight;
        mTabStripHeightSupplier = tabStripHeightSupplier;
        mUseStaggeredLayout = DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity);

        mRootView = new RootView(mActivity);
        mRootView.setPadding(0, mTabStripHeightSupplier.get(), 0, 0);

        mTabStripHeightChangeCallback =
                newHeight ->
                        mRootView.setPadding(
                                mRootView.getPaddingLeft(),
                                newHeight,
                                mRootView.getPaddingRight(),
                                mRootView.getPaddingBottom());
        mTabStripHeightSupplier.addObserver(mTabStripHeightChangeCallback);

        mUiConfig = new UiConfig(mRootView);
        mRecyclerView = setUpView();
        mStreamViewResizer =
                FeedStreamViewResizer.createAndAttach(mActivity, mRecyclerView, mUiConfig);

        // Pull-to-refresh set up.
        if (mSwipeRefreshLayout != null && mSwipeRefreshLayout.getParent() == null) {
            mSwipeRefreshLayout.addView(mRecyclerView);
            mRootView.addView(mSwipeRefreshLayout);
        } else {
            mRootView.addView(mRecyclerView);
        }
        if (mSwipeRefreshLayout != null) {
            mSwipeRefreshLayout.addOnRefreshListener(this);
        }

        mHandler = new Handler(Looper.getMainLooper());

        // MVC setup for feed header.
        if (WebFeedBridge.isWebFeedEnabled()) {
            mSectionHeaderView =
                    (SectionHeaderView)
                            LayoutInflater.from(mActivity)
                                    .inflate(R.layout.new_tab_page_multi_feed_header, null, false);
        } else {
            mSectionHeaderView =
                    (SectionHeaderView)
                            LayoutInflater.from(mActivity)
                                    .inflate(
                                            R.layout.new_tab_page_feed_v2_expandable_header,
                                            null,
                                            false);
        }
        mSectionHeaderModel = SectionHeaderListProperties.create(toolbarHeight);

        SectionHeaderViewBinder binder = new SectionHeaderViewBinder();
        mSectionHeaderModelChangeProcessor =
                PropertyModelChangeProcessor.create(
                        mSectionHeaderModel, mSectionHeaderView, binder);
        mSectionHeaderListModelChangeProcessor =
                new ListModelChangeProcessor<>(
                        mSectionHeaderModel.get(SectionHeaderListProperties.SECTION_HEADERS_KEY),
                        mSectionHeaderView,
                        binder);
        mSectionHeaderModel
                .get(SectionHeaderListProperties.SECTION_HEADERS_KEY)
                .addObserver(mSectionHeaderListModelChangeProcessor);

        FeedOptionsCoordinator optionsCoordinator = new FeedOptionsCoordinator(mActivity);

        mSectionHeaderModel.set(
                SectionHeaderListProperties.EXPANDING_DRAWER_VIEW_KEY,
                optionsCoordinator.getView());

        if (mNtpHeader != null && ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_CONTAINMENT)) {
            int bottomPadding =
                    mActivity.getResources().getDimensionPixelSize(R.dimen.feed_header_top_margin);
            mNtpHeader.setPadding(
                    mNtpHeader.getPaddingLeft(),
                    mNtpHeader.getPaddingTop(),
                    mNtpHeader.getPaddingRight(),
                    bottomPadding);

            updateNtpHeaderMargins();
        }

        // Mediator should be created before any Stream changes.
        boolean useUiConfig = ntpHeader != null && mUseStaggeredLayout;
        mMediator =
                new FeedSurfaceMediator(
                        this,
                        mActivity,
                        snapScrollHelper,
                        mSectionHeaderModel,
                        getTabIdFromLaunchOrigin(launchOrigin),
                        actionDelegate,
                        optionsCoordinator,
                        useUiConfig ? mUiConfig : null,
                        profile);

        FeedSurfaceTracker.getInstance().trackSurface(this);

        // Creates streams, initiates content changes.
        mMediator.updateContent();
    }

    void updateNtpHeaderMargins() {
        if (mNtpHeader == null) {
            return;
        }

        // Apply negative margins to the NTP header in order to compensate the containment paddings
        // applied to the whole NTP for non-wide display. This is to allow all the elements in the
        // NTP header to keep using their existing margins/paddings settings.
        int feed_containment_margin =
                mActivity.getResources().getDimensionPixelSize(R.dimen.feed_containment_margin);
        int margin = mUiConfig.getCurrentDisplayStyle().isWide() ? 0 : -feed_containment_margin;
        FrameLayout.LayoutParams layoutParams =
                new FrameLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        layoutParams.setMarginStart(margin);
        layoutParams.setMarginEnd(margin);
        mNtpHeader.setLayoutParams(layoutParams);
    }

    int getToolbarHeight() {
        return mToolbarHeight;
    }

    void setToolbarHairlineVisibility(boolean isVisible) {
        Toolbar toolbar = mToolbarSupplier.get();
        // If the ToolbarLayout isn't visible, we shouldn't change the toolbar_hairline to be
        // visible.
        if (toolbar == null || !toolbar.isBrowsingModeToolbarVisible() && isVisible) {
            return;
        }
        toolbar.setBrowsingModeHairlineVisibility(isVisible);
    }

    /**
     * @return the position of the in-feed header, or an error value Integer.MAX_VALUE when
     *     mScrollableContainerDelegate isn't initialized successfully.
     */
    int getFeedHeaderPosition() {
        if (mScrollableContainerDelegate != null) {
            return mScrollableContainerDelegate.getTopPositionRelativeToContainerView(
                    mSectionHeaderView);
        }
        return Integer.MAX_VALUE;
    }

    @Override
    public void hasContentChanged(@StreamKind int kind, boolean hasContent) {
        if (kind == StreamKind.FOLLOWING) {
            mWebFeedHasContent = hasContent;
        }
    }

    private void stopScrollTracking() {
        if (mScrollableContainerDelegate != null) {
            mScrollableContainerDelegate.removeScrollListener(mDependencyProvider);
            mScrollableContainerDelegate = null;
        }
    }

    public void maybeShowWebFeedAwarenessIph() {
        if (mWebFeedHasContent
                && FeedFeatures.shouldUseWebFeedAwarenessIPH()
                && !FeedFeatures.isFeedFollowUiUpdateEnabled()) {
            UserEducationHelper helper = new UserEducationHelper(mActivity, mProfile, mHandler);
            mSectionHeaderView.showWebFeedAwarenessIph(
                    helper, StreamTabId.FOLLOWING, new Scroller());
        }
    }

    @Override
    public void nonNativeContentLoaded(@StreamKind int kind) {
        // We want to show the web feed IPH on the first load of the FOR_YOU feed.
        if (kind == StreamKind.FOR_YOU) {
            // After the web feed content has loaded, we will know if we have any content, and
            // it is safe to show the IPH.
            maybeShowWebFeedAwarenessIph();
        }
    }

    @Override
    public void destroy() {
        if (mSwipeRefreshLayout != null) {
            if (mSwipeRefreshLayout.isRefreshing()) {
                mSwipeRefreshLayout.setRefreshing(false);
                updateReloadButtonVisibility(/* isReloading= */ false);
            }
            mSwipeRefreshLayout.removeOnRefreshListener(this);
            mSwipeRefreshLayout.disableSwipe();
            mSwipeRefreshLayout = null;
        }
        stopBubbleTriggering();
        if (mFeedSurfaceLifecycleManager != null) mFeedSurfaceLifecycleManager.destroy();
        mFeedSurfaceLifecycleManager = null;
        stopScrollTracking();
        if (mSectionHeaderModelChangeProcessor != null) {
            mSectionHeaderModelChangeProcessor.destroy();
            mSectionHeaderModel
                    .get(SectionHeaderListProperties.SECTION_HEADERS_KEY)
                    .removeObserver(mSectionHeaderListModelChangeProcessor);
        }

        // Destroy mediator after all other related controller/processors are destroyed.
        mMediator.destroy();

        FeedSurfaceTracker.getInstance().untrackSurface(this);
        if (mHybridListRenderer != null) {
            mHybridListRenderer.unbind();
        }
        mRootView.removeAllViews();
        mTabStripHeightSupplier.removeObserver(mTabStripHeightChangeCallback);
    }

    /**
     * Enables/disables the pull-to-refresh.
     *
     * @param enabled Whether the pull-to-refresh should be enabled.
     */
    public void enableSwipeRefresh(boolean enabled) {
        if (mSwipeRefreshLayout != null) {
            if (enabled) {
                mSwipeRefreshLayout.enableSwipe(null);
            } else {
                mSwipeRefreshLayout.disableSwipe();
            }
        }
    }

    @Override
    public TouchEnabledDelegate getTouchEnabledDelegate() {
        return mMediator;
    }

    @Override
    public FeedSurfaceScrollDelegate getScrollDelegate() {
        return mMediator;
    }

    @Override
    public UiConfig getUiConfig() {
        return mUiConfig;
    }

    @Override
    public View getView() {
        return mRootView;
    }

    @Override
    public boolean shouldCaptureThumbnail() {
        return mMediator.shouldCaptureThumbnail();
    }

    @Override
    public void captureThumbnail(Canvas canvas) {
        ViewUtils.captureBitmap(mRootView, canvas);
        mMediator.onThumbnailCaptured();
    }

    @Override
    public void reload() {
        manualRefresh();
    }

    /**
     * Implements SwipeRefreshLayout.OnRefreshListener to be used only for pull
     * to refresh.
     */
    @Override
    public void onRefresh() {
        manualRefresh();
        getFeatureEngagementTracker().notifyEvent(EventConstants.FEED_SWIPE_REFRESHED);
    }

    public void nonSwipeRefresh() {
        if (mSwipeRefreshLayout != null) {
            mSwipeRefreshLayout.startRefreshingAtTheBottom();
        }
        manualRefresh();
    }

    private void manualRefresh() {
        updateReloadButtonVisibility(/* isReloading= */ true);
        if (mReliabilityLogger != null) {
            mReliabilityLogger
                    .getLaunchLogger()
                    .logManualRefresh(SystemClock.elapsedRealtimeNanos());
        }
        mMediator.manualRefresh(
                (Boolean v) -> {
                    updateReloadButtonVisibility(/* isReloading= */ false);
                    if (mSwipeRefreshLayout == null) return;
                    mSwipeRefreshLayout.setRefreshing(false);
                    mSwipeRefreshLayout.setAccessibilityLiveRegion(
                            View.ACCESSIBILITY_LIVE_REGION_NONE);
                    mSwipeRefreshLayout.setContentDescription("");
                });
    }

    void updateReloadButtonVisibility(boolean isReloading) {
        Toolbar toolbar = mToolbarSupplier.get();
        if (toolbar != null) {
            toolbar.updateReloadButtonVisibility(isReloading);
        }
    }

    /**
     * @return The {@link FeedSurfaceLifecycleManager} that manages the lifecycle of the {@link
     *         Stream}.
     */
    FeedSurfaceLifecycleManager getSurfaceLifecycleManager() {
        return mFeedSurfaceLifecycleManager;
    }

    /**
     * @return whether this coordinator is currently active.
     */
    @Override
    public boolean isActive() {
        return mIsActive;
    }

    /** Shows the feed. */
    @Override
    public void onSurfaceOpened() {
        // Guard on isStartupCalled.
        if (!FeedSurfaceTracker.getInstance().isStartupCalled()) return;
        mIsActive = true;
        for (Observer observer : mObservers) {
            observer.surfaceOpened();
        }
        mMediator.onSurfaceOpened();
    }

    /** Hides the feed. */
    @Override
    public void onSurfaceClosed() {
        if (!FeedSurfaceTracker.getInstance().isStartupCalled()) return;
        mIsActive = false;
        mMediator.onSurfaceClosed();
    }

    /** Returns a string usable for restoring the UI to current state. */
    @Override
    public String getSavedInstanceStateString() {
        return mMediator.getSavedInstanceString();
    }

    /** Restores the UI to a previously saved state. */
    @Override
    public void restoreInstanceState(String state) {
        mMediator.restoreSavedInstanceState(state);
    }

    /** Sets the {@link StreamTabId} of the feed given a {@link NewTabPageLaunchOrigin}. */
    public void setTabIdFromLaunchOrigin(@NewTabPageLaunchOrigin int launchOrigin) {
        mMediator.setTabId(getTabIdFromLaunchOrigin(launchOrigin));
    }

    /*
     * Returns true if the supervised user feed should be displayed.
     */
    public boolean shouldDisplaySupervisedFeed() {
        return mProfile.isChild();
    }

    /**
     * Gets the appropriate {@link StreamTabId} for the given {@link NewTabPageLaunchOrigin}.
     *
     * <p>If coming from a Web Feed button, open the following tab, otherwise open the for you tab.
     */
    @VisibleForTesting
    @StreamTabId
    int getTabIdFromLaunchOrigin(@NewTabPageLaunchOrigin int launchOrigin) {
        return launchOrigin == NewTabPageLaunchOrigin.WEB_FEED
                ? StreamTabId.FOLLOWING
                : StreamTabId.DEFAULT;
    }

    private RecyclerView setUpView() {
        mContentManager = new FeedListContentManager();
        ProcessScope processScope = FeedSurfaceTracker.getInstance().getXSurfaceProcessScope();
        if (processScope != null) {
            mDependencyProvider =
                    new FeedSurfaceScopeDependencyProviderImpl(
                            mActivity, mActivity, mShowDarkBackground);

            mSurfaceScope = processScope.obtainFeedSurfaceScope(mDependencyProvider);
            if (mScrollableContainerDelegate != null) {
                mScrollableContainerDelegate.addScrollListener(mDependencyProvider);
            }
        } else {
            mDependencyProvider = null;
            mSurfaceScope = null;
        }
        if (mSurfaceScope != null) {
            mHybridListRenderer = mSurfaceScope.provideListRenderer();

            if (mPrivacyPreferencesManager.isMetricsReportingEnabled()
                    || CommandLine.getInstance()
                            .hasSwitch("force-enable-feed-reliability-logging")) {
                FeedLaunchReliabilityLogger launchLogger =
                        mSurfaceScope.getLaunchReliabilityLogger();
                FeedUserInteractionReliabilityLogger userInteractionLogger =
                        mSurfaceScope.getUserInteractionReliabilityLogger();
                FeedCardOpeningReliabilityLogger cardOpeningLogger =
                        mSurfaceScope.getCardOpeningReliabilityLogger();
                mReliabilityLogger =
                        new FeedReliabilityLogger(
                                launchLogger, userInteractionLogger, cardOpeningLogger);
                launchLogger.logUiStarting(
                        SurfaceType.NEW_TAB_PAGE, mEmbeddingSurfaceCreatedTimeNs);
            }

        } else {
            mHybridListRenderer = new NativeViewListRenderer(mActivity);
        }

        RecyclerView view;
        if (mHybridListRenderer != null) {
            int gutterPadding = -1;
            if (mUseStaggeredLayout) {
                gutterPadding =
                        mActivity
                                .getResources()
                                .getDimensionPixelSize(
                                        ChromeFeatureList.isEnabled(
                                                        ChromeFeatureList.FEED_CONTAINMENT)
                                                ? R.dimen.feed_containment_gutter_padding_per_column
                                                : R.dimen.feed_gutter_padding_per_column);
            }
            // XSurface returns a View, but it should be a RecyclerView.
            view =
                    (RecyclerView)
                            mHybridListRenderer.bind(mContentManager, mViewportView, gutterPadding);
            view.setId(R.id.feed_stream_recycler_view);
            view.setClipToPadding(false);

            if (ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_CONTAINMENT)) {
                // Used to draw containment background.
                view.addItemDecoration(
                        new FeedItemDecoration(
                                mActivity,
                                this,
                                (resId) -> {
                                    return AppCompatResources.getDrawable(mActivity, resId);
                                },
                                gutterPadding));
            }
            view.setBackground(
                    AppCompatResources.getDrawable(mActivity, R.drawable.home_surface_background));

            // Work around https://crbug.com/943873 where default focus highlight shows up after
            // toggling dark mode.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                view.setDefaultFocusHighlightEnabled(false);
            }
            if (mOverScrollDisabled) {
                view.setOverScrollMode(View.OVER_SCROLL_NEVER);
            }
            // Always add the TracingAndPerfScrollListener so debugging traces and metrics continue
            // to work.
            view.addOnScrollListener(new TracingAndPerfScrollListener());
        } else {
            view = null;
        }
        return view;
    }

    /** @return The {@link RecyclerView} associated with this feed. */
    public RecyclerView getRecyclerView() {
        return mRecyclerView;
    }

    /** @return The {@link FeedSurfaceScope} used to create this feed. */
    FeedSurfaceScope getSurfaceScope() {
        return mSurfaceScope;
    }

    /** @return The {@link HybridListRenderer} used to render this feed. */
    HybridListRenderer getHybridListRenderer() {
        return mHybridListRenderer;
    }

    /** @return The {@link FeedListContentManager} managing the contents of this feed. */
    FeedListContentManager getContentManager() {
        return mContentManager;
    }

    /**
     * @return This surface's {@link FeedReliabilityLogger}.
     */
    @Override
    public FeedReliabilityLogger getReliabilityLogger() {
        return mReliabilityLogger;
    }

    /**
     * Configures header views and properties for feed:
     * Adds the feed headers, creates the feed lifecycle manager, adds swipe-to-refresh if needed.
     */
    void setupHeaders(boolean feedEnabled) {
        // Directly add header views to content manager.
        List<View> headerList = new ArrayList<>();
        if (mNtpHeader != null) {
            headerList.add(mNtpHeader);
        }

        if (feedEnabled) {
            mActionDelegate.onStreamCreated();
            mFeedSurfaceLifecycleManager =
                    mDelegate.createStreamLifecycleManager(mActivity, this, mProfile);
            headerList.add(mSectionHeaderView);
            if (mSwipeRefreshLayout != null) {
                mSwipeRefreshLayout.enableSwipe(mScrollableContainerDelegate);
            }
        } else {
            if (mFeedSurfaceLifecycleManager != null) {
                mFeedSurfaceLifecycleManager.destroy();
                mFeedSurfaceLifecycleManager = null;
            }
            if (mSwipeRefreshLayout != null) {
                mSwipeRefreshLayout.disableSwipe();
            }
        }
        setHeaders(headerList);

        // Explicitly request focus on the scroll container to avoid UrlBar being focused after
        // mRootView containers are refreshed.
        mRecyclerView.requestFocus();
    }

    /**
     * Creates a flavor {@Link FeedStream} without any other side-effects.
     *
     * @param kind Kind of stream being created.
     * @return The FeedStream created.
     */
    FeedStream createFeedStream(@StreamKind int kind, Stream.StreamsMediator streamsMediator) {
        return new FeedStream(
                mActivity,
                mProfile,
                mSnackbarManager,
                mBottomSheetController,
                mWindowAndroid,
                mShareSupplier,
                kind,
                mActionDelegate,
                /* feedContentFirstLoadWatcher= */ this,
                streamsMediator,
                /* singleWebFeedParameters= */ null,
                new FeedSurfaceRendererBridge.Factory() {});
    }

    private void setHeaders(List<View> headerViews) {
        // Build the list of headers we want, and then replace existing headers.
        List<FeedListContentManager.FeedContent> headerList = new ArrayList<>();
        boolean hasSigninPromoView = false;
        for (View header : headerViews) {
            // Feed header view in multi does not need padding added.
            int lateralPaddingsPx = getLateralPaddingsPx();

            if (header instanceof NewTabPageLayout) {
                lateralPaddingsPx = 0;
            } else if (header == mSectionHeaderView) {
                lateralPaddingsPx = 0;
                if (!ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_CONTAINMENT)) {
                    mSectionHeaderView.setBackground(
                            AppCompatResources.getDrawable(
                                    mActivity, R.drawable.home_surface_background));
                }
            } else if (header == mSigninPromoView) {
                hasSigninPromoView = true;
                lateralPaddingsPx =
                        mActivity
                                .getResources()
                                .getDimensionPixelSize(
                                        ChromeFeatureList.isEnabled(
                                                        ChromeFeatureList.FEED_CONTAINMENT)
                                                ? R.dimen
                                                        .feed_containment_signin_promo_lateral_paddings
                                                : R.dimen.signin_promo_lateral_paddings);
                ((PersonalizedSigninPromoView) mSigninPromoView)
                        .setCardBackgroundResource(
                                ChromeFeatureList.isEnabled(ChromeFeatureList.FEED_CONTAINMENT)
                                        ? R.drawable.home_surface_background_rounded
                                        : R.drawable.home_surface_ui_background);
            }

            FeedListContentManager.NativeViewContent content =
                    new FeedListContentManager.NativeViewContent(
                            lateralPaddingsPx, "Header" + header.hashCode(), header);
            headerList.add(content);
        }
        if (mContentManager.replaceRange(0, mHeaderCount, headerList)) {
            mHeaderCount = headerList.size();
            mMediator.notifyHeadersChanged(mHeaderCount);
        }
        // The section header is the last header to be added, excluding sign-in promo, save its
        // index.
        mSectionHeaderIndex = headerViews.size() - (hasSigninPromoView ? 2 : 1);
    }

    /**
     * @return The {@link SectionHeaderListProperties} model for the Feed section header.
     */
    PropertyModel getSectionHeaderModelForTest() {
        return mSectionHeaderModel;
    }

    /** @return The {@link View} for this class. */
    View getSigninPromoView() {
        if (mSigninPromoView == null) {
            LayoutInflater inflater = LayoutInflater.from(mRootView.getContext());
            mSigninPromoView =
                    inflater.inflate(
                            R.layout.sync_promo_view_content_suggestions, mRootView, false);
        }
        return mSigninPromoView;
    }

    /** Update header views in the Feed. */
    void updateHeaderViews(boolean isSignInPromoVisible) {
        if (!mMediator.hasStreams()) return;

        List<View> headers = new ArrayList<>();
        if (mNtpHeader != null) {
            headers.add(mNtpHeader);
        }

        headers.add(mSectionHeaderView);

        if (isSignInPromoVisible) {
            headers.add(getSigninPromoView());
        }
        setHeaders(headers);
    }

    public FeedSurfaceMediator getMediatorForTesting() {
        return mMediator;
    }

    public void setMediatorForTesting(FeedSurfaceMediator mediator) {
        mMediator = mediator;
    }

    public View getSignInPromoViewForTesting() {
        return getSigninPromoView();
    }

    public View getSectionHeaderViewForTesting() {
        return mSectionHeaderView;
    }

    /**
     * Initializes things related to the bubbles which will start listening to scroll events to
     * determine whether a bubble should be triggered.
     *
     * You must stop the IPH with #stopBubbleTriggering before tearing down feed components, e.g.,
     * on #destroy. This also applies for the case where the feed stream is deleted when disabled
     * (e.g., by policy).
     */
    void initializeBubbleTriggering() {
        // Don't do anything when there is no feed stream because the bubble isn't needed in
        // that case.
        if (!mMediator.hasStreams()) return;

        // Provide a delegate for the container of the feed surface that is handled by the feed
        // coordinator itself when not provided externally (e.g., by the NewTabPage).
        if (mScrollableContainerDelegate == null) {
            mScrollableContainerDelegate = new ScrollableContainerDelegateImpl();
        }

        createHeaderIphScrollListener();
        createRefreshIphScrollListener();
    }

    private void createHeaderIphScrollListener() {
        mHeaderIphScrollListener =
                new HeaderIphScrollListener(
                        this,
                        mScrollableContainerDelegate,
                        () -> {
                            UserEducationHelper helper =
                                    new UserEducationHelper(mActivity, mProfile, mHandler);
                            mSectionHeaderView.showMenuIph(helper);
                        });
        mScrollableContainerDelegate.addScrollListener(mHeaderIphScrollListener);
    }

    private void createRefreshIphScrollListener() {
        mRefreshIphScrollListener =
                new RefreshIphScrollListener(
                        this,
                        mScrollableContainerDelegate,
                        () -> {
                            UserEducationHelper helper =
                                    new UserEducationHelper(mActivity, mProfile, mHandler);
                            mSwipeRefreshLayout.showIPH(helper);
                        });
        mScrollableContainerDelegate.addScrollListener(mRefreshIphScrollListener);
    }

    /**
     * Stops and deletes things related to the bubbles. Must be called before tearing down feed
     * components, e.g., on #destroy. This also applies for the case where the feed stream is
     * deleted when disabled (e.g., by policy).
     */
    private void stopBubbleTriggering() {
        if (mMediator.hasStreams() && mScrollableContainerDelegate != null) {
            if (mHeaderIphScrollListener != null) {
                mScrollableContainerDelegate.removeScrollListener(mHeaderIphScrollListener);
                mHeaderIphScrollListener = null;
            }
            if (mRefreshIphScrollListener != null) {
                mScrollableContainerDelegate.removeScrollListener(mRefreshIphScrollListener);
                mRefreshIphScrollListener = null;
            }
        }
        stopScrollTracking();
    }

    @Override
    public Tracker getFeatureEngagementTracker() {
        return TrackerFactory.getTrackerForProfile(mProfile);
    }

    @Override
    public boolean isFeedExpanded() {
        return mSectionHeaderModel.get(SectionHeaderListProperties.IS_SECTION_ENABLED_KEY);
    }

    @Override
    public boolean isSignedIn() {
        return FeedServiceBridge.isSignedIn();
    }

    @Override
    public boolean isFeedHeaderPositionInContainerSuitableForIPH(float headerMaxPosFraction) {
        assert headerMaxPosFraction >= 0.0f && headerMaxPosFraction <= 1.0f
                : "Max position fraction should be ranging between 0.0 and 1.0";

        int topPosInStream =
                mScrollableContainerDelegate.getTopPositionRelativeToContainerView(
                        mSectionHeaderView);
        if (topPosInStream < 0) return false;
        if (topPosInStream
                > headerMaxPosFraction * mScrollableContainerDelegate.getRootViewHeight()) {
            return false;
        }

        return true;
    }

    @Override
    public long getCurrentTimeMs() {
        return System.currentTimeMillis();
    }

    @Override
    public long getLastFetchTimeMs() {
        return mMediator.getLastFetchTimeMsForCurrentStream();
    }

    @Override
    public boolean canScrollUp() {
        // mSwipeRefreshLayout is set to NULL when this instance is destroyed, but
        // RefreshIphScrollListener.onHeaderOffsetChanged may still be triggered which will call
        // into this method.
        return (mSwipeRefreshLayout == null) ? true : mSwipeRefreshLayout.canScrollVertically(-1);
    }

    @Override
    public void addObserver(SurfaceCoordinator.Observer observer) {
        mObservers.addObserver(observer);
    }

    @Override
    public void removeObserver(SurfaceCoordinator.Observer observer) {
        mObservers.removeObserver(observer);
    }

    @Override
    public void onActivityPaused() {
        if (mReliabilityLogger != null) {
            mReliabilityLogger.onActivityPaused();
        }
    }

    @Override
    public void onActivityResumed() {
        if (mReliabilityLogger != null) {
            mReliabilityLogger.onActivityResumed();
        }
    }

    public boolean isLoadingFeed() {
        return mMediator.isLoadingFeed();
    }

    private int getLateralPaddingsPx() {
        return mActivity
                .getResources()
                .getDimensionPixelSize(R.dimen.ntp_header_lateral_paddings_v2);
    }

    public void setReliabilityLoggerForTesting(FeedReliabilityLogger logger) {
        var oldValue = mReliabilityLogger;
        mReliabilityLogger = logger;
        ResettersForTesting.register(() -> mReliabilityLogger = oldValue);
    }

    public void clearScrollableContainerDelegateForTesting() {
        mScrollableContainerDelegate = null;
    }

    public FeedActionDelegate getActionDelegateForTesting() {
        return mActionDelegate;
    }

    FrameLayout getRootViewForTesting() {
        return mRootView;
    }
}