1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * vim: ts=4 sw=4 expandtab:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 package org.mozilla.geckoview;
8 
9 import org.mozilla.gecko.AndroidGamepadManager;
10 import org.mozilla.gecko.EventDispatcher;
11 import org.mozilla.gecko.InputMethods;
12 import org.mozilla.gecko.SurfaceViewWrapper;
13 import org.mozilla.gecko.util.ThreadUtils;
14 
15 import android.annotation.SuppressLint;
16 import android.annotation.TargetApi;
17 import android.app.Activity;
18 import android.content.Context;
19 import android.content.ContextWrapper;
20 import android.content.res.Configuration;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Matrix;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.graphics.Region;
28 import android.os.Build;
29 import android.os.Handler;
30 import androidx.annotation.AnyThread;
31 import androidx.annotation.IntDef;
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.UiThread;
35 import androidx.core.view.ViewCompat;
36 import android.util.AttributeSet;
37 import android.util.DisplayMetrics;
38 import android.util.Log;
39 import android.util.SparseArray;
40 import android.util.TypedValue;
41 import android.view.DisplayCutout;
42 import android.view.KeyEvent;
43 import android.view.MotionEvent;
44 import android.view.Surface;
45 import android.view.SurfaceView;
46 import android.view.TextureView;
47 import android.view.View;
48 import android.view.ViewGroup;
49 import android.view.ViewStructure;
50 import android.view.autofill.AutofillManager;
51 import android.view.autofill.AutofillValue;
52 import android.view.inputmethod.EditorInfo;
53 import android.view.inputmethod.InputConnection;
54 import android.view.inputmethod.InputMethodManager;
55 import android.widget.FrameLayout;
56 
57 import java.lang.annotation.Retention;
58 import java.lang.annotation.RetentionPolicy;
59 
60 @UiThread
61 public class GeckoView extends FrameLayout {
62     private static final String LOGTAG = "GeckoView";
63     private static final boolean DEBUG = false;
64 
65     protected final @NonNull Display mDisplay = new Display();
66 
67     private Integer mLastCoverColor;
68     protected @Nullable GeckoSession mSession;
69     private boolean mStateSaved;
70 
71     private @Nullable SurfaceViewWrapper mSurfaceWrapper;
72 
73     private boolean mIsResettingFocus;
74 
75     private boolean mAutofillEnabled = true;
76 
77     private GeckoSession.SelectionActionDelegate mSelectionActionDelegate;
78     private Autofill.Delegate mAutofillDelegate;
79 
80     private class Display implements SurfaceViewWrapper.Listener {
81         private final int[] mOrigin = new int[2];
82 
83         private GeckoDisplay mDisplay;
84         private boolean mValid;
85 
86         private int mClippingHeight;
87         private int mDynamicToolbarMaxHeight;
88 
acquire(final GeckoDisplay display)89         public void acquire(final GeckoDisplay display) {
90             mDisplay = display;
91 
92             if (!mValid) {
93                 return;
94             }
95 
96             setVerticalClipping(mClippingHeight);
97 
98             // Tell display there is already a surface.
99             onGlobalLayout();
100             if (GeckoView.this.mSurfaceWrapper != null) {
101                 final SurfaceViewWrapper wrapper = GeckoView.this.mSurfaceWrapper;
102                 mDisplay.surfaceChanged(wrapper.getSurface(),
103                         wrapper.getWidth(), wrapper.getHeight());
104                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
105                 GeckoView.this.setActive(true);
106             }
107         }
108 
release()109         public GeckoDisplay release() {
110             if (mValid) {
111                 if (mDisplay != null) {
112                     mDisplay.surfaceDestroyed();
113                 }
114                 GeckoView.this.setActive(false);
115             }
116 
117             final GeckoDisplay display = mDisplay;
118             mDisplay = null;
119             return display;
120         }
121 
122         @Override // SurfaceListener
onSurfaceChanged(final Surface surface, final int width, final int height)123         public void onSurfaceChanged(final Surface surface,
124                                    final int width, final int height) {
125             if (mDisplay != null) {
126                 mDisplay.surfaceChanged(surface, width, height);
127                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
128                 if (!mValid) {
129                     GeckoView.this.setActive(true);
130                 }
131             }
132             mValid = true;
133         }
134 
135         @Override // SurfaceListener
onSurfaceDestroyed()136         public void onSurfaceDestroyed() {
137             if (mDisplay != null) {
138                 mDisplay.surfaceDestroyed();
139                 GeckoView.this.setActive(false);
140             }
141             mValid = false;
142         }
143 
onGlobalLayout()144         public void onGlobalLayout() {
145             if (mDisplay == null) {
146                 return;
147             }
148             if (GeckoView.this.mSurfaceWrapper != null) {
149                 GeckoView.this.mSurfaceWrapper.getView().getLocationOnScreen(mOrigin);
150                 mDisplay.screenOriginChanged(mOrigin[0], mOrigin[1]);
151                 // cutout support
152                 if (Build.VERSION.SDK_INT >= 28) {
153                     final DisplayCutout cutout = GeckoView.this.mSurfaceWrapper.getView().getRootWindowInsets().getDisplayCutout();
154                     if (cutout != null) {
155                         mDisplay.safeAreaInsetsChanged(cutout.getSafeInsetTop(), cutout.getSafeInsetRight(), cutout.getSafeInsetBottom(), cutout.getSafeInsetLeft());
156                     }
157                 }
158             }
159         }
160 
shouldPinOnScreen()161         public boolean shouldPinOnScreen() {
162             return mDisplay != null ? mDisplay.shouldPinOnScreen() : false;
163         }
164 
setVerticalClipping(final int clippingHeight)165         public void setVerticalClipping(final int clippingHeight) {
166             mClippingHeight = clippingHeight;
167 
168             if (mDisplay != null) {
169                 mDisplay.setVerticalClipping(clippingHeight);
170             }
171         }
172 
setDynamicToolbarMaxHeight(final int height)173         public void setDynamicToolbarMaxHeight(final int height) {
174             mDynamicToolbarMaxHeight = height;
175 
176             // Reset the vertical clipping value to zero whenever we change
177             // the dynamic toolbar __max__ height so that it can be properly
178             // propagated to both the main thread and the compositor thread,
179             // thus we will be able to reset the __current__ toolbar height
180             // on the both threads whatever the __current__ toolbar height is.
181             setVerticalClipping(0);
182 
183             if (mDisplay != null) {
184                 mDisplay.setDynamicToolbarMaxHeight(height);
185             }
186         }
187 
188         /**
189          * Request a {@link Bitmap} of the visible portion of the web page currently being
190          * rendered.
191          *
192          * @return A {@link GeckoResult} that completes with a {@link Bitmap} containing
193          * the pixels and size information of the currently visible rendered web page.
194          */
195         @UiThread
capturePixels()196         @NonNull GeckoResult<Bitmap> capturePixels() {
197             if (mDisplay == null) {
198                 return GeckoResult.fromException(new IllegalStateException("Display must be created before pixels can be captured"));
199             }
200 
201             return mDisplay.capturePixels();
202         }
203     }
204 
205     @SuppressWarnings("checkstyle:javadocmethod")
GeckoView(final Context context)206     public GeckoView(final Context context) {
207         super(context);
208         init();
209     }
210 
211     @SuppressWarnings("checkstyle:javadocmethod")
GeckoView(final Context context, final AttributeSet attrs)212     public GeckoView(final Context context, final AttributeSet attrs) {
213         super(context, attrs);
214         init();
215     }
216 
getActivityFromContext(final Context outerContext)217     private static Activity getActivityFromContext(final Context outerContext) {
218         Context context = outerContext;
219         while (context instanceof ContextWrapper) {
220             if (context instanceof Activity) {
221                 return (Activity) context;
222             }
223             context = ((ContextWrapper) context).getBaseContext();
224         }
225         return null;
226     }
227 
init()228     private void init() {
229         setFocusable(true);
230         setFocusableInTouchMode(true);
231         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
232 
233         // We are adding descendants to this LayerView, but we don't want the
234         // descendants to affect the way LayerView retains its focus.
235         setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
236 
237         // This will stop PropertyAnimator from creating a drawing cache (i.e. a
238         // bitmap) from a SurfaceView, which is just not possible (the bitmap will be
239         // transparent).
240         setWillNotCacheDrawing(false);
241 
242         mSurfaceWrapper = new SurfaceViewWrapper(getContext());
243         mSurfaceWrapper.setBackgroundColor(Color.WHITE);
244         addView(mSurfaceWrapper.getView(),
245                 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
246                                            ViewGroup.LayoutParams.MATCH_PARENT));
247 
248         mSurfaceWrapper.setListener(mDisplay);
249 
250         final Activity activity = getActivityFromContext(getContext());
251         if (activity != null) {
252             mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
253         }
254 
255         mAutofillDelegate = new AndroidAutofillDelegate();
256     }
257 
258     /**
259      * Set a color to cover the display surface while a document is being shown. The color
260      * is automatically cleared once the new document starts painting.
261      *
262      * @param color Cover color.
263      */
coverUntilFirstPaint(final int color)264     public void coverUntilFirstPaint(final int color) {
265         mLastCoverColor = color;
266         if (mSession != null) {
267             mSession.getCompositorController().setClearColor(color);
268         }
269         coverUntilFirstPaintInternal(color);
270     }
271 
uncover()272     private void uncover() {
273         coverUntilFirstPaintInternal(Color.TRANSPARENT);
274     }
275 
coverUntilFirstPaintInternal(final int color)276     private void coverUntilFirstPaintInternal(final int color) {
277         ThreadUtils.assertOnUiThread();
278 
279         if (mSurfaceWrapper != null) {
280             mSurfaceWrapper.setBackgroundColor(color);
281         }
282     }
283 
284     /**
285      * This GeckoView instance will be backed by a {@link SurfaceView}.
286      *
287      * This option offers the best performance at the price of not being
288      * able to animate GeckoView.
289      */
290     public static final int BACKEND_SURFACE_VIEW = 1;
291     /**
292      * This GeckoView instance will be backed by a {@link TextureView}.
293      *
294      * This option offers worse performance compared to {@link #BACKEND_SURFACE_VIEW}
295      * but allows you to animate GeckoView or to paint a GeckoView on top of another GeckoView.
296      */
297     public static final int BACKEND_TEXTURE_VIEW = 2;
298 
299     @Retention(RetentionPolicy.SOURCE)
300     @IntDef({BACKEND_SURFACE_VIEW, BACKEND_TEXTURE_VIEW})
301     /* protected */ @interface ViewBackend {}
302 
303     /**
304      * Set which view should be used by this GeckoView instance to display content.
305      *
306      * By default, GeckoView will use a {@link SurfaceView}.
307      *
308      * @param backend Any of {@link #BACKEND_SURFACE_VIEW BACKEND_*}.
309      */
setViewBackend(final @ViewBackend int backend)310     public void setViewBackend(final @ViewBackend int backend) {
311         removeView(mSurfaceWrapper.getView());
312 
313         if (backend == BACKEND_SURFACE_VIEW) {
314             mSurfaceWrapper.useSurfaceView(getContext());
315         } else if (backend == BACKEND_TEXTURE_VIEW) {
316             mSurfaceWrapper.useTextureView(getContext());
317         }
318 
319         addView(mSurfaceWrapper.getView());
320     }
321 
322     /**
323      * Return whether the view should be pinned on the screen. When pinned, the view
324      * should not be moved on the screen due to animation, scrolling, etc. A common reason
325      * for the view being pinned is when the user is dragging a selection caret inside
326      * the view; normal user interaction would be disrupted in that case if the view
327      * was moved on screen.
328      *
329      * @return True if view should be pinned on the screen.
330      */
shouldPinOnScreen()331     public boolean shouldPinOnScreen() {
332         ThreadUtils.assertOnUiThread();
333 
334         return mDisplay.shouldPinOnScreen();
335     }
336 
337     /**
338      * Update the amount of vertical space that is clipped or visibly obscured in the bottom portion
339      * of the view. Tells gecko where to put bottom fixed elements so they are fully visible.
340      *
341      * Optional call. The display's visible vertical space has changed. Must be
342      * called on the application main thread.
343      *
344      * @param clippingHeight The height of the bottom clipped space in screen pixels.
345      */
setVerticalClipping(final int clippingHeight)346     public void setVerticalClipping(final int clippingHeight) {
347         ThreadUtils.assertOnUiThread();
348 
349         mDisplay.setVerticalClipping(clippingHeight);
350     }
351 
352     /**
353      * Set the maximum height of the dynamic toolbar(s).
354      *
355      * If there are two or more dynamic toolbars, the height value should be the total amount of
356      * the height of each dynamic toolbar.
357      *
358      * @param height The the maximum height of the dynamic toolbar(s).
359      */
setDynamicToolbarMaxHeight(final int height)360     public void setDynamicToolbarMaxHeight(final int height) {
361         mDisplay.setDynamicToolbarMaxHeight(height);
362     }
363 
setActive(final boolean active)364     /* package */ void setActive(final boolean active) {
365         if (mSession != null) {
366             mSession.setActive(active);
367         }
368     }
369 
370     // TODO: Bug 1670805 this should really be configurable
371     // Default dark color for about:blank, keep it in sync with PresShell.cpp
372     final static int DEFAULT_DARK_COLOR = 0xFF2A2A2E;
373 
defaultColor()374     private int defaultColor() {
375         // If the app set a default color, just use that
376         if (mLastCoverColor != null) {
377             return mLastCoverColor;
378         }
379 
380         if (mSession == null || !mSession.isOpen()) {
381             return Color.WHITE;
382         }
383 
384         // ... otherwise use the prefers-color-scheme color
385         return mSession.getRuntime().usesDarkTheme() ?
386                 DEFAULT_DARK_COLOR : Color.WHITE;
387     }
388 
389     /**
390      * Unsets the current session from this instance and returns it, if any. You must call
391      * this before {@link #setSession(GeckoSession)} if there is already an open session
392      * set for this instance.
393      *
394      * Note: this method does not close the session and the session remains active. The
395      * caller is responsible for calling {@link GeckoSession#close()} when appropriate.
396      *
397      * @return The {@link GeckoSession} that was set for this instance. May be null.
398      */
399     @UiThread
releaseSession()400     public @Nullable GeckoSession releaseSession() {
401         ThreadUtils.assertOnUiThread();
402 
403         if (mSession == null) {
404             return null;
405         }
406 
407         final GeckoSession session = mSession;
408         mSession.releaseDisplay(mDisplay.release());
409         mSession.getOverscrollEdgeEffect().setInvalidationCallback(null);
410         mSession.getCompositorController().setFirstPaintCallback(null);
411 
412         if (mSession.getAccessibility().getView() == this) {
413             mSession.getAccessibility().setView(null);
414         }
415 
416         if (mSession.getTextInput().getView() == this) {
417             mSession.getTextInput().setView(null);
418         }
419 
420         if (mSession.getSelectionActionDelegate() == mSelectionActionDelegate) {
421             mSession.setSelectionActionDelegate(null);
422         }
423 
424         if (mSession.getAutofillDelegate() == mAutofillDelegate) {
425             mSession.setAutofillDelegate(null);
426         }
427 
428         if (isFocused()) {
429             mSession.setFocused(false);
430         }
431         mSession = null;
432         return session;
433     }
434 
435     /**
436      * Attach a session to this view. If this instance already has an open session, you must use
437      * {@link #releaseSession()} first, otherwise {@link IllegalStateException}
438      * will be thrown. This is to avoid potentially leaking the currently opened session.
439      *
440      * @param session The session to be attached.
441      * @throws IllegalArgumentException if an existing open session is already set.
442      */
443     @UiThread
setSession(@onNull final GeckoSession session)444     public void setSession(@NonNull final GeckoSession session) {
445         ThreadUtils.assertOnUiThread();
446 
447         if (mSession != null && mSession.isOpen()) {
448             throw new IllegalStateException("Current session is open");
449         }
450 
451         releaseSession();
452 
453         mSession = session;
454 
455         // Make sure the clear color is set to the default
456         mSession.getCompositorController()
457                 .setClearColor(defaultColor());
458 
459         if (ViewCompat.isAttachedToWindow(this)) {
460             mDisplay.acquire(session.acquireDisplay());
461         }
462 
463         final Context context = getContext();
464         session.getOverscrollEdgeEffect().setTheme(context);
465         session.getOverscrollEdgeEffect().setInvalidationCallback(new Runnable() {
466             @Override
467             public void run() {
468                 if (Build.VERSION.SDK_INT >= 16) {
469                     GeckoView.this.postInvalidateOnAnimation();
470                 } else {
471                     GeckoView.this.postInvalidateDelayed(10);
472                 }
473             }
474         });
475 
476         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
477         final TypedValue outValue = new TypedValue();
478         if (context.getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight,
479                                                 outValue, true)) {
480             session.getPanZoomController().setScrollFactor(outValue.getDimension(metrics));
481         } else {
482             session.getPanZoomController().setScrollFactor(0.075f * metrics.densityDpi);
483         }
484 
485         session.getCompositorController().setFirstPaintCallback(this::uncover);
486 
487         if (session.getTextInput().getView() == null) {
488             session.getTextInput().setView(this);
489         }
490 
491         if (session.getAccessibility().getView() == null) {
492             session.getAccessibility().setView(this);
493         }
494 
495         if (session.getSelectionActionDelegate() == null && mSelectionActionDelegate != null) {
496             session.setSelectionActionDelegate(mSelectionActionDelegate);
497         }
498 
499         if (mAutofillEnabled) {
500             session.setAutofillDelegate(mAutofillDelegate);
501         }
502 
503         if (isFocused()) {
504             session.setFocused(true);
505         }
506     }
507 
508     @AnyThread
509     @SuppressWarnings("checkstyle:javadocmethod")
getSession()510     public @Nullable GeckoSession getSession() {
511         return mSession;
512     }
513 
514     @AnyThread
getEventDispatcher()515     /* package */ @NonNull EventDispatcher getEventDispatcher() {
516         return mSession.getEventDispatcher();
517     }
518 
519     @SuppressWarnings("checkstyle:javadocmethod")
getPanZoomController()520     public @NonNull PanZoomController getPanZoomController() {
521         ThreadUtils.assertOnUiThread();
522         return mSession.getPanZoomController();
523     }
524 
525     @Override
onAttachedToWindow()526     public void onAttachedToWindow() {
527         if (mSession != null) {
528             final GeckoRuntime runtime = mSession.getRuntime();
529             if (runtime != null) {
530                 runtime.orientationChanged();
531             }
532         }
533 
534         if (mSession != null) {
535             mDisplay.acquire(mSession.acquireDisplay());
536         }
537 
538         super.onAttachedToWindow();
539     }
540 
541     @Override
onDetachedFromWindow()542     public void onDetachedFromWindow() {
543         super.onDetachedFromWindow();
544 
545         if (mSession == null) {
546             return;
547         }
548 
549         // Release the display before we detach from the window.
550         mSession.releaseDisplay(mDisplay.release());
551     }
552 
553     @Override
onConfigurationChanged(final Configuration newConfig)554     protected void onConfigurationChanged(final Configuration newConfig) {
555         super.onConfigurationChanged(newConfig);
556 
557         if (mSession != null) {
558             final GeckoRuntime runtime = mSession.getRuntime();
559             if (runtime != null) {
560                 // onConfigurationChanged is not called for 180 degree orientation changes,
561                 // we will miss such rotations and the screen orientation will not be
562                 // updated.
563                 runtime.orientationChanged(newConfig.orientation);
564                 runtime.configurationChanged(newConfig);
565             }
566         }
567     }
568 
569     @Override
gatherTransparentRegion(final Region region)570     public boolean gatherTransparentRegion(final Region region) {
571         // For detecting changes in SurfaceView layout, we take a shortcut here and
572         // override gatherTransparentRegion, instead of registering a layout listener,
573         // which is more expensive.
574         if (mSurfaceWrapper != null) {
575             mDisplay.onGlobalLayout();
576         }
577         return super.gatherTransparentRegion(region);
578     }
579 
580     @Override
onWindowFocusChanged(final boolean hasWindowFocus)581     public void onWindowFocusChanged(final boolean hasWindowFocus) {
582         super.onWindowFocusChanged(hasWindowFocus);
583 
584         // Only call setFocus(true) when the window gains focus. Any focus loss could be temporary
585         // (e.g. due to auto-fill popups) and we don't want to call setFocus(false) in those cases.
586         // Instead, we call setFocus(false) in onWindowVisibilityChanged.
587         if (mSession != null && hasWindowFocus && isFocused()) {
588             mSession.setFocused(true);
589         }
590     }
591 
592     @Override
onWindowVisibilityChanged(final int visibility)593     protected void onWindowVisibilityChanged(final int visibility) {
594         super.onWindowVisibilityChanged(visibility);
595 
596         // We can be reasonably sure that the focus loss is not temporary, so call setFocus(false).
597         if (mSession != null && visibility != View.VISIBLE && !hasWindowFocus()) {
598             mSession.setFocused(false);
599         }
600     }
601 
602     @Override
onFocusChanged(final boolean gainFocus, final int direction, final Rect previouslyFocusedRect)603     protected void onFocusChanged(final boolean gainFocus, final int direction,
604                                   final Rect previouslyFocusedRect) {
605         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
606 
607         if (mIsResettingFocus) {
608             return;
609         }
610 
611         if (mSession != null) {
612             mSession.setFocused(gainFocus);
613         }
614 
615         if (!gainFocus) {
616             return;
617         }
618 
619         post(new Runnable() {
620             @Override
621             public void run() {
622                 if (!isFocused()) {
623                     return;
624                 }
625 
626                 final InputMethodManager imm = InputMethods.getInputMethodManager(getContext());
627                 // Bug 1404111: Through View#onFocusChanged, the InputMethodManager queues
628                 // up a checkFocus call for the next spin of the message loop, so by
629                 // posting this Runnable after super#onFocusChanged, the IMM should have
630                 // completed its focus change handling at this point and we should be the
631                 // active view for input handling.
632 
633                 // If however onViewDetachedFromWindow for the previously active view gets
634                 // called *after* onFocusChanged, but *before* the focus change has been
635                 // fully processed by the IMM with the help of checkFocus, the IMM will
636                 // lose track of the currently active view, which means that we can't
637                 // interact with the IME.
638                 if (!imm.isActive(GeckoView.this)) {
639                     // If that happens, we bring the IMM's internal state back into sync
640                     // by clearing and resetting our focus.
641                     mIsResettingFocus = true;
642                     clearFocus();
643                     // After calling clearFocus we might regain focus automatically, but
644                     // we explicitly request it again in case this doesn't happen.  If
645                     // we've already got the focus back, this will then be a no-op anyway.
646                     requestFocus();
647                     mIsResettingFocus = false;
648                 }
649             }
650         });
651     }
652 
653     @Override
getHandler()654     public Handler getHandler() {
655         if (Build.VERSION.SDK_INT >= 24 || mSession == null) {
656             return super.getHandler();
657         }
658         return mSession.getTextInput().getHandler(super.getHandler());
659     }
660 
661     @Override
onCreateInputConnection(final EditorInfo outAttrs)662     public InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
663         if (mSession == null) {
664             return null;
665         }
666         return mSession.getTextInput().onCreateInputConnection(outAttrs);
667     }
668 
669     @Override
onKeyPreIme(final int keyCode, final KeyEvent event)670     public boolean onKeyPreIme(final int keyCode, final KeyEvent event) {
671         if (super.onKeyPreIme(keyCode, event)) {
672             return true;
673         }
674         return mSession != null &&
675                mSession.getTextInput().onKeyPreIme(keyCode, event);
676     }
677 
678     @Override
onKeyUp(final int keyCode, final KeyEvent event)679     public boolean onKeyUp(final int keyCode, final KeyEvent event) {
680         if (super.onKeyUp(keyCode, event)) {
681             return true;
682         }
683         return mSession != null &&
684                mSession.getTextInput().onKeyUp(keyCode, event);
685     }
686 
687     @Override
onKeyDown(final int keyCode, final KeyEvent event)688     public boolean onKeyDown(final int keyCode, final KeyEvent event) {
689         if (super.onKeyDown(keyCode, event)) {
690             return true;
691         }
692         return mSession != null &&
693                mSession.getTextInput().onKeyDown(keyCode, event);
694     }
695 
696     @Override
onKeyLongPress(final int keyCode, final KeyEvent event)697     public boolean onKeyLongPress(final int keyCode, final KeyEvent event) {
698         if (super.onKeyLongPress(keyCode, event)) {
699             return true;
700         }
701         return mSession != null &&
702                mSession.getTextInput().onKeyLongPress(keyCode, event);
703     }
704 
705     @Override
onKeyMultiple(final int keyCode, final int repeatCount, final KeyEvent event)706     public boolean onKeyMultiple(final int keyCode, final int repeatCount, final KeyEvent event) {
707         if (super.onKeyMultiple(keyCode, repeatCount, event)) {
708             return true;
709         }
710         return mSession != null &&
711                mSession.getTextInput().onKeyMultiple(keyCode, repeatCount, event);
712     }
713 
714     @Override
dispatchDraw(final Canvas canvas)715     public void dispatchDraw(final Canvas canvas) {
716         super.dispatchDraw(canvas);
717 
718         if (mSession != null) {
719             mSession.getOverscrollEdgeEffect().draw(canvas);
720         }
721     }
722 
723     @SuppressLint("ClickableViewAccessibility")
724     @Override
onTouchEvent(final MotionEvent event)725     public boolean onTouchEvent(final MotionEvent event) {
726         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
727             requestFocus();
728         }
729 
730         if (mSession == null) {
731             return false;
732         }
733 
734         mSession.getPanZoomController().onTouchEvent(event);
735         return true;
736     }
737 
738     /**
739      * Dispatches a {@link MotionEvent} to the {@link PanZoomController}. This is the same as
740      * {@link #onTouchEvent(MotionEvent)}, but instead returns a {@link PanZoomController.InputResult}
741      * indicating how the event was handled.
742      *
743      * NOTE: It is highly recommended to only call this with ACTION_DOWN or in otherwise
744      * limited capacity. Returning a GeckoResult for every touch event will generate
745      * a lot of allocations and unnecessary GC pressure.
746      *
747      * @param event A {@link MotionEvent}
748      * @return A GeckoResult resolving to {@link PanZoomController.InputResultDetail}.
749      */
onTouchEventForDetailResult(final @NonNull MotionEvent event)750     public @NonNull GeckoResult<PanZoomController.InputResultDetail> onTouchEventForDetailResult(final @NonNull MotionEvent event) {
751         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
752             requestFocus();
753         }
754 
755         if (mSession == null) {
756             return GeckoResult.fromValue(
757                 new PanZoomController.InputResultDetail(PanZoomController.INPUT_RESULT_UNHANDLED,
758                                                         PanZoomController.SCROLLABLE_FLAG_NONE,
759                                                         PanZoomController.OVERSCROLL_FLAG_NONE));
760         }
761 
762         // NOTE: Treat mouse events as "touch" rather than as "mouse", so mouse can be
763         // used to pan/zoom. Call onMouseEvent() instead for behavior similar to desktop.
764         return mSession.getPanZoomController().onTouchEventForDetailResult(event);
765     }
766 
767     @Override
onGenericMotionEvent(final MotionEvent event)768     public boolean onGenericMotionEvent(final MotionEvent event) {
769         if (AndroidGamepadManager.handleMotionEvent(event)) {
770             return true;
771         }
772 
773         if (mSession == null) {
774             return true;
775         }
776 
777         if (mSession.getAccessibility().onMotionEvent(event)) {
778             return true;
779         }
780 
781         mSession.getPanZoomController().onMotionEvent(event);
782         return true;
783     }
784 
785     @Override
onProvideAutofillVirtualStructure(final ViewStructure structure, final int flags)786     public void onProvideAutofillVirtualStructure(final ViewStructure structure,
787                                                   final int flags) {
788         super.onProvideAutofillVirtualStructure(structure, flags);
789 
790         if (mSession == null) {
791             return;
792         }
793 
794         final Autofill.Session autofillSession = mSession.getAutofillSession();
795         autofillSession.fillViewStructure(this, structure, flags);
796     }
797 
798     @Override
799     @TargetApi(26)
autofill(@onNull final SparseArray<AutofillValue> values)800     public void autofill(@NonNull final SparseArray<AutofillValue> values) {
801         super.autofill(values);
802 
803         if (mSession == null) {
804             return;
805         }
806         final SparseArray<CharSequence> strValues = new SparseArray<>(values.size());
807         for (int i = 0; i < values.size(); i++) {
808             final AutofillValue value = values.valueAt(i);
809             if (value.isText()) {
810                 // Only text is currently supported.
811                 strValues.put(values.keyAt(i), value.getTextValue());
812             }
813         }
814         mSession.autofill(strValues);
815     }
816 
817     /**
818      * Request a {@link Bitmap} of the visible portion of the web page currently being
819      * rendered.
820      *
821      * See {@link GeckoDisplay#capturePixels} for more details.
822      *
823      * @return A {@link GeckoResult} that completes with a {@link Bitmap} containing
824      * the pixels and size information of the currently visible rendered web page.
825      */
826     @UiThread
capturePixels()827     public @NonNull GeckoResult<Bitmap> capturePixels() {
828         return mDisplay.capturePixels();
829     }
830 
831     /**
832      * Sets whether or not this View participates in Android autofill.
833      *
834      * When enabled, this will set an {@link Autofill.Delegate} on the
835      * {@link GeckoSession} for this instance.
836      *
837      * @param enabled Whether or not Android autofill is enabled for this view.
838      */
839     @TargetApi(26)
setAutofillEnabled(final boolean enabled)840     public void setAutofillEnabled(final boolean enabled) {
841         mAutofillEnabled = enabled;
842 
843         if (mSession != null) {
844             if (!enabled && mSession.getAutofillDelegate() == mAutofillDelegate) {
845                 mSession.setAutofillDelegate(null);
846             } else if (enabled) {
847                 mSession.setAutofillDelegate(mAutofillDelegate);
848             }
849         }
850     }
851 
852     /**
853      * @return Whether or not Android autofill is enabled for this view.
854      */
855     @TargetApi(26)
getAutofillEnabled()856     public boolean getAutofillEnabled() {
857         return mAutofillEnabled;
858     }
859 
860     private class AndroidAutofillDelegate implements Autofill.Delegate {
861 
displayRectForId(@onNull final GeckoSession session, @NonNull final Autofill.Node node)862         private Rect displayRectForId(@NonNull final GeckoSession session,
863                                       @NonNull final Autofill.Node node) {
864             if (node == null) {
865                 return new Rect(0, 0, 0, 0);
866             }
867 
868             final Matrix matrix = new Matrix();
869             final RectF rectF = new RectF(node.getDimensions());
870             session.getPageToScreenMatrix(matrix);
871             matrix.mapRect(rectF);
872 
873             final Rect screenRect = new Rect();
874             rectF.roundOut(screenRect);
875             return screenRect;
876         }
877 
878         @Override
onAutofill(@onNull final GeckoSession session, final int notification, final Autofill.Node node)879         public void onAutofill(@NonNull final GeckoSession session,
880                                final int notification,
881                                final Autofill.Node node) {
882             ThreadUtils.assertOnUiThread();
883             if (Build.VERSION.SDK_INT < 26) {
884                 return;
885             }
886 
887             final AutofillManager manager =
888                     GeckoView.this.getContext().getSystemService(AutofillManager.class);
889             if (manager == null) {
890                 return;
891             }
892 
893             try {
894                 switch (notification) {
895                     case Autofill.Notify.SESSION_STARTED:
896                         // This line seems necessary for auto-fill to work on the initial page.
897                     case Autofill.Notify.SESSION_CANCELED:
898                         manager.cancel();
899                         break;
900                     case Autofill.Notify.SESSION_COMMITTED:
901                         manager.commit();
902                         break;
903                     case Autofill.Notify.NODE_FOCUSED:
904                         manager.notifyViewEntered(
905                                 GeckoView.this, node.getId(),
906                                 displayRectForId(session, node));
907                         break;
908                     case Autofill.Notify.NODE_BLURRED:
909                         manager.notifyViewExited(GeckoView.this, node.getId());
910                         break;
911                     case Autofill.Notify.NODE_UPDATED:
912                         manager.notifyValueChanged(
913                                 GeckoView.this,
914                                 node.getId(),
915                                 AutofillValue.forText(node.getValue()));
916                         break;
917                 }
918             } catch (final SecurityException e) {
919                 Log.e(LOGTAG, "Failed to call Autofill Manager API: ", e);
920             }
921         }
922     }
923 }
924