chromium/chrome/android/javatests/src/org/chromium/chrome/browser/webapps/WebApkUpdateIntegrationTest.java

// Copyright 2022 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.assertTrue;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.TextUtils;

import androidx.test.filters.LargeTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.PackageManagerWrapper;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.test.MockCertVerifierRuleAndroid;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.webapk.lib.client.WebApkValidator;
import org.chromium.components.webapk.lib.common.WebApkMetaDataKeys;
import org.chromium.components.webapk.proto.WebApkProto;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.webapk.lib.common.WebApkConstants;

import java.io.FileInputStream;

/** Integration tests for WebAPK feature. */
@RunWith(ChromeJUnit4ClassRunner.class)
@DoNotBatch(reason = "The update pipeline runs once per startup.")
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    ChromeSwitches.CHECK_FOR_WEB_MANIFEST_UPDATE_ON_STARTUP
})
public class WebApkUpdateIntegrationTest {
    public final WebApkActivityTestRule mActivityTestRule = new WebApkActivityTestRule();

    public MockCertVerifierRuleAndroid mCertVerifierRule =
            new MockCertVerifierRuleAndroid(0 /* net::OK */);

    @Rule
    public RuleChain mRuleChain =
            RuleChain.emptyRuleChain().around(mActivityTestRule).around(mCertVerifierRule);

    private static final String WEBAPK_PACKAGE_NAME = "org.chromium.webapk.test";

    // Android Manifest meta data for {@link WEBAPK_PACKAGE_NAME}.
    // TODO(eirage): change all to use mTestServer.
    private static final String WEBAPK_MANIFEST_URL = "/chrome/test/data/banners/manifest.json";
    private static final String WEBAPK_START_URL =
            "/chrome/test/data/banners/manifest_test_page.html";
    private static final String WEBAPK_SCOPE_URL = "/chrome/test/data/banners/";
    private static final String WEBAPK_NAME = "Manifest test app";
    private static final String WEBAPK_SHORT_NAME = "Manifest test app";
    private static final String ICON_URL = "/chrome/test/data/banners/image-512px.png";
    private static final String ICON_MURMUR2_HASH = "7742433188808797392";
    private static final String ICON_URL2 = "/chrome/test/data/banners/512x512-red.png";
    private static final String ICON_MURMUR2_HASH2 = "7742433188808797392";
    private static final String DISPLAY_MODE = "standalone";
    private static final String ORIENTATION = "portrait";
    private static final int SHELL_APK_VERSION = 1000;
    private static final String THEME_COLOR = "1L";
    private static final String BACKGROUND_COLOR = "2L";
    private static final String DARK_THEME_COLOR = "3L";
    private static final String DARK_BACKGROUND_COLOR = "4L";

    private EmbeddedTestServer mTestServer;
    private TestContext mTestContext;

    private Bundle mTestMetaData;

    private class TestContext extends ContextWrapper {
        public TestContext(Context baseContext) {
            super(baseContext);
        }

        @Override
        public PackageManager getPackageManager() {
            return new PackageManagerWrapper(super.getPackageManager()) {
                @Override
                public ApplicationInfo getApplicationInfo(String packageName, int flags) {
                    try {
                        ApplicationInfo ai = super.getApplicationInfo(packageName, flags);
                        if (TextUtils.equals(packageName, WEBAPK_PACKAGE_NAME)) {
                            ai.metaData = mTestMetaData;
                        }
                        return ai;
                    } catch (Exception e) {
                    }
                    return null;
                }
            };
        }
    }

    @Before
    public void setUp() throws Exception {
        mActivityTestRule.getEmbeddedTestServerRule().setServerUsesHttps(true);
        mTestContext = new TestContext(ContextUtils.getApplicationContext());
        ContextUtils.initApplicationContextForTests(mTestContext);
        mTestServer = mActivityTestRule.getTestServer();
        mTestMetaData = defaultMetaData();

        WebApkValidator.setDisableValidationForTesting(true);
        WebApkUpdateManager.setUpdatesDisabledForTesting(false);
    }

    private Bundle defaultMetaData() throws Exception {
        Bundle bundle = new Bundle();
        bundle.putString(WebApkMetaDataKeys.NAME, WEBAPK_NAME);
        bundle.putString(WebApkMetaDataKeys.SHORT_NAME, WEBAPK_SHORT_NAME);
        bundle.putString(WebApkMetaDataKeys.DISPLAY_MODE, DISPLAY_MODE);
        bundle.putString(WebApkMetaDataKeys.ORIENTATION, ORIENTATION);
        bundle.putString(WebApkMetaDataKeys.THEME_COLOR, THEME_COLOR);
        bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, BACKGROUND_COLOR);
        bundle.putString(WebApkMetaDataKeys.DARK_THEME_COLOR, DARK_THEME_COLOR);
        bundle.putString(WebApkMetaDataKeys.DARK_BACKGROUND_COLOR, DARK_BACKGROUND_COLOR);
        bundle.putInt(WebApkMetaDataKeys.SHELL_APK_VERSION, SHELL_APK_VERSION);
        bundle.putString(
                WebApkMetaDataKeys.WEB_MANIFEST_URL, mTestServer.getURL(WEBAPK_MANIFEST_URL));
        bundle.putString(WebApkMetaDataKeys.START_URL, mTestServer.getURL(WEBAPK_START_URL));
        bundle.putString(WebApkMetaDataKeys.SCOPE, mTestServer.getURL(WEBAPK_SCOPE_URL));
        Resources res =
                mTestContext.getPackageManager().getResourcesForApplication(WEBAPK_PACKAGE_NAME);
        bundle.putInt(
                WebApkMetaDataKeys.ICON_ID,
                res.getIdentifier("app_icon", "mipmap", WEBAPK_PACKAGE_NAME));
        bundle.putInt(
                WebApkMetaDataKeys.SPLASH_ID,
                res.getIdentifier("splash_icon", "drawable", WEBAPK_PACKAGE_NAME));

        bundle.putString(
                WebApkMetaDataKeys.ICON_URLS_AND_ICON_MURMUR2_HASHES,
                String.join(
                        " ",
                        mTestServer.getURL(ICON_URL),
                        ICON_MURMUR2_HASH,
                        mTestServer.getURL(ICON_URL2),
                        ICON_MURMUR2_HASH2));
        return bundle;
    }

    // Wait for the name change dialog and dismiss it.
    private void waitForDialog() {
        CriteriaHelper.pollUiThread(
                () -> {
                    ModalDialogManager manager =
                            mActivityTestRule.getActivity().getModalDialogManager();
                    PropertyModel dialog = manager.getCurrentDialogForTest();
                    if (dialog == null) return false;
                    dialog.get(ModalDialogProperties.CONTROLLER)
                            .onClick(dialog, ModalDialogProperties.ButtonType.POSITIVE);
                    return true;
                });
    }

    private void waitForHistogram() {}

    private WebApkProto.WebApk parseRequestProto(String path) throws Exception {
        FileInputStream requestFile = new FileInputStream(path);
        return WebApkProto.WebApk.parseFrom(requestFile);
    }

    /*
     * Test update flow triggered after WebAPK launch.
     */
    @Test
    @LargeTest
    @Feature({"Webapps"})
    public void testStoreUpdateRequestToFile() throws Exception {
        String pageUrl = mTestServer.getURL(WEBAPK_START_URL);
        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "WebApk.Update.ShellVersion", SHELL_APK_VERSION);

        WebappActivity activity = mActivityTestRule.startWebApkActivity(pageUrl);
        assertEquals(ActivityType.WEB_APK, activity.getActivityType());
        assertEquals(pageUrl, activity.getIntentDataProvider().getUrlToLoad());

        waitForDialog();
        histogramWatcher.pollInstrumentationThreadUntilSatisfied();

        WebappDataStorage storage =
                WebappRegistry.getInstance()
                        .getWebappDataStorage(
                                WebApkConstants.WEBAPK_ID_PREFIX + WEBAPK_PACKAGE_NAME);
        String updateRequestPath = storage.getPendingUpdateRequestPath();
        assertNotNull(updateRequestPath);

        WebApkProto.WebApk proto = parseRequestProto(updateRequestPath);

        assertEquals(proto.getPackageName(), WEBAPK_PACKAGE_NAME);
        assertEquals(proto.getVersion(), "1");
        assertEquals(proto.getManifestUrl(), mTestServer.getURL(WEBAPK_MANIFEST_URL));
        assertEquals(proto.getAppKey(), mTestServer.getURL(WEBAPK_MANIFEST_URL));
        assertEquals(proto.getManifest().getName(), WEBAPK_NAME);
        assertEquals(proto.getManifest().getShortName(), WEBAPK_SHORT_NAME);
        assertEquals(proto.getManifest().getStartUrl(), mTestServer.getURL(WEBAPK_START_URL));
        assertEquals(proto.getManifest().getScopes(0), mTestServer.getURL(WEBAPK_SCOPE_URL));
        assertEquals(proto.getManifest().getId(), mTestServer.getURL(WEBAPK_START_URL));
        assertEquals(proto.getManifest().getOrientation(), "landscape");
        assertEquals(proto.getManifest().getDisplayMode(), "standalone");

        assertEquals(proto.getManifest().getIconsCount(), 3);
        // 1st: primary icon from old shell icon, has image data but no hash.
        WebApkProto.Image icon1 = proto.getManifest().getIconsList().get(0);
        assertFalse(icon1.hasSrc());
        assertFalse(icon1.hasHash());
        assertTrue(icon1.hasImageData());
        assertFalse(icon1.getImageData().isEmpty());
        assertEquals(icon1.getPurposesCount(), 1);
        assertEquals(icon1.getPurposesList().get(0), WebApkProto.Image.Purpose.ANY);
        assertEquals(icon1.getUsagesCount(), 1);
        assertEquals(icon1.getUsagesList().get(0), WebApkProto.Image.Usage.PRIMARY_ICON);

        // 2nd: splash icon url matches the hash map. has image data and hash.
        WebApkProto.Image icon2 = proto.getManifest().getIconsList().get(1);
        assertEquals(icon2.getSrc(), mTestServer.getURL(ICON_URL));
        assertEquals(icon2.getHash(), ICON_MURMUR2_HASH);
        assertTrue(icon2.hasImageData());
        assertFalse(icon2.getImageData().isEmpty());
        assertEquals(icon2.getPurposesCount(), 1);
        assertEquals(icon2.getPurposesList().get(0), WebApkProto.Image.Purpose.ANY);
        assertEquals(icon2.getUsagesCount(), 1);
        assertEquals(icon2.getUsagesList().get(0), WebApkProto.Image.Usage.SPLASH_ICON);

        // 3nd icon from the url2hash map, has url and hash but no data.
        WebApkProto.Image icon3 = proto.getManifest().getIconsList().get(2);
        assertEquals(icon3.getSrc(), mTestServer.getURL(ICON_URL2));
        assertEquals(icon3.getHash(), ICON_MURMUR2_HASH2);
        assertFalse(icon3.hasImageData());
    }
}