chromium/components/installedapp/android/java/src/org/chromium/components/installedapp/InstalledAppProviderTest.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.components.installedapp;

import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Pair;

import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import org.chromium.base.Callback;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.JniMocker;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import org.chromium.content_public.browser.test.mock.MockRenderFrameHost;
import org.chromium.installedapp.mojom.InstalledAppProvider;
import org.chromium.installedapp.mojom.RelatedApplication;
import org.chromium.url.GURL;
import org.chromium.url.mojom.Url;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/** Ensure that the InstalledAppProvider returns the correct apps. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class InstalledAppProviderTest {
    private static final String ASSET_STATEMENTS_KEY =
            InstalledAppProviderImpl.ASSET_STATEMENTS_KEY;
    private static final String RELATION_HANDLE_ALL_URLS =
            "delegate_permission/common.handle_all_urls";
    private static final String NAMESPACE_WEB =
            InstalledAppProviderImpl.ASSET_STATEMENT_NAMESPACE_WEB;
    private static final String PLATFORM_ANDROID =
            InstalledAppProviderImpl.RELATED_APP_PLATFORM_ANDROID;
    private static final String PLATFORM_WEBAPP =
            InstalledAppProviderImpl.RELATED_APP_PLATFORM_WEBAPP;
    private static final int MAX_ALLOWED_RELATED_APPS =
            InstalledAppProviderImpl.MAX_ALLOWED_RELATED_APPS;
    private static final String PLATFORM_OTHER = "itunes";
    // Note: Android package name and origin deliberately unrelated (there is no requirement that
    // they be the same).
    private static final String PACKAGE_NAME_1 = "com.app1.package";
    private static final String PACKAGE_NAME_2 = "com.app2.package";
    private static final String PACKAGE_NAME_3 = "com.app3.package";
    private static final String URL_UNRELATED = "https://appstore.example.com/app1";
    private static final String ORIGIN = "https://example.com:8000";
    private static final String URL_ON_ORIGIN =
            "https://example.com:8000/path/to/page.html?key=value#fragment";
    private static final String MANIFEST_URL = "https://example.com:8000/manifest.json";
    private static final String OTHER_MANIFEST_URL = "https://example2.com:8000/manifest.json";
    private static final String OTHER_MANIFEST_URL3 = "https://example3.com:8000/manifest.json";
    private static final String OTHER_MANIFEST_URL4 = "https://example4.com:8000/manifest.json";
    private static final String ORIGIN_SYNTAX_ERROR = "https:{";
    private static final String ORIGIN_MISSING_SCHEME = "path/only";
    private static final String ORIGIN_MISSING_HOST = "file:///path/piece";
    private static final String ORIGIN_MISSING_PORT = "http://example.com";
    private static final String ORIGIN_DIFFERENT_SCHEME = "http://example.com:8000";
    private static final String ORIGIN_DIFFERENT_HOST = "https://example.org:8000";
    private static final String ORIGIN_DIFFERENT_PORT = "https://example.com:8001";

    @Rule public JniMocker mocker = new JniMocker();

    @Mock private MockRenderFrameHost mMockRenderFrameHost;
    private FakePackageManager mFakePackageManager;
    private InstalledAppProviderTestImpl mInstalledAppProvider;
    private FakeInstantAppsHandler mFakeInstantAppsHandler;
    private TestInstalledAppProviderImplJni mTestInstalledAppProviderImplJni;

    private static class FakePackageManager extends PackageManagerDelegate {
        private Map<String, PackageInfo> mPackageInfo = new HashMap<>();
        private Map<String, Resources> mResources = new HashMap<>();

        // The set of installed WebAPKs identified by their manifest URL.
        private Set<String> mInstalledWebApks = new HashSet<>();

        public void addPackageInfo(PackageInfo packageInfo) {
            mPackageInfo.put(packageInfo.packageName, packageInfo);
        }

        public void addResources(String packageName, Resources resources) {
            mResources.put(packageName, resources);
        }

        public void addWebApk(String manifestUrl) {
            mInstalledWebApks.add(manifestUrl);
        }

        public boolean isWebApkInstalled(String manifestUrl) {
            return mInstalledWebApks.contains(manifestUrl);
        }

        @Override
        public ApplicationInfo getApplicationInfo(String packageName, int flags)
                throws NameNotFoundException {
            return getPackageInfo(packageName, flags).applicationInfo;
        }

        @Override
        public Resources getResourcesForApplication(ApplicationInfo appInfo)
                throws NameNotFoundException {
            if (!mResources.containsKey(appInfo.packageName)) throw new NameNotFoundException();
            return mResources.get(appInfo.packageName);
        }

        @Override
        public PackageInfo getPackageInfo(String packageName, int flags)
                throws NameNotFoundException {
            if (!mPackageInfo.containsKey(packageName)) throw new NameNotFoundException();
            return mPackageInfo.get(packageName);
        }
    }

    private class InstalledAppProviderTestImpl extends InstalledAppProviderImpl {
        public InstalledAppProviderTestImpl(
                RenderFrameHost renderFrameHost, FakeInstantAppsHandler instantAppsHandler) {
            super(
                    new BrowserContextHandle() {
                        @Override
                        public long getNativeBrowserContextPointer() {
                            return 1;
                        }
                    },
                    renderFrameHost,
                    instantAppsHandler::isInstantAppAvailable);
        }

        @Override
        public boolean isWebApkInstalled(String manifestUrl) {
            return mFakePackageManager.isWebApkInstalled(manifestUrl);
        }
    }

    private static class TestInstalledAppProviderImplJni
            implements InstalledAppProviderImpl.Natives {
        private final Map<String, String> mRelationMap = new HashMap<>();
        private ArrayList<Pair<Callback<Boolean>, Boolean>> mCallbacks;

        public void addVerfication(String webDomain, String manifestUrl) {
            mRelationMap.put(webDomain, manifestUrl);
        }

        @Override
        public void checkDigitalAssetLinksRelationshipForWebApk(
                BrowserContextHandle browserContextHandle,
                String webDomain,
                String manifestUrl,
                Callback<Boolean> callback) {
            boolean result =
                    mRelationMap.containsKey(webDomain)
                            && mRelationMap.get(webDomain).equals(manifestUrl);
            if (mCallbacks == null) {
                callback.onResult(result);
                return;
            }

            mCallbacks.add(Pair.create(callback, result));
            if (mCallbacks.size() == 3) {
                Pair<Callback<Boolean>, Boolean> stashed = mCallbacks.get(1);
                stashed.first.onResult(stashed.second);
                stashed = mCallbacks.get(0);
                stashed.first.onResult(stashed.second);
                stashed = mCallbacks.get(2);
                stashed.first.onResult(stashed.second);
            }
        }

        // If called, the callbacks passed to {@link checkDigitalAssetLinksRelationshipForWebApk}
        // won't be executed until it's been called three times, and then they'll be executed out of
        // order.
        void rearrangeOrderOfResults() {
            mCallbacks = new ArrayList<>();
        }
    }

    /**
     * FakeInstantAppsHandler lets us mock getting RelatedApplications from a URL in the absence of
     * proper GMSCore calls.
     */
    private static class FakeInstantAppsHandler {
        private final List<Pair<String, Boolean>> mRelatedApplicationList;

        public FakeInstantAppsHandler() {
            mRelatedApplicationList = new ArrayList<Pair<String, Boolean>>();
        }

        public void addInstantApp(String url, boolean holdback) {
            mRelatedApplicationList.add(Pair.create(url, holdback));
        }

        public void resetForTest() {
            mRelatedApplicationList.clear();
        }

        // TODO(thildebr): When the implementation of isInstantAppAvailable is complete, we need to
        // test its functionality instead of stubbing it out here. Instead we can create a wrapper
        // around the GMSCore functionality we need and override that here instead.
        public boolean isInstantAppAvailable(
                String url, boolean checkHoldback, boolean includeUserPrefersBrowser) {
            for (Pair<String, Boolean> pair : mRelatedApplicationList) {
                if (url.startsWith(pair.first) && checkHoldback == pair.second) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Helper function allows for the "installation" of Android package names and setting up
     * Resources for installed packages.
     */
    private void setMetaDataAndResourcesForTest(
            String packageName, Bundle metaData, Resources resources) {
        PackageInfo packageInfo = new PackageInfo();
        packageInfo.packageName = packageName;
        packageInfo.applicationInfo = new ApplicationInfo();
        packageInfo.applicationInfo.packageName = packageName;
        packageInfo.applicationInfo.metaData = metaData;

        mFakePackageManager.addPackageInfo(packageInfo);
        mFakePackageManager.addResources(packageName, resources);
    }

    /**
     * Fakes the Resources object, allowing lookup of a single String value.
     *
     * <p>Note: The real Resources object defines a mapping to many values. This fake object only
     * allows a single value in the mapping, and it must be a String (which is all that is required
     * for these tests).
     */
    private static class FakeResources extends Resources {
        private static AssetManager sAssetManager = createAssetManager();
        private final int mId;
        private final String mValue;

        private static AssetManager createAssetManager() {
            try {
                return AssetManager.class.getConstructor().newInstance();
            } catch (Exception e) {
                return null;
            }
        }

        // Do not warn about deprecated call to Resources(); the documentation says code is not
        // supposed to create its own Resources object, but we are using it to fake out the
        // Resources, and there is no other way to do that.
        @SuppressWarnings("deprecation")
        public FakeResources(int identifier, String value) {
            super(sAssetManager, null, null);
            mId = identifier;
            mValue = value;
        }

        @Override
        public int getIdentifier(String name, String defType, String defPackage) {
            if (name == null) throw new NullPointerException();

            // There is *no guarantee* (in the Digital Asset Links spec) about what the string
            // resource should be called ("asset_statements" is just an example). Therefore,
            // getIdentifier cannot be used to get the asset statements string. Always fail the
            // lookup here, to ensure the implementation isn't relying on any particular hard-coded
            // string.
            return 0;
        }

        @Override
        public String getString(int id) {
            if (id != mId) {
                throw new Resources.NotFoundException("id 0x" + Integer.toHexString(id));
            }

            return mValue;
        }
    }

    /** Creates a metaData bundle with a single resource-id key. */
    private static Bundle createMetaData(String metaDataName, int metaDataResourceId) {
        Bundle metaData = new Bundle();
        metaData.putInt(metaDataName, metaDataResourceId);
        return metaData;
    }

    /**
     * Sets a resource with a single key-value pair in an Android package's manifest.
     *
     * <p>The value is always a string.
     */
    private void setStringResource(String packageName, String key, String value) {
        int identifier = 0x1234;
        Bundle metaData = createMetaData(key, identifier);
        FakeResources resources = new FakeResources(identifier, value);
        setMetaDataAndResourcesForTest(packageName, metaData, resources);
    }

    /** Creates a valid Android asset statement string. */
    private String createAssetStatement(String platform, String relation, String url) {
        return String.format(
                "{\"relation\": [\"%s\"], \"target\": {\"namespace\": \"%s\", \"site\": \"%s\"}}",
                relation, platform, url);
    }

    /**
     * Sets an asset statement to an Android package's manifest (in the fake package manager).
     *
     * <p>Only one asset statement can be set for a given package (if this is called twice on the
     * same package, overwrites the previous asset statement).
     *
     * <p>This corresponds to a Statement List in the Digital Asset Links spec v1.
     */
    private void setAssetStatement(
            String packageName, String platform, String relation, String url) {
        String statements = "[" + createAssetStatement(platform, relation, url) + "]";
        setStringResource(packageName, ASSET_STATEMENTS_KEY, statements);
    }

    /** Creates a RelatedApplication to put in the web app manifest. */
    private RelatedApplication createRelatedApplication(String platform, String id, String url) {
        RelatedApplication application = new RelatedApplication();
        application.platform = platform;
        application.id = id;
        application.url = url;
        return application;
    }

    /**
     * Calls filterInstalledApps with the given inputs, and tests that the expected result is
     * returned.
     */
    private void verifyInstalledApps(
            RelatedApplication[] manifestRelatedApps,
            RelatedApplication[] expectedInstalledRelatedApps)
            throws Exception {
        final AtomicBoolean called = new AtomicBoolean(false);
        Url manifestUrl = new Url();
        manifestUrl.url = MANIFEST_URL;

        mInstalledAppProvider.filterInstalledApps(
                manifestRelatedApps,
                manifestUrl,
                new InstalledAppProvider.FilterInstalledApps_Response() {
                    @Override
                    public void call(RelatedApplication[] installedRelatedApps) {
                        Assert.assertEquals(
                                expectedInstalledRelatedApps.length, installedRelatedApps.length);

                        for (int i = 0; i < installedRelatedApps.length; i++) {
                            Assert.assertEquals(
                                    expectedInstalledRelatedApps[i], installedRelatedApps[i]);
                        }
                        called.set(true);
                    }
                });
        CriteriaHelper.pollUiThreadNested(() -> called.get());
    }

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();

        mTestInstalledAppProviderImplJni = new TestInstalledAppProviderImplJni();
        mocker.mock(InstalledAppProviderImplJni.TEST_HOOKS, mTestInstalledAppProviderImplJni);

        GURL urlOnOrigin = new GURL(URL_ON_ORIGIN);
        Mockito.when(mMockRenderFrameHost.getLastCommittedURL()).thenReturn(urlOnOrigin);

        mFakePackageManager = new FakePackageManager();
        mFakeInstantAppsHandler = new FakeInstantAppsHandler();
        mInstalledAppProvider =
                new InstalledAppProviderTestImpl(mMockRenderFrameHost, mFakeInstantAppsHandler);
        mInstalledAppProvider.setPackageManagerDelegateForTest(mFakePackageManager);
    }

    /** Origin of the page using the API is missing certain parts of the URI. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOriginMissingParts() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };
        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};

        GURL originMissingScheme = new GURL(ORIGIN_MISSING_SCHEME);
        Mockito.when(mMockRenderFrameHost.getLastCommittedURL()).thenReturn(originMissingScheme);
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        GURL originMissingHost = new GURL(ORIGIN_MISSING_HOST);
        Mockito.when(mMockRenderFrameHost.getLastCommittedURL()).thenReturn(originMissingHost);
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * No related Android apps.
     *
     * <p>An Android app relates to the web app, but not mutual.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testNoRelatedApps() throws Exception {
        // The web manifest has no related apps.
        RelatedApplication manifestRelatedApps[] = new RelatedApplication[] {};

        // One Android app is installed named |PACKAGE_NAME_1|. It has a related web app with origin
        // |ORIGIN|.
        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * One related Android app with no id (package name).
     *
     * <p>An Android app relates to the web app, but not mutual.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedAppNoId() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {createRelatedApplication(PLATFORM_ANDROID, null, null)};

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * One related app (from a non-Android platform).
     *
     * <p>An Android app with the same id relates to the web app. This should be ignored since the
     * manifest doesn't mention the Android app.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedNonAndroidApp() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_OTHER, PACKAGE_NAME_1, null)
                };

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * One related Android app; Android app is not installed.
     *
     * <p>Another Android app relates to the web app, but not mutual.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedAppNotInstalled() throws Exception {
        // The web manifest has a related Android app named |PACKAGE_NAME_1|.
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        // One Android app is installed named |PACKAGE_NAME_2|. It has a related web app with origin
        // |ORIGIN|.
        setAssetStatement(PACKAGE_NAME_2, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app manifest has an asset_statements key, but the resource it links to is missing. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedAppBrokenAssetStatementsResource() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        Bundle metaData = createMetaData(ASSET_STATEMENTS_KEY, 0x1234);
        String statements =
                "[" + createAssetStatement(NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN) + "]";
        FakeResources resources = new FakeResources(0x4321, statements);
        setMetaDataAndResourcesForTest(PACKAGE_NAME_1, metaData, resources);
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** One related Android app; Android app is not mutually related (has no asset_statements). */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedAppNoAssetStatements() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setStringResource(PACKAGE_NAME_1, null, null);
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** One related Android app; Android app is not mutually related (has no asset_statements). */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedAppNoAssetStatementsNullMetadata() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        FakeResources resources = new FakeResources(0x4321, null);
        setMetaDataAndResourcesForTest(PACKAGE_NAME_1, null, resources);
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * One related Android app; Android app is related to other origins.
     *
     * <p>Tests three cases: - The Android app is related to a web app with a different scheme. -
     * The Android app is related to a web app with a different host. - The Android app is related
     * to a web app with a different port.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneRelatedAppRelatedToDifferentOrigins() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_DIFFERENT_SCHEME);
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_DIFFERENT_HOST);
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_DIFFERENT_PORT);
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** One related Android app; Android app is installed and mutually related. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOneInstalledRelatedApp() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * Change the frame URL and ensure the app relates to the new URL, not the old one.
     *
     * <p>This simulates navigating the frame while keeping the same Mojo service open.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testDynamicallyChangingUrl() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_DIFFERENT_SCHEME);

        // Should be empty, since Android app does not relate to this frame's origin.
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        // Simulate a navigation to a different origin.
        GURL originDifferentScheme = new GURL(ORIGIN_DIFFERENT_SCHEME);
        Mockito.when(mMockRenderFrameHost.getLastCommittedURL()).thenReturn(originDifferentScheme);

        // Now the result should include the Android app that relates to the new origin.
        expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        // Simulate the native RenderFrameHost disappearing.
        Mockito.when(mMockRenderFrameHost.getLastCommittedURL()).thenReturn(null);

        expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** One related Android app (installed and mutually related), with a non-null URL field. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testInstalledRelatedAppWithUrl() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, URL_UNRELATED)
                };

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** One related Android app; Android app is related to multiple origins. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testMultipleAssetStatements() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        // Create an asset_statements field with multiple statements. The second one matches the web
        // app.
        String statements =
                "["
                        + createAssetStatement(
                                NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_DIFFERENT_HOST)
                        + ", "
                        + createAssetStatement(NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN)
                        + "]";
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** A JSON syntax error in the Android app's asset statement. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementSyntaxError() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statements = "[{\"target\" {}}]";
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** The Android app's asset statement is not an array. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementNotArray() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statement = createAssetStatement(NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statement);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** The Android app's asset statement array contains non-objects. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementArrayNoObjects() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statements =
                "["
                        + createAssetStatement(NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN)
                        + ", 4]";
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        // Expect it to ignore the integer and successfully parse the valid object.
        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * Android app has no "relation" in the asset statement.
     *
     * <p>Currently, the relation string (in the Android package's asset statement) is ignored, so
     * the app is still returned as "installed".
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementNoRelation() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statements =
                String.format(
                        "[{\"target\": {\"namespace\": \"%s\", \"site\": \"%s\"}}]",
                        NAMESPACE_WEB, ORIGIN);
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        // TODO(mgiuca): [Spec issue] Should we require a specific relation string, rather than any
        // or no relation?
        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * Android app is related with a non-standard relation.
     *
     * <p>Currently, the relation string (in the Android package's asset statement) is ignored, so
     * any will do. Is this desirable, or do we want to require a specific relation string?
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementNonStandardRelation() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, "nonstandard/relation", ORIGIN);

        // TODO(mgiuca): [Spec issue] Should we require a specific relation string, rather than any
        // or no relation?
        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app has no "target" in the asset statement. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementNoTarget() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statements = String.format("[{\"relation\": [\"%s\"]}]", RELATION_HANDLE_ALL_URLS);
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app has no "namespace" in the asset statement. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementNoNamespace() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statements =
                String.format(
                        "[{\"relation\": [\"%s\"], \"target\": {\"site\": \"%s\"}}]",
                        RELATION_HANDLE_ALL_URLS, ORIGIN);
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app is related, but not to the web namespace. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testNonWebAssetStatement() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(PACKAGE_NAME_1, "play", RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app has no "site" in the asset statement. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementNoSite() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String statements =
                String.format(
                        "[{\"relation\": [\"%s\"], \"target\": {\"namespace\": \"%s\"}}]",
                        RELATION_HANDLE_ALL_URLS, NAMESPACE_WEB);
        setStringResource(PACKAGE_NAME_1, ASSET_STATEMENTS_KEY, statements);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app has a syntax error in the "site" field of the asset statement. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementSiteSyntaxError() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_SYNTAX_ERROR);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Android app has a "site" field missing certain parts of the URI (scheme, host, port). */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementSiteMissingParts() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_MISSING_SCHEME);
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_MISSING_HOST);
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);

        setAssetStatement(
                PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN_MISSING_PORT);
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * One related Android app; Android app is related with a path part in the "site" field.
     *
     * <p>The path part shouldn't really be there (according to the Digital Asset Links spec), but
     * if it is, we are lenient and just ignore it (matching only the origin).
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testAssetStatementSiteHasPath() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        String site = ORIGIN + "/path";
        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, site);

        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * One related Android app; Android app is installed and mutually related.
     *
     * <p>Another Android app relates to the web app, but not mutual.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testExtraInstalledApp() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);
        setAssetStatement(PACKAGE_NAME_2, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * Two related Android apps; Android apps both installed and mutually related.
     *
     * <p>Web app also related to an app with the same name on another platform, and another Android
     * app which is not installed.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testMultipleInstalledRelatedApps() throws Exception {
        RelatedApplication[] manifestRelatedApps =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null),
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_2, null),
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_3, null)
                };

        setAssetStatement(PACKAGE_NAME_2, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);
        setAssetStatement(PACKAGE_NAME_3, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        RelatedApplication[] expectedInstalledRelatedApps =
                new RelatedApplication[] {manifestRelatedApps[1], manifestRelatedApps[2]};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Tests the pseudo-random artificial delay to counter a timing attack. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testArtificialDelay() throws Exception {
        byte[] salt = {
            0x64, 0x09, -0x68, -0x25, 0x70, 0x11, 0x25, 0x24, 0x68, -0x1a, 0x08, 0x79, -0x12, -0x50,
            0x3b, -0x57, -0x17, -0x4d, 0x46, 0x02
        };
        PackageHash.setGlobalSaltForTesting(salt);
        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        // Installed app.
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null)
                };
        RelatedApplication[] expectedInstalledRelatedApps = manifestRelatedApps;
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
        // This expectation is based on HMAC_SHA256(salt, packageName encoded in UTF-8), taking the
        // low 10 bits of the first two bytes of the result / 100.
        Assert.assertEquals(2, mInstalledAppProvider.mLastDelayForTesting);

        // Non-installed app.
        manifestRelatedApps =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_2, null)
                };
        expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
        // This expectation is based on HMAC_SHA256(salt, packageName encoded in UTF-8), taking the
        // low 10 bits of the first two bytes of the result / 100.
        Assert.assertEquals(5, mInstalledAppProvider.mLastDelayForTesting);

        // Own WebAPK.
        manifestRelatedApps =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_WEBAPP, null, MANIFEST_URL)
                };
        expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
        // This expectation is based on HMAC_SHA256(salt, manifestUrl encoded in UTF-8), taking the
        // low 10 bits of the first two bytes of the result / 100.
        Assert.assertEquals(3, mInstalledAppProvider.mLastDelayForTesting);

        // Another WebAPK.
        manifestRelatedApps =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_WEBAPP, null, OTHER_MANIFEST_URL)
                };
        expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
        // This expectation is based on HMAC_SHA256(salt, manifestUrl encoded in UTF-8), taking the
        // low 10 bits of the first two bytes of the result / 100.
        Assert.assertEquals(8, mInstalledAppProvider.mLastDelayForTesting);
    }

    @Test
    @SmallTest
    @UiThreadTest
    public void testMultipleAppsIncludingInstantApps() throws Exception {
        RelatedApplication[] manifestRelatedApps =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null),
                    // Instant Apps:
                    createRelatedApplication(
                            PLATFORM_ANDROID,
                            InstalledAppProviderImpl.INSTANT_APP_ID_STRING,
                            ORIGIN),
                    createRelatedApplication(
                            PLATFORM_ANDROID,
                            InstalledAppProviderImpl.INSTANT_APP_HOLDBACK_ID_STRING,
                            ORIGIN)
                };

        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);
        mFakeInstantAppsHandler.addInstantApp(ORIGIN, true);

        RelatedApplication[] expectedInstalledRelatedApps =
                new RelatedApplication[] {manifestRelatedApps[0], manifestRelatedApps[2]};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /**
     * Multiple related uninstalled apps (over the allowed limit) followed by one related Android
     * app which is installed and mutually related.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testRelatedAppsOverAllowedThreshold() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[MAX_ALLOWED_RELATED_APPS + 1];
        for (int i = 0; i < MAX_ALLOWED_RELATED_APPS; i++) {
            manifestRelatedApps[i] =
                    createRelatedApplication(
                            PLATFORM_ANDROID, PACKAGE_NAME_2 + String.valueOf(i), null);
        }
        manifestRelatedApps[MAX_ALLOWED_RELATED_APPS] =
                createRelatedApplication(PLATFORM_ANDROID, PACKAGE_NAME_1, null);
        setAssetStatement(PACKAGE_NAME_1, NAMESPACE_WEB, RELATION_HANDLE_ALL_URLS, ORIGIN);

        // Although the app is installed, and verifiable, it was included after the maximum allowed
        // number of related apps.
        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Check that a website can find its own WebAPK when installed. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testInstalledWebApkForWebsite() throws Exception {
        RelatedApplication webApk = createRelatedApplication(PLATFORM_WEBAPP, null, MANIFEST_URL);
        RelatedApplication manifestRelatedApps[] = new RelatedApplication[] {webApk};
        mFakePackageManager.addWebApk(MANIFEST_URL);

        RelatedApplication[] expectedInstalledRelatedApps = new RelatedApplication[] {webApk};
        verifyInstalledApps(manifestRelatedApps, expectedInstalledRelatedApps);
    }

    /** Check that a website can find another WebAPK when installed & verfied. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testInstalledWebApkForOtherWebsite() throws Exception {
        RelatedApplication webApk =
                createRelatedApplication(PLATFORM_WEBAPP, null, OTHER_MANIFEST_URL);
        RelatedApplication manifestRelatedApps[] = new RelatedApplication[] {webApk};
        mFakePackageManager.addWebApk(OTHER_MANIFEST_URL);

        verifyInstalledApps(manifestRelatedApps, new RelatedApplication[] {});

        mTestInstalledAppProviderImplJni.addVerfication(OTHER_MANIFEST_URL, MANIFEST_URL);
        verifyInstalledApps(manifestRelatedApps, new RelatedApplication[] {webApk});
    }

    /** Check that a website can query another WebAPK when not installed but verfied. */
    @Test
    @SmallTest
    @UiThreadTest
    public void testInstalledWebApkForOtherWebsiteNotInstalled() throws Exception {
        RelatedApplication webApk =
                createRelatedApplication(PLATFORM_WEBAPP, null, OTHER_MANIFEST_URL);
        RelatedApplication manifestRelatedApps[] = new RelatedApplication[] {webApk};

        mTestInstalledAppProviderImplJni.addVerfication(MANIFEST_URL, OTHER_MANIFEST_URL);
        verifyInstalledApps(manifestRelatedApps, new RelatedApplication[] {});
    }

    /**
     * Tests that the order of returned filtered apps matches the order of the apps passed to {@link
     * filterInstalledApps}.
     */
    @Test
    @SmallTest
    @UiThreadTest
    public void testOrderOfResults() throws Exception {
        RelatedApplication manifestRelatedApps[] =
                new RelatedApplication[] {
                    createRelatedApplication(PLATFORM_WEBAPP, null, OTHER_MANIFEST_URL),
                    createRelatedApplication(PLATFORM_WEBAPP, null, OTHER_MANIFEST_URL3),
                    createRelatedApplication(PLATFORM_WEBAPP, null, OTHER_MANIFEST_URL4)
                };
        mFakePackageManager.addWebApk(OTHER_MANIFEST_URL);
        mFakePackageManager.addWebApk(OTHER_MANIFEST_URL3);
        mFakePackageManager.addWebApk(OTHER_MANIFEST_URL4);
        mTestInstalledAppProviderImplJni.addVerfication(OTHER_MANIFEST_URL, MANIFEST_URL);
        mTestInstalledAppProviderImplJni.addVerfication(OTHER_MANIFEST_URL3, MANIFEST_URL);
        mTestInstalledAppProviderImplJni.addVerfication(OTHER_MANIFEST_URL4, MANIFEST_URL);

        mTestInstalledAppProviderImplJni.rearrangeOrderOfResults();
        verifyInstalledApps(manifestRelatedApps, manifestRelatedApps);
    }
}