chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarColorController.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.customtabs.features.toolbar;

import static org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider.CustomTabProfileType.INCOGNITO;

import android.app.Activity;
import android.content.res.ColorStateList;

import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.blink.mojom.DisplayMode;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappExtras;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar.CustomTabTabObserver;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.omnibox.styles.OmniboxResourceProvider;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.theme.ThemeUtils;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.browser.toolbar.ToolbarManager;
import org.chromium.chrome.browser.ui.google_bottom_bar.GoogleBottomBarCoordinator;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import javax.inject.Inject;

/** Maintains the toolbar color for {@link CustomTabActivity}. */
@ActivityScope
public class CustomTabToolbarColorController {
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        ToolbarColorType.THEME_COLOR,
        ToolbarColorType.DEFAULT_COLOR,
        ToolbarColorType.INTENT_TOOLBAR_COLOR
    })
    public @interface ToolbarColorType {
        int THEME_COLOR = 0;
        int DEFAULT_COLOR = 1;
        // BrowserServicesIntentDataProvider#getToolbarColor() should be used.
        int INTENT_TOOLBAR_COLOR = 2;
    }

    /**
     * Interface used to receive a predicate that tells if the current tab is in preview mode.
     * This makes the {@link #computeToolbarColorType()} test-friendly.
     */
    public interface BooleanFunction {
        boolean get();
    }

    private final BrowserServicesIntentDataProvider mIntentDataProvider;
    private final Activity mActivity;
    private final TabObserverRegistrar mTabObserverRegistrar;
    private final CustomTabActivityTabProvider mTabProvider;
    private final TopUiThemeColorProvider mTopUiThemeColorProvider;

    private ToolbarManager mToolbarManager;
    private boolean mUseTabThemeColor;

    @Inject
    public CustomTabToolbarColorController(
            BrowserServicesIntentDataProvider intentDataProvider,
            Activity activity,
            CustomTabActivityTabProvider tabProvider,
            TabObserverRegistrar tabObserverRegistrar,
            TopUiThemeColorProvider topUiThemeColorProvider) {
        mIntentDataProvider = intentDataProvider;
        mActivity = activity;
        mTabProvider = tabProvider;
        mTabObserverRegistrar = tabObserverRegistrar;
        mTopUiThemeColorProvider = topUiThemeColorProvider;
    }

    /**
     * Computes the toolbar color type.
     * Returns a 'type' instead of a color so that the function can be used by non-toolbar UI
     * surfaces with different values for {@link ToolbarColorType.DEFAULT_COLOR}.
     */
    public static int computeToolbarColorType(
            BrowserServicesIntentDataProvider intentDataProvider,
            boolean useTabThemeColor,
            @Nullable Tab tab) {
        if (intentDataProvider.isOpenedByChrome()) {
            if (intentDataProvider.getColorProvider().hasCustomToolbarColor()) {
                return ToolbarColorType.INTENT_TOOLBAR_COLOR;
            }
            return (tab == null) ? ToolbarColorType.DEFAULT_COLOR : ToolbarColorType.THEME_COLOR;
        }

        if (shouldUseDefaultThemeColorForFullscreen(intentDataProvider)) {
            return ToolbarColorType.DEFAULT_COLOR;
        }

        if (tab != null && useTabThemeColor) {
            return ToolbarColorType.THEME_COLOR;
        }

        return intentDataProvider.getColorProvider().hasCustomToolbarColor()
                ? ToolbarColorType.INTENT_TOOLBAR_COLOR
                : ToolbarColorType.DEFAULT_COLOR;
    }

    /**
     * Notifies the ColorController that the ToolbarManager has been created and is ready for
     * use. ToolbarManager isn't passed directly to the constructor because it's not guaranteed to
     * be initialized yet.
     */
    public void onToolbarInitialized(ToolbarManager manager) {
        mToolbarManager = manager;
        assert manager != null : "Toolbar manager not initialized";

        observeTabToUpdateColor();

        updateColor();
    }

    private void observeTabToUpdateColor() {
        mTabObserverRegistrar.registerActivityTabObserver(
                new CustomTabTabObserver() {
                    @Override
                    public void onPageLoadFinished(Tab tab, GURL url) {
                        // Update the color when the page load finishes.
                        updateColor();
                    }

                    @Override
                    public void onUrlUpdated(Tab tab) {
                        // Update the color on every new URL.
                        updateColor();
                    }

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

                    @Override
                    public void onShown(Tab tab, @TabSelectionType int type) {
                        updateColor();
                    }

                    @Override
                    public void onObservingDifferentTab(@NonNull Tab tab) {
                        updateColor();
                    }
                });
    }

    /**
     * Sets whether the tab's theme color should be used for the toolbar and triggers an update of
     * the toolbar color if needed.
     */
    public void setUseTabThemeColor(boolean useTabThemeColor) {
        if (mUseTabThemeColor == useTabThemeColor) return;

        mUseTabThemeColor = useTabThemeColor;
        updateColor();
    }

    /** Updates the color of the Activity's CCT Toolbar. */
    private void updateColor() {
        if (mToolbarManager == null) return;

        mToolbarManager.setShouldUpdateToolbarPrimaryColor(true);
        final Tab tab = mTabProvider.getTab();
        final @ToolbarColorType int toolbarColorType =
                computeToolbarColorType(mIntentDataProvider, mUseTabThemeColor, tab);
        final @ColorInt int color = computeColor(tab, toolbarColorType);
        final @BrandedColorScheme int brandedColorScheme =
                computeBrandedColorScheme(toolbarColorType, color);
        final ColorStateList tint =
                ThemeUtils.getThemedToolbarIconTint(mActivity, brandedColorScheme);
        mToolbarManager.onThemeColorChanged(color, false);
        mToolbarManager.onTintChanged(tint, tint, brandedColorScheme);
        mToolbarManager.setShouldUpdateToolbarPrimaryColor(false);
    }

    private @ColorInt int computeColor(Tab tab, @ToolbarColorType int toolbarColorType) {
        // TODO(b/300419189): Pass the CCT Top Bar Color in AGSA intent after Page Insights Hub is
        // launched
        if (GoogleBottomBarCoordinator.isFeatureEnabled()
                && CustomTabsConnection.getInstance()
                        .shouldEnableGoogleBottomBarForIntent(mIntentDataProvider)) {
            return mActivity.getColor(R.color.google_bottom_bar_background_color);
        }
        return switch (toolbarColorType) {
            case ToolbarColorType.THEME_COLOR -> mTopUiThemeColorProvider.calculateColor(
                    tab, tab.getThemeColor());
            case ToolbarColorType.DEFAULT_COLOR -> getDefaultColor();
            case ToolbarColorType.INTENT_TOOLBAR_COLOR -> mIntentDataProvider
                    .getColorProvider()
                    .getToolbarColor();
            default -> getDefaultColor();
        };
    }

    private @BrandedColorScheme int computeBrandedColorScheme(
            @ToolbarColorType int toolbarColorType, @ColorInt int toolbarColor) {
        final boolean isIncognitoBranded = mIntentDataProvider.getCustomTabMode() == INCOGNITO;
        return switch (toolbarColorType) {
            case ToolbarColorType.THEME_COLOR -> OmniboxResourceProvider.getBrandedColorScheme(
                    mActivity, isIncognitoBranded, toolbarColor);
            case ToolbarColorType.DEFAULT_COLOR -> isIncognitoBranded
                    ? BrandedColorScheme.INCOGNITO
                    : BrandedColorScheme.APP_DEFAULT;
            case ToolbarColorType.INTENT_TOOLBAR_COLOR -> ColorUtils
                            .shouldUseLightForegroundOnBackground(toolbarColor)
                    ? BrandedColorScheme.DARK_BRANDED_THEME
                    : BrandedColorScheme.LIGHT_BRANDED_THEME;
            default -> BrandedColorScheme.APP_DEFAULT;
        };
    }

    private int getDefaultColor() {
        return ChromeColors.getDefaultThemeColor(
                mActivity, mIntentDataProvider.getCustomTabMode() == INCOGNITO);
    }

    private static boolean shouldUseDefaultThemeColorForFullscreen(
            BrowserServicesIntentDataProvider intentDataProvider) {
        // Don't use the theme color provided by the page if we're in display: fullscreen. This
        // works around an issue where the status bars go transparent and can't be seen on top of
        // the page content when users swipe them in or they appear because the on-screen keyboard
        // was triggered.
        WebappExtras webappExtras = intentDataProvider.getWebappExtras();
        return (webappExtras != null && webappExtras.displayMode == DisplayMode.FULLSCREEN);
    }
}