chromium/components/stylus_handwriting/android/java/src/org/chromium/components/stylus_handwriting/AndroidStylusWritingHandler.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 android.view.PointerIcon.TYPE_HANDWRITING;

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.provider.Settings;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorBoundsInfo;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;

import androidx.annotation.RequiresApi;

import org.chromium.base.Log;
import org.chromium.content_public.browser.StylusWritingHandler;
import org.chromium.content_public.browser.WebContents;

import java.util.List;

/** Allows stylus handwriting using the Android stylus writing APIs introduced in Android T. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class AndroidStylusWritingHandler implements StylusWritingHandler, StylusApiOption {
    private static final String TAG = "AndroidStylus";

    private final InputMethodManager mInputMethodManager;

    private StylusHandwritingInitiator mStylusHandwritingInitiator;

    public static boolean isEnabled(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return false;

        int value = -1;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            value =
                    Settings.Secure.getInt(
                            context.getContentResolver(), "stylus_handwriting_enabled", 1);
        } else {
            value =
                    Settings.Global.getInt(
                            context.getContentResolver(), "stylus_handwriting_enabled", -1);
        }

        if (value != 1) {
            Log.d(TAG, "Stylus feature disabled.", value);
            return false;
        }

        InputMethodManager inputMethodManager = context.getSystemService(InputMethodManager.class);
        List<InputMethodInfo> inputMethods = inputMethodManager.getInputMethodList();
        String defaultIme =
                Settings.Secure.getString(
                        context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);

        if (defaultIme == null) {
            Log.d(
                    TAG,
                    "Stylus handwriting feature is not supported as "
                            + "default IME could not be fetched.");
            return false;
        }

        ComponentName defaultImePackage = ComponentName.unflattenFromString(defaultIme);

        for (InputMethodInfo inputMethod : inputMethods) {
            if (!inputMethod.getComponent().equals(defaultImePackage)) continue;

            boolean result = inputMethod.supportsStylusHandwriting();

            Log.d(TAG, "Stylus feature supported by IME: %s", result);
            return result;
        }

        Log.d(TAG, "Couldn't find IME");
        return false;
    }

    AndroidStylusWritingHandler(Context context) {
        mInputMethodManager = context.getSystemService(InputMethodManager.class);
        mStylusHandwritingInitiator = new StylusHandwritingInitiator(mInputMethodManager);
    }

    @Override
    public void onWebContentsChanged(Context context, WebContents webContents) {
        webContents.setStylusWritingHandler(this);

        Log.d(TAG, "Setting on web contents, %s", webContents.getViewAndroidDelegate() != null);
        if (webContents.getViewAndroidDelegate() == null) return;

        View view = webContents.getViewAndroidDelegate().getContainerView();
        view.setAutoHandwritingEnabled(false);
    }

    @Override
    public boolean handleTouchEvent(MotionEvent event, View currentView) {
        return mStylusHandwritingInitiator.onTouchEvent(event, currentView);
    }

    @Override
    public boolean canShowSoftKeyboard() {
        return true;
    }

    @Override
    public boolean shouldInitiateStylusWriting() {
        return true;
    }

    @Override
    public EditorBoundsInfo onEditElementFocusedForStylusWriting(
            Rect focusedEditBounds,
            Point cursorPosition,
            float scaleFactor,
            int contentOffsetY,
            View view) {
        Log.d(TAG, "Start Stylus Writing");
        StylusApiOption.recordStylusHandwritingTriggered(Api.ANDROID);
        // Start stylus writing after edit element is focused so that InputConnection is current
        // focused element.
        mInputMethodManager.startStylusHandwriting(view);
        RectF bounds =
                new RectF(
                        focusedEditBounds.left / scaleFactor,
                        focusedEditBounds.top / scaleFactor,
                        focusedEditBounds.right / scaleFactor,
                        focusedEditBounds.bottom / scaleFactor);
        return new EditorBoundsInfo.Builder()
                .setEditorBounds(bounds)
                .setHandwritingBounds(bounds)
                .build();
    }

    @Override
    public EditorBoundsInfo onFocusedNodeChanged(
            Rect editableBoundsOnScreenDip,
            boolean isEditable,
            View currentView,
            float scaleFactor,
            int contentOffsetY) {
        RectF bounds = new RectF(editableBoundsOnScreenDip);
        return new EditorBoundsInfo.Builder()
                .setEditorBounds(bounds)
                .setHandwritingBounds(bounds)
                .build();
    }

    @Override
    public int getStylusPointerIcon() {
        return TYPE_HANDWRITING;
    }

    void setHandwritingInitiatorForTesting(StylusHandwritingInitiator stylusHandwritingInitiator) {
        mStylusHandwritingInitiator = stylusHandwritingInitiator;
    }
}