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