chromium/chrome/android/java/src/org/chromium/chrome/browser/ui/system/StatusBarColorController.java

// Copyright 2019 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 android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.view.Window;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.CallbackController;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutStateProvider.LayoutStateObserver;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.DestroyObserver;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.omnibox.UrlFocusChangeListener;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionsDropdownScrollListener;
import org.chromium.chrome.browser.status_indicator.StatusIndicatorCoordinator;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.tab_management.TabUiThemeUtil;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.browser.toolbar.top.TopToolbarCoordinator;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.scrim.ScrimProperties;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.util.ColorUtils;

/**
 * Maintains the status bar color for a {@link Window}.
 *
 * <p>TODO(crbug.com/40915553): Prevent initialization of StatusBarColorController for automotive.
 */
public class StatusBarColorController
        implements DestroyObserver,
                TopToolbarCoordinator.UrlExpansionObserver,
                StatusIndicatorCoordinator.StatusIndicatorObserver,
                UrlFocusChangeListener,
                OmniboxSuggestionsDropdownScrollListener,
                TopToolbarCoordinator.ToolbarColorObserver {
    public static final @ColorInt int UNDEFINED_STATUS_BAR_COLOR = Color.TRANSPARENT;
    public static final @ColorInt int DEFAULT_STATUS_BAR_COLOR = Color.argb(0x01, 0, 0, 0);

    /** Provides the base status bar color. */
    public interface StatusBarColorProvider {
        /**
         * @return The base status bar color to override default colors used in the
         *         {@link StatusBarColorController}. If this returns
         *         {@link #DEFAULT_STATUS_BAR_COLOR}, {@link StatusBarColorController} will use the
         *         default status bar color.
         *         If this returns a color other than {@link #UNDEFINED_STATUS_BAR_COLOR} and
         *         {@link #DEFAULT_STATUS_BAR_COLOR}, the {@link StatusBarColorController} will
         *         always use the color provided by this method to adjust the status bar color.
         *         This color may be used as-is or adjusted due to a scrim overlay.
         */
        @ColorInt
        int getBaseStatusBarColor(Tab tab);
    }

    private final Window mWindow;
    private final boolean mIsTablet;
    private @Nullable LayoutStateProvider mLayoutStateProvider;
    private final StatusBarColorProvider mStatusBarColorProvider;
    private final ActivityTabProvider.ActivityTabTabObserver mStatusBarColorTabObserver;
    private final Callback<TabModel> mCurrentTabModelObserver;
    private final TopUiThemeColorProvider mTopUiThemeColor;
    private final @ColorInt int mStandardPrimaryBgColor;
    private final @ColorInt int mIncognitoPrimaryBgColor;
    private final @ColorInt int mStandardDefaultThemeColor;
    private final @ColorInt int mIncognitoDefaultThemeColor;
    private final @ColorInt int mActiveOmniboxDefaultColor;
    private final @ColorInt int mIncognitoActiveOmniboxColor;
    private final @ColorInt int mStandardScrolledOmniboxColor;
    private final @ColorInt int mIncognitoScrolledOmniboxColor;
    private final @ColorInt int mBackgroundColorForNtp;
    private boolean mToolbarColorChanged;
    private @ColorInt int mToolbarColor;

    private @Nullable TabModelSelector mTabModelSelector;
    private CallbackController mCallbackController = new CallbackController();
    private @Nullable Tab mCurrentTab;
    private boolean mIsInOverviewMode;
    private boolean mIsIncognitoBranded;
    private boolean mIsOmniboxFocused;
    private boolean mAreSuggestionsScrolled;

    private @ColorInt int mScrimColor = ScrimProperties.INVALID_COLOR;
    private float mStatusBarScrimFraction;

    private boolean mShouldUpdateStatusBarColorForNtp;
    private @ColorInt int mStatusIndicatorColor;
    private @ColorInt int mStatusBarColorWithoutStatusIndicator;

    // Tab strip transition states.
    private boolean mTabStripHiddenOnTablet;
    private @ColorInt int mTabStripTransitionOverlayColor = ScrimProperties.INVALID_COLOR;
    private float mTabStripTransitionOverlayAlpha;
    private boolean mAllowToolbarColorOnTablets;

    private final LayoutStateObserver mLayoutStateObserver =
            new LayoutStateObserver() {
                @Override
                public void onStartedShowing(@LayoutType int layoutType) {
                    if (layoutType != LayoutType.TAB_SWITCHER) {
                        return;
                    }
                    mIsInOverviewMode = true;
                }

                @Override
                public void onFinishedHiding(@LayoutType int layoutType) {
                    if (layoutType != LayoutType.TAB_SWITCHER) {
                        return;
                    }
                    mIsInOverviewMode = false;
                    updateStatusBarColor();
                }
            };

    /**
     * Constructs a StatusBarColorController.
     *
     * @param window The Android app window, used to access decor view and set the status color.
     * @param isTablet Whether the current context is on a tablet.
     * @param context The Android context used to load colors.
     * @param statusBarColorProvider An implementation of {@link StatusBarColorProvider}.
     * @param layoutManagerSupplier Supplies the layout manager.
     * @param activityLifecycleDispatcher Allows observation of the activity lifecycle.
     * @param tabProvider The {@link ActivityTabProvider} to get current tab of the activity.
     * @param topUiThemeColorProvider The {@link ThemeColorProvider} for top UI.
     */
    public StatusBarColorController(
            Window window,
            boolean isTablet,
            Context context,
            StatusBarColorProvider statusBarColorProvider,
            ObservableSupplier<LayoutManager> layoutManagerSupplier,
            ActivityLifecycleDispatcher activityLifecycleDispatcher,
            ActivityTabProvider tabProvider,
            TopUiThemeColorProvider topUiThemeColorProvider) {
        mWindow = window;
        mIsTablet = isTablet;
        mStatusBarColorProvider = statusBarColorProvider;
        mAllowToolbarColorOnTablets = false;

        mStandardPrimaryBgColor = ChromeColors.getPrimaryBackgroundColor(context, false);
        mIncognitoPrimaryBgColor = ChromeColors.getPrimaryBackgroundColor(context, true);
        mStandardDefaultThemeColor = ChromeColors.getDefaultThemeColor(context, false);
        mIncognitoDefaultThemeColor = ChromeColors.getDefaultThemeColor(context, true);
        mBackgroundColorForNtp =
                ChromeColors.getSurfaceColor(
                        context, R.dimen.home_surface_background_color_elevation);
        mStatusIndicatorColor = UNDEFINED_STATUS_BAR_COLOR;

        // TODO(b/41494931): Share code with LocationBarCoordinator's constructor.
        mActiveOmniboxDefaultColor =
                ChromeColors.getSurfaceColor(
                        context, R.dimen.omnibox_suggestion_dropdown_bg_elevation);
        mIncognitoActiveOmniboxColor = context.getColor(R.color.omnibox_dropdown_bg_incognito);
        // TODO(b/41494931): Share code with ToolbarPhone#getToolbarDefaultColor().
        mStandardScrolledOmniboxColor =
                ChromeColors.getSurfaceColor(context, R.dimen.toolbar_text_box_elevation);
        mIncognitoScrolledOmniboxColor =
                context.getColor(R.color.default_bg_color_dark_elev_2_baseline);

        mStatusBarColorTabObserver =
                new ActivityTabProvider.ActivityTabTabObserver(tabProvider) {
                    @Override
                    public void onShown(Tab tab, @TabSelectionType int type) {
                        updateStatusBarColor();
                    }

                    @Override
                    public void onDidChangeThemeColor(Tab tab, @ColorInt int color) {
                        updateStatusBarColor();
                    }

                    @Override
                    public void onContentChanged(Tab tab) {
                        final boolean newShouldUpdateStatusBarColorForNtp = isStandardNtp();
                        // Also update the status bar color if the content was previously an NTP,
                        // because an NTP can use a different status bar color than the default
                        // theme color. In this case, the theme color might not change, and thus
                        // #onDidChangeThemeColor
                        // might not get called.
                        if (mShouldUpdateStatusBarColorForNtp
                                || newShouldUpdateStatusBarColorForNtp) {
                            updateStatusBarColor();
                        }
                        mShouldUpdateStatusBarColorForNtp = newShouldUpdateStatusBarColorForNtp;
                    }

                    @Override
                    public void onActivityAttachmentChanged(
                            Tab tab, @Nullable WindowAndroid window) {
                        // Stop referring to the Tab once detached from an activity. Will be
                        // restored by |onObservingDifferentTab|.
                        if (window == null) mCurrentTab = null;
                    }

                    @Override
                    public void onDestroyed(Tab tab) {
                        // Make sure that #mCurrentTab is cleared because #onObservingDifferentTab()
                        // might not be notified early enough when
                        // #onUrlExpansionPercentageChanged() is
                        // called.
                        mCurrentTab = null;
                        mShouldUpdateStatusBarColorForNtp = false;
                    }

                    @Override
                    protected void onObservingDifferentTab(Tab tab, boolean hint) {
                        mCurrentTab = tab;
                        mShouldUpdateStatusBarColorForNtp = isStandardNtp();

                        // |tab == null| means we're switching tabs - by the tab switcher or by
                        // swiping on the omnibox. These cases are dealt with differently,
                        // elsewhere.
                        if (tab == null) return;
                        updateStatusBarColor();
                    }
                };

        mCurrentTabModelObserver =
                (tabModel) -> {
                    mIsIncognitoBranded = tabModel.isIncognitoBranded();
                    // When opening a new Incognito Tab from a normal Tab (or vice versa), the
                    // status bar color is updated. However, this update is triggered after the
                    // animation, so we update here for the duration of the new Tab animation.
                    // See https://crbug.com/917689.
                    updateStatusBarColor();
                };

        if (layoutManagerSupplier != null) {
            layoutManagerSupplier.addObserver(
                    mCallbackController.makeCancelable(
                            layoutManager -> {
                                assert layoutManager != null;
                                mLayoutStateProvider = layoutManager;
                                mLayoutStateProvider.addObserver(mLayoutStateObserver);
                            }));
        }

        activityLifecycleDispatcher.register(this);
        mTopUiThemeColor = topUiThemeColorProvider;
        mToolbarColorChanged = false;
    }

    // DestroyObserver implementation.
    @Override
    public void onDestroy() {
        mStatusBarColorTabObserver.destroy();
        if (mLayoutStateProvider != null) {
            mLayoutStateProvider.removeObserver(mLayoutStateObserver);
        }
        if (mTabModelSelector != null) {
            mTabModelSelector.getCurrentTabModelSupplier().removeObserver(mCurrentTabModelObserver);
        }
        if (mCallbackController != null) {
            mCallbackController.destroy();
            mCallbackController = null;
        }
    }

    // TopToolbarCoordinator.UrlExpansionObserver implementation.
    @Override
    public void onUrlExpansionProgressChanged() {
        if (mShouldUpdateStatusBarColorForNtp) updateStatusBarColor();
    }

    // TopToolbarCoordinator.ToolbarColorObserver implementation.
    @Override
    public void onToolbarColorChanged(@ColorInt int color) {
        // Status bar color on tablets could change when the tab strip transition is enabled,
        // where it might be required for the status bar color to match the toolbar color when the
        // tab strip is hidden.
        if (mIsTablet && !mAllowToolbarColorOnTablets) return;

        // Set mToolbarColorChanged to true as an extra check to prevent rendering status bar with
        // default color if toolbar never changes, for example, in dark mode.
        mToolbarColorChanged = true;
        mToolbarColor = color;
        updateStatusBarColor();
    }

    // StatusIndicatorCoordinator.StatusIndicatorObserver implementation.
    @Override
    public void onStatusIndicatorColorChanged(@ColorInt int newColor) {
        mStatusIndicatorColor = newColor;
        updateStatusBarColor();
    }

    @Override
    public void onUrlFocusChange(boolean hasFocus) {
        mIsOmniboxFocused = hasFocus;
        updateStatusBarColor();
    }

    @Override
    public void onSuggestionDropdownScroll() {
        mAreSuggestionsScrolled = true;
        updateStatusBarColor();
    }

    @Override
    public void onSuggestionDropdownOverscrolledToTop() {
        mAreSuggestionsScrolled = false;
        updateStatusBarColor();
    }

    /**
     * Update the scrim color on the status bar.
     * @param scrimColor The scrim color int.
     */
    public void setScrimColor(@ColorInt int scrimColor) {
        mScrimColor = scrimColor;
    }

    /**
     * @return The current scrim color for the status bar.
     */
    public @ColorInt int getScrimColorForTesting() {
        return mScrimColor;
    }

    /**
     * Update the scrim amount on the status bar.
     * @param fraction The scrim fraction in range [0, 1].
     */
    public void setStatusBarScrimFraction(float fraction) {
        mStatusBarScrimFraction = fraction;
        updateStatusBarColor();
    }

    /**
     * Add the tab strip transition scrim overlay on the status bar during a tab strip transition.
     *
     * @param overlayColor The overlay color.
     * @param overlayAlpha The alpha that |overlayColor| should have on the status bar color.
     */
    public void setTabStripColorOverlay(@ColorInt int overlayColor, float overlayAlpha) {
        assert mIsTablet;
        if (!mAllowToolbarColorOnTablets) return;
        mTabStripTransitionOverlayColor = overlayColor;
        mTabStripTransitionOverlayAlpha = overlayAlpha;
        updateStatusBarColor();
    }

    /**
     * @param tabModelSelector The {@link TabModelSelector} to check whether incognito model is
     *                         selected.
     */
    public void setTabModelSelector(TabModelSelector tabModelSelector) {
        assert mTabModelSelector == null : "mTabModelSelector should only be set once.";
        mTabModelSelector = tabModelSelector;
        if (mTabModelSelector != null) {
            mTabModelSelector.getCurrentTabModelSupplier().addObserver(mCurrentTabModelObserver);
            mIsIncognitoBranded = mTabModelSelector.isIncognitoBrandedModelSelected();
            updateStatusBarColor();
        }
    }

    /** Calculate and update the status bar's color. */
    public void updateStatusBarColor() {
        mStatusBarColorWithoutStatusIndicator = calculateBaseStatusBarColor();
        @ColorInt
        int statusBarColor = applyStatusBarIndicatorColor(mStatusBarColorWithoutStatusIndicator);
        statusBarColor = applyTabStripOverlay(statusBarColor);
        statusBarColor = applyCurrentScrimToColor(statusBarColor);
        setStatusBarColor(mWindow, statusBarColor);
    }

    /**
     * Set whether the tab strip is hidden or visible on a tablet. This state will be used to
     * determine the base status bar color on a tablet.
     *
     * @param tabStripHiddenOnTablet Whether the tab strip is hidden or visible on a tablet.
     */
    public void setTabStripHiddenOnTablet(boolean tabStripHiddenOnTablet) {
        assert mIsTablet;
        if (!mAllowToolbarColorOnTablets) return;
        mTabStripHiddenOnTablet = tabStripHiddenOnTablet;
    }

    /**
     * @return The status bar color without the status indicator's color taken into consideration.
     *         However, scrimming isn't included since it's managed completely by this class.
     */
    public @ColorInt int getStatusBarColorWithoutStatusIndicator() {
        return mStatusBarColorWithoutStatusIndicator;
    }

    private @ColorInt int calculateBaseStatusBarColor() {
        // Return overridden status bar color from StatusBarColorProvider if specified.
        @ColorInt
        int baseStatusBarColor = mStatusBarColorProvider.getBaseStatusBarColor(mCurrentTab);
        if (baseStatusBarColor == DEFAULT_STATUS_BAR_COLOR) {
            return calculateDefaultStatusBarColor();
        }
        if (baseStatusBarColor != UNDEFINED_STATUS_BAR_COLOR) {
            return baseStatusBarColor;
        }

        if (mIsTablet) {
            // When applicable, the status bar should use the default tab strip color, that is not
            // affected by an activity focus change.
            return mTabStripHiddenOnTablet
                    ? mToolbarColor
                    : TabUiThemeUtil.getTabStripBackgroundColor(
                            mWindow.getContext(), mIsIncognitoBranded);
        }

        // When Omnibox gains focus, we want to clear the status bar theme color.
        // The theme should be restored when Omnibox focus clears.
        if (mIsOmniboxFocused) {
            // If the flag is enabled, we will use the toolbar color.
            if (mToolbarColorChanged) return mToolbarColor;
            return calculateDefaultStatusBarColor();
        }

        // Return status bar color in overview mode.
        if (mIsInOverviewMode) {
            // Toolbar will notify status bar color controller about the toolbar color during
            // overview animation.
            return mToolbarColor;
        }

        // Return New Tab Page background color in New Tab Page.
        if (isStandardNtp()) {
            return mBackgroundColorForNtp;
        }

        // Return status bar color to match the toolbar.
        // If the flag is enabled, we will use the toolbar color.
        if (mToolbarColorChanged) return mToolbarColor;
        return mTopUiThemeColor.getThemeColorOrFallback(
                mCurrentTab, calculateDefaultStatusBarColor());
    }

    /** Calculates the default status bar color based on the incognito state. */
    private @ColorInt int calculateDefaultStatusBarColor() {
        if (!mIsOmniboxFocused) {
            return mIsIncognitoBranded ? mIncognitoDefaultThemeColor : mStandardDefaultThemeColor;
        }

        if (mAreSuggestionsScrolled) {
            return mIsIncognitoBranded
                    ? mIncognitoScrolledOmniboxColor
                    : mStandardScrolledOmniboxColor;
        } else {
            return mIsIncognitoBranded ? mIncognitoActiveOmniboxColor : mActiveOmniboxDefaultColor;
        }
    }

    /**
     * Set device status bar to a given color. Also, set the status bar icons to a dark color if
     * needed.
     * @param window The current window of the UI view.
     * @param color The color that the status bar should be set to.
     */
    public static void setStatusBarColor(Window window, @ColorInt int color) {
        if (UiUtils.isSystemUiThemingDisabled()) return;

        final View root = window.getDecorView().getRootView();
        boolean needsDarkStatusBarIcons = !ColorUtils.shouldUseLightForegroundOnBackground(color);
        UiUtils.setStatusBarIconColor(root, needsDarkStatusBarIcons);
        UiUtils.setStatusBarColor(window, color);
    }

    /**
     * Takes status bar indicator into account in status bar color computation.
     * @param color The status bar color without the status indicator's color taken into
     *              consideration. (as specified in
     *              {@link getStatusBarColorWithoutStatusIndicator()}).
     * @return The resulting color.
     */
    private @ColorInt int applyStatusBarIndicatorColor(@ColorInt int darkenedBaseColor) {
        if (mStatusIndicatorColor == UNDEFINED_STATUS_BAR_COLOR) return darkenedBaseColor;

        return mStatusIndicatorColor;
    }

    /**
     * Get the scrim applied color if the scrim is showing. Otherwise, return the original color.
     * @param color Color to maybe apply scrim to.
     * @return The resulting color.
     */
    private @ColorInt int applyCurrentScrimToColor(@ColorInt int color) {
        if (mScrimColor == ScrimProperties.INVALID_COLOR) {
            final View root = mWindow.getDecorView().getRootView();
            final Context context = root.getContext();
            mScrimColor = context.getColor(R.color.default_scrim_color);
        }
        // Apply a color overlay if the scrim is showing.
        return ColorUtils.overlayColor(color, mScrimColor, mStatusBarScrimFraction);
    }

    /**
     * Apply and get the tab strip overlay applied color if the strip transition scrim is showing.
     *
     * @param color Base color to apply the overlay to.
     */
    private @ColorInt int applyTabStripOverlay(@ColorInt int color) {
        // If the tab strip is transitioning on a tablet, apply the strip overlay on the status bar.
        if (mIsTablet && mTabStripTransitionOverlayAlpha > 0) {
            return ColorUtils.getColorWithOverlay(
                    color, mTabStripTransitionOverlayColor, mTabStripTransitionOverlayAlpha);
        }
        return color;
    }

    /**
     * Determines whether the status bar color could use the toolbar color on tablets when certain
     * conditions are met. This is currently supported on ChromeTabbedActivity's only.
     *
     * @param allowToolbarColorOnTablets Whether the status bar color could use the toolbar color.
     */
    // TODO(crbug.com/324313724): Support this for other activities.
    public void setAllowToolbarColorOnTablets(boolean allowToolbarColorOnTablets) {
        mAllowToolbarColorOnTablets = allowToolbarColorOnTablets;
    }

    /**
     * @return Whether or not the current tab is a new tab page in standard mode.
     */
    private boolean isStandardNtp() {
        return mCurrentTab != null && mCurrentTab.getNativePage() instanceof NewTabPage;
    }
}