chromium/chrome/android/javatests/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFinderTest.java

// 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.chrome.browser.payments;

import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.payments.AndroidPaymentAppFinder;
import org.chromium.components.payments.AppCreationFailureReason;
import org.chromium.components.payments.CSPChecker;
import org.chromium.components.payments.PaymentApp;
import org.chromium.components.payments.PaymentAppFactoryDelegate;
import org.chromium.components.payments.PaymentAppFactoryInterface;
import org.chromium.components.payments.PaymentAppFactoryParams;
import org.chromium.components.payments.PaymentFeatureList;
import org.chromium.components.payments.PaymentManifestDownloader;
import org.chromium.components.payments.PaymentManifestParser;
import org.chromium.components.payments.PaymentManifestWebDataService;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.payments.mojom.PaymentDetailsModifier;
import org.chromium.payments.mojom.PaymentItem;
import org.chromium.payments.mojom.PaymentMethodData;
import org.chromium.payments.mojom.PaymentOptions;
import org.chromium.url.GURL;
import org.chromium.url.Origin;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/** An integration test for the Android payment app finder. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
// TODO(crbug.com/344662668): Failing when batched, batch this again.
@MediumTest
public class AndroidPaymentAppFinderTest
        implements PaymentAppFactoryDelegate, PaymentAppFactoryParams {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    /** Simulates a package manager in memory. */
    private final MockPackageManagerDelegate mPackageManager = new MockPackageManagerDelegate();

    /** Downloads from the test server. */
    private static class TestServerDownloader extends PaymentManifestDownloader {
        private GURL mTestServerUrl;

        /**
         * @param url The URL of the test server.
         */
        /* package */ void setTestServerUrl(GURL url) {
            assert mTestServerUrl == null : "Test server URL should be set only once";
            mTestServerUrl = url;
        }

        @Override
        public void downloadPaymentMethodManifest(
                Origin merchantOrigin, GURL methodName, ManifestDownloadCallback callback) {
            super.downloadPaymentMethodManifest(
                    merchantOrigin, substituteTestServerUrl(methodName), callback);
        }

        @Override
        public void downloadWebAppManifest(
                Origin paymentMethodManifestOrigin,
                GURL webAppManifestUrl,
                ManifestDownloadCallback callback) {
            super.downloadWebAppManifest(
                    paymentMethodManifestOrigin,
                    substituteTestServerUrl(webAppManifestUrl),
                    callback);
        }

        private GURL substituteTestServerUrl(GURL url) {
            GURL changedUrl =
                    new GURL(url.getSpec().replaceAll("https://", mTestServerUrl.getSpec()));
            if (!changedUrl.isValid()) {
                assert false;
                return null;
            }
            return changedUrl;
        }
    }

    private final TestServerDownloader mDownloader = new TestServerDownloader();

    private EmbeddedTestServer mServer;
    private List<PaymentApp> mPaymentApps;
    private boolean mAllPaymentAppsCreated;
    private Map<String, PaymentMethodData> mMethodData;
    private PaymentOptions mPaymentOptions;
    private String mTwaPackageName;

    // PaymentAppFactoryDelegate implementation.
    @Override
    public PaymentAppFactoryParams getParams() {
        return this;
    }

    // PaymentAppFactoryDelegate implementation.
    @Override
    public boolean hasClosed() {
        return false;
    }

    // PaymentAppFactoryDelegate implementation.
    @Override
    public void onPaymentAppCreated(PaymentApp paymentApp) {
        mPaymentApps.add(paymentApp);
    }

    // PaymentAppFactoryDelegate implementation.
    @Override
    public void onPaymentAppCreationError(
            String errorMessage, @AppCreationFailureReason int errorReason) {}

    // PaymentAppFactoryDelegate implementation.
    @Override
    public void onDoneCreatingPaymentApps(PaymentAppFactoryInterface unusedFactory) {
        mAllPaymentAppsCreated = true;
    }

    // PaymentAppFactoryDelegate implementation.
    @Override
    public CSPChecker getCSPChecker() {
        return new CSPChecker() {
            @Override
            public void allowConnectToSource(
                    GURL url,
                    GURL urlBeforeRedirects,
                    boolean didFollowRedirect,
                    Callback<Boolean> resultCallback) {
                resultCallback.onResult(/* allow= */ true);
            }
        };
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public WebContents getWebContents() {
        return mActivityTestRule.getActivity().getCurrentWebContents();
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public RenderFrameHost getRenderFrameHost() {
        return getWebContents().getMainFrame();
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public Map<String, PaymentDetailsModifier> getUnmodifiableModifiers() {
        return Collections.unmodifiableMap(new HashMap<String, PaymentDetailsModifier>());
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public Origin getPaymentRequestSecurityOrigin() {
        return PaymentManifestDownloader.createOpaqueOriginForTest();
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public String getTopLevelOrigin() {
        return "https://top.level.origin";
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public String getPaymentRequestOrigin() {
        return "https://payment.request.origin";
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public Map<String, PaymentMethodData> getMethodData() {
        return mMethodData;
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public PaymentItem getRawTotal() {
        // This test doesn't need this value.
        return null;
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public PaymentOptions getPaymentOptions() {
        return mPaymentOptions;
    }

    public void setRequestShipping(boolean requestShipping) {
        mPaymentOptions.requestShipping = requestShipping;
    }

    // PaymentAppFactoryParams implementation.
    @Override
    public @Nullable String getTwaPackageName() {
        return mTwaPackageName;
    }

    @Override
    public boolean isOffTheRecord() {
        return false;
    }

    @Before
    public void setUp() throws Throwable {
        mActivityTestRule.startMainActivityOnBlankPage();
        mPackageManager.reset();
        mServer =
                EmbeddedTestServer.createAndStartServer(
                        ApplicationProvider.getApplicationContext());
        mDownloader.setTestServerUrl(new GURL(mServer.getURL("/components/test/data/payments/")));
        mPaymentApps = new ArrayList<>();
        mAllPaymentAppsCreated = false;
        mPaymentOptions = new PaymentOptions();
        mTwaPackageName = null;
    }

    /** Absence of installed apps should result in no payment apps. */
    @Test
    @Feature({"Payments"})
    public void testNoApps() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("basic-card");
        methods.add("https://alicepay.test/webpay");
        methods.add("https://bobpay.test/webpay");
        methods.add("https://charliepay.test/webpay");
        methods.add("https://davepay.test/webpay");

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /** Payment apps without metadata should be filtered out. */
    @Test
    @Feature({"Payments"})
    public void testNoMetadata() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("basic-card");
        mPackageManager.installPaymentApp(
                "BobPay", "com.bobpay", null /* no metadata */, /* signature= */ "01");

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated(/*no identifier*/ );
    }

    /** Payment apps cannot use a payment method without explicit authorization. */
    @Test
    @Feature({"Payments"})
    public void testPaymentAppsRequireExplicitAuthorizationForPaymentMethods() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://frankpay.test/webpay");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "" /* no default payment method name in metadata */,
                /* signature= */ "AA");
        mPackageManager.setStringArrayMetaData(
                "com.alicepay", new String[] {"https://frankpay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertTrue("No apps should still match the query", mPaymentApps.isEmpty());
    }

    /** Payment apps without a human-readable name should be filtered out. */
    @Test
    @Feature({"Payments"})
    public void testEmptyLabel() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("basic-card");
        mPackageManager.installPaymentApp(
                "" /* empty label */,
                "com.bobpay",
                "basic-card",
                /* signature= */ "01020304050607080900");

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /** Invalid and relative URLs cannot be used as payment method names. */
    @Test
    @Feature({"Payments"})
    public void testInvalidPaymentMethodNames() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://"); // Invalid URL.
        methods.add("../index.html"); // Relative URL.
        mPackageManager.installPaymentApp(
                "BobPay", "com.bobpay", "https://", /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "../index.html",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /** Non-URL payment method names are hard-coded to those defined in W3C. */
    @Test
    @Feature({"Payments"})
    public void testTwoAppsWithIncorrectMethodNames() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("basic-card");
        methods.add("incorrect-method-name"); // Even if merchant supports it, Chrome filters out
        // unknown non-URL method names.
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "incorrect-method-name",
                /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "incorrect-method-name",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /**
     * Test BobPay with https://bobpay.test/webpay payment method name, which the payment app
     * supports through the "default_applications" directive in the
     * https://bobpay.test/payment-manifest.json file. BobPay has the correct signature that matches
     * the fingerprint in https://bobpay.test/app.json.
     */
    @Test
    @Feature({"Payments"})
    public void testOneUrlMethodNameApp() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());
    }

    /** When Chrome is not running in TWA, the app store billing methods should be filtered out. */
    @Test
    @Feature({"Payments"})
    public void testIgnoreAppStoreMethodsInNonTwa() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");

        addAppStoreMethodAndFindApps(
                /* appStorePackageName= */ "com.bobpay",
                /* appStorePaymentMethod= */ new GURL("https://bobpay.test/webpay"),
                methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /**
     * Test BobPay with an incorrect signature and https://bobpay.test/webpay payment method name.
     */
    @Test
    @Feature({"Payments"})
    public void testOneUrlMethodNameAppWithWrongSignature() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                "AA" /* incorrect signature */);

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /** A payment app whose package info cannot be retrieved should be filtered out. */
    @Test
    @Feature({"Payments"})
    public void testOneAppWithoutPackageInfo() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay", "com.bobpay", "https://bobpay.test/webpay", null /* no package info*/);

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /** Unsigned payment app should be filtered out. */
    @Test
    @Feature({"Payments"})
    public void testOneAppWithoutSignatures() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay", "com.bobpay", "https://bobpay.test/webpay", "" /* no signatures */);

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());
    }

    /**
     * Test https://davepay.test/webpay payment method, the "default_applications" of which supports
     * two different package names: one for production and one for development version of the
     * payment app. Both of these apps should be found. Repeated lookups should continue finding the
     * two apps.
     */
    @Test
    @Feature({"Payments"})
    public void testTwoUrlMethodNameAppsWithSameMethodName() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://davepay.test/webpay");
        mPackageManager.installPaymentApp(
                "DavePay",
                "com.davepay.prod",
                "https://davepay.test/webpay",
                /* signature= */ "44444444442222222222");
        mPackageManager.installPaymentApp(
                "DavePay Dev",
                "com.davepay.dev",
                "https://davepay.test/webpay",
                /* signature= */ "44444444441111111111");

        findApps(methods);

        Assert.assertEquals("2 apps should match the query", 2, mPaymentApps.size());
        Set<String> appIdentifiers = new HashSet<>();
        appIdentifiers.add(mPaymentApps.get(0).getIdentifier());
        appIdentifiers.add(mPaymentApps.get(1).getIdentifier());
        Assert.assertTrue(appIdentifiers.contains("com.davepay.prod"));
        Assert.assertTrue(appIdentifiers.contains("com.davepay.dev"));

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("2 apps should match the query again", 2, mPaymentApps.size());
        appIdentifiers.clear();
        appIdentifiers.add(mPaymentApps.get(0).getIdentifier());
        appIdentifiers.add(mPaymentApps.get(1).getIdentifier());
        Assert.assertTrue(appIdentifiers.contains("com.davepay.prod"));
        Assert.assertTrue(appIdentifiers.contains("com.davepay.dev"));
    }

    /**
     * If the merchant supports https://bobpay.test/webpay and https://alicepay.test/webpay payment
     * method names and the user has an app for each of those, then both apps should be found.
     * Repeated lookups should succeed.
     */
    @Test
    @Feature({"Payments"})
    public void testTwoUrlMethodNameAppsWithDifferentMethodNames() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        methods.add("https://alicepay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        Assert.assertEquals("2 apps should match the query", 2, mPaymentApps.size());
        Set<String> appIdentifiers = new HashSet<>();
        appIdentifiers.add(mPaymentApps.get(0).getIdentifier());
        appIdentifiers.add(mPaymentApps.get(1).getIdentifier());
        Assert.assertTrue(appIdentifiers.contains("com.bobpay"));
        Assert.assertTrue(appIdentifiers.contains("com.alicepay"));

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("2 apps should match the query again", 2, mPaymentApps.size());
        appIdentifiers.clear();
        appIdentifiers.add(mPaymentApps.get(0).getIdentifier());
        appIdentifiers.add(mPaymentApps.get(1).getIdentifier());
        Assert.assertTrue(appIdentifiers.contains("com.bobpay"));
        Assert.assertTrue(appIdentifiers.contains("com.alicepay"));
    }

    /**
     * If the merchant supports a couple of payment methods, one of which does not have a valid
     * manifest, then all apps that support the invalid manifest should be filtered out. Repeated
     * calls should continue finding only the payment app for the valid manifest.
     */
    @Test
    @Feature({"Payments"})
    public void testOneValidManifestAndOneInvalidManifestWithPaymentAppsForBoth() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        methods.add("https://not-valid.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "NotValid",
                "com.not-valid",
                "https://not-valid.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("1 app should match the query again", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());
    }

    /**
     * Repeated lookups of payment apps for URL method names should continue finding the same
     * payment apps.
     */
    @Test
    @Feature({"Payments"})
    public void testTwoUrlMethodNameAppsTwice() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        methods.add("https://alicepay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        assertPaymentAppsCreated("com.bobpay", "com.alicepay");

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated("com.bobpay", "com.alicepay");
    }

    /**
     * Test CharliePay Dev with https://charliepay.test/webpay payment method, which supports both
     * dev and prod versions of the app through multiple web app manifests. Repeated app look ups
     * should be successful.
     */
    @Test
    @Feature({"Payments"})
    public void testCharliePayDev() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://charliepay.test/webpay");
        mPackageManager.installPaymentApp(
                "CharliePay",
                "com.charliepay.dev",
                "https://charliepay.test/webpay",
                /* signature= */ "33333333333111111111");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.charliepay.dev", mPaymentApps.get(0).getIdentifier());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("1 app should match the query again", 1, mPaymentApps.size());
        Assert.assertEquals("com.charliepay.dev", mPaymentApps.get(0).getIdentifier());
    }

    /**
     * Test DavePay Dev with https://davepay.test/webpay payment method, which supports both dev and
     * prod versions of the app through multiple sections of "related_applications" entry in the
     * same web app manifest. Repeated app look ups should be successful.
     */
    @Test
    @Feature({"Payments"})
    public void testDavePayDev() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://davepay.test/webpay");
        mPackageManager.installPaymentApp(
                "DavePay",
                "com.davepay.dev",
                "https://davepay.test/webpay",
                /* signature= */ "44444444441111111111");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.davepay.dev", mPaymentApps.get(0).getIdentifier());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("1 app should match the query again", 1, mPaymentApps.size());
        Assert.assertEquals("com.davepay.dev", mPaymentApps.get(0).getIdentifier());
    }

    /**
     * Test a valid installation of EvePay with 55555555551111111111 signature and
     * https://evepay.test/webpay payment method, which supports a couple of different signatures
     * (with the same package name) through different web app manifests. Repeated app look ups
     * should be successful.
     */
    @Test
    @Feature({"Payments"})
    public void testValidEvePay1() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://evepay.test/webpay");
        mPackageManager.installPaymentApp(
                "EvePay",
                "com.evepay",
                "https://evepay.test/webpay",
                /* signature= */ "55555555551111111111");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.evepay", mPaymentApps.get(0).getIdentifier());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("1 app should match the query again", 1, mPaymentApps.size());
        Assert.assertEquals("com.evepay", mPaymentApps.get(0).getIdentifier());
    }

    /**
     * Test a valid installation of EvePay with 55555555552222222222 signature and
     * https://evepay.test/webpay payment method, which supports a couple of different signatures
     * (with the same package name) through different web app manifests. Repeated app look ups
     * should be successful.
     */
    @Test
    @Feature({"Payments"})
    public void testValidEvePay2() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://evepay.test/webpay");
        mPackageManager.installPaymentApp(
                "EvePay",
                "com.evepay",
                "https://evepay.test/webpay",
                /* signature= */ "55555555552222222222");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.evepay", mPaymentApps.get(0).getIdentifier());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("1 app should match the query again", 1, mPaymentApps.size());
        Assert.assertEquals("com.evepay", mPaymentApps.get(0).getIdentifier());
    }

    /**
     * Test an invalid installation of EvePay with https://evepay.test/webpay payment method, which
     * supports several different signatures (with the same package name) through different web app
     * manifests. Repeated app look ups should find no payment apps.
     */
    @Test
    @Feature({"Payments"})
    public void testInvalidEvePay() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://evepay.test/webpay");
        mPackageManager.installPaymentApp(
                "EvePay", "com.evepay", "https://evepay.test/webpay", /* signature= */ "55");

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertTrue("No apps should match the query again", mPaymentApps.isEmpty());
    }

    /**
     * Test https://frankpay.test/webpay payment method, which is invalid because it contains
     * {"supported_origins": "*"}. Repeated app look ups should find no payment apps.
     */
    @Test
    @Feature({"Payments"})
    public void testFrankPayDoesNotMatchAnyPaymentApps() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://frankpay.test/webpay");
        mPackageManager.installPaymentApp(
                "AlicePay", "com.alicepay", "https://alicepay.test/webpay", /* signature= */ "00");
        mPackageManager.installPaymentApp(
                "BobPay", "com.bobpay", "basic-card", /* signature= */ "11");
        mPackageManager.installPaymentApp(
                "AlicePay", "com.charliepay", "invalid-payment-method-name", /* signature= */ "22");
        mPackageManager.setStringArrayMetaData(
                "com.alicepay", new String[] {"https://frankpay.test/webpay"});
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://frankpay.test/webpay"});
        mPackageManager.setStringArrayMetaData(
                "com.charliepay", new String[] {"https://frankpay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated(/*no identifiers*/ );
    }

    /**
     * Verify unable to use a payment app that has wrong signature for default payment method and is
     * not explicitly authorized to use any other method either.
     */
    @Test
    @Feature({"Payments"})
    public void testInvalidSignatureAndPaymentMethodDoesNotMatchAnyPaymentApps() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://frankpay.test/webpay");
        methods.add("https://bobpay.test/webpay");
        methods.add("invalid-payment-method-name");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                "00" /* Invalid signature for https://bobpay.test/webpay. */);
        mPackageManager.setStringArrayMetaData(
                "com.bobpay",
                new String[] {"invalid-payment-method-name", "https://frankpay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertTrue("No apps should still match the query", mPaymentApps.isEmpty());
    }

    /**
     * Verify that only a valid AlicePay app can use https://georgepay.test/webpay payment method
     * name, because https://georgepay.test/payment-manifest.json contains {"supported_origins":
     * ["https://alicepay.test"]}. Repeated app look ups should be successful.
     */
    @Test
    @Feature({"Payments"})
    public void testGeorgePaySupportsPaymentAppsFromAlicePayOrigin() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://georgepay.test/webpay");
        // Valid AlicePay:
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");
        mPackageManager.setStringArrayMetaData(
                "com.alicepay", new String[] {"https://georgepay.test/webpay"});
        // Invalid AlicePay:
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.fake-alicepay" /* invalid package name*/,
                "https://alicepay.test/webpay",
                "00" /* invalid signature */);
        mPackageManager.setStringArrayMetaData(
                "com.fake-alicepay", new String[] {"https://georgepay.test/webpay"});
        // Valid BobPay:
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://georgepay.test/webpay"});
        // A "basic-card" app.
        mPackageManager.installPaymentApp(
                "CharliePay",
                "com.charliepay.dev",
                "basic-card",
                /* signature= */ "33333333333111111111");
        mPackageManager.setStringArrayMetaData(
                "com.charliepay.dev", new String[] {"https://georgepay.test/webpay"});

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.alicepay", mPaymentApps.get(0).getIdentifier());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated("com.alicepay");
    }

    /**
     * Verify that AlicePay app with incorrect signature cannot use https://georgepay.test/webpay
     * payment method, which contains {"supported_origins": ["https://alicepay.test"]} in
     * https://georgepay.test/payment-manifest.json file. Repeated app look ups should find no apps.
     */
    @Test
    @Feature({"Payments"})
    public void testInvalidSignatureAlicePayAppCannotUseGeorgePayMethodName() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://georgepay.test/webpay");
        // AlicePay with invalid signature:
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                "00" /* invalid signature */);
        mPackageManager.setStringArrayMetaData(
                "com.alicepay", new String[] {"https://georgepay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertTrue("No apps should match the query again", mPaymentApps.isEmpty());
    }

    /**
     * Verify that BobPay app cannot use https://georgepay.test/webpay payment method, because
     * https://georgepay.test/payment-manifest.json contains {"supported_origins":
     * ["https://alicepay.test"]} and no "https://bobpay.test". BobPay can still use its own payment
     * method name, however. Repeated app look ups should succeed.
     */
    @Test
    @Feature({"Payments"})
    public void testValidBobPayCannotUseGeorgePayMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        methods.add("https://georgepay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://georgepay.test/webpay"});

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());
        Assert.assertEquals(
                "1 payment method should be enabled",
                1,
                mPaymentApps.get(0).getInstrumentMethodNames().size());
        Assert.assertEquals(
                "https://bobpay.test/webpay",
                mPaymentApps.get(0).getInstrumentMethodNames().iterator().next());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertEquals("1 app should still match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());
        Assert.assertEquals(
                "1 payment method should still be enabled",
                1,
                mPaymentApps.get(0).getInstrumentMethodNames().size());
        Assert.assertEquals(
                "https://bobpay.test/webpay",
                mPaymentApps.get(0).getInstrumentMethodNames().iterator().next());
    }

    /**
     * Verify that HenryPay can not use https://henrypay.test/webpay payment method name and BobPay
     * can not use it because https://henrypay.test/payment-manifest.json contains invalid
     * "supported_origins": "*". Repeated app look ups should find no payment apps.
     */
    @Test
    @Feature({"Payments"})
    public void testUrlPaymentMethodWithDefaultApplication() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://henrypay.test/webpay");
        mPackageManager.installPaymentApp(
                "HenryPay",
                "com.henrypay",
                "https://henrypay.test/webpay",
                /* signature= */ "55555555551111111111");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://henrypay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated(/*no identifiers*/ );
    }

    /**
     * Verify that no payment app can use https://henrypay.test/webpay, because it does not
     * explicitly authorize any payment app.
     */
    @Test
    @Feature({"Payments"})
    public void testNonUriDefaultPaymentMethodAppCanUseMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://henrypay.test/webpay");
        mPackageManager.installPaymentApp(
                "BobPay", "com.bobpay", "basic-card", /* signature= */ "AA");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://henrypay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        Assert.assertTrue("No apps should still match the query", mPaymentApps.isEmpty());
    }

    /**
     * Verify that IkePay can use https://ikepay.test/webpay payment method name because it's a
     * default application and AlicePay can use it because https://ikepay.test/payment-manifest.json
     * contains "supported_origins": ["https://alicepay.test"]. BobPay cannot use this payment
     * method. Repeated app look ups should succeed.
     */
    @Test
    @Feature({"Payments"})
    public void testUrlPaymentMethodWithDefaultApplicationAndOneSupportedOrigin() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://ikepay.test/webpay");
        mPackageManager.installPaymentApp(
                "IkePay",
                "com.ikepay",
                "https://ikepay.test/webpay",
                /* signature= */ "66666666661111111111");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");
        mPackageManager.setStringArrayMetaData(
                "com.alicepay", new String[] {"https://ikepay.test/webpay"});
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://ikepay.test/webpay"});

        findApps(methods);

        assertPaymentAppsCreated("com.ikepay", "com.alicepay");

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated("com.ikepay", "com.alicepay");
    }

    /**
     * Verify that no payment app can use https://henrypay.test/webpay, because it does not
     * explicitly authorize any payment app.
     */
    @Test
    @Feature({"Payments"})
    public void testDuplicateDefaultAndSupportedMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://henrypay.test/webpay");
        mPackageManager.installPaymentApp(
                "HenryPay",
                "com.henrypay",
                "https://henrypay.test/webpay",
                /* signature= */ "55555555551111111111");
        mPackageManager.setStringArrayMetaData(
                "com.henrypay", new String[] {"https://henrypay.test/webpay"});

        findApps(methods);

        Assert.assertTrue("No apps should match the query", mPaymentApps.isEmpty());

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated(/*no identifiers*/ );
    }

    /**
     * The basic test for {@link AndroidPaymentAppFinder#findAndroidPaymentApps} to find a app-store
     * (e.g., Google Store) billing app.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingApp() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated("com.merchant.twa");
    }

    /**
     * In the context of finding the app store billing app in TWA, test that the TWA's installer
     * cannot be null. The special conditions about this test is that the installer package name is
     * mocked to null.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppInstallerNotNull() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", null);
        findApps(methods);

        assertNoPaymentAppsCreated();
    }

    /**
     * In the context of finding the app store billing app in TWA, test that the TWA's installer is
     * linked to a supported app store billing method. The special conditions about this test is
     * that the installer package name is mocked to be an unknown app store.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppInstallerLinkedToSupportedMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", "com.unknown.appstore");
        findApps(methods);

        assertNoPaymentAppsCreated();
    }

    /**
     * In the context of finding the app store billing app in TWA, test that the TWA's installer's
     * app store method is the one that the TWA is requesting for payment. The special conditions
     * about this test is that the finder adds a new app store, the installer package name is set to
     * be the app store, but the app-store method that the TWA requests is not of the same store.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppInstallerMethodIsRequested() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", "another.known.appstore");
        addAppStoreMethodAndFindApps(
                /* appStorePackageName= */ "another.known.appstore",
                /* appStorePaymentMethod= */ new GURL("https://another.known.appstore/billing"),
                methods);

        assertNoPaymentAppsCreated();
    }

    /**
     * For finding app store billing app, test scenario where no payment app has been installed. The
     * test setting intentionally omits the app installations.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppNoAppAvailable() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated(/*no identifiers*/ );
    }

    /**
     * For finding app store billing app, test scenario where the app's meta data is null. The test
     * setting intentionally set the payment app's meta data to null.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppNullMetaData() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");

        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                null,
                /* signature= */ "01020304050607080900");

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated(/*no identifiers*/ );
    }

    /**
     * For finding app store billing app, test that the TWA only has default app store but no
     * support the billing method in its Android manifest. The test setting intentionally omits the
     * setting of the twa's supported methods.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppTwaHasDefaultAppStoreMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated("com.merchant.twa");
    }

    /**
     * For finding app store billing app, test that the TWA has support the billing method but no
     * default method in its manifest. The test setting intentionally set TWA's default method to a
     * non-store method.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppTwaHasSupportedAppStoreMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "an://invalid.url",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated("com.merchant.twa");
    }

    /**
     * For finding app store billing app, test that the TWA can request only the app store method of
     * its installer app store. The test intentionally add another app store, request the methods of
     * both stores, and set the twa to be installed from one of them.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppTwaCanSupportOnlyOneAppStoreMethods() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        methods.add("https://another.appstore.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://another.appstore.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        addAppStoreMethodAndFindApps(
                "com.another.appstore", new GURL("https://another.appstore.com/billing"), methods);

        assertPaymentAppsCreated("com.merchant.twa");
        assertPaymentAppHasMethods(mPaymentApps.get(0), "https://play.google.com/billing");
    }

    /**
     * For finding app store billing app, test that Chrome must be in TWA to use app store billing.
     * The test setting intentionally omits the twa mocking.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppMustInTwa() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        methods.add("https://bobpay.test/webpay");

        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://bobpay.test/webpay"});

        findApps(methods);

        assertPaymentAppsCreated("com.bobpay");
    }

    /**
     * For finding app store billing app, test that the payment request must support the app store
     * billing method to be able to use it. The test setting intentionally omits the app store
     * billing method in the payment request.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppNotRequested() throws Throwable {
        Set<String> noRequestedMethod = new HashSet<>();
        noRequestedMethod.add("https://bobpay.test/webpay");

        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://bobpay.test/webpay"});

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(noRequestedMethod);

        assertPaymentAppsCreated("com.bobpay");
    }

    /**
     * For finding app store billing app, test that Chrome can display payment apps of app-store
     * billing method and the normal (non-billing) payment method at the same time. The test setting
     * includes a normal native payment method and play billing method, and expects to see the
     * payment apps of both are displayed.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppInclusiveForBillingAndNonBilling() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://bobpay.test/webpay"});

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated("com.merchant.twa", "com.bobpay");
    }

    /**
     * For finding app store billing app, test that if delegation is requested along with the
     * app-store billing method, the app-store billing method would be ignored. The test setting
     * requests the shipping or payer contact delegation.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppDelegationRejectBilling() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        methods.add("https://bobpay.test/webpay");
        setRequestShipping(true);
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://bobpay.test/webpay"});

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated("com.bobpay");
    }

    /**
     * For finding app store billing app, test that the TWA's installer app store must be a
     * allowlisted one. The test setting sets the twa installer app store to be an unsupported one.
     */
    @Test
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppMustInSupportedAppStore() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://another.playstore.com/billing");
        methods.add("https://bobpay.test/webpay");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://another.playstore.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://another.playstore.com/billing"});
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://bobpay.test/webpay"});

        setMockTrustedWebActivity("com.merchant.twa", "com.another.appstore");
        findApps(methods);

        assertPaymentAppsCreated("com.bobpay");
    }

    /**
     * For finding app store billing app, test that the TWA can be installed from any source in
     * debug. The test setting sets the twa to be installed from an arbitrary source, and set the
     * debug mode enabled.
     */
    @Test
    @EnableFeatures({PaymentFeatureList.WEB_PAYMENTS_APP_STORE_BILLING_DEBUG})
    @Feature({"Payments"})
    public void testFindAppStoreBillingAppAllowedAnySourceInDebug() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.merchant.twa", new String[] {"https://play.google.com/billing"});

        setMockTrustedWebActivity("com.merchant.twa", "com.arbitrary.appstore");
        findApps(methods);

        assertPaymentAppsCreated("com.merchant.twa");
    }

    /**
     * If a payment method supports two apps from different origins, both apps should be found.
     * Repeated app look ups should succeed.
     */
    @Test
    @Feature({"Payments"})
    public void testTwoAppsFromDifferentOriginsWithTheSamePaymentMethod() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://jonpay.test/webpay");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "https://alicepay.test/webpay",
                /* signature= */ "ABCDEFABCDEFABCDEFAB");
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                /* signature= */ "01020304050607080900");
        mPackageManager.setStringArrayMetaData(
                "com.alicepay", new String[] {"https://jonpay.test/webpay"});
        mPackageManager.setStringArrayMetaData(
                "com.bobpay", new String[] {"https://jonpay.test/webpay"});

        findApps(methods);

        assertPaymentAppsCreated("com.alicepay", "com.bobpay");

        mPaymentApps.clear();
        mAllPaymentAppsCreated = false;

        findApps(methods);

        assertPaymentAppsCreated("com.alicepay", "com.bobpay");
    }

    /** Non-URL payment methods are not supported. */
    @Test
    @Feature({"Payments"})
    public void testNonUrlPaymentMethodNames() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("basic-card");
        methods.add("interledger");
        methods.add("payee-credit-transfer");
        methods.add("payer-credit-transfer");
        methods.add("tokenized-card");
        methods.add("not-supported");
        mPackageManager.installPaymentApp(
                "AlicePay",
                "com.alicepay",
                "" /* no default payment method name in metadata */,
                /* signature= */ "AA");
        mPackageManager.setStringArrayMetaData(
                "com.alicepay",
                new String[] {
                    "basic-card",
                    "interledger",
                    "payee-credit-transfer",
                    "payer-credit-transfer",
                    "tokenized-card",
                    "not-supported"
                });

        findApps(methods);

        Assert.assertTrue(mPaymentApps.isEmpty());
    }

    /**
     * Test BobPay with https://bobpay.test/webpay payment method name, and supported delegations
     */
    @Test
    @Feature({"Payments"})
    public void testPaymentAppWithSupportedDelegations() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        String[] supportedDelegations = {
            "shippingAddress", "payerName", "payerEmail", "payerPhone"
        };
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                supportedDelegations,
                /* signature= */ "01020304050607080900");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());

        // Verify supported delegations
        Assert.assertTrue(mPaymentApps.get(0).handlesShippingAddress());
        Assert.assertTrue(mPaymentApps.get(0).handlesPayerName());
        Assert.assertTrue(mPaymentApps.get(0).handlesPayerEmail());
        Assert.assertTrue(mPaymentApps.get(0).handlesPayerPhone());
    }

    /** Test that Chrome should not crash because of invalid supported delegations */
    @Test
    @Feature({"Payments"})
    public void testPaymentAppWithInavalidDelegationValue() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://bobpay.test/webpay");
        String[] invalidDelegations = {"invalidDelegation"};
        mPackageManager.installPaymentApp(
                "BobPay",
                "com.bobpay",
                "https://bobpay.test/webpay",
                invalidDelegations,
                /* signature= */ "01020304050607080900");

        findApps(methods);

        Assert.assertEquals("1 app should match the query", 1, mPaymentApps.size());
        Assert.assertEquals("com.bobpay", mPaymentApps.get(0).getIdentifier());

        // Verify that invalid delegation values are ignored.
        Assert.assertFalse(mPaymentApps.get(0).handlesShippingAddress());
        Assert.assertFalse(mPaymentApps.get(0).handlesPayerName());
        Assert.assertFalse(mPaymentApps.get(0).handlesPayerEmail());
        Assert.assertFalse(mPaymentApps.get(0).handlesPayerPhone());
    }

    /** Test that the Play Billing app store payment app is marked as preferred. */
    @Test
    @Feature({"Payments"})
    public void testPreferredPaymentApp() throws Throwable {
        Set<String> methods = new HashSet<>();
        methods.add("https://play.google.com/billing");
        mPackageManager.installPaymentApp(
                "MerchantTwaApp",
                "com.merchant.twa",
                "https://play.google.com/billing",
                /* signature= */ "01020304050607080900");

        setMockTrustedWebActivity("com.merchant.twa", "com.android.vending");
        findApps(methods);

        assertPaymentAppsCreated("com.merchant.twa");

        Assert.assertTrue(mPaymentApps.get(0).isPreferred());
    }

    private void findApps(Set<String> methodNames) throws Throwable {
        addAppStoreMethodAndFindApps(
                /* appStorePackageName= */ null, /* appStorePaymentMethod= */ null, methodNames);
    }

    private void addAppStoreMethodAndFindApps(
            String appStorePackageName, GURL appStorePaymentMethod, Set<String> methodNames)
            throws Throwable {
        mMethodData = buildMethodData(methodNames);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    AndroidPaymentAppFinder finder =
                            new AndroidPaymentAppFinder(
                                    new PaymentManifestWebDataService(getWebContents()),
                                    mDownloader,
                                    new PaymentManifestParser(),
                                    mPackageManager,
                                    /* delegate= */ this,
                                    /* factory= */ null);
                    finder.bypassIsReadyToPayServiceInTest();
                    if (appStorePackageName != null) {
                        assert appStorePaymentMethod != null;
                        assert appStorePaymentMethod.isValid();
                        finder.addAppStoreForTest(appStorePackageName, appStorePaymentMethod);
                    }
                    finder.findAndroidPaymentApps();
                });
        CriteriaHelper.pollInstrumentationThread(() -> mAllPaymentAppsCreated);
    }

    private static Map<String, PaymentMethodData> buildMethodData(Set<String> methodNames) {
        Map<String, PaymentMethodData> result = new HashMap<>();
        for (String methodName : methodNames) {
            PaymentMethodData methodData = new PaymentMethodData();
            methodData.supportedMethod = methodName;
            result.put(methodName, methodData);
        }
        return result;
    }

    private void setMockTrustedWebActivity(String twaPackageName, String installerPackageName) {
        mTwaPackageName = twaPackageName;
        mPackageManager.mockInstallerForPackage(twaPackageName, installerPackageName);
    }

    private void assertPaymentAppsCreated(String... expectedIds) {
        Set<String> ids = new HashSet<>();
        for (PaymentApp app : mPaymentApps) {
            ids.add(app.getIdentifier());
        }
        Assert.assertEquals(
                String.format(
                        Locale.getDefault(),
                        "Expected %d apps, but got %d apps instead.",
                        expectedIds.length,
                        ids.size()),
                expectedIds.length,
                ids.size());
        for (String expectedId : expectedIds) {
            Assert.assertTrue(
                    String.format(
                            Locale.getDefault(),
                            "Expected id %s is not found. "
                                    + "Expected identifiers: %s. "
                                    + "Actual identifiers: %s",
                            expectedId,
                            Arrays.toString(expectedIds),
                            ids.toString()),
                    ids.contains(expectedId));
        }
    }

    private void assertNoPaymentAppsCreated() {
        assertPaymentAppsCreated();
    }

    private void assertPaymentAppHasMethods(PaymentApp app, String... expectedMethodNames) {
        Set<String> methodNames = app.getInstrumentMethodNames();
        Assert.assertEquals(
                String.format(
                        Locale.getDefault(),
                        "Expected %d methods, but got %d methods instead.",
                        expectedMethodNames.length,
                        methodNames.size()),
                expectedMethodNames.length,
                methodNames.size());
        for (String expectedId : expectedMethodNames) {
            Assert.assertTrue(
                    String.format(
                            Locale.getDefault(),
                            "Expected method %s is not found. "
                                    + "Expected methods: %s. "
                                    + "Actual methods: %s",
                            expectedId,
                            Arrays.toString(expectedMethodNames),
                            methodNames.toString()),
                    methodNames.contains(expectedId));
        }
    }
}