1 // Copyright 2014 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.fullscreen;
6 
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.animation.ValueAnimator;
10 import android.app.Activity;
11 import android.view.Gravity;
12 import android.view.View;
13 import android.view.ViewGroup;
14 import android.widget.FrameLayout;
15 
16 import androidx.annotation.IntDef;
17 import androidx.annotation.Nullable;
18 import androidx.annotation.VisibleForTesting;
19 
20 import org.chromium.base.ActivityState;
21 import org.chromium.base.ApplicationStatus;
22 import org.chromium.base.ApplicationStatus.ActivityStateListener;
23 import org.chromium.base.ObserverList;
24 import org.chromium.base.supplier.ObservableSupplierImpl;
25 import org.chromium.base.task.PostTask;
26 import org.chromium.chrome.browser.ActivityTabProvider;
27 import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
28 import org.chromium.chrome.browser.app.ChromeActivity;
29 import org.chromium.chrome.browser.browser_controls.BrowserControlsSizer;
30 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
31 import org.chromium.chrome.browser.browser_controls.BrowserControlsUtils;
32 import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
33 import org.chromium.chrome.browser.tab.SadTab;
34 import org.chromium.chrome.browser.tab.Tab;
35 import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
36 import org.chromium.chrome.browser.tab.TabBrowserControlsOffsetHelper;
37 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
38 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
39 import org.chromium.chrome.browser.tabmodel.TabSwitchMetrics;
40 import org.chromium.chrome.browser.toolbar.ControlContainer;
41 import org.chromium.chrome.browser.vr.VrModuleProvider;
42 import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
43 import org.chromium.content_public.browser.UiThreadTaskTraits;
44 import org.chromium.content_public.common.BrowserControlsState;
45 import org.chromium.ui.util.TokenHolder;
46 import org.chromium.ui.vr.VrModeObserver;
47 
48 import java.lang.annotation.Retention;
49 import java.lang.annotation.RetentionPolicy;
50 
51 /**
52  * A class that manages browser control visibility and positioning.
53  */
54 public class BrowserControlsManager
55         implements ActivityStateListener, VrModeObserver, BrowserControlsSizer {
56     // The amount of time to delay the control show request after returning to a once visible
57     // activity.  This delay is meant to allow Android to run its Activity focusing animation and
58     // have the controls scroll back in smoothly once that has finished.
59     private static final long ACTIVITY_RETURN_SHOW_REQUEST_DELAY_MS = 100;
60 
61     /**
62      * Maximum duration for the control container slide-in animation and the duration for the
63      * browser controls height change animation. Note that this value matches the one in
64      * browser_controls_offset_manager.cc.
65      */
66     private static final int CONTROLS_ANIMATION_DURATION_MS = 200;
67 
68     private final Activity mActivity;
69     private final BrowserStateBrowserControlsVisibilityDelegate mBrowserVisibilityDelegate;
70     @ControlsPosition
71     private final int mControlsPosition;
72     private final TokenHolder mHidingTokenHolder = new TokenHolder(this::scheduleVisibilityUpdate);
73 
74     /**
75      * An observable for browser controls being at its minimum height or not.
76      * This is as good as the controls being hidden when both min heights are 0.
77      */
78     private final ObservableSupplierImpl<Boolean> mControlsAtMinHeight =
79             new ObservableSupplierImpl<>();
80 
81     private TabModelSelectorTabObserver mTabControlsObserver;
82     @Nullable
83     private ControlContainer mControlContainer;
84     private int mTopControlContainerHeight;
85     private int mTopControlsMinHeight;
86     private int mBottomControlContainerHeight;
87     private int mBottomControlsMinHeight;
88     private boolean mAnimateBrowserControlsHeightChanges;
89 
90     private int mRendererTopControlOffset;
91     private int mRendererBottomControlOffset;
92     private int mRendererTopContentOffset;
93     private int mRendererTopControlsMinHeightOffset;
94     private int mRendererBottomControlsMinHeightOffset;
95     private float mControlOffsetRatio;
96     private boolean mOffsetsChanged;
97     private ActivityTabTabObserver mActiveTabObserver;
98 
99     private final ObserverList<BrowserControlsStateProvider.Observer> mControlsObservers =
100             new ObserverList<>();
101     private FullscreenHtmlApiHandler mHtmlApiHandler;
102     @Nullable
103     private Tab mTab;
104 
105     /** The animator for the Android browser controls. */
106     private ValueAnimator mControlsAnimator;
107 
108     /**
109      * Indicates if control offset is in the overridden state by animation. Stays {@code true}
110      * from animation start till the next offset update from compositor arrives.
111      */
112     private boolean mOffsetOverridden;
113 
114     @IntDef({ControlsPosition.TOP, ControlsPosition.NONE})
115     @Retention(RetentionPolicy.SOURCE)
116     public @interface ControlsPosition {
117         /** Controls are at the top, eg normal ChromeTabbedActivity. */
118         int TOP = 0;
119         /** Controls are not present, eg NoTouchActivity. */
120         int NONE = 1;
121     }
122 
123     private final Runnable mUpdateVisibilityRunnable = new Runnable() {
124         @Override
125         public void run() {
126             int visibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE;
127             if (mControlContainer == null
128                     || mControlContainer.getView().getVisibility() == visibility) {
129                 return;
130             }
131             // requestLayout is required to trigger a new gatherTransparentRegion(), which
132             // only occurs together with a layout and let's SurfaceFlinger trim overlays.
133             // This may be almost equivalent to using View.GONE, but we still use View.INVISIBLE
134             // since drawing caches etc. won't be destroyed, and the layout may be less expensive.
135             mControlContainer.getView().setVisibility(visibility);
136             mControlContainer.getView().requestLayout();
137         }
138     };
139 
140     /**
141      * Creates an instance of the browser controls manager.
142      * @param activity The activity that supports browser controls.
143      * @param controlsPosition Where the browser controls are.
144      */
BrowserControlsManager(Activity activity, @ControlsPosition int controlsPosition)145     public BrowserControlsManager(Activity activity, @ControlsPosition int controlsPosition) {
146         this(activity, controlsPosition, true);
147     }
148 
149     /**
150      * Creates an instance of the browser controls manager.
151      * @param activity The activity that supports browser controls.
152      * @param controlsPosition Where the browser controls are.
153      * @param exitFullscreenOnStop Whether fullscreen mode should exit on stop - should be
154      *                             true for Activities that are not always fullscreen.
155      */
BrowserControlsManager(Activity activity, @ControlsPosition int controlsPosition, boolean exitFullscreenOnStop)156     public BrowserControlsManager(Activity activity, @ControlsPosition int controlsPosition,
157             boolean exitFullscreenOnStop) {
158         mActivity = activity;
159         mControlsPosition = controlsPosition;
160         mControlsAtMinHeight.set(false);
161         mHtmlApiHandler =
162                 new FullscreenHtmlApiHandler(activity, mControlsAtMinHeight, exitFullscreenOnStop);
163         mBrowserVisibilityDelegate = new BrowserStateBrowserControlsVisibilityDelegate(
164                 mHtmlApiHandler.getPersistentFullscreenModeSupplier());
165         mBrowserVisibilityDelegate.addObserver((constraints) -> {
166             if (constraints == BrowserControlsState.SHOWN) setPositionsForTabToNonFullscreen();
167         });
168         VrModuleProvider.registerVrModeObserver(this);
169         if (isInVr()) onEnterVr();
170     }
171 
172     /**
173      * Initializes the browser controls manager with the required dependencies.
174      *
175      * @param controlContainer Container holding the controls (Toolbar).
176      * @param activityTabProvider Provider of the current activity tab.
177      * @param modelSelector The tab model selector that will be monitored for tab changes.
178      * @param resControlContainerHeight The dimension resource ID for the control container height.
179      */
initialize(@ullable ControlContainer controlContainer, ActivityTabProvider activityTabProvider, final TabModelSelector modelSelector, int resControlContainerHeight)180     public void initialize(@Nullable ControlContainer controlContainer,
181             ActivityTabProvider activityTabProvider, final TabModelSelector modelSelector,
182             int resControlContainerHeight) {
183         mHtmlApiHandler.initialize(activityTabProvider, modelSelector);
184         ApplicationStatus.registerStateListenerForActivity(this, mActivity);
185         mActiveTabObserver = new ActivityTabTabObserver(activityTabProvider) {
186             @Override
187             protected void onObservingDifferentTab(Tab tab, boolean hint) {
188                 setTab(tab);
189             }
190         };
191 
192         mTabControlsObserver = new TabModelSelectorTabObserver(modelSelector) {
193             @Override
194             public void onInteractabilityChanged(Tab tab, boolean interactable) {
195                 if (!interactable || tab != getTab()) return;
196                 TabBrowserControlsOffsetHelper helper = TabBrowserControlsOffsetHelper.get(tab);
197                 if (!helper.offsetInitialized()) return;
198 
199                 onOffsetsChanged(helper.topControlsOffset(), helper.bottomControlsOffset(),
200                         helper.contentOffset(), helper.topControlsMinHeightOffset(),
201                         helper.bottomControlsMinHeightOffset());
202             }
203 
204             @Override
205             public void onCrash(Tab tab) {
206                 if (tab == getTab() && SadTab.isShowing(tab)) showAndroidControls(false);
207             }
208 
209             @Override
210             public void onRendererResponsiveStateChanged(Tab tab, boolean isResponsive) {
211                 if (tab == getTab() && !isResponsive) showAndroidControls(false);
212             }
213 
214             @Override
215             public void onBrowserControlsOffsetChanged(Tab tab, int topControlsOffset,
216                     int bottomControlsOffset, int contentOffset, int topControlsMinHeightOffset,
217                     int bottomControlsMinHeightOffset) {
218                 if (tab == getTab() && tab.isUserInteractable() && !tab.isNativePage()) {
219                     onOffsetsChanged(topControlsOffset, bottomControlsOffset, contentOffset,
220                             topControlsMinHeightOffset, bottomControlsMinHeightOffset);
221                 }
222             }
223         };
224         assert controlContainer != null || mControlsPosition == ControlsPosition.NONE;
225         mControlContainer = controlContainer;
226 
227         switch (mControlsPosition) {
228             case ControlsPosition.TOP:
229                 assert resControlContainerHeight != ChromeActivity.NO_CONTROL_CONTAINER;
230                 mTopControlContainerHeight =
231                         mActivity.getResources().getDimensionPixelSize(resControlContainerHeight);
232                 break;
233             case ControlsPosition.NONE:
234                 // Treat the case of no controls as controls always being totally offscreen.
235                 mControlOffsetRatio = 1.0f;
236                 break;
237         }
238 
239         mRendererTopContentOffset = mTopControlContainerHeight;
240         updateControlOffset();
241         scheduleVisibilityUpdate();
242     }
243 
244     /**
245      * @return {@link FullscreenManager} object.
246      */
getFullscreenManager()247     public FullscreenManager getFullscreenManager() {
248         return mHtmlApiHandler;
249     }
250 
251     @Override
getBrowserVisibilityDelegate()252     public BrowserStateBrowserControlsVisibilityDelegate getBrowserVisibilityDelegate() {
253         return mBrowserVisibilityDelegate;
254     }
255 
256     /**
257      * @return The currently selected tab for fullscreen.
258      */
259     @Nullable
260     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
getTab()261     public Tab getTab() {
262         return mTab;
263     }
264 
setTab(@ullable Tab tab)265     private void setTab(@Nullable Tab tab) {
266         Tab previousTab = getTab();
267         mTab = tab;
268         if (previousTab != tab) {
269             if (tab != null) {
270                 mBrowserVisibilityDelegate.showControlsTransient();
271                 if (tab.isUserInteractable()) restoreControlsPositions();
272             }
273         }
274 
275         if (tab == null && mBrowserVisibilityDelegate.get() != BrowserControlsState.HIDDEN) {
276             setPositionsForTabToNonFullscreen();
277         }
278     }
279 
280     // ActivityStateListener
281 
282     @Override
onActivityStateChange(Activity activity, int newState)283     public void onActivityStateChange(Activity activity, int newState) {
284         if (newState == ActivityState.STARTED) {
285             PostTask.postDelayedTask(UiThreadTaskTraits.DEFAULT,
286                     mBrowserVisibilityDelegate::showControlsTransient,
287                     ACTIVITY_RETURN_SHOW_REQUEST_DELAY_MS);
288         } else if (newState == ActivityState.DESTROYED) {
289             ApplicationStatus.unregisterActivityStateListener(this);
290         }
291     }
292 
293     @Override
getBrowserControlHiddenRatio()294     public float getBrowserControlHiddenRatio() {
295         return mControlOffsetRatio;
296     }
297 
298     /**
299      * @return True if the browser controls are showing as much as the min height. Note that this is
300      * the same as
301      * {@link BrowserControlsUtils#areBrowserControlsOffScreen(BrowserControlsStateProvider)} when
302      * both min-heights are 0.
303      */
304     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
areBrowserControlsAtMinHeight()305     public boolean areBrowserControlsAtMinHeight() {
306         return mControlsAtMinHeight.get();
307     }
308 
309     @Override
setBottomControlsHeight(int bottomControlsHeight, int bottomControlsMinHeight)310     public void setBottomControlsHeight(int bottomControlsHeight, int bottomControlsMinHeight) {
311         if (mBottomControlContainerHeight == bottomControlsHeight
312                 && mBottomControlsMinHeight == bottomControlsMinHeight) {
313             return;
314         }
315         mBottomControlContainerHeight = bottomControlsHeight;
316         mBottomControlsMinHeight = bottomControlsMinHeight;
317         for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
318             obs.onBottomControlsHeightChanged(
319                     mBottomControlContainerHeight, mBottomControlsMinHeight);
320         }
321     }
322 
323     @Override
setTopControlsHeight(int topControlsHeight, int topControlsMinHeight)324     public void setTopControlsHeight(int topControlsHeight, int topControlsMinHeight) {
325         if (mTopControlContainerHeight == topControlsHeight
326                 && mTopControlsMinHeight == topControlsMinHeight) {
327             return;
328         }
329 
330         final int oldTopHeight = mTopControlContainerHeight;
331         final int oldTopMinHeight = mTopControlsMinHeight;
332         mTopControlContainerHeight = topControlsHeight;
333         mTopControlsMinHeight = topControlsMinHeight;
334 
335         if (!canAnimateNativeBrowserControls()) {
336             if (shouldAnimateBrowserControlsHeightChanges()) {
337                 runBrowserDrivenTopControlsHeightChangeAnimation(oldTopHeight, oldTopMinHeight);
338             } else {
339                 showAndroidControls(false);
340             }
341         }
342 
343         for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
344             obs.onTopControlsHeightChanged(mTopControlContainerHeight, mTopControlsMinHeight);
345         }
346     }
347 
348     @Override
setAnimateBrowserControlsHeightChanges( boolean animateBrowserControlsHeightChanges)349     public void setAnimateBrowserControlsHeightChanges(
350             boolean animateBrowserControlsHeightChanges) {
351         mAnimateBrowserControlsHeightChanges = animateBrowserControlsHeightChanges;
352     }
353 
354     @Override
getTopControlsHeight()355     public int getTopControlsHeight() {
356         return mTopControlContainerHeight;
357     }
358 
359     @Override
getTopControlsMinHeight()360     public int getTopControlsMinHeight() {
361         return mTopControlsMinHeight;
362     }
363 
364     @Override
getBottomControlsHeight()365     public int getBottomControlsHeight() {
366         return mBottomControlContainerHeight;
367     }
368 
369     @Override
getBottomControlsMinHeight()370     public int getBottomControlsMinHeight() {
371         return mBottomControlsMinHeight;
372     }
373 
374     @Override
shouldAnimateBrowserControlsHeightChanges()375     public boolean shouldAnimateBrowserControlsHeightChanges() {
376         return mAnimateBrowserControlsHeightChanges;
377     }
378 
379     @Override
getContentOffset()380     public int getContentOffset() {
381         return mRendererTopContentOffset;
382     }
383 
384     @Override
getTopControlOffset()385     public int getTopControlOffset() {
386         return mRendererTopControlOffset;
387     }
388 
389     @Override
getTopControlsMinHeightOffset()390     public int getTopControlsMinHeightOffset() {
391         return mRendererTopControlsMinHeightOffset;
392     }
393 
getBottomContentOffset()394     private int getBottomContentOffset() {
395         return BrowserControlsUtils.getBottomContentOffset(this);
396     }
397 
398     @Override
getBottomControlOffset()399     public int getBottomControlOffset() {
400         // If the height is currently 0, the offset generated by the bottom controls should be too.
401         // TODO(crbug.com/103602): Send a offset update from the browser controls manager when the
402         // height changes to ensure correct offsets (removing the need for min()).
403         return Math.min(mRendererBottomControlOffset, mBottomControlContainerHeight);
404     }
405 
406     @Override
getBottomControlsMinHeightOffset()407     public int getBottomControlsMinHeightOffset() {
408         return mRendererBottomControlsMinHeightOffset;
409     }
410 
updateControlOffset()411     private void updateControlOffset() {
412         if (mControlsPosition == ControlsPosition.NONE) return;
413 
414         if (getTopControlsHeight() == 0) {
415             // Treat the case of 0 height as controls being totally offscreen.
416             mControlOffsetRatio = 1.0f;
417         } else {
418             mControlOffsetRatio =
419                     Math.abs((float) mRendererTopControlOffset / getTopControlsHeight());
420         }
421     }
422 
423     @Override
getTopVisibleContentOffset()424     public float getTopVisibleContentOffset() {
425         return getTopControlsHeight() + getTopControlOffset();
426     }
427 
428     @Override
addObserver(BrowserControlsStateProvider.Observer obs)429     public void addObserver(BrowserControlsStateProvider.Observer obs) {
430         mControlsObservers.addObserver(obs);
431     }
432 
433     @Override
removeObserver(BrowserControlsStateProvider.Observer obs)434     public void removeObserver(BrowserControlsStateProvider.Observer obs) {
435         mControlsObservers.removeObserver(obs);
436     }
437 
438     /**
439      * Utility routine for ensuring visibility updates are synchronized with
440      * animation, preventing message loop stalls due to untimely invalidation.
441      */
scheduleVisibilityUpdate()442     private void scheduleVisibilityUpdate() {
443         if (mControlContainer == null) {
444             return;
445         }
446         final int desiredVisibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE;
447         if (mControlContainer.getView().getVisibility() == desiredVisibility) return;
448         mControlContainer.getView().removeCallbacks(mUpdateVisibilityRunnable);
449         mControlContainer.getView().postOnAnimation(mUpdateVisibilityRunnable);
450     }
451 
452     /**
453      * Forces the Android controls to hide. While there are acquired tokens the browser controls
454      * Android view will always be hidden, otherwise they will show/hide based on position.
455      *
456      * NB: this only affects the Android controls. For controlling composited toolbar visibility,
457      * implement {@link BrowserControlsVisibilityDelegate#canShowBrowserControls()}.
458      */
hideAndroidControls()459     private int hideAndroidControls() {
460         return mHidingTokenHolder.acquireToken();
461     }
462 
463     @Override
hideAndroidControlsAndClearOldToken(int oldToken)464     public int hideAndroidControlsAndClearOldToken(int oldToken) {
465         int newToken = hideAndroidControls();
466         mHidingTokenHolder.releaseToken(oldToken);
467         return newToken;
468     }
469 
470     @Override
releaseAndroidControlsHidingToken(int token)471     public void releaseAndroidControlsHidingToken(int token) {
472         mHidingTokenHolder.releaseToken(token);
473     }
474 
shouldShowAndroidControls()475     private boolean shouldShowAndroidControls() {
476         if (mControlContainer == null) return false;
477         if (mHidingTokenHolder.hasTokens()) {
478             return false;
479         }
480         if (offsetOverridden()) return true;
481 
482         boolean showControls = !BrowserControlsUtils.drawControlsAsTexture(this);
483         ViewGroup contentView = mTab != null ? mTab.getContentView() : null;
484         if (contentView == null) return showControls;
485 
486         for (int i = 0; i < contentView.getChildCount(); i++) {
487             View child = contentView.getChildAt(i);
488             if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) continue;
489 
490             FrameLayout.LayoutParams layoutParams =
491                     (FrameLayout.LayoutParams) child.getLayoutParams();
492             if (Gravity.TOP == (layoutParams.gravity & Gravity.FILL_VERTICAL)) {
493                 showControls = true;
494                 break;
495             }
496         }
497 
498         return showControls;
499     }
500 
501     /**
502      * Updates the positions of the browser controls and content to the default non fullscreen
503      * values.
504      */
setPositionsForTabToNonFullscreen()505     private void setPositionsForTabToNonFullscreen() {
506         Tab tab = getTab();
507         if (tab == null || !tab.isInitialized()
508                 || TabBrowserControlsConstraintsHelper.getConstraints(tab)
509                         != BrowserControlsState.HIDDEN) {
510             setPositionsForTab(0, 0, getTopControlsHeight(), getTopControlsMinHeight(),
511                     getBottomControlsMinHeight());
512         } else {
513             // Tab isn't null and the BrowserControlsState is HIDDEN. In this case, set the offsets
514             // to values that will position the browser controls at the min-height.
515             setPositionsForTab(getTopControlsMinHeight() - getTopControlsHeight(),
516                     getBottomControlsHeight() - getBottomControlsMinHeight(),
517                     getTopControlsMinHeight(), getTopControlsMinHeight(),
518                     getBottomControlsMinHeight());
519         }
520     }
521 
522     /**
523      * Updates the positions of the browser controls and content based on the desired position of
524      * the current tab.
525      * @param topControlsOffset The Y offset of the top controls in px.
526      * @param bottomControlsOffset The Y offset of the bottom controls in px.
527      * @param topContentOffset The Y offset for the content in px.
528      * @param topControlsMinHeightOffset The Y offset for the top controls min-height in px.
529      * @param bottomControlsMinHeightOffset The Y offset for the bottom controls min-height in px.
530      */
setPositionsForTab(int topControlsOffset, int bottomControlsOffset, int topContentOffset, int topControlsMinHeightOffset, int bottomControlsMinHeightOffset)531     private void setPositionsForTab(int topControlsOffset, int bottomControlsOffset,
532             int topContentOffset, int topControlsMinHeightOffset,
533             int bottomControlsMinHeightOffset) {
534         // This min/max logic is here to handle changes in the browser controls height. For example,
535         // if we change either height to 0, the offsets of the controls should also be 0. This works
536         // assuming we get an event from the renderer after the browser control heights change.
537         int rendererTopControlOffset = Math.max(topControlsOffset, -getTopControlsHeight());
538         int rendererBottomControlOffset = Math.min(bottomControlsOffset, getBottomControlsHeight());
539 
540         int rendererTopContentOffset =
541                 Math.min(topContentOffset, rendererTopControlOffset + getTopControlsHeight());
542 
543         if (rendererTopControlOffset == mRendererTopControlOffset
544                 && rendererBottomControlOffset == mRendererBottomControlOffset
545                 && rendererTopContentOffset == mRendererTopContentOffset
546                 && topControlsMinHeightOffset == mRendererTopControlsMinHeightOffset
547                 && bottomControlsMinHeightOffset == mRendererBottomControlsMinHeightOffset) {
548             return;
549         }
550 
551         mRendererTopControlOffset = rendererTopControlOffset;
552         mRendererBottomControlOffset = rendererBottomControlOffset;
553         mRendererTopControlsMinHeightOffset = topControlsMinHeightOffset;
554         mRendererBottomControlsMinHeightOffset = bottomControlsMinHeightOffset;
555         mRendererTopContentOffset = rendererTopContentOffset;
556 
557         mControlsAtMinHeight.set(getContentOffset() == getTopControlsMinHeight()
558                 && getBottomContentOffset() == getBottomControlsMinHeight());
559         updateControlOffset();
560         notifyControlOffsetChanged();
561     }
562 
notifyControlOffsetChanged()563     private void notifyControlOffsetChanged() {
564         scheduleVisibilityUpdate();
565         if (shouldShowAndroidControls()) {
566             mControlContainer.getView().setTranslationY(getTopControlOffset());
567         }
568 
569         // Whether we need the compositor to draw again to update our animation.
570         // Should be |false| when the browser controls are only moved through the page
571         // scrolling.
572         boolean needsAnimate = shouldShowAndroidControls();
573         for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
574             obs.onControlsOffsetChanged(getTopControlOffset(), getTopControlsMinHeightOffset(),
575                     getBottomControlOffset(), getBottomControlsMinHeightOffset(), needsAnimate);
576         }
577     }
578 
579     /**
580      * Called when offset values related with fullscreen functionality has been changed by the
581      * compositor.
582      * @param topControlsOffsetY The Y offset of the top controls in physical pixels.
583      * @param bottomControlsOffsetY The Y offset of the bottom controls in physical pixels.
584      * @param contentOffsetY The Y offset of the content in physical pixels.
585      * @param topControlsMinHeightOffsetY The current offset of the top controls min-height.
586      * @param bottomControlsMinHeightOffsetY The current offset of the bottom controls min-height.
587      */
onOffsetsChanged(int topControlsOffsetY, int bottomControlsOffsetY, int contentOffsetY, int topControlsMinHeightOffsetY, int bottomControlsMinHeightOffsetY)588     private void onOffsetsChanged(int topControlsOffsetY, int bottomControlsOffsetY,
589             int contentOffsetY, int topControlsMinHeightOffsetY,
590             int bottomControlsMinHeightOffsetY) {
591         // Cancel any animation on the Android controls and let compositor drive the offset updates.
592         resetControlsOffsetOverridden();
593 
594         Tab tab = getTab();
595         if (SadTab.isShowing(tab) || tab.isNativePage()) {
596             showAndroidControls(false);
597         } else {
598             updateBrowserControlsOffsets(false, topControlsOffsetY, bottomControlsOffsetY,
599                     contentOffsetY, topControlsMinHeightOffsetY, bottomControlsMinHeightOffsetY);
600         }
601         TabSwitchMetrics.setActualTabSwitchLatencyMetricRequired();
602     }
603 
604     @Override
showAndroidControls(boolean animate)605     public void showAndroidControls(boolean animate) {
606         if (animate) {
607             runBrowserDrivenShowAnimation();
608         } else {
609             updateBrowserControlsOffsets(true, 0, 0, getTopControlsHeight(),
610                     getTopControlsMinHeight(), getBottomControlsMinHeight());
611         }
612     }
613 
614     /**
615      * Restores the controls positions to the cached positions of the active Tab.
616      */
restoreControlsPositions()617     private void restoreControlsPositions() {
618         resetControlsOffsetOverridden();
619 
620         // Make sure the dominant control offsets have been set.
621         Tab tab = getTab();
622         TabBrowserControlsOffsetHelper offsetHelper = null;
623         if (tab != null) offsetHelper = TabBrowserControlsOffsetHelper.get(tab);
624 
625         // Browser controls should always be shown on native pages and restoring offsets might cause
626         // the controls to get stuck in an invalid position.
627         if (offsetHelper != null && offsetHelper.offsetInitialized() && tab != null
628                 && !tab.isNativePage()) {
629             updateBrowserControlsOffsets(false, offsetHelper.topControlsOffset(),
630                     offsetHelper.bottomControlsOffset(), offsetHelper.contentOffset(),
631                     offsetHelper.topControlsMinHeightOffset(),
632                     offsetHelper.bottomControlsMinHeightOffset());
633         } else {
634             showAndroidControls(false);
635         }
636         TabBrowserControlsConstraintsHelper.updateEnabledState(tab);
637     }
638 
639     /**
640      * Helper method to update offsets and notify offset changes to observers if necessary.
641      */
updateBrowserControlsOffsets(boolean toNonFullscreen, int topControlsOffset, int bottomControlsOffset, int topContentOffset, int topControlsMinHeightOffset, int bottomControlsMinHeightOffset)642     private void updateBrowserControlsOffsets(boolean toNonFullscreen, int topControlsOffset,
643             int bottomControlsOffset, int topContentOffset, int topControlsMinHeightOffset,
644             int bottomControlsMinHeightOffset) {
645         if (isInVr()) {
646             rawTopContentOffsetChangedForVr();
647             // The dip scale of java UI and WebContents are different while in VR, leading to a
648             // mismatch in size in pixels when converting from dips. Since we hide the controls in
649             // VR anyways, just set the offsets to what they're supposed to be with the controls
650             // hidden.
651             // TODO(mthiesse): Should we instead just set the top controls height to be 0 while in
652             // VR?
653             topControlsOffset = -getTopControlsHeight();
654             bottomControlsOffset = getBottomControlsHeight();
655             topContentOffset = 0;
656             topControlsMinHeightOffset = 0;
657             bottomControlsMinHeightOffset = 0;
658             setPositionsForTab(topControlsOffset, bottomControlsOffset, topContentOffset,
659                     topControlsMinHeightOffset, bottomControlsMinHeightOffset);
660         } else if (toNonFullscreen) {
661             setPositionsForTabToNonFullscreen();
662         } else {
663             setPositionsForTab(topControlsOffset, bottomControlsOffset, topContentOffset,
664                     topControlsMinHeightOffset, bottomControlsMinHeightOffset);
665         }
666     }
667 
668     @Override
offsetOverridden()669     public boolean offsetOverridden() {
670         return mOffsetOverridden;
671     }
672 
673     /**
674      * Sets the flat indicating if browser control offset is overridden by animation.
675      * @param flag Boolean flag of the new offset overridden state.
676      */
setOffsetOverridden(boolean flag)677     private void setOffsetOverridden(boolean flag) {
678         mOffsetOverridden = flag;
679     }
680 
681     /**
682      * Helper method to cancel overridden offset on Android browser controls.
683      */
resetControlsOffsetOverridden()684     private void resetControlsOffsetOverridden() {
685         if (!offsetOverridden()) return;
686         if (mControlsAnimator != null) mControlsAnimator.cancel();
687         setOffsetOverridden(false);
688     }
689 
690     /**
691      * Helper method to run slide-in animations on the Android browser controls views.
692      */
runBrowserDrivenShowAnimation()693     private void runBrowserDrivenShowAnimation() {
694         if (mControlsAnimator != null) return;
695 
696         setOffsetOverridden(true);
697 
698         final float hiddenRatio = getBrowserControlHiddenRatio();
699         final int topControlHeight = getTopControlsHeight();
700         final int topControlOffset = getTopControlOffset();
701 
702         // Set animation start value to current renderer controls offset.
703         mControlsAnimator = ValueAnimator.ofInt(topControlOffset, 0);
704         mControlsAnimator.setDuration(
705                 (long) Math.abs(hiddenRatio * CONTROLS_ANIMATION_DURATION_MS));
706         mControlsAnimator.addListener(new AnimatorListenerAdapter() {
707             @Override
708             public void onAnimationEnd(Animator animation) {
709                 mControlsAnimator = null;
710             }
711 
712             @Override
713             public void onAnimationCancel(Animator animation) {
714                 updateBrowserControlsOffsets(false, 0, 0, topControlHeight,
715                         getTopControlsMinHeight(), getBottomControlsMinHeight());
716             }
717         });
718         mControlsAnimator.addUpdateListener((animator) -> {
719             updateBrowserControlsOffsets(false, (int) animator.getAnimatedValue(), 0,
720                     topControlHeight, getTopControlsMinHeight(), getBottomControlsMinHeight());
721         });
722         mControlsAnimator.start();
723     }
724 
runBrowserDrivenTopControlsHeightChangeAnimation( int oldTopControlsHeight, int oldTopControlsMinHeight)725     private void runBrowserDrivenTopControlsHeightChangeAnimation(
726             int oldTopControlsHeight, int oldTopControlsMinHeight) {
727         if (mControlsAnimator != null) return;
728         assert getContentOffset()
729                 == oldTopControlsHeight
730             : "Height change animations are implemented for fully shown controls only!";
731 
732         setOffsetOverridden(true);
733 
734         final int newTopControlsHeight = getTopControlsHeight();
735         final int newTopControlsMinHeight = getTopControlsMinHeight();
736 
737         mControlsAnimator = ValueAnimator.ofFloat(0.f, 1.f);
738         mControlsAnimator.addUpdateListener((animator) -> {
739             final float topControlsMinHeightOffset = oldTopControlsMinHeight
740                     + (float) animator.getAnimatedValue()
741                             * (newTopControlsMinHeight - oldTopControlsMinHeight);
742             final float topContentOffset = oldTopControlsHeight
743                     + (float) animator.getAnimatedValue()
744                             * (newTopControlsHeight - oldTopControlsHeight);
745             final float topControlsOffset = topContentOffset - newTopControlsHeight;
746 
747             updateBrowserControlsOffsets(false, (int) topControlsOffset, getBottomControlOffset(),
748                     (int) topContentOffset, (int) topControlsMinHeightOffset,
749                     getBottomControlsMinHeightOffset());
750         });
751         mControlsAnimator.setDuration(CONTROLS_ANIMATION_DURATION_MS);
752         mControlsAnimator.addListener(new AnimatorListenerAdapter() {
753             @Override
754             public void onAnimationEnd(Animator animation) {
755                 updateBrowserControlsOffsets(false, 0, 0, getTopControlsHeight(),
756                         getTopControlsMinHeight(), getBottomControlsMinHeight());
757                 mControlsAnimator = null;
758             }
759         });
760         mControlsAnimator.start();
761     }
762 
canAnimateNativeBrowserControls()763     private boolean canAnimateNativeBrowserControls() {
764         final Tab tab = getTab();
765         return tab != null && tab.isUserInteractable() && !tab.isNativePage();
766     }
767 
768     // VR-related methods to make this class test-friendly. These are overridden in unit tests.
769 
isInVr()770     protected boolean isInVr() {
771         return VrModuleProvider.getDelegate().isInVr();
772     }
773 
rawTopContentOffsetChangedForVr()774     protected void rawTopContentOffsetChangedForVr() {
775         // TODO(https://crbug.com/1055619): VR wants to wait until the controls are fully hidden, as
776         // otherwise there may be a brief race where the omnibox is rendered over the webcontents.
777         // However, something seems to be happening in the case where the browser is launched on the
778         // NTP, such that the top content offset is never set to 0. If we can figure out what that
779         // is, we should be passing the TopContentOffset into this method again.
780         VrModuleProvider.getDelegate().rawTopContentOffsetChanged(0);
781     }
782 
783     @Override
onEnterVr()784     public void onEnterVr() {
785         restoreControlsPositions();
786     }
787 
788     @Override
onExitVr()789     public void onExitVr() {
790         // Clear the VR-specific overrides for controls height.
791         restoreControlsPositions();
792 
793         // Show the Controls explicitly because under some situations, like when we're showing a
794         // Native Page, the renderer won't send any new offsets.
795         showAndroidControls(false);
796     }
797 
798     /**
799      * Destroys the BrowserControlsManager
800      */
destroy()801     public void destroy() {
802         mTab = null;
803         mHtmlApiHandler.destroy();
804         if (mActiveTabObserver != null) mActiveTabObserver.destroy();
805         mBrowserVisibilityDelegate.destroy();
806         if (mTabControlsObserver != null) mTabControlsObserver.destroy();
807         VrModuleProvider.unregisterVrModeObserver(this);
808     }
809 
810     @VisibleForTesting
getTabControlsObserverForTesting()811     public TabModelSelectorTabObserver getTabControlsObserverForTesting() {
812         return mTabControlsObserver;
813     }
814 
815     @VisibleForTesting
getControlsAnimatorForTesting()816     ValueAnimator getControlsAnimatorForTesting() {
817         return mControlsAnimator;
818     }
819 
820     @VisibleForTesting
getControlsAnimationDurationMsForTesting()821     int getControlsAnimationDurationMsForTesting() {
822         return CONTROLS_ANIMATION_DURATION_MS;
823     }
824 }
825