Scribe (Android stylus handwriting text input) (#52943)

The engine API for Android's Scribe stylus handwriting input. Just bare bones handwriting itself, does not support special gestures, which will come in subsequent PR(s).
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 2f9eb9f..cc48375 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -44294,6 +44294,7 @@
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java + ../../../flutter/LICENSE
@@ -44317,6 +44318,7 @@
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java + ../../../flutter/LICENSE
@@ -47176,6 +47178,7 @@
 FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java
@@ -47202,6 +47205,7 @@
 FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java
 FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 6f06243..a0745b4 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -294,6 +294,7 @@
   "io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
   "io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java",
   "io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
+  "io/flutter/embedding/engine/systemchannels/ScribeChannel.java",
   "io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
   "io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java",
   "io/flutter/embedding/engine/systemchannels/SystemChannel.java",
@@ -320,6 +321,7 @@
   "io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java",
   "io/flutter/plugin/editing/InputConnectionAdaptor.java",
   "io/flutter/plugin/editing/ListenableEditingState.java",
+  "io/flutter/plugin/editing/ScribePlugin.java",
   "io/flutter/plugin/editing/SpellCheckPlugin.java",
   "io/flutter/plugin/editing/TextEditingDelta.java",
   "io/flutter/plugin/editing/TextInputPlugin.java",
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index 0d5ae3a..3f3f047 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -63,6 +63,7 @@
 import io.flutter.embedding.engine.renderer.RenderSurface;
 import io.flutter.embedding.engine.systemchannels.SettingsChannel;
 import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.editing.ScribePlugin;
 import io.flutter.plugin.editing.SpellCheckPlugin;
 import io.flutter.plugin.editing.TextInputPlugin;
 import io.flutter.plugin.localization.LocalizationPlugin;
@@ -133,6 +134,7 @@
   @Nullable private MouseCursorPlugin mouseCursorPlugin;
   @Nullable private TextInputPlugin textInputPlugin;
   @Nullable private SpellCheckPlugin spellCheckPlugin;
+  @Nullable private ScribePlugin scribePlugin;
   @Nullable private LocalizationPlugin localizationPlugin;
   @Nullable private KeyboardManager keyboardManager;
   @Nullable private AndroidTouchProcessor androidTouchProcessor;
@@ -1120,10 +1122,12 @@
     if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
       mouseCursorPlugin = new MouseCursorPlugin(this, this.flutterEngine.getMouseCursorChannel());
     }
+
     textInputPlugin =
         new TextInputPlugin(
             this,
             this.flutterEngine.getTextInputChannel(),
+            this.flutterEngine.getScribeChannel(),
             this.flutterEngine.getPlatformViewsController());
 
     try {
@@ -1136,6 +1140,10 @@
       Log.e(TAG, "TextServicesManager not supported by device, spell check disabled.");
     }
 
+    scribePlugin =
+        new ScribePlugin(
+            this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel());
+
     localizationPlugin = this.flutterEngine.getLocalizationPlugin();
 
     keyboardManager = new KeyboardManager(this);
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
index 4c80b90..683dfea 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
@@ -34,6 +34,7 @@
 import io.flutter.embedding.engine.systemchannels.PlatformChannel;
 import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
 import io.flutter.embedding.engine.systemchannels.RestorationChannel;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.SettingsChannel;
 import io.flutter.embedding.engine.systemchannels.SpellCheckChannel;
 import io.flutter.embedding.engine.systemchannels.SystemChannel;
@@ -100,6 +101,7 @@
   @NonNull private final RestorationChannel restorationChannel;
   @NonNull private final PlatformChannel platformChannel;
   @NonNull private final ProcessTextChannel processTextChannel;
+  @NonNull private final ScribeChannel scribeChannel;
   @NonNull private final SettingsChannel settingsChannel;
   @NonNull private final SpellCheckChannel spellCheckChannel;
   @NonNull private final SystemChannel systemChannel;
@@ -337,6 +339,7 @@
     platformChannel = new PlatformChannel(dartExecutor);
     processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager());
     restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
+    scribeChannel = new ScribeChannel(dartExecutor);
     settingsChannel = new SettingsChannel(dartExecutor);
     spellCheckChannel = new SpellCheckChannel(dartExecutor);
     systemChannel = new SystemChannel(dartExecutor);
@@ -610,6 +613,12 @@
     return textInputChannel;
   }
 
+  /** System channel that sends and receives Scribe requests and results. */
+  @NonNull
+  public ScribeChannel getScribeChannel() {
+    return scribeChannel;
+  }
+
   /** System channel that sends and receives spell check requests and results. */
   @NonNull
   public SpellCheckChannel getSpellCheckChannel() {
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
new file mode 100644
index 0000000..7070000
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
@@ -0,0 +1,147 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.embedding.engine.systemchannels;
+
+import static io.flutter.Build.API_LEVELS;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.Log;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+/**
+ * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate the
+ * Scribe handwriting text input feature.
+ */
+public class ScribeChannel {
+  private static final String TAG = "ScribeChannel";
+
+  @VisibleForTesting
+  public static final String METHOD_IS_FEATURE_AVAILABLE = "Scribe.isFeatureAvailable";
+
+  @VisibleForTesting
+  public static final String METHOD_IS_STYLUS_HANDWRITING_AVAILABLE =
+      "Scribe.isStylusHandwritingAvailable";
+
+  @VisibleForTesting
+  public static final String METHOD_START_STYLUS_HANDWRITING = "Scribe.startStylusHandwriting";
+
+  public final MethodChannel channel;
+  private ScribeMethodHandler scribeMethodHandler;
+
+  @NonNull
+  public final MethodChannel.MethodCallHandler parsingMethodHandler =
+      new MethodChannel.MethodCallHandler() {
+        @Override
+        public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+          if (scribeMethodHandler == null) {
+            Log.v(TAG, "No ScribeMethodHandler registered. Scribe call not handled.");
+            return;
+          }
+          String method = call.method;
+          Log.v(TAG, "Received '" + method + "' message.");
+          switch (method) {
+            case METHOD_IS_FEATURE_AVAILABLE:
+              isFeatureAvailable(call, result);
+              break;
+            case METHOD_IS_STYLUS_HANDWRITING_AVAILABLE:
+              isStylusHandwritingAvailable(call, result);
+              break;
+            case METHOD_START_STYLUS_HANDWRITING:
+              startStylusHandwriting(call, result);
+              break;
+            default:
+              result.notImplemented();
+              break;
+          }
+        }
+      };
+
+  private void isFeatureAvailable(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+    try {
+      final boolean isAvailable = scribeMethodHandler.isFeatureAvailable();
+      result.success(isAvailable);
+    } catch (IllegalStateException exception) {
+      result.error("error", exception.getMessage(), null);
+    }
+  }
+
+  private void isStylusHandwritingAvailable(
+      @NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+    if (Build.VERSION.SDK_INT < API_LEVELS.API_34) {
+      result.error("error", "Requires API level 34 or higher.", null);
+      return;
+    }
+
+    try {
+      final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable();
+      result.success(isAvailable);
+    } catch (IllegalStateException exception) {
+      result.error("error", exception.getMessage(), null);
+    }
+  }
+
+  private void startStylusHandwriting(
+      @NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+    if (Build.VERSION.SDK_INT < API_LEVELS.API_33) {
+      result.error("error", "Requires API level 33 or higher.", null);
+      return;
+    }
+
+    try {
+      scribeMethodHandler.startStylusHandwriting();
+      result.success(null);
+    } catch (IllegalStateException exception) {
+      result.error("error", exception.getMessage(), null);
+    }
+  }
+
+  public ScribeChannel(@NonNull DartExecutor dartExecutor) {
+    channel = new MethodChannel(dartExecutor, "flutter/scribe", JSONMethodCodec.INSTANCE);
+    channel.setMethodCallHandler(parsingMethodHandler);
+  }
+
+  /**
+   * Sets the {@link ScribeMethodHandler} which receives all requests for scribe sent through this
+   * channel.
+   */
+  public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHandler) {
+    this.scribeMethodHandler = scribeMethodHandler;
+  }
+
+  public interface ScribeMethodHandler {
+    /**
+     * Responds to the {@code result} with success and a boolean indicating whether or not stylus
+     * handwriting is available.
+     */
+    boolean isFeatureAvailable();
+
+    /**
+     * Responds to the {@code result} with success and a boolean indicating whether or not stylus
+     * handwriting is available.
+     */
+    @TargetApi(API_LEVELS.API_34)
+    @RequiresApi(API_LEVELS.API_34)
+    boolean isStylusHandwritingAvailable();
+
+    /**
+     * Requests to start Scribe stylus handwriting, which will respond to the {@code result} with
+     * either success if handwriting input has started or error otherwise.
+     */
+    @TargetApi(API_LEVELS.API_33)
+    @RequiresApi(API_LEVELS.API_33)
+    void startStylusHandwriting();
+  }
+
+  // TODO(justinmc): Scribe stylus gestures should be supported here.
+  // https://github.com/flutter/flutter/issues/156018
+}
diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
index 1f688d7..aa96ea0 100644
--- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
+++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
@@ -33,6 +33,7 @@
 import androidx.core.view.inputmethod.InputConnectionCompat;
 import io.flutter.Log;
 import io.flutter.embedding.engine.FlutterJNI;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
 import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
@@ -51,6 +52,7 @@
 
   private final View mFlutterView;
   private final int mClient;
+  private final ScribeChannel scribeChannel;
   private final TextInputChannel textInputChannel;
   private final ListenableEditingState mEditable;
   private final EditorInfo mEditorInfo;
@@ -69,6 +71,7 @@
       View view,
       int client,
       TextInputChannel textInputChannel,
+      ScribeChannel scribeChannel,
       KeyboardDelegate keyboardDelegate,
       ListenableEditingState editable,
       EditorInfo editorInfo,
@@ -77,6 +80,7 @@
     mFlutterView = view;
     mClient = client;
     this.textInputChannel = textInputChannel;
+    this.scribeChannel = scribeChannel;
     mEditable = editable;
     mEditable.addEditingStateListener(this);
     mEditorInfo = editorInfo;
@@ -100,10 +104,19 @@
       View view,
       int client,
       TextInputChannel textInputChannel,
+      ScribeChannel scribeChannel,
       KeyboardDelegate keyboardDelegate,
       ListenableEditingState editable,
       EditorInfo editorInfo) {
-    this(view, client, textInputChannel, keyboardDelegate, editable, editorInfo, new FlutterJNI());
+    this(
+        view,
+        client,
+        textInputChannel,
+        scribeChannel,
+        keyboardDelegate,
+        editable,
+        editorInfo,
+        new FlutterJNI());
   }
 
   private ExtractedText getExtractedText(ExtractedTextRequest request) {
@@ -262,6 +275,10 @@
     return result;
   }
 
+  // TODO(justinmc): Scribe stylus gestures should be supported here via
+  // performHandwritingGesture.
+  // https://github.com/flutter/flutter/issues/156018
+
   // Sanitizes the index to ensure the index is within the range of the
   // contents of editable.
   private static int clampIndexToEditable(int index, Editable editable) {
diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
new file mode 100644
index 0000000..35fb2ff
--- /dev/null
+++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
@@ -0,0 +1,106 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugin.editing;
+
+import static io.flutter.Build.API_LEVELS;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
+
+/**
+ * {@link ScribePlugin} is the implementation of all functionality needed for handwriting stylus
+ * text input.
+ *
+ * <p>The plugin handles requests for scribe sent by the {@link
+ * io.flutter.embedding.engine.systemchannels.ScribeChannel}.
+ *
+ * <p>On API versions below 33, the plugin does nothing.
+ */
+public class ScribePlugin implements ScribeChannel.ScribeMethodHandler {
+
+  @NonNull private final ScribeChannel mScribeChannel;
+  @NonNull private final InputMethodManager mInputMethodManager;
+  @NonNull private View mView;
+
+  public ScribePlugin(
+      @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) {
+    if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
+      view.setAutoHandwritingEnabled(false);
+    }
+
+    mView = view;
+    mInputMethodManager = imm;
+    mScribeChannel = scribeChannel;
+
+    mScribeChannel.setScribeMethodHandler(this);
+  }
+
+  /**
+   * Sets the View in which Scribe input is handled.
+   *
+   * <p>Only one View can be set at any given time.
+   */
+  public void setView(@NonNull View view) {
+    if (view == mView) {
+      return;
+    }
+    mView = view;
+  }
+
+  /**
+   * Unregisters this {@code ScribePlugin} as the {@code ScribeChannel.ScribeMethodHandler}, for the
+   * {@link io.flutter.embedding.engine.systemchannels.ScribeChannel}.
+   *
+   * <p>Do not invoke any methods on a {@code ScribePlugin} after invoking this method.
+   */
+  public void destroy() {
+    mScribeChannel.setScribeMethodHandler(null);
+  }
+
+  /**
+   * Returns true if the InputMethodManager supports Scribe stylus handwriting input.
+   *
+   * <p>Call this or isFeatureAvailable before calling startStylusHandwriting to make sure it's
+   * available.
+   */
+  @TargetApi(API_LEVELS.API_34)
+  @RequiresApi(API_LEVELS.API_34)
+  @Override
+  public boolean isStylusHandwritingAvailable() {
+    return mInputMethodManager.isStylusHandwritingAvailable();
+  }
+
+  /**
+   * Starts stylus handwriting input.
+   *
+   * <p>Typically isStylusHandwritingAvailable should be called first to determine whether this is
+   * supported by the IME.
+   */
+  @TargetApi(API_LEVELS.API_33)
+  @RequiresApi(API_LEVELS.API_33)
+  @Override
+  public void startStylusHandwriting() {
+    mInputMethodManager.startStylusHandwriting(mView);
+  }
+
+  /**
+   * A convenience method to check if Scribe is available.
+   *
+   * <p>Differs from isStylusHandwritingAvailable in that it can be called from any API level
+   * without throwing an error.
+   *
+   * <p>Call this or isStylusHandwritingAvailable before calling startStylusHandwriting to make sure
+   * it's available.
+   */
+  @Override
+  public boolean isFeatureAvailable() {
+    return Build.VERSION.SDK_INT >= API_LEVELS.API_34 && isStylusHandwritingAvailable();
+  }
+}
diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
index beae708..41cd19e 100644
--- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
@@ -29,6 +29,7 @@
 import androidx.core.view.inputmethod.EditorInfoCompat;
 import io.flutter.Log;
 import io.flutter.embedding.android.KeyboardManager;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
 import io.flutter.plugin.platform.PlatformViewsController;
@@ -42,6 +43,7 @@
   @NonNull private final View mView;
   @NonNull private final InputMethodManager mImm;
   @NonNull private final AutofillManager afm;
+  @NonNull private final ScribeChannel scribeChannel;
   @NonNull private final TextInputChannel textInputChannel;
   @NonNull private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
   @Nullable private TextInputChannel.Configuration configuration;
@@ -66,6 +68,7 @@
   public TextInputPlugin(
       @NonNull View view,
       @NonNull TextInputChannel textInputChannel,
+      @NonNull ScribeChannel scribeChannel,
       @NonNull PlatformViewsController platformViewsController) {
     mView = view;
     // Create a default object.
@@ -153,6 +156,8 @@
 
     textInputChannel.requestExistingInputState();
 
+    this.scribeChannel = scribeChannel;
+
     this.platformViewsController = platformViewsController;
     this.platformViewsController.attachTextInputPlugin(this);
   }
@@ -341,9 +346,23 @@
       EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString);
     }
 
+    if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
+      EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true);
+    }
+    // TODO(justinmc): Scribe stylus gestures should be supported here via
+    // outAttrs.setSupportedHandwritingGestures and
+    // outAttrs.setSupportedHandwritingGesturePreviews.
+    // https://github.com/flutter/flutter/issues/156018
+
     InputConnectionAdaptor connection =
         new InputConnectionAdaptor(
-            view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs);
+            view,
+            inputTarget.id,
+            textInputChannel,
+            scribeChannel,
+            keyboardManager,
+            mEditable,
+            outAttrs);
     outAttrs.initialSelStart = mEditable.getSelectionStart();
     outAttrs.initialSelEnd = mEditable.getSelectionEnd();
 
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index 8dbdae1..d20ea56 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -56,6 +56,7 @@
 import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
 import io.flutter.embedding.engine.systemchannels.NavigationChannel;
 import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.SettingsChannel;
 import io.flutter.embedding.engine.systemchannels.SystemChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@@ -237,7 +238,11 @@
     PlatformViewsController platformViewsController =
         mNativeView.getPluginRegistry().getPlatformViewsController();
     mTextInputPlugin =
-        new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController);
+        new TextInputPlugin(
+            this,
+            new TextInputChannel(dartExecutor),
+            new ScribeChannel(dartExecutor),
+            platformViewsController);
     mKeyboardManager = new KeyboardManager(this);
 
     if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index 132aea4..ae1e371 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -49,6 +49,7 @@
 import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
 import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
 import io.flutter.embedding.engine.systemchannels.NavigationChannel;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.SettingsChannel;
 import io.flutter.embedding.engine.systemchannels.SystemChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@@ -1481,6 +1482,7 @@
     when(engine.getSettingsChannel()).thenReturn(fakeSettingsChannel);
     when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class));
     when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
+    when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class));
 
     return engine;
   }
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java
new file mode 100644
index 0000000..c69f624
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java
@@ -0,0 +1,202 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.embedding.engine.systemchannels;
+
+import static io.flutter.Build.API_LEVELS;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.TargetApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.FlutterException;
+import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
+import java.nio.ByteBuffer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public class ScribeChannelTest {
+  private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler(
+      BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) {
+    MethodCall methodCall = new MethodCall(method, null);
+    ByteBuffer encodedMethodCall = JSONMethodCodec.INSTANCE.encodeMethodCall(methodCall);
+    BinaryMessenger.BinaryReply mockReply = mock(BinaryMessenger.BinaryReply.class);
+    binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), mockReply);
+    return mockReply;
+  }
+
+  ScribeChannel.ScribeMethodHandler mockHandler;
+  BinaryMessenger.BinaryMessageHandler binaryMessageHandler;
+
+  @Before
+  public void setUp() {
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> binaryMessageHandlerCaptor =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
+    mockHandler = mock(ScribeChannel.ScribeMethodHandler.class);
+    ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger);
+
+    scribeChannel.setScribeMethodHandler(mockHandler);
+
+    verify((BinaryMessenger) mockBinaryMessenger, times(1))
+        .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
+
+    binaryMessageHandler = binaryMessageHandlerCaptor.getValue();
+  }
+
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void respondsToStartStylusHandwriting() {
+    BinaryMessenger.BinaryReply mockReply =
+        sendToBinaryMessageHandler(
+            binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING);
+
+    verify(mockReply)
+        .reply(
+            argThat(
+                (ByteBuffer reply) -> {
+                  reply.flip();
+                  try {
+                    final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+                    return decodedReply == null;
+                  } catch (FlutterException e) {
+                    return false;
+                  }
+                }));
+    verify(mockHandler).startStylusHandwriting();
+  }
+
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void respondsToIsFeatureAvailable() {
+    BinaryMessenger.BinaryReply mockReply =
+        sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_IS_FEATURE_AVAILABLE);
+
+    verify(mockReply)
+        .reply(
+            argThat(
+                (ByteBuffer reply) -> {
+                  reply.flip();
+                  try {
+                    final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+                    // Should succeed and should tell whether or not Scribe is available by
+                    // using a boolean.
+                    return decodedReply.getClass() == java.lang.Boolean.class;
+                  } catch (FlutterException e) {
+                    return false;
+                  }
+                }));
+    verify(mockHandler).isFeatureAvailable();
+  }
+
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void respondsToIsStylusHandwritingAvailable() {
+    BinaryMessenger.BinaryReply mockReply =
+        sendToBinaryMessageHandler(
+            binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE);
+
+    verify(mockReply)
+        .reply(
+            argThat(
+                (ByteBuffer reply) -> {
+                  reply.flip();
+                  try {
+                    final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+                    // Should succeed and should tell whether or not Scribe is available by
+                    // using a boolean.
+                    return decodedReply.getClass() == java.lang.Boolean.class;
+                  } catch (FlutterException e) {
+                    return false;
+                  }
+                }));
+    verify(mockHandler).isStylusHandwritingAvailable();
+  }
+
+  @Config(sdk = API_LEVELS.API_32)
+  @TargetApi(API_LEVELS.API_32)
+  @Test
+  public void respondsToStartStylusHandwritingWhenAPILevelUnsupported() {
+    BinaryMessenger.BinaryReply mockReply =
+        sendToBinaryMessageHandler(
+            binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING);
+
+    verify(mockReply)
+        .reply(
+            argThat(
+                (ByteBuffer reply) -> {
+                  reply.flip();
+                  try {
+                    final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+                    return false;
+                  } catch (FlutterException e) {
+                    // Should fail because the API version is too low.
+                    return true;
+                  }
+                }));
+    verify(mockHandler, never()).startStylusHandwriting();
+  }
+
+  @Config(sdk = API_LEVELS.API_33)
+  @TargetApi(API_LEVELS.API_33)
+  @Test
+  public void respondsToIsFeatureAvailableWhenAPILevelUnsupported() {
+    BinaryMessenger.BinaryReply mockReply =
+        sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_IS_FEATURE_AVAILABLE);
+
+    verify(mockReply)
+        .reply(
+            argThat(
+                (ByteBuffer reply) -> {
+                  reply.flip();
+                  try {
+                    final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+                    // Should succeed and indicate that Scribe is not available.
+                    return decodedReply.getClass() == java.lang.Boolean.class
+                        && !((boolean) decodedReply);
+                  } catch (FlutterException e) {
+                    return false;
+                  }
+                }));
+    verify(mockHandler).isFeatureAvailable();
+  }
+
+  @Config(sdk = API_LEVELS.API_33)
+  @TargetApi(API_LEVELS.API_33)
+  @Test
+  public void respondsToIsStylusHandwritingAvailableWhenAPILevelUnsupported() {
+    BinaryMessenger.BinaryReply mockReply =
+        sendToBinaryMessageHandler(
+            binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE);
+
+    verify(mockReply)
+        .reply(
+            argThat(
+                (ByteBuffer reply) -> {
+                  reply.flip();
+                  try {
+                    final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+                    return false;
+                  } catch (FlutterException e) {
+                    // Should fail because the API version is too low.
+                    return true;
+                  }
+                }));
+    verify(mockHandler, never()).isStylusHandwritingAvailable();
+  }
+}
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
index 1158f1d..8ca3469 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
@@ -43,6 +43,7 @@
 import io.flutter.embedding.android.KeyboardManager;
 import io.flutter.embedding.engine.FlutterJNI;
 import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
 import io.flutter.plugin.common.JSONMethodCodec;
 import io.flutter.plugin.common.MethodCall;
@@ -105,6 +106,7 @@
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
     int inputTargetId = 0;
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState mEditable = new ListenableEditingState(null, testView);
     Selection.setSelection(mEditable, 0, 0);
     ListenableEditingState spyEditable = spy(mEditable);
@@ -113,7 +115,13 @@
 
     InputConnectionAdaptor inputConnectionAdaptor =
         new InputConnectionAdaptor(
-            testView, inputTargetId, textInputChannel, mockKeyboardManager, spyEditable, outAttrs);
+            testView,
+            inputTargetId,
+            textInputChannel,
+            scribeChannel,
+            mockKeyboardManager,
+            spyEditable,
+            outAttrs);
 
     // Send an enter key and make sure the Editable received it.
     FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, '\n');
@@ -199,12 +207,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -260,12 +270,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -291,12 +303,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -328,12 +342,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -363,12 +379,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -401,12 +419,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -436,12 +456,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -475,12 +497,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -512,12 +536,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -547,12 +573,14 @@
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
     ListenableEditingState editable = sampleEditable(0, 0);
     InputConnectionAdaptor adaptor =
         new InputConnectionAdaptor(
             testView,
             client,
             textInputChannel,
+            scribeChannel,
             mockKeyboardManager,
             editable,
             null,
@@ -1080,6 +1108,7 @@
             testView,
             1,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             editable,
             new EditorInfo());
@@ -1131,6 +1160,7 @@
             testView,
             1,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             editable,
             new EditorInfo());
@@ -1277,6 +1307,7 @@
     View testView = new View(ApplicationProvider.getApplicationContext());
     int client = 0;
     TextInputChannel textInputChannel = mock(TextInputChannel.class);
+    ScribeChannel scribeChannel = mock(ScribeChannel.class);
     FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
     when(mockFlutterJNI.isCodePointEmoji(anyInt()))
         .thenAnswer((invocation) -> Emoji.isEmoji((int) invocation.getArguments()[0]));
@@ -1290,7 +1321,14 @@
         .thenAnswer(
             (invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0]));
     return new InputConnectionAdaptor(
-        testView, client, textInputChannel, mockKeyboardManager, editable, null, mockFlutterJNI);
+        testView,
+        client,
+        textInputChannel,
+        scribeChannel,
+        mockKeyboardManager,
+        editable,
+        null,
+        mockFlutterJNI);
   }
 
   private static class Emoji {
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
index 8001166..e867dd5 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
@@ -14,6 +14,7 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import io.flutter.embedding.android.KeyboardManager;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
 import java.util.ArrayList;
 import org.junit.Before;
@@ -280,6 +281,7 @@
             testView,
             0,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             editingState,
             new EditorInfo());
@@ -305,6 +307,7 @@
             testView,
             0,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             editingState,
             new EditorInfo());
@@ -339,6 +342,7 @@
             testView,
             0,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             editingState,
             new EditorInfo());
@@ -399,6 +403,7 @@
             testView,
             0,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             editingState,
             new EditorInfo());
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java
new file mode 100644
index 0000000..386c75a
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java
@@ -0,0 +1,106 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugin.editing;
+
+import static io.flutter.Build.API_LEVELS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public class ScribePluginTest {
+  private final Context ctx = ApplicationProvider.getApplicationContext();
+
+  ScribePlugin scribePlugin;
+  InputMethodManager mockImm;
+  View testView;
+
+  @Before
+  public void setUp() {
+    ScribeChannel mockScribeChannel = mock(ScribeChannel.class);
+    testView = new View(ctx);
+    mockImm = mock(InputMethodManager.class);
+    if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
+      when(mockImm.isStylusHandwritingAvailable()).thenReturn(true);
+    }
+    scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel);
+  }
+
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void scribePluginIsFeatureAvailable() {
+    assertEquals(scribePlugin.isFeatureAvailable(), true);
+
+    verify(mockImm).isStylusHandwritingAvailable();
+  }
+
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void scribePluginIsStylusHandwritingAvailable() {
+    assertEquals(scribePlugin.isStylusHandwritingAvailable(), true);
+
+    verify(mockImm).isStylusHandwritingAvailable();
+  }
+
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void scribePluginStartStylusHandwriting() {
+    scribePlugin.startStylusHandwriting();
+
+    verify(mockImm).startStylusHandwriting(testView);
+  }
+
+  @Config(sdk = API_LEVELS.API_32)
+  @TargetApi(API_LEVELS.API_32)
+  @Test
+  public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() {
+    assertNotNull(scribePlugin);
+
+    assertThrows(
+        NoSuchMethodError.class,
+        () -> {
+          scribePlugin.startStylusHandwriting();
+        });
+  }
+
+  @Config(sdk = API_LEVELS.API_33)
+  @TargetApi(API_LEVELS.API_33)
+  @Test
+  public void scribePluginIsFeatureAvailableWhenAPILevelUnsupported() {
+    assertEquals(scribePlugin.isFeatureAvailable(), false);
+  }
+
+  @Config(sdk = API_LEVELS.API_33)
+  @TargetApi(API_LEVELS.API_33)
+  @Test
+  public void scribePluginIsStylusHandwritingAvailableWhenAPILevelUnsupported() {
+    assertNotNull(scribePlugin);
+
+    assertThrows(
+        NoSuchMethodError.class,
+        () -> {
+          scribePlugin.isStylusHandwritingAvailable();
+        });
+  }
+}
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
index 2656902..77f71f0 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
@@ -2,6 +2,7 @@
 
 import static io.flutter.Build.API_LEVELS;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -45,6 +46,7 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
+import androidx.core.view.inputmethod.EditorInfoCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import io.flutter.embedding.android.FlutterView;
@@ -54,6 +56,7 @@
 import io.flutter.embedding.engine.dart.DartExecutor;
 import io.flutter.embedding.engine.loader.FlutterLoader;
 import io.flutter.embedding.engine.renderer.FlutterRenderer;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
 import io.flutter.plugin.common.BinaryMessenger;
@@ -133,8 +136,10 @@
     FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     ArgumentCaptor<String> channelCaptor = ArgumentCaptor.forClass(String.class);
     ArgumentCaptor<ByteBuffer> bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
@@ -152,8 +157,10 @@
     testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
     View testView = new View(ctx);
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -194,8 +201,10 @@
     testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
     View testView = new View(ctx);
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     // Here's no textInputPlugin.setTextInputClient()
     textInputPlugin.setTextInputEditingState(
@@ -211,8 +220,10 @@
     testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
     View testView = new View(ctx);
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -261,8 +272,10 @@
     EditorInfo outAttrs = new EditorInfo();
     outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     CharSequence newText = "I do not fear computers. I fear the lack of them.";
 
     // Change InputTarget to FRAMEWORK_CLIENT.
@@ -374,8 +387,10 @@
     EditorInfo outAttrs = new EditorInfo();
     outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     CharSequence newText = "I do not fear computers. I fear the lack of them.";
     final TextEditingDelta expectedDelta =
         new TextEditingDelta("", 0, 0, newText, newText.length(), newText.length(), 0, 49);
@@ -503,8 +518,10 @@
     EditorInfo outAttrs = new EditorInfo();
     outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     CharSequence newText = "I do not fear computers. I fear the lack of them.";
     final TextEditingDelta expectedDelta =
         new TextEditingDelta("", 0, 0, newText, newText.length(), newText.length(), 0, 49);
@@ -612,8 +629,10 @@
     EditorInfo outAttrs = new EditorInfo();
     outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     CharSequence newText = "I do not fear computers. I fear the lack of them.";
     final TextEditingDelta expectedDelta =
         new TextEditingDelta(
@@ -722,8 +741,10 @@
     EditorInfo outAttrs = new EditorInfo();
     outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     CharSequence newText = "helfo";
     final TextEditingDelta expectedDelta = new TextEditingDelta(newText, 0, 5, "hello", 5, 5, 0, 5);
 
@@ -829,8 +850,10 @@
     EditorInfo outAttrs = new EditorInfo();
     outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     // Change InputTarget to FRAMEWORK_CLIENT.
     textInputPlugin.setTextInputClient(
@@ -920,8 +943,10 @@
     testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
     View testView = new View(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -958,8 +983,10 @@
     testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
     View testView = new View(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1006,8 +1033,10 @@
     testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
     View testView = new View(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1107,8 +1136,10 @@
 
     View testView = new View(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1134,8 +1165,10 @@
   public void destroy_clearTextInputMethodHandler() {
     View testView = new View(ctx);
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     verify(textInputChannel, times(1)).setTextInputMethodHandler(isNotNull());
     textInputPlugin.destroy();
     verify(textInputChannel, times(1)).setTextInputMethodHandler(isNull());
@@ -1150,8 +1183,10 @@
     View testView = new View(ctx);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1225,8 +1260,10 @@
     View testView = new View(ctx);
     DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1263,8 +1300,10 @@
     View testView = new View(ctx);
     DartExecutor dartExecutor = mock(DartExecutor.class);
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1293,8 +1332,10 @@
     View testView = new View(ctx);
     DartExecutor dartExecutor = mock(DartExecutor.class);
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1321,8 +1362,10 @@
     View testView = new View(ctx);
     DartExecutor dartExecutor = mock(DartExecutor.class);
     TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     textInputPlugin.setTextInputClient(
         0,
         new TextInputChannel.Configuration(
@@ -1351,6 +1394,74 @@
             | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
   }
 
+  @Config(minSdk = API_LEVELS.API_34)
+  @TargetApi(API_LEVELS.API_34)
+  @Test
+  public void inputConnection_setsStylusHandwritingAvailable() {
+    View testView = new View(ctx);
+    DartExecutor dartExecutor = mock(DartExecutor.class);
+    TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
+    TextInputPlugin textInputPlugin =
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
+    textInputPlugin.setTextInputClient(
+        0,
+        new TextInputChannel.Configuration(
+            false,
+            false,
+            true,
+            true,
+            false,
+            TextInputChannel.TextCapitalization.NONE,
+            new TextInputChannel.InputType(TextInputChannel.TextInputType.MULTILINE, false, false),
+            null,
+            null,
+            null,
+            null,
+            null));
+
+    EditorInfo editorInfo = new EditorInfo();
+    InputConnection connection =
+        textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo);
+
+    assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+  }
+
+  @Config(sdk = API_LEVELS.API_32)
+  @TargetApi(API_LEVELS.API_32)
+  @Test
+  public void inputConnection_doesNotcallSetsStylusHandwritingAvailableWhenAPILevelUnsupported() {
+    View testView = new View(ctx);
+    DartExecutor dartExecutor = mock(DartExecutor.class);
+    TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
+    TextInputPlugin textInputPlugin =
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
+    textInputPlugin.setTextInputClient(
+        0,
+        new TextInputChannel.Configuration(
+            false,
+            false,
+            true,
+            true,
+            false,
+            TextInputChannel.TextCapitalization.NONE,
+            new TextInputChannel.InputType(TextInputChannel.TextInputType.MULTILINE, false, false),
+            null,
+            null,
+            null,
+            null,
+            null));
+
+    EditorInfo editorInfo = new EditorInfo();
+    InputConnection connection =
+        textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo);
+
+    assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+  }
+
   // -------- Start: Autofill Tests -------
   @Test
   public void autofill_enabledByDefault() {
@@ -1359,8 +1470,10 @@
     }
     FlutterView testView = new FlutterView(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     final TextInputChannel.Configuration.Autofill autofill =
         new TextInputChannel.Configuration.Autofill(
             "1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
@@ -1419,8 +1532,10 @@
     }
     FlutterView testView = new FlutterView(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     final TextInputChannel.Configuration.Autofill autofill =
         new TextInputChannel.Configuration.Autofill(
             "1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
@@ -1456,8 +1571,10 @@
     }
     FlutterView testView = new FlutterView(ctx);
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     final TextInputChannel.Configuration.Autofill autofill =
         new TextInputChannel.Configuration.Autofill(
             "1",
@@ -1501,8 +1618,10 @@
     }
     FlutterView testView = getTestView();
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     final TextInputChannel.Configuration.Autofill autofill1 =
         new TextInputChannel.Configuration.Autofill(
             "1",
@@ -1593,8 +1712,10 @@
     // Migrate to ActivityScenario by following https://github.com/robolectric/robolectric/pull/4736
     FlutterView testView = getTestView();
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     final TextInputChannel.Configuration.Autofill autofill =
         new TextInputChannel.Configuration.Autofill(
             "1",
@@ -1646,8 +1767,10 @@
     TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class));
     FlutterView testView = getTestView();
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     // Set up an autofill scenario with 2 fields.
     final TextInputChannel.Configuration.Autofill autofill1 =
@@ -1728,6 +1851,7 @@
             testView,
             0,
             mock(TextInputChannel.class),
+            mock(ScribeChannel.class),
             mockKeyboardManager,
             (ListenableEditingState) textInputPlugin.getEditable(),
             new EditorInfo());
@@ -1782,8 +1906,10 @@
     TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class));
     FlutterView testView = getTestView();
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     // Set up an autofill scenario with 2 fields.
     final TextInputChannel.Configuration.Autofill autofill1 =
@@ -1877,8 +2003,10 @@
     }
     FlutterView testView = new FlutterView(ctx);
     TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     // Set up an autofill scenario with 2 fields.
     final TextInputChannel.Configuration.Autofill autofillConfig =
         new TextInputChannel.Configuration.Autofill(
@@ -1928,8 +2056,10 @@
     TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class));
     FlutterView testView = getTestView();
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     // Set up an autofill scenario with 2 fields.
     final TextInputChannel.Configuration.Autofill autofill1 =
@@ -2041,6 +2171,7 @@
         ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
     DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
     TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
 
     EventHandler mockEventHandler = mock(EventHandler.class);
     TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE));
@@ -2048,7 +2179,8 @@
 
     View testView = new View(ctx);
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     verify(mockBinaryMessenger, times(1))
         .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
@@ -2072,6 +2204,7 @@
         ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
     DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
     TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger);
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
 
     EventHandler mockEventHandler = mock(EventHandler.class);
     TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE));
@@ -2079,7 +2212,8 @@
 
     View testView = new View(ctx);
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
 
     verify(mockBinaryMessenger, times(1))
         .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
@@ -2108,8 +2242,10 @@
     when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
 
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
     FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
     FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
@@ -2189,8 +2325,10 @@
             View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
 
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
     FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
     FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
@@ -2268,8 +2406,10 @@
     when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
 
     TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+    ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
     TextInputPlugin textInputPlugin =
-        new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+        new TextInputPlugin(
+            testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
     ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
     FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
     FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
index f9ebf55..4b36393 100644
--- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
@@ -44,6 +44,7 @@
 import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
 import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
 import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel.PlatformViewTouch;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
 import io.flutter.embedding.engine.systemchannels.SettingsChannel;
 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
 import io.flutter.plugin.common.MethodCall;
@@ -1668,6 +1669,7 @@
     when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class));
     when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
     when(engine.getSettingsChannel()).thenReturn(new SettingsChannel(executor));
+    when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class));
     when(engine.getPlatformViewsController()).thenReturn(platformViewsController);
     when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
     when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));