Commit cc5487e9 authored by Liquan (Max) Gu's avatar Liquan (Max) Gu Committed by Commit Bot

[PlayBilling] Support app-store billing in AndroidPaymentAppFinder

After:
When a merchant page runs in a Trusted Web Activity that's installed
from an app store (e.g., Google Play) and the PaymentRequest is not
requesting shipping or payer contact, if it satisfies the following
conditions, the TWA itself would be included:
- the PaymentRequest supports the (TWA installer) app store's
billing method in the payment request.
- the TWA can handle pay intents.
- the TWA can handle the app store billing method.

Before:
AndroidPaymentAppFinder could not include a TWA for an app store
billing method.

Change:
* In AndroidPaymentAppFinder#findAndroidPaymentApps, check if the
merchant page is a TWA installed from app store and if the
PaymentRequest is requesting shipping or payer contact. If it is,
add the TWA itself as a payment app if it's eligible.
* remove mIgnoredMethods.

Note:
* Counterintuitively, the merchant page would send the pay intent to
the TWA instead of the Play Store, because it would be the TWA who
is responsible to interact with the app stores.

Bug: 1064740

Change-Id: I0a2f6baebae422aaeab574e3c39a10cd61fafb4a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2135152
Commit-Queue: Liquan (Max) Gu <maxlg@chromium.org>
Reviewed-by: default avatarRouslan Solomakhin <rouslan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#757128}
parent 7284136f
......@@ -15,7 +15,6 @@ import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.payments.PaymentManifestVerifier.ManifestVerifyCallback;
import org.chromium.components.payments.MethodStrings;
import org.chromium.components.payments.PaymentManifestDownloader;
......@@ -23,6 +22,7 @@ import org.chromium.components.payments.PaymentManifestParser;
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 org.chromium.url.URI;
import java.util.ArrayList;
......@@ -69,12 +69,6 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
/* package */ static final String META_DATA_NAME_OF_SUPPORTED_DELEGATIONS =
"org.chromium.payment_supported_delegations";
/*
* The ignored payment method identifiers. Payment apps with this payment method identifier are
* ignored.
*/
private final Set<String> mIgnoredMethods = new HashSet<>();
private final Set<String> mNonUriPaymentMethods = new HashSet<>();
private final Set<URI> mUriPaymentMethods = new HashSet<>();
private final PaymentManifestDownloader mDownloader;
......@@ -86,10 +80,13 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
private final boolean mIsIncognito;
/**
* A map from an app-store app's package name to its billing method. All of the supported
* app-store billing method must insert an entry to this map.
* 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, String> mAppStoreBillingMethodMap = new HashMap();
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
......@@ -173,9 +170,10 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
PaymentAppFactoryInterface factory) {
mDelegate = delegate;
mIgnoredMethods.add(MethodStrings.GOOGLE_PLAY_BILLING);
mAppStoreBillingMethodMap.put(PLAY_STORE_PACKAGE_NAME, MethodStrings.GOOGLE_PLAY_BILLING);
mAppStores.put(PLAY_STORE_PACKAGE_NAME, new GURL(MethodStrings.GOOGLE_PLAY_BILLING));
for (GURL method : mAppStores.values()) {
assert method.isValid();
}
mDownloader = downloader;
mWebDataService = webDataService;
......@@ -187,19 +185,62 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
mIsIncognito = activity != null && activity.getCurrentTabModel().isIncognito();
}
private boolean isInTwaInstalledFromAppStore() {
ChromeActivity activity =
ChromeActivity.fromWebContents(mDelegate.getParams().getWebContents());
if (activity == null) return false;
if (!(activity instanceof CustomTabActivity)) return false;
CustomTabActivity customTabActivity = ((CustomTabActivity) activity);
if (!customTabActivity.isInTwaMode()) return false;
String twaPackageName = customTabActivity.getTwaPackage();
private boolean isInTwaInstalledFromAppStore(ChromeActivity activity) {
assert activity != null;
String twaPackageName = mPackageManagerDelegate.getTwaPackageName(activity);
if (twaPackageName == null) return false;
String installerPackageName =
activity.getPackageManager().getInstallerPackageName(twaPackageName);
String installerPackageName = mPackageManagerDelegate.getInstallerPackage(twaPackageName);
if (installerPackageName == null) return false;
return mAppStoreBillingMethodMap.keySet().contains(installerPackageName);
return mAppStores.containsKey(installerPackageName);
}
/** Precondition: {@link #isInTwaInstalledFromAppStore} returns true. */
private void findAppStoreBillingApp(
ChromeActivity activity, List<ResolveInfo> allInstalledPaymentApps) {
assert activity != null;
// The following asserts are assumed to have been checked in {@link
// isInTwaInstalledFromAppStore}.
String twaPackageName = mPackageManagerDelegate.getTwaPackageName(activity);
assert twaPackageName != null;
String installerAppStorePackageName =
mPackageManagerDelegate.getInstallerPackage(twaPackageName);
assert installerAppStorePackageName != null;
GURL appStoreBillingUriMethod = mAppStores.get(installerAppStorePackageName);
assert appStoreBillingUriMethod != null;
assert appStoreBillingUriMethod.isValid();
String appStoreBillingMethod = appStoreBillingUriMethod.getSpec();
if (!mDelegate.getParams().getMethodData().containsKey(appStoreBillingMethod)) return;
ResolveInfo twaApp = findAppWithPackageNameAndSupportedMethod(
allInstalledPaymentApps, twaPackageName, appStoreBillingUriMethod);
if (twaApp == null) {
android.util.Log.d(TAG, "The current TWA cannot handle Payment Request.");
return;
}
onValidPaymentAppForPaymentMethodName(twaApp, appStoreBillingMethod);
}
private ResolveInfo findAppWithPackageNameAndSupportedMethod(
List<ResolveInfo> apps, String packageName, GURL uriMethod) {
assert packageName != null;
assert uriMethod != null;
for (int i = 0; i < apps.size(); i++) {
ResolveInfo app = apps.get(i);
String appPackageName = app.activityInfo.packageName;
if (!packageName.equals(appPackageName)) continue;
String defaultMethod = app.activityInfo.metaData == null
? null
: app.activityInfo.metaData.getString(
META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME);
GURL defaultUriMethod = new GURL(defaultMethod);
if ((uriMethod.isValid()
&& getSupportedPaymentMethods(app.activityInfo)
.contains(uriMethod.getSpec()))
|| (defaultUriMethod.isValid() && uriMethod.equals(defaultUriMethod))) {
return app;
}
}
return null;
}
/**
......@@ -218,7 +259,7 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
for (String method : mDelegate.getParams().getMethodData().keySet()) {
assert !TextUtils.isEmpty(method);
if (mIgnoredMethods.contains(method)) continue;
if (mAppStores.containsValue(new GURL(method))) continue;
if (supportedNonUriPaymentMethods.contains(method)) {
mNonUriPaymentMethods.add(method);
} else if (UriUtils.looksLikeUriMethod(method)) {
......@@ -245,9 +286,17 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
}
}
if (isInTwaInstalledFromAppStore()) {
// TODO(crbug.com/1064740): the finder would special-case the TWA installed from App
// Store to return only the app-store app.
// WebContents is possible to attach to different activities on {@link PaymentRequest}
// created and shown. Ideally {@link #findAppStoreBillingApp} should have based on the
// activity that is used when PaymentRequest is shown. But we intentionally not do that for
// the sake of simple design and better performance. Plus, for app store billing case in
// particular, it's unusual for a TWA to switch to CCT without destroying JavaScript context
// and, consequently, the {@link PaymentRequest} object.
ChromeActivity activity =
ChromeActivity.fromWebContents(mDelegate.getParams().getWebContents());
if (!mDelegate.getParams().requestShippingOrPayerContact() && activity != null
&& isInTwaInstalledFromAppStore(activity)) {
findAppStoreBillingApp(activity, allInstalledPaymentApps);
}
// All URI methods for which manifests should be downloaded. For example, if merchant
......@@ -646,14 +695,14 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
}
/**
* Ignores the given payment method identifier, so no Android payment apps for this method are
* looked up in findAndroidPaymentApps(). Calling this multiple times will union the new payment
* methods with the existing set.
* Add an app store for testing.
*
* @param ignoredPaymentMethodIdentifier The ignored payment method identifier.
* @param packageName The package name of the app store.
* @param paymentMethod The payment method identifier of the app store.
*/
@VisibleForTesting
/* package */ void ignorePaymentMethodForTest(String ignoredPaymentMethodIdentifier) {
mIgnoredMethods.add(ignoredPaymentMethodIdentifier);
/* package */ void addAppStoreForTest(String packageName, GURL paymentMethod) {
assert paymentMethod.isValid();
mAppStores.put(packageName, paymentMethod);
}
}
......@@ -20,6 +20,8 @@ import androidx.annotation.Nullable;
import org.chromium.base.ContextUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import java.util.List;
......@@ -123,4 +125,31 @@ public class PackageManagerDelegate {
}
return resources == null ? null : resources.getStringArray(resourceId);
}
/**
* Get the package name of an activity if it is a Trusted Web Activity.
* @param activity An activity that is intended to check whether its a Trusted Web Activity and
* get the package name from. Not allowed to be null.
* @return The package name of a given activity if it is a Trusted Web Activity; null otherwise.
*/
@Nullable
public String getTwaPackageName(ChromeActivity activity) {
assert activity != null;
if (!(activity instanceof CustomTabActivity)) return null;
CustomTabActivity customTabActivity = ((CustomTabActivity) activity);
if (!customTabActivity.isInTwaMode()) return null;
return customTabActivity.getTwaPackage();
}
/**
* Get the package name of a specified package's installer app.
* @param packageName The package name of the specified package. Not allowed to be null.
* @return The package name of the installer app.
*/
@Nullable
public String getInstallerPackage(String packageName) {
assert packageName != null;
return ContextUtils.getApplicationContext().getPackageManager().getInstallerPackageName(
packageName);
}
}
......@@ -95,4 +95,12 @@ public interface PaymentAppFactoryParams {
default String getTotalAmountCurrency() {
return null;
}
/**
* @return Whether the PaymentRequest is requesting delegation of either shipping or payer
* contact.
*/
default boolean requestShippingOrPayerContact() {
return false;
}
}
......@@ -2622,6 +2622,12 @@ public class PaymentRequestImpl
return mRawTotal.amount.currency;
}
// PaymentAppFactoryParams implementation.
@Override
public boolean requestShippingOrPayerContact() {
return mRequestShipping || mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail;
}
// PaymentAppFactoryDelegate implementation.
@Override
public PaymentAppFactoryParams getParams() {
......
......@@ -16,6 +16,8 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import org.chromium.chrome.browser.ChromeActivity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
......@@ -31,6 +33,10 @@ class MockPackageManagerDelegate extends PackageManagerDelegate {
private final List<ResolveInfo> mServices = new ArrayList<>();
private final Map<ApplicationInfo, String[]> mResources = new HashMap<>();
private String mMockTwaPackage;
// A map of a package name to its installer's package name.
private Map<String, String> mMockInstallerPackageMap = new HashMap<>();
/**
* Simulates an installed payment app with no supported delegations.
*
......@@ -140,6 +146,27 @@ class MockPackageManagerDelegate extends PackageManagerDelegate {
mLabels.clear();
}
/**
* Mock the current package to be a Trust Web Activity package.
* @param mockTwaPackage The intended package nam, not allowed to be null.
*/
public void setMockTrustedWebActivity(String mockTwaPackage) {
assert mockTwaPackage != null;
mMockTwaPackage = mockTwaPackage;
}
/**
* Mock the installer of a specified package.
* @param packageName The package name that is intended to mock a installer for.
* @param installerPackageName The package name intended to be set as the installer of the
* specified package. not allowed to be null.
*/
public void mockInstallerForPackage(String packageName, String installerPackageName) {
assert installerPackageName != null;
assert packageName != null;
mMockInstallerPackageMap.put(packageName, installerPackageName);
}
@Override
public List<ResolveInfo> getActivitiesThatCanRespondToIntentWithMetaData(Intent intent) {
return mActivities;
......@@ -177,4 +204,17 @@ class MockPackageManagerDelegate extends PackageManagerDelegate {
assert STRING_ARRAY_RESOURCE_ID == resourceId;
return mResources.get(applicationInfo);
}
@Override
@Nullable
public String getInstallerPackage(String packageName) {
return !mMockInstallerPackageMap.isEmpty() ? mMockInstallerPackageMap.get(packageName)
: super.getInstallerPackage(packageName);
}
@Override
@Nullable
public String getTwaPackageName(ChromeActivity activity) {
return mMockTwaPackage != null ? mMockTwaPackage : super.getTwaPackageName(activity);
}
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment