chromium/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/ActivityTabWebContentsDelegateAndroid.java

// Copyright 2019 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.app.tab_activity_glue;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Rect;
import android.media.AudioManager;
import android.view.KeyEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.blink.mojom.DisplayMode;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.SwipeRefreshHandler;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.CompositorViewHolder;
import org.chromium.chrome.browser.contextmenu.ContextMenuUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
import org.chromium.chrome.browser.init.ChromeActivityNativeDelegate;
import org.chromium.chrome.browser.media.PictureInPicture;
import org.chromium.chrome.browser.night_mode.WebContentsDarkModeController;
import org.chromium.chrome.browser.policy.PolicyAuditor;
import org.chromium.chrome.browser.policy.PolicyAuditor.AuditEvent;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabWebContentsDelegateAndroid;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ResourceRequestBody;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modaldialog.SimpleModalDialogController;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;

/**
 * {@link WebContentsDelegateAndroid} that interacts with {@link Activity} and those
 * of the lifetime of the activity to process requests from underlying {@link WebContents}
 * for a given {@link Tab}.
 */
public class ActivityTabWebContentsDelegateAndroid extends TabWebContentsDelegateAndroid {
    private static final String TAG = "ActivityTabWCDA";

    private final ArrayMap<WebContents, GURL> mWebContentsUrlMapping = new ArrayMap<>();

    private final Tab mTab;

    @Nullable private Activity mActivity;
    private final ChromeActivityNativeDelegate mChromeActivityNativeDelegate;
    private final boolean mIsCustomTab;
    private final BrowserControlsStateProvider mBrowserControlsStateProvider;
    private final FullscreenManager mFullscreenManager;
    private final TabCreatorManager mTabCreatorManager;
    private final Supplier<TabModelSelector> mTabModelSelectorSupplier;
    private final Supplier<CompositorViewHolder> mCompositorViewHolderSupplier;
    private final Supplier<ModalDialogManager> mModalDialogManagerSupplier;

    public ActivityTabWebContentsDelegateAndroid(
            Tab tab,
            Activity activity,
            ChromeActivityNativeDelegate chromeActivityNativeDelegate,
            boolean isCustomTab,
            BrowserControlsStateProvider browserControlsStateProvider,
            FullscreenManager fullscreenManager,
            TabCreatorManager tabCreatorManager,
            @NonNull Supplier<TabModelSelector> tabModelSelectorSupplier,
            @NonNull Supplier<CompositorViewHolder> compositorViewHolderSupplier,
            @NonNull Supplier<ModalDialogManager> modalDialogManagerSupplier) {
        mTab = tab;
        mActivity = activity;
        mChromeActivityNativeDelegate = chromeActivityNativeDelegate;
        mIsCustomTab = isCustomTab;
        mBrowserControlsStateProvider = browserControlsStateProvider;
        mFullscreenManager = fullscreenManager;
        mTabCreatorManager = tabCreatorManager;
        mTabModelSelectorSupplier = tabModelSelectorSupplier;
        mCompositorViewHolderSupplier = compositorViewHolderSupplier;
        mModalDialogManagerSupplier = modalDialogManagerSupplier;

        tab.addObserver(
                new EmptyTabObserver() {
                    @Override
                    public void onActivityAttachmentChanged(
                            Tab tab, @Nullable WindowAndroid window) {
                        if (window == null) mActivity = null;
                    }

                    @Override
                    public void onDestroyed(Tab tab) {
                        tab.removeObserver(this);
                    }
                });
    }

    @Override
    public void openNewTab(
            GURL url,
            String extraHeaders,
            ResourceRequestBody postData,
            int disposition,
            boolean isRendererInitiated) {
        // New tabs are handled by the tab model (see
        // TabWebContentsDelegateAndroid::OpenURLFromTab().
        assert false;
    }

    @Override
    public int getDisplayMode() {
        return DisplayMode.BROWSER;
    }

    @Override
    public void showRepostFormWarningDialog() {
        // When the dialog is visible, keeping the refresh animation active
        // in the background is distracting and unnecessary (and likely to
        // jank when the dialog is shown).
        SwipeRefreshHandler handler = SwipeRefreshHandler.get(mTab);
        if (handler != null) handler.reset();

        showRepostFormWarningTabModalDialog();
    }

    @Override
    public void webContentsCreated(
            WebContents sourceWebContents,
            long openerRenderProcessId,
            long openerRenderFrameId,
            String frameName,
            GURL targetUrl,
            WebContents newWebContents) {
        // The URL can't be taken from the WebContents if it's paused.  Save it for later.
        assert !mWebContentsUrlMapping.containsKey(newWebContents);
        mWebContentsUrlMapping.put(newWebContents, targetUrl);
    }

    @Override
    public boolean isFullscreenForTabOrPending() {
        return mFullscreenManager != null
                ? mFullscreenManager.getPersistentFullscreenMode()
                : false;
    }

    @Override
    protected boolean shouldResumeRequestsForCreatedWindow() {
        return true;
    }

    @Override
    protected boolean addNewContents(
            WebContents sourceWebContents,
            WebContents webContents,
            int disposition,
            Rect initialPosition,
            boolean userGesture) {
        assert mWebContentsUrlMapping.containsKey(webContents);

        TabCreator tabCreator = mTabCreatorManager.getTabCreator(mTab.isIncognito());
        assert tabCreator != null;

        // Grab the URL, which might not be available via the Tab.
        GURL url = mWebContentsUrlMapping.remove(webContents);

        // Skip opening a new Tab if it doesn't make sense.
        if (mTab.isClosing()) return false;

        // Creating new Tabs asynchronously requires starting a new Activity to create the Tab,
        // so the Tab returned will always be null.  There's no way to know synchronously
        // whether the Tab is created, so assume it's always successful.
        boolean success =
                tabCreator.createTabWithWebContents(
                        mTab, webContents, TabLaunchType.FROM_LONGPRESS_FOREGROUND, url);

        if (success) {
            if (disposition == WindowOpenDisposition.NEW_FOREGROUND_TAB) {
                RecordUserAction.record("LinkNavigationOpenedInForegroundTab");
            } else if (disposition == WindowOpenDisposition.NEW_POPUP) {
                PolicyAuditor auditor = AppHooks.get().getPolicyAuditor();
                if (auditor != null) {
                    auditor.notifyAuditEvent(
                            ContextUtils.getApplicationContext(),
                            AuditEvent.OPEN_POPUP_URL_SUCCESS,
                            url.getSpec(),
                            "");
                }
            }
        }

        return success;
    }

    @Override
    public void activateContents() {
        if (mActivity == null) {
            Log.e(TAG, "Activity not set activateContents().  Bailing out.");
            return;
        }

        if (mChromeActivityNativeDelegate.isActivityFinishingOrDestroyed()) {
            Log.e(TAG, "Activity destroyed before calling activateContents().  Bailing out.");
            return;
        }
        if (!mTab.isInitialized()) {
            Log.e(TAG, "Tab not initialized before calling activateContents().  Bailing out.");
            return;
        }

        // Do nothing if the tab can currently be interacted with by the user.
        if (mTab.isUserInteractable()) return;

        TabModel model = mTabModelSelectorSupplier.get().getModel(mTab.isIncognito());
        int index = model.indexOf(mTab);
        if (index == TabModel.INVALID_TAB_INDEX) return;
        TabModelUtils.setIndex(model, index);

        // Do nothing if the mActivity is visible (STOPPED is the only valid invisible state as we
        // explicitly check isActivityFinishingOrDestroyed above).
        if (ApplicationStatus.getStateForActivity(mActivity) == ActivityState.STOPPED) {
            bringActivityToForeground();
        }
    }

    /** Brings chrome's Activity to foreground, if it is not so. */
    protected void bringActivityToForeground() {
        // This intent is sent in order to get the activity back to the foreground if it was
        // not already. The previous call will activate the right tab in the context of the
        // TabModel but will only show the tab to the user if Chrome was already in the
        // foreground.
        // The intent is getting the tabId mostly because it does not cost much to do so.
        // When receiving the intent, the tab associated with the tabId should already be
        // active.
        // Note that calling only the intent in order to activate the tab is slightly slower
        // because it will change the tab when the intent is handled, which happens after
        // Chrome gets back to the foreground.
        Intent newIntent =
                IntentHandler.createTrustedBringTabToFrontIntent(
                        mTab.getId(), IntentHandler.BringToFrontSource.ACTIVATE_TAB);
        if (newIntent != null) {
            newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            ContextUtils.getApplicationContext().startActivity(newIntent);
        }
    }

    @Override
    public boolean takeFocus(boolean reverse) {
        if (mActivity == null) return false;
        if (reverse) {
            View menuButton = mActivity.findViewById(R.id.menu_button);
            if (menuButton != null && menuButton.isShown()) {
                return menuButton.requestFocus();
            }

            View tabSwitcherButton = mActivity.findViewById(R.id.tab_switcher_button);
            if (tabSwitcherButton != null && tabSwitcherButton.isShown()) {
                return tabSwitcherButton.requestFocus();
            }
        } else {
            View urlBar = mActivity.findViewById(R.id.url_bar);
            if (urlBar != null) return urlBar.requestFocus();
        }
        return false;
    }

    @Override
    public void handleKeyboardEvent(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN && mActivity != null) {
            if (mActivity.onKeyDown(event.getKeyCode(), event)) return;

            // Handle the Escape key here (instead of in KeyboardShortcuts.java), so it doesn't
            // interfere with other parts of the activity (e.g. the URL bar).
            if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) {
                WebContents wc = mTab.getWebContents();
                if (wc != null) wc.stop();
                return;
            }
        }
        handleMediaKey(event);
    }

    /**
     * Redispatches unhandled media keys. This allows bluetooth headphones with play/pause or
     * other buttons to function correctly.
     */
    private void handleMediaKey(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.KEYCODE_MUTE:
            case KeyEvent.KEYCODE_HEADSETHOOK:
            case KeyEvent.KEYCODE_MEDIA_PLAY:
            case KeyEvent.KEYCODE_MEDIA_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_STOP:
            case KeyEvent.KEYCODE_MEDIA_NEXT:
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
            case KeyEvent.KEYCODE_MEDIA_REWIND:
            case KeyEvent.KEYCODE_MEDIA_RECORD:
            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
            case KeyEvent.KEYCODE_MEDIA_CLOSE:
            case KeyEvent.KEYCODE_MEDIA_EJECT:
            case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
                AudioManager am =
                        (AudioManager)
                                ContextUtils.getApplicationContext()
                                        .getSystemService(Context.AUDIO_SERVICE);
                am.dispatchMediaKeyEvent(e);
                break;
            default:
                break;
        }
    }

    @Override
    protected void setOverlayMode(boolean useOverlayMode) {
        if (mCompositorViewHolderSupplier.hasValue()) {
            mCompositorViewHolderSupplier.get().setOverlayMode(useOverlayMode);
        }
    }

    @Override
    public int getTopControlsHeight() {
        return mBrowserControlsStateProvider != null
                ? mBrowserControlsStateProvider.getTopControlsHeight()
                : 0;
    }

    @Override
    public int getTopControlsMinHeight() {
        return mBrowserControlsStateProvider != null
                ? mBrowserControlsStateProvider.getTopControlsMinHeight()
                : 0;
    }

    @Override
    public int getBottomControlsHeight() {
        return mBrowserControlsStateProvider != null
                ? mBrowserControlsStateProvider.getBottomControlsHeight()
                : 0;
    }

    @Override
    public int getBottomControlsMinHeight() {
        return mBrowserControlsStateProvider != null
                ? mBrowserControlsStateProvider.getBottomControlsMinHeight()
                : 0;
    }

    @Override
    public boolean shouldAnimateBrowserControlsHeightChanges() {
        return mBrowserControlsStateProvider != null
                && mBrowserControlsStateProvider.shouldAnimateBrowserControlsHeightChanges();
    }

    @Override
    public boolean controlsResizeView() {
        return mCompositorViewHolderSupplier.hasValue()
                && mCompositorViewHolderSupplier.get().controlsResizeView();
    }

    @Override
    public int getVirtualKeyboardHeight() {
        if (mActivity == null) return 0;

        View rootView = mActivity.getWindow().getDecorView().getRootView();
        return mTab.getWindowAndroid().getKeyboardDelegate().calculateTotalKeyboardHeight(rootView);
    }

    @Override
    public void enterFullscreenModeForTab(boolean prefersNavigationBar, boolean prefersStatusBar) {
        if (mFullscreenManager != null) {
            mFullscreenManager.onEnterFullscreen(
                    mTab, new FullscreenOptions(prefersNavigationBar, prefersStatusBar));
        }
    }

    @Override
    public void fullscreenStateChangedForTab(
            boolean prefersNavigationBar, boolean prefersStatusBar) {
        // State-only changes are useful for recursive fullscreen activation. Early out if
        // fullscreen mode is not on.
        if (mFullscreenManager == null || !mFullscreenManager.getPersistentFullscreenMode()) return;
        mFullscreenManager.onEnterFullscreen(
                mTab, new FullscreenOptions(prefersNavigationBar, prefersStatusBar));
    }

    @Override
    public void exitFullscreenModeForTab() {
        if (mFullscreenManager != null) mFullscreenManager.onExitFullscreen(mTab);
    }

    @Override
    protected boolean isPictureInPictureEnabled() {
        return mActivity != null
                ? PictureInPicture.isEnabled(mActivity.getApplicationContext())
                : false;
    }

    @Override
    protected boolean isNightModeEnabled() {
        return mActivity != null ? ColorUtils.inNightMode(mActivity) : false;
    }

    @Override
    protected boolean isForceDarkWebContentEnabled() {
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.FORCE_WEB_CONTENTS_DARK_MODE)) {
            return true;
        }
        if (!ChromeFeatureList.isEnabled(
                ChromeFeatureList.DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING)) {
            return false;
        }
        WebContents webContents = mTab.getWebContents();
        if (webContents == null) {
            return false;
        }
        Profile profile = mTab.getProfile();
        return isNightModeEnabled()
                && WebContentsDarkModeController.isEnabledForUrl(
                        profile, webContents.getVisibleUrl());
    }

    @Override
    protected boolean isCustomTab() {
        return mIsCustomTab;
    }

    private void showRepostFormWarningTabModalDialog() {
        // As a rule, showRepostFormWarningDialog should only be called on active tabs, as it's
        // called right after WebContents::Activate. But in various corner cases, that
        // activation may fail.
        if (mActivity == null || !mTab.isUserInteractable()) {
            mTab.getWebContents().getNavigationController().cancelPendingReload();
            return;
        }

        ModalDialogManager modalDialogManager = mModalDialogManagerSupplier.get();
        ModalDialogProperties.Controller dialogController =
                new SimpleModalDialogController(
                        modalDialogManager,
                        (Integer dismissalCause) -> {
                            if (!mTab.isInitialized()) return;
                            switch (dismissalCause) {
                                case DialogDismissalCause.POSITIVE_BUTTON_CLICKED:
                                    mTab.getWebContents()
                                            .getNavigationController()
                                            .continuePendingReload();
                                    break;
                                case DialogDismissalCause.ACTIVITY_DESTROYED:
                                case DialogDismissalCause.TAB_DESTROYED:
                                    // Intentionally ignored as the tab object is gone.
                                    break;
                                default:
                                    mTab.getWebContents()
                                            .getNavigationController()
                                            .cancelPendingReload();
                                    break;
                            }
                        });

        Resources resources = mActivity.getResources();
        PropertyModel dialogModel =
                new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                        .with(ModalDialogProperties.CONTROLLER, dialogController)
                        .with(
                                ModalDialogProperties.TITLE,
                                resources,
                                R.string.http_post_warning_title)
                        .with(
                                ModalDialogProperties.MESSAGE_PARAGRAPH_1,
                                resources.getString(R.string.http_post_warning))
                        .with(
                                ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                resources,
                                R.string.http_post_warning_resend)
                        .with(
                                ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                resources,
                                R.string.cancel)
                        .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
                        .build();

        modalDialogManager.showDialog(dialogModel, ModalDialogManager.ModalDialogType.TAB, true);
    }

    @Override
    protected boolean isModalContextMenu() {
        return !ContextMenuUtils.usePopupContextMenuForContext(mActivity);
    }
}