chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CloseButtonNavigator.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.customtabs;

import static org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController.FinishReason.USER_NAVIGATION;

import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController.FinishReason;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.customtabs.features.minimizedcustomtab.CustomTabMinimizationManagerHolder;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationHistory;
import org.chromium.content_public.browser.WebContents;

import java.util.function.Predicate;

import javax.inject.Inject;

/**
 * Closes the tab or navigates back when the Custom Tabs close button is pressed. The algorithm
 * depends on whether the tab is a child tab - {@link Tab#getParentId()} != Tab.INVALID_TAB_ID.
 *
 * If the tab is not a child tab:
 * Navigates to the most recent page which matches a criteria. We call this page the landing page.
 * For instance, Trusted Web Activities show the close button when the user has left the verified
 * origin. If the user then presses the close button, we want to navigate back to the verified
 * origin instead of closing the Activity.
 *
 * If the tab is a child tab:
 * Webapps: Closes the current tab
 * Other: Same algorithm as non-child tabs.
 *
 * Thread safety: Should only be called on UI thread.
 * Native: Requires native.
 */
@ActivityScope
public class CloseButtonNavigator {
    @Nullable private Predicate<String> mLandingPagePredicate;
    private final CustomTabActivityTabController mTabController;
    private final CustomTabActivityTabProvider mTabProvider;
    private final CustomTabMinimizationManagerHolder mMinimizationManagerHolder;
    private final boolean mButtonClosesChildTab;

    @Inject
    public CloseButtonNavigator(
            CustomTabActivityTabController tabController,
            CustomTabActivityTabProvider tabProvider,
            BrowserServicesIntentDataProvider intentDataProvider,
            CustomTabMinimizationManagerHolder minimizationManagerHolder) {
        mTabController = tabController;
        mTabProvider = tabProvider;
        mButtonClosesChildTab = intentDataProvider.isWebappOrWebApkActivity();
        mMinimizationManagerHolder = minimizationManagerHolder;
    }

    /**
     * Sets the criteria for the page to go back to.
     *
     * @param criteria A predicate that returns true when given the URL of a landing page.
     */
    public void setLandingPageCriteria(Predicate<String> criteria) {
        assert mLandingPagePredicate == null : "Conflicting criteria for close button navigation.";

        mLandingPagePredicate = criteria;
    }

    private boolean isLandingPage(String url) {
        return mLandingPagePredicate != null && mLandingPagePredicate.test(url);
    }

    /** Handles navigation and Tab closures that should occur when the close button is pressed. */
    public void navigateOnClose(Callback<@FinishReason Integer> finishCallback) {
        // If the tab is a child tab and |mButtonClosesChildTab| == true, close the child tab.
        Tab currentTab = mTabProvider.getTab();
        boolean isFromChildTab =
                (currentTab != null && currentTab.getParentId() != Tab.INVALID_TAB_ID);
        if (isFromChildTab && mButtonClosesChildTab) {
            mTabController.closeTab();
            return;
        }

        // Search for a landing page in the history of the current Tab and then close if if none
        // found. Continue until a landing page is found or all Tabs are closed.
        int numTabsClosed = 0;
        while (mTabProvider.getTab() != null) {
            // See if there's a close button navigation in our current Tab.
            NavigationController navigationController = getNavigationController();
            if (navigationController != null && navigateSingleTab(getNavigationController())) {
                return;
            }

            // If the tab is currently minimized, finishing the Activity without closing all the
            // tabs prevents the `CustomTabTabObserver#onAllTabsClosed` from being sent. So, we
            // should let the last tab get closed if the tab is minimized because the flashing won't
            // be visible in that case.
            var minimizationManager = mMinimizationManagerHolder.getMinimizationManager();
            boolean isMinimized = minimizationManager != null && minimizationManager.isMinimized();
            if (mTabController.onlyOneTabRemaining() && !isMinimized) {
                // If we call mTabController.closeTab() and wait for the Activity to close as a
                // result, we have a blank screen flashing before closing. https://crbug.com/1518767
                finishCallback.onResult(USER_NAVIGATION);
                ++numTabsClosed;
                break;
            }
            mTabController.closeTab();
            ++numTabsClosed;

            // Check whether the close button navigation would have stopped on the newly revealed
            // Tab. We don't check this at the start of the loop (or make navigateSingleTab
            // consider the current Tab) because in that case if the user started on a landing page,
            // we would not navigate at all.
            Tab nextTab = mTabProvider.getTab();
            if (nextTab != null && isLandingPage(nextTab.getUrl().getSpec())) {
                return;
            }
        }

        if (numTabsClosed > 0) {
            RecordHistogram.recordCount100Histogram(
                    "CustomTabs.TabCounts.OnClosingAllTabs", numTabsClosed);
        }
    }

    /**
     * Navigates to the most recent landing page on the current Tab. Returns {@code false} if no
     * criteria for what is a landing page has been given or no such page can be found.
     */
    private boolean navigateSingleTab(@Nullable NavigationController controller) {
        if (mLandingPagePredicate == null || controller == null) return false;

        NavigationHistory history = controller.getNavigationHistory();
        for (int i = history.getCurrentEntryIndex() - 1; i >= 0; i--) {
            String url = history.getEntryAtIndex(i).getUrl().getSpec();
            if (!isLandingPage(url)) continue;

            controller.goToNavigationIndex(i);
            return true;
        }

        return false;
    }

    private @Nullable NavigationController getNavigationController() {
        Tab tab = mTabProvider.getTab();
        if (tab == null) return null;
        WebContents webContents = tab.getWebContents();
        if (webContents == null) return null;
        return webContents.getNavigationController();
    }
}