chromium/chrome/browser/ui/android/ephemeraltab/java/src/org/chromium/chrome/browser/ephemeraltab/EphemeralTabMediator.java

// Copyright 2020 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.ephemeraltab;

import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.ViewGroup;

import androidx.annotation.DrawableRes;

import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.ObserverList.RewindableIterator;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.embedder_support.delegate.WebContentsDelegateAndroid;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.components.security_state.ConnectionSecurityLevel;
import org.chromium.components.security_state.SecurityStateModel;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.common.ResourceRequestBody;
import org.chromium.ui.widget.Toast;
import org.chromium.url.GURL;

/** Mediator class for preview tab, responsible for communicating with other objects. */
public class EphemeralTabMediator {
    /** The delay (four video frames) after which the hide progress will be hidden. */
    private static final long HIDE_PROGRESS_BAR_DELAY_MS = (1000 / 60) * 4;

    private final BottomSheetController mBottomSheetController;
    private final EphemeralTabCoordinator.FaviconLoader mFaviconLoader;
    private final ObserverList<EphemeralTabObserver> mObservers;
    private final int mTopControlsHeightDp;

    private WebContents mWebContents;
    private EphemeralTabSheetContent mSheetContent;
    private WebContentsObserver mWebContentsObserver;
    private WebContentsDelegateAndroid mWebContentsDelegate;
    private Profile mProfile;

    /** Constructor. */
    public EphemeralTabMediator(
            BottomSheetController bottomSheetController,
            EphemeralTabCoordinator.FaviconLoader faviconLoader,
            int topControlsHeightDp) {
        mBottomSheetController = bottomSheetController;
        mFaviconLoader = faviconLoader;
        mTopControlsHeightDp = topControlsHeightDp;
        mObservers = new ObserverList<EphemeralTabObserver>();
    }

    /** Initializes various objects for a new tab. */
    void init(
            WebContents webContents,
            ContentView contentView,
            EphemeralTabSheetContent sheetContent,
            Profile profile) {
        // Ensures that initialization is performed only when a new tab is opened.
        assert mProfile == null && mWebContentsObserver == null && mWebContentsDelegate == null;

        mProfile = profile;
        mWebContents = webContents;
        mSheetContent = sheetContent;
        createWebContentsObserver();
        createWebContentsDelegate();
        mSheetContent.attachWebContents(mWebContents, contentView, mWebContentsDelegate);
    }

    /** Add observer to be notified of ephemeral tab events. */
    void addObserver(EphemeralTabObserver ephemeralTabObserver) {
        mObservers.addObserver(ephemeralTabObserver);
    }

    /** Remove observer. */
    void removeObserver(EphemeralTabObserver ephemeralTabObserver) {
        mObservers.removeObserver(ephemeralTabObserver);
    }

    /** Clear observers. */
    void clearObservers() {
        mObservers.clear();
    }

    /** Notify observers on toolbar creation. */
    public void onToolbarCreated(ViewGroup toolbarView) {
        RewindableIterator<EphemeralTabObserver> observersIterator =
                mObservers.rewindableIterator();
        while (observersIterator.hasNext()) {
            observersIterator.next().onToolbarCreated(toolbarView);
        }
    }

    /** Notify observers on navigation start. */
    public void onNavigationStarted(GURL clickedUrl) {
        RewindableIterator<EphemeralTabObserver> observersIterator =
                mObservers.rewindableIterator();
        while (observersIterator.hasNext()) {
            observersIterator.next().onNavigationStarted(clickedUrl);
        }
    }

    /** Notify observers on title set. */
    public void onTitleSet(EphemeralTabSheetContent sheetContent, String title) {
        RewindableIterator<EphemeralTabObserver> observersIterator =
                mObservers.rewindableIterator();
        while (observersIterator.hasNext()) {
            observersIterator.next().onTitleSet(sheetContent, title);
        }
    }

    /** Loads a new URL into the tab and makes it visible. */
    void requestShowContent(GURL url, String title) {
        loadUrl(url);
        mSheetContent.updateTitle(title);
        mBottomSheetController.requestShowContent(mSheetContent, true);
    }

    private void loadUrl(GURL url) {
        mWebContents.getNavigationController().loadUrl(new LoadUrlParams(url.getSpec()));
    }

    private void createWebContentsObserver() {
        assert mWebContentsObserver == null;
        mWebContentsObserver =
                new WebContentsObserver(mWebContents) {
                    /** Whether the currently loaded page is an error (interstitial) page. */
                    private boolean mIsOnErrorPage;

                    private GURL mCurrentUrl;

                    @Override
                    public void loadProgressChanged(float progress) {
                        if (mSheetContent != null) mSheetContent.setProgress(progress);
                    }

                    @Override
                    public void didStartNavigationInPrimaryMainFrame(NavigationHandle navigation) {
                        if (!navigation.isSameDocument()) {
                            GURL url = navigation.getUrl();
                            if (url.equals(mCurrentUrl)) return;

                            // The link Back to Safety on the interstitial page will go to the
                            // previous page. If there is no previous page, i.e. previous page is
                            // NTP, the preview tab will be closed.
                            if (mIsOnErrorPage && UrlUtilities.isNtpUrl(url)) {
                                mBottomSheetController.hideContent(
                                        mSheetContent, /* animate= */ true);
                                mCurrentUrl = null;
                                return;
                            }

                            onNavigationStarted(url);

                            mCurrentUrl = url;
                            mFaviconLoader.loadFavicon(
                                    url, (drawable) -> onFaviconAvailable(drawable), mProfile);
                        }
                    }

                    @Override
                    public void titleWasSet(String title) {
                        mSheetContent.updateTitle(title);
                        onTitleSet(mSheetContent, title);
                    }

                    @Override
                    public void didFinishNavigationInPrimaryMainFrame(NavigationHandle navigation) {
                        if (navigation.hasCommitted()) {
                            mIsOnErrorPage = navigation.isErrorPage();
                            mSheetContent.updateURL(mWebContents.get().getVisibleUrl());
                        } else if (navigation.isDownload()) {
                            // Not viewable contents such as download. Show a toast and close the
                            // tab.
                            Toast.makeText(
                                            ContextUtils.getApplicationContext(),
                                            R.string.ephemeral_tab_sheet_not_viewable,
                                            Toast.LENGTH_SHORT)
                                    .show();
                            mBottomSheetController.hideContent(mSheetContent, /* animate= */ true);
                        }
                    }
                };
    }

    private void onFaviconAvailable(Drawable drawable) {
        if (mSheetContent != null) mSheetContent.startFaviconAnimation(drawable);
    }

    private void createWebContentsDelegate() {
        assert mWebContentsDelegate == null;
        mWebContentsDelegate =
                new WebContentsDelegateAndroid() {
                    @Override
                    public void visibleSSLStateChanged() {
                        if (mSheetContent == null) return;
                        int securityLevel =
                                SecurityStateModel.getSecurityLevelForWebContents(mWebContents);
                        mSheetContent.setSecurityIcon(getSecurityIconResource(securityLevel));
                        mSheetContent.updateURL(mWebContents.getVisibleUrl());
                    }

                    @Override
                    public void openNewTab(
                            GURL url,
                            String extraHeaders,
                            ResourceRequestBody postData,
                            int disposition,
                            boolean isRendererInitiated) {
                        // We never open a separate tab when navigating in a preview tab.
                        loadUrl(url);
                    }

                    @Override
                    public boolean shouldCreateWebContents(GURL targetUrl) {
                        loadUrl(targetUrl);
                        return false;
                    }

                    @Override
                    public void loadingStateChanged(boolean shouldShowLoadingUI) {
                        boolean isLoading = mWebContents != null && mWebContents.isLoading();
                        if (isLoading) {
                            if (mSheetContent == null) return;
                            mSheetContent.setProgress(0);
                            mSheetContent.setProgressVisible(true);
                        } else {
                            // Hides the Progress Bar after a delay to make sure it is rendered for
                            // at least a few frames, otherwise its completion won't be visually
                            // noticeable.
                            new Handler()
                                    .postDelayed(
                                            () -> {
                                                if (mSheetContent != null) {
                                                    mSheetContent.setProgressVisible(false);
                                                }
                                            },
                                            HIDE_PROGRESS_BAR_DELAY_MS);
                        }
                    }

                    @Override
                    public int getTopControlsHeight() {
                        return mTopControlsHeightDp;
                    }
                };
    }

    private static @DrawableRes int getSecurityIconResource(
            @ConnectionSecurityLevel int securityLevel) {
        switch (securityLevel) {
            case ConnectionSecurityLevel.NONE:
            case ConnectionSecurityLevel.WARNING:
                return R.drawable.omnibox_info;
            case ConnectionSecurityLevel.DANGEROUS:
                return R.drawable.omnibox_not_secure_warning;
            case ConnectionSecurityLevel.SECURE_WITH_POLICY_INSTALLED_CERT:
            case ConnectionSecurityLevel.SECURE:
                return R.drawable.omnibox_https_valid;
            default:
                assert false;
        }
        return 0;
    }

    /** Destroys the objects used for the current preview tab. */
    void destroyContent() {
        if (mWebContentsObserver != null) {
            mWebContentsObserver.destroy();
            mWebContentsObserver = null;
        }
        mWebContentsDelegate = null;
        mWebContents = null;
        mSheetContent = null;
        mProfile = null;
        clearObservers();
    }
}