chromium/content/public/android/java/src/org/chromium/content/browser/input/StylusGestureConverter.java

// Copyright 2023 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.input;

import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Build;
import android.view.inputmethod.DeleteGesture;
import android.view.inputmethod.DeleteRangeGesture;
import android.view.inputmethod.HandwritingGesture;
import android.view.inputmethod.InsertGesture;
import android.view.inputmethod.JoinOrSplitGesture;
import android.view.inputmethod.RemoveSpaceGesture;
import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.SelectRangeGesture;

import androidx.annotation.IntDef;
import androidx.annotation.RequiresApi;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.blink.mojom.StylusWritingGestureAction;
import org.chromium.blink.mojom.StylusWritingGestureData;
import org.chromium.blink.mojom.StylusWritingGestureGranularity;
import org.chromium.gfx.mojom.Rect;
import org.chromium.mojo_base.mojom.String16;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Converts stylus rich gestures from their Android representation to their Blink representation.
 */
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class StylusGestureConverter {
    // Should be kept in sync with StylusHandwritingGesture in tools/metrics/histograms/enums.xml.
    // These values are persisted to logs. Entries should not be renumbered and
    // numeric values should never be reused.
    // Entries with the DW prefix are used by Samsung's DirectWriting service. All other entries are
    // used by Android stylus handwriting.
    @IntDef({
        UmaGestureType.DW_DELETE_TEXT,
        UmaGestureType.DW_ADD_SPACE_OR_TEXT,
        UmaGestureType.DW_REMOVE_SPACES,
        UmaGestureType.DW_SPLIT_OR_MERGE,
        UmaGestureType.SELECT,
        UmaGestureType.INSERT,
        UmaGestureType.DELETE,
        UmaGestureType.REMOVE_SPACE,
        UmaGestureType.JOIN_OR_SPLIT,
        UmaGestureType.SELECT_RANGE,
        UmaGestureType.DELETE_RANGE,
        UmaGestureType.NUM_ENTRIES
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface UmaGestureType {
        int DW_DELETE_TEXT = 0;
        int DW_ADD_SPACE_OR_TEXT = 1;
        int DW_REMOVE_SPACES = 2;
        int DW_SPLIT_OR_MERGE = 3;
        int SELECT = 4;
        int INSERT = 5;
        int DELETE = 6;
        int REMOVE_SPACE = 7;
        int JOIN_OR_SPLIT = 8;
        int SELECT_RANGE = 9;
        int DELETE_RANGE = 10;
        int NUM_ENTRIES = 11;
    }

    public static void logGestureType(@UmaGestureType int gestureType) {
        RecordHistogram.recordEnumeratedHistogram(
                "InputMethod.StylusHandwriting.Gesture", gestureType, UmaGestureType.NUM_ENTRIES);
    }

    public static StylusWritingGestureData createGestureData(HandwritingGesture gesture) {
        if (gesture instanceof SelectGesture) {
            logGestureType(UmaGestureType.SELECT);
            return createGestureData((SelectGesture) gesture);
        } else if (gesture instanceof InsertGesture) {
            logGestureType(UmaGestureType.INSERT);
            return createGestureData((InsertGesture) gesture);
        } else if (gesture instanceof DeleteGesture) {
            logGestureType(UmaGestureType.DELETE);
            return createGestureData((DeleteGesture) gesture);
        } else if (gesture instanceof RemoveSpaceGesture) {
            logGestureType(UmaGestureType.REMOVE_SPACE);
            return createGestureData((RemoveSpaceGesture) gesture);
        } else if (gesture instanceof JoinOrSplitGesture) {
            logGestureType(UmaGestureType.JOIN_OR_SPLIT);
            return createGestureData((JoinOrSplitGesture) gesture);
        } else if (gesture instanceof SelectRangeGesture) {
            logGestureType(UmaGestureType.SELECT_RANGE);
            return createGestureData((SelectRangeGesture) gesture);
        } else if (gesture instanceof DeleteRangeGesture) {
            logGestureType(UmaGestureType.DELETE_RANGE);
            return createGestureData((DeleteRangeGesture) gesture);
        }
        return null;
    }

    /**
     * Creates a StylusWritingGestureData object from a SelectGesture.
     * @param gesture The SelectGesture to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(SelectGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.SELECT_TEXT;
        gestureData.granularity =
                gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD
                        ? StylusWritingGestureGranularity.WORD
                        : StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        Rect[] areas = toTwoMojoRects(gesture.getSelectionArea());
        gestureData.startRect = areas[0];
        gestureData.endRect = areas[1];
        return gestureData;
    }

    /**
     * Creates a StylusWritingGestureData object from an InsertGesture.
     * @param gesture The InsertGesture to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(InsertGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.ADD_SPACE_OR_TEXT;
        gestureData.granularity = StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        gestureData.textToInsert = toMojoString(gesture.getTextToInsert());
        gestureData.startRect = toMojoRect(gesture.getInsertionPoint());
        return gestureData;
    }

    /**
     * Creates a StylusWritingGestureData object from a DeleteGesture.
     * @param gesture The DeleteGesture to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(DeleteGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.DELETE_TEXT;
        gestureData.granularity =
                gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD
                        ? StylusWritingGestureGranularity.WORD
                        : StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        Rect[] areas = toTwoMojoRects(gesture.getDeletionArea());
        gestureData.startRect = areas[0];
        gestureData.endRect = areas[1];
        return gestureData;
    }

    /**
     * Creates a StylusWritingGestureData object from a RemoveSpaceGesture.
     * @param gesture The RemoveSpaceGesture object to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(RemoveSpaceGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.REMOVE_SPACES;
        gestureData.granularity = StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        gestureData.startRect = toMojoRect(gesture.getStartPoint());
        gestureData.endRect = toMojoRect(gesture.getEndPoint());
        return gestureData;
    }

    /**
     * Creates a StylusWritingGestureData object from a JoinOrSplitGesture.
     * @param gesture The JoinOrSplitGesture to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(JoinOrSplitGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.SPLIT_OR_MERGE;
        gestureData.granularity = StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        gestureData.startRect = toMojoRect(gesture.getJoinOrSplitPoint());
        return gestureData;
    }

    /**
     * Creates a StylusWritingGestureData object from a SelectRangeGesture.
     * @param gesture The SelectRangeGesture to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(SelectRangeGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.SELECT_TEXT;
        gestureData.granularity =
                gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD
                        ? StylusWritingGestureGranularity.WORD
                        : StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        gestureData.startRect = toMojoRect(gesture.getSelectionStartArea());
        gestureData.endRect = toMojoRect(gesture.getSelectionEndArea());
        return gestureData;
    }

    /**
     * Creates a StylusWritingGestureData object from a DeleteRangeGesture.
     * @param gesture The DeleteRangeGesture to extract data from.
     * @return A StylusWritingGestureData object to pass through Mojo to blink.
     */
    private static StylusWritingGestureData createGestureData(DeleteRangeGesture gesture) {
        StylusWritingGestureData gestureData = new StylusWritingGestureData();
        gestureData.action = StylusWritingGestureAction.DELETE_TEXT;
        gestureData.granularity =
                gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD
                        ? StylusWritingGestureGranularity.WORD
                        : StylusWritingGestureGranularity.CHARACTER;
        gestureData.textAlternative = toMojoString(gesture.getFallbackText());
        gestureData.startRect = toMojoRect(gesture.getDeletionStartArea());
        gestureData.endRect = toMojoRect(gesture.getDeletionEndArea());
        return gestureData;
    }

    /**
     * Takes an Android RectF and converts it to a Mojo Rect object.
     * @param rect The Android representation of a rectangle with four floats representing the left,
     * top, right and bottom positions of the rectangle.
     * @return A Mojo rectangle which consists of a point (represented by x and y integers) and a
     * size (represented by width and height integers).
     */
    private static Rect toMojoRect(RectF rect) {
        Rect mojoRect = new Rect();
        mojoRect.x = Math.round(rect.left);
        mojoRect.y = Math.round(rect.top);
        mojoRect.width = Math.round(rect.right - rect.left);
        mojoRect.height = Math.round(rect.bottom - rect.top);
        return mojoRect;
    }

    /**
     * Takes an Android PointF and converts it to a zero-sized Mojo Rect object.
     * @param point The Android representation of a point with two floats for the x and y position.
     * @return A Mojo rectangle with an area of 0 at the provided point.
     */
    private static Rect toMojoRect(PointF point) {
        Rect rect = new Rect();
        rect.x = Math.round(point.x);
        rect.y = Math.round(point.y);
        rect.width = 0;
        rect.height = 0;
        return rect;
    }

    /**
     * Converts an Android RectF object to an array of 2 Mojo Rect objects. These Rect objects have
     * an area of 0 and represent the left center and right center of the given RectF.
     * @param area The Android RectF to convert to two Mojo Rect objects.
     * @return An area of 2 Mojo Rect objects representing the left and right centers of the RectF.
     */
    private static Rect[] toTwoMojoRects(RectF area) {
        PointF left = new PointF(area.left, (area.top + area.bottom) / 2.0f);
        PointF right = new PointF(area.right, (area.top + area.bottom) / 2.0f);
        Rect[] areas = new Rect[2];
        areas[0] = toMojoRect(left);
        areas[1] = toMojoRect(right);
        return areas;
    }

    /**
     * Converts a Java String object to the String16 representation compatible with Mojo.
     * @param string A Java String to convert to the String16 format.
     * @return A String16 object which wraps an array of short integers for each character in the
     * string.
     */
    private static String16 toMojoString(String string) {
        int len = string != null ? string.length() : 0;
        short[] data = new short[len];
        for (int i = 0; i < data.length; i++) {
            data[i] = (short) string.charAt(i);
        }
        String16 mojoString = new String16();
        mojoString.data = data;
        return mojoString;
    }

    private StylusGestureConverter() {}
}