chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java

// Copyright 2015 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.customtabs;

import static androidx.annotation.VisibleForTesting.PRIVATE;
import static androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK;
import static androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Browser;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSessionToken;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.IntentUtils;
import org.chromium.base.cached_flags.AllCachedFieldTrialParameters;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.BackupSigninProcessor;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.LaunchIntentDispatcher;
import org.chromium.chrome.browser.app.metrics.LaunchCauseMetrics;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider.CustomTabsUiType;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.customtabs.dependency_injection.BaseCustomTabActivityComponent;
import org.chromium.chrome.browser.customtabs.features.CustomTabNavigationBarController;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabHistoryIPHController;
import org.chromium.chrome.browser.dependency_injection.ChromeActivityCommonsModule;
import org.chromium.chrome.browser.firstrun.FirstRunSignInProcessor;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fonts.FontPreloader;
import org.chromium.chrome.browser.history.HistoryManager;
import org.chromium.chrome.browser.history.HistoryManagerUtils;
import org.chromium.chrome.browser.history.HistoryTabHelper;
import org.chromium.chrome.browser.infobar.InfoBarContainer;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.night_mode.NightModeStateProvider;
import org.chromium.chrome.browser.page_info.ChromePageInfo;
import org.chromium.chrome.browser.page_info.ChromePageInfoHighlight;
import org.chromium.chrome.browser.searchwidget.SearchActivityClientImpl;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TrustedCdn;
import org.chromium.chrome.browser.ui.google_bottom_bar.GoogleBottomBarCoordinator;
import org.chromium.components.page_info.PageInfoController.OpenedFromSource;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.util.ColorUtils;

/** The activity for custom tabs. It will be launched on top of a client's task. */
public class CustomTabActivity extends BaseCustomTabActivity {
    private CustomTabsSessionToken mSession;

    private final CustomTabsConnection mConnection = CustomTabsConnection.getInstance();
    private int mNumOmniboxNavigationEventsPerSession;

    /** Contains all the parameters of the EXPERIMENTS_FOR_AGSA feature. */
    public static final AllCachedFieldTrialParameters EXPERIMENTS_FOR_AGSA_PARAMS =
            ChromeFeatureList.newAllCachedFieldTrialParameters(
                    ChromeFeatureList.EXPERIMENTS_FOR_AGSA);

    /** Prevents Tapjacking on T-. See crbug.com/1430867 */
    private static final boolean sPreventTouches =
            Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU;

    private CustomTabsOpenTimeRecorder mOpenTimeRecorder;

    /**
     * The last MotionEvent object blocked due to the activity being in paused state.  We're
     * interested in MotionEvent#ACTION_DOWN which is likely the very first event received when
     * multi-window mode is entered. We inject this one after the activity is resumed (or
     * it regains the focus) in order to recover the corresponding user gesture which otherwise
     * would have gone missing.
     */
    private MotionEvent mBlockedEvent;

    private CustomTabActivityTabProvider.Observer mTabChangeObserver =
            new CustomTabActivityTabProvider.Observer() {
                @Override
                public void onInitialTabCreated(@NonNull Tab tab, int mode) {
                    onTabInitOrSwapped(tab);
                }

                @Override
                public void onTabSwapped(@NonNull Tab tab) {
                    onTabInitOrSwapped(tab);
                }

                @Override
                public void onAllTabsClosed() {
                    resetPostMessageHandlersForCurrentSession();
                }
            };

    private void onTabInitOrSwapped(@Nullable Tab tab) {
        resetPostMessageHandlersForCurrentSession();
        if (tab != null) maybeCreateHistoryTabHelper(tab);
    }

    private void maybeCreateHistoryTabHelper(Tab tab) {
        if (!HistoryManager.isAppSpecificHistoryEnabled() || mIntentDataProvider.isAuthTab()) {
            return;
        }
        String appId = mIntentDataProvider.getClientPackageNameIdentitySharing();
        if (appId != null) HistoryTabHelper.from(tab).setAppId(appId, tab.getWebContents());
    }

    @Override
    protected BaseCustomTabActivityComponent createComponent(
            ChromeActivityCommonsModule commonsModule) {
        BaseCustomTabActivityComponent component = super.createComponent(commonsModule);
        mOpenTimeRecorder =
                new CustomTabsOpenTimeRecorder(
                        getLifecycleDispatcher(),
                        mNavigationController,
                        this::isFinishing,
                        mIntentDataProvider);
        return component;
    }

    @Override
    protected Drawable getBackgroundDrawable() {
        int initialBackgroundColor =
                mIntentDataProvider.getColorProvider().getInitialBackgroundColor();
        if (mIntentDataProvider.isTrustedIntent() && initialBackgroundColor != Color.TRANSPARENT) {
            return new ColorDrawable(initialBackgroundColor);
        } else {
            return super.getBackgroundDrawable();
        }
    }

    @Override
    protected void changeBackgroundColorForResizing() {
        if (!mBaseCustomTabRootUiCoordinator.changeBackgroundColorForResizing()) {
            super.changeBackgroundColorForResizing();
        }
    }

    @Override
    public void performPreInflationStartup() {
        super.performPreInflationStartup();
        mTabProvider.addObserver(mTabChangeObserver);
        // We might have missed an onInitialTabCreated event.
        onTabInitOrSwapped(mTabProvider.getTab());

        mSession = mIntentDataProvider.getSession();

        CustomTabNavigationBarController.update(getWindow(), mIntentDataProvider, this);
    }

    @Override
    public void performPostInflationStartup() {
        super.performPostInflationStartup();

        FontPreloader.getInstance().onPostInflationStartupCustomTabActivity();

        mRootUiCoordinator.getStatusBarColorController().updateStatusBarColor();

        // Properly attach tab's InfoBarContainer to the view hierarchy if the tab is already
        // attached to a ChromeActivity, as the main tab might have been initialized prior to
        // inflation.
        if (mTabProvider.getTab() != null) {
            ViewGroup bottomContainer = findViewById(R.id.bottom_container);
            InfoBarContainer.get(mTabProvider.getTab()).setParentView(bottomContainer);
        }

        // Setting task title and icon to be null will preserve the client app's title and icon.
        setTaskDescription(
                new ActivityManager.TaskDescription(
                        null, null, mIntentDataProvider.getColorProvider().getToolbarColor()));

        GoogleBottomBarCoordinator googleBottomBarCoordinator =
                mBaseCustomTabRootUiCoordinator.getGoogleBottomBarCoordinator();

        if (googleBottomBarCoordinator != null) {
            View googleBottomBarView = googleBottomBarCoordinator.createGoogleBottomBarView();
            CustomTabBottomBarDelegate delegate = getComponent().resolveBottomBarDelegate();
            delegate.setBottomBarHeight(googleBottomBarCoordinator.getBottomBarHeightInPx());
            delegate.setKeepContentView(true);
            delegate.setBottomBarContentView(googleBottomBarView);
            delegate.setCustomButtonsUpdater(googleBottomBarCoordinator::updateBottomBarButton);
        }

        getComponent().resolveBottomBarDelegate().showBottomBarIfNecessary();
    }

    @Override
    protected void onFirstDrawComplete() {
        super.onFirstDrawComplete();

        FontPreloader.getInstance().onFirstDrawCustomTabActivity();
    }

    @Override
    public void finishNativeInitialization() {
        if (!mIntentDataProvider.isInfoPage()) {
            FirstRunSignInProcessor.openSyncSettingsIfScheduled(this);
            BackupSigninProcessor.start(this);
        }

        mConnection.showSignInToastIfNecessary(mSession, getIntent());

        new CustomTabTrustedCdnPublisherUrlVisibility(
                getWindowAndroid(),
                getLifecycleDispatcher(),
                () -> {
                    if (ChromeFeatureList.isEnabled(
                            ChromeFeatureList.CCT_EXTEND_TRUSTED_CDN_PUBLISHER)) {
                        return mConnection.isTrustedCdnPublisherUrlPackage(
                                mIntentDataProvider.getClientPackageName());
                    }
                    String urlPackage = mConnection.getTrustedCdnPublisherUrlPackage();
                    return urlPackage != null
                            && urlPackage.equals(
                                    mConnection.getClientPackageNameForSession(mSession));
                });
        super.finishNativeInitialization();
    }

    @Override
    protected void handleFinishAndClose(boolean warmupOnFinish) {
        if (mOpenTimeRecorder != null) mOpenTimeRecorder.updateCloseCause();
        super.handleFinishAndClose(warmupOnFinish);
    }

    @Override
    protected void onUserLeaveHint() {
        if (mOpenTimeRecorder != null) mOpenTimeRecorder.onUserLeaveHint();
        super.onUserLeaveHint();
    }

    private void resetPostMessageHandlersForCurrentSession() {
        Tab tab = mTabProvider.getTab();
        WebContents webContents = tab == null ? null : tab.getWebContents();
        mConnection.resetPostMessageHandlerForSession(
                mIntentDataProvider.getSession(), webContents);
    }

    @Override
    public String getPackageName() {
        if (mShouldOverridePackage
                && mIntentDataProvider instanceof CustomTabIntentDataProvider intentDataProvider) {
            return intentDataProvider.getInsecureClientPackageNameForOnFinishAnimation();
        }
        return super.getPackageName();
    }

    @Override
    public boolean onOptionsItemSelected(int itemId, @Nullable Bundle menuItemData) {
        int menuIndex =
                CustomTabAppMenuPropertiesDelegate.getIndexOfMenuItemFromBundle(menuItemData);
        if (menuIndex >= 0) {
            ((CustomTabIntentDataProvider) mIntentDataProvider)
                    .clickMenuItemWithUrlAndTitle(
                            this,
                            menuIndex,
                            getActivityTab().getUrl().getSpec(),
                            getActivityTab().getTitle());
            RecordUserAction.record("CustomTabsMenuCustomMenuItem");
            return true;
        }

        return super.onOptionsItemSelected(itemId, menuItemData);
    }

    @Override
    public boolean onMenuOrKeyboardAction(int id, boolean fromMenu) {
        if (id == R.id.bookmark_this_page_id) {
            mTabBookmarkerSupplier.get().addOrEditBookmark(getActivityTab());
            RecordUserAction.record("MobileMenuAddToBookmarks");
            return true;
        } else if (id == R.id.open_in_browser_id) {
            // Need to get tab before calling openCurrentUrlInBrowser or else it will be null.
            Tab tab = mTabProvider.getTab();
            if (mNavigationController.openCurrentUrlInBrowser()) {
                RecordUserAction.record("CustomTabsMenuOpenInChrome");
                WebContents webContents = tab == null ? null : tab.getWebContents();
                mConnection.notifyOpenInBrowser(mSession, webContents);
            }
            return true;
        } else if (id == R.id.info_menu_id) {
            Tab tab = getTabModelSelector().getCurrentTab();
            if (tab == null) return false;
            String publisher = TrustedCdn.getContentPublisher(tab);
            new ChromePageInfo(
                            getModalDialogManagerSupplier(),
                            publisher,
                            OpenedFromSource.MENU,
                            mRootUiCoordinator.getMerchantTrustSignalsCoordinatorSupplier()::get,
                            mRootUiCoordinator.getEphemeralTabCoordinatorSupplier(),
                            getTabCreator(getCurrentTabModel().isIncognito()))
                    .show(tab, ChromePageInfoHighlight.noHighlight());
            return true;
        } else if (id == R.id.open_history_menu_id) {
            // The menu is visible only when the app-specific history is enabled. Assert that.
            assert HistoryManager.isAppSpecificHistoryEnabled();
            HistoryManagerUtils.showAppSpecificHistoryManager(
                    this,
                    getTabModelSelector().isIncognitoSelected(),
                    mIntentDataProvider.getClientPackageNameIdentitySharing());

            CustomTabHistoryIPHController historyIPH =
                    mBaseCustomTabRootUiCoordinator.getHistoryIPHController();
            if (historyIPH != null) {
                historyIPH.notifyUserEngaged();
            }
            return true;
        }
        return super.onMenuOrKeyboardAction(id, fromMenu);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (sPreventTouches && shouldPreventTouch(ev)) {
            // Discard the events which may be trickling down from an overlay activity above.
            return true;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void finish() {
        RecordHistogram.recordLinearCountHistogram(
                "CustomTabs.Omnibox.NumNavigationsPerSession",
                mNumOmniboxNavigationEventsPerSession,
                1,
                10,
                10);

        super.finish();
    }

    @VisibleForTesting(otherwise = PRIVATE)
    boolean shouldPreventTouch(MotionEvent ev) {
        if (ApplicationStatus.getStateForActivity(this) == ActivityState.RESUMED) return false;
        mBlockedEvent = ev;
        return true;
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        // No need to do the following from Q and onward where multi-resume state is supported
        // in split screen mode.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return;

        if (hasFocus
                && mBlockedEvent != null
                && MultiWindowUtils.getInstance().isInMultiWindowMode(this)) {
            mBlockedEvent.setAction(MotionEvent.ACTION_DOWN);
            super.dispatchTouchEvent(mBlockedEvent); // Inject the blocked event
            mBlockedEvent = null;
        }
    }

    /**
     * Show the web page with CustomTabActivity, without any navigation control. Used in showing the
     * terms of services page or help pages for Chrome.
     *
     * @param context The current activity context.
     * @param url The url of the web page.
     */
    public static void showInfoPage(Context context, String url) {
        // TODO(xingliu): The title text will be the html document title, figure out if we want to
        // use Chrome strings here as EmbedContentViewActivity does.
        CustomTabsIntent customTabIntent =
                new CustomTabsIntent.Builder()
                        .setShowTitle(true)
                        .setColorScheme(
                                ColorUtils.inNightMode(context)
                                        ? COLOR_SCHEME_DARK
                                        : COLOR_SCHEME_LIGHT)
                        .build();
        customTabIntent.intent.setData(Uri.parse(url));

        Intent intent =
                LaunchIntentDispatcher.createCustomTabActivityIntent(
                        context, customTabIntent.intent);
        intent.setPackage(context.getPackageName());
        intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, CustomTabsUiType.INFO_PAGE);
        intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
        if (!(context instanceof Activity)) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        IntentUtils.addTrustedIntentExtras(intent);

        context.startActivity(intent);
    }

    @Override
    protected boolean requiresFirstRunToBeCompleted(Intent intent) {
        // Custom Tabs can be used to open Chrome help pages before the ToS has been accepted.
        if (CustomTabIntentDataProvider.isTrustedCustomTab(intent, mSession)
                && IntentUtils.safeGetIntExtra(
                                intent,
                                CustomTabIntentDataProvider.EXTRA_UI_TYPE,
                                CustomTabIntentDataProvider.CustomTabsUiType.DEFAULT)
                        == CustomTabIntentDataProvider.CustomTabsUiType.INFO_PAGE) {
            return false;
        }

        return super.requiresFirstRunToBeCompleted(intent);
    }

    @Override
    protected LaunchCauseMetrics createLaunchCauseMetrics() {
        return new CustomTabLaunchCauseMetrics(this);
    }

    public NightModeStateProvider getNightModeStateProviderForTesting() {
        return super.getNightModeStateProvider();
    }

    @Override
    protected void setDefaultTaskDescription() {
        // mIntentDataProvider is not ready when the super calls this method. So, we skip setting
        // the task description here, and do it in #performPostInflationStartup();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode != Activity.RESULT_OK) return;

        if (ChromeFeatureList.isEnabled(ChromeFeatureList.SEARCH_IN_CCT)
                && SearchActivityClientImpl.isOmniboxResult(requestCode, data)) {
            LoadUrlParams params =
                    SearchActivityClientImpl.getOmniboxResult(requestCode, resultCode, data);

            RecordHistogram.recordBooleanHistogram(
                    "CustomTabs.Omnibox.FocusResultedInNavigation", params != null);

            if (params == null) return;

            mNumOmniboxNavigationEventsPerSession++;
            // Yield to give the called activity time to close.
            // Loading URL directly will result in Activity closing after URL loading completes.
            PostTask.postTask(TaskTraits.UI_DEFAULT, () -> mTabProvider.getTab().loadUrl(params));
        }

        if (HistoryManager.isAppSpecificHistoryEnabled()
                && requestCode == HistoryManagerUtils.HISTORY_REQUEST_CODE) {
            LoadUrlParams params =
                    new LoadUrlParams(
                            data.getData().toString(),
                            IntentHandler.getTransitionTypeFromIntent(data, PageTransition.LINK));
            mTabProvider.getTab().loadUrl(params);
        }
    }
}