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