Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4766)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/payments/PaymentRequestImpl.java

Issue 1904553003: Java implementation of PaymentRequest mojo service (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: address comments Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/payments/PaymentRequestImpl.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/payments/PaymentRequestImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/payments/PaymentRequestImpl.java
index b66f3b01d36925e0152aa159b48b7b614b795360..27347ea9d5f8e6801f2a5283b1d29d94aea867f6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/payments/PaymentRequestImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/payments/PaymentRequestImpl.java
@@ -4,32 +4,132 @@
package org.chromium.chrome.browser.payments;
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.text.TextUtils;
+
+import org.chromium.base.Callback;
+import org.chromium.base.Log;
+import org.chromium.chrome.browser.autofill.PersonalDataManager;
+import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile;
+import org.chromium.chrome.browser.favicon.FaviconHelper;
+import org.chromium.chrome.browser.payments.ui.LineItem;
+import org.chromium.chrome.browser.payments.ui.PaymentInformation;
+import org.chromium.chrome.browser.payments.ui.PaymentOption;
+import org.chromium.chrome.browser.payments.ui.PaymentRequestUI;
+import org.chromium.chrome.browser.payments.ui.SectionInformation;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.content.browser.ContentViewCore;
import org.chromium.content_public.browser.WebContents;
import org.chromium.mojo.system.MojoException;
import org.chromium.mojom.payments.PaymentDetails;
+import org.chromium.mojom.payments.PaymentItem;
import org.chromium.mojom.payments.PaymentOptions;
import org.chromium.mojom.payments.PaymentRequest;
import org.chromium.mojom.payments.PaymentRequestClient;
+import org.chromium.mojom.payments.PaymentResponse;
+import org.chromium.mojom.payments.ShippingOption;
+import org.chromium.ui.base.WindowAndroid;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Pattern;
/**
* Android implementation of the PaymentRequest service defined in
* third_party/WebKit/public/platform/modules/payments/payment_request.mojom.
*/
-public class PaymentRequestImpl implements PaymentRequest {
+public class PaymentRequestImpl implements PaymentRequest, PaymentRequestUI.Client,
+ PaymentApp.InstrumentsCallback, PaymentInstrument.DetailsCallback {
+ /**
+ * The size for the favicon in density-independent pixels.
+ */
+ private static final int FAVICON_SIZE_DP = 24;
+
+ private static final String TAG = "cr_PaymentRequest";
+ private Activity mContext;
+ private String mMerchantName;
+ private String mOrigin;
+ private Bitmap mFavicon;
+ private List<PaymentApp> mApps;
+ private PaymentRequestClient mClient;
+ private Set<String> mSupportedMethods;
+ private List<PaymentItem> mPaymentItems;
+ private List<LineItem> mLineItems;
+ private SectionInformation mShippingOptions;
+ private JSONObject mData;
+ private SectionInformation mShippingAddresses;
+ private List<PaymentApp> mPendingApps;
+ private List<PaymentInstrument> mPendingInstruments;
+ private SectionInformation mPaymentMethods;
+ private PaymentRequestUI mUI;
+ private Callback<PaymentInformation> mPaymentInformationCallback;
+ private Pattern mRegionCodePattern;
+
/**
* Builds the dialog.
*
* @param webContents The web contents that have invoked the PaymentRequest API.
*/
- public PaymentRequestImpl(WebContents webContents) {}
+ public PaymentRequestImpl(WebContents webContents) {
+ if (webContents == null) return;
+
+ ContentViewCore contentViewCore = ContentViewCore.fromWebContents(webContents);
+ if (contentViewCore == null) return;
+
+ WindowAndroid window = contentViewCore.getWindowAndroid();
+ if (window == null) return;
+
+ mContext = window.getActivity().get();
+ if (mContext == null) return;
+
+ mMerchantName = webContents.getTitle();
+ mOrigin = webContents.getVisibleUrl();
+
+ final FaviconHelper faviconHelper = new FaviconHelper();
+ float scale = mContext.getResources().getDisplayMetrics().density;
+ faviconHelper.getLocalFaviconImageForURL(Profile.getLastUsedProfile(),
+ webContents.getVisibleUrl(), (int) (FAVICON_SIZE_DP * scale + 0.5f),
+ new FaviconHelper.FaviconImageCallback() {
+ @Override
+ public void onFaviconAvailable(Bitmap bitmap, String iconUrl) {
+ faviconHelper.destroy();
+ if (bitmap == null) return;
+ if (mUI == null) {
+ mFavicon = bitmap;
+ return;
+ }
+ mUI.setTitleBitmap(bitmap);
+ }
+ });
+
+ mApps = PaymentAppFactory.create(webContents);
+ mRegionCodePattern = Pattern.compile(AutofillAddress.REGION_CODE_PATTERN);
+ }
/**
* Called by the renderer to provide an endpoint for callbacks.
*/
@Override
public void setClient(PaymentRequestClient client) {
- assert client != null;
- client.onError();
+ assert mClient == null;
+
+ mClient = client;
+
+ if (mClient == null) return;
+
+ if (mContext == null) {
+ disconnectFromClientWithDebugMessage("Web contents don't have associated activity");
+ }
}
/**
@@ -37,29 +137,405 @@ public class PaymentRequestImpl implements PaymentRequest {
*/
@Override
public void show(String[] supportedMethods, PaymentDetails details, PaymentOptions options,
- String stringifiedData) {}
+ String stringifiedData) {
+ if (mClient == null) return;
+
+ if (mSupportedMethods != null) {
+ disconnectFromClientWithDebugMessage("PaymentRequest.show() called more than once.");
+ return;
+ }
+
+ mSupportedMethods = getValidatedSupportedMethods(supportedMethods);
+ if (mSupportedMethods == null) {
+ disconnectFromClientWithDebugMessage("Invalid payment methods");
+ return;
+ }
+
+ mLineItems = getValidatedLineItems(details);
+ if (mLineItems == null) {
+ disconnectFromClientWithDebugMessage("Invalid line items");
+ return;
+ }
+ mPaymentItems = Arrays.asList(details.items);
+
+ mShippingOptions =
+ getValidatedShippingOptions(details.items[0].amount.currencyCode, details);
+ if (mShippingOptions == null) {
+ disconnectFromClientWithDebugMessage("Invalid shipping options");
+ return;
+ }
+
+ mData = getValidatedData(mSupportedMethods, stringifiedData);
+ if (mData == null) {
+ disconnectFromClientWithDebugMessage("Invalid payment method specific data");
+ return;
+ }
+
+ List<AutofillAddress> addresses = new ArrayList<>();
+ List<AutofillProfile> profiles = PersonalDataManager.getInstance().getProfiles();
+ for (int i = 0; i < profiles.size(); i++) {
+ AutofillProfile profile = profiles.get(i);
+ if (profile.getCountryCode() != null
+ && mRegionCodePattern.matcher(profile.getCountryCode()).matches()
+ && profile.getStreetAddress() != null && profile.getRegion() != null
+ && profile.getLocality() != null && profile.getDependentLocality() != null
+ && profile.getPostalCode() != null && profile.getSortingCode() != null
+ && profile.getCompanyName() != null && profile.getFullName() != null) {
+ addresses.add(new AutofillAddress(profile));
+ }
+ }
+
+ if (addresses.isEmpty()) {
+ mShippingAddresses = new SectionInformation();
+ } else {
+ mShippingAddresses = new SectionInformation(0, addresses);
+ onShippingAddressChanged(addresses.get(0));
+ }
+
+ mPendingApps = new ArrayList<>(mApps);
+ mPendingInstruments = new ArrayList<>();
+ boolean isGettingInstruments = false;
+
+ for (int i = 0; i < mApps.size(); i++) {
+ PaymentApp app = mApps.get(i);
+ Set<String> appMethods = app.getSupportedMethodNames();
+ appMethods.retainAll(mSupportedMethods);
+ if (!appMethods.isEmpty()) {
+ isGettingInstruments = true;
+ app.getInstruments(mPaymentItems, this);
+ }
+ }
+
+ if (!isGettingInstruments) mPaymentMethods = new SectionInformation();
+
+ boolean requestShipping = options != null && options.requestShipping;
+ mUI = new PaymentRequestUI(mContext, this, requestShipping, mMerchantName, mOrigin);
+ if (mFavicon != null) mUI.setTitleBitmap(mFavicon);
+ mFavicon = null;
+ }
+
+ private HashSet<String> getValidatedSupportedMethods(String[] methods) {
+ // Payment methods are required.
+ if (methods == null || methods.length == 0) return null;
+
+ HashSet<String> result = new HashSet<>();
+ for (int i = 0; i < methods.length; i++) {
+ // Payment methods should be non-empty.
+ if (TextUtils.isEmpty(methods[i])) return null;
+ result.add(methods[i]);
+ }
+
+ return result;
+ }
+
+ private List<LineItem> getValidatedLineItems(PaymentDetails details) {
+ // Line items are required.
+ if (details == null || details.items == null || details.items.length == 0) return null;
+
+ for (int i = 0; i < details.items.length; i++) {
+ PaymentItem item = details.items[i];
+ // "id", "label", "currencyCode", and "value" should be non-empty.
+ if (item == null || TextUtils.isEmpty(item.id) || TextUtils.isEmpty(item.label)
+ || item.amount == null || TextUtils.isEmpty(item.amount.currencyCode)
+ || TextUtils.isEmpty(item.amount.value)) {
+ return null;
+ }
+ }
+
+ CurrencyStringFormatter formatter = new CurrencyStringFormatter(
+ details.items[0].amount.currencyCode, Locale.getDefault());
+
+ // Currency codes should be in correct format.
+ if (!formatter.isValidAmountCurrencyCode(details.items[0].amount.currencyCode)) return null;
+
+ List<LineItem> result = new ArrayList<>(details.items.length);
+ for (int i = 0; i < details.items.length; i++) {
+ PaymentItem item = details.items[i];
+
+ // All currency codes must match.
+ if (!item.amount.currencyCode.equals(details.items[0].amount.currencyCode)) return null;
+
+ // Value should be in correct format.
+ if (!formatter.isValidAmountValue(item.amount.value)) return null;
+
+ result.add(new LineItem(item.label,
+ i == details.items.length - 1 ? item.amount.currencyCode : "",
+ formatter.format(item.amount.value)));
+ }
+
+ return result;
+ }
+
+ private SectionInformation getValidatedShippingOptions(
+ String itemsCurrencyCode, PaymentDetails details) {
+ // Shipping options are optional.
+ if (details.shippingOptions == null || details.shippingOptions.length == 0) {
+ return new SectionInformation();
+ }
+
+ CurrencyStringFormatter formatter =
+ new CurrencyStringFormatter(itemsCurrencyCode, Locale.getDefault());
+
+ List<PaymentOption> result = new ArrayList<>();
+ for (int i = 0; i < details.shippingOptions.length; i++) {
+ ShippingOption option = details.shippingOptions[i];
+
+ // Each "id", "label", "currencyCode", and "value" should be non-empty.
+ // Each "value" should be a valid amount value.
+ // Each "currencyCode" should match the line items' currency codes.
+ if (option == null || TextUtils.isEmpty(option.id) || TextUtils.isEmpty(option.label)
+ || option.amount == null || TextUtils.isEmpty(option.amount.currencyCode)
+ || TextUtils.isEmpty(option.amount.value)
+ || !itemsCurrencyCode.equals(option.amount.currencyCode)
+ || !formatter.isValidAmountValue(option.amount.value)) {
+ return null;
+ }
+
+ result.add(new PaymentOption(option.id, option.label,
+ formatter.format(option.amount.value), PaymentOption.NO_ICON));
+ }
+
+ return new SectionInformation(result.size() == 1 ? 0 : SectionInformation.NO_SELECTION,
+ result);
+ }
+
+ private JSONObject getValidatedData(Set<String> supportedMethods, String stringifiedData) {
+ if (TextUtils.isEmpty(stringifiedData)) return new JSONObject();
+
+ JSONObject result;
+ try {
+ result = new JSONObject(stringifiedData);
+ } catch (JSONException e) {
+ // Payment method specific data should be a JSON object.
+ return null;
+ }
+
+ Iterator<String> it = result.keys();
+ while (it.hasNext()) {
+ String name = it.next();
+ // Each key should be one of the supported payment methods.
+ if (!supportedMethods.contains(name)) return null;
+ // Each value should be a JSON object.
+ if (result.optJSONObject(name) == null) return null;
+ }
+
+ return result;
+ }
+
+ /**
+ * Called to retrieve the data to show in the initial PaymentRequest UI.
+ */
+ @Override
+ public void getDefaultPaymentInformation(Callback<PaymentInformation> callback) {
+ mPaymentInformationCallback = callback;
+
+ if (mPaymentMethods == null) return;
+
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ provideDefaultPaymentInformation();
+ }
+ });
+ }
+
+ private void provideDefaultPaymentInformation() {
+ mPaymentInformationCallback.onResult(new PaymentInformation(
+ mLineItems.get(mLineItems.size() - 1), mShippingAddresses.getSelectedItem(),
+ mShippingOptions.getSelectedItem(), mPaymentMethods.getSelectedItem()));
+ mPaymentInformationCallback = null;
+ }
+
+ @Override
+ public void getLineItems(final Callback<List<LineItem>> callback) {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onResult(mLineItems);
+ }
+ });
+ }
+
+ @Override
+ public void getShippingAddresses(final Callback<SectionInformation> callback) {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onResult(mShippingAddresses);
+ }
+ });
+ }
+
+ @Override
+ public void getShippingOptions(final Callback<SectionInformation> callback) {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onResult(mShippingOptions);
+ }
+ });
+ }
+
+ @Override
+ public void getPaymentMethods(final Callback<SectionInformation> callback) {
+ assert mPaymentMethods != null;
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onResult(mPaymentMethods);
+ }
+ });
+ }
+
+ @Override
+ public void onShippingAddressChanged(PaymentOption selectedShippingAddress) {
+ assert selectedShippingAddress instanceof AutofillAddress;
+ mShippingAddresses.setSelectedItem(selectedShippingAddress);
+ mClient.onShippingAddressChange(
+ ((AutofillAddress) selectedShippingAddress).toShippingAddress());
+ }
+
+ @Override
+ public void onShippingOptionChanged(PaymentOption selectedShippingOption) {
+ mShippingOptions.setSelectedItem(selectedShippingOption);
+ mClient.onShippingOptionChange(selectedShippingOption.getIdentifier());
+ }
+
+ @Override
+ public void onPaymentMethodChanged(PaymentOption selectedPaymentMethod) {
+ assert selectedPaymentMethod instanceof PaymentInstrument;
+ mPaymentMethods.setSelectedItem(selectedPaymentMethod);
+ }
+
+ @Override
+ public void onPayClicked(PaymentOption selectedShippingAddress,
+ PaymentOption selectedShippingOption, PaymentOption selectedPaymentMethod) {
+ assert selectedPaymentMethod instanceof PaymentInstrument;
+ PaymentInstrument instrument = (PaymentInstrument) selectedPaymentMethod;
+ instrument.getDetails(mMerchantName, mOrigin, mPaymentItems,
+ mData.optJSONObject(instrument.getMethodName()), this);
+ }
+
+ @Override
+ public void onDismiss() {
+ disconnectFromClientWithDebugMessage("Dialog dismissed");
+ closeUI(false);
+ }
/**
* Called by the merchant website to abort the payment.
*/
@Override
- public void abort() {}
+ public void abort() {
+ mClient = null;
+ closeUI(false);
+ }
/**
* Called when the merchant website has processed the payment.
*/
@Override
- public void complete(boolean success) {}
+ public void complete(boolean success) {
+ closeUI(success);
+ }
/**
* Called when the renderer closes the Mojo connection.
*/
@Override
- public void close() {}
+ public void close() {
+ mClient = null;
+ closeUI(false);
+ }
/**
* Called when the Mojo connection encounters an error.
*/
@Override
- public void onConnectionError(MojoException e) {}
+ public void onConnectionError(MojoException e) {
+ mClient = null;
+ closeUI(false);
+ }
+
+ /**
+ * Called after retrieving the list of payment instruments in an app.
+ */
+ @Override
+ public void onInstrumentsReady(PaymentApp app, List<PaymentInstrument> instruments) {
+ mPendingApps.remove(app);
+
+ if (instruments != null) {
+ for (int i = 0; i < instruments.size(); i++) {
+ PaymentInstrument instrument = instruments.get(i);
+ if (mSupportedMethods.contains(instrument.getMethodName())) {
+ mPendingInstruments.add(instrument);
+ } else {
+ instrument.dismiss();
+ }
+ }
+ }
+
+ if (mPendingApps.isEmpty()) {
+ if (mPendingInstruments.isEmpty()) {
+ mPaymentMethods = new SectionInformation();
+ } else {
+ mPaymentMethods = new SectionInformation(0, mPendingInstruments);
+ mPendingInstruments.clear();
+ }
+
+ if (mPaymentInformationCallback != null) provideDefaultPaymentInformation();
+ }
+ }
+
+ /**
+ * Called after retrieving instrument details.
+ */
+ @Override
+ public void onInstrumentDetailsReady(String methodName, String stringifiedDetails) {
+ PaymentResponse response = new PaymentResponse();
+ response.methodName = methodName;
+ response.stringifiedDetails = stringifiedDetails;
+ mClient.onPaymentResponse(response);
+ }
+
+ /**
+ * Called if unable to retrieve instrument details.
+ */
+ @Override
+ public void onInstrumentDetailsError() {
+ disconnectFromClientWithDebugMessage("Fialed to retrieve payment instrument details");
+ closeUI(false);
+ }
+
+ private void disconnectFromClientWithDebugMessage(String debugMessage) {
+ Log.d(TAG, debugMessage);
+ mClient.onError();
+ mClient = null;
+ }
+
+ /**
+ * Closes the UI. If the client is still connected, then it's notified of UI hiding.
+ */
+ private void closeUI(boolean paymentSuccess) {
+ if (mUI != null) {
+ mUI.close(paymentSuccess, new Runnable() {
+ @Override
+ public void run() {
+ if (mClient == null) return;
+ mClient.onComplete();
+ mClient = null;
+ }
+ });
+ mUI = null;
+ }
+
+ if (mPaymentMethods != null) {
+ for (int i = 0; i < mPaymentMethods.getSize(); i++) {
+ PaymentOption option = mPaymentMethods.getItem(i);
+ assert option instanceof PaymentInstrument;
+ ((PaymentInstrument) option).dismiss();
+ }
+ mPaymentMethods = null;
+ }
+ }
}

Powered by Google App Engine
This is Rietveld 408576698