blob: cb0f24d3f3134189e27459a39655a04bd295d9ac [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver;
import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior;
import org.chromium.chrome.browser.lifecycle.Destroyable;
import org.chromium.chrome.browser.ntp.NewTabPage;
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.tab.TabThemeColorHelper;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.toolbar.ToolbarColors;
import org.chromium.chrome.browser.toolbar.top.TopToolbarCoordinator;
import org.chromium.components.browser_ui.styles.ChromeColors;
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 ChromeActivity}.
*/
public class StatusBarColorController
implements Destroyable, TopToolbarCoordinator.UrlExpansionObserver,
StatusIndicatorCoordinator.StatusIndicatorObserver {
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 or Android
* version.
*/
@ColorInt
int getBaseStatusBarColor(Tab tab);
}
private final Window mWindow;
private final boolean mIsTablet;
private @Nullable OverviewModeBehavior mOverviewModeBehavior;
private final StatusBarColorProvider mStatusBarColorProvider;
private final ActivityTabProvider.ActivityTabTabObserver mStatusBarColorTabObserver;
private final TabModelSelectorObserver mTabModelSelectorObserver;
private final @ColorInt int mStandardPrimaryBgColor;
private final @ColorInt int mIncognitoPrimaryBgColor;
private final @ColorInt int mStandardDefaultThemeColor;
private final @ColorInt int mIncognitoDefaultThemeColor;
private @Nullable TabModelSelector mTabModelSelector;
private ObservableSupplier<OverviewModeBehavior> mOverviewModeBehaviorSupplier;
private Callback<OverviewModeBehavior> mOverviewModeBehaviorSupplierObserver;
private @Nullable OverviewModeBehavior.OverviewModeObserver mOverviewModeObserver;
private @Nullable Tab mCurrentTab;
private boolean mIsInOverviewMode;
private boolean mIsIncognito;
private @ColorInt int mScrimColor;
private float mStatusBarScrimFraction;
private float mToolbarUrlExpansionPercentage;
private boolean mShouldUpdateStatusBarColorForNTP;
private @ColorInt int mStatusIndicatorColor;
private @ColorInt int mStatusBarColorWithoutStatusIndicator;
/**
* @param chromeActivity The {@link ChromeActivity} that this class is attached to.
* @param tabProvider The {@link ActivityTabProvider} to get current tab of the activity.
*/
public StatusBarColorController(
ChromeActivity chromeActivity, ActivityTabProvider tabProvider) {
mWindow = chromeActivity.getWindow();
mIsTablet = chromeActivity.isTablet();
mStatusBarColorProvider = chromeActivity;
Resources resources = chromeActivity.getResources();
mStandardPrimaryBgColor = ChromeColors.getPrimaryBackgroundColor(resources, false);
mIncognitoPrimaryBgColor = ChromeColors.getPrimaryBackgroundColor(resources, true);
mStandardDefaultThemeColor = ChromeColors.getDefaultThemeColor(resources, false);
mIncognitoDefaultThemeColor = ChromeColors.getDefaultThemeColor(resources, true);
mStatusIndicatorColor = UNDEFINED_STATUS_BAR_COLOR;
mStatusBarColorTabObserver = new ActivityTabProvider.ActivityTabTabObserver(tabProvider) {
@Override
public void onShown(Tab tab, @TabSelectionType int type) {
updateStatusBarColor();
}
@Override
public void onDidChangeThemeColor(Tab tab, 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();
}
};
mTabModelSelectorObserver = new EmptyTabModelSelectorObserver() {
@Override
public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
mIsIncognito = newModel.isIncognito();
// 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();
}
};
mOverviewModeBehaviorSupplier = chromeActivity.getOverviewModeBehaviorSupplier();
if (mOverviewModeBehaviorSupplier != null) {
mOverviewModeBehaviorSupplierObserver = new Callback<OverviewModeBehavior>() {
@Override
public void onResult(OverviewModeBehavior overviewModeBehavior) {
assert overviewModeBehavior != null;
mOverviewModeBehavior = overviewModeBehavior;
mOverviewModeObserver = new EmptyOverviewModeObserver() {
@Override
public void onOverviewModeStartedShowing(boolean showToolbar) {
mIsInOverviewMode = true;
updateStatusBarColor();
}
@Override
public void onOverviewModeFinishedHiding() {
mIsInOverviewMode = false;
updateStatusBarColor();
}
};
mOverviewModeBehavior.addOverviewModeObserver(mOverviewModeObserver);
// TODO(crbug.com/1084528): Replace with OneShotSupplier when it is available.
mOverviewModeBehaviorSupplier.removeObserver(this);
mOverviewModeBehaviorSupplier = null;
}
};
mOverviewModeBehaviorSupplier.addObserver(mOverviewModeBehaviorSupplierObserver);
}
chromeActivity.getLifecycleDispatcher().register(this);
}
// Destroyable implementation.
@Override
public void destroy() {
mStatusBarColorTabObserver.destroy();
if (mOverviewModeBehaviorSupplier != null) {
mOverviewModeBehaviorSupplier.removeObserver(mOverviewModeBehaviorSupplierObserver);
}
if (mOverviewModeBehavior != null) {
mOverviewModeBehavior.removeOverviewModeObserver(mOverviewModeObserver);
}
if (mTabModelSelector != null) {
mTabModelSelector.removeObserver(mTabModelSelectorObserver);
}
}
// TopToolbarCoordinator.UrlExpansionObserver implementation.
@Override
public void onUrlExpansionPercentageChanged(float percentage) {
mToolbarUrlExpansionPercentage = percentage;
if (mShouldUpdateStatusBarColorForNTP) updateStatusBarColor();
}
// StatusIndicatorCoordinator.StatusIndicatorObserver implementation.
@Override
public void onStatusIndicatorColorChanged(@ColorInt int newColor) {
mStatusIndicatorColor = newColor;
updateStatusBarColor();
}
/**
* 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();
}
/**
* @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.addObserver(mTabModelSelectorObserver);
}
public void updateStatusBarColor() {
mStatusBarColorWithoutStatusIndicator = calculateBaseStatusBarColor();
if (shouldDarkenStatusBarColor()) {
mStatusBarColorWithoutStatusIndicator =
ColorUtils.getDarkenedColorForStatusBar(mStatusBarColorWithoutStatusIndicator);
}
int statusBarColor = applyStatusBarIndicatorColor(mStatusBarColorWithoutStatusIndicator);
statusBarColor = applyCurrentScrimToColor(statusBarColor);
setStatusBarColor(statusBarColor);
}
// TODO(sinansahin): Confirm pre-M expectations with UX and update as needed.
/**
* @return The status bar color without the status indicator's color taken into consideration.
* Color returned from this method includes darkening if the OS version doesn't support
* light status bar icons (pre-M). 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.
final int baseStatusBarColor = mStatusBarColorProvider.getBaseStatusBarColor(mCurrentTab);
if (baseStatusBarColor == DEFAULT_STATUS_BAR_COLOR) {
return calculateDefaultStatusBarColor();
}
if (baseStatusBarColor != UNDEFINED_STATUS_BAR_COLOR) {
return baseStatusBarColor;
}
// We don't adjust status bar color for tablet when status bar color is not overridden by
// StatusBarColorProvider.
if (mIsTablet) return Color.BLACK;
// Return status bar color in overview mode.
if (mIsInOverviewMode) {
if (shouldDarkenStatusBarColor()) return Color.BLACK;
return (mIsIncognito && ToolbarColors.canUseIncognitoToolbarThemeColorInOverview())
? mIncognitoPrimaryBgColor
: mStandardPrimaryBgColor;
}
// Return status bar color in standard NewTabPage. If location bar is not shown in NTP, we
// use the tab theme color regardless of the URL expansion percentage.
if (isLocationBarShownInNTP()) {
if (shouldDarkenStatusBarColor()) return Color.BLACK;
return ColorUtils.getColorWithOverlay(
TabThemeColorHelper.getBackgroundColor(mCurrentTab),
TabThemeColorHelper.getColor(mCurrentTab), mToolbarUrlExpansionPercentage);
}
// Return status bar color to match the toolbar.
if (mCurrentTab != null && !TabThemeColorHelper.isDefaultColorUsed(mCurrentTab)) {
return TabThemeColorHelper.getColor(mCurrentTab);
}
return calculateDefaultStatusBarColor();
}
/**
* Calculates the default status bar color based on the Android version and incognito state.
*/
private @ColorInt int calculateDefaultStatusBarColor() {
if (shouldDarkenStatusBarColor()) {
return Color.BLACK;
}
return mIsIncognito ? mIncognitoDefaultThemeColor : mStandardDefaultThemeColor;
}
/**
* Set device status bar to a given color. Also, set the status bar icons to a dark color if
* needed.
* @param color The color that the status bar should be set to.
*/
private void setStatusBarColor(@ColorInt int color) {
if (UiUtils.isSystemUiThemingDisabled()) return;
final View root = mWindow.getDecorView().getRootView();
boolean needsDarkStatusBarIcons = !ColorUtils.shouldUseLightForegroundOnBackground(color);
ApiCompatibilityUtils.setStatusBarIconColor(root, needsDarkStatusBarIcons);
ApiCompatibilityUtils.setStatusBarColor(mWindow, 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 shouldDarkenStatusBarColor()
? ColorUtils.getDarkenedColorForStatusBar(mStatusIndicatorColor)
: 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 (shouldDarkenStatusBarColor()) return color;
if (mScrimColor == 0) {
final View root = mWindow.getDecorView().getRootView();
final Resources resources = root.getResources();
mScrimColor = ApiCompatibilityUtils.getColor(resources, R.color.black_alpha_65);
}
// Apply a color overlay if the scrim is showing.
float scrimColorAlpha = (mScrimColor >>> 24) / 255f;
int scrimColorOpaque = mScrimColor & 0xFF000000;
return ColorUtils.getColorWithOverlay(
color, scrimColorOpaque, mStatusBarScrimFraction * scrimColorAlpha);
}
/**
* @return Whether to to darken the status bar color because the OS version does not support
* light status bar icons.
*/
private boolean shouldDarkenStatusBarColor() {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.M);
}
/**
* @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;
}
/**
* @return Whether or not the fake location bar is shown on the current NTP.
*/
private boolean isLocationBarShownInNTP() {
if (!isStandardNTP()) return false;
final NewTabPage newTabPage = (NewTabPage) mCurrentTab.getNativePage();
return newTabPage != null && newTabPage.isLocationBarShownInNTP();
}
}