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); |
+ } |
+ } |
} |
} |