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

// Copyright 2024 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.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;

import android.content.res.Resources;
import android.graphics.Color;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.android.XmlResourceParserImpl;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
import org.w3c.dom.Document;

import org.chromium.base.FakeTimeTestRule;
import org.chromium.base.task.test.BackgroundShadowAsyncTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappIcon;
import org.chromium.chrome.browser.browserservices.intents.WebappInfo;
import org.chromium.chrome.browser.browserservices.intents.WebappIntentUtils;
import org.chromium.chrome.test.util.browser.webapps.WebApkIntentDataProviderBuilder;
import org.chromium.components.sync.protocol.WebApkSpecifics;

import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/** Tests the WebApkSyncService class */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {BackgroundShadowAsyncTask.class})
@LooperMode(LooperMode.Mode.LEGACY)
public class WebApkSyncServiceTest {
    private static final String START_URL = "https://example.com/start";
    private static final String MANIFEST_ID = "https://example.com/id";
    private static final String PACKAGE_NAME = "org.chromium.webapk";
    private static final String SCOPE = "https://example.com/";
    private static final String NAME = "My App";
    private static final String SHORT_NAME = "app";
    private static final long TOOLBAR_COLOR = Color.WHITE;
    private static final int PRIMARY_ICON_ID = 12;
    private static final String ICON_URL = "https://example.com/icon.png";
    private static final String ICON_MURMUR2_HASH = "5";
    private static final String ICON_URL2 = "https://example.com/icon2.png";

    @Mock private Resources mMockResources;

    @Rule public FakeTimeTestRule mFakeClockRule = new FakeTimeTestRule();

    private String mPrimaryIconXmlContents;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    private XmlResourceParserImpl getMockXmlResourceParserImpl() {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setIgnoringComments(true);
            factory.setIgnoringElementContentWhitespace(true);
            DocumentBuilder documentBuilder = factory.newDocumentBuilder();
            Document document =
                    documentBuilder.parse(
                            new ByteArrayInputStream(mPrimaryIconXmlContents.getBytes()));

            return new XmlResourceParserImpl(document, "file", PACKAGE_NAME, PACKAGE_NAME, null);
        } catch (Exception e) {
            return null;
        }
    }

    public WebappDataStorage registerWebappAndGetStorage(String packageName) throws Exception {
        String webappId = WebappIntentUtils.getIdForWebApkPackage(packageName);
        try {
            CallbackHelper helper = new CallbackHelper();
            WebappRegistry.getInstance()
                    .register(
                            webappId,
                            new WebappRegistry.FetchWebappDataStorageCallback() {
                                @Override
                                public void onWebappDataStorageRetrieved(
                                        WebappDataStorage storage) {
                                    helper.notifyCalled();
                                }
                            });
            BackgroundShadowAsyncTask.runBackgroundTasks();
            ShadowLooper.runUiThreadTasks();
            helper.waitForOnly();
        } catch (TimeoutException e) {
            fail();
        }

        return WebappRegistry.getInstance().getWebappDataStorage(webappId);
    }

    @Test
    public void testGetWebApkSyncSpecifics() throws Exception {
        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(PACKAGE_NAME, START_URL)
                        .setWebApkManifestId(MANIFEST_ID)
                        .setName(NAME)
                        .setShortName(SHORT_NAME)
                        .setToolbarColor(TOOLBAR_COLOR)
                        .setScope(SCOPE)
                        .build();
        WebappInfo webApkInfo = WebappInfo.create(intentDataProvider);
        WebappDataStorage storage = registerWebappAndGetStorage(PACKAGE_NAME);
        storage.updateLastUsedTime();

        WebApkSpecifics webApkSpecifics = WebApkSyncService.getWebApkSpecifics(webApkInfo, storage);

        assertEquals(MANIFEST_ID, webApkSpecifics.getManifestId());
        assertEquals(START_URL, webApkSpecifics.getStartUrl());
        assertEquals(SCOPE, webApkSpecifics.getScope());
        assertTrue(webApkSpecifics.hasName());
        assertEquals(NAME, webApkSpecifics.getName());
        assertTrue(webApkSpecifics.hasThemeColor());
        assertEquals(TOOLBAR_COLOR, webApkSpecifics.getThemeColor());
        assertEquals(0, webApkSpecifics.getIconInfosCount());
        // From 1653000000000L May 19 2022 18:40:00 GMT-0400.
        assertEquals(13297473600000000L, webApkSpecifics.getLastUsedTimeWindowsEpochMicros());
    }

    @Test
    public void testGetSyncSpecificsWithEmptyValues() throws Exception {
        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(PACKAGE_NAME, START_URL)
                        .setWebApkManifestId(MANIFEST_ID)
                        .setShortName(SHORT_NAME)
                        .build();
        WebappInfo webApkInfo = WebappInfo.create(intentDataProvider);
        WebappDataStorage storage = registerWebappAndGetStorage(PACKAGE_NAME);

        WebApkSpecifics webApkSpecifics = WebApkSyncService.getWebApkSpecifics(webApkInfo, storage);

        assertEquals(MANIFEST_ID, webApkSpecifics.getManifestId());
        assertEquals(START_URL, webApkSpecifics.getStartUrl());
        assertEquals(SCOPE, webApkSpecifics.getScope());

        assertTrue(webApkSpecifics.hasName());
        assertEquals(SHORT_NAME, webApkSpecifics.getName());

        assertEquals(0, webApkSpecifics.getThemeColor());
        assertEquals(0, webApkSpecifics.getIconInfosCount());
    }

    @Test
    public void testGetIconInfo() throws Exception {
        mPrimaryIconXmlContents =
                String.format(
                        "<bitmap xmlns:android='http://schemas.android.com/apk/res/android'"
                                + "  android:src='@mipmap/app_icon_xxhdpi'"
                                + "  iconUrl='%s'"
                                + "  iconHash='%s'"
                                + "/>",
                        ICON_URL, ICON_MURMUR2_HASH);
        when(mMockResources.getXml(anyInt())).thenReturn(getMockXmlResourceParserImpl());

        WebappIcon testIcon =
                new WebappIcon(
                        PACKAGE_NAME,
                        PRIMARY_ICON_ID,
                        mMockResources,
                        WebappIcon.ICON_WITH_URL_AND_HASH_SHELL_VERSION);
        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(PACKAGE_NAME, START_URL)
                        .setWebApkManifestId(MANIFEST_ID)
                        .setPrimaryIcon(testIcon)
                        .setShellApkVersion(WebappIcon.ICON_WITH_URL_AND_HASH_SHELL_VERSION)
                        .build();
        WebappInfo webApkInfo = WebappInfo.create(intentDataProvider);
        WebappDataStorage storage = registerWebappAndGetStorage(PACKAGE_NAME);

        WebApkSpecifics webApkSpecifics = WebApkSyncService.getWebApkSpecifics(webApkInfo, storage);

        assertEquals(1, webApkSpecifics.getIconInfosCount());
        assertEquals(ICON_URL, webApkSpecifics.getIconInfos(0).getUrl());
    }

    @Test
    public void testGetIconsFallback() throws Exception {
        WebappIcon testIcon = new WebappIcon();
        Map<String, String> iconUrlAndIconMurmur2HashMap = new HashMap<String, String>();
        iconUrlAndIconMurmur2HashMap.put(ICON_URL, ICON_MURMUR2_HASH);
        iconUrlAndIconMurmur2HashMap.put(ICON_URL2, ICON_MURMUR2_HASH);
        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(PACKAGE_NAME, START_URL)
                        .setWebApkManifestId(MANIFEST_ID)
                        .setPrimaryIcon(testIcon)
                        .setIconUrlToMurmur2HashMap(iconUrlAndIconMurmur2HashMap)
                        .build();
        WebappInfo webApkInfo = WebappInfo.create(intentDataProvider);
        WebappDataStorage storage = registerWebappAndGetStorage(PACKAGE_NAME);

        WebApkSpecifics webApkSpecifics = WebApkSyncService.getWebApkSpecifics(webApkInfo, storage);

        assertEquals(2, webApkSpecifics.getIconInfosCount());
        assertEquals(ICON_URL, webApkSpecifics.getIconInfos(0).getUrl());
        assertEquals(ICON_URL2, webApkSpecifics.getIconInfos(1).getUrl());
    }
}