chromium/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappRegistryTest.java

// Copyright 2015 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.webapps;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.text.TextUtils;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ContextUtils;
import org.chromium.base.task.test.BackgroundShadowAsyncTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.ColorProvider;
import org.chromium.chrome.browser.browserservices.intents.WebApkExtras;
import org.chromium.chrome.browser.browserservices.intents.WebappExtras;
import org.chromium.chrome.browser.browserservices.intents.WebappInfo;
import org.chromium.chrome.browser.browsing_data.UrlFilters;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.webapps.WebappRegistry.GetWebApkSpecificsImplSetWebappInfoForTesting;
import org.chromium.chrome.test.util.browser.webapps.WebApkIntentDataProviderBuilder;
import org.chromium.components.sync.protocol.WebApkSpecifics;
import org.chromium.ui.util.ColorUtils;

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

/**
 * Tests the WebappRegistry class by ensuring that it persists data to
 * SharedPreferences as expected.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {BackgroundShadowAsyncTask.class})
@LooperMode(LooperMode.Mode.LEGACY)
public class WebappRegistryTest {
    // These were copied from WebappRegistry for backward compatibility checking.
    private static final String REGISTRY_FILE_NAME = "webapp_registry";
    private static final String KEY_WEBAPP_SET = "webapp_set";
    private static final String KEY_LAST_CLEANUP = "last_cleanup";

    private static final String START_URL = "https://foo.com";

    private static final int INITIAL_TIME = 0;

    private SharedPreferences mSharedPreferences;
    private boolean mCallbackCalled;

    @Rule public JniMocker mJniMocker = new JniMocker();

    private static class FetchStorageCallback
            implements WebappRegistry.FetchWebappDataStorageCallback {
        BrowserServicesIntentDataProvider mIntentDataProvider;
        boolean mCallbackCalled;

        FetchStorageCallback(BrowserServicesIntentDataProvider intentDataProvider) {
            mIntentDataProvider = intentDataProvider;
        }

        @Override
        public void onWebappDataStorageRetrieved(WebappDataStorage storage) {
            mCallbackCalled = true;
            if (mIntentDataProvider != null) {
                storage.updateFromWebappIntentDataProvider(mIntentDataProvider);
            }
        }

        boolean getCallbackCalled() {
            return mCallbackCalled;
        }
    }

    private static class TestWebApkSyncServiceJni implements WebApkSyncService.Natives {
        @Override
        public void onWebApkUsed(byte[] webApkSpecifics, boolean isInstall) {}

        @Override
        public void onWebApkUninstalled(String manifestId) {}

        @Override
        public void removeOldWebAPKsFromSync(long currentTimeMsSinceUnixEpoch) {}

        @Override
        public void fetchRestorableApps(
                Profile profile, WebApkSyncService.PwaRestorableListCallback callback) {}
    }

    @Before
    public void setUp() {
        WebappRegistry.refreshSharedPrefsForTesting();
        mSharedPreferences =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(REGISTRY_FILE_NAME, Context.MODE_PRIVATE);
        mSharedPreferences.edit().putLong(KEY_LAST_CLEANUP, INITIAL_TIME).commit();

        mCallbackCalled = false;

        mJniMocker.mock(WebApkSyncServiceJni.TEST_HOOKS, new TestWebApkSyncServiceJni());
    }

    private void registerWebapp(BrowserServicesIntentDataProvider intentDataProvider)
            throws Exception {
        registerWebappWithId(intentDataProvider.getWebappExtras().id, intentDataProvider);
    }

    private void registerWebappWithId(
            String webappId, BrowserServicesIntentDataProvider intentDataProvider)
            throws Exception {
        FetchStorageCallback callback = new FetchStorageCallback(intentDataProvider);
        WebappRegistry.getInstance().register(webappId, callback);

        // Run background tasks to make sure the data is committed. Run UI thread tasks to make sure
        // the last used time is updated.
        BackgroundShadowAsyncTask.runBackgroundTasks();
        ShadowLooper.runUiThreadTasks();
        assertTrue(callback.getCallbackCalled());
    }

    @Test
    @Feature({"Webapp"})
    public void testBackwardCompatibility() {
        assertEquals(REGISTRY_FILE_NAME, WebappRegistry.REGISTRY_FILE_NAME);
        assertEquals(KEY_WEBAPP_SET, WebappRegistry.KEY_WEBAPP_SET);
        assertEquals(KEY_LAST_CLEANUP, WebappRegistry.KEY_LAST_CLEANUP);
    }

    @Test
    @Feature({"Webapp"})
    public void testWebappRegistrationAddsToSharedPrefs() throws Exception {
        registerWebappWithId("test", null);
        Set<String> actual = getRegisteredWebapps();
        assertEquals(1, actual.size());
        assertTrue(actual.contains("test"));
    }

    @Test
    @Feature({"Webapp"})
    public void testWebappRegistrationUpdatesLastUsed() throws Exception {
        registerWebappWithId("test", null);

        long after = System.currentTimeMillis();
        SharedPreferences webAppPrefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "test",
                                Context.MODE_PRIVATE);
        long actual =
                webAppPrefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertTrue("Timestamp is out of range", actual <= after);
    }

    @Test
    @Feature({"Webapp"})
    public void testWebappIdsRetrieval() {
        final Set<String> expected = addWebappsToRegistry("first", "second");
        assertEquals(expected, WebappRegistry.getRegisteredWebappIdsForTesting());
    }

    @Test
    @Feature({"Webapp"})
    public void testWebappIdsRetrievalRegisterRetrival() throws Exception {
        final Set<String> expected = addWebappsToRegistry("first");
        assertEquals(expected, WebappRegistry.getRegisteredWebappIdsForTesting());

        // Force a re-read of the preferences from disk. Add a new web app via the registry.
        WebappRegistry.refreshSharedPrefsForTesting();
        registerWebappWithId("second", null);

        // A copy of the expected set needs to be made as the SharedPreferences is using the copy
        // that was passed to it.
        final Set<String> secondExpected = new HashSet<>(expected);
        secondExpected.add("second");
        assertEquals(secondExpected, WebappRegistry.getRegisteredWebappIdsForTesting());
    }

    /**
     * Test behaviour when there is a webapp with a null id registered. See crbug.com/1055566
     * for details of the bug which caused this to occur.
     */
    @Test
    @Feature({"Webapp"})
    public void testWebappNullId() throws Exception {
        addWebappsToRegistry(new String[] {null});
        registerWebappWithId(null, createShortcutIntentDataProvider("https://www.google.ca"));
        assertEquals(1, WebappRegistry.getRegisteredWebappIdsForTesting().size());

        WebappRegistry.refreshSharedPrefsForTesting();

        // Does not crash.
        assertEquals(
                null,
                WebappRegistry.getInstance().getWebappDataStorageForUrl("https://www.google.ca/"));

        long currentTime = System.currentTimeMillis();
        WebappRegistry.getInstance()
                .unregisterOldWebapps(currentTime + WebappRegistry.FULL_CLEANUP_DURATION);
        assertTrue(WebappRegistry.getRegisteredWebappIdsForTesting().isEmpty());
    }

    @Test
    @Feature({"Webapp"})
    public void testUnregisterClearsRegistry() throws Exception {
        Map<String, String> apps = new HashMap<>();
        apps.put("webapp1", "http://example.com/index.html");
        apps.put("webapp2", "https://www.google.com/foo/bar");
        apps.put("webapp3", "https://www.chrome.com");

        for (Map.Entry<String, String> app : apps.entrySet()) {
            registerWebappWithId(app.getKey(), createShortcutIntentDataProvider(app.getValue()));
        }

        // Partial deletion.
        WebappRegistry.getInstance()
                .unregisterWebappsForUrlsImpl(
                        new UrlFilters.OneUrl("http://example.com/index.html"));

        Set<String> registeredWebapps = getRegisteredWebapps();
        assertEquals(2, registeredWebapps.size());
        for (String appName : apps.keySet()) {
            assertEquals(
                    !TextUtils.equals(appName, "webapp1"), registeredWebapps.contains(appName));
        }

        // Full deletion.
        WebappRegistry.getInstance().unregisterWebappsForUrlsImpl(new UrlFilters.AllUrls());
        assertTrue(getRegisteredWebapps().isEmpty());
    }

    @Test
    @Feature({"Webapp"})
    public void testUnregisterClearsWebappDataStorage() throws Exception {
        Map<String, String> apps = new HashMap<>();
        apps.put("webapp1", "http://example.com/index.html");
        apps.put("webapp2", "https://www.google.com/foo/bar");
        apps.put("webapp3", "https://www.chrome.com");

        for (Map.Entry<String, String> app : apps.entrySet()) {
            registerWebappWithId(app.getKey(), createShortcutIntentDataProvider(app.getValue()));
        }

        for (String appName : apps.keySet()) {
            SharedPreferences webAppPrefs =
                    ContextUtils.getApplicationContext()
                            .getSharedPreferences(
                                    WebappDataStorage.SHARED_PREFS_FILE_PREFIX + appName,
                                    Context.MODE_PRIVATE);
            webAppPrefs.edit().putLong(WebappDataStorage.KEY_LAST_USED, 100L).apply();
        }

        // Partial deletion.
        WebappRegistry.getInstance()
                .unregisterWebappsForUrlsImpl(
                        new UrlFilters.OneUrl("http://example.com/index.html"));

        for (String appName : apps.keySet()) {
            SharedPreferences webAppPrefs =
                    ContextUtils.getApplicationContext()
                            .getSharedPreferences(
                                    WebappDataStorage.SHARED_PREFS_FILE_PREFIX + appName,
                                    Context.MODE_PRIVATE);
            assertEquals(TextUtils.equals(appName, "webapp1"), webAppPrefs.getAll().isEmpty());
        }

        // Full deletion.
        WebappRegistry.getInstance().unregisterWebappsForUrlsImpl(new UrlFilters.AllUrls());
        for (String appName : apps.keySet()) {
            SharedPreferences webAppPrefs =
                    ContextUtils.getApplicationContext()
                            .getSharedPreferences(
                                    WebappDataStorage.SHARED_PREFS_FILE_PREFIX + appName,
                                    Context.MODE_PRIVATE);
            assertTrue(webAppPrefs.getAll().isEmpty());
        }
    }

    @Test
    @Feature({"Webapp"})
    public void testCleanupDoesNotRunTooOften() {
        // Put the current time to just before the task should run.
        long currentTime = INITIAL_TIME + WebappRegistry.FULL_CLEANUP_DURATION - 1;

        addWebappsToRegistry("oldWebapp");
        SharedPreferences webAppPrefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "oldWebapp",
                                Context.MODE_PRIVATE);
        webAppPrefs.edit().putLong(WebappDataStorage.KEY_LAST_USED, Long.MIN_VALUE).apply();

        // Force a re-read of the preferences from disk.
        WebappRegistry.refreshSharedPrefsForTesting();
        WebappRegistry.getInstance().unregisterOldWebapps(currentTime);

        Set<String> actual = getRegisteredWebapps();
        assertEquals(new HashSet<>(Arrays.asList("oldWebapp")), actual);

        long actualLastUsed =
                webAppPrefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(Long.MIN_VALUE, actualLastUsed);

        // The last cleanup time was set to 0 in setUp() so check that this hasn't changed.
        long lastCleanup = mSharedPreferences.getLong(WebappRegistry.KEY_LAST_CLEANUP, -1);
        assertEquals(INITIAL_TIME, lastCleanup);
    }

    @Test
    @Feature({"Webapp"})
    public void testCleanupDoesNotRemoveRecentApps() {
        // Put the current time such that the task runs.
        long currentTime = INITIAL_TIME + WebappRegistry.FULL_CLEANUP_DURATION;

        // Put the last used time just inside the no-cleanup window.
        addWebappsToRegistry("recentWebapp");
        SharedPreferences webAppPrefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "recentWebapp",
                                Context.MODE_PRIVATE);
        long lastUsed = currentTime - WebappRegistry.WEBAPP_UNOPENED_CLEANUP_DURATION + 1;
        webAppPrefs.edit().putLong(WebappDataStorage.KEY_LAST_USED, lastUsed).apply();

        // Force a re-read of the preferences from disk.
        WebappRegistry.refreshSharedPrefsForTesting();

        // Because the time is just inside the window, there should be a cleanup but the web app
        // should not be deleted as it was used recently. The last cleanup time should also be
        // set to the current time.
        WebappRegistry.getInstance().unregisterOldWebapps(currentTime);

        Set<String> actual = getRegisteredWebapps();
        assertEquals(new HashSet<>(Arrays.asList("recentWebapp")), actual);

        long actualLastUsed =
                webAppPrefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(lastUsed, actualLastUsed);

        long lastCleanup = mSharedPreferences.getLong(WebappRegistry.KEY_LAST_CLEANUP, -1);
        assertEquals(currentTime, lastCleanup);
    }

    @Test
    @Feature({"Webapp"})
    public void testCleanupRemovesOldApps() {
        // Put the current time such that the task runs.
        long currentTime = INITIAL_TIME + WebappRegistry.FULL_CLEANUP_DURATION;

        // Put the last used time just outside the no-cleanup window.
        addWebappsToRegistry("oldWebapp");
        SharedPreferences webAppPrefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "oldWebapp",
                                Context.MODE_PRIVATE);
        long lastUsed = currentTime - WebappRegistry.WEBAPP_UNOPENED_CLEANUP_DURATION;
        webAppPrefs.edit().putLong(WebappDataStorage.KEY_LAST_USED, lastUsed).apply();

        // Force a re-read of the preferences from disk.
        WebappRegistry.refreshSharedPrefsForTesting();

        // Because the time is just inside the window, there should be a cleanup of old web apps and
        // the last cleaned up time should be set to the current time.
        WebappRegistry.getInstance().unregisterOldWebapps(currentTime);

        Set<String> actual = getRegisteredWebapps();
        assertTrue(actual.isEmpty());

        long actualLastUsed =
                webAppPrefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(WebappDataStorage.TIMESTAMP_INVALID, actualLastUsed);

        long lastCleanup = mSharedPreferences.getLong(WebappRegistry.KEY_LAST_CLEANUP, -1);
        assertEquals(currentTime, lastCleanup);
    }

    @Test
    @Feature({"WebApk"})
    public void testCleanupRemovesUninstalledWebApks() throws Exception {
        String webApkPackage1 = "uninstalledWebApk1";
        String webApkPackage2 = "uninstalledWebApk2";

        BrowserServicesIntentDataProvider intentDataProvider1 =
                new WebApkIntentDataProviderBuilder(webApkPackage1, START_URL).build();
        registerWebapp(intentDataProvider1);

        BrowserServicesIntentDataProvider intentDataProvider2 =
                new WebApkIntentDataProviderBuilder(webApkPackage2, START_URL).build();
        registerWebapp(intentDataProvider2);

        // Verify that both WebAPKs are registered.
        assertEquals(2, getRegisteredWebapps().size());
        assertTrue(isRegisteredWebapp(intentDataProvider1));
        assertTrue(isRegisteredWebapp(intentDataProvider2));

        // Set the current time such that the task runs.
        long currentTime = System.currentTimeMillis() + WebappRegistry.FULL_CLEANUP_DURATION;
        // Because the time is just inside the window, there should be a cleanup of
        // uninstalled WebAPKs and the last cleaned up time should be set to the
        // current time.
        WebappRegistry.getInstance().unregisterOldWebapps(currentTime);

        assertTrue(getRegisteredWebapps().isEmpty());

        long lastCleanup = mSharedPreferences.getLong(WebappRegistry.KEY_LAST_CLEANUP, -1);
        assertEquals(currentTime, lastCleanup);
    }

    @Test
    @Feature({"WebApk"})
    public void testCleanupDoesNotRemoveInstalledWebApks() throws Exception {
        String webApkPackage = "installedWebApk";
        String uninstalledWebApkPackage = "uninstalledWebApk";

        BrowserServicesIntentDataProvider webApkIntentDataProvider =
                new WebApkIntentDataProviderBuilder(webApkPackage, START_URL).build();
        registerWebapp(webApkIntentDataProvider);

        BrowserServicesIntentDataProvider uninstalledWebApkIntentDataProvider =
                new WebApkIntentDataProviderBuilder(uninstalledWebApkPackage, START_URL).build();
        registerWebapp(uninstalledWebApkIntentDataProvider);

        // Verify that both WebAPKs are registered.
        assertEquals(2, getRegisteredWebapps().size());
        assertTrue(isRegisteredWebapp(webApkIntentDataProvider));
        assertTrue(isRegisteredWebapp(uninstalledWebApkIntentDataProvider));

        Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager())
                .addPackage(webApkPackage);

        // Set the current time such that the task runs.
        long currentTime = System.currentTimeMillis() + WebappRegistry.FULL_CLEANUP_DURATION;
        // Because the time is just inside the window, there should be a cleanup of
        // uninstalled WebAPKs and the last cleaned up time should be set to the
        // current time.
        WebappRegistry.getInstance().unregisterOldWebapps(currentTime);

        assertEquals(1, getRegisteredWebapps().size());
        assertTrue(isRegisteredWebapp(webApkIntentDataProvider));

        long lastCleanup = mSharedPreferences.getLong(WebappRegistry.KEY_LAST_CLEANUP, -1);
        assertEquals(currentTime, lastCleanup);
    }

    @Test
    @Feature({"WebApk"})
    public void testCleanupDoesRemoveOldInstalledWebApks() throws Exception {
        String deprecatedWebApkIdPrefix = "webapk:";
        String webApkPackage = "installedWebApk";
        BrowserServicesIntentDataProvider webApkIntentDataProvider =
                new WebApkIntentDataProviderBuilder(webApkPackage, START_URL).build();
        String deprecatedWebApkId =
                deprecatedWebApkIdPrefix
                        + webApkIntentDataProvider.getWebApkExtras().webApkPackageName;

        registerWebappWithId(deprecatedWebApkId, webApkIntentDataProvider);
        registerWebapp(webApkIntentDataProvider);

        // Verify that both WebAPKs are registered.
        Set<String> actual = getRegisteredWebapps();
        assertEquals(2, actual.size());
        assertTrue(actual.contains(deprecatedWebApkId));
        assertTrue(actual.contains(webApkIntentDataProvider.getWebappExtras().id));

        Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager())
                .addPackage(webApkPackage);

        // Set the current time such that the task runs.
        long currentTime = System.currentTimeMillis() + WebappRegistry.FULL_CLEANUP_DURATION;
        // Because the time is just inside the window, there should be a cleanup of
        // uninstalled WebAPKs and the last cleaned up time should be set to the
        // current time.
        WebappRegistry.getInstance().unregisterOldWebapps(currentTime);

        actual = getRegisteredWebapps();
        assertEquals(1, actual.size());
        assertTrue(actual.contains(webApkIntentDataProvider.getWebappExtras().id));

        long lastCleanup = mSharedPreferences.getLong(WebappRegistry.KEY_LAST_CLEANUP, -1);
        assertEquals(currentTime, lastCleanup);
    }

    @Test
    @Feature({"Webapp"})
    public void testClearWebappHistory() throws Exception {
        final String webapp1Url = "https://www.google.com";
        final String webapp2Url = "https://drive.google.com";
        BrowserServicesIntentDataProvider webappIntentDataProvider1 =
                createShortcutIntentDataProvider(webapp1Url);
        BrowserServicesIntentDataProvider webappIntentDataProvider2 =
                createShortcutIntentDataProvider(webapp2Url);

        registerWebappWithId("webapp1", webappIntentDataProvider1);
        registerWebappWithId("webapp2", webappIntentDataProvider2);

        SharedPreferences webapp1Prefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "webapp1",
                                Context.MODE_PRIVATE);
        SharedPreferences webapp2Prefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "webapp2",
                                Context.MODE_PRIVATE);

        long webapp1OriginalLastUsed =
                webapp2Prefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        long webapp2OriginalLastUsed =
                webapp2Prefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertTrue(webapp1OriginalLastUsed != WebappDataStorage.TIMESTAMP_INVALID);
        assertTrue(webapp2OriginalLastUsed != WebappDataStorage.TIMESTAMP_INVALID);

        // Clear data for |webapp1Url|.
        WebappRegistry.getInstance()
                .clearWebappHistoryForUrlsImpl(new UrlFilters.OneUrl(webapp1Url));

        Set<String> actual = getRegisteredWebapps();
        assertEquals(2, actual.size());
        assertTrue(actual.contains("webapp1"));
        assertTrue(actual.contains("webapp2"));

        // Verify that the last used time for the first web app is
        // WebappDataStorage.TIMESTAMP_INVALID, while for the second one it's unchanged.
        long actualLastUsed =
                webapp1Prefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(WebappDataStorage.TIMESTAMP_INVALID, actualLastUsed);
        actualLastUsed =
                webapp2Prefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(webapp2OriginalLastUsed, actualLastUsed);

        // Verify that the URL and scope for the first web app is WebappDataStorage.URL_INVALID,
        // while for the second one it's unchanged.
        String actualScope =
                webapp1Prefs.getString(WebappDataStorage.KEY_SCOPE, WebappDataStorage.URL_INVALID);
        assertEquals(WebappDataStorage.URL_INVALID, actualScope);
        String actualUrl =
                webapp1Prefs.getString(WebappDataStorage.KEY_URL, WebappDataStorage.URL_INVALID);
        assertEquals(WebappDataStorage.URL_INVALID, actualUrl);
        actualScope =
                webapp2Prefs.getString(WebappDataStorage.KEY_SCOPE, WebappDataStorage.URL_INVALID);
        assertEquals(webapp2Url + "/", actualScope);
        actualUrl =
                webapp2Prefs.getString(WebappDataStorage.KEY_URL, WebappDataStorage.URL_INVALID);
        assertEquals(webapp2Url, actualUrl);

        // Clear data for all urls.
        WebappRegistry.getInstance().clearWebappHistoryForUrlsImpl(new UrlFilters.AllUrls());

        // Verify that the last used time for both web apps is WebappDataStorage.TIMESTAMP_INVALID.
        actualLastUsed =
                webapp1Prefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(WebappDataStorage.TIMESTAMP_INVALID, actualLastUsed);
        actualLastUsed =
                webapp2Prefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertEquals(WebappDataStorage.TIMESTAMP_INVALID, actualLastUsed);

        // Verify that the URL and scope for both web apps is WebappDataStorage.URL_INVALID.
        actualScope =
                webapp1Prefs.getString(WebappDataStorage.KEY_SCOPE, WebappDataStorage.URL_INVALID);
        assertEquals(WebappDataStorage.URL_INVALID, actualScope);
        actualUrl =
                webapp1Prefs.getString(WebappDataStorage.KEY_URL, WebappDataStorage.URL_INVALID);
        assertEquals(WebappDataStorage.URL_INVALID, actualUrl);
        actualScope =
                webapp2Prefs.getString(WebappDataStorage.KEY_SCOPE, WebappDataStorage.URL_INVALID);
        assertEquals(WebappDataStorage.URL_INVALID, actualScope);
        actualUrl =
                webapp2Prefs.getString(WebappDataStorage.KEY_URL, WebappDataStorage.URL_INVALID);
        assertEquals(WebappDataStorage.URL_INVALID, actualUrl);
    }

    @Test
    @Feature({"Webapp"})
    public void testGetAfterClearWebappHistory() throws Exception {
        registerWebappWithId("webapp", null);

        SharedPreferences webappPrefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "webapp",
                                Context.MODE_PRIVATE);
        WebappRegistry.getInstance().clearWebappHistoryForUrlsImpl(new UrlFilters.AllUrls());

        // Open the webapp up and set the last used time.
        WebappRegistry.getInstance().getWebappDataStorage("webapp").updateLastUsedTime();

        // Verify that the last used time is valid.
        long actualLastUsed =
                webappPrefs.getLong(
                        WebappDataStorage.KEY_LAST_USED, WebappDataStorage.TIMESTAMP_INVALID);
        assertTrue(WebappDataStorage.TIMESTAMP_INVALID != actualLastUsed);
    }

    @Test
    @Feature({"Webapp"})
    public void testUpdateAfterClearWebappHistory() throws Exception {
        final String webappUrl = "http://www.google.com";
        final String webappScope = "http://www.google.com/";
        final BrowserServicesIntentDataProvider webappIntentDataProvider =
                createShortcutIntentDataProvider(webappUrl);
        registerWebappWithId("webapp", webappIntentDataProvider);

        SharedPreferences webappPrefs =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(
                                WebappDataStorage.SHARED_PREFS_FILE_PREFIX + "webapp",
                                Context.MODE_PRIVATE);

        // Verify that the URL and scope match the original in the intent.
        String actualUrl =
                webappPrefs.getString(WebappDataStorage.KEY_URL, WebappDataStorage.URL_INVALID);
        assertEquals(webappUrl, actualUrl);
        String actualScope =
                webappPrefs.getString(WebappDataStorage.KEY_SCOPE, WebappDataStorage.URL_INVALID);
        assertEquals(webappScope, actualScope);

        WebappRegistry.getInstance().clearWebappHistoryForUrlsImpl(new UrlFilters.AllUrls());

        // Update the webapp from the intent again.
        WebappDataStorage storage = WebappRegistry.getInstance().getWebappDataStorage("webapp");
        storage.updateFromWebappIntentDataProvider(webappIntentDataProvider);

        // Verify that the URL and scope match the original in the intent.
        actualUrl = webappPrefs.getString(WebappDataStorage.KEY_URL, WebappDataStorage.URL_INVALID);
        assertEquals(webappUrl, actualUrl);
        actualScope =
                webappPrefs.getString(WebappDataStorage.KEY_SCOPE, WebappDataStorage.URL_INVALID);
        assertEquals(webappScope, actualScope);
    }

    @Test
    @Feature({"Webapp"})
    public void testGetWebappDataStorageForUrl() throws Exception {
        // Ensure that getWebappDataStorageForUrl returns the correct WebappDataStorage object.
        // URLs should return the WebappDataStorage with the longest scope that the URL starts with.
        final String webapp1Url = "https://www.google.com/";
        final String webapp2Url = "https://drive.google.com/";
        final String webapp3Url = "https://www.google.com/drive/index.html";
        final String webapp4Url = "https://www.google.com/drive/docs/index.html";

        final String webapp3Scope = "https://www.google.com/drive/";
        final String webapp4Scope = "https://www.google.com/drive/docs/";

        final String test1Url = "https://www.google.com/index.html";
        final String test2Url = "https://www.google.com/drive/recent.html";
        final String test3Url = "https://www.google.com/drive/docs/recent.html";
        final String test4Url = "https://www.google.com/drive/docs/recent/index.html";
        final String test5Url = "https://drive.google.com/docs/recent/trash";
        final String test6Url = "https://maps.google.com/";

        BrowserServicesIntentDataProvider intentDataProvider1 =
                createShortcutIntentDataProvider(webapp1Url);
        BrowserServicesIntentDataProvider intentDataProvider2 =
                createShortcutIntentDataProvider(webapp2Url);
        BrowserServicesIntentDataProvider intentDataProvider3 =
                createShortcutIntentDataProvider(webapp3Url);
        BrowserServicesIntentDataProvider intentDataProvider4 =
                createShortcutIntentDataProvider(webapp4Url);

        // Register the four web apps.
        registerWebappWithId("webapp1", intentDataProvider1);
        registerWebappWithId("webapp2", intentDataProvider2);
        registerWebappWithId("webapp3", intentDataProvider3);
        registerWebappWithId("webapp4", intentDataProvider4);

        // test1Url should return webapp1.
        WebappDataStorage storage1 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(test1Url);
        assertEquals(webapp1Url, storage1.getUrl());
        assertEquals(webapp1Url, storage1.getScope());

        // test2Url should return webapp3.
        WebappDataStorage storage2 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(test2Url);
        assertEquals(webapp3Url, storage2.getUrl());
        assertEquals(webapp3Scope, storage2.getScope());

        // test3Url should return webapp4.
        WebappDataStorage storage3 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(test3Url);
        assertEquals(webapp4Url, storage3.getUrl());
        assertEquals(webapp4Scope, storage3.getScope());

        // test4Url should return webapp4.
        WebappDataStorage storage4 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(test4Url);
        assertEquals(webapp4Url, storage4.getUrl());
        assertEquals(webapp4Scope, storage4.getScope());

        // test5Url should return webapp2.
        WebappDataStorage storage5 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(test5Url);
        assertEquals(webapp2Url, storage5.getUrl());
        assertEquals(webapp2Url, storage5.getScope());

        // test6Url doesn't correspond to a web app, so the storage returned is null.
        WebappDataStorage storage6 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(test6Url);
        assertEquals(null, storage6);
    }

    @Test
    @Feature({"WebApk"})
    public void testGetWebappDataStorageForUrlWithWebApk() throws Exception {
        final String startUrl = START_URL;
        final String testUrl = START_URL + "/index.html";

        BrowserServicesIntentDataProvider webApkIntentDataProvider =
                new WebApkIntentDataProviderBuilder("org.chromium.webapk", startUrl).build();
        registerWebapp(webApkIntentDataProvider);

        // testUrl should return null.
        WebappDataStorage storage1 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(testUrl);
        assertNull(storage1);

        String webappId = "webapp";
        registerWebappWithId(webappId, createShortcutIntentDataProvider(startUrl));

        // testUrl should return the webapp.
        WebappDataStorage storage2 =
                WebappRegistry.getInstance().getWebappDataStorageForUrl(testUrl);
        assertEquals(webappId, storage2.getId());
    }

    @Test
    @Feature({"WebApk"})
    public void testHasWebApkForOrigin() throws Exception {
        final String startUrl = START_URL + "/test_page.html";
        final String testOrigin = START_URL;
        final String testPackageName = "org.chromium.webapk";

        assertFalse(WebappRegistry.getInstance().hasAtLeastOneWebApkForOrigin(testOrigin));

        String webappId = "webapp";
        registerWebappWithId(webappId, createShortcutIntentDataProvider(startUrl));
        assertFalse(WebappRegistry.getInstance().hasAtLeastOneWebApkForOrigin(testOrigin));

        BrowserServicesIntentDataProvider webApkIntentDataProvider =
                new WebApkIntentDataProviderBuilder(testPackageName, startUrl).build();
        registerWebapp(webApkIntentDataProvider);
        // Still fails because the WebAPK is "no longer installed" according to PackageManager.
        assertFalse(WebappRegistry.getInstance().hasAtLeastOneWebApkForOrigin(testOrigin));

        Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager())
                .addPackage(testPackageName);
        assertTrue(WebappRegistry.getInstance().hasAtLeastOneWebApkForOrigin(testOrigin));
    }

    @Test
    @Feature({"WebApk"})
    public void testFindWebApkWithManifestId() throws Exception {
        final String testManifestId = START_URL + "/id";
        final String testPackageName = "org.chromium.webapk";

        assertNull(WebappRegistry.getInstance().getWebappDataStorageForManifestId(testManifestId));

        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(testPackageName, START_URL)
                        .setWebApkManifestId(testManifestId)
                        .build();
        registerWebapp(intentDataProvider);

        WebappDataStorage storage =
                WebappRegistry.getInstance().getWebappDataStorageForManifestId(testManifestId);
        assertNotNull(storage);
        assertEquals(storage.getWebApkManifestId(), testManifestId);
        assertEquals(storage.getWebApkPackageName(), testPackageName);

        final String anotherManifestId = START_URL + "/test_page.html";
        assertNull(
                WebappRegistry.getInstance().getWebappDataStorageForManifestId(anotherManifestId));
    }

    @Test
    @Feature({"WebApk"})
    public void testFindWebApkPackageWithManifestId() throws Exception {
        final String testManifestId = START_URL + "/id";
        final String testPackageName = "org.chromium.webapk";

        assertNull(WebappRegistry.getInstance().findWebApkWithManifestId(testManifestId));

        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(testPackageName, START_URL)
                        .setWebApkManifestId(testManifestId)
                        .build();
        registerWebapp(intentDataProvider);

        assertEquals(
                WebappRegistry.getInstance().findWebApkWithManifestId(testManifestId),
                testPackageName);

        final String anotherManifestId = START_URL + "/test_page.html";
        assertNull(
                WebappRegistry.getInstance().getWebappDataStorageForManifestId(anotherManifestId));
    }

    @Test
    @Feature({"WebApk"})
    public void testGetWebApkSyncDatas() throws Exception {
        final String testStartUrl1 = START_URL;
        final String testManifestId1 = testStartUrl1 + "/id";
        final String testPackageName1 = "org.chromium.webapk";
        final String testName1 = "My App";
        final String testShortName1 = "app";
        final long testToolbarColor1 = Color.WHITE;
        final String testScope1 = testStartUrl1;

        final String testStartUrl2 = START_URL + "/2";
        final String testManifestId2 = testStartUrl2 + "/id";
        final String testPackageName2 = "org.chromium.webapk2";
        final String testName2 = null;
        final String testShortName2 = "app2";
        final long testToolbarColor2 = Color.BLACK;
        final String testScope2 = testStartUrl2;

        final String testStartUrl3 = START_URL + "/3";
        final String testManifestId3 = null;
        final String testPackageName3 = "org.chromium.webapk3";
        final String testName3 = "My App3";
        final String testShortName3 = "";
        final long testToolbarColor3 = ColorUtils.INVALID_COLOR;
        final String testScope3 = testStartUrl3;

        final String testStartUrl4 = START_URL + "/4";
        final String testManifestId4 = testStartUrl4 + "/id";
        final String testPackageName4 = "org.chromium.webapk4";
        final String testName4 = "My App4";
        final String testShortName4 = "app4";
        final long testToolbarColor4 = ColorUtils.INVALID_COLOR;
        final String testScope4 = null;

        WebappRegistry webApkRegistry = WebappRegistry.getInstance();
        Map<String, BrowserServicesIntentDataProvider> expectedIntentDataProviders =
                new HashMap<String, BrowserServicesIntentDataProvider>();

        BrowserServicesIntentDataProvider intentDataProvider1 =
                new WebApkIntentDataProviderBuilder(testPackageName1, testStartUrl1)
                        .setWebApkManifestId(testManifestId1)
                        .setName(testName1)
                        .setShortName(testShortName1)
                        .setToolbarColor(testToolbarColor1)
                        .setScope(testScope1)
                        .build();
        expectedIntentDataProviders.put(testScope1, intentDataProvider1);

        BrowserServicesIntentDataProvider intentDataProvider2 =
                new WebApkIntentDataProviderBuilder(testPackageName2, testStartUrl2)
                        .setWebApkManifestId(testManifestId2)
                        .setName(testName2)
                        .setShortName(testShortName2)
                        .setToolbarColor(testToolbarColor2)
                        .setScope(testScope2)
                        .build();
        expectedIntentDataProviders.put(testScope2, intentDataProvider2);

        // This one will not be returned because it has no manifest id.
        BrowserServicesIntentDataProvider intentDataProvider3 =
                new WebApkIntentDataProviderBuilder(testPackageName3, testStartUrl3)
                        .setWebApkManifestId(testManifestId3)
                        .setName(testName3)
                        .setShortName(testShortName3)
                        .setToolbarColor(testToolbarColor3)
                        .setScope(testScope3)
                        .build();
        expectedIntentDataProviders.put(testScope3, intentDataProvider3);

        // This one will not be returned because it has no scope.
        BrowserServicesIntentDataProvider intentDataProvider4 =
                new WebApkIntentDataProviderBuilder(testPackageName4, testStartUrl4)
                        .setWebApkManifestId(testManifestId4)
                        .setName(testName4)
                        .setShortName(testShortName4)
                        .setToolbarColor(testToolbarColor4)
                        .setScope(testScope4)
                        .build();

        GetWebApkSpecificsImplSetWebappInfoForTesting setWebappInfoForTesting =
                (scope) -> {
                    WebApkDataProvider.setWebappInfoForTesting(
                            WebappInfo.create(expectedIntentDataProviders.get(scope)));
                };

        assertEquals(0, webApkRegistry.getWebApkSpecificsImpl(setWebappInfoForTesting).size());

        registerWebapp(intentDataProvider1);
        registerWebapp(intentDataProvider2);
        registerWebapp(intentDataProvider3);
        registerWebapp(intentDataProvider4);

        List<WebApkSpecifics> webApkSpecificsList =
                webApkRegistry.getWebApkSpecificsImpl(setWebappInfoForTesting);
        assertEquals(2, webApkSpecificsList.size());

        Set<String> visitedScopes = new HashSet<String>();
        for (WebApkSpecifics webApkSpecifics : webApkSpecificsList) {
            BrowserServicesIntentDataProvider intentDataProvider =
                    expectedIntentDataProviders.get(webApkSpecifics.getScope());
            WebApkExtras webApkExtras = intentDataProvider.getWebApkExtras();
            WebappExtras webappExtras = intentDataProvider.getWebappExtras();
            ColorProvider colorProvider = intentDataProvider.getColorProvider();

            assertEquals(webApkExtras.manifestId, webApkSpecifics.getManifestId());
            assertEquals(webApkExtras.manifestStartUrl, webApkSpecifics.getStartUrl());

            if (webappExtras.name != null && !webappExtras.name.equals("")) {
                assertTrue(webApkSpecifics.hasName());
                assertEquals(webappExtras.name, webApkSpecifics.getName());
            } else if (webappExtras.shortName != null) {
                assertTrue(webApkSpecifics.hasName());
                assertEquals(webappExtras.shortName, webApkSpecifics.getName());
            } else {
                assertFalse(webApkSpecifics.hasName());
            }

            if (colorProvider.hasCustomToolbarColor()) {
                assertTrue(webApkSpecifics.hasThemeColor());
                assertEquals(colorProvider.getToolbarColor(), webApkSpecifics.getThemeColor());
            } else {
                assertFalse(webApkSpecifics.hasThemeColor());
            }

            assertEquals(webappExtras.scopeUrl, webApkSpecifics.getScope());

            visitedScopes.add(webApkSpecifics.getScope());
        }

        assertEquals(2, visitedScopes.size());
    }

    private Set<String> addWebappsToRegistry(String... webapps) {
        final Set<String> expected = new HashSet<>(Arrays.asList(webapps));
        mSharedPreferences.edit().putStringSet(WebappRegistry.KEY_WEBAPP_SET, expected).apply();
        return expected;
    }

    private boolean isRegisteredWebapp(BrowserServicesIntentDataProvider webappIntentDataProvider) {
        String id = webappIntentDataProvider.getWebappExtras().id;
        return getRegisteredWebapps().contains(id);
    }

    private Set<String> getRegisteredWebapps() {
        return mSharedPreferences.getStringSet(
                WebappRegistry.KEY_WEBAPP_SET, Collections.<String>emptySet());
    }

    private BrowserServicesIntentDataProvider createShortcutIntentDataProvider(final String url) {
        return WebappIntentDataProviderFactory.create(
                ShortcutHelper.createWebappShortcutIntentForTesting("id", url));
    }
}