1 // Copyright 2019 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.toolbar.top;
6 
7 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.ACCESSIBILITY_ENABLED;
8 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.BUTTONS_CLICKABLE;
9 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IDENTITY_DISC_AT_START;
10 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IDENTITY_DISC_CLICK_HANDLER;
11 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IDENTITY_DISC_DESCRIPTION;
12 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IDENTITY_DISC_IMAGE;
13 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IDENTITY_DISC_IS_VISIBLE;
14 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.INCOGNITO_STATE_PROVIDER;
15 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.INCOGNITO_SWITCHER_VISIBLE;
16 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IN_START_SURFACE_MODE;
17 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IS_INCOGNITO;
18 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.IS_VISIBLE;
19 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.LOGO_IS_VISIBLE;
20 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.NEW_TAB_BUTTON_AT_START;
21 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.NEW_TAB_BUTTON_HIGHLIGHT;
22 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.NEW_TAB_BUTTON_IS_VISIBLE;
23 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.NEW_TAB_CLICK_HANDLER;
24 import static org.chromium.chrome.browser.toolbar.top.StartSurfaceToolbarProperties.TRANSLATION_Y;
25 
26 import android.view.View;
27 
28 import androidx.annotation.VisibleForTesting;
29 
30 import org.chromium.base.Callback;
31 import org.chromium.base.CallbackController;
32 import org.chromium.base.supplier.ObservableSupplier;
33 import org.chromium.base.supplier.Supplier;
34 import org.chromium.chrome.browser.layouts.LayoutStateProvider;
35 import org.chromium.chrome.browser.layouts.LayoutType;
36 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
37 import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
38 import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider;
39 import org.chromium.chrome.browser.tabmodel.TabModel;
40 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
41 import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
42 import org.chromium.chrome.browser.toolbar.ButtonData;
43 import org.chromium.chrome.browser.toolbar.menu_button.MenuButtonCoordinator;
44 import org.chromium.chrome.browser.user_education.IPHCommandBuilder;
45 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
46 import org.chromium.chrome.features.start_surface.StartSurfaceState;
47 import org.chromium.components.search_engines.TemplateUrlService.TemplateUrlServiceObserver;
48 import org.chromium.ui.modelutil.PropertyModel;
49 
50 /** The mediator implements interacts between the views and the caller. */
51 class StartSurfaceToolbarMediator {
52     private final PropertyModel mPropertyModel;
53     private final Callback<IPHCommandBuilder> mShowIPHCallback;
54     private final boolean mHideIncognitoSwitchWhenNoTabs;
55     private final boolean mHideIncognitoSwitchOnHomePage;
56     private final boolean mShowNewTabAndIdentityDiscAtStart;
57     private final Supplier<ButtonData> mIdentityDiscButtonSupplier;
58 
59     private TabModelSelector mTabModelSelector;
60     private TemplateUrlServiceObserver mTemplateUrlObserver;
61     private TabModelSelectorObserver mTabModelSelectorObserver;
62     private LayoutStateProvider mLayoutStateProvider;
63     private LayoutStateProvider.LayoutStateObserver mLayoutStateObserver;
64     private MenuButtonCoordinator mMenuButtonCoordinator;
65     @StartSurfaceState
66     private int mOverviewModeState;
67     private boolean mIsGoogleSearchEngine;
68 
69     private CallbackController mCallbackController = new CallbackController();
70     private float mNonIncognitoHomepageTranslationY;
71 
StartSurfaceToolbarMediator(PropertyModel model, Callback<IPHCommandBuilder> showIPHCallback, boolean hideIncognitoSwitchWhenNoTabs, boolean hideIncognitoSwitchOnHomePage, boolean showNewTabAndIdentityDiscAtStart, MenuButtonCoordinator menuButtonCoordinator, ObservableSupplier<Boolean> identityDiscStateSupplier, Supplier<ButtonData> identityDiscButtonSupplier)72     StartSurfaceToolbarMediator(PropertyModel model, Callback<IPHCommandBuilder> showIPHCallback,
73             boolean hideIncognitoSwitchWhenNoTabs, boolean hideIncognitoSwitchOnHomePage,
74             boolean showNewTabAndIdentityDiscAtStart, MenuButtonCoordinator menuButtonCoordinator,
75             ObservableSupplier<Boolean> identityDiscStateSupplier,
76             Supplier<ButtonData> identityDiscButtonSupplier) {
77         mPropertyModel = model;
78         mOverviewModeState = StartSurfaceState.NOT_SHOWN;
79         mShowIPHCallback = showIPHCallback;
80         mHideIncognitoSwitchWhenNoTabs = hideIncognitoSwitchWhenNoTabs;
81         mHideIncognitoSwitchOnHomePage = hideIncognitoSwitchOnHomePage;
82         mShowNewTabAndIdentityDiscAtStart = showNewTabAndIdentityDiscAtStart;
83         mMenuButtonCoordinator = menuButtonCoordinator;
84         mIdentityDiscButtonSupplier = identityDiscButtonSupplier;
85         identityDiscStateSupplier.addObserver((canShowHint) -> {
86             // If the identity disc wants to be hidden and is hidden, there's nothing we need to do.
87             if (!canShowHint && !mPropertyModel.get(IDENTITY_DISC_IS_VISIBLE)) return;
88             updateIdentityDisc(mIdentityDiscButtonSupplier.get());
89         });
90     }
91 
onNativeLibraryReady()92     void onNativeLibraryReady() {
93         assert mTemplateUrlObserver == null;
94 
95         mTemplateUrlObserver = new TemplateUrlServiceObserver() {
96             @Override
97             public void onTemplateURLServiceChanged() {
98                 updateLogoVisibility(TemplateUrlServiceFactory.get().isDefaultSearchEngineGoogle());
99             }
100         };
101 
102         TemplateUrlServiceFactory.get().addObserver(mTemplateUrlObserver);
103         mIsGoogleSearchEngine = TemplateUrlServiceFactory.get().isDefaultSearchEngineGoogle();
104         updateLogoVisibility(TemplateUrlServiceFactory.get().isDefaultSearchEngineGoogle());
105     }
106 
destroy()107     void destroy() {
108         if (mTemplateUrlObserver != null) {
109             TemplateUrlServiceFactory.get().removeObserver(mTemplateUrlObserver);
110         }
111         if (mTabModelSelectorObserver != null) {
112             mTabModelSelector.removeObserver(mTabModelSelectorObserver);
113         }
114         if (mLayoutStateObserver != null) {
115             mLayoutStateProvider.removeObserver(mLayoutStateObserver);
116         }
117         if (mCallbackController != null) {
118             mCallbackController.destroy();
119             mCallbackController = null;
120         }
121     }
122 
onStartSurfaceStateChanged( @tartSurfaceState int newState, boolean shouldShowStartSurfaceToolbar)123     void onStartSurfaceStateChanged(
124             @StartSurfaceState int newState, boolean shouldShowStartSurfaceToolbar) {
125         mOverviewModeState = newState;
126         setStartSurfaceToolbarVisibility(shouldShowStartSurfaceToolbar);
127         updateIncognitoSwitchVisibility();
128         updateNewTabButtonVisibility();
129         updateLogoVisibility(mIsGoogleSearchEngine);
130         updateIdentityDisc(mIdentityDiscButtonSupplier.get());
131         updateTranslationY(mNonIncognitoHomepageTranslationY);
132     }
133 
onStartSurfaceHeaderOffsetChanged(int verticalOffset)134     void onStartSurfaceHeaderOffsetChanged(int verticalOffset) {
135         updateTranslationY(verticalOffset);
136     }
137 
shouldHideToolbarContainer(int toolbarHeight)138     boolean shouldHideToolbarContainer(int toolbarHeight) {
139         // If it's on the non-incognito homepage, start surface toolbar is visible (omnibox has no
140         // focus), and scrolling offset is smaller than toolbar's height, we need to hide toolbar
141         // container until start surface toolbar is disappearing.
142         return mOverviewModeState == StartSurfaceState.SHOWN_HOMEPAGE
143                 && !mPropertyModel.get(IS_INCOGNITO) && mPropertyModel.get(IS_VISIBLE)
144                 && -mPropertyModel.get(TRANSLATION_Y) != 0
145                 && -mPropertyModel.get(TRANSLATION_Y) < toolbarHeight;
146     }
147 
setOnNewTabClickHandler(View.OnClickListener listener)148     void setOnNewTabClickHandler(View.OnClickListener listener) {
149         mPropertyModel.set(NEW_TAB_CLICK_HANDLER, listener);
150     }
151 
setTabModelSelector(TabModelSelector selector)152     void setTabModelSelector(TabModelSelector selector) {
153         mTabModelSelector = selector;
154 
155         if (mTabModelSelectorObserver == null) {
156             mTabModelSelectorObserver = new EmptyTabModelSelectorObserver() {
157                 @Override
158                 public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
159                     mPropertyModel.set(IS_INCOGNITO, mTabModelSelector.isIncognitoSelected());
160                     updateIdentityDisc(mIdentityDiscButtonSupplier.get());
161                     updateIncognitoSwitchVisibility();
162                 }
163             };
164         }
165         mPropertyModel.set(IS_INCOGNITO, mTabModelSelector.isIncognitoSelected());
166         updateIdentityDisc(mIdentityDiscButtonSupplier.get());
167         mTabModelSelector.addObserver(mTabModelSelectorObserver);
168     }
169 
updateIncognitoSwitchVisibility()170     private void updateIncognitoSwitchVisibility() {
171         if (mOverviewModeState == StartSurfaceState.SHOWN_HOMEPAGE && mHideIncognitoSwitchOnHomePage
172                 || mShowNewTabAndIdentityDiscAtStart) {
173             mPropertyModel.set(INCOGNITO_SWITCHER_VISIBLE, false);
174             return;
175         }
176 
177         if (mHideIncognitoSwitchWhenNoTabs) {
178             mPropertyModel.set(INCOGNITO_SWITCHER_VISIBLE, hasIncognitoTabs());
179         } else {
180             mPropertyModel.set(INCOGNITO_SWITCHER_VISIBLE, true);
181         }
182     }
183 
184     // TODO(crbug.com/1042997): share with TabSwitcherModeTTPhone.
hasIncognitoTabs()185     private boolean hasIncognitoTabs() {
186         if (mTabModelSelector == null) return false;
187 
188         // Check if there is no incognito tab, or all the incognito tabs are being closed.
189         TabModel incognitoTabModel = mTabModelSelector.getModel(true);
190         for (int i = 0; i < incognitoTabModel.getCount(); i++) {
191             if (!incognitoTabModel.getTabAt(i).isClosing()) return true;
192         }
193         return false;
194     }
195 
setStartSurfaceMode(boolean inStartSurfaceMode)196     void setStartSurfaceMode(boolean inStartSurfaceMode) {
197         mPropertyModel.set(IN_START_SURFACE_MODE, inStartSurfaceMode);
198     }
199 
setStartSurfaceToolbarVisibility(boolean shouldShowStartSurfaceToolbar)200     void setStartSurfaceToolbarVisibility(boolean shouldShowStartSurfaceToolbar) {
201         mPropertyModel.set(IS_VISIBLE, shouldShowStartSurfaceToolbar);
202     }
203 
setIncognitoStateProvider(IncognitoStateProvider provider)204     void setIncognitoStateProvider(IncognitoStateProvider provider) {
205         mPropertyModel.set(INCOGNITO_STATE_PROVIDER, provider);
206     }
207 
onAccessibilityStatusChanged(boolean enabled)208     void onAccessibilityStatusChanged(boolean enabled) {
209         mPropertyModel.set(ACCESSIBILITY_ENABLED, enabled);
210         updateNewTabButtonVisibility();
211     }
212 
setLayoutStateProvider(LayoutStateProvider layoutStateProvider)213     void setLayoutStateProvider(LayoutStateProvider layoutStateProvider) {
214         assert layoutStateProvider != null;
215         assert mLayoutStateProvider == null : "the mLayoutStateProvider should set at most once.";
216 
217         mLayoutStateProvider = layoutStateProvider;
218         mLayoutStateObserver = new LayoutStateProvider.LayoutStateObserver() {
219             @Override
220             public void onStartedShowing(@LayoutType int layoutType, boolean showToolbar) {
221                 if (layoutType == LayoutType.TAB_SWITCHER) {
222                     updateIncognitoSwitchVisibility();
223                     if (mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_OMNIBOX_ONLY
224                             || mOverviewModeState
225                                     == StartSurfaceState.SHOWN_TABSWITCHER_TRENDY_TERMS
226                             || mShowNewTabAndIdentityDiscAtStart) {
227                         mPropertyModel.set(NEW_TAB_BUTTON_AT_START, true);
228                     }
229                     if (mShowNewTabAndIdentityDiscAtStart) {
230                         mPropertyModel.set(IDENTITY_DISC_AT_START, true);
231                     }
232                 }
233             }
234             @Override
235             public void onFinishedShowing(@LayoutType int layoutType) {
236                 if (layoutType == LayoutType.TAB_SWITCHER) {
237                     mPropertyModel.set(BUTTONS_CLICKABLE, true);
238                     mMenuButtonCoordinator.setClickable(true);
239                 }
240             }
241             @Override
242             public void onStartedHiding(
243                     @LayoutType int layoutType, boolean showToolbar, boolean delayAnimation) {
244                 if (layoutType == LayoutType.TAB_SWITCHER) {
245                     mPropertyModel.set(BUTTONS_CLICKABLE, false);
246                     mMenuButtonCoordinator.setClickable(false);
247                 }
248             }
249         };
250         mLayoutStateProvider.addObserver(mLayoutStateObserver);
251     }
252 
253     /**
254      * @param highlight If the new tab button should be highlighted.
255      */
setNewTabButtonHighlight(boolean highlight)256     void setNewTabButtonHighlight(boolean highlight) {
257         mPropertyModel.set(NEW_TAB_BUTTON_HIGHLIGHT, highlight);
258     }
259 
updateLogoVisibility(boolean isGoogleSearchEngine)260     private void updateLogoVisibility(boolean isGoogleSearchEngine) {
261         mIsGoogleSearchEngine = isGoogleSearchEngine;
262         boolean shouldShowLogo =
263                 (mOverviewModeState == StartSurfaceState.SHOWN_HOMEPAGE
264                         || mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_TASKS_ONLY
265                         || mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_OMNIBOX_ONLY
266                         || mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_TRENDY_TERMS)
267                 && mIsGoogleSearchEngine;
268         mPropertyModel.set(LOGO_IS_VISIBLE, shouldShowLogo);
269     }
270 
271     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
updateIdentityDisc(ButtonData buttonData)272     void updateIdentityDisc(ButtonData buttonData) {
273         boolean shouldShow = buttonData.canShow && !mTabModelSelector.isIncognitoSelected();
274         if (shouldShow) {
275             mPropertyModel.set(IDENTITY_DISC_CLICK_HANDLER, buttonData.onClickListener);
276             // Take a defensive copy of the Drawable, since Drawables aren't immutable, and another
277             // view mutating our drawable could cause it to display incorrectly.
278             mPropertyModel.set(
279                     IDENTITY_DISC_IMAGE, buttonData.drawable.getConstantState().newDrawable());
280             mPropertyModel.set(IDENTITY_DISC_DESCRIPTION, buttonData.contentDescriptionResId);
281             mPropertyModel.set(IDENTITY_DISC_IS_VISIBLE, true);
282             mShowIPHCallback.onResult(buttonData.iphCommandBuilder);
283         } else {
284             mPropertyModel.set(IDENTITY_DISC_IS_VISIBLE, false);
285         }
286     }
287 
updateNewTabButtonVisibility()288     private void updateNewTabButtonVisibility() {
289         // This toolbar is only shown for tab switcher when accessibility is enabled. Note that
290         // OverviewListLayout will be shown as the tab switcher instead of the star surface.
291         boolean isShownTabswitcherState = mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER
292                 || mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_TASKS_ONLY
293                 || mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_OMNIBOX_ONLY
294                 || mOverviewModeState == StartSurfaceState.SHOWN_TABSWITCHER_TRENDY_TERMS
295                 || ChromeAccessibilityUtil.get().isAccessibilityEnabled();
296         mPropertyModel.set(NEW_TAB_BUTTON_IS_VISIBLE, isShownTabswitcherState);
297     }
298 
updateTranslationY(float transY)299     private void updateTranslationY(float transY) {
300         if (mOverviewModeState == StartSurfaceState.SHOWN_HOMEPAGE
301                 && !mPropertyModel.get(IS_INCOGNITO)) {
302             // If it's on the non-incognito homepage, document the homepage translationY.
303             mNonIncognitoHomepageTranslationY = transY;
304             // Update the translationY of the toolbarView.
305             mPropertyModel.set(TRANSLATION_Y, transY);
306         } else {
307             // If it's not on the non-incognito homepage, set the translationY as 0.
308             mPropertyModel.set(TRANSLATION_Y, 0);
309         }
310     }
311 
312     @VisibleForTesting
313     @StartSurfaceState
getOverviewModeStateForTesting()314     int getOverviewModeStateForTesting() {
315         return mOverviewModeState;
316     }
317 }
318