1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.tab;
6 
7 import android.graphics.Color;
8 
9 import androidx.annotation.Nullable;
10 import androidx.annotation.VisibleForTesting;
11 
12 import org.chromium.base.UserData;
13 import org.chromium.chrome.browser.previews.Previews;
14 import org.chromium.components.browser_ui.styles.ChromeColors;
15 import org.chromium.components.security_state.ConnectionSecurityLevel;
16 import org.chromium.components.security_state.SecurityStateModel;
17 import org.chromium.content_public.browser.NavigationHandle;
18 import org.chromium.content_public.browser.RenderWidgetHostView;
19 import org.chromium.content_public.browser.WebContents;
20 import org.chromium.net.NetError;
21 import org.chromium.ui.base.WindowAndroid;
22 import org.chromium.ui.util.ColorUtils;
23 
24 /**
25  * Manages theme color used for {@link Tab}. Destroyed together with the tab.
26  */
27 public class TabThemeColorHelper extends EmptyTabObserver implements UserData {
28     private static final Class<TabThemeColorHelper> USER_DATA_KEY = TabThemeColorHelper.class;
29     private final TabImpl mTab;
30 
31     private int mDefaultColor;
32     private int mColor;
33 
34     /**
35      * The default background color used for {@link #mTab} if the associate web content doesn't
36      * specify a background color.
37      */
38     private int mDefaultBackgroundColor;
39 
40     /** Whether or not the default color is used. */
41     private boolean mIsDefaultColorUsed;
42 
43     /**
44      * Whether or not the color provided by the web page is used. False if the web page does not
45      * provide a custom theme color.
46      */
47     private boolean mIsUsingColorFromTabContents;
48 
createForTab(Tab tab)49     public static void createForTab(Tab tab) {
50         assert get(tab) == null;
51         tab.getUserDataHost().setUserData(USER_DATA_KEY, new TabThemeColorHelper(tab));
52     }
53 
54     @Nullable
get(Tab tab)55     public static TabThemeColorHelper get(Tab tab) {
56         return tab.getUserDataHost().getUserData(USER_DATA_KEY);
57     }
58 
59     /** Convenience method that returns theme color of {@link Tab}. */
getColor(Tab tab)60     public static int getColor(Tab tab) {
61         return get(tab).getColor();
62     }
63 
64     /** Convenience method that returns default theme color of {@link Tab}. */
getDefaultColor(Tab tab)65     public static int getDefaultColor(Tab tab) {
66         return get(tab).getDefaultColor();
67     }
68 
69     /** @return Whether default theme color is used for the specified {@link Tab}. */
isDefaultColorUsed(Tab tab)70     public static boolean isDefaultColorUsed(Tab tab) {
71         return get(tab).mIsDefaultColorUsed;
72     }
73 
74     /** @return Whether the color provided by the web page is used for the specified {@link Tab}. */
isUsingColorFromTabContents(Tab tab)75     public static boolean isUsingColorFromTabContents(Tab tab) {
76         return get(tab).mIsUsingColorFromTabContents;
77     }
78 
79     /** @return Whether background color of the specified {@link Tab}. */
getBackgroundColor(Tab tab)80     public static int getBackgroundColor(Tab tab) {
81         return get(tab).getBackgroundColor();
82     }
83 
TabThemeColorHelper(Tab tab)84     private TabThemeColorHelper(Tab tab) {
85         mTab = (TabImpl) tab;
86         mDefaultColor = calculateDefaultColor();
87         mIsDefaultColorUsed = true;
88         mIsUsingColorFromTabContents = false;
89         mColor = mDefaultColor;
90         updateDefaultBackgroundColor();
91         tab.addObserver(this);
92 
93         updateThemeColor(false);
94     }
95 
updateDefaultColor()96     private void updateDefaultColor() {
97         mDefaultColor = calculateDefaultColor();
98         updateIfNeeded(false);
99     }
100 
calculateDefaultColor()101     private int calculateDefaultColor() {
102         return ChromeColors.getDefaultThemeColor(
103                 mTab.getContext().getResources(), mTab.isIncognito());
104     }
105 
updateDefaultBackgroundColor()106     private void updateDefaultBackgroundColor() {
107         mDefaultBackgroundColor =
108                 ChromeColors.getPrimaryBackgroundColor(mTab.getContext().getResources(), false);
109     }
110 
111     /**
112      * Updates the theme color based on if the page is native, the theme color changed, etc.
113      * @param didWebContentsThemeColorChange If the theme color of the web contents is known to have
114      *                                       changed.
115      */
updateThemeColor(boolean didWebContentsThemeColorChange)116     private void updateThemeColor(boolean didWebContentsThemeColorChange) {
117         // Start by assuming the current theme color is the one that should be used. This will
118         // either be transparent, the last theme color, or the color restored from TabState.
119         int themeColor = mColor;
120 
121         // Only use the web contents for the theme color if it is known to have changed. This
122         // corresponds to the didChangeThemeColor in WebContentsObserver.
123         if (mTab.getWebContents() != null && didWebContentsThemeColorChange) {
124             themeColor = mTab.getWebContents().getThemeColor();
125             mIsUsingColorFromTabContents = themeColor != TabState.UNSPECIFIED_THEME_COLOR
126                     && ColorUtils.isValidThemeColor(themeColor);
127             if (mIsUsingColorFromTabContents) {
128                 mIsDefaultColorUsed = false;
129             }
130         }
131 
132         boolean isThemingAllowed = checkThemingAllowed();
133         if (!isThemingAllowed) {
134             mIsUsingColorFromTabContents = false;
135         }
136         if (!mIsUsingColorFromTabContents) {
137             themeColor = mDefaultColor;
138             mIsDefaultColorUsed = true;
139             if (mTab.getActivity() != null && isThemingAllowed) {
140                 int customThemeColor = mTab.getActivity().getActivityThemeColor();
141                 if (customThemeColor != TabState.UNSPECIFIED_THEME_COLOR) {
142                     themeColor = customThemeColor;
143                     mIsDefaultColorUsed = false;
144                 }
145             }
146         }
147 
148         // Ensure there is no alpha component to the theme color as that is not supported in the
149         // dependent UI.
150         mColor = themeColor | 0xFF000000;
151     }
152 
153     /**
154      * Returns whether theming the activity is allowed (either by the web contents or by the
155      * activity).
156      */
checkThemingAllowed()157     private boolean checkThemingAllowed() {
158         // Do not apply the theme color if there are any security issues on the page.
159         final int securityLevel =
160                 SecurityStateModel.getSecurityLevelForWebContents(mTab.getWebContents());
161         return securityLevel != ConnectionSecurityLevel.DANGEROUS
162                 && securityLevel != ConnectionSecurityLevel.SECURE_WITH_POLICY_INSTALLED_CERT
163                 && (mTab.getActivity() == null || !mTab.getActivity().isTablet())
164                 && (mTab.getActivity() == null
165                         || !mTab.getActivity().getNightModeStateProvider().isInNightMode())
166                 && !mTab.isNativePage() && !mTab.isIncognito() && !Previews.isPreview(mTab);
167     }
168 
169     /**
170      * Determines if the theme color has changed and notifies the listeners if it has.
171      * @param didWebContentsThemeColorChange If the theme color of the web contents is known to have
172      *                                       changed.
173      */
updateIfNeeded(boolean didWebContentsThemeColorChange)174     public void updateIfNeeded(boolean didWebContentsThemeColorChange) {
175         int oldThemeColor = mColor;
176         updateThemeColor(didWebContentsThemeColorChange);
177         if (oldThemeColor == mColor) return;
178         mTab.notifyThemeColorChanged(mColor);
179     }
180 
181     /**
182      * @return The default theme color for this tab.
183      */
184     @VisibleForTesting
getDefaultColor()185     public int getDefaultColor() {
186         return mDefaultColor;
187     }
188 
189     /**
190      * @return The current theme color based on the value passed from the web contents and the
191      *         security state.
192      */
getColor()193     public int getColor() {
194         return mColor;
195     }
196 
197     /**
198      * Returns the background color of the associate web content of {@link #mTab}, or the default
199      * background color if the web content background color is not specified (i.e. transparent).
200      * See native WebContentsAndroid#GetBackgroundColor.
201      * @return The background color of {@link #mTab}.
202      */
getBackgroundColor()203     public int getBackgroundColor() {
204         if (mTab.isNativePage()) return mTab.getNativePage().getBackgroundColor();
205 
206         WebContents tabWebContents = mTab.getWebContents();
207         RenderWidgetHostView rwhv =
208                 tabWebContents == null ? null : tabWebContents.getRenderWidgetHostView();
209         final int backgroundColor = rwhv != null ? rwhv.getBackgroundColor() : Color.TRANSPARENT;
210         return backgroundColor == Color.TRANSPARENT ? mDefaultBackgroundColor : backgroundColor;
211     }
212 
213     // TabObserver
214 
215     @Override
onInitialized( Tab tab, String appId, @Nullable Boolean hasThemeColor, int themeColor)216     public void onInitialized(
217             Tab tab, String appId, @Nullable Boolean hasThemeColor, int themeColor) {
218         if (hasThemeColor == null) return;
219 
220         // Update from TabState.
221         mIsUsingColorFromTabContents = hasThemeColor;
222         mIsDefaultColorUsed = !mIsUsingColorFromTabContents;
223         mColor = mIsDefaultColorUsed ? getDefaultColor() : themeColor;
224         updateIfNeeded(false);
225     }
226 
227     @Override
onSSLStateUpdated(Tab tab)228     public void onSSLStateUpdated(Tab tab) {
229         updateIfNeeded(false);
230     }
231 
232     @Override
onUrlUpdated(Tab tab)233     public void onUrlUpdated(Tab tab) {
234         updateIfNeeded(false);
235     }
236 
237     @Override
onDidFailLoad(Tab tab, boolean isMainFrame, int errorCode, String failingUrl)238     public void onDidFailLoad(Tab tab, boolean isMainFrame, int errorCode, String failingUrl) {
239         updateIfNeeded(true);
240     }
241 
242     @Override
onDidFinishNavigation(Tab tab, NavigationHandle navigation)243     public void onDidFinishNavigation(Tab tab, NavigationHandle navigation) {
244         if (navigation.errorCode() != NetError.OK) updateIfNeeded(true);
245     }
246 
247     @Override
onActivityAttachmentChanged(Tab tab, @Nullable WindowAndroid window)248     public void onActivityAttachmentChanged(Tab tab, @Nullable WindowAndroid window) {
249         updateDefaultColor();
250         updateDefaultBackgroundColor();
251         updateIfNeeded(/* didWebContentsThemeColorChange= */ true);
252     }
253 
254     @Override
onDestroyed(Tab tab)255     public void onDestroyed(Tab tab) {
256         tab.removeObserver(this);
257     }
258 }
259