Chromium Code Reviews| 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 e74843a71f50cc0f3739be8961eb62a1b79994b4..6ad5ab34af6d18624b32be8e979359d70b7450e8 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,77 +6,328 @@ 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.net.Uri; |
| +import org.chromium.chrome.browser.payments.ManifestDownloader.ManifestDownloadCallback; |
| +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.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"; |
| - private 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: |
|
gogerald1
2017/01/23 17:18:35
one more space for indentation
please use gerrit instead
2017/02/23 19:57:50
Done.
|
| + * 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); |
|
gogerald1
2017/01/23 17:18:35
nit: It looks a little more clear to me if we cons
please use gerrit instead
2017/02/23 19:57:50
Done.
|
| + } |
| + mResult = new HashMap<>(); |
| + } |
| + |
| + /** Initiates the processing of looking 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()) { |
|
gogerald1
2017/01/23 17:18:35
nits: positive logic first?
please use gerrit instead
2017/02/23 19:57:50
Done.
|
| + 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; |
| - 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(ManifestDownloader.MANIFEST_SCHEME)) continue; |
| + |
| + payIntent.setData(Uri.parse(methodName)); |
| + List<ResolveInfo> apps = pm.queryIntentActivities(payIntent, 0); |
| + if (apps.isEmpty()) continue; |
|
gogerald1
2017/01/23 17:18:34
record and remove these unsupported methods from m
please use gerrit instead
2017/02/23 19:57:50
I don't see the advantage of this. Please explain.
|
| + |
| + verifiers.add(new PaymentManifestVerifier(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); |
|
gogerald1
2017/01/23 17:18:34
Could we do this when creating AndroidPaymentApp s
please use gerrit instead
2017/02/23 19:57:50
I don't see how that would improve efficiency. Ple
|
| + 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); |
|
gogerald1
2017/01/23 17:18:34
Is this means that the app must support ACTION_IS_
please use gerrit instead
2017/02/23 19:57:50
Fixed. It should be optional.
|
| + } |
| + } |
| + |
| + 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 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 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(String methodName, List<ResolveInfo> matchingApps) { |
| + assert methodName != null; |
| + assert matchingApps != null; |
| + 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); |
| + } |
| + } |
| + |
| + /** |
| + * Begins the process of verifying that the discovered native Android payment apps have |
| + * the sufficient privileges to handle this payment method. |
| + */ |
| + public void verify() { |
| + ManifestDownloader.download(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)) { |
|
gogerald1
2017/01/23 17:18:35
add comments to explain * means all payment apps a
please use gerrit instead
2017/02/23 19:57:50
Done.
|
| + 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); |
| + } |
| + } |
| } |
| } |