chromium/chrome/browser/creator/android/java/src/org/chromium/chrome/browser/creator/CreatorCoordinator.java

// Copyright 2022 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.creator;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.supplier.UnownedUserDataSupplier;
import org.chromium.base.version_info.VersionInfo;
import org.chromium.chrome.browser.feed.FeedActionDelegate;
import org.chromium.chrome.browser.feed.FeedContentFirstLoadWatcher;
import org.chromium.chrome.browser.feed.FeedListContentManager;
import org.chromium.chrome.browser.feed.FeedListContentManager.FeedContent;
import org.chromium.chrome.browser.feed.FeedStream;
import org.chromium.chrome.browser.feed.FeedStreamViewResizer;
import org.chromium.chrome.browser.feed.FeedSurfaceRendererBridge;
import org.chromium.chrome.browser.feed.FeedSurfaceScopeDependencyProviderImpl;
import org.chromium.chrome.browser.feed.FeedSurfaceTracker;
import org.chromium.chrome.browser.feed.NativeViewListRenderer;
import org.chromium.chrome.browser.feed.SingleWebFeedEntryPoint;
import org.chromium.chrome.browser.feed.SingleWebFeedParameters;
import org.chromium.chrome.browser.feed.Stream;
import org.chromium.chrome.browser.feed.StreamKind;
import org.chromium.chrome.browser.feed.webfeed.WebFeedBridge;
import org.chromium.chrome.browser.feed.webfeed.WebFeedBridge.QueryResult;
import org.chromium.chrome.browser.feed.webfeed.WebFeedBridge.WebFeedMetadata;
import org.chromium.chrome.browser.feed.webfeed.WebFeedSubscriptionStatus;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.xsurface.HybridListRenderer;
import org.chromium.chrome.browser.xsurface.ProcessScope;
import org.chromium.chrome.browser.xsurface.feed.FeedSurfaceScope;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerFactory;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;

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

/**
 * Sets up the Coordinator for Cormorant Creator surface.  It is based on the doc at
 * https://chromium.googlesource.com/chromium/src/+/HEAD/docs/ui/android/mvc_simple_list_tutorial.md
 */
public class CreatorCoordinator
        implements FeedContentFirstLoadWatcher, View.OnLayoutChangeListener {
    private final ViewGroup mCreatorViewGroup;
    private CreatorMediator mMediator;
    private CreatorTabMediator mTabMediator;
    private Activity mActivity;
    private FeedListContentManager mContentManager;
    private UiConfig mUiConfig;
    private RecyclerView mRecyclerView;
    private View mProfileView;
    private ViewGroup mLayoutView;
    private HybridListRenderer mHybridListRenderer;
    private FeedSurfaceScope mSurfaceScope;
    private FeedSurfaceScopeDependencyProviderImpl mDependencyProvider;
    private PropertyModel mCreatorModel;
    private PropertyModelChangeProcessor<PropertyModel, CreatorProfileView, PropertyKey>
            mCreatorProfileModelChangeProcessor;
    private PropertyModelChangeProcessor<PropertyModel, CreatorToolbarView, PropertyKey>
            mCreatorToolbarModelChangeProcessor;

    private final SnackbarManager mSnackbarManager;
    private final CreatorSnackbarController mCreatorSnackbarController;
    private final WindowAndroid mWindowAndroid;
    private BottomSheetController mBottomSheetController;
    private ScrimCoordinator mScrim;
    private ViewGroup mBottomSheetContainer;
    private ViewGroup mLayout;
    private Profile mProfile;
    private Stream mStream;
    private int mHeaderCount;

    private EmptyBottomSheetObserver mSheetObserver;
    private ContentView mContentView;
    private WebContents mWebContents;
    private int mCurrentMaxViewHeight;
    private CreatorTabSheetContent mSheetContent;
    private boolean mPeeked;
    private boolean mFullyOpened;
    private WebContentsCreator mCreatorWebContents;
    private NewTabCreator mCreatorOpenTab;
    private final UnownedUserDataSupplier<ShareDelegate> mBottomsheetShareDelegateSupplier;
    private GURL mBottomSheetUrl;
    private int mEntryPoint;

    private @Nullable FeedStreamViewResizer mStreamViewResizer;

    private static final String CREATOR_PROFILE_ID = "CreatorProfileView";
    private static final String CREATOR_PRIVACY_ID = "CreatorPrivacyId";

    /**
     * Constructor for the CreatorCoordinator.
     *
     * @param activity the Creator Activity this is a part of.
     * @param webFeedId the ID that is is used to create the feed.
     * @param snackbarManager the snackbarManager that is used for the feed.
     * @param windowAndroid the window needed by the feed
     * @param profile the Profile of the user.
     * @param url the url used by the creator profile.
     * @param creatorWebContents the interface to generate webcontents for the bottomsheet.
     * @param creatorOpenTab the interface to open urls in a new tab, used by the bottomsheet.
     * @param bottomsheetShareDelegateSupplier an empty share delegate supplier, used by the
     *         bottomsheet.
     * @param entryPoint the SingleWebFeedEntryPoint has the Activity been launched with.
     * @param isFollowing the initial state of if the creator is being followed.
     */
    public CreatorCoordinator(
            Activity activity,
            byte[] webFeedId,
            SnackbarManager snackbarManager,
            WindowAndroid windowAndroid,
            Profile profile,
            String url,
            WebContentsCreator creatorWebContents,
            NewTabCreator creatorOpenTab,
            UnownedUserDataSupplier<ShareDelegate> bottomsheetShareDelegateSupplier,
            int entryPoint,
            boolean isFollowing,
            SignInInterstitialInitiator signInInterstitialInitiator) {
        mActivity = activity;
        mProfile = profile;
        mSnackbarManager = snackbarManager;
        mWindowAndroid = windowAndroid;
        mRecyclerView = setUpView();
        mCreatorWebContents = creatorWebContents;
        mCreatorOpenTab = creatorOpenTab;
        mBottomsheetShareDelegateSupplier = bottomsheetShareDelegateSupplier;
        mEntryPoint = entryPoint;
        mCreatorSnackbarController = new CreatorSnackbarController(mActivity, mSnackbarManager);

        mProfileView =
                (View) LayoutInflater.from(mActivity).inflate(R.layout.creator_profile, null);
        List<FeedListContentManager.FeedContent> contentPreviewsList = new ArrayList<>();
        contentPreviewsList.add(
                new FeedListContentManager.NativeViewContent(
                        getContentPreviewsPaddingPx(), CREATOR_PROFILE_ID, mProfileView));
        mContentManager.addContents(0, contentPreviewsList);
        mHeaderCount = 1;

        // Inflate the XML
        mCreatorViewGroup =
                (ViewGroup) LayoutInflater.from(mActivity).inflate(R.layout.creator_activity, null);
        mLayoutView = mCreatorViewGroup.findViewById(R.id.creator_layout);
        mUiConfig = new UiConfig(mLayoutView);
        mStreamViewResizer =
                FeedStreamViewResizer.createAndAttach(mActivity, mRecyclerView, mUiConfig);
        mLayoutView.addView(mRecyclerView);

        // Generate Creator Model
        mCreatorModel = generateCreatorModel(webFeedId, url, isFollowing);
        // Attempt to avoid possible extra query if we already have metadata.
        if (webFeedId != null) {
            getWebFeedMetadata();
        }
        initBottomSheet();

        mCreatorProfileModelChangeProcessor =
                PropertyModelChangeProcessor.create(
                        mCreatorModel,
                        (CreatorProfileView) mProfileView,
                        CreatorProfileViewBinder::bind);
        mCreatorToolbarModelChangeProcessor =
                PropertyModelChangeProcessor.create(
                        mCreatorModel,
                        (CreatorToolbarView) mLayoutView,
                        CreatorToolbarViewBinder::bind);
        setUpToolbarListener();

        mMediator =
                new CreatorMediator(
                        mActivity,
                        mCreatorModel,
                        mCreatorSnackbarController,
                        signInInterstitialInitiator);
    }

    /**
     * Query for webfeedId if we don't have it, and then create the FeedStream.
     *
     * @param FeedActionDelegate Interface for Feed actions implemented by the Browser.
     * @param Supplier<ShareDelegate> Supplier of the interface to expose sharing.
     */
    public void queryFeedStream(
            FeedActionDelegate feedActionDelegate, Supplier<ShareDelegate> shareDelegateSupplier) {
        if (mCreatorModel.get(CreatorProperties.WEB_FEED_ID_KEY) == null) {
            Callback<QueryResult> queryWebFeedIdCallback =
                    result -> {
                        mCreatorModel.set(
                                CreatorProperties.WEB_FEED_ID_KEY, result.webFeedId.getBytes());
                        mCreatorModel.set(CreatorProperties.TITLE_KEY, result.title);
                        if (TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.URL_KEY))) {
                            mCreatorModel.set(CreatorProperties.URL_KEY, result.url);
                            mCreatorModel.set(
                                    CreatorProperties.FORMATTED_URL_KEY,
                                    UrlFormatter
                                            .formatUrlForDisplayOmitSchemePathAndTrivialSubdomains(
                                                    new GURL(result.url)));
                        }
                        initFeedStream(feedActionDelegate, shareDelegateSupplier);
                    };
            WebFeedBridge.queryWebFeed(
                    mCreatorModel.get(CreatorProperties.URL_KEY), queryWebFeedIdCallback);
        } else if (TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.TITLE_KEY))
                || TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.URL_KEY))) {
            Callback<QueryResult> queryWebFeedIdCallback =
                    result -> {
                        mCreatorModel.set(CreatorProperties.TITLE_KEY, result.title);
                        mCreatorModel.set(CreatorProperties.URL_KEY, result.url);
                        mCreatorModel.set(
                                CreatorProperties.FORMATTED_URL_KEY,
                                UrlFormatter.formatUrlForDisplayOmitSchemePathAndTrivialSubdomains(
                                        new GURL(result.url)));
                    };
            WebFeedBridge.queryWebFeedId(
                    new String(mCreatorModel.get(CreatorProperties.WEB_FEED_ID_KEY)),
                    queryWebFeedIdCallback);
            initFeedStream(feedActionDelegate, shareDelegateSupplier);
        }
    }

    /**
     * Create the FeedStream and bind it to the RecyclerView.
     *
     * @param FeedActionDelegate Interface for Feed actions implemented by the Browser.
     * @param Supplier<ShareDelegate> Supplier of the interface to expose sharing.
     */
    private void initFeedStream(
            FeedActionDelegate feedActionDelegate, Supplier<ShareDelegate> shareDelegateSupplier) {
        mStream =
                new FeedStream(
                        mActivity,
                        mProfile,
                        mSnackbarManager,
                        mBottomSheetController,
                        mWindowAndroid,
                        /* shareSupplier= */ shareDelegateSupplier,
                        StreamKind.SINGLE_WEB_FEED,
                        feedActionDelegate,
                        /* feedContentFirstLoadWatcher= */ this,
                        /* streamsMediator= */ new StreamsMediatorImpl(),
                        new SingleWebFeedParameters(
                                mCreatorModel.get(CreatorProperties.WEB_FEED_ID_KEY), mEntryPoint),
                        new FeedSurfaceRendererBridge.Factory() {});

        if (mEntryPoint == SingleWebFeedEntryPoint.MENU) {
            mStream.addOnContentChangedListener(new ContentChangedListener());
        }

        mStream.bind(
                mRecyclerView,
                mContentManager,
                /* feedScrollState= */ null,
                mSurfaceScope,
                mHybridListRenderer,
                null,
                mHeaderCount);
    }

    private class StreamsMediatorImpl implements Stream.StreamsMediator {
        @Override
        public void disableFollowButton() {
            mProfileView.findViewById(R.id.creator_follow_button).setEnabled(false);
            mProfileView.findViewById(R.id.creator_following_button).setEnabled(false);
        }
    }

    public ViewGroup getView() {
        return mCreatorViewGroup;
    }

    @VisibleForTesting
    public View getProfileView() {
        return mProfileView;
    }

    public PropertyModel getCreatorModel() {
        return mCreatorModel;
    }

    public BottomSheetController getBottomSheetController() {
        return mBottomSheetController;
    }

    private RecyclerView setUpView() {
        // TODO(crbug.com/40872531): Refactor NTP naming out of the general Feed code.
        mContentManager = new FeedListContentManager();
        ProcessScope processScope = FeedSurfaceTracker.getInstance().getXSurfaceProcessScope();

        if (processScope != null) {
            mDependencyProvider =
                    new FeedSurfaceScopeDependencyProviderImpl(
                            mActivity, mActivity, ColorUtils.inNightMode(mActivity));
            mSurfaceScope = processScope.obtainFeedSurfaceScope(mDependencyProvider);
        } else {
            mDependencyProvider = null;
            mSurfaceScope = null;
        }

        if (mSurfaceScope != null) {
            mHybridListRenderer = mSurfaceScope.provideListRenderer();
        } else {
            mHybridListRenderer = new NativeViewListRenderer(mActivity);
        }

        RecyclerView view;
        if (mHybridListRenderer != null) {
            view =
                    (RecyclerView)
                            mHybridListRenderer.bind(
                                    mContentManager,
                                    /* mViewportView= */ null,
                                    /* useStaggeredLayout= */ false);
            view.setId(R.id.creator_feed_stream_recycler_view);
            view.setClipToPadding(false);
            view.setBackgroundColor(SemanticColorUtils.getDefaultBgColor(mActivity));
            view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
        } else {
            view = null;
        }

        return view;
    }

    private int getContentPreviewsPaddingPx() {
        return mActivity.getResources().getDimensionPixelSize(R.dimen.content_previews_padding);
    }

    private PropertyModel generateCreatorModel(byte[] webFeedId, String url, boolean following) {
        String formattedUrl =
                UrlFormatter.formatUrlForDisplayOmitSchemePathAndTrivialSubdomains(new GURL(url));
        PropertyModel model =
                new PropertyModel.Builder(CreatorProperties.ALL_KEYS)
                        .with(CreatorProperties.WEB_FEED_ID_KEY, webFeedId)
                        .with(CreatorProperties.URL_KEY, url)
                        .with(CreatorProperties.IS_FOLLOWED_KEY, following)
                        .with(CreatorProperties.IS_TOOLBAR_VISIBLE_KEY, false)
                        .with(CreatorProperties.FORMATTED_URL_KEY, formattedUrl)
                        .build();
        return model;
    }

    private void getWebFeedMetadata() {
        Callback<WebFeedMetadata> metadata_callback =
                result -> {
                    @WebFeedSubscriptionStatus
                    int subscriptionStatus =
                            result == null
                                    ? WebFeedSubscriptionStatus.UNKNOWN
                                    : result.subscriptionStatus;
                    if (subscriptionStatus == WebFeedSubscriptionStatus.UNKNOWN
                            || subscriptionStatus == WebFeedSubscriptionStatus.NOT_SUBSCRIBED) {
                        mCreatorModel.set(CreatorProperties.IS_FOLLOWED_KEY, false);
                    } else if (subscriptionStatus == WebFeedSubscriptionStatus.SUBSCRIBED) {
                        mCreatorModel.set(CreatorProperties.IS_FOLLOWED_KEY, true);
                    }
                    if (TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.TITLE_KEY))
                            && TextUtils.isEmpty(result.title)) {
                        mCreatorModel.set(CreatorProperties.TITLE_KEY, result.title);
                    }
                    if (TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.URL_KEY))
                            && result.visitUrl.isValid()) {
                        mCreatorModel.set(CreatorProperties.URL_KEY, result.visitUrl.getSpec());
                        mCreatorModel.set(
                                CreatorProperties.FORMATTED_URL_KEY,
                                UrlFormatter.formatUrlForDisplayOmitSchemePathAndTrivialSubdomains(
                                        result.visitUrl));
                    }
                };
        WebFeedBridge.getWebFeedMetadata(
                mCreatorModel.get(CreatorProperties.WEB_FEED_ID_KEY), metadata_callback);
    }

    /** Set up the bottom sheet for this activity. */
    private void initBottomSheet() {
        mScrim =
                new ScrimCoordinator(
                        mActivity,
                        new ScrimCoordinator.SystemUiScrimDelegate() {
                            @Override
                            public void setStatusBarScrimFraction(float scrimFraction) {}

                            @Override
                            public void setNavigationBarScrimFraction(float scrimFraction) {}
                        },
                        mCreatorViewGroup,
                        mActivity.getColor(R.color.default_scrim_color));

        mBottomSheetContainer = new FrameLayout(mActivity);
        mBottomSheetContainer.setId(R.id.creator_content_preview_bottom_sheet);
        mBottomSheetContainer.setLayoutParams(
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mCreatorViewGroup.addView(mBottomSheetContainer);
        mBottomSheetController =
                BottomSheetControllerFactory.createBottomSheetController(
                        () -> mScrim,
                        (sheet) -> {},
                        mActivity.getWindow(),
                        KeyboardVisibilityDelegate.getInstance(),
                        () -> mBottomSheetContainer,
                        () -> 0);
    }

    private void setUpToolbarListener() {
        mRecyclerView.addOnScrollListener(
                new OnScrollListener() {
                    @Override
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        mCreatorModel.set(
                                CreatorProperties.IS_TOOLBAR_VISIBLE_KEY,
                                recyclerView.canScrollVertically(-1));
                    }
                });
    }

    @Override
    public void nonNativeContentLoaded(@StreamKind int kind) {}

    /**
     * Entry point for preview tab flow. This will create an creator tab and show it in the
     * bottom sheet.
     *
     * @param url The URL to be shown.
     */
    public void requestOpenSheet(GURL url) {
        mBottomSheetUrl = url;
        if (mTabMediator == null) {
            float topControlsHeight =
                    mActivity.getResources().getDimensionPixelSize(R.dimen.toolbar_height_no_shadow)
                            / mWindowAndroid.getDisplay().getDipScale();
            mTabMediator =
                    new CreatorTabMediator(
                            mBottomSheetController,
                            new FaviconLoader(mActivity),
                            (int) topControlsHeight);
        }
        if (mWebContents == null) {
            assert mSheetContent == null;
            createWebContents();
            mSheetObserver =
                    new EmptyBottomSheetObserver() {
                        @Override
                        public void onSheetContentChanged(BottomSheetContent newContent) {
                            if (newContent != mSheetContent) {
                                mPeeked = false;
                                destroyWebContents();
                            }
                        }

                        @Override
                        public void onSheetStateChanged(int newState, int reason) {
                            if (mSheetContent == null) return;
                            switch (newState) {
                                case BottomSheetController.SheetState.PEEK:
                                    if (!mPeeked) {
                                        mPeeked = true;
                                    }
                                    break;
                                case BottomSheetController.SheetState.FULL:
                                    if (!mFullyOpened) {
                                        mFullyOpened = true;
                                    }
                                    break;
                            }
                        }

                        @Override
                        public void onSheetOffsetChanged(float heightFraction, float offsetPx) {
                            if (mSheetContent == null) return;
                            mSheetContent.showOpenInNewTabButton(heightFraction);
                        }
                    };
            mBottomSheetController.addObserver(mSheetObserver);
            IntentRequestTracker intentRequestTracker = mWindowAndroid.getIntentRequestTracker();
            assert intentRequestTracker != null
                    : "ActivityWindowAndroid must have a IntentRequestTracker.";
            mSheetContent =
                    new CreatorTabSheetContent(
                            mActivity,
                            this::openInNewTab,
                            this::onToolbarClick,
                            this::close,
                            getMaxViewHeight(),
                            intentRequestTracker,
                            mBottomsheetShareDelegateSupplier);
            mTabMediator.init(mWebContents, mContentView, mSheetContent, mProfile);
            mLayoutView.addOnLayoutChangeListener(this);
        }

        mPeeked = false;
        mFullyOpened = false;
        mTabMediator.requestShowContent(url, mCreatorModel.get(CreatorProperties.TITLE_KEY));
    }

    @Override
    public void onLayoutChange(
            View view,
            int left,
            int top,
            int right,
            int bottom,
            int oldLeft,
            int oldTop,
            int oldRight,
            int oldBottom) {
        if (mSheetContent == null) return;

        // It may not be possible to update the content height when the actual height changes
        // due to the current tab not being ready yet. Try it later again when the tab
        // (hence MaxViewHeight) becomes valid.
        int maxViewHeight = getMaxViewHeight();
        if (maxViewHeight == 0 || mCurrentMaxViewHeight == maxViewHeight) return;
        mSheetContent.updateContentHeight(maxViewHeight);
        mCurrentMaxViewHeight = maxViewHeight;
    }

    /**
     * @return The maximum base view height for sheet content view.
     * */
    private int getMaxViewHeight() {
        return mCreatorViewGroup.getHeight();
    }

    /** Close the bottomsheet tab. */
    public void close() {
        mBottomSheetController.hideContent(mSheetContent, /* animate= */ true);
    }

    private void openInNewTab() {
        String url =
                mBottomSheetUrl.isValid()
                        ? mBottomSheetUrl.getSpec()
                        : mCreatorModel.get(CreatorProperties.URL_KEY);
        mBottomSheetController.hideContent(
                mSheetContent, /* animate= */ true, StateChangeReason.PROMOTE_TAB);
        mCreatorOpenTab.createNewTab(new LoadUrlParams(url));
    }

    private void onToolbarClick() {
        int state = mBottomSheetController.getSheetState();
        if (state == BottomSheetController.SheetState.PEEK) {
            mBottomSheetController.expandSheet();
        } else if (state == BottomSheetController.SheetState.FULL) {
            mBottomSheetController.collapseSheet(true);
        }
    }

    private void createWebContents() {
        assert mWebContents == null;

        // Creates an initially hidden WebContents which gets shown when the panel is opened.
        mWebContents = mCreatorWebContents.createWebContents();

        mContentView = ContentView.createContentView(mActivity, mWebContents);

        mWebContents.setDelegates(
                VersionInfo.getProductVersion(),
                ViewAndroidDelegate.createBasicDelegate(mContentView),
                mContentView,
                mWindowAndroid,
                WebContents.createDefaultInternalsHolder());
    }

    private void destroyWebContents() {
        mSheetContent = null; // Will be destroyed by BottomSheet controller.

        mPeeked = false;
        mFullyOpened = false;

        if (mWebContents != null) {
            mWebContents.destroy();
            mWebContents = null;
            mContentView = null;
        }

        if (mMediator != null) mTabMediator.destroyContent();

        mLayoutView.removeOnLayoutChangeListener(this);
        if (mSheetObserver != null) mBottomSheetController.removeObserver(mSheetObserver);
    }

    void setStreamForTest(Stream stream) {
        mStream = stream;
    }

    class ContentChangedListener implements Stream.ContentChangedListener {
        @Override
        public void onContentChanged(List<FeedContent> feedContents) {
            if (feedContents == null) return;
            boolean hasError = false;
            // Assume native cards beyond the header are errors.
            for (int i = mHeaderCount; i < feedContents.size(); i++) {
                FeedContent content = feedContents.get(i);
                if (content.isNativeView()) {
                    hasError = true;
                    break;
                }
            }
            // If no error cards are found, then remove the listener and add privacy header.
            if (!hasError) {
                List<FeedContent> privacyList = new ArrayList<>();
                View privacyView =
                        LayoutInflater.from(mActivity).inflate(R.layout.creator_privacy, null);
                privacyList.add(
                        new FeedListContentManager.NativeViewContent(
                                getContentPreviewsPaddingPx(), CREATOR_PRIVACY_ID, privacyView));
                mContentManager.addContents(mHeaderCount, privacyList);
                mHeaderCount += privacyList.size();
                mStream.removeOnContentChangedListener(this);
                mStream.notifyNewHeaderCount(mHeaderCount);
            } else if (TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.URL_KEY))
                    || TextUtils.isEmpty(mCreatorModel.get(CreatorProperties.TITLE_KEY))) {
                // If there is an error, hide the profile section if either the creator URL or
                // creator title is unavailable.
                mContentManager.removeContents(0, 1);
                mHeaderCount -= 1;
                mStream.removeOnContentChangedListener(this);
                mStream.notifyNewHeaderCount(mHeaderCount);
            }
        }
    }

    /**
     * Helper class to generate a favicon for a given URL and resize it to the desired dimensions
     * for displaying it on the image view.
     */
    static class FaviconLoader {
        private final Context mContext;
        private final FaviconHelper mFaviconHelper;
        private final RoundedIconGenerator mIconGenerator;
        private final int mFaviconSize;

        /**
         * The FaviconLoader constructor.
         * @param context The context where the Favicon will be loaded.
         */
        public FaviconLoader(Context context) {
            mContext = context;
            mFaviconHelper = new FaviconHelper();
            mIconGenerator = FaviconUtils.createCircularIconGenerator(mContext);
            mFaviconSize =
                    mContext.getResources().getDimensionPixelSize(R.dimen.preview_tab_favicon_size);
        }

        /**
         * Generates a favicon for a given URL. If no favicon was could be found or generated from
         * the URL, a default favicon will be shown.
         *
         * @param url The URL for which favicon is to be generated.
         * @param callback The callback to be invoked to display the final image.
         * @param profile The profile for which favicon service is used.
         */
        public void loadFavicon(final GURL url, Callback<Drawable> callback, Profile profile) {
            assert profile != null;
            FaviconHelper.FaviconImageCallback imageCallback =
                    (bitmap, iconUrl) -> {
                        Drawable drawable;
                        if (bitmap != null) {
                            drawable =
                                    FaviconUtils.createRoundedBitmapDrawable(
                                            mContext.getResources(), bitmap);
                        } else {
                            drawable =
                                    UiUtils.getTintedDrawable(
                                            mContext,
                                            R.drawable.ic_globe_24dp,
                                            R.color.default_icon_color_tint_list);
                        }

                        callback.onResult(drawable);
                    };

            mFaviconHelper.getLocalFaviconImageForURL(profile, url, mFaviconSize, imageCallback);
        }
    }
}