chromium/components/stylus_handwriting/android/junit/src/org/chromium/components/stylus_handwriting/StylusHandwritingInitiatorTest.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 org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;

/** Unit tests for {@link StylusHandwritingInitiator}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.TIRAMISU)
public class StylusHandwritingInitiatorTest {
    private @Mock ViewGroup mContainerView;
    private @Mock Context mContext;
    private @Mock InputMethodManager mInputMethodManager;
    private AndroidStylusWritingHandler mHandler;

    @Before
    public void setUp() {
        openMocks(this);
        when(mContext.getSystemService(InputMethodManager.class)).thenReturn(mInputMethodManager);
        mHandler = new AndroidStylusWritingHandler(mContext);
    }

    // Testing that if the movement of Stylus is larger than the slop, startStylusHandwriting should
    // be called.
    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void handlesLargerThanSlopMoveEventForStylus() {
        int initX = 10;
        int initY = 10;
        MotionEvent downEvent =
                createMotionEvent(
                        MotionEvent.ACTION_POINTER_DOWN,
                        MotionEvent.TOOL_TYPE_STYLUS,
                        initX,
                        initY,
                        0,
                        1);
        int x = initX + 10;
        int y = initY + 10;
        MotionEvent moveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);
        MotionEvent upEvent =
                createMotionEvent(
                        MotionEvent.ACTION_POINTER_UP, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);
        mHandler.handleTouchEvent(downEvent, mContainerView);
        mHandler.handleTouchEvent(moveEvent, mContainerView);
        mHandler.handleTouchEvent(upEvent, mContainerView);
        verify(mInputMethodManager).startStylusHandwriting(mContainerView);
    }

    // Testing that if the movement of Stylus is even with the slop, startStylusHandwriting should
    // be called.
    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void handlesEqualToSlopMoveEventForStylus() {
        int initX = 10;
        int initY = 10;
        MotionEvent downEvent =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_STYLUS, initX, initY, 0, 1);
        int x = initX + 2;
        int y = initY + 2;
        MotionEvent moveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);
        MotionEvent upEvent =
                createMotionEvent(MotionEvent.ACTION_UP, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);
        mHandler.handleTouchEvent(downEvent, mContainerView);
        mHandler.handleTouchEvent(moveEvent, mContainerView);
        mHandler.handleTouchEvent(upEvent, mContainerView);
        verify(mInputMethodManager).startStylusHandwriting(mContainerView);
    }

    // Testing that if the movement of Stylus is smaller than the slop, startStylusHandwriting
    // should not be called.
    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void handlesSmallerThanSlopMoveEventForStylus() {
        int initX = 10;
        int initY = 10;
        MotionEvent downEvent =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_STYLUS, initX, initY, 0, 1);
        int x = initX + 1;
        int y = initY + 1;
        MotionEvent moveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);
        MotionEvent upEvent =
                createMotionEvent(MotionEvent.ACTION_UP, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);
        mHandler.handleTouchEvent(downEvent, mContainerView);
        mHandler.handleTouchEvent(moveEvent, mContainerView);
        mHandler.handleTouchEvent(upEvent, mContainerView);
        verify(mInputMethodManager, never()).startStylusHandwriting(mContainerView);
    }

    // Testing that the code handles multiple move events in one go
    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void handlesMultipleMoveEventsForStylus() {
        int initX = 10;
        int initY = 10;
        MotionEvent downEvent =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_STYLUS, initX, initY, 0, 1);
        int firstMoveX = initX + 1;
        int firstMoveY = initY + 1;
        MotionEvent firstMoveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE,
                        MotionEvent.TOOL_TYPE_STYLUS,
                        firstMoveX,
                        firstMoveY,
                        1,
                        1);
        mHandler.handleTouchEvent(downEvent, mContainerView);
        mHandler.handleTouchEvent(firstMoveEvent, mContainerView);
        verify(mInputMethodManager, never()).startStylusHandwriting(mContainerView);
        int secondMoveX = initX + 10;
        int secondMoveY = initY + 10;
        MotionEvent secondMoveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE,
                        MotionEvent.TOOL_TYPE_STYLUS,
                        secondMoveX,
                        secondMoveY,
                        2,
                        1);
        MotionEvent upEvent =
                createMotionEvent(
                        MotionEvent.ACTION_UP,
                        MotionEvent.TOOL_TYPE_STYLUS,
                        secondMoveX,
                        secondMoveY,
                        2,
                        1);
        mHandler.handleTouchEvent(secondMoveEvent, mContainerView);
        mHandler.handleTouchEvent(upEvent, mContainerView);
        verify(mInputMethodManager).startStylusHandwriting(mContainerView);
    }

    // Testing that if two styluses are active and one of them is stopped being used, the other one
    // should still be active.
    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void handlesMoreThanOneStyluses() {
        int initX = 10;
        int initY = 10;
        int x = initX + 10;
        int y = initY + 10;
        MotionEvent downEventStylusOne =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_STYLUS, initX, initY, 0, 1);
        MotionEvent downEventStylusTwo =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_STYLUS, initX, initY, 1, 2);
        MotionEvent moveEventStylusTwo =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_STYLUS, x, y, 2, 2);
        MotionEvent upEventStylusTwo =
                createMotionEvent(
                        MotionEvent.ACTION_POINTER_UP,
                        MotionEvent.TOOL_TYPE_STYLUS,
                        initX,
                        initY,
                        3,
                        2);
        MotionEvent moveEventStylusOne =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_STYLUS, x, y, 4, 1);
        mHandler.handleTouchEvent(downEventStylusOne, mContainerView);
        mHandler.handleTouchEvent(downEventStylusTwo, mContainerView);
        mHandler.handleTouchEvent(moveEventStylusTwo, mContainerView);
        mHandler.handleTouchEvent(upEventStylusTwo, mContainerView);
        verify(mInputMethodManager, never()).startStylusHandwriting(mContainerView);
        mHandler.handleTouchEvent(moveEventStylusOne, mContainerView);
        verify(mInputMethodManager).startStylusHandwriting(mContainerView);
    }

    // Testing that if the tool type is something other than a Stylus, startStylusHandwriting
    // should not be called.
    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void touchEventsDoNotTriggerHandwriting() {
        int initX = 10;
        int initY = 10;
        MotionEvent downEvent =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_FINGER, initX, initY, 0, 1);
        int x = initX + 10;
        int y = initY + 10;
        MotionEvent moveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_FINGER, x, y, 1, 1);
        MotionEvent upEvent =
                createMotionEvent(MotionEvent.ACTION_UP, MotionEvent.TOOL_TYPE_FINGER, x, y, 1, 1);
        mHandler.handleTouchEvent(downEvent, mContainerView);
        mHandler.handleTouchEvent(moveEvent, mContainerView);
        mHandler.handleTouchEvent(upEvent, mContainerView);
        verify(mInputMethodManager, never()).startStylusHandwriting(mContainerView);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    @EnableFeatures("UseHandwritingInitiator")
    public void handwritingIsNotTriggeredIfViewIsNotWritable() {
        int initX = 10;
        int initY = 10;
        MotionEvent downEvent =
                createMotionEvent(
                        MotionEvent.ACTION_DOWN, MotionEvent.TOOL_TYPE_STYLUS, initX, initY, 0, 1);
        int x = initX + 10;
        int y = initY + 10;
        MotionEvent moveEvent =
                createMotionEvent(
                        MotionEvent.ACTION_MOVE, MotionEvent.TOOL_TYPE_STYLUS, x, y, 1, 1);

        StylusHandwritingInitiator stylusHandwritingInitiatorSpy =
                spy(new StylusHandwritingInitiator(mInputMethodManager));
        doReturn(false).when(stylusHandwritingInitiatorSpy).isViewWritable();
        AndroidStylusWritingHandler androidStylusWritingHandler =
                new AndroidStylusWritingHandler(mContext);
        androidStylusWritingHandler.setHandwritingInitiatorForTesting(
                stylusHandwritingInitiatorSpy);

        androidStylusWritingHandler.handleTouchEvent(downEvent, mContainerView);
        androidStylusWritingHandler.handleTouchEvent(moveEvent, mContainerView);
        verify(mInputMethodManager, never()).startStylusHandwriting(mContainerView);
    }

    public MotionEvent createMotionEvent(
            int actionType, int toolType, int x, int y, int eventTime, int deviceId) {
        MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
        properties.toolType = toolType;
        MotionEvent.PointerCoords pointerFirstCoords = new MotionEvent.PointerCoords();
        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1];
        pointerFirstCoords.x = x;
        pointerFirstCoords.y = y;
        pointerCoords[0] = pointerFirstCoords;

        properties.id = 0;

        MotionEvent stylusEvent =
                MotionEvent.obtain(
                        SystemClock.uptimeMillis(),
                        eventTime,
                        actionType,
                        1,
                        new MotionEvent.PointerProperties[] {properties},
                        pointerCoords,
                        0,
                        0,
                        0f,
                        0f,
                        deviceId,
                        0,
                        2,
                        0);

        return stylusEvent;
    }
}