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