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

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

Issue 2645813006: Download web payment manifests. (Closed)
Patch Set: "basic-card" robustness Created 3 years, 11 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/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);
+ }
+ }
}
}

Powered by Google App Engine
This is Rietveld 408576698