Chromium Code Reviews| 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..94731360ca869e0323ca6c11e0d62c9bbb8ff560 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,120 @@ |
| package org.chromium.chrome.browser.payments; |
| +import android.app.Activity; |
| +import android.graphics.Bitmap; |
| +import android.text.TextUtils; |
| + |
| +import org.chromium.base.Callback; |
| +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.HashSet; |
| +import java.util.Iterator; |
| +import java.util.LinkedList; |
| +import java.util.List; |
| /** |
| * 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, |
|
Ted C
2016/04/22 03:59:37
indent 8 from the start of the previous line
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + PaymentInstrument.DetailsCallback { |
| + /** |
| + * The size for the favicon in density-independent pixels. |
| + */ |
| + private static final int FAVICON_SIZE_DP = 24; |
| + |
| + private Activity mContext; |
| + private String mMerchantName; |
| + private String mOrigin; |
| + private Bitmap mFavicon; |
| + private List<PaymentApp> mApps; |
| + private PaymentRequestClient mClient; |
| + private HashSet<String> mSupportedMethods; |
|
Ted C
2016/04/22 03:59:38
Set/List generic versions plz
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + private ArrayList<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 Callback<SectionInformation> mPaymentMethodsCallback; |
| + private boolean mClientCompletion; |
| + |
| /** |
| * 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; |
|
Ted C
2016/04/22 03:59:38
personal aside...we should really write a helper f
please use gerrit instead
2016/04/25 19:22:30
Acknowledged.
|
| + |
| + 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); |
| + } |
| /** |
| * Called by the renderer to provide an endpoint for callbacks. |
| */ |
| @Override |
| public void setClient(PaymentRequestClient client) { |
| - assert client != null; |
| - client.onError(); |
| + mClient = client; |
|
Ted C
2016/04/22 03:59:37
can this be called more than once?
should we asse
please use gerrit instead
2016/04/25 19:22:31
Should not be called more than once. Added the ass
|
| + if (mClient == null) return; |
| + if (mContext == null) mClient.onError(); |
| } |
| /** |
| @@ -37,29 +125,341 @@ 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) { |
| + mClient.onError(); |
|
Ted C
2016/04/22 03:59:37
for debugging, we might want to log each of these
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + return; |
| + } |
| + |
| + mSupportedMethods = getValidatedSupportedMethods(supportedMethods); |
| + if (mSupportedMethods == null) { |
| + mClient.onError(); |
| + return; |
| + } |
| + |
| + mLineItems = getValidatedLineItems(details); |
| + if (mLineItems == null) { |
| + mClient.onError(); |
| + return; |
| + } |
| + |
| + mShippingOptions = getValidatedShippingOptions(details); |
| + if (mShippingOptions == null) { |
| + mClient.onError(); |
| + return; |
| + } |
| + |
| + mData = getValidatedData(mSupportedMethods, stringifiedData); |
| + if (mData == null) { |
| + mClient.onError(); |
| + return; |
| + } |
| + |
| + List<AutofillAddress> addresses = new LinkedList<AutofillAddress>(); |
| + for (AutofillProfile profile : PersonalDataManager.getInstance().getProfiles()) { |
|
Ted C
2016/04/22 03:59:37
same comment about iterator vs for int i = 0...
a
please use gerrit instead
2016/04/25 19:22:32
Done.
|
| + addresses.add(new AutofillAddress(profile)); |
| + } |
| + |
| + if (addresses.isEmpty()) { |
| + mShippingAddresses = new SectionInformation(); |
| + } else { |
| + mShippingAddresses = new SectionInformation(0, addresses); |
| + } |
| + |
| + mPendingApps = new LinkedList<PaymentApp>(mApps); |
| + mPendingInstruments = new LinkedList<PaymentInstrument>(); |
| + boolean isGettingInstruments = false; |
| + |
| + for (PaymentApp app : mApps) { |
| + HashSet<String> appMethods = app.getSupportedMethodNames(); |
| + appMethods.retainAll(mSupportedMethods); |
| + if (!appMethods.isEmpty()) { |
| + isGettingInstruments = true; |
| + app.getInstruments(mLineItems, 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); |
|
Ted C
2016/04/22 03:59:37
should we set mFavicon = null after this?
please use gerrit instead
2016/04/25 19:22:32
Done.
|
| + } |
| + |
| + private HashSet<String> getValidatedSupportedMethods(String[] methods) { |
|
Ted C
2016/04/22 03:59:37
s/HashSet/Set
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + if (methods == null || methods.length == 0) return null; |
| + |
| + HashSet<String> result = new HashSet<String>(); |
|
Ted C
2016/04/22 03:59:37
when creating like this, you can do:
Set<String>
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + for (String method : methods) { |
| + if (TextUtils.isEmpty(method)) return null; |
|
Ted C
2016/04/22 03:59:37
continue or return?
please use gerrit instead
2016/04/25 19:22:31
"return null" to indicate invalid method name.
Ted C
2016/04/25 22:49:02
I'm still somewhat surprised we don't just skip ov
please use gerrit instead
2016/04/25 23:19:24
Acknowledged.
|
| + result.add(method); |
| + } |
| + |
| + return result; |
| + } |
| + |
| + private ArrayList<LineItem> getValidatedLineItems(PaymentDetails details) { |
|
Ted C
2016/04/22 03:59:37
List
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + if (null == details || details.items == null || details.items.length == 0) return null; |
|
Ted C
2016/04/22 03:59:37
details == null is the more consistent thing to do
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + |
| + for (PaymentItem item : details.items) { |
| + 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; |
| + } |
| + } |
| + |
| + PaymentItem total = details.items[details.items.length - 1]; |
| + String totalCurrencyCode = total.amount.currencyCode; |
| + CurrencyStringFormatter totalFormatter = new CurrencyStringFormatter(totalCurrencyCode); |
| + ArrayList<LineItem> result = new ArrayList<LineItem>(details.items.length); |
| + |
| + for (PaymentItem item : details.items) { |
|
Ted C
2016/04/22 03:59:37
i would do i < length - 1 and skip the == total ch
please use gerrit instead
2016/04/25 19:22:32
Done.
|
| + if (item == total) break; |
| + String currencyCode = item.amount.currencyCode; |
| + if (currencyCode.equals(totalCurrencyCode)) { |
| + result.add(new LineItem(item.label, "", totalFormatter.format(item.amount.value))); |
| + } else { |
| + result.add(new LineItem(item.label, currencyCode, |
|
Ted C
2016/04/22 03:59:37
yikes...we support multiple currencies in a single
please use gerrit instead
2016/04/25 19:22:31
Although the spec does not explicitly prohibit thi
|
| + new CurrencyStringFormatter(currencyCode).format(item.amount.value))); |
| + } |
| + } |
| + |
| + result.add(new LineItem( |
| + total.label, totalCurrencyCode, totalFormatter.format(total.amount.value))); |
| + |
| + return result; |
| + } |
| + |
| + private SectionInformation getValidatedShippingOptions(PaymentDetails details) { |
| + if (details.shippingOptions == null || details.shippingOptions.length == 0) { |
| + return new SectionInformation(); |
| + } |
| + |
| + ArrayList<PaymentOption> result = new ArrayList<PaymentOption>(); |
| + for (ShippingOption option : details.shippingOptions) { |
| + if (option == null || TextUtils.isEmpty(option.id) || TextUtils.isEmpty(option.label) |
| + || option.amount == null || TextUtils.isEmpty(option.amount.currencyCode) |
| + || TextUtils.isEmpty(option.amount.value)) { |
| + return null; |
|
Ted C
2016/04/22 03:59:37
return or continue?
please use gerrit instead
2016/04/25 19:22:31
"return null" to indicate invalid shipping options
|
| + } |
| + result.add(new PaymentOption(option.id, option.label, "", PaymentOption.NO_ICON)); |
| + } |
| + |
| + return new SectionInformation( |
| + result.size() == 1 ? 0 : SectionInformation.NO_SELECTION, result); |
| + } |
| + |
| + private JSONObject getValidatedData(HashSet<String> supportedMethods, String stringifiedData) { |
| + if (TextUtils.isEmpty(stringifiedData)) return new JSONObject(); |
| + |
| + JSONObject result; |
| + try { |
| + result = new JSONObject(stringifiedData); |
| + } catch (JSONException e) { |
| + return null; |
| + } |
| + |
| + Iterator<String> it = result.keys(); |
| + while (it.hasNext()) { |
| + String name = it.next(); |
| + if (!supportedMethods.contains(name)) return null; |
|
Ted C
2016/04/22 03:59:37
same...return or continue?
please use gerrit instead
2016/04/25 19:22:31
"return null" to indicate invalid data. I'll add a
|
| + 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; |
| + provideDefaultPaymentInformation(); |
|
Ted C
2016/04/22 03:59:37
in general I would avoid this indirection for now.
please use gerrit instead
2016/04/25 19:22:31
I call provideDefault() also when I've finished lo
|
| + } |
| + |
| + private void provideDefaultPaymentInformation() { |
| + mPaymentInformationCallback.onResult(new PaymentInformation( |
| + mLineItems.get(mLineItems.size() - 1), mShippingAddresses.getSelectedItem(), |
| + mShippingOptions.getSelectedItem(), mPaymentMethods.getSelectedItem())); |
| + mPaymentInformationCallback = null; |
| + } |
| + |
| + @Override |
| + public void getLineItems(Callback<ArrayList<LineItem>> callback) { |
| + callback.onResult(mLineItems); |
|
Ted C
2016/04/22 03:59:37
same async-ness for all the callbacks
please use gerrit instead
2016/04/25 19:22:31
Done.
|
| + } |
| + |
| + @Override |
| + public void getShippingAddresses(Callback<SectionInformation> callback) { |
| + callback.onResult(mShippingAddresses); |
| + } |
| + |
| + @Override |
| + public void getShippingOptions(Callback<SectionInformation> callback) { |
| + callback.onResult(mShippingOptions); |
| + } |
| + |
| + @Override |
| + public void getPaymentMethods(Callback<SectionInformation> callback) { |
| + mPaymentMethodsCallback = callback; |
| + if (mPaymentMethods == null) return; |
| + providePaymentMethods(); |
| + } |
| + |
| + private void providePaymentMethods() { |
| + mPaymentMethodsCallback.onResult(mPaymentMethods); |
| + mPaymentMethodsCallback = null; |
| + } |
| + |
| + @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, mLineItems, |
| + mData.optJSONObject(instrument.getMethodName()), this); |
| + } |
| + |
| + @Override |
| + public void onDismiss() { |
| + mClient.onError(); |
| + closeUI(false); |
| + } |
| /** |
| * Called by the merchant website to abort the payment. |
| */ |
| @Override |
| - public void abort() {} |
| + public void abort() { |
| + closeUI(false); |
| + } |
| /** |
| * Called when the merchant website has processed the payment. |
| */ |
| @Override |
| - public void complete(boolean success) {} |
| + public void complete(boolean success) { |
| + mClientCompletion = true; |
| + closeUI(success); |
| + } |
| /** |
| * Called when the renderer closes the Mojo connection. |
| */ |
| @Override |
| - public void close() {} |
| + public void close() { |
| + closeUI(false); |
| + } |
| /** |
| * Called when the Mojo connection encounters an error. |
| */ |
| @Override |
| - public void onConnectionError(MojoException e) {} |
| + public void onConnectionError(MojoException e) { |
| + 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 (PaymentInstrument instrument : instruments) { |
| + 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(); |
| + if (mPaymentMethodsCallback != null) providePaymentMethods(); |
| + } |
| + } |
| + |
| + /** |
| + * 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() { |
| + mClient.onError(); |
| + closeUI(false); |
| + } |
| + |
| + /** |
| + * Stop communications with the client and closes the UI. |
| + */ |
| + private void closeUI(boolean paymentSuccess) { |
| + if (mUI != null) { |
| + mUI.close(paymentSuccess, new Runnable() { |
| + @Override |
| + public void run() { |
| + if (!mClientCompletion) return; |
| + if (mClient == null) return; |
| + mClient.onComplete(); |
| + mClient = null; |
| + } |
| + }); |
| + mUI = null; |
| + } else { |
| + mClient = 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; |
| + } |
| + } |
| } |