1 // Copyright 2015 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.compositor.bottombar; 6 7 import android.text.TextUtils; 8 import android.view.View; 9 import android.view.View.MeasureSpec; 10 import android.view.ViewGroup; 11 import android.view.ViewGroup.MarginLayoutParams; 12 13 import androidx.annotation.VisibleForTesting; 14 15 import org.chromium.base.annotations.CalledByNative; 16 import org.chromium.base.annotations.NativeMethods; 17 import org.chromium.chrome.browser.WebContentsFactory; 18 import org.chromium.chrome.browser.app.ChromeActivity; 19 import org.chromium.chrome.browser.content.ContentUtils; 20 import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager; 21 import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl; 22 import org.chromium.chrome.browser.tab.Tab; 23 import org.chromium.chrome.browser.version.ChromeVersionInfo; 24 import org.chromium.components.embedder_support.delegate.WebContentsDelegateAndroid; 25 import org.chromium.components.embedder_support.view.ContentView; 26 import org.chromium.components.external_intents.ExternalNavigationHandler; 27 import org.chromium.components.navigation_interception.InterceptNavigationDelegate; 28 import org.chromium.components.navigation_interception.NavigationParams; 29 import org.chromium.content_public.browser.LoadUrlParams; 30 import org.chromium.content_public.browser.NavigationHandle; 31 import org.chromium.content_public.browser.RenderCoordinates; 32 import org.chromium.content_public.browser.WebContents; 33 import org.chromium.content_public.browser.WebContentsObserver; 34 import org.chromium.content_public.common.ResourceRequestBody; 35 import org.chromium.ui.base.ViewAndroidDelegate; 36 37 /** 38 * Content container for an OverlayPanel. This class is responsible for the management of the 39 * WebContents displayed inside of a panel and exposes a simple API relevant to actions a 40 * panel has. 41 */ 42 public class OverlayPanelContent { 43 44 /** The WebContents that this panel will display. */ 45 private WebContents mWebContents; 46 47 /** The container view that this panel uses. */ 48 private ViewGroup mContainerView; 49 50 /** The pointer to the native version of this class. */ 51 private long mNativeOverlayPanelContentPtr; 52 53 /** Used for progress bar events. */ 54 private final WebContentsDelegateAndroid mWebContentsDelegate; 55 56 /** The activity that this content is contained in. */ 57 private ChromeActivity mActivity; 58 59 /** Observer used for tracking loading and navigation. */ 60 private WebContentsObserver mWebContentsObserver; 61 62 /** The URL that was directly loaded using the {@link #loadUrl(String)} method. */ 63 private String mLoadedUrl; 64 65 /** Whether the content has started loading a URL. */ 66 private boolean mDidStartLoadingUrl; 67 68 /** 69 * Whether we should reuse any existing WebContents instead of deleting and recreating. 70 * See crbug.com/682953 for details. 71 */ 72 private boolean mShouldReuseWebContents; 73 74 /** 75 * Whether the WebContents is processing a pending navigation. 76 * NOTE(pedrosimonetti): This is being used to prevent redirections on the SERP to be 77 * interpreted as a regular navigation, which should cause the Contextual Search Panel 78 * to be promoted as a Tab. This was added to work around a server bug that has been fixed. 79 * Just checking for whether the Content has been touched is enough to determine whether a 80 * navigation should be promoted (assuming it was caused by the touch), as done in 81 * {@link ContextualSearchManager#shouldPromoteSearchNavigation()}. 82 * For more details, see crbug.com/441048 83 * TODO(pedrosimonetti): remove this from M48 or move it to Contextual Search Panel. 84 */ 85 private boolean mIsProcessingPendingNavigation; 86 87 /** Whether the content view is currently being displayed. */ 88 private boolean mIsContentViewShowing; 89 90 /** The observer used by this object to inform implementers of different events. */ 91 private OverlayContentDelegate mContentDelegate; 92 93 /** Used to observe progress bar events. */ 94 private OverlayContentProgressObserver mProgressObserver; 95 96 /** If a URL is set to delayed load (load on user interaction), it will be stored here. */ 97 private String mPendingUrl; 98 99 // http://crbug.com/522266 : An instance of InterceptNavigationDelegateImpl should be kept in 100 // java layer. Otherwise, the instance could be garbage-collected unexpectedly. 101 private InterceptNavigationDelegate mInterceptNavigationDelegate; 102 103 /** Set to {@code True} if opened for an incognito tab. */ 104 private boolean mIsIncognito; 105 106 /** The desired size of the {@link ContentView} associated with this panel content. */ 107 private int mContentViewWidth; 108 private int mContentViewHeight; 109 private boolean mSubtractBarHeight; 110 111 /** The height of the bar at the top of the OverlayPanel in pixels. */ 112 private final int mBarHeightPx; 113 114 /** Sets the top offset of the overlay panel in pixel. 0 when fully expanded. */ 115 private int mPanelTopOffsetPx; 116 117 private class OverlayViewDelegate extends ViewAndroidDelegate { OverlayViewDelegate(ViewGroup v)118 public OverlayViewDelegate(ViewGroup v) { 119 super(v); 120 } 121 122 @Override setViewPosition(View view, float x, float y, float width, float height, int leftMargin, int topMargin)123 public void setViewPosition(View view, float x, float y, float width, float height, 124 int leftMargin, int topMargin) { 125 super.setViewPosition(view, x, y, width, height, leftMargin, topMargin); 126 127 // Applies top offset depending on the overlay panel state. 128 MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); 129 lp.topMargin += mPanelTopOffsetPx + mBarHeightPx; 130 } 131 } 132 133 // ============================================================================================ 134 // InterceptNavigationDelegateImpl 135 // ============================================================================================ 136 137 // Used to intercept intent navigations. 138 // TODO(jeremycho): Consider creating a Tab with the Panel's WebContents. 139 // which would also handle functionality like long-press-to-paste. 140 private class InterceptNavigationDelegateImpl implements InterceptNavigationDelegate { 141 final ExternalNavigationHandler mExternalNavHandler; 142 InterceptNavigationDelegateImpl()143 public InterceptNavigationDelegateImpl() { 144 Tab tab = mActivity.getActivityTab(); 145 mExternalNavHandler = (tab != null && tab.getWebContents() != null) 146 ? new ExternalNavigationHandler(new ExternalNavigationDelegateImpl(tab)) 147 : null; 148 } 149 150 @Override shouldIgnoreNavigation(NavigationParams navigationParams)151 public boolean shouldIgnoreNavigation(NavigationParams navigationParams) { 152 // If either of the required params for the delegate are null, do not call the 153 // delegate and ignore the navigation. 154 if (mExternalNavHandler == null || navigationParams == null) return true; 155 // TODO(mdjones): Rather than passing the two navigation params, instead consider 156 // passing a boolean to make this API simpler. 157 return !mContentDelegate.shouldInterceptNavigation(mExternalNavHandler, 158 navigationParams); 159 } 160 } 161 162 // ============================================================================================ 163 // Constructor 164 // ============================================================================================ 165 166 /** 167 * @param contentDelegate An observer for events that occur on this content. If null is passed 168 * for this parameter, the default one will be used. 169 * @param progressObserver An observer for progress related events. 170 * @param activity The ChromeActivity that contains this object. 171 * @param isIncognito {@True} if opened for an incognito tab 172 * @param barHeight The height of the bar at the top of the OverlayPanel in dp. 173 */ OverlayPanelContent(OverlayContentDelegate contentDelegate, OverlayContentProgressObserver progressObserver, ChromeActivity activity, boolean isIncognito, float barHeight)174 public OverlayPanelContent(OverlayContentDelegate contentDelegate, 175 OverlayContentProgressObserver progressObserver, ChromeActivity activity, 176 boolean isIncognito, float barHeight) { 177 mNativeOverlayPanelContentPtr = OverlayPanelContentJni.get().init(OverlayPanelContent.this); 178 mContentDelegate = contentDelegate; 179 mProgressObserver = progressObserver; 180 mActivity = activity; 181 mIsIncognito = isIncognito; 182 mBarHeightPx = (int) (barHeight * mActivity.getResources().getDisplayMetrics().density); 183 184 mWebContentsDelegate = new WebContentsDelegateAndroid() { 185 private boolean mIsFullscreen; 186 187 @Override 188 public void loadingStateChanged(boolean toDifferentDocument) { 189 boolean isLoading = mWebContents != null && mWebContents.isLoading(); 190 if (isLoading) { 191 mProgressObserver.onProgressBarStarted(); 192 } else { 193 mProgressObserver.onProgressBarFinished(); 194 } 195 } 196 197 @Override 198 public void visibleSSLStateChanged() { 199 mContentDelegate.onSSLStateUpdated(); 200 } 201 202 @Override 203 public void enterFullscreenModeForTab(boolean prefersNavigationBar) { 204 mIsFullscreen = true; 205 } 206 207 @Override 208 public void exitFullscreenModeForTab() { 209 mIsFullscreen = false; 210 } 211 212 @Override 213 public boolean isFullscreenForTabOrPending() { 214 return mIsFullscreen; 215 } 216 217 @Override 218 public void openNewTab(String url, String extraHeaders, ResourceRequestBody postData, 219 int disposition, boolean isRendererInitiated) { 220 mContentDelegate.onOpenNewTabRequested(url); 221 } 222 223 @Override 224 public boolean shouldCreateWebContents(String targetUrl) { 225 mContentDelegate.onOpenNewTabRequested(targetUrl); 226 return false; 227 } 228 229 @Override 230 public int getTopControlsHeight() { 231 return (int) (mBarHeightPx 232 / mActivity.getWindowAndroid().getDisplay().getDipScale()); 233 } 234 235 @Override 236 public int getBottomControlsHeight() { 237 return 0; 238 } 239 }; 240 } 241 242 // ============================================================================================ 243 // WebContents related 244 // ============================================================================================ 245 246 /** 247 * Load a URL; this will trigger creation of a new WebContents if being loaded immediately, 248 * otherwise one is created when the panel's content becomes visible. 249 * @param url The URL that should be loaded. 250 * @param shouldLoadImmediately If a URL should be loaded immediately or wait until visibility 251 * changes. 252 */ loadUrl(String url, boolean shouldLoadImmediately)253 public void loadUrl(String url, boolean shouldLoadImmediately) { 254 mPendingUrl = null; 255 256 if (!shouldLoadImmediately) { 257 mPendingUrl = url; 258 } else { 259 createNewWebContents(); 260 mLoadedUrl = url; 261 mDidStartLoadingUrl = true; 262 mIsProcessingPendingNavigation = true; 263 mWebContents.getNavigationController().loadUrl(new LoadUrlParams(url)); 264 } 265 } 266 267 /** 268 * Whether we should reuse any existing WebContents instead of deleting and recreating. 269 * @param reuse {@code true} if we want to reuse the WebContents. 270 */ setReuseWebContents(boolean reuse)271 public void setReuseWebContents(boolean reuse) { 272 mShouldReuseWebContents = reuse; 273 } 274 275 /** 276 * Call this when a loadUrl request has failed to notify the panel that the WebContents can 277 * be reused. See crbug.com/682953 for details. 278 */ onLoadUrlFailed()279 void onLoadUrlFailed() { 280 setReuseWebContents(true); 281 } 282 283 /** 284 * Set the desired size of the underlying {@link ContentView}. This is determined 285 * by the {@link OverlayPanel} before the creation of the content view. 286 * @param width The width of the content view. 287 * @param height The height of the content view. 288 * @param subtractBarHeight if {@code true} view height should be smaller by {@code mBarHeight}. 289 */ setContentViewSize(int width, int height, boolean subtractBarHeight)290 void setContentViewSize(int width, int height, boolean subtractBarHeight) { 291 mContentViewWidth = width; 292 mContentViewHeight = height; 293 mSubtractBarHeight = subtractBarHeight; 294 } 295 296 /** 297 * Makes the content visible, causing it to be rendered. 298 */ showContent()299 public void showContent() { 300 setVisibility(true); 301 } 302 303 /** 304 * Sets the top offset of the overlay panel that varies as the panel state changes. 305 * @param offset Top offset in pixel. 306 */ setPanelTopOffset(int offset)307 public void setPanelTopOffset(int offset) { 308 mPanelTopOffsetPx = offset; 309 } 310 311 /** 312 * Create a new WebContents that will be managed by this panel. 313 */ createNewWebContents()314 private void createNewWebContents() { 315 if (mWebContents != null) { 316 // If the WebContents has already been created, but never used, 317 // then there's no need to create a new one. 318 if (!mDidStartLoadingUrl || mShouldReuseWebContents) return; 319 320 destroyWebContents(); 321 } 322 323 // Creates an initially hidden WebContents which gets shown when the panel is opened. 324 mWebContents = WebContentsFactory.createWebContents(mIsIncognito, true); 325 326 ContentView cv = ContentView.createContentView( 327 mActivity, null /* eventOffsetHandler */, mWebContents); 328 if (mContentViewWidth != 0 || mContentViewHeight != 0) { 329 int width = mContentViewWidth == 0 ? ContentView.DEFAULT_MEASURE_SPEC 330 : MeasureSpec.makeMeasureSpec(mContentViewWidth, MeasureSpec.EXACTLY); 331 int height = mContentViewHeight == 0 ? ContentView.DEFAULT_MEASURE_SPEC 332 : MeasureSpec.makeMeasureSpec(mContentViewHeight, MeasureSpec.EXACTLY); 333 cv.setDesiredMeasureSpec(width, height); 334 } 335 336 OverlayViewDelegate delegate = new OverlayViewDelegate(cv); 337 mWebContents.initialize(ChromeVersionInfo.getProductVersion(), delegate, cv, 338 mActivity.getWindowAndroid(), WebContents.createDefaultInternalsHolder()); 339 ContentUtils.setUserAgentOverride(mWebContents); 340 341 // Transfers the ownership of the WebContents to the native OverlayPanelContent. 342 OverlayPanelContentJni.get().setWebContents(mNativeOverlayPanelContentPtr, 343 OverlayPanelContent.this, mWebContents, mWebContentsDelegate); 344 345 mWebContentsObserver = 346 new WebContentsObserver(mWebContents) { 347 @Override 348 public void didStartLoading(String url) { 349 mContentDelegate.onContentLoadStarted(url); 350 } 351 352 @Override 353 public void loadProgressChanged(float progress) { 354 mProgressObserver.onProgressBarUpdated(progress); 355 } 356 357 @Override 358 public void navigationEntryCommitted() { 359 mContentDelegate.onNavigationEntryCommitted(); 360 } 361 362 @Override 363 public void didStartNavigation(NavigationHandle navigation) { 364 if (navigation.isInMainFrame() && !navigation.isSameDocument()) { 365 String url = navigation.getUrlString(); 366 mContentDelegate.onMainFrameLoadStarted( 367 url, !TextUtils.equals(url, mLoadedUrl)); 368 } 369 } 370 371 @Override 372 public void titleWasSet(String title) { 373 mContentDelegate.onTitleUpdated(title); 374 } 375 376 @Override 377 public void didFinishNavigation(NavigationHandle navigation) { 378 if (navigation.hasCommitted() && navigation.isInMainFrame()) { 379 mIsProcessingPendingNavigation = false; 380 mContentDelegate.onMainFrameNavigation(navigation.getUrlString(), 381 !TextUtils.equals(navigation.getUrlString(), mLoadedUrl), 382 isHttpFailureCode(navigation.httpStatusCode()), 383 navigation.isErrorPage()); 384 } 385 } 386 }; 387 388 mContainerView = cv; 389 mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(); 390 OverlayPanelContentJni.get().setInterceptNavigationDelegate(mNativeOverlayPanelContentPtr, 391 OverlayPanelContent.this, mInterceptNavigationDelegate, mWebContents); 392 393 mContentDelegate.onContentViewCreated(); 394 resizePanelContentView(); 395 mActivity.getCompositorViewHolder().addView(mContainerView, 1); 396 } 397 398 /** 399 * Destroy this panel's WebContents. 400 */ destroyWebContents()401 private void destroyWebContents() { 402 if (mWebContents != null) { 403 mActivity.getCompositorViewHolder().removeView(mContainerView); 404 405 // Native destroy will call up to destroy the Java WebContents. 406 OverlayPanelContentJni.get().destroyWebContents( 407 mNativeOverlayPanelContentPtr, OverlayPanelContent.this); 408 mWebContents = null; 409 if (mWebContentsObserver != null) { 410 mWebContentsObserver.destroy(); 411 mWebContentsObserver = null; 412 } 413 414 mDidStartLoadingUrl = false; 415 mIsProcessingPendingNavigation = false; 416 mShouldReuseWebContents = false; 417 418 setVisibility(false); 419 } 420 } 421 422 // ============================================================================================ 423 // Utilities 424 // ============================================================================================ 425 426 /** 427 * Calls updateBrowserControlsState on the WebContents. 428 * @param areControlsHidden Whether the browser controls are hidden for the web contents. If 429 * false, the web contents viewport always accounts for the controls. 430 * Otherwise the web contents never accounts for them. 431 */ updateBrowserControlsState(boolean areControlsHidden)432 public void updateBrowserControlsState(boolean areControlsHidden) { 433 OverlayPanelContentJni.get().updateBrowserControlsState( 434 mNativeOverlayPanelContentPtr, OverlayPanelContent.this, areControlsHidden); 435 } 436 437 /** 438 * @return Whether a pending navigation if being processed. 439 */ isProcessingPendingNavigation()440 public boolean isProcessingPendingNavigation() { 441 return mIsProcessingPendingNavigation; 442 } 443 444 /** 445 * Reset the content's scroll position to (0, 0). 446 */ resetContentViewScroll()447 public void resetContentViewScroll() { 448 if (mWebContents != null) { 449 mWebContents.getEventForwarder().scrollTo(0, 0); 450 } 451 } 452 453 /** 454 * @return The Y scroll position. 455 */ getContentVerticalScroll()456 public float getContentVerticalScroll() { 457 return mWebContents != null 458 ? RenderCoordinates.fromWebContents(mWebContents).getScrollYPixInt() 459 : -1.f; 460 } 461 462 /** 463 * Sets the visibility of the Search Content View. 464 * @param isVisible True to make it visible. 465 */ setVisibility(boolean isVisible)466 private void setVisibility(boolean isVisible) { 467 if (mIsContentViewShowing == isVisible) return; 468 469 mIsContentViewShowing = isVisible; 470 471 if (isVisible) { 472 // If the last call to loadUrl was specified to be delayed, load it now. 473 if (!TextUtils.isEmpty(mPendingUrl)) loadUrl(mPendingUrl, true); 474 475 // The WebContents is created with the search request, but if none was made we'll need 476 // one in order to display an empty panel. 477 if (mWebContents == null) createNewWebContents(); 478 479 // NOTE(pedrosimonetti): Calling onShow() on the WebContents will cause the page 480 // to be rendered. This has a side effect of causing the page to be included in 481 // your Web History (if enabled). For this reason, onShow() should only be called 482 // when we know for sure the page will be seen by the user. 483 if (mWebContents != null) mWebContents.onShow(); 484 485 mContentDelegate.onContentViewSeen(); 486 } else { 487 if (mWebContents != null) mWebContents.onHide(); 488 } 489 490 mContentDelegate.onVisibilityChanged(isVisible); 491 } 492 493 /** 494 * @return Whether the given HTTP result code represents a failure or not. 495 */ isHttpFailureCode(int httpResultCode)496 private static boolean isHttpFailureCode(int httpResultCode) { 497 return httpResultCode <= 0 || httpResultCode >= 400; 498 } 499 500 /** 501 * @return true if the content is visible on the page. 502 */ isContentShowing()503 public boolean isContentShowing() { 504 return mIsContentViewShowing; 505 } 506 507 // ============================================================================================ 508 // Methods for managing this panel's WebContents. 509 // ============================================================================================ 510 511 /** 512 * Reset this object's native pointer to 0; 513 */ 514 @CalledByNative clearNativePanelContentPtr()515 private void clearNativePanelContentPtr() { 516 assert mNativeOverlayPanelContentPtr != 0; 517 mNativeOverlayPanelContentPtr = 0; 518 } 519 520 /** 521 * @return The associated {@link WebContents}. 522 */ getWebContents()523 public WebContents getWebContents() { 524 return mWebContents; 525 } 526 527 /** 528 * @return The associated {@link ContentView}. 529 */ getContainerView()530 public ViewGroup getContainerView() { 531 return mContainerView; 532 } 533 resizePanelContentView()534 void resizePanelContentView() { 535 WebContents webContents = getWebContents(); 536 if (webContents == null) return; 537 int viewHeight = mContentViewHeight - (mSubtractBarHeight ? mBarHeightPx : 0); 538 OverlayPanelContentJni.get().onPhysicalBackingSizeChanged(mNativeOverlayPanelContentPtr, 539 OverlayPanelContent.this, webContents, mContentViewWidth, viewHeight); 540 mWebContents.setSize(mContentViewWidth, viewHeight); 541 } 542 543 /** 544 * Remove the list history entry from this panel if it was within a certain timeframe. 545 * @param historyUrl The URL to remove. 546 * @param urlTimeMs The time the URL was navigated to. 547 */ removeLastHistoryEntry(String historyUrl, long urlTimeMs)548 public void removeLastHistoryEntry(String historyUrl, long urlTimeMs) { 549 OverlayPanelContentJni.get().removeLastHistoryEntry( 550 mNativeOverlayPanelContentPtr, OverlayPanelContent.this, historyUrl, urlTimeMs); 551 } 552 553 /** 554 * Destroy the native component of this class. 555 */ 556 @VisibleForTesting destroy()557 public void destroy() { 558 if (mWebContents != null) destroyWebContents(); 559 560 // Tests will not create the native pointer, so we need to check if it's not zero 561 // otherwise calling OverlayPanelContentJni.get().destroy with zero will make Chrome crash. 562 if (mNativeOverlayPanelContentPtr != 0L) { 563 OverlayPanelContentJni.get().destroy( 564 mNativeOverlayPanelContentPtr, OverlayPanelContent.this); 565 } 566 } 567 568 @NativeMethods 569 interface Natives { 570 // Native calls. init(OverlayPanelContent caller)571 long init(OverlayPanelContent caller); 572 destroy(long nativeOverlayPanelContent, OverlayPanelContent caller)573 void destroy(long nativeOverlayPanelContent, OverlayPanelContent caller); removeLastHistoryEntry(long nativeOverlayPanelContent, OverlayPanelContent caller, String historyUrl, long urlTimeMs)574 void removeLastHistoryEntry(long nativeOverlayPanelContent, OverlayPanelContent caller, 575 String historyUrl, long urlTimeMs); onPhysicalBackingSizeChanged(long nativeOverlayPanelContent, OverlayPanelContent caller, WebContents webContents, int width, int height)576 void onPhysicalBackingSizeChanged(long nativeOverlayPanelContent, 577 OverlayPanelContent caller, WebContents webContents, int width, int height); setWebContents(long nativeOverlayPanelContent, OverlayPanelContent caller, WebContents webContents, WebContentsDelegateAndroid delegate)578 void setWebContents(long nativeOverlayPanelContent, OverlayPanelContent caller, 579 WebContents webContents, WebContentsDelegateAndroid delegate); destroyWebContents(long nativeOverlayPanelContent, OverlayPanelContent caller)580 void destroyWebContents(long nativeOverlayPanelContent, OverlayPanelContent caller); setInterceptNavigationDelegate(long nativeOverlayPanelContent, OverlayPanelContent caller, InterceptNavigationDelegate delegate, WebContents webContents)581 void setInterceptNavigationDelegate(long nativeOverlayPanelContent, 582 OverlayPanelContent caller, InterceptNavigationDelegate delegate, 583 WebContents webContents); updateBrowserControlsState(long nativeOverlayPanelContent, OverlayPanelContent caller, boolean areControlsHidden)584 void updateBrowserControlsState(long nativeOverlayPanelContent, OverlayPanelContent caller, 585 boolean areControlsHidden); 586 } 587 } 588