chromium/chrome/android/junit/src/org/chromium/chrome/browser/firstrun/FirstRunIntegrationUnitTest.java

// Copyright 2018 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.firstrun;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserManager;

import androidx.browser.customtabs.CustomTabsIntent;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowApplication;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.init.BrowserParts;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.searchwidget.SearchActivity;
import org.chromium.chrome.browser.webapps.WebApkIntentDataProviderFactory;
import org.chromium.chrome.browser.webapps.WebappActivity;
import org.chromium.chrome.browser.webapps.WebappLauncherActivity;
import org.chromium.components.webapk.lib.client.WebApkValidator;
import org.chromium.components.webapk.lib.common.WebApkMetaDataKeys;
import org.chromium.webapk.lib.common.WebApkConstants;
import org.chromium.webapk.test.WebApkTestHelper;

import java.util.ArrayList;
import java.util.List;

/** JUnit tests for first run triggering code. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {FirstRunIntegrationUnitTest.MockChromeBrowserInitializer.class})
@DisabledTest(message = "https://crbug.com/358543444")
public final class FirstRunIntegrationUnitTest {
    /** Do nothing version of {@link ChromeBrowserInitializer}. */
    @Implements(ChromeBrowserInitializer.class)
    public static class MockChromeBrowserInitializer {
        @Implementation
        public void __constructor__() {}

        @Implementation
        public void handlePreNativeStartupAndLoadLibraries(final BrowserParts parts) {}
    }

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    private final List<ActivityController> mActivityControllerList = new ArrayList<>();

    private Context mContext;
    private ShadowApplication mShadowApplication;

    @Before
    public void setUp() {
        mContext = RuntimeEnvironment.application;
        mShadowApplication = ShadowApplication.getInstance();

        UserManager userManager = Mockito.mock(UserManager.class);
        Mockito.when(userManager.isDemoUser()).thenReturn(false);
        mShadowApplication.setSystemService(Context.USER_SERVICE, userManager);

        FirstRunStatus.setFirstRunFlowComplete(false);
        WebApkValidator.setDisableValidationForTesting(true);
    }

    @After
    public void tearDown() {
        for (ActivityController activityController : mActivityControllerList) {
            activityController.destroy();
        }
    }

    /** Checks that the intent component targets the passed-in class. */
    private boolean checkIntentComponentClass(Intent intent, Class componentClass) {
        if (intent == null || intent.getComponent() == null) return false;

        String intentClassName = intent.getComponent().getClassName();
        return componentClass.getName().equals(intentClassName);
    }

    /** Builds activity using the component class name from the provided intent. */
    @SuppressWarnings("unchecked")
    private void buildActivityWithClassNameFromIntent(Intent intent) {
        Class<? extends Activity> activityClass = null;
        try {
            activityClass =
                    (Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());
        } catch (ClassNotFoundException e) {
            Assert.fail();
        }
        createActivity(activityClass, intent);
    }

    /**
     * Launches {@link WebappLauncherActivity}. If WebappLauncherActivity is relaunched, waits for
     * the relaunch to occur.
     */
    private void launchWebappLauncherActivityProcessRelaunch(Intent intent) {
        createActivity(WebappLauncherActivity.class, intent);
        Intent launchedIntent = mShadowApplication.peekNextStartedActivity();
        if (checkIntentComponentClass(launchedIntent, WebappLauncherActivity.class)) {
            // Pop the WebappLauncherActivity from the 'started activities' list.
            mShadowApplication.getNextStartedActivity();
            buildActivityWithClassNameFromIntent(launchedIntent);
        }
    }

    /** Checks that {@link FirstRunActivity} was launched. */
    private void assertFirstRunActivityLaunched() {
        Intent launchedIntent = mShadowApplication.getNextStartedActivity();
        Assert.assertNotNull(launchedIntent);

        Assert.assertTrue(checkIntentComponentClass(launchedIntent, FirstRunActivity.class));
    }

    private <T extends Activity> Activity createActivity(Class<T> clazz, Intent intent) {
        ActivityController<T> activityController =
                Robolectric.buildActivity(clazz, intent).create();
        T activity = activityController.get();
        mActivityControllerList.add(activityController);
        return activity;
    }

    @Test
    public void testGenericViewIntentGoesToFirstRun() {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://test.com"));
        intent.setPackage(mContext.getPackageName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Activity launcherActivity = createActivity(ChromeLauncherActivity.class, intent);
        assertFirstRunActivityLaunched();
        Assert.assertTrue(launcherActivity.isFinishing());
    }

    @Test
    public void testRedirectCustomTabActivityToFirstRun() {
        CustomTabsIntent customTabIntent = new CustomTabsIntent.Builder().build();
        customTabIntent.intent.setPackage(mContext.getPackageName());
        customTabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        customTabIntent.launchUrl(mContext, Uri.parse("http://test.com"));
        Intent launchedIntent = mShadowApplication.getNextStartedActivity();
        Assert.assertNotNull(launchedIntent);

        Activity launcherActivity = createActivity(ChromeLauncherActivity.class, launchedIntent);
        assertFirstRunActivityLaunched();
        Assert.assertTrue(launcherActivity.isFinishing());
    }

    @Test
    public void testRedirectChromeTabbedActivityToFirstRun() {
        Intent intent = new Intent();
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Activity tabbedActivity = createActivity(ChromeTabbedActivity.class, intent);
        assertFirstRunActivityLaunched();
        Assert.assertTrue(tabbedActivity.isFinishing());
    }

    @Test
    public void testRedirectSearchActivityToFirstRun() {
        Intent intent = new Intent();
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Activity searchActivity = createActivity(SearchActivity.class, intent);
        assertFirstRunActivityLaunched();
        Assert.assertTrue(searchActivity.isFinishing());
    }

    /**
     * Tests that when the first run experience is shown by a WebAPK that the WebAPK is launched
     * when the user finishes the first run experience. In the case where the WebAPK (as opposed
     * to WebappActivity) displays the splash screen this is necessary for correct behaviour when
     * the user taps the app icon and the WebAPK is still running.
     */
    @Test
    public void testFreRelaunchesWebApkNotWebApkActivity() {
        String webApkPackageName = "org.chromium.webapk.name";
        String startUrl = "https://pwa.rocks/";

        Bundle bundle = new Bundle();
        bundle.putString(WebApkMetaDataKeys.START_URL, startUrl);
        WebApkTestHelper.registerWebApkWithMetaData(
                webApkPackageName, bundle, /* shareTargetMetaData= */ null);
        WebApkTestHelper.addIntentFilterForUrl(webApkPackageName, startUrl);

        Intent intent = WebApkTestHelper.createMinimalWebApkIntent(webApkPackageName, startUrl);
        intent.putExtra(WebApkConstants.EXTRA_SPLASH_PROVIDED_BY_WEBAPK, true);

        launchWebappLauncherActivityProcessRelaunch(intent);

        Intent launchedIntent = mShadowApplication.getNextStartedActivity();
        Assert.assertTrue(checkIntentComponentClass(launchedIntent, FirstRunActivity.class));
        PendingIntent freCompleteLaunchIntent =
                launchedIntent.getParcelableExtra(
                        FirstRunActivityBase.EXTRA_FRE_COMPLETE_LAUNCH_INTENT);
        Assert.assertNotNull(freCompleteLaunchIntent);
        Assert.assertEquals(
                webApkPackageName,
                Shadows.shadowOf(freCompleteLaunchIntent).getSavedIntent().getPackage());
    }

    /**
     * Test that if a WebAPK only requires the lightweight FRE and a user has gone through the
     * lightweight FRE that the WebAPK launches and no FRE is shown to the user.
     */
    @Test
    public void testUserAcceptedLightweightFreLaunch() {
        FirstRunStatus.setLightweightFirstRunFlowComplete(true);

        String webApkPackageName = "unbound.webapk";
        String startUrl = "https://pwa.rocks/";

        Bundle bundle = new Bundle();
        bundle.putString(WebApkMetaDataKeys.START_URL, startUrl);
        WebApkTestHelper.registerWebApkWithMetaData(
                webApkPackageName, bundle, /* shareTargetMetaData= */ null);
        WebApkTestHelper.addIntentFilterForUrl(webApkPackageName, startUrl);

        Intent intent = WebApkTestHelper.createMinimalWebApkIntent(webApkPackageName, startUrl);

        launchWebappLauncherActivityProcessRelaunch(intent);

        Intent launchedIntent = mShadowApplication.getNextStartedActivity();
        Assert.assertTrue(checkIntentComponentClass(launchedIntent, WebappActivity.class));
        buildActivityWithClassNameFromIntent(launchedIntent);

        // No FRE should have been launched.
        Assert.assertNull(mShadowApplication.getNextStartedActivity());
    }

    /** Test that the lightweight first run experience is used for unbound WebAPKs. */
    @Test
    public void testLightweightFre() {
        String webApkPackageName = "unbound.webapk";
        String startUrl = "https://pwa.rocks/";

        Bundle bundle = new Bundle();
        bundle.putString(WebApkMetaDataKeys.START_URL, startUrl);
        WebApkTestHelper.registerWebApkWithMetaData(
                webApkPackageName, bundle, /* shareTargetMetaData= */ null);
        WebApkTestHelper.addIntentFilterForUrl(webApkPackageName, startUrl);

        Intent intent = WebApkTestHelper.createMinimalWebApkIntent(webApkPackageName, startUrl);

        launchWebappLauncherActivityProcessRelaunch(intent);

        Intent launchedIntent = mShadowApplication.getNextStartedActivity();
        Assert.assertTrue(
                checkIntentComponentClass(launchedIntent, LightweightFirstRunActivity.class));
    }

    /**
     * Test that {@link WebappLauncherActivity} shows the regular full first run experience when it
     * is launched with an intent which both:
     * - Has a WebAPK package extra which meets the lightweight first run activity requirements
     * - Refers to an invalid WebAPK
     */
    @Test
    public void testFullFreIfWebApkInvalid() {
        String webApkPackageName = "unbound.webapk";
        String startUrl = "https://pwa.rocks/";

        Bundle bundle = new Bundle();
        bundle.putString(WebApkMetaDataKeys.START_URL, startUrl);
        WebApkTestHelper.registerWebApkWithMetaData(
                webApkPackageName, bundle, /* shareTargetMetaData= */ null);
        // Cause WebApkValidator#canWebApkHandleUrl() to fail (but not
        // WebApkIntentDataProviderFactory#create()) by not registering the intent handlers for the
        // WebAPK.

        Intent intent = WebApkTestHelper.createMinimalWebApkIntent(webApkPackageName, startUrl);
        Assert.assertNotNull(WebApkIntentDataProviderFactory.create(intent));

        launchWebappLauncherActivityProcessRelaunch(intent);

        Intent launchedIntent = mShadowApplication.getNextStartedActivity();
        Assert.assertTrue(checkIntentComponentClass(launchedIntent, FirstRunActivity.class));

        // WebappLauncherActivity (not the WebAPK) should be launched when the WebAPK completes.
        PendingIntent freCompleteLaunchIntent =
                launchedIntent.getParcelableExtra(
                        FirstRunActivityBase.EXTRA_FRE_COMPLETE_LAUNCH_INTENT);
        Assert.assertNotNull(freCompleteLaunchIntent);
        Assert.assertEquals(
                mContext.getPackageName(),
                Shadows.shadowOf(freCompleteLaunchIntent).getSavedIntent().getPackage());
    }
}