chromium/components/stylus_handwriting/android/java/src/org/chromium/components/stylus_handwriting/StylusHandwritingInitiator.java

// Copyright 2024 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.components.stylus_handwriting;

import static android.view.MotionEvent.TOOL_TYPE_STYLUS;

import android.os.Build.VERSION_CODES;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.ui.base.ViewUtils;

/**
 * This class handles stylus touch events on a view to initiate handwriting input (requires Android
 * Tiramisu API level 33 or higher). It tracks stylus touch information and determines when to
 * initiate handwriting input based on pre-defined conditions like stylus type, movement slop, and
 * view writability.
 */
@RequiresApi(VERSION_CODES.TIRAMISU)
class StylusHandwritingInitiator {

    private static class StylusTouchData {
        public final float stylusDownX;
        public final float stylusDownY;
        public final int deviceId;

        // Constructor to update gathered Stylus information
        public StylusTouchData(int deviceId, float stylusDownX, float stylusDownY) {
            this.stylusDownX = stylusDownX;
            this.stylusDownY = stylusDownY;
            this.deviceId = deviceId;
        }
    }

    @Nullable private StylusTouchData mStylusTouchData;
    private final int mHandwritingSlopPx;
    private final InputMethodManager mInputMethodManager;

    StylusHandwritingInitiator(InputMethodManager inputMethodManager) {
        mInputMethodManager = inputMethodManager;
        // Handwriting slop for Stylus is 2dps
        mHandwritingSlopPx = ViewUtils.dpToPx(ContextUtils.getApplicationContext(), 2);
    }

    boolean onTouchEvent(@NonNull MotionEvent motionEvent, View view) {
        // Return false early if the feature is not enabled
        if (!isStylusHandwritingFeatureEnabled()) {
            return false;
        }
        // Return false early if the motion event is not coming from a Stylus.
        if (!isStylusEvent(motionEvent)) {
            clearStylusInfo();
            return false;
        }
        // Return false early if the motion event is coming from a second Stylus.
        int currentDeviceId = motionEvent.getDeviceId();
        if (mStylusTouchData != null && mStylusTouchData.deviceId != currentDeviceId) {
            return false;
        }
        int maskedAction = motionEvent.getActionMasked();
        switch (maskedAction) {
            case MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
                updateStylusInfoOnDownEvent(motionEvent);
            }
            case MotionEvent.ACTION_MOVE -> {
                float current_x = motionEvent.getX();
                float current_y = motionEvent.getY();

                // Return early if the view isn't editable.
                if (!isViewWritable()) {
                    return false;
                }
                if (largerThanSlop(
                        current_x,
                        current_y,
                        mStylusTouchData.stylusDownX,
                        mStylusTouchData.stylusDownY,
                        mHandwritingSlopPx)) {
                    mInputMethodManager.startStylusHandwriting(view);
                    clearStylusInfo();
                    return true;
                }
                return false;
            }
            case MotionEvent.ACTION_CANCEL,
                    MotionEvent.ACTION_POINTER_UP,
                    MotionEvent.ACTION_UP -> {
                clearStylusInfo();
            }
        }
        return false;
    }

    private void updateStylusInfoOnDownEvent(MotionEvent motionEvent) {
        int actionIndex = motionEvent.getActionIndex();
        mStylusTouchData =
                new StylusTouchData(
                        motionEvent.getDeviceId(),
                        motionEvent.getX(actionIndex),
                        motionEvent.getY(actionIndex));
    }

    private void clearStylusInfo() {
        mStylusTouchData = null;
    }

    private boolean isStylusEvent(MotionEvent motionEvent) {
        return TOOL_TYPE_STYLUS == motionEvent.getToolType(motionEvent.getActionIndex());
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    boolean isViewWritable() {
        // TODO(crbug.com/317299999): Expose API to check whether event is over editable element.
        return true;
    }

    // Method to calculate if the difference between the starting point
    // and the current point of Stylus is bigger than the Slop
    private boolean largerThanSlop(
            float currentX, float currentY, float startingX, float startingY, int handwritingSlop) {
        float distanceX = currentX - startingX;
        float distanceY = currentY - startingY;
        return (distanceX * distanceX) + (distanceY * distanceY)
                >= handwritingSlop * handwritingSlop;
    }

    // Method to check if the corresponding feature flag is enabled
    boolean isStylusHandwritingFeatureEnabled() {
        return (StylusHandwritingFeatureMap.isEnabled(
                StylusHandwritingFeatureMap.USE_HANDWRITING_INITIATOR));
    }
}