chromium/chrome/android/junit/src/org/chromium/chrome/browser/tab/TabUtilsUnitTest.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.tab;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.DisplayMetrics;
import android.util.Size;
import android.view.WindowInsets;
import android.view.WindowMetrics;

import androidx.test.ext.junit.rules.ActivityScenarioRule;

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.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.TabUtils.UseDesktopUserAgentCaller;
import org.chromium.chrome.browser.tab_ui.TabThumbnailView;
import org.chromium.chrome.test.AutomotiveContextWrapperTestRule;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridgeJni;
import org.chromium.components.browser_ui.util.AutomotiveUtils;
import org.chromium.components.browser_ui.util.BrowserUiUtilsCachedFlags;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.display.DisplayUtil;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

/** Unit tests for {@link TabUtils}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {TabUtilsUnitTest.ShadowProfile.class})
public class TabUtilsUnitTest {
    /** A fake {@link Profile} used to reduce dependency. */
    @Implements(Profile.class)
    static class ShadowProfile {
        private static Profile sProfile;

        static void setProfile(Profile profile) {
            sProfile = profile;
        }

        @Resetter
        static void reset() {
            sProfile = null;
        }

        @Implementation
        public static Profile fromWebContents(WebContents webContents) {
            return sProfile;
        }
    }

    @Implements(WindowMetrics.class)
    public static class ShadowWindowMetrics {
        @Implementation
        public static WindowInsets getWindowInsets() {
            return sTestSysBarInsets;
        }
    }

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Rule
    public AutomotiveContextWrapperTestRule mAutomotiveContextWrapperTestRule =
            new AutomotiveContextWrapperTestRule();

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    private static final int TEST_SCREEN_WIDTH = 1000;
    private static final int TEST_SCREEN_HEIGHT = 1000;
    private static final int TEST_NAVIGATION_BAR_HEIGHT = 30;

    private static final int TEST_STATUS_BAR_HEIGHT = 30;
    private static WindowInsets sTestSysBarInsets;

    @Mock WebsitePreferenceBridge.Natives mWebsitePreferenceBridgeJniMock;
    @Mock private Tab mTab;
    @Mock private Tab mTabNative;
    @Mock private WebContents mWebContents;
    @Mock private NavigationController mNavigationController;
    @Mock private Profile mProfile;
    @Mock private BrowserControlsStateProvider mBrowserControlsStateProvider;
    @Mock private Resources mResources;
    @Mock private Configuration mConfiguration;
    @Mock private DisplayMetrics mDisplayMetrics;

    private boolean mRdsDefault;
    private @ContentSettingValues int mRdsException;
    private boolean mIsGlobal;
    private boolean mUseDesktopUserAgent;
    private @TabUserAgent int mTabUserAgent;
    private @TabUserAgent int mTabNativeUserAgent;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        ShadowProfile.setProfile(mProfile);
        mJniMocker.mock(WebsitePreferenceBridgeJni.TEST_HOOKS, mWebsitePreferenceBridgeJniMock);

        when(mTab.isNativePage()).thenReturn(false);
        when(mTabNative.isNativePage()).thenReturn(true);
        when(mTab.getWebContents()).thenReturn(mWebContents);
        when(mTabNative.getWebContents()).thenReturn(mWebContents);
        when(mWebContents.getNavigationController()).thenReturn(mNavigationController);

        doAnswer(invocation -> mRdsDefault)
                .when(mWebsitePreferenceBridgeJniMock)
                .isContentSettingEnabled(any(), eq(ContentSettingsType.REQUEST_DESKTOP_SITE));
        doAnswer(invocation -> mRdsException)
                .when(mWebsitePreferenceBridgeJniMock)
                .getContentSetting(
                        any(), eq(ContentSettingsType.REQUEST_DESKTOP_SITE), any(), any());
        doAnswer(invocation -> mIsGlobal)
                .when(mWebsitePreferenceBridgeJniMock)
                .isContentSettingGlobal(
                        any(), eq(ContentSettingsType.REQUEST_DESKTOP_SITE), any(), any());
        doAnswer(invocation -> mUseDesktopUserAgent)
                .when(mNavigationController)
                .getUseDesktopUserAgent();
        doAnswer(invocation -> mTabUserAgent).when(mTab).getUserAgent();
        doAnswer(invocation -> mTabNativeUserAgent).when(mTabNative).getUserAgent();
        doAnswer(
                        invocation -> {
                            mTabUserAgent = invocation.getArgument(0);
                            return null;
                        })
                .when(mTab)
                .setUserAgent(anyInt());
        doAnswer(
                        invocation -> {
                            mTabNativeUserAgent = invocation.getArgument(0);
                            return null;
                        })
                .when(mTabNative)
                .setUserAgent(anyInt());
        if (VERSION.SDK_INT >= VERSION_CODES.R) {
            sTestSysBarInsets =
                    new WindowInsets.Builder()
                            .setInsets(
                                    WindowInsets.Type.systemBars(),
                                    Insets.of(
                                            0,
                                            TEST_STATUS_BAR_HEIGHT,
                                            0,
                                            TEST_NAVIGATION_BAR_HEIGHT))
                            .build();
        }
    }

    @After
    public void tearDown() {
        ShadowProfile.reset();
    }

    @Test
    public void testSwitchUserAgent() {
        // Test non-native tab.
        TabUtils.switchUserAgent(mTab, false, UseDesktopUserAgentCaller.OTHER);
        verify(mNavigationController)
                .setUseDesktopUserAgent(false, true, UseDesktopUserAgentCaller.OTHER);

        TabUtils.switchUserAgent(mTab, true, UseDesktopUserAgentCaller.OTHER);
        verify(mNavigationController)
                .setUseDesktopUserAgent(true, true, UseDesktopUserAgentCaller.OTHER);

        // Test native tab.
        TabUtils.switchUserAgent(mTabNative, false, UseDesktopUserAgentCaller.OTHER);
        verify(mNavigationController)
                .setUseDesktopUserAgent(false, false, UseDesktopUserAgentCaller.OTHER);

        TabUtils.switchUserAgent(mTabNative, true, UseDesktopUserAgentCaller.OTHER);
        verify(mNavigationController)
                .setUseDesktopUserAgent(true, false, UseDesktopUserAgentCaller.OTHER);
    }

    @Test
    public void testIsUsingDesktopUserAgent() {
        Assert.assertFalse(
                "The result should be false when there is no webContents.",
                TabUtils.isUsingDesktopUserAgent(null));
        mUseDesktopUserAgent = false;
        Assert.assertFalse(
                "Should get RDS from WebContents.", TabUtils.isUsingDesktopUserAgent(mWebContents));
        mUseDesktopUserAgent = true;
        Assert.assertTrue(
                "Should get RDS from WebContents.", TabUtils.isUsingDesktopUserAgent(mWebContents));
    }

    @Test
    public void testGetTabUserAgent_UpgradePath() {
        mTabUserAgent = TabUserAgent.UNSET;
        mUseDesktopUserAgent = false;
        Assert.assertEquals(
                "TabUserAgent is not set up correctly for upgrade path.",
                TabUserAgent.DEFAULT,
                TabUtils.getTabUserAgent(mTab));
        verify(mTab).setUserAgent(TabUserAgent.DEFAULT);

        mTabUserAgent = TabUserAgent.UNSET;
        mUseDesktopUserAgent = true;
        Assert.assertEquals(
                "TabUserAgent is not set up correctly for upgrade path.",
                TabUserAgent.DESKTOP,
                TabUtils.getTabUserAgent(mTab));
        verify(mTab).setUserAgent(TabUserAgent.DESKTOP);
    }

    @Test
    public void testGetTabUserAgent_Mobile() {
        mTabUserAgent = TabUserAgent.MOBILE;
        mUseDesktopUserAgent = false;
        Assert.assertEquals(
                "Read unexpected TabUserAgent value.",
                TabUserAgent.MOBILE,
                TabUtils.getTabUserAgent(mTab));

        mUseDesktopUserAgent = true;
        Assert.assertEquals(
                "Read unexpected TabUserAgent value.",
                TabUserAgent.MOBILE,
                TabUtils.getTabUserAgent(mTab));

        verify(mTab, never()).setUserAgent(anyInt());
    }

    @Test
    public void testGetTabUserAgent_Desktop() {
        mTabUserAgent = TabUserAgent.DESKTOP;
        mUseDesktopUserAgent = false;
        Assert.assertEquals(
                "Read unexpected TabUserAgent value.",
                TabUserAgent.DESKTOP,
                TabUtils.getTabUserAgent(mTab));

        mUseDesktopUserAgent = true;
        Assert.assertEquals(
                "Read unexpected TabUserAgent value.",
                TabUserAgent.DESKTOP,
                TabUtils.getTabUserAgent(mTab));

        verify(mTab, never()).setUserAgent(anyInt());
    }

    @Test
    public void testReadRequestDesktopSiteContentSettings() {
        GURL gurl = JUnitTestGURLs.EXAMPLE_URL;

        // Site level setting is Mobile.
        mRdsException = ContentSettingValues.BLOCK;
        Assert.assertFalse(
                "The result should be false when there is no url",
                TabUtils.readRequestDesktopSiteContentSettings(mProfile, null));
        Assert.assertFalse(
                "The result should match RDS site level setting.",
                TabUtils.readRequestDesktopSiteContentSettings(mProfile, gurl));

        // Site level setting is Desktop.
        mRdsException = ContentSettingValues.ALLOW;
        Assert.assertFalse(
                "The result should be false when there is no url",
                TabUtils.readRequestDesktopSiteContentSettings(mProfile, null));
        Assert.assertTrue(
                "The result should match RDS site level setting.",
                TabUtils.readRequestDesktopSiteContentSettings(mProfile, gurl));
    }

    @Test
    public void testIsRequestDesktopSiteContentSettingsGlobal() {
        GURL gurl = JUnitTestGURLs.EXAMPLE_URL;

        // Content setting is global setting.
        mIsGlobal = true;
        Assert.assertTrue(
                "The result should be true when there is no url",
                TabUtils.isRequestDesktopSiteContentSettingsGlobal(mProfile, null));
        Assert.assertTrue(
                "Content setting is global setting.",
                TabUtils.isRequestDesktopSiteContentSettingsGlobal(mProfile, gurl));

        // Content setting is NOT global setting.
        mIsGlobal = false;
        Assert.assertTrue(
                "The result should be true when there is no url",
                TabUtils.isRequestDesktopSiteContentSettingsGlobal(mProfile, null));
        Assert.assertFalse(
                "Content setting is domain setting.",
                TabUtils.isRequestDesktopSiteContentSettingsGlobal(mProfile, gurl));
    }

    @Test
    @Config(
            sdk = Build.VERSION_CODES.R,
            shadows = {ShadowWindowMetrics.class},
            qualifiers = "w" + TEST_SCREEN_WIDTH + "dp-h" + TEST_SCREEN_HEIGHT + "dp-land")
    public void testGetTabThumbnailAspectRatioWithHorizontalAutomotiveToolbar() {
        mAutomotiveContextWrapperTestRule.setIsAutomotive(true);
        mActivityScenarioRule
                .getScenario()
                .onActivity(
                        activity -> {
                            int horizontalAutomotiveToolbarHeightDp =
                                    AutomotiveUtils.getHorizontalAutomotiveToolbarHeightDp(
                                            activity);
                            callAndVerifyGetTabThumbnailAspectRatio(
                                    activity, horizontalAutomotiveToolbarHeightDp, 0);
                        });
    }

    @Test
    @Config(
            sdk = Build.VERSION_CODES.R,
            shadows = {ShadowWindowMetrics.class},
            qualifiers = "w" + TEST_SCREEN_WIDTH + "dp-h" + TEST_SCREEN_HEIGHT + "dp-land")
    public void testGetTabThumbnailAspectRatioWithVerticalAutomotiveToolbar() {
        mAutomotiveContextWrapperTestRule.setIsAutomotive(true);
        BrowserUiUtilsCachedFlags.getInstance().setVerticalAutomotiveBackButtonToolbarFlag(true);
        mActivityScenarioRule
                .getScenario()
                .onActivity(
                        activity -> {
                            Activity spyActivity = spy(activity);
                            int verticalAutomotiveToolbarWidthDp =
                                    AutomotiveUtils.getVerticalAutomotiveToolbarWidthDp(
                                            spyActivity);
                            callAndVerifyGetTabThumbnailAspectRatio(
                                    spyActivity, 0, verticalAutomotiveToolbarWidthDp);
                        });
    }

    @Test
    public void testUpdateThumbnailMatrix_notOnAutomotiveDevice_thumbnailImageHasOriginalDensity() {
        mAutomotiveContextWrapperTestRule.setIsAutomotive(false);
        int mockImageSize = 100;
        int mockTargetSize = 50;

        TabThumbnailView thumbnailView = Mockito.mock(TabThumbnailView.class);
        Bitmap bitmap = Bitmap.createBitmap(mockImageSize, mockImageSize, Bitmap.Config.ARGB_8888);
        bitmap.setDensity(DisplayMetrics.DENSITY_DEFAULT);
        TabUtils.setDrawableAndUpdateImageMatrix(
                thumbnailView,
                new BitmapDrawable(bitmap),
                new Size(mockTargetSize, mockTargetSize));

        assertNotEquals("The bitmap image density should not be zero.", 0, bitmap.getDensity());
        assertEquals(
                "The bitmap image's density should not be scaled up on non-automotive"
                        + " devices.",
                DisplayMetrics.DENSITY_DEFAULT,
                bitmap.getDensity());
    }

    @Test
    public void testUpdateThumbnailMatrix_onAutomotiveDevice_thumbnailImageHasScaledUpDensity() {
        mAutomotiveContextWrapperTestRule.setIsAutomotive(true);
        int mockImageSize = 100;
        int mockTargetSize = 50;

        TabThumbnailView thumbnailView = Mockito.mock(TabThumbnailView.class);
        doReturn(ContextUtils.getApplicationContext()).when(thumbnailView).getContext();

        Bitmap bitmap = Bitmap.createBitmap(mockImageSize, mockImageSize, Bitmap.Config.ARGB_8888);
        bitmap.setDensity(DisplayMetrics.DENSITY_DEFAULT);
        TabUtils.setDrawableAndUpdateImageMatrix(
                thumbnailView,
                new BitmapDrawable(bitmap),
                new Size(mockTargetSize, mockTargetSize));

        assertNotEquals("The bitmap image density should not be zero.", 0, bitmap.getDensity());
        assertEquals(
                "The bitmap image's density should be scaled up on automotive.",
                DisplayUtil.getUiDensityForAutomotive(
                        ContextUtils.getApplicationContext(), DisplayMetrics.DENSITY_DEFAULT),
                bitmap.getDensity());
    }

    private void callAndVerifyGetTabThumbnailAspectRatio(
            Activity spyActivity,
            int horizontalAutomotiveToolbarHeightDp,
            int verticalAutomotiveToolbarWidthDp) {
        doReturn(0).when(mBrowserControlsStateProvider).getTopControlsHeight();
        float expectedAspectRatio =
                (TEST_SCREEN_WIDTH * 1.f - verticalAutomotiveToolbarWidthDp)
                        / (TEST_SCREEN_HEIGHT * 1.f
                                - horizontalAutomotiveToolbarHeightDp
                                - TEST_STATUS_BAR_HEIGHT
                                - TEST_NAVIGATION_BAR_HEIGHT);
        assertEquals(
                "Thumbnail aspect ratio is not as expected.",
                expectedAspectRatio,
                TabUtils.getTabThumbnailAspectRatio(spyActivity, mBrowserControlsStateProvider),
                0.01);
    }
}