chromium/content/public/android/java/src/org/chromium/content/browser/ContentUiEventHandler.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;

import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.UserData;
import org.chromium.content.browser.input.ImeAdapterImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl.UserDataFactory;
import org.chromium.content_public.browser.ViewEventSink.InternalAccessDelegate;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.MotionEventUtils;
import org.chromium.ui.base.EventForwarder;

/**
 * Called from native to handle UI events that need access to various Java layer
 * content components.
 */
@JNINamespace("content")
public class ContentUiEventHandler implements UserData {
    private final WebContentsImpl mWebContents;
    private InternalAccessDelegate mEventDelegate;
    private long mNativeContentUiEventHandler;

    private static final class UserDataFactoryLazyHolder {
        private static final UserDataFactory<ContentUiEventHandler> INSTANCE =
                ContentUiEventHandler::new;
    }

    public static ContentUiEventHandler fromWebContents(WebContents webContents) {
        return ((WebContentsImpl) webContents)
                .getOrSetUserData(ContentUiEventHandler.class, UserDataFactoryLazyHolder.INSTANCE);
    }

    public ContentUiEventHandler(WebContents webContents) {
        mWebContents = (WebContentsImpl) webContents;
        mNativeContentUiEventHandler =
                ContentUiEventHandlerJni.get().init(ContentUiEventHandler.this, webContents);
    }

    static ContentUiEventHandler createForTesting(
            WebContents webContents, long nativeContentUiEventHandler) {
        ContentUiEventHandler contentUiEventHandler = new ContentUiEventHandler(webContents);
        contentUiEventHandler.mNativeContentUiEventHandler = nativeContentUiEventHandler;
        return contentUiEventHandler;
    }

    public void setEventDelegate(InternalAccessDelegate delegate) {
        mEventDelegate = delegate;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    @CalledByNative
    protected boolean onGenericMotionEvent(MotionEvent event) {
        if (Gamepad.from(mWebContents).onGenericMotionEvent(event)) return true;
        if (JoystickHandler.fromWebContents(mWebContents).onGenericMotionEvent(event)) return true;
        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_SCROLL:
                    onMouseWheelEvent(event);
                    return true;
                case MotionEvent.ACTION_BUTTON_PRESS:
                case MotionEvent.ACTION_BUTTON_RELEASE:
                    // TODO(mustaq): Should we include MotionEvent.TOOL_TYPE_STYLUS here?
                    // https://crbug.com/592082
                    if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
                        return onMouseEvent(event, false);
                    }
            }
        }
        if (isTrackpadEventThatNeedsConversion(event)) {
            return onMouseEvent(event, true);
        }
        return mEventDelegate.super_onGenericMotionEvent(event);
    }

    private boolean isTrackpadEventThatNeedsConversion(MotionEvent event) {
        return mWebContents.getEventForwarder().isTrackpadToMouseEventConversionEnabled()
                && EventForwarder.isTrackpadClickOrClickAndDragEvent(event);
    }

    private void onMouseWheelEvent(MotionEvent event) {
        assert mNativeContentUiEventHandler != 0;
        ContentUiEventHandlerJni.get()
                .sendMouseWheelEvent(
                        mNativeContentUiEventHandler,
                        ContentUiEventHandler.this,
                        MotionEventUtils.getEventTimeNanos(event),
                        event.getX(),
                        event.getY(),
                        event.getAxisValue(MotionEvent.AXIS_HSCROLL),
                        event.getAxisValue(MotionEvent.AXIS_VSCROLL));
    }

    private boolean onMouseEvent(MotionEvent event, boolean shouldConvertToMouseEvent) {
        assert mNativeContentUiEventHandler != 0;
        EventForwarder eventForwarder = mWebContents.getEventForwarder();
        boolean didOffsetEvent = false;
        MotionEvent newEvent = eventForwarder.createOffsetMotionEventIfNeeded(event);
        if (newEvent != event) {
            didOffsetEvent = true;
            event = newEvent;
        }
        ContentUiEventHandlerJni.get()
                .sendMouseEvent(
                        mNativeContentUiEventHandler,
                        ContentUiEventHandler.this,
                        MotionEventUtils.getEventTimeNanos(event),
                        event.getActionMasked(),
                        event.getX(),
                        event.getY(),
                        event.getPointerId(0),
                        event.getPressure(0),
                        event.getOrientation(0),
                        event.getAxisValue(MotionEvent.AXIS_TILT, 0),
                        EventForwarder.getMouseEventActionButton(event),
                        event.getButtonState(),
                        event.getMetaState(),
                        shouldConvertToMouseEvent
                                ? MotionEvent.TOOL_TYPE_MOUSE
                                : event.getToolType(0));
        if (didOffsetEvent) event.recycle();
        return true;
    }

    @CalledByNative
    private boolean onKeyUp(int keyCode, KeyEvent event) {
        return mEventDelegate.super_onKeyUp(keyCode, event);
    }

    @CalledByNative
    private boolean dispatchKeyEvent(KeyEvent event) {
        if (Gamepad.from(mWebContents).dispatchKeyEvent(event)) return true;
        if (!shouldPropagateKeyEvent(event)) {
            return mEventDelegate.super_dispatchKeyEvent(event);
        }

        if (ImeAdapterImpl.fromWebContents(mWebContents).dispatchKeyEvent(event)) return true;

        return mEventDelegate.super_dispatchKeyEvent(event);
    }

    /**
     * Check whether a key should be propagated to the embedder or not.
     * We need to send almost every key to Blink. However:
     * 1. We don't want to block the device on the renderer for
     * some keys like menu, home, call.
     * 2. There are no WebKit equivalents for some of these keys
     * (see app/keyboard_codes_win.h)
     * Note that these are not the same set as KeyEvent.isSystemKey:
     * for instance, AKEYCODE_MEDIA_* will be dispatched to webkit*.
     * 3. KEYCODE_SYSRQ is the keycode received when Print screen is pressed
     * on a hardware keyboard. Do not propagate this to allow Android Platform
     * to take a screenshot.
     */
    private static boolean shouldPropagateKeyEvent(KeyEvent event) {
        int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_MENU
                || keyCode == KeyEvent.KEYCODE_HOME
                || keyCode == KeyEvent.KEYCODE_BACK
                || keyCode == KeyEvent.KEYCODE_CALL
                || keyCode == KeyEvent.KEYCODE_ENDCALL
                || keyCode == KeyEvent.KEYCODE_POWER
                || keyCode == KeyEvent.KEYCODE_HEADSETHOOK
                || keyCode == KeyEvent.KEYCODE_CAMERA
                || keyCode == KeyEvent.KEYCODE_FOCUS
                || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
                || keyCode == KeyEvent.KEYCODE_SYSRQ) {
            return false;
        }
        return true;
    }

    /**
     * @see View#scrollBy(int, int)
     * Currently the ContentView scrolling happens in the native side. In
     * the Java view system, it is always pinned at (0, 0). scrollBy() and scrollTo()
     * are overridden, so that View's mScrollX and mScrollY will be unchanged at
     * (0, 0). This is critical for drawing ContentView correctly.
     */
    @CalledByNative
    private void scrollBy(float dxPix, float dyPix) {
        if (dxPix == 0 && dyPix == 0) return;
        long time = SystemClock.uptimeMillis();
        // It's a very real (and valid) possibility that a fling may still
        // be active when programatically scrolling. Cancelling the fling in
        // such cases ensures a consistent gesture event stream.
        if (GestureListenerManagerImpl.fromWebContents(mWebContents).hasActiveFlingScroll()) {
            ContentUiEventHandlerJni.get()
                    .cancelFling(mNativeContentUiEventHandler, ContentUiEventHandler.this, time);
        }
        ContentUiEventHandlerJni.get()
                .sendScrollEvent(
                        mNativeContentUiEventHandler,
                        ContentUiEventHandler.this,
                        time,
                        dxPix,
                        dyPix);
    }

    @CalledByNative
    private void scrollTo(float xPix, float yPix) {
        final float xCurrentPix = mWebContents.getRenderCoordinates().getScrollXPix();
        final float yCurrentPix = mWebContents.getRenderCoordinates().getScrollYPix();
        final float dxPix = xPix - xCurrentPix;
        final float dyPix = yPix - yCurrentPix;
        scrollBy(dxPix, dyPix);
    }

    @NativeMethods
    interface Natives {
        long init(ContentUiEventHandler caller, WebContents webContents);

        void sendMouseWheelEvent(
                long nativeContentUiEventHandler,
                ContentUiEventHandler caller,
                long timeNs,
                float x,
                float y,
                float ticksX,
                float ticksY);

        void sendMouseEvent(
                long nativeContentUiEventHandler,
                ContentUiEventHandler caller,
                long timeNs,
                int action,
                float x,
                float y,
                int pointerId,
                float pressure,
                float orientation,
                float tilt,
                int changedButton,
                int buttonState,
                int metaState,
                int toolType);

        void sendScrollEvent(
                long nativeContentUiEventHandler,
                ContentUiEventHandler caller,
                long timeMs,
                float deltaX,
                float deltaY);

        void cancelFling(
                long nativeContentUiEventHandler, ContentUiEventHandler caller, long timeMs);
    }
}