Commit 726b6ba2 authored by Liquan(Max) Gu's avatar Liquan(Max) Gu Committed by Commit Bot

[PaymentHandler] Open a blank PaymentHandler bottom-sheet behind a flag

The PaymentHandler UI is opened as an ChromeCustomTab Activity. Going
forwards, we will refactor the Payment Handler to make it based on
bottom-sheet.

For this purpose, this CL adds:
* A blank bottom-sheet-based Payment Handler UI.
* "ScrollToExpandPaymentHandler" to enable the incomplete feature while
we are still developing the refactored solution.

Here's the expected change after the patch:

Flag:
When this flag is disabled (default), Payment Handler is unchanged.
When this flag is enabled (--enable-features=ScrollToExpandPaymentHandler),
the Payment Handler UI is changed to blank widget.

The Payment Handler bottom-sheet:
The Payment Handler UI will be displayed as an empty bottom-sheet. The
swipe-down can dismissing the UI; the swipe-up can maximize the UI.

Bug: 999196

Change-Id: Id839dfe63a9473243c5864295c5ae435ee725e55
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1832702
Commit-Queue: Liquan (Max) Gu <maxlg@chromium.org>
Reviewed-by: default avatarRouslan Solomakhin <rouslan@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Reviewed-by: default avatarAndrew Grieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#702998}
parent fd212a55
......@@ -1199,6 +1199,11 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/payments/CanMakePaymentQuery.java",
"java/src/org/chromium/chrome/browser/payments/CardEditor.java",
"java/src/org/chromium/chrome/browser/payments/ContactEditor.java",
"java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java",
"java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerMediator.java",
"java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerProperties.java",
"java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerViewBinder.java",
"java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerView.java",
"java/src/org/chromium/chrome/browser/payments/JourneyLogger.java",
"java/src/org/chromium/chrome/browser/payments/PackageManagerDelegate.java",
"java/src/org/chromium/chrome/browser/payments/PaymentApp.java",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@color/sheet_bg_color"
android:layout_height="match_parent"
android:layout_width="match_parent">
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</RelativeLayout>
......@@ -312,6 +312,7 @@ public abstract class ChromeFeatureList {
public static final String REMOVE_NAVIGATION_HISTORY = "RemoveNavigationHistory";
public static final String REORDER_BOOKMARKS = "ReorderBookmarks";
public static final String REVAMPED_CONTEXT_MENU = "RevampedContextMenu";
public static final String SCROLL_TO_EXPAND_PAYMENT_HANDLER = "ScrollToExpandPaymentHandler";
public static final String SEND_TAB_TO_SELF = "SyncSendTabToSelf";
public static final String SERVICE_MANAGER_FOR_DOWNLOAD = "ServiceManagerForDownload";
public static final String SERVICE_WORKER_PAYMENT_APPS = "ServiceWorkerPaymentApps";
......
......@@ -23,6 +23,8 @@ import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.BrowserServicesMetrics;
import org.chromium.chrome.browser.browserservices.TrustedWebActivityClient;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.payments.PaymentRequestImpl;
import org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.document.AsyncTabCreationParams;
......@@ -41,6 +43,8 @@ import org.chromium.webapk.lib.client.WebApkIdentityServiceClient;
import org.chromium.webapk.lib.client.WebApkNavigationClient;
import org.chromium.webapk.lib.client.WebApkValidator;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
/**
......@@ -75,7 +79,14 @@ public class ServiceTabLauncher {
// Open popup window in custom tab.
// Note that this is used by PaymentRequestEvent.openWindow().
if (disposition == WindowOpenDisposition.NEW_POPUP) {
if (!createPopupCustomTab(requestId, url, incognito)) {
boolean success = false;
try {
success = PaymentHandlerCoordinator.isEnabled()
? PaymentRequestImpl.openPaymentHandlerWindow(new URI(url))
: createPopupCustomTab(requestId, url, incognito);
} catch (URISyntaxException e) { /* Intentionally leave blank, so success is false. */
}
if (!success) {
PostTask.postTask(UiThreadTaskTraits.DEFAULT,
() -> onWebContentsForRequestAvailable(requestId, null));
}
......
......@@ -23,11 +23,13 @@ import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile;
import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
import org.chromium.chrome.browser.autofill.PersonalDataManager.NormalizedAddressRequestDelegate;
import org.chromium.chrome.browser.browserservices.Origin;
import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver;
import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior;
import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior.OverviewModeObserver;
import org.chromium.chrome.browser.favicon.FaviconHelper;
import org.chromium.chrome.browser.page_info.CertificateChainHelper;
import org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator;
import org.chromium.chrome.browser.payments.micro.MicrotransactionCoordinator;
import org.chromium.chrome.browser.payments.ui.ContactDetailsSection;
import org.chromium.chrome.browser.payments.ui.LineItem;
......@@ -268,10 +270,10 @@ public class PaymentRequestImpl
private static boolean sIsLocalCanMakePaymentQueryQuotaEnforcedForTest;
/**
* True if show() was called in any PaymentRequestImpl object. Used to prevent showing more than
* one PaymentRequest UI per browser process.
* Hold the currently showing PaymentRequest. Used to prevent showing more than one
* PaymentRequest UI per browser process.
*/
private static boolean sIsAnyPaymentRequestShowing;
private static PaymentRequestImpl sShowingPaymentRequest;
/** Monitors changes in the TabModelSelector. */
private final TabModelSelectorObserver mSelectorObserver = new EmptyTabModelSelectorObserver() {
......@@ -376,6 +378,7 @@ public class PaymentRequestImpl
private MicrotransactionCoordinator mMicrotransactionUi;
private Callback<PaymentInformation> mPaymentInformationCallback;
private PaymentInstrument mInvokedPaymentInstrument;
private PaymentHandlerCoordinator mPaymentHandlerUi;
private boolean mMerchantSupportsAutofillPaymentInstruments;
private boolean mUserCanAddCreditCard;
private boolean mHideServerAutofillInstruments;
......@@ -721,7 +724,7 @@ public class PaymentRequestImpl
activity, mAutofillProfiles, mContactEditor, mJourneyLogger);
}
setIsAnyPaymentRequestShowing(true);
setShowingPaymentRequest(this);
mUI = new PaymentRequestUI(activity, this, mRequestShipping,
/* requestShippingOption= */ mRequestShipping,
mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail,
......@@ -865,6 +868,21 @@ public class PaymentRequestImpl
triggerPaymentAppUiSkipIfApplicable(chromeActivity);
}
private void dimBackgroundIfNotBottomSheetPaymentHandler(PaymentInstrument selectedInstrument) {
// Putting isEnabled() last is intentional. It's to ensure not to confused the unexecuted
// group and the disabled in A/B testing.
if ((selectedInstrument instanceof ServiceWorkerPaymentApp)
&& PaymentHandlerCoordinator.isEnabled()) {
// When the Payment Handler (PH) UI is based on Activity, dimming the Payment
// Request (PR) UI does not dim the PH; when it's based on bottom-sheet, dimming
// the PR dims both UIs. As bottom-sheet itself has dimming effect, dimming PR
// is unnecessary for the bottom-sheet PH. For now, ServiceWorkerPaymentApp is the only
// payment app that can open the bottom-sheet.
return;
}
mUI.dimBackground();
}
private void triggerPaymentAppUiSkipIfApplicable(ChromeActivity chromeActivity) {
// If we are skipping showing the Payment Request UI, we should call into the
// PaymentApp immediately after we determine the instruments are ready and UI is shown.
......@@ -891,7 +909,7 @@ public class PaymentRequestImpl
&& !mIsUserGestureShow)) {
mUI.show();
} else {
mUI.dimBackground();
dimBackgroundIfNotBottomSheetPaymentHandler(selectedInstrument);
mDidRecordShowEvent = true;
mShouldRecordAbortReason = true;
mJourneyLogger.setEventOccurred(Event.SKIPPED_SHOW);
......@@ -934,7 +952,7 @@ public class PaymentRequestImpl
mCurrencyFormatterMap.get(mRawTotal.amount.currency),
mUiShoppingCart.getTotal(), this::onMicrotransactionUiConfirmed,
this::onMicrotransactionUiDismissed)) {
setIsAnyPaymentRequestShowing(true);
setShowingPaymentRequest(this);
mDidRecordShowEvent = true;
mShouldRecordAbortReason = true;
mJourneyLogger.setEventOccurred(Event.SHOWN);
......@@ -1143,6 +1161,41 @@ public class PaymentRequestImpl
return changePaymentMethodFromInvokedApp(methodName, stringifiedData);
}
/**
* Called to open a new PaymentHandler UI on the showing PaymentRequest.
* @param url The url of the payment app to be displayed in the UI.
* @return Whether the opening is successful.
*/
public static boolean openPaymentHandlerWindow(URI url) {
return sShowingPaymentRequest != null
&& sShowingPaymentRequest.openPaymentHandlerWindowInternal(url);
}
/**
* Called to open a new PaymentHandler UI on this PaymentRequest.
* @param url The url of the payment app to be displayed in the UI.
* @return Whether the opening is successful.
*/
private boolean openPaymentHandlerWindowInternal(URI url) {
assert mInvokedPaymentInstrument != null;
assert mInvokedPaymentInstrument instanceof ServiceWorkerPaymentApp;
assert new Origin(url.toString())
.equals(new Origin(((ServiceWorkerPaymentApp) mInvokedPaymentInstrument)
.getScope()
.toString()));
if (mPaymentHandlerUi != null) return false;
mPaymentHandlerUi = new PaymentHandlerCoordinator();
ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents);
if (chromeActivity == null) return false;
return mPaymentHandlerUi.show(chromeActivity, this::onPaymentHandlerUiDismissed);
}
private void onPaymentHandlerUiDismissed() {
ensureHideAndResetPaymentHandlerUi();
ServiceWorkerPaymentAppBridge.onClosingPaymentAppWindow(mWebContents);
}
@Override
public boolean isInvokedInstrumentValidForPaymentMethodIdentifier(String methodName) {
return mInvokedPaymentInstrument != null
......@@ -2531,6 +2584,12 @@ public class PaymentRequestImpl
PersonalDataManager.getInstance().normalizeAddress(address.getProfile(), this);
}
private void ensureHideAndResetPaymentHandlerUi() {
if (mPaymentHandlerUi == null) return;
mPaymentHandlerUi.hide();
mPaymentHandlerUi = null;
}
/**
* Closes the UI and destroys native objects. If the client is still connected, then it's
* notified of UI hiding. This PaymentRequestImpl object can't be reused after this function is
......@@ -2545,10 +2604,11 @@ public class PaymentRequestImpl
* always pass "true."
*/
private void closeUIAndDestroyNativeObjects(boolean immediateClose) {
ensureHideAndResetPaymentHandlerUi();
if (mMicrotransactionUi != null) {
mMicrotransactionUi.hide();
mMicrotransactionUi = null;
setIsAnyPaymentRequestShowing(false);
setShowingPaymentRequest(null);
}
if (mUI != null) {
......@@ -2560,7 +2620,7 @@ public class PaymentRequestImpl
closeClient();
});
mUI = null;
setIsAnyPaymentRequestShowing(false);
setShowingPaymentRequest(null);
}
mIsCurrentPaymentRequestShowing = false;
......@@ -2612,16 +2672,18 @@ public class PaymentRequestImpl
}
/**
* @return Whether any instance of PaymentRequest has received a show() call. Don't use this
* function to check whether the current instance has received a show() call.
* @return Whether any instance of PaymentRequest has received a show() call.
* Don't use this function to check whether the current instance has
* received a show() call.
*/
private static boolean getIsAnyPaymentRequestShowing() {
return sIsAnyPaymentRequestShowing;
return sShowingPaymentRequest != null;
}
/** @param isShowing Whether any instance of PaymentRequest has received a show() call. */
private static void setIsAnyPaymentRequestShowing(boolean isShowing) {
sIsAnyPaymentRequestShowing = isShowing;
/** @param paymentRequest The currently showing PaymentRequestImpl. */
private static void setShowingPaymentRequest(PaymentRequestImpl paymentRequest) {
assert sShowingPaymentRequest == null || paymentRequest == null;
sShowingPaymentRequest = paymentRequest;
}
@VisibleForTesting
......
......@@ -214,6 +214,10 @@ public class ServiceWorkerPaymentApp extends PaymentInstrument implements Paymen
mPaymentHandlerHost = host;
}
/*package*/ URI getScope() {
return mScope;
}
@Override
public void getInstruments(String id, Map<String, PaymentMethodData> methodDataMap,
String origin, String iframeOrigin, byte[][] unusedCertificateChain,
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
/**
* 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;
/** Observer for the dismissal of the payment-handler UI. */
public interface DismissObserver {
/**
* Called after the user has dismissed the payment-handler UI by swiping it down or tapping
* on the scrim behind the UI.
*/
void onDismissed();
}
/** Constructs the payment-handler component coordinator. */
public PaymentHandlerCoordinator() {
assert isEnabled();
}
/**
* Shows the payment-handler UI.
*
* @param chromeActivity The activity where the UI should be shown.
* @param dismissObserver The observer to be notified when the user has dismissed the UI.
* @return Whether the payment-handler UI was shown. Can be false if the UI was suppressed.
*/
public boolean show(ChromeActivity activity, DismissObserver dismissObserver) {
assert mHider == null : "Already showing payment-handler UI";
PaymentHandlerMediator mediator = new PaymentHandlerMediator(this::hide, dismissObserver);
BottomSheetController bottomSheetController = activity.getBottomSheetController();
bottomSheetController.getBottomSheet().addObserver(mediator);
PropertyModel model = new PropertyModel.Builder(PaymentHandlerProperties.ALL_KEYS).build();
PaymentHandlerView view = new PaymentHandlerView(activity);
PropertyModelChangeProcessor changeProcessor =
PropertyModelChangeProcessor.create(model, view, PaymentHandlerViewBinder::bind);
mHider = () -> {
changeProcessor.destroy();
bottomSheetController.getBottomSheet().removeObserver(mediator);
bottomSheetController.hideContent(/*content=*/view, /*animate=*/true);
};
boolean result = bottomSheetController.requestShowContent(view, /*animate=*/true);
if (result) bottomSheetController.expandSheet();
return result;
}
/** Hides the payment-handler UI. */
public void hide() {
if (mHider == null) return;
mHider.run();
}
/**
* @return Whether this solution (as opposed to the Chrome-custom-tab based solution) of
* PaymentHandler is enabled. This solution is intended to replace the other
* solution.
*/
public static boolean isEnabled() {
return ChromeFeatureList.isEnabled(ChromeFeatureList.SCROLL_TO_EXPAND_PAYMENT_HANDLER);
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator.DismissObserver;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet.SheetState;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
/**
* PaymentHandler mediator, which is responsible for receiving events from the view and notifies the
* backend (the coordinator).
*/
/* package */ class PaymentHandlerMediator extends EmptyBottomSheetObserver {
private final Runnable mHider;
private final DismissObserver mDismissObserver;
/* package */ PaymentHandlerMediator(Runnable hider, DismissObserver dismissObserver) {
mHider = hider;
mDismissObserver = dismissObserver;
}
// EmptyBottomSheetObserver:
@Override
public void onSheetStateChanged(@SheetState int newState) {
switch (newState) {
case SheetState.HIDDEN:
mHider.run();
mDismissObserver.onDismissed();
break;
}
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 org.chromium.ui.modelutil.PropertyKey;
/** PaymentHandler UI properties, which fully describe the state of the UI. */
/* package */ class PaymentHandlerProperties {
// TODO(maxlg): Should add more keys after we add more states to the widget .
/* package */ static final PropertyKey[] ALL_KEYS = new PropertyKey[] {};
// Prevent instantiation.
private PaymentHandlerProperties() {}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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.content.Context;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet.BottomSheetContent;
/** PaymentHandler UI. */
/* package */ class PaymentHandlerView implements BottomSheetContent {
private final View mToolbarView;
private final View mContentView;
/* package */ PaymentHandlerView(Context context) {
mToolbarView = LayoutInflater.from(context).inflate(R.layout.payment_handler_toolbar, null);
mContentView = LayoutInflater.from(context).inflate(R.layout.payment_handler_content, null);
}
// BottomSheetContent:
@Override
public View getContentView() {
return mContentView;
}
@Override
@Nullable
public View getToolbarView() {
return mToolbarView;
}
@Override
public int getVerticalScrollOffset() {
return 0;
}
@Override
public void destroy() {}
@Override
@BottomSheet.ContentPriority
public int getPriority() {
// If multiple bottom sheets are queued up to be shown, prioritize payment-handler, because
// it's triggered by a user gesture, such as a click on <button>Buy this article</button>.
return BottomSheet.ContentPriority.HIGH;
}
@Override
public boolean isPeekStateEnabled() {
return false;
}
@Override
public boolean wrapContentEnabled() {
return false;
}
@Override
public int getSheetContentDescriptionStringId() {
return R.string.payment_request_payment_method_section_name;
}
@Override
public int getSheetHalfHeightAccessibilityStringId() {
return R.string.payment_request_payment_method_section_name;
}
@Override
public int getSheetFullHeightAccessibilityStringId() {
return R.string.payment_request_payment_method_section_name;
}
@Override
public int getSheetClosedAccessibilityStringId() {
return R.string.payment_request_payment_method_section_name;
}
@Override
public boolean swipeToDismissEnabled() {
// flinging down hard enough will close the sheet.
return true;
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
/**
* PaymentHandler view binder, which is stateless. It is called to bind a given model to a given
* view. Should contain as little business logic as possible.
*/
/* package */ class PaymentHandlerViewBinder {
/* package */ static void bind(
PropertyModel model, PaymentHandlerView view, PropertyKey propertyKey) {
// TODO(maxlg): bind model properties to view after adding view widgets.
}
}
......@@ -161,6 +161,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&kReaderModeInCCT,
&kReorderBookmarks,
&kRevampedContextMenu,
&kScrollToExpandPaymentHandler,
&kSearchEnginePromoExistingDevice,
&kSearchEnginePromoNewDevice,
&kServiceManagerForBackgroundPrefetch,
......@@ -503,6 +504,9 @@ const base::Feature kReorderBookmarks{"ReorderBookmarks",
const base::Feature kRevampedContextMenu{"RevampedContextMenu",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kScrollToExpandPaymentHandler{
"ScrollToExpandPaymentHandler", base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kServiceManagerForBackgroundPrefetch{
"ServiceManagerForBackgroundPrefetch", base::FEATURE_DISABLED_BY_DEFAULT};
......
......@@ -96,6 +96,7 @@ extern const base::Feature kReachedCodeProfiler;
extern const base::Feature kReorderBookmarks;
extern const base::Feature kReaderModeInCCT;
extern const base::Feature kRevampedContextMenu;
extern const base::Feature kScrollToExpandPaymentHandler;
extern const base::Feature kSearchEnginePromoExistingDevice;
extern const base::Feature kSearchEnginePromoNewDevice;
extern const base::Feature kServiceManagerForBackgroundPrefetch;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment