chromium/chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.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.payments.handler;

import android.app.Activity;

import org.chromium.base.version_info.VersionInfo;
import org.chromium.chrome.browser.content.WebContentsFactory;
import org.chromium.chrome.browser.payments.handler.toolbar.PaymentHandlerToolbarCoordinator;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObscuringHandler;
import org.chromium.chrome.browser.tab.TabObscuringHandlerSupplier;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorSupplier;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.components.payments.InputProtector;
import org.chromium.components.payments.PaymentHandlerNavigationThrottle;
import org.chromium.components.thinwebview.ThinWebView;
import org.chromium.components.thinwebview.ThinWebViewConstraints;
import org.chromium.components.thinwebview.ThinWebViewFactory;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.SelectionClient;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.url.GURL;

/**
 * PaymentHandler coordinator, which owns the component overall, i.e., creates other objects in the
 * component and connects them. It decouples the implementation of this component from other
 * components and acts as the point of contact between them. Any code in this component that needs
 * to interact with another component does that through this coordinator.
 */
public class PaymentHandlerCoordinator {
    private Runnable mHider;
    private WebContents mPaymentHandlerWebContents;
    private PaymentHandlerToolbarCoordinator mToolbarCoordinator;
    private InputProtector mInputProtector = new InputProtector();

    /** Constructs the payment-handler component coordinator. */
    public PaymentHandlerCoordinator() {}

    /** Observes the state changes of the payment-handler UI. */
    public interface PaymentHandlerUiObserver {
        /** Called when Payment Handler UI is closed. */
        void onPaymentHandlerUiClosed();

        /** Called when Payment Handler UI is shown. */
        void onPaymentHandlerUiShown();
    }

    /**
     * Shows the payment-handler UI.
     *
     * @param paymentRequestWebContents The WebContents of the merchant's frame.
     * @param url The url of the payment handler app, i.e., that of
     *     "PaymentRequestEvent.openWindow(url)".
     * @param uiObserver The {@link PaymentHandlerUiObserver} that observes this Payment Handler UI.
     * @return The WebContents of the payment handler that's just opened when the showing is
     *     successful; null if failed. When null is returned, caller should also call hide().
     */
    public WebContents show(
            WebContents paymentRequestWebContents, GURL url, PaymentHandlerUiObserver uiObserver) {
        assert mHider == null : "Already showing payment-handler UI";
        assert paymentRequestWebContents != null;
        WindowAndroid windowAndroid = paymentRequestWebContents.getTopLevelNativeWindow();
        if (windowAndroid == null) return null;
        Activity activity = windowAndroid.getActivity().get();
        if (activity == null) return null;
        Profile profile = Profile.fromWebContents(paymentRequestWebContents);
        if (profile == null) return null;

        mInputProtector.markShowTime();
        mPaymentHandlerWebContents =
                WebContentsFactory.createWebContents(profile, /* initiallyHidden= */ false, false);
        PaymentHandlerNavigationThrottle.markPaymentHandlerWebContents(mPaymentHandlerWebContents);
        ContentView webContentView =
                ContentView.createContentView(activity, mPaymentHandlerWebContents);
        initializeWebContents(windowAndroid, webContentView, url);

        mToolbarCoordinator =
                new PaymentHandlerToolbarCoordinator(
                        activity,
                        mPaymentHandlerWebContents,
                        url,
                        windowAndroid::getModalDialogManager);

        BottomSheetController bottomSheetController =
                BottomSheetControllerProvider.from(windowAndroid);
        Tab currentTab = TabModelSelectorSupplier.getCurrentTabFrom(windowAndroid);
        TabObscuringHandler tabObscuringHandler =
                TabObscuringHandlerSupplier.getValueOrNullFrom(windowAndroid);
        if (bottomSheetController == null || currentTab == null || tabObscuringHandler == null) {
            return null;
        }

        PropertyModel model = new PropertyModel.Builder(PaymentHandlerProperties.ALL_KEYS).build();
        PaymentHandlerMediator mediator =
                new PaymentHandlerMediator(
                        model,
                        this::hide,
                        /* paymentRequestWebContents= */ paymentRequestWebContents,
                        /* paymentHandlerWebContents= */ mPaymentHandlerWebContents,
                        uiObserver,
                        currentTab.getView(),
                        mToolbarCoordinator.getToolbarHeightPx(),
                        bottomSheetController,
                        tabObscuringHandler,
                        activity,
                        mInputProtector);
        activity.getWindow().getDecorView().addOnLayoutChangeListener(mediator);

        bottomSheetController.addObserver(mediator);
        mPaymentHandlerWebContents.addObserver(mediator);

        mToolbarCoordinator.setCloseButtonOnClickCallback(mediator::onToolbarCloseButtonClicked);
        IntentRequestTracker intentRequestTracker = windowAndroid.getIntentRequestTracker();
        assert intentRequestTracker != null;
        ThinWebView thinWebView =
                ThinWebViewFactory.create(
                        activity, new ThinWebViewConstraints(), intentRequestTracker);
        assert webContentView.getParent() == null;
        thinWebView.attachWebContents(mPaymentHandlerWebContents, webContentView, null);
        PaymentHandlerView view =
                new PaymentHandlerView(
                        activity,
                        mPaymentHandlerWebContents,
                        mToolbarCoordinator.getView(),
                        thinWebView.getView(),
                        mInputProtector);
        assert mToolbarCoordinator.getToolbarHeightPx() == view.getToolbarHeightPx();
        PropertyModelChangeProcessor changeProcessor =
                PropertyModelChangeProcessor.create(model, view, PaymentHandlerViewBinder::bind);
        mHider =
                () -> {
                    changeProcessor.destroy();
                    bottomSheetController.removeObserver(mediator);
                    bottomSheetController.hideContent(/* content= */ view, /* animate= */ true);
                    uiObserver.onPaymentHandlerUiClosed();
                    assert activity.getWindow() != null;
                    assert activity.getWindow().getDecorView() != null;
                    activity.getWindow().getDecorView().removeOnLayoutChangeListener(mediator);
                    mediator.destroy();
                    thinWebView.destroy();
                    mPaymentHandlerWebContents.destroy();
                };
        boolean isShowSuccess = bottomSheetController.requestShowContent(view, /* animate= */ true);
        if (!isShowSuccess) return null;

        return mPaymentHandlerWebContents;
    }

    private void initializeWebContents(
            WindowAndroid windowAndroid, ContentView webContentView, GURL url) {
        mPaymentHandlerWebContents.setDelegates(
                VersionInfo.getProductVersion(),
                ViewAndroidDelegate.createBasicDelegate(webContentView),
                webContentView,
                windowAndroid,
                WebContents.createDefaultInternalsHolder());

        SelectionPopupController controller =
                SelectionPopupController.fromWebContents(mPaymentHandlerWebContents);
        controller.setActionModeCallback(
                new PaymentHandlerActionModeCallback(mPaymentHandlerWebContents));
        controller.setSelectionClient(
                SelectionClient.createSmartSelectionClient(mPaymentHandlerWebContents));

        mPaymentHandlerWebContents
                .getNavigationController()
                .loadUrl(new LoadUrlParams(url.getSpec()));
    }

    /**
     * Get the WebContents of the Payment Handler for testing purpose. In other situations,
     * WebContents should not be leaked outside the Payment Handler.
     *
     * @return The WebContents of the Payment Handler.
     */
    public WebContents getWebContentsForTest() {
        return mPaymentHandlerWebContents;
    }

    /** Hides the payment-handler UI. */
    public void hide() {
        if (mHider == null) return;
        mHider.run();
        mHider = null;
    }

    public void clickSecurityIconForTest() {
        mToolbarCoordinator.clickSecurityIconForTest();
    }

    public void clickCloseButtonForTest() {
        mToolbarCoordinator.clickCloseButtonForTest();
    }

    public void setInputProtectorForTest(InputProtector inputProtector) {
        mInputProtector = inputProtector;
    }
}