chromium/components/stylus_handwriting/android/junit/src/org/chromium/components/stylus_handwriting/DirectWritingTriggerTest.java

// Copyright 2022 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.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.ViewGroup;

import androidx.annotation.RequiresApi;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.components.stylus_handwriting.test_support.ShadowDirectWritingSettingsHelper;
import org.chromium.content_public.browser.StylusWritingImeCallback;
import org.chromium.content_public.browser.WebContents;

/** Unit tests for {@link DirectWritingTrigger}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {ShadowDirectWritingSettingsHelper.class})
public class DirectWritingTriggerTest {
    @Mock private WebContents mWebContents;
    @Mock private DirectWritingServiceBinder mDwServiceBinder;
    @Mock private StylusWritingImeCallback mStylusWritingImeCallback;
    @Mock private ViewGroup mContainerView;
    @Mock private DirectWritingServiceCallback mDwServiceCallback;

    private Context mContext;
    private DirectWritingTrigger mDwTrigger;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mDwTrigger = spy(new DirectWritingTrigger());
        mDwTrigger.setServiceBinderForTest(mDwServiceBinder);
        doReturn(mContainerView).when(mStylusWritingImeCallback).getContainerView();
        mContext = RuntimeEnvironment.application;
        doReturn(mContext).when(mContainerView).getContext();

        // DirectWritingTrigger class comes into action only when Setting is enabled.
        ShadowDirectWritingSettingsHelper.setEnabled(true);
    }

    @After
    public void tearDown() {
        // Reset shadow settings.
        ShadowDirectWritingSettingsHelper.setEnabled(false);
    }

    private MotionEvent getMotionEvent(int toolType, int action) {
        MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[1];
        MotionEvent.PointerProperties pp1 = new MotionEvent.PointerProperties();
        pp1.id = 0;
        pp1.toolType = toolType;
        pointerProperties[0] = pp1;
        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1];
        MotionEvent.PointerCoords pc = new MotionEvent.PointerCoords();
        pc.x = 0;
        pc.y = 0;
        pointerCoords[0] = pc;
        return MotionEvent.obtain(
                SystemClock.uptimeMillis(),
                SystemClock.uptimeMillis() + 1,
                action,
                1,
                pointerProperties,
                pointerCoords,
                0,
                0,
                1.0f,
                1.0f,
                0,
                0,
                0,
                0);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnWebContentsChanged() {
        // Test that settings are updated and callback is created if null, when WebContents is set.
        mDwTrigger.setServiceCallbackForTest(mDwServiceCallback);
        doReturn(mStylusWritingImeCallback).when(mWebContents).getStylusWritingImeCallback();
        mDwTrigger.onWebContentsChanged(mContext, mWebContents);
        verify(mDwTrigger).updateDWSettings(mContext);
        verify(mWebContents).setStylusWritingHandler(mDwTrigger);
        verify(mWebContents).getStylusWritingImeCallback();
        verify(mDwServiceCallback).setImeCallback(mStylusWritingImeCallback);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnImeAdapterDestroyed() {
        // Set Ime callback via onWebContentsChanged.
        doReturn(mStylusWritingImeCallback).when(mWebContents).getStylusWritingImeCallback();
        mDwTrigger.onWebContentsChanged(mContext, mWebContents);
        assertNotNull(mDwTrigger.getStylusWritingImeCallbackForTest());

        mDwTrigger.setServiceCallbackForTest(mDwServiceCallback);
        mDwTrigger.onImeAdapterDestroyed();
        assertNull(mDwTrigger.getStylusWritingImeCallbackForTest());
        verify(mDwServiceCallback).setImeCallback(null);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testServiceCallbackCreation() {
        // Test that callback is created when settings are updated.
        assertNull(mDwTrigger.getServiceCallback());
        mDwTrigger.updateDWSettings(mContext);
        assertNotNull(mDwTrigger.getServiceCallback());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testCanShowSoftKeyboard() {
        assertFalse(mDwTrigger.canShowSoftKeyboard());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnFocusChanged_lostFocus() {
        ShadowDirectWritingSettingsHelper.setEnabled(false);
        mDwTrigger.updateDWSettings(mContext);

        mDwTrigger.onFocusChanged(false);
        // hide toolbar is not called when feature is disabled.
        verify(mDwServiceBinder, never()).hideDWToolbar();
        // stop recognition is not called until StylusWritingImeCallback is set.
        verify(mDwServiceBinder, never()).onStopRecognition(any(), any(), any());

        ShadowDirectWritingSettingsHelper.setEnabled(true);
        mDwTrigger.updateDWSettings(mContext);
        mDwTrigger.onFocusChanged(false);
        verify(mDwServiceBinder).hideDWToolbar();
        // stop recognition is not called until StylusWritingImeCallback is set.
        verify(mDwServiceBinder, never()).onStopRecognition(any(), any(), any());

        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        // Set Ime callback via onWebContentsChanged.
        doReturn(mStylusWritingImeCallback).when(mWebContents).getStylusWritingImeCallback();
        mDwTrigger.onWebContentsChanged(mContext, mWebContents);
        mDwTrigger.onFocusChanged(false);
        verify(mDwServiceBinder).onStopRecognition(null, null, mContainerView);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnFocusChanged_gainFocus() {
        mDwTrigger.updateDWSettings(mContext);
        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        // No action when focus is gained.
        mDwTrigger.onFocusChanged(true);
        verify(mDwServiceBinder, never()).hideDWToolbar();
        verify(mDwServiceBinder, never()).onStopRecognition(any(), any(), any());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testShouldInitiateStylusWriting() {
        mDwTrigger.updateDWSettings(mContext);
        // requestStartStylusWriting returns false until service is connected.
        // Pass view = null as DW doesn't use the view.
        assertFalse(mDwTrigger.shouldInitiateStylusWriting());
        assertFalse(mDwTrigger.stylusWritingDetected());

        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        assertTrue(mDwTrigger.shouldInitiateStylusWriting());
        assertTrue(mDwTrigger.stylusWritingDetected());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnDetachedFromWindow() {
        // Unbind service is not called until service is connected, and settings is updated.
        mDwTrigger.onDetachedFromWindow(mContext);
        verify(mDwServiceBinder, never()).unbindService(any());

        mDwTrigger.updateDWSettings(mContext);
        mDwTrigger.onDetachedFromWindow(mContext);
        verify(mDwServiceBinder, never()).unbindService(any());

        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        mDwTrigger.onDetachedFromWindow(mContext);
        verify(mDwServiceBinder).unbindService(mContext);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    @RequiresApi(api = Build.VERSION_CODES.P)
    public void testHandleHoverEvent_bindServiceWithToolTypeStylus() {
        // Service is not bound until dw setting is updated as enabled, on Hover enter.
        MotionEvent hoverEnterEvent =
                getMotionEvent(MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.ACTION_HOVER_ENTER);
        mDwTrigger.handleHoverEvent(hoverEnterEvent, mContainerView);
        verify(mDwServiceBinder, never()).bindService(any(), any());

        mDwTrigger.updateDWSettings(mContext);
        // Service is bound only for Hover enter and not hover move.
        MotionEvent hoverMoveEvent =
                getMotionEvent(MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.ACTION_HOVER_MOVE);
        mDwTrigger.handleHoverEvent(hoverMoveEvent, mContainerView);
        verify(mDwServiceBinder, never()).bindService(any(), any());

        mDwTrigger.handleHoverEvent(hoverEnterEvent, mContainerView);
        verify(mDwServiceBinder).bindService(eq(mContext), any());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    @RequiresApi(api = Build.VERSION_CODES.P)
    public void testHandleHoverEvent_serviceAlreadyConnected() {
        MotionEvent me =
                getMotionEvent(MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.ACTION_HOVER_ENTER);
        // Service is not bound if it is already connected.
        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        mDwTrigger.handleHoverEvent(me, mContainerView);
        verify(mDwServiceBinder, never()).bindService(any(), any());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    @RequiresApi(api = Build.VERSION_CODES.P)
    public void testHandleHoverEvent_bindServiceWithToolTypeEraser() {
        mDwTrigger.updateDWSettings(mContext);
        MotionEvent me =
                getMotionEvent(MotionEvent.TOOL_TYPE_ERASER, MotionEvent.ACTION_HOVER_ENTER);
        mDwTrigger.handleHoverEvent(me, mContainerView);
        verify(mDwServiceBinder).bindService(eq(mContext), any());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    @RequiresApi(api = Build.VERSION_CODES.P)
    public void testHandleHoverEvent_serviceNotConnectedForNonStylusEvent() {
        mDwTrigger.updateDWSettings(mContext);
        MotionEvent mouseMoveEvent =
                getMotionEvent(MotionEvent.TOOL_TYPE_MOUSE, MotionEvent.ACTION_HOVER_ENTER);
        mDwTrigger.handleHoverEvent(mouseMoveEvent, mContainerView);
        verify(mDwServiceBinder, never()).bindService(any(), any());
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnWindowFocusChanged_gainFocus() {
        // Test behaviour when window gains focus with DW setting disabled.
        ShadowDirectWritingSettingsHelper.setEnabled(false);
        mDwTrigger.onWindowFocusChanged(mContext, true);
        verify(mDwTrigger).updateDWSettings(mContext);
        verify(mDwServiceBinder, never()).hideDWToolbar();
        verify(mDwServiceBinder, never()).onWindowFocusChanged(any(), anyBoolean());

        ShadowDirectWritingSettingsHelper.setEnabled(true);
        mDwTrigger.onWindowFocusChanged(mContext, true);
        verify(mDwServiceBinder, never()).hideDWToolbar();
        verify(mDwServiceBinder).onWindowFocusChanged(mContext, true);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testOnWindowFocusChanged_lostFocus() {
        // Test behaviour when window loses focus with DW setting disabled.
        ShadowDirectWritingSettingsHelper.setEnabled(false);
        mDwTrigger.onWindowFocusChanged(mContext, false);
        verify(mDwTrigger, never()).updateDWSettings(any());
        verify(mDwServiceBinder, never()).hideDWToolbar();
        verify(mDwServiceBinder, never()).onWindowFocusChanged(any(), anyBoolean());

        // Test behaviour when window loses focus with DW setting already enabled.
        ShadowDirectWritingSettingsHelper.setEnabled(true);
        mDwTrigger.updateDWSettings(mContext);

        mDwTrigger.onWindowFocusChanged(mContext, false);
        // Verify that updateDWSettings is not called again.
        verify(mDwTrigger, times(1)).updateDWSettings(mContext);
        verify(mDwServiceBinder).hideDWToolbar();
        verify(mDwServiceBinder).onWindowFocusChanged(mContext, false);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testFocusNodeChanged_isEditable() {
        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        ShadowDirectWritingSettingsHelper.setEnabled(true);
        mDwTrigger.updateDWSettings(mContext);
        mDwTrigger.setServiceCallbackForTest(mDwServiceCallback);
        // Simulate an ACTION_UP to check if stop recognition is called when editable is focused.
        MotionEvent me = getMotionEvent(MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.ACTION_UP);
        mDwTrigger.handleTouchEvent(me, mContainerView);

        Rect editableBounds = new Rect(0, 0, 20, 20);
        ArgumentCaptor<MotionEvent> eventReceived = ArgumentCaptor.forClass(MotionEvent.class);
        mDwTrigger.onFocusedNodeChanged(editableBounds, true, mContainerView, 2, 5);
        Rect scaledBounds =
                new Rect(
                        editableBounds.left * 2,
                        editableBounds.top * 2 + 5,
                        editableBounds.right * 2,
                        editableBounds.bottom * 2 + 5);
        verify(mDwServiceCallback).updateEditableBounds(eq(scaledBounds), any());
        verify(mDwServiceBinder).updateEditableBounds(scaledBounds, mContainerView, true);
        verify(mDwServiceBinder)
                .onStopRecognition(eventReceived.capture(), eq(scaledBounds), eq(mContainerView));
        assertEquals(eventReceived.getValue().getAction(), MotionEvent.ACTION_UP);
    }

    @Test
    @Feature({"Stylus Handwriting"})
    public void testFocusNodeChanged_isNotEditable() {
        doReturn(true).when(mDwServiceBinder).isServiceConnected();
        ShadowDirectWritingSettingsHelper.setEnabled(true);
        mDwTrigger.updateDWSettings(mContext);
        mDwTrigger.setServiceCallbackForTest(mDwServiceCallback);
        // Simulate an ACTION_UP to verify hide DW toolbar is called when node is not editable.
        MotionEvent me = getMotionEvent(MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.ACTION_UP);
        mDwTrigger.handleTouchEvent(me, mContainerView);

        Rect editableBounds = new Rect(0, 0, 20, 20);
        mDwTrigger.onFocusedNodeChanged(editableBounds, false, mContainerView, 1, 20);
        editableBounds.offset(0, 20);
        verify(mDwServiceCallback).updateEditableBounds(eq(editableBounds), any());
        // Verify that hide DW toolbar is called and stop recognition is also called.
        verify(mDwServiceBinder).hideDWToolbar();
        verify(mDwServiceBinder).onStopRecognition(null, null, mContainerView);
        verify(mDwServiceBinder, never())
                .updateEditableBounds(editableBounds, mContainerView, true);
        verify(mDwServiceBinder, never())
                .onStopRecognition(any(), eq(editableBounds), eq(mContainerView));
    }
}