| Index: chrome/android/java/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFactory.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFactory.java
|
| index ca17fba70d3f7331e3b8ac1aa571d33828819049..eff6db526231c505906a239c5e562b0b64d07407 100644
|
| --- a/chrome/android/java/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFactory.java
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFactory.java
|
| @@ -6,85 +6,339 @@ package org.chromium.chrome.browser.payments;
|
|
|
| import android.content.Context;
|
| import android.content.Intent;
|
| +import android.content.pm.PackageInfo;
|
| import android.content.pm.PackageManager;
|
| +import android.content.pm.PackageManager.NameNotFoundException;
|
| import android.content.pm.ResolveInfo;
|
| +import android.content.pm.Signature;
|
| import android.graphics.drawable.Drawable;
|
| import android.net.Uri;
|
| import android.util.Pair;
|
|
|
| import org.chromium.base.ContextUtils;
|
| +import org.chromium.chrome.browser.payments.ManifestParser.Manifest;
|
| +import org.chromium.chrome.browser.payments.ManifestParser.ManifestParseCallback;
|
| import org.chromium.chrome.browser.payments.PaymentAppFactory.PaymentAppCreatedCallback;
|
| import org.chromium.chrome.browser.payments.PaymentAppFactory.PaymentAppFactoryAddition;
|
| +import org.chromium.chrome.browser.payments.PaymentManifestDownloader.ManifestDownloadCallback;
|
| import org.chromium.content_public.browser.WebContents;
|
|
|
| +import java.util.ArrayList;
|
| import java.util.HashMap;
|
| +import java.util.HashSet;
|
| import java.util.List;
|
| import java.util.Map;
|
| import java.util.Set;
|
|
|
| /** Builds instances of payment apps based on installed third party Android payment apps. */
|
| public class AndroidPaymentAppFactory implements PaymentAppFactoryAddition {
|
| - private static final String ACTION_IS_READY_TO_PAY =
|
| - "org.chromium.intent.action.IS_READY_TO_PAY";
|
| -
|
| /** Prefix of all method names that can be constructed using this factory. */
|
| - static final String METHOD_PREFIX = "https://";
|
| + public static final String METHOD_PREFIX = "https://";
|
|
|
| - /** The action name for the Pay Basic-card Intent. */
|
| - private static final String ACTION_PAY_BASIC_CARD = "org.chromium.intent.action.PAY_BASIC_CARD";
|
| + @Override
|
| + public void create(Context context, WebContents webContents, Set<String> methods,
|
| + PaymentAppCreatedCallback callback) {
|
| + new PaymentAppFinder(context, webContents, methods, callback).find();
|
| + }
|
|
|
| /**
|
| - * The basic-card payment method name used by merchant and defined by W3C:
|
| - * https://w3c.github.io/webpayments-methods-card/#method-id
|
| + * Finds installed native Android payment apps and verifies their signatures according to the
|
| + * payment method manifests. The "basic-card" payment method is an exception: it's a common
|
| + * payment method that can be used by any payment app.
|
| */
|
| - private static final String BASIC_CARD_PAYMENT_METHOD = "basic-card";
|
| + private static class PaymentAppFinder {
|
| + /** The name of the intent for the service to check whether an app is ready to pay. */
|
| + private static final String ACTION_IS_READY_TO_PAY =
|
| + "org.chromium.intent.action.IS_READY_TO_PAY";
|
|
|
| - @Override
|
| - public void create(Context context, WebContents webContents, Set<String> methods,
|
| - PaymentAppCreatedCallback callback) {
|
| - Map<String, AndroidPaymentApp> installedApps = new HashMap<>();
|
| - PackageManager pm = context.getPackageManager();
|
| - Intent payIntent = new Intent();
|
| + /** The name of the intent for the action of paying using "basic-card" method. */
|
| + private static final String ACTION_PAY_BASIC_CARD =
|
| + "org.chromium.intent.action.PAY_BASIC_CARD";
|
|
|
| - for (String methodName : methods) {
|
| - if (methodName.startsWith(METHOD_PREFIX)) {
|
| - payIntent.setAction(AndroidPaymentApp.ACTION_PAY);
|
| - payIntent.setData(Uri.parse(methodName));
|
| - } else if (methodName.equals(BASIC_CARD_PAYMENT_METHOD)) {
|
| + /**
|
| + * The basic-card payment method name used by merchant and defined by W3C:
|
| + * https://w3c.github.io/webpayments-methods-card/#method-id
|
| + */
|
| + private static final String BASIC_CARD_PAYMENT_METHOD = "basic-card";
|
| +
|
| +
|
| + /** The maximum number of payment method manifests to download. */
|
| + private static final int MAX_NUMBER_OF_MANIFESTS = 10;
|
| +
|
| + private final Context mContext;
|
| + private final WebContents mWebContents;
|
| + private final PaymentAppCreatedCallback mCallback;
|
| +
|
| + /**
|
| + * A map of payment method names to the list of unverified (yet) Android apps that claim to
|
| + * handle these methods.
|
| + */
|
| + private final Map<String, Set<ResolveInfo>> mPendingApps;
|
| +
|
| + /** A map of Android package name to the payment app. */
|
| + private final Map<String, AndroidPaymentApp> mResult;
|
| +
|
| + /**
|
| + * Builds a native Android payment app finder.
|
| + *
|
| + * @param context The application context.
|
| + * @param webContents The web contents that invoked the web payments API.
|
| + * @param methods The list of payment methods requested by the merchant.
|
| + * @param callback The asynchronous callback to be invoked (on the UI thread) when all
|
| + * Android payment apps have been found.
|
| + */
|
| + public PaymentAppFinder(Context context, WebContents webContents, Set<String> methods,
|
| + PaymentAppCreatedCallback callback) {
|
| + mContext = context;
|
| + mWebContents = webContents;
|
| + mCallback = callback;
|
| + mPendingApps = new HashMap<>();
|
| + for (String method : methods) {
|
| + mPendingApps.put(method, null);
|
| + }
|
| + mResult = new HashMap<>();
|
| + }
|
| +
|
| + /** Looks for native Android payment apps. */
|
| + public void find() {
|
| + PackageManager pm = mContext.getPackageManager();
|
| + Intent payIntent = new Intent();
|
| +
|
| + if (mPendingApps.containsKey(BASIC_CARD_PAYMENT_METHOD)) {
|
| payIntent.setAction(ACTION_PAY_BASIC_CARD);
|
| - payIntent.setData(null);
|
| - } else {
|
| - continue;
|
| + List<ResolveInfo> apps = pm.queryIntentActivities(payIntent, 0);
|
| + if (!apps.isEmpty()) {
|
| + mPendingApps.put(BASIC_CARD_PAYMENT_METHOD, new HashSet<>(apps));
|
| + } else {
|
| + mPendingApps.remove(BASIC_CARD_PAYMENT_METHOD);
|
| + }
|
| }
|
|
|
| - List<ResolveInfo> matches = pm.queryIntentActivities(payIntent, 0);
|
| - for (int i = 0; i < matches.size(); i++) {
|
| - ResolveInfo match = matches.get(i);
|
| - String packageName = match.activityInfo.packageName;
|
| - // Do not recommend disabled apps.
|
| - if (!PaymentPreferencesUtil.isAndroidPaymentAppEnabled(packageName)) continue;
|
| - AndroidPaymentApp installedApp = installedApps.get(packageName);
|
| - if (installedApp == null) {
|
| - CharSequence label = match.loadLabel(pm);
|
| - installedApp =
|
| - new AndroidPaymentApp(webContents, packageName, match.activityInfo.name,
|
| - label == null ? "" : label.toString(), match.loadIcon(pm));
|
| - callback.onPaymentAppCreated(installedApp);
|
| - installedApps.put(packageName, installedApp);
|
| + payIntent.setAction(AndroidPaymentApp.ACTION_PAY);
|
| + List<PaymentManifestVerifier> verifiers = new ArrayList<>();
|
| + for (String methodName : mPendingApps.keySet()) {
|
| + if (!methodName.startsWith(METHOD_PREFIX)) continue;
|
| +
|
| + payIntent.setData(Uri.parse(methodName));
|
| + List<ResolveInfo> apps = pm.queryIntentActivities(payIntent, 0);
|
| + if (apps.isEmpty()) continue;
|
| +
|
| + verifiers.add(new PaymentManifestVerifier(mWebContents, methodName, apps));
|
| + mPendingApps.put(methodName, new HashSet<>(apps));
|
| + if (verifiers.size() == MAX_NUMBER_OF_MANIFESTS) break;
|
| + }
|
| +
|
| + Set<ResolveInfo> basicCardPendingApps = mPendingApps.get(BASIC_CARD_PAYMENT_METHOD);
|
| + if (basicCardPendingApps != null) {
|
| + // Iterate over a copy of the set, because onValidPaymentApp alters the original.
|
| + Set<ResolveInfo> copySet = new HashSet<>(basicCardPendingApps);
|
| + for (ResolveInfo app : copySet) {
|
| + onValidPaymentApp(BASIC_CARD_PAYMENT_METHOD, app);
|
| }
|
| - installedApp.addMethodName(methodName);
|
| }
|
| +
|
| + for (int i = 0; i < verifiers.size(); i++) {
|
| + verifiers.get(i).verify();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Enables invoking the given native Android payment app for the given payment method.
|
| + * Called when the app has been found to have the right privileges to handle this payment
|
| + * method.
|
| + *
|
| + * @param methodName The payment method name that the payment app offers to handle.
|
| + * @param resolveInfo Identifying information for the native Android payment app.
|
| + */
|
| + private void onValidPaymentApp(String methodName, ResolveInfo resolveInfo) {
|
| + PackageManager pm = mContext.getPackageManager();
|
| + String packageName = resolveInfo.activityInfo.packageName;
|
| + AndroidPaymentApp app = mResult.get(packageName);
|
| + if (app == null) {
|
| + CharSequence label = resolveInfo.loadLabel(pm);
|
| + app = new AndroidPaymentApp(mWebContents, packageName,
|
| + resolveInfo.activityInfo.name, label == null ? "" : label.toString(),
|
| + resolveInfo.loadIcon(pm));
|
| + mResult.put(packageName, app);
|
| + }
|
| + app.addMethodName(methodName);
|
| + removePendingApp(methodName, resolveInfo);
|
| + }
|
| +
|
| + /**
|
| + * Disables invoking the given native Android payment app for the given payment method.
|
| + * Called when the app has been found to not have the right privileges to handle this
|
| + * payment app.
|
| + *
|
| + * @param methodName The payment method name that the payment app offers to handle.
|
| + * @param resolveInfo Identifying information for the native Android payment app.
|
| + */
|
| + private void onInvalidApp(String methodName, ResolveInfo resolveInfo) {
|
| + removePendingApp(methodName, resolveInfo);
|
| + }
|
| +
|
| + /** Removes the method/app pair from the list of pending information to be verified. */
|
| + private void removePendingApp(String methodName, ResolveInfo resolveInfo) {
|
| + Set<ResolveInfo> pendingAppsForMethod = mPendingApps.get(methodName);
|
| + pendingAppsForMethod.remove(resolveInfo);
|
| + if (pendingAppsForMethod.isEmpty()) mPendingApps.remove(methodName);
|
| + if (mPendingApps.isEmpty()) onSearchFinished();
|
| + }
|
| +
|
| + /**
|
| + * Disables invoking any native Android payment app for the given payment method. Called if
|
| + * unable to download or parse the payment method manifest.
|
| + *
|
| + * @param methodName The payment method name that has an invalid payment method manifest.
|
| + */
|
| + private void onInvalidManifest(String methodName) {
|
| + mPendingApps.remove(methodName);
|
| + if (mPendingApps.isEmpty()) onSearchFinished();
|
| }
|
|
|
| - List<ResolveInfo> matches = pm.queryIntentServices(new Intent(ACTION_IS_READY_TO_PAY), 0);
|
| - for (int i = 0; i < matches.size(); i++) {
|
| - ResolveInfo match = matches.get(i);
|
| - String packageName = match.serviceInfo.packageName;
|
| - AndroidPaymentApp installedApp = installedApps.get(packageName);
|
| - if (installedApp != null) installedApp.setIsReadyToPayAction(match.serviceInfo.name);
|
| + /**
|
| + * Checks for IS_READY_TO_PAY service in each valid payment app and returns the valid apps
|
| + * to the caller. Called when finished verifying all payment methods and apps.
|
| + */
|
| + private void onSearchFinished() {
|
| + List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServices(
|
| + new Intent(ACTION_IS_READY_TO_PAY), 0);
|
| + for (int i = 0; i < resolveInfos.size(); i++) {
|
| + ResolveInfo resolveInfo = resolveInfos.get(i);
|
| + AndroidPaymentApp app = mResult.get(resolveInfo.serviceInfo.packageName);
|
| + if (app != null) {
|
| + app.setIsReadyToPayAction(resolveInfo.serviceInfo.name);
|
| + mCallback.onPaymentAppCreated(app);
|
| + }
|
| + }
|
| +
|
| + mCallback.onAllPaymentAppsCreated();
|
| }
|
|
|
| - callback.onAllPaymentAppsCreated();
|
| + /**
|
| + * Verifies that the discovered native Android payment apps have the sufficient privileges
|
| + * to handle a single payment method. Downloads and parses the manifest to compare package
|
| + * names, versions, and signatures to the apps.
|
| + */
|
| + private class PaymentManifestVerifier
|
| + implements ManifestDownloadCallback, ManifestParseCallback {
|
| + private final WebContents mWebContents;
|
| + private final String mMethodName;
|
| + private final List<AppInfo> mMatchingApps;
|
| +
|
| + /** Identifying information about an installed native Android payment app. */
|
| + private class AppInfo {
|
| + /** Identifies a native Android payment app. */
|
| + public ResolveInfo resolveInfo;
|
| +
|
| + /** The version code for the native Android payment app, e.g., 123. */
|
| + public long version;
|
| +
|
| + /**
|
| + * The SHA256 certificate fingerprints for the native Android payment app, .e.g,
|
| + * ["308201dd30820146020101300d06092a864886f70d01010505003037311630140"]. Order does
|
| + * not matter for comparison.
|
| + */
|
| + public Set<String> sha256CertFingerprints;
|
| + }
|
| +
|
| + /**
|
| + * Builds the manifest verifier.
|
| + *
|
| + * @param webContents The web contents to use as the context when downloading the
|
| + * payment app manifest. If the web contents go away, the download
|
| + * is cancelled.
|
| + * @param methodName The name of the payment method name that apps offer to handle.
|
| + * Must be a valid URL that starts with "https://".
|
| + * @param matchingApps The identifying information for the native Android payment apps
|
| + * that offer to handle this payment method.
|
| + */
|
| + public PaymentManifestVerifier(
|
| + WebContents webContents, String methodName, List<ResolveInfo> matchingApps) {
|
| + mWebContents = webContents;
|
| + mMethodName = methodName;
|
| + mMatchingApps = new ArrayList<>();
|
| + for (int i = 0; i < matchingApps.size(); i++) {
|
| + AppInfo appInfo = new AppInfo();
|
| + appInfo.resolveInfo = matchingApps.get(i);
|
| + mMatchingApps.add(appInfo);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Verifies that the discovered native Android payment apps have the sufficient
|
| + * privileges to handle this payment method.
|
| + */
|
| + public void verify() {
|
| + PaymentManifestDownloader.download(mWebContents, mMethodName, this);
|
| + }
|
| +
|
| + @Override
|
| + public void onManifestDownloadSuccess(String content) {
|
| + ManifestParser.parse(content, this);
|
| + }
|
| +
|
| + @Override
|
| + public void onManifestDownloadFailure() {
|
| + onInvalidManifest(mMethodName);
|
| + }
|
| +
|
| + @Override
|
| + public void onManifestParseSuccess(List<Manifest> manifests) {
|
| + for (int i = 0; i < manifests.size(); i++) {
|
| + Manifest manifest = manifests.get(i);
|
| + if ("*".equals(manifest.packageName)) {
|
| + for (int j = 0; j < mMatchingApps.size(); j++) {
|
| + onValidPaymentApp(mMethodName, mMatchingApps.get(j).resolveInfo);
|
| + }
|
| + return;
|
| + }
|
| + }
|
| +
|
| + PackageManager pm = mContext.getPackageManager();
|
| + for (int i = 0; i < mMatchingApps.size(); i++) {
|
| + AppInfo appInfo = mMatchingApps.get(i);
|
| + try {
|
| + PackageInfo packageInfo =
|
| + pm.getPackageInfo(appInfo.resolveInfo.activityInfo.packageName,
|
| + PackageManager.GET_SIGNATURES);
|
| + appInfo.version = packageInfo.versionCode;
|
| + appInfo.sha256CertFingerprints = new HashSet<>();
|
| + Signature[] signatures = packageInfo.signatures;
|
| + for (int j = 0; j < signatures.length; j++) {
|
| + appInfo.sha256CertFingerprints.add(signatures[j].toCharsString());
|
| + }
|
| + } catch (NameNotFoundException e) {
|
| + // Leaving appInfo.sha256CertFingerprints uninitialized will call
|
| + // onInvalidApp() for this app below.
|
| + }
|
| + }
|
| +
|
| + for (int i = 0; i < mMatchingApps.size(); i++) {
|
| + AppInfo appInfo = mMatchingApps.get(i);
|
| + boolean isAllowed = false;
|
| + for (int j = 0; j < manifests.size(); j++) {
|
| + Manifest manifest = manifests.get(j);
|
| + if (appInfo.resolveInfo.activityInfo.packageName.equals(
|
| + manifest.packageName)
|
| + && appInfo.version >= manifest.version
|
| + && appInfo.sha256CertFingerprints != null
|
| + && appInfo.sha256CertFingerprints.equals(
|
| + manifest.sha256CertFingerprints)) {
|
| + onValidPaymentApp(mMethodName, appInfo.resolveInfo);
|
| + isAllowed = true;
|
| + break;
|
| + }
|
| + }
|
| + if (!isAllowed) onInvalidApp(mMethodName, appInfo.resolveInfo);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void onManifestParseFailure() {
|
| + onInvalidManifest(mMethodName);
|
| + }
|
| + }
|
| }
|
|
|
| /**
|
|
|