chromium/components/stylus_handwriting/android/junit/src/org/chromium/components/stylus_handwriting/AndroidStylusWritingHandlerTest.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.components.stylus_handwriting;

import static android.view.PointerIcon.TYPE_HANDWRITING;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

import android.content.ComponentName;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.view.ViewGroup;
import android.view.inputmethod.EditorBoundsInfo;
import android.view.inputmethod.InputMethodInfo;
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.HistogramWatcher;
import org.chromium.components.stylus_handwriting.test_support.ShadowGlobalSettings;
import org.chromium.components.stylus_handwriting.test_support.ShadowSecureSettings;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewAndroidDelegate;

import java.util.List;

/** Unit tests for {@link AndroidStylusWritingHandler}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        sdk = Build.VERSION_CODES.TIRAMISU,
        shadows = {ShadowGlobalSettings.class, ShadowSecureSettings.class})
public class AndroidStylusWritingHandlerTest {
    @Mock private Context mContext;
    @Mock private InputMethodManager mInputMethodManager;
    @Mock private InputMethodInfo mInputMethodInfo;

    private AndroidStylusWritingHandler mHandler;

    @Before
    public void setUp() {
        openMocks(this);
        when(mContext.getSystemService(InputMethodManager.class)).thenReturn(mInputMethodManager);
        when(mInputMethodManager.getInputMethodList()).thenReturn(List.of(mInputMethodInfo));
        when(mInputMethodInfo.getComponent())
                .thenReturn(
                        ComponentName.unflattenFromString(
                                ShadowSecureSettings.DEFAULT_IME_PACKAGE));
        mHandler = new AndroidStylusWritingHandler(mContext);
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.S)
    public void handlerIsDisabled_beforeT() {
        assertFalse(AndroidStylusWritingHandler.isEnabled(mContext));
    }

    @Test
    public void handlerIsDisabled_withFeatureDisabled() {
        when(mInputMethodInfo.supportsStylusHandwriting()).thenReturn(true);
        ShadowGlobalSettings.setHandwritingEnabled(false);
        assertFalse(AndroidStylusWritingHandler.isEnabled(mContext));
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    public void handlerUsesSecureSettings_onAndroidU() {
        when(mInputMethodInfo.supportsStylusHandwriting()).thenReturn(true);
        ShadowGlobalSettings.setHandwritingEnabled(false);
        ShadowSecureSettings.setHandwritingEnabled(true);
        assertTrue(AndroidStylusWritingHandler.isEnabled(mContext));
    }

    @Test
    public void handlerIsDisabled_noKeyboard() {
        ShadowGlobalSettings.setHandwritingEnabled(true);
        when(mInputMethodManager.getInputMethodList()).thenReturn(List.of());
        assertFalse(AndroidStylusWritingHandler.isEnabled(mContext));
    }

    @Test
    public void handlerIsDisabled_noKeyboardSupport() {
        when(mInputMethodInfo.supportsStylusHandwriting()).thenReturn(false);
        ShadowGlobalSettings.setHandwritingEnabled(true);
        assertFalse(AndroidStylusWritingHandler.isEnabled(mContext));
    }

    @Test
    public void handlerIsEnabled_withFeatureEnabledAndKeyboardSupport() {
        when(mInputMethodInfo.supportsStylusHandwriting()).thenReturn(true);
        ShadowGlobalSettings.setHandwritingEnabled(true);
        assertTrue(AndroidStylusWritingHandler.isEnabled(mContext));
    }

    @Test
    public void webContentsChanged_StylusWritingHandlerIsSet() {
        WebContents webContents = mock(WebContents.class);
        ViewGroup containerView = mock(ViewGroup.class);
        ViewAndroidDelegate viewAndroidDelegate =
                ViewAndroidDelegate.createBasicDelegate(containerView);
        when(webContents.getViewAndroidDelegate()).thenReturn(viewAndroidDelegate);

        mHandler.onWebContentsChanged(mContext, webContents);
        verify(webContents).setStylusWritingHandler(eq(mHandler));
    }

    @Test
    public void testAutoHandwritingIsDisabled() {
        WebContents webContents = mock(WebContents.class);
        ViewGroup containerView = mock(ViewGroup.class);
        ViewAndroidDelegate viewAndroidDelegate =
                ViewAndroidDelegate.createBasicDelegate(containerView);
        when(webContents.getViewAndroidDelegate()).thenReturn(viewAndroidDelegate);

        mHandler.onWebContentsChanged(mContext, webContents);
        // Ensure we notify Android that we handle our own handwriting.
        verify(containerView).setAutoHandwritingEnabled(false);
    }

    @Test
    public void testShowSoftKeyboardEnabled() {
        assertTrue(mHandler.canShowSoftKeyboard());
    }

    @Test
    public void editElementFocusedReturnsDips() {
        Rect boundsInPix = new Rect(20, 20, 80, 80);
        Point cursorPositionInPix = new Point(40, 40);
        // onEditElementFocusedForStylusWriting receives coordinates in pixels and should convert
        // them to DIPs. It does not use the contentOffset as this is taken into account by
        // CursorAnchorInfo's matrix.
        EditorBoundsInfo editorBoundsInfo =
                mHandler.onEditElementFocusedForStylusWriting(
                        boundsInPix, cursorPositionInPix, 4, 10, null);
        assertEquals(new RectF(5, 5, 20, 20), editorBoundsInfo.getEditorBounds());
        assertEquals(new RectF(5, 5, 20, 20), editorBoundsInfo.getHandwritingBounds());
    }

    @Test
    public void onFocusedNodeChangedReturnsDips() {
        Rect boundsInDips = new Rect(20, 20, 80, 80);
        // onFocusedNodeChanged receives coordinates in DIPs and should return them in the same
        // format. It does not use the contentOffset as this is taken into account by
        // CursorAnchorInfo's matrix.
        EditorBoundsInfo editorBoundsInfo =
                mHandler.onFocusedNodeChanged(boundsInDips, true, null, 4, 10);
        assertEquals(new RectF(boundsInDips), editorBoundsInfo.getEditorBounds());
        assertEquals(new RectF(boundsInDips), editorBoundsInfo.getHandwritingBounds());
    }

    /**
     * Verify that stylus handwriting is started after edit element is focused so that
     * InputConnection is current focused element. See https://crbug.com/1512519
     */
    @Test
    public void testStartStylusHandwriting() {
        ViewGroup containerView = mock(ViewGroup.class);
        Rect boundsInPix = new Rect(20, 20, 80, 80);
        Point cursorPositionInPix = new Point(40, 40);
        mHandler.onEditElementFocusedForStylusWriting(
                boundsInPix, cursorPositionInPix, 4, 10, containerView);
        verify(mInputMethodManager).startStylusHandwriting(containerView);
    }

    @Test
    public void testStylusHandwritingLogsApiOption() {
        ViewGroup containerView = mock(ViewGroup.class);
        Rect boundsInPix = new Rect(20, 20, 80, 80);
        Point cursorPositionInPix = new Point(40, 40);
        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "InputMethod.StylusHandwriting.Triggered", StylusApiOption.Api.ANDROID);
        mHandler.onEditElementFocusedForStylusWriting(
                boundsInPix, cursorPositionInPix, 4, 10, containerView);
        histogramWatcher.assertExpected();
    }

    @Test
    public void testShouldInitiateStylusWriting() {
        assertTrue(mHandler.shouldInitiateStylusWriting());
        verify(mInputMethodManager, never()).startStylusHandwriting(any());
    }

    @Test
    public void testHandlerUsesAndroidIcon() {
        assertEquals(TYPE_HANDWRITING, mHandler.getStylusPointerIcon());
    }
}