chromium/chrome/android/javatests/src/org/chromium/chrome/browser/ui/system/StatusBarColorControllerTest.java

// Copyright 2020 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.ui.system;

import static androidx.test.espresso.matcher.ViewMatchers.withId;

import static org.junit.Assert.assertEquals;

import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

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

import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.layouts.LayoutTestUtils;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.tab_management.TabUiThemeUtil;
import org.chromium.chrome.browser.toolbar.top.ToolbarLayout;
import org.chromium.chrome.browser.toolbar.top.ToolbarPhone;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.NewTabPageTestUtils;
import org.chromium.chrome.test.util.OmniboxTestUtils;
import org.chromium.chrome.test.util.browser.ThemeTestUtils;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.scrim.ScrimProperties;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.content_public.browser.test.util.TestTouchUtils;
import org.chromium.ui.test.util.DeviceRestriction;
import org.chromium.ui.test.util.UiRestriction;
import org.chromium.ui.util.ColorUtils;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * {@link StatusBarColorController} tests. There are additional status bar color tests in {@link
 * BrandColorTest}.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class StatusBarColorControllerTest {
    @ClassRule
    public static ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, true);

    private @ColorInt int mScrimColor;
    private OmniboxTestUtils mOmniboxUtils;

    @Before
    public void setUp() {
        mScrimColor = sActivityTestRule.getActivity().getColor(R.color.default_scrim_color);
        mOmniboxUtils = new OmniboxTestUtils(sActivityTestRule.getActivity());
    }

    /** Test that the status bar color is toggled when toggling incognito while in overview mode. */
    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    @DisabledTest(message = "crbug.com/353460498")
    public void testColorToggleIncognitoInTabSwitcher() throws Exception {
        ChromeTabbedActivity activity = sActivityTestRule.getActivity();
        final int expectedOverviewStandardColor =
                ChromeColors.getPrimaryBackgroundColor(activity, false);
        final int expectedOverviewIncognitoColor =
                ChromeColors.getPrimaryBackgroundColor(activity, true);

        sActivityTestRule.loadUrlInNewTab(
                "about:blank", /* incognito= */ true, TabLaunchType.FROM_CHROME_UI);
        TabModelSelector tabModelSelector = activity.getTabModelSelector();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tabModelSelector.selectModel(/* incognito= */ true);
                });
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    activity.getLayoutManager()
                            .showLayout(LayoutType.TAB_SWITCHER, /* animate= */ false);
                });

        waitForStatusBarColor(activity, expectedOverviewIncognitoColor);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tabModelSelector.selectModel(/* incognito= */ false);
                });
        ThemeTestUtils.assertStatusBarColor(activity, expectedOverviewStandardColor);
    }

    /**
     * Test that the default color (and not the active tab's brand color) is used in overview mode.
     */
    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    public void testBrandColorIgnoredInTabSwitcher() throws Exception {
        ChromeTabbedActivity activity = sActivityTestRule.getActivity();
        final int expectedDefaultStandardColor = ChromeColors.getDefaultThemeColor(activity, false);

        String pageWithBrandColorUrl =
                sActivityTestRule
                        .getTestServer()
                        .getURL("/chrome/test/data/android/theme_color_test.html");
        sActivityTestRule.loadUrl(pageWithBrandColorUrl);
        ThemeTestUtils.waitForThemeColor(activity, Color.RED);
        waitForStatusBarColor(activity, Color.RED);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    activity.getLayoutManager()
                            .showLayout(LayoutType.TAB_SWITCHER, /* animate= */ false);
                });
        waitForStatusBarColor(activity, expectedDefaultStandardColor);
    }

    /** Test the color of status bar when used in NTP. */
    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    @DisabledTest(message = "https://issues.chromium.org/issues/341157444")
    public void testStatusBarColorNtp() throws Exception {
        ChromeTabbedActivity activity = sActivityTestRule.getActivity();
        final int expectedColor =
                ChromeColors.getSurfaceColor(
                        activity, R.dimen.home_surface_background_color_elevation);

        sActivityTestRule.loadUrlInNewTab(UrlConstants.NTP_URL, false);
        NewTabPageTestUtils.waitForNtpLoaded(activity.getActivityTab());

        // Scroll the toolbar up and let it pinned on top.
        scrollUpToolbarUntilPinnedAtTop(activity);
        waitForStatusBarColor(activity, expectedColor);
    }

    /** Test that the status indicator color is included in the color calculation correctly. */
    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    public void testColorWithStatusIndicator() {
        final ChromeActivity activity = sActivityTestRule.getActivity();
        final StatusBarColorController statusBarColorController =
                sActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getStatusBarColorController();
        final Supplier<Integer> statusBarColor = () -> activity.getWindow().getStatusBarColor();
        final int initialColor = statusBarColor.get();

        // Initially, StatusBarColorController#getStatusBarColorWithoutStatusIndicator should return
        // the same color as the current status bar color.
        Assert.assertEquals(
                "Wrong initial value returned by #getStatusBarColorWithoutStatusIndicator().",
                initialColor,
                statusBarColorController.getStatusBarColorWithoutStatusIndicator());

        // Set a status indicator color.
        ThreadUtils.runOnUiThreadBlocking(
                () -> statusBarColorController.onStatusIndicatorColorChanged(Color.BLUE));

        Assert.assertEquals("Wrong status bar color.", Color.BLUE, statusBarColor.get().intValue());

        // StatusBarColorController#getStatusBarColorWithoutStatusIndicator should still return the
        // initial color.
        Assert.assertEquals(
                "Wrong value returned by #getStatusBarColorWithoutStatusIndicator().",
                initialColor,
                statusBarColorController.getStatusBarColorWithoutStatusIndicator());

        // Set scrim.
        ThreadUtils.runOnUiThreadBlocking(
                () -> statusBarColorController.setStatusBarScrimFraction(.5f));

        // The resulting color should be a scrimmed version of the status bar color.
        Assert.assertEquals(
                "Wrong status bar color w/ scrim.",
                getScrimmedColor(Color.BLUE, .5f),
                statusBarColor.get().intValue());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Remove scrim.
                    statusBarColorController.setStatusBarScrimFraction(.0f);
                    // Set the status indicator color to the default, i.e. transparent.
                    statusBarColorController.onStatusIndicatorColorChanged(Color.TRANSPARENT);
                });

        // Now, the status bar color should be back to the initial color.
        Assert.assertEquals(
                "Wrong status bar color after the status indicator color is set to default.",
                initialColor,
                statusBarColor.get().intValue());
    }

    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @DisabledTest(message = "b/352622267")
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    public void testFocusAndScrollColors() throws Exception {
        ChromeTabbedActivity activity = sActivityTestRule.getActivity();
        final StatusBarColorController statusBarColorController =
                sActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getStatusBarColorController();
        loadUrlInNewTabAndWaitForShowing("about:blank", /* incognito= */ false);

        mOmniboxUtils.requestFocus();
        final @ColorInt int focusedColor =
                ChromeColors.getSurfaceColor(
                        activity, R.dimen.omnibox_suggestion_dropdown_bg_elevation);

        statusBarColorController.onSuggestionDropdownScroll();
        final @ColorInt int scrolledColor =
                ChromeColors.getSurfaceColor(activity, R.dimen.toolbar_text_box_elevation);
        waitForStatusBarColor(activity, scrolledColor);

        statusBarColorController.onSuggestionDropdownOverscrolledToTop();
        waitForStatusBarColor(activity, focusedColor);

        TabModelSelector tabModelSelector = activity.getTabModelSelectorSupplier().get();
        ThreadUtils.runOnUiThread(() -> tabModelSelector.selectModel(/* incognito= */ true));
        loadUrlInNewTabAndWaitForShowing("about:blank", /* incognito= */ true);

        mOmniboxUtils.requestFocus();
        final @ColorInt int focusedIncognitoColor =
                ContextCompat.getColor(activity, R.color.omnibox_dropdown_bg_incognito);
        waitForStatusBarColor(activity, focusedIncognitoColor);

        statusBarColorController.onSuggestionDropdownScroll();
        final @ColorInt int scrolledIncognitoColor =
                ContextCompat.getColor(activity, R.color.default_bg_color_dark_elev_2_baseline);
        waitForStatusBarColor(activity, scrolledIncognitoColor);

        statusBarColorController.onSuggestionDropdownOverscrolledToTop();
        waitForStatusBarColor(activity, focusedIncognitoColor);
    }

    /** Test that the theme color is cleared when the Omnibox gains focus. */
    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    public void testBrandColorIgnoredWhenOmniboxIsFocused() throws Exception {
        ChromeTabbedActivity activity = sActivityTestRule.getActivity();
        final @ColorInt int expectedFocusedColor =
                ChromeColors.getSurfaceColor(
                        activity, R.dimen.omnibox_suggestion_dropdown_bg_elevation);

        String pageWithBrandColorUrl =
                sActivityTestRule
                        .getTestServer()
                        .getURL("/chrome/test/data/android/theme_color_test.html");
        loadUrlAndWaitForShowing(pageWithBrandColorUrl);
        ThemeTestUtils.waitForThemeColor(activity, Color.RED);
        waitForStatusBarColor(activity, Color.RED);

        mOmniboxUtils.requestFocus();
        waitForStatusBarColor(activity, expectedFocusedColor);
        mOmniboxUtils.clearFocus();
        waitForStatusBarColor(activity, Color.RED);
    }

    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    public void testBrandColorIgnoredWhenOmniboxIsFocused_FeatureMatchToolbarColorEnabled()
            throws Exception {
        ChromeTabbedActivity activity = sActivityTestRule.getActivity();
        final int expectedFocusedColor =
                ChromeColors.getSurfaceColor(
                        activity, R.dimen.omnibox_suggestion_dropdown_bg_elevation);

        String pageWithBrandColorUrl =
                sActivityTestRule
                        .getTestServer()
                        .getURL("/chrome/test/data/android/theme_color_test.html");
        sActivityTestRule.loadUrl(pageWithBrandColorUrl);
        ThemeTestUtils.waitForThemeColor(activity, Color.RED);
        mOmniboxUtils.waitAnimationsComplete();
        waitForStatusBarColor(activity, Color.RED);
        waitForStatusBarColorToMatchToolbarColor(activity);

        mOmniboxUtils.requestFocus();
        mOmniboxUtils.waitAnimationsComplete();
        waitForStatusBarColor(activity, expectedFocusedColor);
        waitForStatusBarColorToMatchToolbarColor(activity);
        mOmniboxUtils.clearFocus();
        mOmniboxUtils.waitAnimationsComplete();
        waitForStatusBarColor(activity, Color.RED);
        waitForStatusBarColorToMatchToolbarColor(activity);
    }

    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE}) // Status bar is always black on tablets
    public void testColorWithStatusIndicator_FeatureMatchToolbarColorEnabled() {
        final ChromeActivity activity = sActivityTestRule.getActivity();
        final StatusBarColorController statusBarColorController =
                sActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getStatusBarColorController();
        final Supplier<Integer> statusBarColor = () -> activity.getWindow().getStatusBarColor();
        final int initialColor = statusBarColor.get();

        // Initially, StatusBarColorController#getStatusBarColorWithoutStatusIndicator should return
        // the same color as the current status bar color.
        Assert.assertEquals(
                "Wrong initial value returned by #getStatusBarColorWithoutStatusIndicator().",
                initialColor,
                statusBarColorController.getStatusBarColorWithoutStatusIndicator());

        // Set a status indicator color.
        ThreadUtils.runOnUiThreadBlocking(
                () -> statusBarColorController.onStatusIndicatorColorChanged(Color.BLUE));

        Assert.assertEquals("Wrong status bar color.", Color.BLUE, statusBarColor.get().intValue());

        // StatusBarColorController#getStatusBarColorWithoutStatusIndicator should still return the
        // initial color.
        Assert.assertEquals(
                "Wrong value returned by #getStatusBarColorWithoutStatusIndicator().",
                initialColor,
                statusBarColorController.getStatusBarColorWithoutStatusIndicator());

        // Set scrim.
        ThreadUtils.runOnUiThreadBlocking(
                () -> statusBarColorController.setStatusBarScrimFraction(.5f));

        Assert.assertEquals(
                "Wrong status bar color w/ scrim",
                getScrimmedColor(Color.BLUE, .5f),
                statusBarColor.get().intValue());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Remove scrim.
                    statusBarColorController.setStatusBarScrimFraction(.0f);
                    // Set the status indicator color to the default, i.e. transparent.
                    statusBarColorController.onStatusIndicatorColorChanged(Color.TRANSPARENT);
                });

        // Now, the status bar color should be back to the initial color.
        Assert.assertEquals(
                "Wrong status bar color after the status indicator color is set to default.",
                initialColor,
                statusBarColor.get().intValue());
    }

    /** Test status bar color for Tab Strip Redesign Folio. */
    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({
        UiRestriction.RESTRICTION_TYPE_TABLET,
        DeviceRestriction.RESTRICTION_TYPE_NON_AUTO
    })
    public void testStatusBarColorForTabStripRedesignFolioTablet() {
        final ChromeActivity activity = sActivityTestRule.getActivity();
        final StatusBarColorController statusBarColorController =
                sActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getStatusBarColorController();

        ThreadUtils.runOnUiThreadBlocking(() -> statusBarColorController.updateStatusBarColor());
        assertEquals(
                "Wrong value returned for Tab Strip Redesign Folio.",
                TabUiThemeUtil.getTabStripBackgroundColor(activity, false),
                activity.getWindow().getStatusBarColor());
    }

    @Test
    @LargeTest
    @Feature({"StatusBar"})
    @Restriction({
        UiRestriction.RESTRICTION_TYPE_TABLET,
        DeviceRestriction.RESTRICTION_TYPE_NON_AUTO
    })
    public void testStatusBarColorOnTabletDuringTabStripTransition() {
        final ChromeActivity activity = sActivityTestRule.getActivity();
        final StatusBarColorController statusBarColorController =
                sActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getStatusBarColorController();
        statusBarColorController.setAllowToolbarColorOnTablets(true);

        var toolbarColor = sActivityTestRule.getActivity().getToolbarManager().getPrimaryColor();

        // We will invoke #onToolbarColorChanged() on a tablet that in turn invokes
        // #updateStatusBarColor() to assert that it sets |mToolbarColor| as expected. The status
        // bar should use |mToolbarColor| when the tab strip is hidden, and the tab strip background
        // color otherwise.

        // Assume that the tab strip is initially hidden.
        statusBarColorController.setTabStripHiddenOnTablet(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> statusBarColorController.onToolbarColorChanged(toolbarColor));
        assertEquals(
                "Status bar color on tablet should match the toolbar background when the tab strip"
                        + " is hidden.",
                toolbarColor,
                activity.getWindow().getStatusBarColor());

        // Simulate an in-progress hide->show transition, where a scrim will be added on the status
        // bar.
        // TabStripHeightObserver#onHeightChanged() is expected to update the final strip visibility
        // state in StatusBarColorController for this transition once the control container margins
        // are updated and before the transition runs to completion.
        statusBarColorController.setTabStripHiddenOnTablet(false);
        ThreadUtils.runOnUiThreadBlocking(
                () -> statusBarColorController.setTabStripColorOverlay(toolbarColor, 0.5f));
        assertEquals(
                "Status bar color on tablet should use the tab strip transition scrim overlay"
                        + " during a strip transition.",
                ColorUtils.getColorWithOverlay(
                        TabUiThemeUtil.getTabStripBackgroundColor(activity, false),
                        toolbarColor,
                        0.5f),
                activity.getWindow().getStatusBarColor());

        // Simulate transition completion by resetting the transition overlay state in
        // StatusBarColorController.
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        statusBarColorController.setTabStripColorOverlay(
                                ScrimProperties.INVALID_COLOR, 0f));
        assertEquals(
                "Status bar color on tablet should match the default tab strip background when the"
                        + " tab strip is visible.",
                TabUiThemeUtil.getTabStripBackgroundColor(activity, false),
                activity.getWindow().getStatusBarColor());
    }

    /** Test status bar is always black in Automotive devices. */
    @Test
    @SmallTest
    @Feature({"StatusBar, Automotive Toolbar"})
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_AUTO)
    public void testStatusBarBlackInAutomotive() {
        final ChromeActivity activity = sActivityTestRule.getActivity();
        assertEquals(
                "Status bar should always be black in automotive devices.",
                Color.BLACK,
                activity.getWindow().getStatusBarColor());
    }

    private int getScrimmedColor(@ColorInt int color, float fraction) {
        return ColorUtils.overlayColor(color, mScrimColor, fraction);
    }

    private void waitForStatusBarColor(Activity activity, int expectedColor)
            throws ExecutionException, TimeoutException {
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            activity.getWindow().getStatusBarColor(), Matchers.is(expectedColor));
                },
                CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    private void waitForStatusBarColorToMatchToolbarColor(Activity activity)
            throws ExecutionException, TimeoutException {
        ToolbarLayout toolbar = activity.findViewById(R.id.toolbar);
        Assert.assertTrue(
                "ToolbarLayout should be of type ToolbarPhone to get and check toolbar background.",
                toolbar instanceof ToolbarPhone);

        final int toolbarColor = ((ToolbarPhone) toolbar).getBackgroundDrawable().getColor();
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            activity.getWindow().getStatusBarColor(), Matchers.is(toolbarColor));
                },
                CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    private void scrollUpToolbarUntilPinnedAtTop(Activity activity) {
        Resources resources = activity.getResources();
        // Drag the Feed header title to scroll the toolbar to the top.
        int toY =
                -resources.getDimensionPixelOffset(R.dimen.toolbar_height_no_shadow)
                        - activity.findViewById(R.id.logo_holder).getHeight();
        TestTouchUtils.dragCompleteView(
                InstrumentationRegistry.getInstrumentation(),
                activity.findViewById(R.id.header_title),
                0,
                0,
                0,
                toY,
                10);

        // Toolbar layout view should show.
        onViewWaiting(withId(R.id.toolbar));
    }

    private void loadUrlAndWaitForShowing(String url) {
        sActivityTestRule.loadUrl(url);
        waitForShowing();
    }

    private void loadUrlInNewTabAndWaitForShowing(String url, boolean incognito) {
        sActivityTestRule.loadUrlInNewTab(url, incognito);
        waitForShowing();
    }

    private void waitForShowing() {
        // At least for now, StaticLayout requests focus when it's doneShowing(). When exactly this
        // happens is quite racy and can cause flakes, as it removes focus from the omnibox. See
        // crbug.com/342539152. Unclear if we even want this, see crbug.com/40249125.
        LayoutManagerImpl lmi = sActivityTestRule.getActivity().getLayoutManagerSupplier().get();
        LayoutTestUtils.waitForLayout(lmi, LayoutType.BROWSING);
    }
}