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.app.Activity;
8 import android.content.Context;
9 import android.graphics.RectF;
10 import android.os.Handler;
11 import android.view.View;
12 import android.view.ViewGroup;
13 
14 import androidx.annotation.IntDef;
15 import androidx.annotation.VisibleForTesting;
16 
17 import org.chromium.base.ActivityState;
18 import org.chromium.base.ApplicationStatus;
19 import org.chromium.base.ApplicationStatus.ActivityStateListener;
20 import org.chromium.chrome.browser.app.ChromeActivity;
21 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager.PanelPriority;
22 import org.chromium.chrome.browser.compositor.layouts.Layout;
23 import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
24 import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
25 import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeHandler;
26 import org.chromium.chrome.browser.compositor.layouts.eventfilter.GestureHandler;
27 import org.chromium.chrome.browser.compositor.layouts.eventfilter.OverlayPanelEventFilter;
28 import org.chromium.chrome.browser.compositor.layouts.eventfilter.ScrollDirection;
29 import org.chromium.chrome.browser.layouts.EventFilter;
30 import org.chromium.chrome.browser.layouts.SceneOverlay;
31 import org.chromium.chrome.browser.layouts.components.VirtualView;
32 import org.chromium.chrome.browser.layouts.scene_layer.SceneOverlayLayer;
33 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
34 import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
35 import org.chromium.content_public.browser.SelectionPopupController;
36 import org.chromium.content_public.browser.WebContents;
37 import org.chromium.content_public.common.BrowserControlsState;
38 import org.chromium.ui.base.LocalizationUtils;
39 import org.chromium.ui.resources.ResourceManager;
40 
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.List;
44 
45 /**
46  * Controls the Overlay Panel.
47  */
48 public class OverlayPanel extends OverlayPanelAnimation implements ActivityStateListener,
49         EdgeSwipeHandler, GestureHandler, OverlayPanelContentFactory, SceneOverlay {
50 
51     /** The delay after which the hide progress will be hidden. */
52     private static final long HIDE_PROGRESS_BAR_DELAY_MS = 1000 / 60 * 4;
53 
54     /** State of the Overlay Panel. */
55     @IntDef({PanelState.UNDEFINED, PanelState.CLOSED, PanelState.PEEKED, PanelState.EXPANDED,
56             PanelState.MAXIMIZED})
57     @Retention(RetentionPolicy.SOURCE)
58     public @interface PanelState {
59         // Values can't have gaps and should be numerated from 0.
60         // Values CLOSED - MAXIMIZED are sorted and show next states.
61         // TODO(pedrosimonetti): consider removing the UNDEFINED state
62         int UNDEFINED = 0;
63         int CLOSED = 1;
64         int PEEKED = 2;
65         int EXPANDED = 3;
66         int MAXIMIZED = 4;
67         int NUM_ENTRIES = 5;
68     }
69 
70     /**
71      * The reason for a change in the Overlay Panel's state.
72      */
73     @IntDef({StateChangeReason.UNKNOWN, StateChangeReason.RESET, StateChangeReason.BACK_PRESS,
74             StateChangeReason.TEXT_SELECT_TAP, StateChangeReason.TEXT_SELECT_LONG_PRESS,
75             StateChangeReason.INVALID_SELECTION, StateChangeReason.CLEARED_SELECTION,
76             StateChangeReason.BASE_PAGE_TAP, StateChangeReason.BASE_PAGE_SCROLL,
77             StateChangeReason.SEARCH_BAR_TAP, StateChangeReason.SERP_NAVIGATION,
78             StateChangeReason.TAB_PROMOTION, StateChangeReason.CLICK, StateChangeReason.SWIPE,
79             StateChangeReason.FLING, StateChangeReason.OPTIN, StateChangeReason.OPTOUT,
80             StateChangeReason.CLOSE_BUTTON, StateChangeReason.PANEL_SUPPRESS,
81             StateChangeReason.PANEL_UNSUPPRESS, StateChangeReason.TAP_SUPPRESS,
82             StateChangeReason.NAVIGATION})
83     @Retention(RetentionPolicy.SOURCE)
84     public @interface StateChangeReason {
85         int UNKNOWN = 0;
86         int RESET = 1;
87         int BACK_PRESS = 2;
88         int TEXT_SELECT_TAP = 3;
89         int TEXT_SELECT_LONG_PRESS = 4;
90         int INVALID_SELECTION = 5;
91         int CLEARED_SELECTION = 6;
92         int BASE_PAGE_TAP = 7;
93         int BASE_PAGE_SCROLL = 8;
94         int SEARCH_BAR_TAP = 9;
95         int SERP_NAVIGATION = 10;
96         int TAB_PROMOTION = 11;
97         int CLICK = 12;
98         int SWIPE = 13;
99         int FLING = 14;
100         int OPTIN = 15;
101         int OPTOUT = 16;
102         int CLOSE_BUTTON = 17;
103         int PANEL_SUPPRESS = 18;
104         int PANEL_UNSUPPRESS = 19;
105         int TAP_SUPPRESS = 20;
106         int NAVIGATION = 21;
107         // Always update MAX_VALUE to match the last StateChangeReason in the list.
108         int MAX_VALUE = 21;
109     }
110 
111     /** A layout manager for tracking changes in the active layout. */
112     private final LayoutManagerImpl mLayoutManager;
113 
114     /** The observer that reacts to scene-change events. */
115     private final SceneChangeObserver mSceneChangeObserver;
116 
117     /** The activity this panel is in. */
118     protected ChromeActivity mActivity;
119 
120     /** The initial height of the Overlay Panel. */
121     private float mInitialPanelHeight;
122 
123     /** The initial location of a touch on the panel */
124     private float mInitialPanelTouchY;
125 
126     /** Whether a touch gesture has been detected. */
127     private boolean mHasDetectedTouchGesture;
128 
129     /** The EventFilter that this panel uses. */
130     protected EventFilter mEventFilter;
131 
132     /** That factory that creates OverlayPanelContents. */
133     private OverlayPanelContentFactory mContentFactory;
134 
135     /** Container for content the panel will show. */
136     private OverlayPanelContent mContent;
137 
138     /** OverlayPanel manager handle for notifications of opening and closing. */
139     protected OverlayPanelManager mPanelManager;
140 
141     /** If the base page text controls have been cleared. */
142     private boolean mDidClearTextControls;
143 
144     /** If the panel should be ignoring swipe events (for compatibility mode). */
145     private boolean mIgnoreSwipeEvents;
146 
147     /** This is used to make sure there is one show request to one close request. */
148     private boolean mPanelShown;
149 
150     /**
151      * Cache the viewport width and height of the screen to filter SceneOverlay#onSizeChanged
152      * events.
153      */
154     private float mViewportWidth;
155     private float mViewportHeight;
156 
157     // ============================================================================================
158     // Constructor
159     // ============================================================================================
160 
161     /**
162      * @param context The current Android {@link Context}.
163      * @param layoutManager A {@link LayoutManagerImpl} for observing changes in the active layout.
164      * @param panelManager The {@link OverlayPanelManager} responsible for showing panels.
165      */
OverlayPanel( Context context, LayoutManagerImpl layoutManager, OverlayPanelManager panelManager)166     public OverlayPanel(
167             Context context, LayoutManagerImpl layoutManager, OverlayPanelManager panelManager) {
168         super(context, layoutManager);
169         mLayoutManager = layoutManager;
170         mContentFactory = this;
171 
172         mPanelManager = panelManager;
173         mPanelManager.registerPanel(this);
174         mEventFilter = new OverlayPanelEventFilter(mContext, this);
175 
176         mSceneChangeObserver = new SceneChangeObserver() {
177             @Override
178             public void onTabSelectionHinted(int tabId) {}
179 
180             @Override
181             public void onSceneChange(Layout layout) {
182                 closePanel(StateChangeReason.UNKNOWN, false);
183             }
184         };
185         // mLayoutManager will be null in testing.
186         if (mLayoutManager != null) mLayoutManager.addSceneChangeObserver(mSceneChangeObserver);
187     }
188 
189     /**
190      * Destroy the native components associated with this panel's content.
191      */
destroy()192     public void destroy() {
193         closePanel(StateChangeReason.UNKNOWN, false);
194         if (mLayoutManager != null) mLayoutManager.removeSceneChangeObserver(mSceneChangeObserver);
195         ApplicationStatus.unregisterActivityStateListener(this);
196     }
197 
198     /**
199      * Destroy the components associated with this panel. This should be overridden by panel
200      * implementations to destroy text views and other elements.
201      */
destroyComponents()202     protected void destroyComponents() {
203         destroyOverlayPanelContent();
204     }
205 
206     @Override
onClosed(@tateChangeReason int reason)207     protected void onClosed(@StateChangeReason int reason) {
208         mPanelShown = false;
209         setBasePageTextControlsVisibility(true);
210         destroyComponents();
211         mPanelManager.notifyPanelClosed(this, reason);
212     }
213 
214     // ============================================================================================
215     // General API
216     // ============================================================================================
217 
218     /**
219      * @return True if the panel is in the PEEKED state.
220      */
isPeeking()221     public boolean isPeeking() {
222         return doesPanelHeightMatchState(PanelState.PEEKED);
223     }
224 
225     /**
226      * @return Whether the Panel is in its expanded state.
227      */
isExpanded()228     public boolean isExpanded() {
229         return doesPanelHeightMatchState(PanelState.EXPANDED);
230     }
231 
232     @Override
closePanel(@tateChangeReason int reason, boolean animate)233     public void closePanel(@StateChangeReason int reason, boolean animate) {
234         // If the panel hasn't peeked, then it shouldn't need to close.
235         if (!mPanelShown) return;
236 
237         super.closePanel(reason, animate);
238     }
239 
240     /**
241      * Request that this panel be shown.
242      * @param reason The reason the panel is being shown.
243      */
requestPanelShow(@tateChangeReason int reason)244     public void requestPanelShow(@StateChangeReason int reason) {
245         if (mPanelShown) return;
246 
247         if (mPanelManager != null) {
248             mPanelManager.requestPanelShow(this, reason);
249         }
250     }
251 
252     @Override
peekPanel(@tateChangeReason int reason)253     public void peekPanel(@StateChangeReason int reason) {
254         // TODO(mdjones): This is making a protected API public and should be removed. Animation
255         // should only be controlled by the OverlayPanelManager.
256 
257         // Since the OverlayPanelManager can show panels without requestPanelShow being called, the
258         // flag for the panel being shown should be set to true here.
259         mPanelShown = true;
260         super.peekPanel(reason);
261     }
262 
263     /**
264      * @param url The URL that the panel should load.
265      */
loadUrlInPanel(String url)266     public void loadUrlInPanel(String url) {
267         getOverlayPanelContent().loadUrl(url, true);
268     }
269 
270     /**
271      * @param url The URL that the panel should load.
272      * @param shouldLoadImmediately If the URL should be loaded immediately when this method is
273      *                              called.
274      */
loadUrlInPanel(String url, boolean shouldLoadImmediately)275     public void loadUrlInPanel(String url, boolean shouldLoadImmediately) {
276         getOverlayPanelContent().loadUrl(url, shouldLoadImmediately);
277     }
278 
279     /**
280      * @return True if a URL has been loaded in the panel's current WebContents.
281      */
isProcessingPendingNavigation()282     public boolean isProcessingPendingNavigation() {
283         return mContent != null && mContent.isProcessingPendingNavigation();
284     }
285 
286     /**
287      * @param activity The ChromeActivity associated with the panel.
288      */
setChromeActivity(ChromeActivity activity)289     public void setChromeActivity(ChromeActivity activity) {
290         mActivity = activity;
291         if (mActivity != null) {
292             ApplicationStatus.registerStateListenerForActivity(this, mActivity);
293         }
294 
295         initializeUiState();
296     }
297 
298     /**
299      * Notify the panel's content that it has been touched.
300      * @param x The X position of the touch in dp.
301      */
notifyBarTouched(float x)302     public void notifyBarTouched(float x) {
303         if (!isCoordinateInsideCloseButton(x)) {
304             getOverlayPanelContent().showContent();
305         }
306     }
307 
308     /**
309      * Acknowledges that there was a touch in the search content view, though no immediate action
310      * needs to be taken. This should be overridden by child classes.
311      * TODO(mdjones): Get a better name for this.
312      */
onTouchSearchContentViewAck()313     public void onTouchSearchContentViewAck() {
314     }
315 
316     /**
317      * Get a panel's display priority. This has a default to MEDIUM and should be overridden by
318      * child classes.
319      * @return The panel's display priority.
320      */
getPriority()321     public @PanelPriority int getPriority() {
322         return PanelPriority.MEDIUM;
323     }
324 
325     /**
326      * @return True if a panel can be suppressed. This should be overridden by each panel.
327      */
canBeSuppressed()328     public boolean canBeSuppressed() {
329         return false;
330     }
331 
332     /**
333      * @return The absolute amount in DP that the browser controls have shifted off screen.
334      */
getBrowserControlsOffsetDp()335     protected float getBrowserControlsOffsetDp() {
336         if (mActivity == null) return 0.0f;
337         return -mActivity.getBrowserControlsManager().getTopControlOffset() * mPxToDp;
338     }
339 
340     /**
341      * Set the visibility of the base page text selection controls. This will also attempt to
342      * remove focus from the base page to clear any open controls.
343      * @param visible If the text controls are visible.
344      */
setBasePageTextControlsVisibility(boolean visible)345     protected void setBasePageTextControlsVisibility(boolean visible) {
346         if (mActivity == null || mActivity.getActivityTab() == null) return;
347 
348         WebContents baseWebContents = mActivity.getActivityTab().getWebContents();
349         if (baseWebContents == null) return;
350 
351         // If the panel does not have focus or isn't open, return.
352         if (isPanelOpened() && mDidClearTextControls && !visible) return;
353         if (!isPanelOpened() && !mDidClearTextControls && visible) return;
354 
355         mDidClearTextControls = !visible;
356 
357         SelectionPopupController spc = SelectionPopupController.fromWebContents(baseWebContents);
358         if (!visible) spc.setPreserveSelectionOnNextLossOfFocus(true);
359         updateFocus(baseWebContents, visible);
360 
361         spc.updateTextSelectionUI(visible);
362     }
363 
364     // Claim or lose focus of the content view of the base WebContents. This keeps
365     // the state of the text selected for overlay in consistent way.
updateFocus(WebContents baseWebContents, boolean focusBaseView)366     private static void updateFocus(WebContents baseWebContents, boolean focusBaseView) {
367         View baseContentView = baseWebContents.getViewAndroidDelegate() != null
368                 ? baseWebContents.getViewAndroidDelegate().getContainerView()
369                 : null;
370         if (baseContentView == null) return;
371 
372         if (focusBaseView) {
373             baseContentView.requestFocus();
374         } else {
375             baseContentView.clearFocus();
376         }
377     }
378 
379     /**
380      * Returns whether the panel has been activated -- asked to show.  It may not yet be physically
381      * showing due animation.  Use {@link #isShowing} instead to determine if the panel is
382      * physically visible.
383      * @return Whether the panel is showing or about to show.
384      */
isActive()385     public boolean isActive() {
386         return mPanelShown;
387     }
388 
389     // ============================================================================================
390     // ActivityStateListener
391     // ============================================================================================
392 
393     @Override
onActivityStateChange(Activity activity, int newState)394     public void onActivityStateChange(Activity activity, int newState) {
395         boolean isMultiWindowMode = MultiWindowUtils.getInstance().isLegacyMultiWindow(mActivity)
396                 || MultiWindowUtils.getInstance().isInMultiWindowMode(mActivity);
397 
398         // In multi-window mode the activity that was interacted with last is resumed and
399         // all others are paused. We should not close Contextual Search in this case,
400         // because the activity may be visible even though it is paused.
401         if (isMultiWindowMode
402                 && (newState == ActivityState.PAUSED || newState == ActivityState.RESUMED)) {
403             return;
404         }
405 
406         if (newState == ActivityState.RESUMED
407                 || newState == ActivityState.STOPPED
408                 || newState == ActivityState.DESTROYED) {
409             closePanel(StateChangeReason.UNKNOWN, false);
410         }
411     }
412 
413     // ============================================================================================
414     // Content
415     // ============================================================================================
416 
417     /**
418      * @return True if the panel's content view is showing.
419      */
isContentShowing()420     public boolean isContentShowing() {
421         return mContent != null && mContent.isContentShowing();
422     }
423 
424     /**
425      * @return The WebContents that this panel currently holds.
426      */
getWebContents()427     public WebContents getWebContents() {
428         return mContent != null ? mContent.getWebContents() : null;
429     }
430 
431     /**
432      * @return The container view that this panel currently holds.
433      */
getContainerView()434     public ViewGroup getContainerView() {
435         return mContent != null ? mContent.getContainerView() : null;
436     }
437 
438     /**
439      * Call this when a loadUrl request has failed to notify the panel that the WebContents can
440      * be reused.  See crbug.com/682953 for details.
441      */
onLoadUrlFailed()442     public void onLoadUrlFailed() {
443         if (mContent != null) mContent.onLoadUrlFailed();
444     }
445 
446     /**
447      * Progress observer progress indicator animation for a panel.
448      */
449     public class PanelProgressObserver extends OverlayContentProgressObserver {
450         @Override
onProgressBarStarted()451         public void onProgressBarStarted() {
452             setProgressBarCompletion(0);
453             setProgressBarVisible(true);
454             requestUpdate();
455         }
456 
457         @Override
onProgressBarUpdated(float progress)458         public void onProgressBarUpdated(float progress) {
459             setProgressBarCompletion(progress);
460             requestUpdate();
461         }
462 
463         @Override
onProgressBarFinished()464         public void onProgressBarFinished() {
465             // Hides the Progress Bar after a delay to make sure it is rendered for at least
466             // a few frames, otherwise its completion won't be visually noticeable.
467             new Handler().postDelayed(new Runnable() {
468                 @Override
469                 public void run() {
470                     setProgressBarVisible(false);
471                     requestUpdate();
472                 }
473             }, HIDE_PROGRESS_BAR_DELAY_MS);
474         }
475     }
476 
477     /**
478      * Create a new OverlayPanelContent object. This can be overridden for tests.
479      * @return A new OverlayPanelContent object.
480      */
481     @Override
createNewOverlayPanelContent()482     public OverlayPanelContent createNewOverlayPanelContent() {
483         return new OverlayPanelContent(new OverlayContentDelegate(),
484                 new OverlayContentProgressObserver(), mActivity, /* isIncognito= */ false,
485                 getBarHeight());
486     }
487 
488     /**
489      * Add any other objects that depend on the OverlayPanelContent having already been created.
490      */
createNewOverlayPanelContentInternal()491     private OverlayPanelContent createNewOverlayPanelContentInternal() {
492         OverlayPanelContent content = mContentFactory.createNewOverlayPanelContent();
493         content.setContentViewSize(
494                 getContentViewWidthPx(), getContentViewHeightPx(), isFullWidthSizePanel());
495         return content;
496     }
497 
498     /**
499      * @return A new OverlayPanelContent if the instance was null or the existing one.
500      */
getOverlayPanelContent()501     protected OverlayPanelContent getOverlayPanelContent() {
502         // Only create the content when necessary
503         if (mContent == null) {
504             mContent = createNewOverlayPanelContentInternal();
505         }
506         return mContent;
507     }
508 
509     /**
510      * Destroy the native components of the OverlayPanelContent.
511      */
destroyOverlayPanelContent()512     protected void destroyOverlayPanelContent() {
513         // It is possible that an OverlayPanelContent was never created for this panel.
514         if (mContent != null) {
515             mContent.destroy();
516             mContent = null;
517         }
518     }
519 
520     /**
521      * Updates the browser controls state for the base tab.  As these values are set at the renderer
522      * level, there is potential for this impacting other tabs that might share the same
523      * process. See {@link BrowserControlsState.update(Tab, tab, int current, boolean animate)}
524      * @param current The desired current state for the controls.  Pass
525      *                {@link BrowserControlsState#BOTH} to preserve the current position.
526      * @param animate Whether the controls should animate to the specified ending condition or
527      *                should jump immediately.
528      */
updateBrowserControlsState(int current, boolean animate)529     public void updateBrowserControlsState(int current, boolean animate) {
530         TabBrowserControlsConstraintsHelper.update(mActivity.getActivityTab(), current, animate);
531     }
532 
533     /**
534      * Sets the top control state based on the internals of the panel.
535      */
updateBrowserControlsState()536     public void updateBrowserControlsState() {
537         if (mContent == null) return;
538 
539         // Consider the ContentView height to be fullscreen, and inform the system that
540         // the Toolbar is always visible (from the Compositor's perspective), even though
541         // the Toolbar and Base Page might be offset outside the screen. This means the
542         // renderer will consider the ContentView height to be the fullscreen height
543         // minus the Toolbar height.
544         //
545         // This is necessary to fix the bugs: crbug.com/510205 and crbug.com/510206
546         mContent.updateBrowserControlsState(isFullWidthSizePanel());
547     }
548 
549     /**
550      * Remove the last entry from history provided it is in a given time frame.
551      * @param historyUrl The URL to remove.
552      * @param urlTimeMs The time that the URL was visited.
553      */
removeLastHistoryEntry(String historyUrl, long urlTimeMs)554     public void removeLastHistoryEntry(String historyUrl, long urlTimeMs) {
555         if (mContent == null) return;
556         // Expose OverlayPanelContent method.
557         mContent.removeLastHistoryEntry(historyUrl, urlTimeMs);
558     }
559 
560     /**
561      * @return The vertical scroll position of the content.
562      */
getContentVerticalScroll()563     public float getContentVerticalScroll() {
564         return mContent != null ? mContent.getContentVerticalScroll() : 0.0f;
565     }
566 
567     // ============================================================================================
568     // OverlayPanelBase methods.
569     // ============================================================================================
570 
571     @Override
onHeightAnimationFinished()572     protected void onHeightAnimationFinished() {
573         super.onHeightAnimationFinished();
574 
575         if (getPanelState() == PanelState.PEEKED || getPanelState() == PanelState.CLOSED) {
576             setBasePageTextControlsVisibility(true);
577         } else {
578             setBasePageTextControlsVisibility(false);
579         }
580         if (mContent != null) {
581             mContent.setPanelTopOffset((int) ((mViewportHeight - getHeight()) / mPxToDp));
582         }
583     }
584 
585     @Override
getControlContainerHeightResource()586     protected int getControlContainerHeightResource() {
587         // TODO(mdjones): Investigate passing this in to the constructor instead.
588         assert mActivity != null;
589         return mActivity.getControlContainerHeightResource();
590     }
591 
592     // ============================================================================================
593     // Layout Integration
594     // ============================================================================================
595 
596     /**
597      * Updates the Panel so it preserves its state when the orientation changes.
598      */
updatePanelForOrientationChange()599     protected void updatePanelForOrientationChange() {
600         resizePanelToState(getPanelState(), StateChangeReason.UNKNOWN);
601     }
602 
603     // ============================================================================================
604     // Generic Event Handling
605     // ============================================================================================
606 
607     /**
608      * Handles the beginning of the swipe gesture.
609      */
handleSwipeStart()610     public void handleSwipeStart() {
611         cancelHeightAnimation();
612 
613         mHasDetectedTouchGesture = false;
614         mInitialPanelHeight = getHeight();
615     }
616 
617     /**
618      * Handles the movement of the swipe gesture.
619      *
620      * @param ty The movement's total displacement in dps.
621      */
handleSwipeMove(float ty)622     public void handleSwipeMove(float ty) {
623         if (mContent != null && ty > 0 && getPanelState() == PanelState.MAXIMIZED) {
624             // Resets the Content View scroll position when swiping the Panel down
625             // after being maximized.
626             mContent.resetContentViewScroll();
627         }
628 
629         // Negative ty value means an upward movement so subtracting ty means expanding the panel.
630         setClampedPanelHeight(mInitialPanelHeight - ty);
631         requestUpdate();
632     }
633 
634     /**
635      * Handles the end of the swipe gesture.
636      */
handleSwipeEnd()637     public void handleSwipeEnd() {
638         // This method will be called after handleFling() and handleClick()
639         // methods because we also need to track down the onUpOrCancel()
640         // action from the Layout. Therefore the animation to the nearest
641         // PanelState should only happen when no other gesture has been
642         // detected.
643         if (!mHasDetectedTouchGesture) {
644             mHasDetectedTouchGesture = true;
645             animateToNearestState();
646         }
647     }
648 
649     /**
650      * Handles the fling gesture.
651      *
652      * @param velocity The velocity of the gesture in dps per second.
653      */
handleFling(float velocity)654     public void handleFling(float velocity) {
655         mHasDetectedTouchGesture = true;
656         animateToProjectedState(velocity);
657     }
658 
659     /**
660      * @param x The x coordinate in dp.
661      * @return Whether the given |x| coordinate is inside the close button.
662      */
isCoordinateInsideCloseButton(float x)663     protected boolean isCoordinateInsideCloseButton(float x) {
664         if (LocalizationUtils.isLayoutRtl()) {
665             return x <= (getCloseIconX() + getCloseIconDimension() + mButtonPaddingDps);
666         } else {
667             return x >= (getCloseIconX() - mButtonPaddingDps);
668         }
669     }
670 
671     /**
672      * @param x The x coordinate in dp.
673      * @return Whether the given |x| coordinate is inside the open-in-new-tab button.
674      */
isCoordinateInsideOpenTabButton(float x)675     protected boolean isCoordinateInsideOpenTabButton(float x) {
676         // Calculation is the same for RTL: within the button plus padding.
677         return getOpenTabIconX() - mButtonPaddingDps <= x
678                 && x <= getOpenTabIconX() + getOpenTabIconDimension() + mButtonPaddingDps;
679     }
680 
681     /**
682      * Handles the click gesture.
683      *
684      * @param x The x coordinate of the gesture.
685      * @param y The y coordinate of the gesture.
686      */
handleClick(float x, float y)687     public void handleClick(float x, float y) {
688         mHasDetectedTouchGesture = true;
689         if (isCoordinateInsideBasePage(x, y)) {
690             closePanel(StateChangeReason.BASE_PAGE_TAP, true);
691         } else if (isCoordinateInsideBar(x, y) && !onInterceptBarClick()) {
692             handleBarClick(x, y);
693         }
694     }
695 
696     /**
697      * Handles the click gesture specifically on the bar.
698      *
699      * @param x The x coordinate of the gesture.
700      * @param y The y coordinate of the gesture.
701      */
handleBarClick(float x, float y)702     protected void handleBarClick(float x, float y) {
703         if (isPeeking()) {
704             expandPanel(StateChangeReason.SEARCH_BAR_TAP);
705         }
706     }
707 
708     /**
709      * Allows the click on the bar to be intercepted.
710      * @return True if the click on the bar was intercepted by this function.
711      */
onInterceptBarClick()712     protected boolean onInterceptBarClick() {
713         return false;
714     }
715 
716     /**
717      * If the panel is intercepting the initial bar swipe event. This should be overridden per
718      * panel.
719      * @return True if the panel intercepted the initial bar swipe.
720      */
onInterceptBarSwipe()721     public boolean onInterceptBarSwipe() {
722         return false;
723     }
724 
725     // ============================================================================================
726     // Gesture Event helpers
727     // ============================================================================================
728 
729     /**
730      * @param x The x coordinate in dp.
731      * @param y The y coordinate in dp.
732      * @return Whether the given coordinate is inside the bar area of the overlay.
733      */
isCoordinateInsideBar(float x, float y)734     public boolean isCoordinateInsideBar(float x, float y) {
735         return isCoordinateInsideOverlayPanel(x, y)
736                 && y >= getOffsetY() && y <= (getOffsetY() + getBarContainerHeight());
737     }
738 
739     /**
740      * @param x The x coordinate in dp.
741      * @param y The y coordinate in dp.
742      * @return Whether the given coordinate is inside the Overlay Content View area.
743      */
isCoordinateInsideContent(float x, float y)744     public boolean isCoordinateInsideContent(float x, float y) {
745         return isCoordinateInsideOverlayPanel(x, y)
746                 && y > getContentY();
747     }
748 
749     /**
750      * @return The horizontal offset of the Overlay Content View in dp.
751      */
getContentX()752     public float getContentX() {
753         return getOffsetX();
754     }
755 
756     /**
757      * @return The vertical offset of the Overlay Content View in dp.
758      */
getContentY()759     public float getContentY() {
760         return getOffsetY() + getBarContainerHeight();
761     }
762 
763     /**
764      * @param x The x coordinate in dp.
765      * @param y The y coordinate in dp.
766      * @return Whether the given coordinate is inside the Overlay Panel area.
767      */
isCoordinateInsideOverlayPanel(float x, float y)768     public boolean isCoordinateInsideOverlayPanel(float x, float y) {
769         return y >= getOffsetY() && y <= (getOffsetY() + getHeight())
770                 &&  x >= getOffsetX() && x <= (getOffsetX() + getWidth());
771     }
772 
773     /**
774      * @param x The x coordinate in dp.
775      * @param y The y coordinate in dp.
776      * @return Whether the given coordinate is inside the Base Page area.
777      */
isCoordinateInsideBasePage(float x, float y)778     private boolean isCoordinateInsideBasePage(float x, float y) {
779         return !isCoordinateInsideOverlayPanel(x, y);
780     }
781 
782     @VisibleForTesting
setOverlayPanelContentFactory(OverlayPanelContentFactory factory)783     public void setOverlayPanelContentFactory(OverlayPanelContentFactory factory) {
784         mContentFactory = factory;
785     }
786 
787     // ============================================================================================
788     // GestureHandler and EdgeSwipeHandler implementation.
789     // ============================================================================================
790 
791     @Override
onDown(float x, float y, boolean fromMouse, int buttons)792     public void onDown(float x, float y, boolean fromMouse, int buttons) {
793         mInitialPanelTouchY = y;
794         handleSwipeStart();
795     }
796 
797     @Override
drag(float x, float y, float deltaX, float deltaY, float tx, float ty)798     public void drag(float x, float y, float deltaX, float deltaY, float tx, float ty) {
799         handleSwipeMove(y - mInitialPanelTouchY);
800     }
801 
802     @Override
onUpOrCancel()803     public void onUpOrCancel() {
804         handleSwipeEnd();
805     }
806 
807     @Override
fling(float x, float y, float velocityX, float velocityY)808     public void fling(float x, float y, float velocityX, float velocityY) {
809         handleFling(velocityY);
810     }
811 
812     @Override
click(float x, float y, boolean fromMouse, int buttons)813     public void click(float x, float y, boolean fromMouse, int buttons) {
814         handleClick(x, y);
815     }
816 
817     @Override
onLongPress(float x, float y)818     public void onLongPress(float x, float y) {}
819 
820     @Override
onPinch(float x0, float y0, float x1, float y1, boolean firstEvent)821     public void onPinch(float x0, float y0, float x1, float y1, boolean firstEvent) {}
822 
823     // EdgeSwipeHandler implementation.
824 
825     @Override
swipeStarted(@crollDirection int direction, float x, float y)826     public void swipeStarted(@ScrollDirection int direction, float x, float y) {
827         if (onInterceptBarSwipe()) {
828             mIgnoreSwipeEvents = true;
829             return;
830         }
831         handleSwipeStart();
832     }
833 
834     @Override
swipeUpdated(float x, float y, float dx, float dy, float tx, float ty)835     public void swipeUpdated(float x, float y, float dx, float dy, float tx, float ty) {
836         if (mIgnoreSwipeEvents) return;
837         handleSwipeMove(ty);
838     }
839 
840     @Override
swipeFinished()841     public void swipeFinished() {
842         if (mIgnoreSwipeEvents) {
843             mIgnoreSwipeEvents = false;
844             return;
845         }
846         handleSwipeEnd();
847     }
848 
849     @Override
swipeFlingOccurred(float x, float y, float tx, float ty, float vx, float vy)850     public void swipeFlingOccurred(float x, float y, float tx, float ty, float vx, float vy) {
851         if (mIgnoreSwipeEvents) return;
852         handleFling(vy);
853     }
854 
855     @Override
isSwipeEnabled(@crollDirection int direction)856     public boolean isSwipeEnabled(@ScrollDirection int direction) {
857         return direction == ScrollDirection.UP && isShowing();
858     }
859 
860     // Other event handlers.
861 
862     /**
863      * The user has performed a down event and has not performed a move or up yet. This event is
864      * commonly used to provide visual feedback to the user to let them know that their action has
865      * been recognized.
866      * See {@link GestureDetector.SimpleOnGestureListener#onShowPress()}.
867      * @param x The x coordinate in dp.
868      * @param y The y coordinate in dp.
869      */
onShowPress(float x, float y)870     public void onShowPress(float x, float y) {}
871 
872     // ============================================================================================
873     // SceneOverlay implementation.
874     // ============================================================================================
875 
876     @Override
getUpdatedSceneOverlayTree( RectF viewport, RectF visibleViewport, ResourceManager resourceManager, float yOffset)877     public SceneOverlayLayer getUpdatedSceneOverlayTree(
878             RectF viewport, RectF visibleViewport, ResourceManager resourceManager, float yOffset) {
879         return null;
880     }
881 
882     @Override
isSceneOverlayTreeShowing()883     public boolean isSceneOverlayTreeShowing() {
884         return isShowing();
885     }
886 
887     @Override
getEventFilter()888     public EventFilter getEventFilter() {
889         return mEventFilter;
890     }
891 
892     @Override
onSizeChanged(float width, float height, float visibleViewportOffsetY, int orientation)893     public void onSizeChanged(float width, float height, float visibleViewportOffsetY,
894             int orientation) {
895         // Filter events that don't change the viewport width or height.
896         if (height != mViewportHeight || width != mViewportWidth) {
897             // We only care if the orientation is changing or we're shifting in/out of multi-window.
898             // In either case the screen's viewport width or height will certainly change.
899             mViewportWidth = width;
900             mViewportHeight = height;
901 
902             onLayoutChanged(width, height, visibleViewportOffsetY);
903             resizePanelContentView();
904         }
905     }
906 
907     /**
908      * Resize the panel's ContentView. Apply adjusted bar size to the height.
909      */
resizePanelContentView()910     protected void resizePanelContentView() {
911         if (!isShowing()) return;
912 
913         OverlayPanelContent panelContent = getOverlayPanelContent();
914 
915         // Device could have been rotated before panel webcontent creation. Update content size.
916         panelContent.setContentViewSize(
917                 getContentViewWidthPx(), getContentViewHeightPx(), isFullWidthSizePanel());
918         panelContent.resizePanelContentView();
919     }
920 
921     @Override
getVirtualViews(List<VirtualView> views)922     public void getVirtualViews(List<VirtualView> views) {
923         // TODO(mdjones): Add views for accessibility.
924     }
925 
926     @Override
handlesTabCreating()927     public boolean handlesTabCreating() {
928         // If the panel is not opened, do not handle tab creating.
929         if (!isPanelOpened()) return false;
930         // Updates BrowserControls' State so the Toolbar becomes visible.
931         // TODO(pedrosimonetti): The transition when promoting to a new tab is only smooth
932         // if the SearchContentView's vertical scroll position is zero. Otherwise the
933         // ContentView will appear to jump in the screen. Coordinate with @dtrainor to solve
934         // this problem.
935         updateBrowserControlsState(BrowserControlsState.BOTH, false);
936         return true;
937     }
938 
939     @Override
shouldHideAndroidBrowserControls()940     public boolean shouldHideAndroidBrowserControls() {
941         return isPanelOpened();
942     }
943 
944     @Override
updateOverlay(long time, long dt)945     public boolean updateOverlay(long time, long dt) {
946         if (isPanelOpened()) setBasePageTextControlsVisibility(false);
947         return true;
948     }
949 
950     @Override
onBackPressed()951     public boolean onBackPressed() {
952         if (!isShowing()) return false;
953         closePanel(StateChangeReason.BACK_PRESS, true);
954         return true;
955     }
956 }
957