// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.payments;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.components.payments.PaymentManifestVerifier.ManifestVerifyCallback;
import org.chromium.components.payments.intent.WebPaymentIntentHelper;
import org.chromium.payments.mojom.PaymentDetailsModifier;
import org.chromium.payments.mojom.PaymentMethodData;
import org.chromium.url.GURL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Finds installed native Android payment apps and verifies their signatures according to the
* payment method manifests. The manifests are located based on the payment method name, which is a
* URL that starts with "https://" (localhosts can be "http://", however). The W3C-published non-URL
* payment method names are exceptions: these are common payment method names that do not have a
* manifest and can be used by any payment app.
*/
public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
private static final String TAG = "PaymentAppFinder";
private static final String PLAY_STORE_PACKAGE_NAME = "com.android.vending";
/** The maximum number of payment method manifests to download. */
private static final int MAX_NUMBER_OF_MANIFESTS = 10;
/** The name of the intent for the service to check whether an app is ready to pay. */
public static final String ACTION_IS_READY_TO_PAY =
"org.chromium.intent.action.IS_READY_TO_PAY";
/** Meta data name of an app's supported payment method names. */
public static final String META_DATA_NAME_OF_PAYMENT_METHOD_NAMES =
"org.chromium.payment_method_names";
/** Meta data name of an app's supported default payment method name. */
public static final String META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME =
"org.chromium.default_payment_method_name";
/** Meta data name of an app's supported delegations' list. */
public static final String META_DATA_NAME_OF_SUPPORTED_DELEGATIONS =
"org.chromium.payment_supported_delegations";
private final Set<GURL> mUrlPaymentMethods = new HashSet<>();
private final PaymentManifestDownloader mDownloader;
private final PaymentManifestWebDataService mWebDataService;
private final PaymentManifestParser mParser;
private final PackageManagerDelegate mPackageManagerDelegate;
private final PaymentAppFactoryDelegate mFactoryDelegate;
private final PaymentAppFactoryInterface mFactory;
private final boolean mIsOffTheRecord;
/**
* The app stores that supports app-store billing methods.
*
* key: the app-store app's package name, e.g., "com.google.vendor" (Google Play Store).
* value: the app-store app's billing method identifier, e.g.,
* "https://play.google.com/billing". Only valid GURLs are allowed.
*/
private final Map<String, GURL> mAppStores = new HashMap();
/**
* A mapping from an Android package name to the payment app with that package name. The apps
* will be sent to the <code>PaymentAppFactoryDelegate</code> once all of their payment methods
* have been validated. The package names are used for identification because they are unique on
* Android. Example contents:
*
* {"com.bobpay.app.v1": androidPaymentApp1, "com.alicepay.app.v1": androidPaymentApp2}
*/
private final Map<String, AndroidPaymentApp> mValidApps = new HashMap<>();
/**
* A mapping from origins of payment apps to the URL payment methods of these apps. Used to look
* up payment apps in <code>mVerifiedPaymentMethods</code> based on the supported origins that
* have been verified in <code>PaymentManifestVerifier</code>. Example contents:
*
* {"https://bobpay.com": ("https://bobpay.com/personal", "https://bobpay.com/business")}
*/
private final Map<GURL, Set<GURL>> mOriginToUrlDefaultMethodsMapping = new HashMap<>();
/**
* A mapping from URL payment methods to the applications that support this payment method,
* but not as their default payment method. Used to find all apps that claim support for a given
* URL payment method when the payment manifest of this method contains
* "supported_origins": "*". Example contents:
*
* {"https://bobpay.com/public-standard": (resolveInfo1, resolveInfo2, resolveInfo3)}
*/
private final Map<GURL, Set<ResolveInfo>> mMethodToSupportedAppsMapping = new HashMap<>();
/** Contains information about a URL payment method. */
private static final class PaymentMethod {
/** The default applications for this payment method. */
public final Set<ResolveInfo> defaultApplications = new HashSet<>();
/** The supported origins of this payment method. */
public final Set<GURL> supportedOrigins = new HashSet<>();
}
/**
* A mapping from URL payment methods to the verified information about these methods. Used to
* accumulate the incremental information that arrives from
* <code>PaymentManifestVerifier</code>s for each of the payment method manifests that need to
* be downloaded. Example contents:
*
* { "https://bobpay.com/business": method1, "https://bobpay.com/personal": method2}
*/
private final Map<GURL, PaymentMethod> mVerifiedPaymentMethods = new HashMap<>();
/*
* A mapping from package names to their IS_READY_TO_PAY service names, e.g.:
*
* {"com.bobpay.app": "com.bobpay.app.IsReadyToPayService"}
*/
private final Map<String, String> mIsReadyToPayServices = new HashMap<>();
private int mPendingVerifiersCount;
private int mPendingIsReadyToPayQueries;
private int mPendingResourceUsersCount;
private boolean mBypassIsReadyToPayServiceInTest;
/**
* Builds a finder for native Android payment apps.
*
* @param webDataService The web data service to cache manifest.
* @param downloader The manifest downloader.
* @param parser The manifest parser.
* @param packageManagerDelegate The package information retriever.
* @param factoryDelegate The merchant requested data and the asynchronous delegate to be
* invoked (on the UI thread) when all Android payment apps have been found.
* @param factory The factory to be used in the delegate.onDoneCreatingPaymentApps(factory)
* call.
*/
public AndroidPaymentAppFinder(
PaymentManifestWebDataService webDataService,
PaymentManifestDownloader downloader,
PaymentManifestParser parser,
PackageManagerDelegate packageManagerDelegate,
PaymentAppFactoryDelegate factoryDelegate,
PaymentAppFactoryInterface factory) {
mFactoryDelegate = factoryDelegate;
mAppStores.put(PLAY_STORE_PACKAGE_NAME, new GURL(MethodStrings.GOOGLE_PLAY_BILLING));
for (GURL method : mAppStores.values()) {
assert method.isValid();
}
mDownloader = downloader;
mWebDataService = webDataService;
mParser = parser;
mPackageManagerDelegate = packageManagerDelegate;
mFactory = factory;
assert mFactoryDelegate.getParams() != null;
mIsOffTheRecord = mFactoryDelegate.getParams().isOffTheRecord();
}
private void findAppStoreBillingApp(List<ResolveInfo> allInstalledPaymentApps) {
assert !mFactoryDelegate.getParams().hasClosed();
String twaPackageName = mFactoryDelegate.getParams().getTwaPackageName();
if (TextUtils.isEmpty(twaPackageName)) return;
ResolveInfo twaApp = findAppWithPackageName(allInstalledPaymentApps, twaPackageName);
if (twaApp == null) return;
List<String> agreedAppStoreMethods = new ArrayList<>();
for (GURL appStoreUriMethod : mAppStores.values()) {
assert appStoreUriMethod != null;
assert appStoreUriMethod.isValid();
String appStoreMethod = removeTrailingSlash(appStoreUriMethod.getSpec());
assert appStoreMethod != null;
if (!mFactoryDelegate.getParams().getMethodData().containsKey(appStoreMethod)) continue;
if (!paymentAppSupportsUriMethod(twaApp, appStoreUriMethod)) continue;
agreedAppStoreMethods.add(appStoreMethod);
}
boolean allowTwaInstalledFromAnySource =
PaymentFeatureList.isEnabled(
PaymentFeatureList.WEB_PAYMENTS_APP_STORE_BILLING_DEBUG);
if (!allowTwaInstalledFromAnySource) {
String installerPackageName =
mPackageManagerDelegate.getInstallerPackage(twaPackageName);
if (installerPackageName == null) return;
GURL appStoreUriMethod = mAppStores.get(installerPackageName);
if (appStoreUriMethod == null) return;
assert appStoreUriMethod.isValid();
String method = appStoreUriMethod.getSpec();
if (!agreedAppStoreMethods.contains(method)) return;
onValidPaymentAppForPaymentMethodName(twaApp, method);
} else {
for (String appStoreMethod : agreedAppStoreMethods) {
onValidPaymentAppForPaymentMethodName(twaApp, appStoreMethod);
}
}
AndroidPaymentApp app = mValidApps.get(twaPackageName);
if (app != null) app.setIsPreferred(true);
}
private boolean paymentAppSupportsUriMethod(ResolveInfo app, GURL urlMethod) {
String defaultMethod =
app.activityInfo.metaData == null
? null
: app.activityInfo.metaData.getString(
META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME);
GURL defaultUrlMethod = new GURL(defaultMethod);
assert urlMethod.isValid();
return (getSupportedPaymentMethods(app.activityInfo).contains(urlMethod.getSpec()))
|| (urlMethod.equals(defaultUrlMethod));
}
private ResolveInfo findAppWithPackageName(List<ResolveInfo> apps, String packageName) {
assert packageName != null;
for (int i = 0; i < apps.size(); i++) {
ResolveInfo app = apps.get(i);
String appPackageName = app.activityInfo.packageName;
if (packageName.equals(appPackageName)) return app;
}
return null;
}
/**
* Finds and validates the installed android payment apps that support the payment method names
* that the merchant is using.
*/
public void findAndroidPaymentApps() {
if (mFactoryDelegate.getParams().hasClosed()) return;
for (String method : mFactoryDelegate.getParams().getMethodData().keySet()) {
assert !TextUtils.isEmpty(method);
GURL url = new GURL(method); // Only URL payment method names are supported.
if (mAppStores.containsValue(url)) continue;
if (UrlUtil.isValidUrlBasedPaymentMethodIdentifier(url)) {
mUrlPaymentMethods.add(url);
}
}
List<ResolveInfo> allInstalledPaymentApps =
mPackageManagerDelegate.getActivitiesThatCanRespondToIntentWithMetaData(
new Intent(WebPaymentIntentHelper.ACTION_PAY));
if (allInstalledPaymentApps.isEmpty()) {
onAllAppsFoundAndValidated();
return;
}
if (!mIsOffTheRecord) {
List<ResolveInfo> services =
mPackageManagerDelegate.getServicesThatCanRespondToIntent(
new Intent(ACTION_IS_READY_TO_PAY));
int numberOfServices = services.size();
for (int i = 0; i < numberOfServices; i++) {
ServiceInfo service = services.get(i).serviceInfo;
mIsReadyToPayServices.put(service.packageName, service.name);
}
}
if (!PaymentOptionsUtils.requestAnyInformation(
mFactoryDelegate.getParams().getPaymentOptions())
&& PaymentFeatureList.isEnabled(
PaymentFeatureList.WEB_PAYMENTS_APP_STORE_BILLING)) {
findAppStoreBillingApp(allInstalledPaymentApps);
}
// All URL methods for which manifests should be downloaded. For example, if merchant
// supports "https://bobpay.com/personal" payment method, but user also has Alice Pay app
// that has the default payment method name of "https://alicepay.com/webpay" that claims to
// support "https://bobpay.com/personal" method as well, then both of these methods will be
// in this set:
//
// ("https://bobpay.com/personal", "https://alicepay.com/webpay")
Set<GURL> urlMethods = new HashSet<>(mUrlPaymentMethods);
// A mapping from all known payment method names to the corresponding payment apps that
// claim to support these payment methods. Example contents:
//
// {"basic-card": (bobPay, alicePay), "https://alicepay.com/webpay": (alicePay)}
//
// In case of non-URL payment methods, such as "basic-card", all apps that claim to support
// it are considered valid. In case of URL payment methods, if no apps claim to support a
// URL method, then no information will be downloaded for this method.
Map<String, Set<ResolveInfo>> methodToAppsMapping = new HashMap<>();
// A mapping from URL payment method names to the corresponding default payment apps. The
// payment manifest verifiers compare these apps against the information in
// "default_applications" of the payment method manifests to determine the validity of these
// apps. Example contents:
//
// {"https://bobpay.com/personal": (bobPay), "https://alicepay.com/webpay": (alicePay)}
Map<GURL, Set<ResolveInfo>> urlMethodToDefaultAppsMapping = new HashMap<>();
// A mapping from URL payment method names to the origins of the payment apps that support
// that method name. The payment manifest verifiers compare these origins against the
// information in "supported_origins" of the payment method manifests to determine validity
// of these origins. Example contents:
//
// {"https://bobpay.com/personal": ("https://alicepay.com")}
Map<GURL, Set<GURL>> urlMethodToSupportedOriginsMapping = new HashMap<>();
for (int i = 0; i < allInstalledPaymentApps.size(); i++) {
ResolveInfo app = allInstalledPaymentApps.get(i);
String defaultMethod =
app.activityInfo.metaData == null
? null
: app.activityInfo.metaData.getString(
META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME);
GURL appOrigin = null;
GURL defaultUrlMethod = null;
if (!TextUtils.isEmpty(defaultMethod)) {
defaultUrlMethod = new GURL(defaultMethod);
// Do not download any manifests for the app whose default payment method identifier
// is an app store payment method identifier, because app store method URLs are used
// only for identification and do not host manifest files.
if (mAppStores.values().contains(defaultUrlMethod)) {
continue;
}
if (UrlUtil.isURLValid(defaultUrlMethod)) {
defaultMethod = urlToStringWithoutTrailingSlash(defaultUrlMethod);
}
if (!methodToAppsMapping.containsKey(defaultMethod)) {
methodToAppsMapping.put(defaultMethod, new HashSet<ResolveInfo>());
}
methodToAppsMapping.get(defaultMethod).add(app);
if (UrlUtil.isURLValid(defaultUrlMethod)) {
urlMethods.add(defaultUrlMethod);
if (!urlMethodToDefaultAppsMapping.containsKey(defaultUrlMethod)) {
urlMethodToDefaultAppsMapping.put(
defaultUrlMethod, new HashSet<ResolveInfo>());
}
urlMethodToDefaultAppsMapping.get(defaultUrlMethod).add(app);
appOrigin = defaultUrlMethod.getOrigin();
if (!mOriginToUrlDefaultMethodsMapping.containsKey(appOrigin)) {
mOriginToUrlDefaultMethodsMapping.put(appOrigin, new HashSet<GURL>());
}
mOriginToUrlDefaultMethodsMapping.get(appOrigin).add(defaultUrlMethod);
}
}
// Note that a payment app with non-URL default payment method (e.g., "basic-card")
// can support URL payment methods (e.g., "https://bobpay.com/public-standard").
Set<String> supportedMethods = getSupportedPaymentMethods(app.activityInfo);
for (String supportedMethod : supportedMethods) {
GURL supportedUrlMethod = new GURL(supportedMethod);
if (!UrlUtil.isURLValid(supportedUrlMethod)) supportedUrlMethod = null;
if (supportedUrlMethod != null && supportedUrlMethod.equals(defaultUrlMethod)) {
continue;
}
// Ignore payment method identifiers of app stores, because app store method URLs
// are used only for identification and do not host manifest files.
if (mAppStores.values().contains(supportedUrlMethod)) {
continue;
}
if (!methodToAppsMapping.containsKey(supportedMethod)) {
methodToAppsMapping.put(supportedMethod, new HashSet<ResolveInfo>());
}
methodToAppsMapping.get(supportedMethod).add(app);
if (supportedUrlMethod == null) continue;
if (!mMethodToSupportedAppsMapping.containsKey(supportedUrlMethod)) {
mMethodToSupportedAppsMapping.put(
supportedUrlMethod, new HashSet<ResolveInfo>());
}
mMethodToSupportedAppsMapping.get(supportedUrlMethod).add(app);
if (appOrigin == null) continue;
if (!urlMethodToSupportedOriginsMapping.containsKey(supportedUrlMethod)) {
urlMethodToSupportedOriginsMapping.put(supportedUrlMethod, new HashSet<GURL>());
}
urlMethodToSupportedOriginsMapping.get(supportedUrlMethod).add(appOrigin);
}
// Record the total number of payment methods that this activity `ResolveInfo app`
// declares to support in its metadata.
if (!TextUtils.isEmpty(defaultMethod)) supportedMethods.add(defaultMethod);
RecordHistogram.recordCustomCountHistogram(
/* name= */ "PaymentRequest.NumberOfSupportedMethods.AndroidApp",
/* sample= */ supportedMethods.size(),
/* min= */ 1,
/* max= */ 10,
/* numBuckets= */ 10);
}
List<PaymentManifestVerifier> manifestVerifiers = new ArrayList<>();
for (GURL urlMethodName : urlMethods) {
if (!methodToAppsMapping.containsKey(urlToStringWithoutTrailingSlash(urlMethodName))) {
continue;
}
if (!mParser.isNativeInitialized()) {
mParser.createNative(mFactoryDelegate.getParams().getWebContents());
}
// Initialize the native side of the downloader, once we know that a manifest file needs
// to be downloaded.
if (!mDownloader.isInitialized()) {
mDownloader.initialize(
mFactoryDelegate.getParams().getWebContents(),
mFactoryDelegate.getCSPChecker());
}
manifestVerifiers.add(
new PaymentManifestVerifier(
mFactoryDelegate.getParams().getPaymentRequestSecurityOrigin(),
urlMethodName,
urlMethodToDefaultAppsMapping.get(urlMethodName),
urlMethodToSupportedOriginsMapping.get(urlMethodName),
mWebDataService,
mDownloader,
mParser,
mPackageManagerDelegate,
/* callback= */ this));
if (manifestVerifiers.size() == MAX_NUMBER_OF_MANIFESTS) {
Log.e(TAG, "Reached maximum number of allowed payment app manifests.");
break;
}
}
if (manifestVerifiers.isEmpty()) {
onAllAppsFoundAndValidated();
return;
}
mPendingVerifiersCount = mPendingResourceUsersCount = manifestVerifiers.size();
for (PaymentManifestVerifier manifestVerifier : manifestVerifiers) {
manifestVerifier.verify();
}
}
/**
* Queries the Android app metadata for the names of the non-default payment methods that the
* given app supports.
*
* @param activityInfo The application information to query.
* @return The set of non-default payment method names that this application supports. Never
* null.
*/
private Set<String> getSupportedPaymentMethods(ActivityInfo activityInfo) {
Set<String> result = new HashSet<>();
String[] nonDefaultPaymentMethodNames =
getStringArrayMetaData(activityInfo, META_DATA_NAME_OF_PAYMENT_METHOD_NAMES);
if (nonDefaultPaymentMethodNames == null) return result;
// Normalize methods that look like URLs in the same way they will be normalized in
// #findAndroidPaymentApps.
for (String method : nonDefaultPaymentMethodNames) {
GURL urlMethod = new GURL(method);
result.add(
UrlUtil.isURLValid(urlMethod)
? urlToStringWithoutTrailingSlash(urlMethod)
: method);
}
return result;
}
/**
* Queries the Android app metadata for a string array.
* @param activityInfo The application information to query.
* @param metaDataName The name of the string array meta data to be retrieved.
* @return The string array.
*/
@Nullable
private String[] getStringArrayMetaData(ActivityInfo activityInfo, String metaDataName) {
if (activityInfo.metaData == null) return null;
int resId = activityInfo.metaData.getInt(metaDataName);
if (resId == 0) return null;
return mPackageManagerDelegate.getStringArrayResourceForApplication(
activityInfo.applicationInfo, resId);
}
@Override
public void onValidDefaultPaymentApp(GURL methodName, ResolveInfo resolveInfo) {
getOrCreateVerifiedPaymentMethod(methodName).defaultApplications.add(resolveInfo);
}
@Override
public void onValidSupportedOrigin(GURL methodName, GURL supportedOrigin) {
getOrCreateVerifiedPaymentMethod(methodName).supportedOrigins.add(supportedOrigin);
}
private PaymentMethod getOrCreateVerifiedPaymentMethod(GURL methodName) {
PaymentMethod verifiedPaymentManifest = mVerifiedPaymentMethods.get(methodName);
if (verifiedPaymentManifest == null) {
verifiedPaymentManifest = new PaymentMethod();
mVerifiedPaymentMethods.put(methodName, verifiedPaymentManifest);
}
return verifiedPaymentManifest;
}
@Override
public void onVerificationError(String errorMessage) {
mFactoryDelegate.onPaymentAppCreationError(errorMessage, AppCreationFailureReason.UNKNOWN);
}
@Override
public void onFinishedVerification() {
mPendingVerifiersCount--;
if (mPendingVerifiersCount != 0) return;
for (Map.Entry<GURL, PaymentMethod> nameAndMethod : mVerifiedPaymentMethods.entrySet()) {
GURL methodName = nameAndMethod.getKey();
if (!mUrlPaymentMethods.contains(methodName)) continue;
PaymentMethod method = nameAndMethod.getValue();
String methodNameString = urlToStringWithoutTrailingSlash(methodName);
for (ResolveInfo app : method.defaultApplications) {
onValidPaymentAppForPaymentMethodName(app, methodNameString);
}
for (GURL supportedOrigin : method.supportedOrigins) {
Set<GURL> supportedAppMethodNames =
mOriginToUrlDefaultMethodsMapping.get(supportedOrigin);
if (supportedAppMethodNames == null) continue;
for (GURL supportedAppMethodName : supportedAppMethodNames) {
PaymentMethod supportedAppMethod =
mVerifiedPaymentMethods.get(supportedAppMethodName);
if (supportedAppMethod == null) continue;
for (ResolveInfo supportedApp : supportedAppMethod.defaultApplications) {
onValidPaymentAppForPaymentMethodName(supportedApp, methodNameString);
}
}
}
}
onAllAppsFoundAndValidated();
}
/**
* Queries the IS_READY_TO_PAY service of all valid payment apps. Only valid payment apps
* receive IS_READY_TO_PAY query to avoid exposing browsing history to malicious apps.
*
* Must be done after all verifiers have finished because some manifest files may validate
* multiple apps and some apps may require multiple manifest file for verification.
*/
private void onAllAppsFoundAndValidated() {
assert mPendingVerifiersCount == 0;
mFactoryDelegate.onCanMakePaymentCalculated(mValidApps.size() > 0);
if (mValidApps.isEmpty() || mFactoryDelegate.getParams().hasClosed()) {
mFactoryDelegate.onDoneCreatingPaymentApps(mFactory);
return;
}
mPendingIsReadyToPayQueries = mValidApps.size();
for (Map.Entry<String, AndroidPaymentApp> entry : mValidApps.entrySet()) {
AndroidPaymentApp app = entry.getValue();
if (mBypassIsReadyToPayServiceInTest) app.bypassIsReadyToPayServiceInTest();
app.maybeQueryIsReadyToPayService(
filterMethodDataForApp(
mFactoryDelegate.getParams().getMethodData(),
app.getInstrumentMethodNames()),
mFactoryDelegate.getParams().getTopLevelOrigin(),
mFactoryDelegate.getParams().getPaymentRequestOrigin(),
mFactoryDelegate.getParams().getCertificateChain(),
filterModifiersForApp(
mFactoryDelegate.getParams().getUnmodifiableModifiers(),
app.getInstrumentMethodNames()),
this::onIsReadyToPayResponse);
}
}
@VisibleForTesting
public void bypassIsReadyToPayServiceInTest() {
mBypassIsReadyToPayServiceInTest = true;
}
private static Map<String, PaymentMethodData> filterMethodDataForApp(
Map<String, PaymentMethodData> methodData, Set<String> appMethodNames) {
Map<String, PaymentMethodData> filtered = new HashMap<>();
for (String methodName : appMethodNames) {
if (methodData.containsKey(methodName)) {
filtered.put(methodName, methodData.get(methodName));
}
}
return filtered;
}
private static Map<String, PaymentDetailsModifier> filterModifiersForApp(
Map<String, PaymentDetailsModifier> modifiers, Set<String> appMethodNames) {
Map<String, PaymentDetailsModifier> filtered = new HashMap<>();
for (String methodName : appMethodNames) {
if (modifiers.containsKey(methodName)) {
filtered.put(methodName, modifiers.get(methodName));
}
}
return filtered;
}
private void onIsReadyToPayResponse(AndroidPaymentApp app, boolean isReadyToPay) {
if (isReadyToPay) mFactoryDelegate.onPaymentAppCreated(app);
if (--mPendingIsReadyToPayQueries == 0) {
mFactoryDelegate.onDoneCreatingPaymentApps(mFactory);
}
}
/**
* Enables the given payment app to use this method name.
*
* @param resolveInfo The payment app that's allowed to use the method name.
* @param methodName The method name that can be used by the app.
*/
private void onValidPaymentAppForPaymentMethodName(ResolveInfo resolveInfo, String methodName) {
if (mFactoryDelegate.getParams().hasClosed()) return;
String packageName = resolveInfo.activityInfo.packageName;
SupportedDelegations appSupportedDelegations =
getAppsSupportedDelegations(resolveInfo.activityInfo);
// Allow-lists the Play Billing method for this feature in order for the Play Billing case
// to skip the sheet in this case.
if (PaymentFeatureList.isEnabled(PaymentFeatureList.ENFORCE_FULL_DELEGATION)
|| methodName.equals(MethodStrings.GOOGLE_PLAY_BILLING)) {
if (!appSupportedDelegations.providesAll(
mFactoryDelegate.getParams().getPaymentOptions())) {
Log.e(TAG, ErrorStrings.SKIP_APP_FOR_PARTIAL_DELEGATION.replace("$", packageName));
return;
}
}
AndroidPaymentApp app = mValidApps.get(packageName);
if (app == null) {
CharSequence label = mPackageManagerDelegate.getAppLabel(resolveInfo);
if (TextUtils.isEmpty(label)) {
Log.e(TAG, "Skipping \"%s\" because of empty label.", packageName);
return;
}
// Dedupe corresponding payment handler which is registered with the default
// payment method name as the scope and the scope is used as the app Id.
String webAppIdCanDeduped =
resolveInfo.activityInfo.metaData == null
? null
: resolveInfo.activityInfo.metaData.getString(
META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME);
app =
new AndroidPaymentApp(
new AndroidPaymentApp.LauncherImpl(
mFactoryDelegate.getParams().getWebContents()),
packageName,
resolveInfo.activityInfo.name,
mIsReadyToPayServices.get(packageName),
label.toString(),
mPackageManagerDelegate.getAppIcon(resolveInfo),
mIsOffTheRecord,
webAppIdCanDeduped,
appSupportedDelegations);
mValidApps.put(packageName, app);
}
// The same method may be added multiple times.
app.addMethodName(methodName);
}
private SupportedDelegations getAppsSupportedDelegations(ActivityInfo activityInfo) {
String[] supportedDelegationNames =
getStringArrayMetaData(activityInfo, META_DATA_NAME_OF_SUPPORTED_DELEGATIONS);
return SupportedDelegations.createFromStringArray(supportedDelegationNames);
}
@Override
public void onFinishedUsingResources() {
mPendingResourceUsersCount--;
if (mPendingResourceUsersCount != 0) return;
mWebDataService.destroy();
if (mDownloader.isInitialized()) mDownloader.destroy();
if (mParser.isNativeInitialized()) mParser.destroyNative();
}
/**
* Converts the given URL to a string without a trailing slash, because payment method
* identifiers typically omit trailing slashes, e.g., "https://google.com/pay" is correct,
* whereas "https://google.com/pay/" is incorrect. This is important because matching payment
* apps to payment requests happens by string equality. Note that GURL.getSpec() can append
* trailing slashes in some instances.
* @param url The URL to stringify.
* @return The URL string without a trailing slash, or null if the input parameter is null.
*/
@Nullable
private static String urlToStringWithoutTrailingSlash(@Nullable GURL url) {
if (url == null) return null;
return removeTrailingSlash(url.getSpec());
}
@Nullable
private static String removeTrailingSlash(@Nullable String string) {
if (string == null) return null;
return string.endsWith("/") ? string.substring(0, string.length() - 1) : string;
}
/**
* Add an app store for testing.
*
* @param packageName The package name of the app store.
* @param paymentMethod The payment method identifier of the app store.
*/
public void addAppStoreForTest(String packageName, GURL paymentMethod) {
assert paymentMethod.isValid();
mAppStores.put(packageName, paymentMethod);
}
}