[webview_flutter_android] Adds Android implementation to override console log (#4702)

Adds the Android implementation for registering a JavaScript console callback. This will allow developers to receive JavaScript console messages in a Dart callback.

This PR contains the `webview_flutter_android` specific changes from PR #4541.

Related issue: flutter/flutter#32908

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
index 4608d1c..4fb48de 100644
--- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.11.0
+
+* Adds support to register a callback to receive JavaScript console messages. See `AndroidWebViewController.onConsoleMessage`.
+
 ## 3.10.1
 
 * Bumps androidx.annotation:annotation from 1.5.0 to 1.7.0.
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
index 567e201..d4cfc69 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
@@ -94,6 +94,62 @@
     }
   }
 
+  /**
+   * Indicates the type of message logged to the console.
+   *
+   * <p>See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel.
+   */
+  public enum ConsoleMessageLevel {
+    /**
+     * Indicates a message is logged for debugging.
+     *
+     * <p>See
+     * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG.
+     */
+    DEBUG(0),
+    /**
+     * Indicates a message is provided as an error.
+     *
+     * <p>See
+     * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR.
+     */
+    ERROR(1),
+    /**
+     * Indicates a message is provided as a basic log message.
+     *
+     * <p>See
+     * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG.
+     */
+    LOG(2),
+    /**
+     * Indicates a message is provided as a tip.
+     *
+     * <p>See
+     * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP.
+     */
+    TIP(3),
+    /**
+     * Indicates a message is provided as a warning.
+     *
+     * <p>See
+     * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING.
+     */
+    WARNING(4),
+    /**
+     * Indicates a message with an unknown level.
+     *
+     * <p>This does not represent an actual value provided by the platform and only indicates a
+     * value was provided that isn't currently supported.
+     */
+    UNKNOWN(5);
+
+    final int index;
+
+    private ConsoleMessageLevel(final int index) {
+      this.index = index;
+    }
+  }
+
   /** Generated class from Pigeon that represents data sent in messages. */
   public static final class WebResourceRequestData {
     private @NonNull String url;
@@ -409,6 +465,136 @@
     }
   }
 
+  /**
+   * Represents a JavaScript console message from WebCore.
+   *
+   * <p>See https://developer.android.com/reference/android/webkit/ConsoleMessage
+   *
+   * <p>Generated class from Pigeon that represents data sent in messages.
+   */
+  public static final class ConsoleMessage {
+    private @NonNull Long lineNumber;
+
+    public @NonNull Long getLineNumber() {
+      return lineNumber;
+    }
+
+    public void setLineNumber(@NonNull Long setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"lineNumber\" is null.");
+      }
+      this.lineNumber = setterArg;
+    }
+
+    private @NonNull String message;
+
+    public @NonNull String getMessage() {
+      return message;
+    }
+
+    public void setMessage(@NonNull String setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"message\" is null.");
+      }
+      this.message = setterArg;
+    }
+
+    private @NonNull ConsoleMessageLevel level;
+
+    public @NonNull ConsoleMessageLevel getLevel() {
+      return level;
+    }
+
+    public void setLevel(@NonNull ConsoleMessageLevel setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"level\" is null.");
+      }
+      this.level = setterArg;
+    }
+
+    private @NonNull String sourceId;
+
+    public @NonNull String getSourceId() {
+      return sourceId;
+    }
+
+    public void setSourceId(@NonNull String setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"sourceId\" is null.");
+      }
+      this.sourceId = setterArg;
+    }
+
+    /** Constructor is non-public to enforce null safety; use Builder. */
+    ConsoleMessage() {}
+
+    public static final class Builder {
+
+      private @Nullable Long lineNumber;
+
+      public @NonNull Builder setLineNumber(@NonNull Long setterArg) {
+        this.lineNumber = setterArg;
+        return this;
+      }
+
+      private @Nullable String message;
+
+      public @NonNull Builder setMessage(@NonNull String setterArg) {
+        this.message = setterArg;
+        return this;
+      }
+
+      private @Nullable ConsoleMessageLevel level;
+
+      public @NonNull Builder setLevel(@NonNull ConsoleMessageLevel setterArg) {
+        this.level = setterArg;
+        return this;
+      }
+
+      private @Nullable String sourceId;
+
+      public @NonNull Builder setSourceId(@NonNull String setterArg) {
+        this.sourceId = setterArg;
+        return this;
+      }
+
+      public @NonNull ConsoleMessage build() {
+        ConsoleMessage pigeonReturn = new ConsoleMessage();
+        pigeonReturn.setLineNumber(lineNumber);
+        pigeonReturn.setMessage(message);
+        pigeonReturn.setLevel(level);
+        pigeonReturn.setSourceId(sourceId);
+        return pigeonReturn;
+      }
+    }
+
+    @NonNull
+    ArrayList<Object> toList() {
+      ArrayList<Object> toListResult = new ArrayList<Object>(4);
+      toListResult.add(lineNumber);
+      toListResult.add(message);
+      toListResult.add(level == null ? null : level.index);
+      toListResult.add(sourceId);
+      return toListResult;
+    }
+
+    static @NonNull ConsoleMessage fromList(@NonNull ArrayList<Object> list) {
+      ConsoleMessage pigeonResult = new ConsoleMessage();
+      Object lineNumber = list.get(0);
+      pigeonResult.setLineNumber(
+          (lineNumber == null)
+              ? null
+              : ((lineNumber instanceof Integer) ? (Integer) lineNumber : (Long) lineNumber));
+      Object message = list.get(1);
+      pigeonResult.setMessage((String) message);
+      Object level = list.get(2);
+      pigeonResult.setLevel(ConsoleMessageLevel.values()[(int) level]);
+      Object sourceId = list.get(3);
+      pigeonResult.setSourceId((String) sourceId);
+      return pigeonResult;
+    }
+  }
+
   public interface Result<T> {
     @SuppressWarnings("UnknownNullness")
     void success(T result);
@@ -2401,6 +2587,9 @@
     void setSynchronousReturnValueForOnShowFileChooser(
         @NonNull Long instanceId, @NonNull Boolean value);
 
+    void setSynchronousReturnValueForOnConsoleMessage(
+        @NonNull Long instanceId, @NonNull Boolean value);
+
     /** The codec used by WebChromeClientHostApi. */
     static @NonNull MessageCodec<Object> getCodec() {
       return new StandardMessageCodec();
@@ -2463,6 +2652,33 @@
           channel.setMessageHandler(null);
         }
       }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger,
+                "dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage",
+                getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                ArrayList<Object> args = (ArrayList<Object>) message;
+                Number instanceIdArg = (Number) args.get(0);
+                Boolean valueArg = (Boolean) args.get(1);
+                try {
+                  api.setSynchronousReturnValueForOnConsoleMessage(
+                      (instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg);
+                  wrapped.add(0, null);
+                } catch (Throwable exception) {
+                  ArrayList<Object> wrappedError = wrapError(exception);
+                  wrapped = wrappedError;
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
     }
   }
   /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
@@ -2536,6 +2752,34 @@
       }
     }
   }
+
+  private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec {
+    public static final WebChromeClientFlutterApiCodec INSTANCE =
+        new WebChromeClientFlutterApiCodec();
+
+    private WebChromeClientFlutterApiCodec() {}
+
+    @Override
+    protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
+      switch (type) {
+        case (byte) 128:
+          return ConsoleMessage.fromList((ArrayList<Object>) readValue(buffer));
+        default:
+          return super.readValueOfType(type, buffer);
+      }
+    }
+
+    @Override
+    protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
+      if (value instanceof ConsoleMessage) {
+        stream.write(128);
+        writeValue(stream, ((ConsoleMessage) value).toList());
+      } else {
+        super.writeValue(stream, value);
+      }
+    }
+  }
+
   /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
   public static class WebChromeClientFlutterApi {
     private final @NonNull BinaryMessenger binaryMessenger;
@@ -2551,7 +2795,7 @@
     }
     /** The codec used by WebChromeClientFlutterApi. */
     static @NonNull MessageCodec<Object> getCodec() {
-      return new StandardMessageCodec();
+      return WebChromeClientFlutterApiCodec.INSTANCE;
     }
 
     public void onProgressChanged(
@@ -2656,6 +2900,20 @@
           new ArrayList<Object>(Collections.singletonList(identifierArg)),
           channelReply -> callback.reply(null));
     }
+    /** Callback to Dart function `WebChromeClient.onConsoleMessage`. */
+    public void onConsoleMessage(
+        @NonNull Long instanceIdArg,
+        @NonNull ConsoleMessage messageArg,
+        @NonNull Reply<Void> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger,
+              "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage",
+              getCodec());
+      channel.send(
+          new ArrayList<Object>(Arrays.asList(instanceIdArg, messageArg)),
+          channelReply -> callback.reply(null));
+    }
   }
   /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
   public interface WebStorageHostApi {
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java
index f5097b6..b383dfd 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java
@@ -6,6 +6,7 @@
 
 import android.os.Build;
 import android.view.View;
+import android.webkit.ConsoleMessage;
 import android.webkit.GeolocationPermissions;
 import android.webkit.PermissionRequest;
 import android.webkit.WebChromeClient;
@@ -27,6 +28,24 @@
   private final InstanceManager instanceManager;
   private final WebViewFlutterApiImpl webViewFlutterApi;
 
+  private static GeneratedAndroidWebView.ConsoleMessageLevel toConsoleMessageLevel(
+      ConsoleMessage.MessageLevel level) {
+    switch (level) {
+      case TIP:
+        return GeneratedAndroidWebView.ConsoleMessageLevel.TIP;
+      case LOG:
+        return GeneratedAndroidWebView.ConsoleMessageLevel.LOG;
+      case WARNING:
+        return GeneratedAndroidWebView.ConsoleMessageLevel.WARNING;
+      case ERROR:
+        return GeneratedAndroidWebView.ConsoleMessageLevel.ERROR;
+      case DEBUG:
+        return GeneratedAndroidWebView.ConsoleMessageLevel.DEBUG;
+    }
+
+    return GeneratedAndroidWebView.ConsoleMessageLevel.UNKNOWN;
+  }
+
   /**
    * Creates a Flutter api that sends messages to Dart.
    *
@@ -149,6 +168,25 @@
         callback);
   }
 
+  /**
+   * Sends a message to Dart to call `WebChromeClient.onConsoleMessage` on the Dart object
+   * representing `instance`.
+   */
+  public void onConsoleMessage(
+      @NonNull WebChromeClient instance,
+      @NonNull ConsoleMessage message,
+      @NonNull Reply<Void> callback) {
+    super.onConsoleMessage(
+        Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)),
+        new GeneratedAndroidWebView.ConsoleMessage.Builder()
+            .setLineNumber((long) message.lineNumber())
+            .setMessage(message.message())
+            .setLevel(toConsoleMessageLevel(message.messageLevel()))
+            .setSourceId(message.sourceId())
+            .build(),
+        callback);
+  }
+
   private long getIdentifierForClient(WebChromeClient webChromeClient) {
     final Long identifier = instanceManager.getIdentifierForStrongReference(webChromeClient);
     if (identifier == null) {
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java
index 635c6c3..cb382d5 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java
@@ -9,6 +9,7 @@
 import android.os.Build;
 import android.os.Message;
 import android.view.View;
+import android.webkit.ConsoleMessage;
 import android.webkit.GeolocationPermissions;
 import android.webkit.PermissionRequest;
 import android.webkit.ValueCallback;
@@ -40,6 +41,7 @@
   public static class WebChromeClientImpl extends SecureWebChromeClient {
     private final WebChromeClientFlutterApiImpl flutterApi;
     private boolean returnValueForOnShowFileChooser = false;
+    private boolean returnValueForOnConsoleMessage = false;
 
     /**
      * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart.
@@ -107,10 +109,21 @@
       flutterApi.onPermissionRequest(this, request, reply -> {});
     }
 
+    @Override
+    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+      flutterApi.onConsoleMessage(this, consoleMessage, reply -> {});
+      return returnValueForOnConsoleMessage;
+    }
+
     /** Sets return value for {@link #onShowFileChooser}. */
     public void setReturnValueForOnShowFileChooser(boolean value) {
       returnValueForOnShowFileChooser = value;
     }
+
+    /** Sets return value for {@link #onConsoleMessage}. */
+    public void setReturnValueForOnConsoleMessage(boolean value) {
+      returnValueForOnConsoleMessage = value;
+    }
   }
 
   /**
@@ -246,4 +259,12 @@
         Objects.requireNonNull(instanceManager.getInstance(instanceId));
     webChromeClient.setReturnValueForOnShowFileChooser(value);
   }
+
+  @Override
+  public void setSynchronousReturnValueForOnConsoleMessage(
+      @NonNull Long instanceId, @NonNull Boolean value) {
+    final WebChromeClientImpl webChromeClient =
+        Objects.requireNonNull(instanceManager.getInstance(instanceId));
+    webChromeClient.setReturnValueForOnConsoleMessage(value);
+  }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java
index 4e09b2e..ef49dde 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java
@@ -16,6 +16,7 @@
 import android.net.Uri;
 import android.os.Message;
 import android.view.View;
+import android.webkit.ConsoleMessage;
 import android.webkit.GeolocationPermissions;
 import android.webkit.PermissionRequest;
 import android.webkit.WebChromeClient;
@@ -162,4 +163,17 @@
     webChromeClient.onGeolocationPermissionsHidePrompt();
     verify(mockFlutterApi).onGeolocationPermissionsHidePrompt(eq(webChromeClient), any());
   }
+
+  @Test
+  public void onConsoleMessage() {
+    webChromeClient.onConsoleMessage(
+        new ConsoleMessage("message", "sourceId", 23, ConsoleMessage.MessageLevel.ERROR));
+    verify(mockFlutterApi).onConsoleMessage(eq(webChromeClient), any(), any());
+  }
+
+  @Test
+  public void setReturnValueForOnConsoleMessage() {
+    webChromeClient.setReturnValueForOnConsoleMessage(true);
+    assertTrue(webChromeClient.onConsoleMessage(null));
+  }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart
index 0e805dd..017e696 100644
--- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart
+++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart
@@ -1310,6 +1310,47 @@
       );
     },
   );
+
+  group('Logging', () {
+    testWidgets('can receive console log messages',
+        (WidgetTester tester) async {
+      const String testPage = '''
+          <!DOCTYPE html>
+          <html>
+          <head>
+            <title>WebResourceError test</title>
+          </head>
+          <body onload="console.debug('Debug message')">
+            <p>Test page</p>
+          </body>
+          </html>
+         ''';
+
+      final Completer<String> debugMessageReceived = Completer<String>();
+      final PlatformWebViewController controller = PlatformWebViewController(
+        const PlatformWebViewControllerCreationParams(),
+      );
+      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
+
+      await controller.setOnConsoleMessage((JavaScriptConsoleMessage message) {
+        debugMessageReceived
+            .complete('${message.level.name}:${message.message}');
+      });
+
+      await controller.loadHtmlString(testPage);
+
+      await tester.pumpWidget(Builder(
+        builder: (BuildContext context) {
+          return PlatformWebViewWidget(
+            PlatformWebViewWidgetCreationParams(controller: controller),
+          ).build(context);
+        },
+      ));
+
+      await expectLater(
+          debugMessageReceived.future, completion('debug:Debug message'));
+    });
+  });
 }
 
 /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.
diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
index 781a98f..92a8fe2 100644
--- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
@@ -73,6 +73,40 @@
 </html>
 ''';
 
+const String kLogExamplePage = '''
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Load file or HTML string example</title>
+</head>
+<body onload="console.log('Logging that the page is loading.')">
+
+<h1>Local demo page</h1>
+<p>
+  This page is used to test the forwarding of console logs to Dart.
+</p>
+
+<style>
+    .btn-group button {
+      padding: 24px; 24px;
+      display: block;
+      width: 25%;
+      margin: 5px 0px 0px 0px;
+    }
+</style>
+
+<div class="btn-group">
+    <button onclick="console.error('This is an error message.')">Error</button>
+    <button onclick="console.warn('This is a warning message.')">Warning</button>
+    <button onclick="console.info('This is a info message.')">Info</button>
+    <button onclick="console.debug('This is a debug message.')">Debug</button>
+    <button onclick="console.log('This is a log message.')">Log</button>
+</div>
+
+</body>
+</html>
+''';
+
 class WebViewExample extends StatefulWidget {
   const WebViewExample({super.key, this.cookieManager});
 
@@ -202,6 +236,7 @@
   transparentBackground,
   setCookie,
   videoExample,
+  logExample,
 }
 
 class SampleMenu extends StatelessWidget {
@@ -265,6 +300,9 @@
           case MenuOptions.videoExample:
             _onVideoExample(context);
             break;
+          case MenuOptions.logExample:
+            _onLogExample();
+            break;
         }
       },
       itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -322,6 +360,10 @@
           child: Text('Transparent background example'),
         ),
         const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.logExample,
+          child: Text('Log example'),
+        ),
+        const PopupMenuItem<MenuOptions>(
           value: MenuOptions.videoExample,
           child: Text('Video example'),
         ),
@@ -497,6 +539,16 @@
 
     return indexFile.path;
   }
+
+  Future<void> _onLogExample() {
+    webViewController
+        .setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
+      debugPrint(
+          '== JS == ${consoleMessage.level.name}: ${consoleMessage.message}');
+    });
+
+    return webViewController.loadHtmlString(kLogExamplePage);
+  }
 }
 
 class NavigationControls extends StatelessWidget {
diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml
index 286b847..acba200 100644
--- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml
@@ -17,7 +17,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  webview_flutter_platform_interface: ^2.4.0
+  webview_flutter_platform_interface: ^2.6.0
 
 dev_dependencies:
   espresso: ^0.2.0
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart
index 508a31b..f3d00eb 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart
@@ -44,6 +44,9 @@
         onGeolocationPermissionsShowPrompt,
     void Function(android_webview.WebChromeClient instance)?
         onGeolocationPermissionsHidePrompt,
+    void Function(android_webview.WebChromeClient instance,
+            android_webview.ConsoleMessage message)?
+        onConsoleMessage,
     void Function(
             android_webview.WebChromeClient instance,
             android_webview.View view,
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart
index 5ed2fc0..d63f2c7 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart
@@ -12,7 +12,8 @@
 import 'android_webview_api_impls.dart';
 import 'instance_manager.dart';
 
-export 'android_webview_api_impls.dart' show FileChooserMode;
+export 'android_webview_api_impls.dart'
+    show ConsoleMessage, ConsoleMessageLevel, FileChooserMode;
 
 /// Root of the Java class hierarchy.
 ///
@@ -1047,6 +1048,7 @@
     this.onGeolocationPermissionsHidePrompt,
     this.onShowCustomView,
     this.onHideCustomView,
+    this.onConsoleMessage,
     @visibleForTesting super.binaryMessenger,
     @visibleForTesting super.instanceManager,
   }) : super.detached() {
@@ -1068,6 +1070,7 @@
     this.onGeolocationPermissionsHidePrompt,
     this.onShowCustomView,
     this.onHideCustomView,
+    this.onConsoleMessage,
     super.binaryMessenger,
     super.instanceManager,
   }) : super.detached();
@@ -1121,6 +1124,10 @@
   /// mode.
   final HideCustomViewCallback? onHideCustomView;
 
+  /// Report a JavaScript console message to the host application.
+  final void Function(WebChromeClient instance, ConsoleMessage message)?
+      onConsoleMessage;
+
   /// Sets the required synchronous return value for the Java method,
   /// `WebChromeClient.onShowFileChooser(...)`.
   ///
@@ -1150,6 +1157,33 @@
     );
   }
 
+  /// Sets the required synchronous return value for the Java method,
+  /// `WebChromeClient.onShowFileChooser(...)`.
+  ///
+  /// The Java method, `WebChromeClient.onConsoleMessage(...)`, requires
+  /// a boolean to be returned and this method sets the returned value for all
+  /// calls to the Java method.
+  ///
+  /// Setting this to true indicates that the client is handling all console
+  /// messages.
+  ///
+  /// Requires [onConsoleMessage] to be nonnull.
+  ///
+  /// Defaults to false.
+  Future<void> setSynchronousReturnValueForOnConsoleMessage(
+    bool value,
+  ) {
+    if (value && onConsoleMessage == null) {
+      throw StateError(
+        'Setting this to true requires `onConsoleMessage` to be nonnull.',
+      );
+    }
+    return api.setSynchronousReturnValueForOnConsoleMessageFromInstance(
+      this,
+      value,
+    );
+  }
+
   @override
   WebChromeClient copy() {
     return WebChromeClient.detached(
@@ -1160,6 +1194,7 @@
       onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt,
       onShowCustomView: onShowCustomView,
       onHideCustomView: onHideCustomView,
+      onConsoleMessage: onConsoleMessage,
       binaryMessenger: _api.binaryMessenger,
       instanceManager: _api.instanceManager,
     );
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart
index 99e1e7f..87f75f1 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart
@@ -32,6 +32,42 @@
   save,
 }
 
+/// Indicates the type of message logged to the console.
+///
+/// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel.
+enum ConsoleMessageLevel {
+  /// Indicates a message is logged for debugging.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG.
+  debug,
+
+  /// Indicates a message is provided as an error.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR.
+  error,
+
+  /// Indicates a message is provided as a basic log message.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG.
+  log,
+
+  /// Indicates a message is provided as a tip.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP.
+  tip,
+
+  /// Indicates a message is provided as a warning.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING.
+  warning,
+
+  /// Indicates a message with an unknown level.
+  ///
+  /// This does not represent an actual value provided by the platform and only
+  /// indicates a value was provided that isn't currently supported.
+  unknown,
+}
+
 class WebResourceRequestData {
   WebResourceRequestData({
     required this.url,
@@ -131,6 +167,45 @@
   }
 }
 
+/// Represents a JavaScript console message from WebCore.
+///
+/// See https://developer.android.com/reference/android/webkit/ConsoleMessage
+class ConsoleMessage {
+  ConsoleMessage({
+    required this.lineNumber,
+    required this.message,
+    required this.level,
+    required this.sourceId,
+  });
+
+  int lineNumber;
+
+  String message;
+
+  ConsoleMessageLevel level;
+
+  String sourceId;
+
+  Object encode() {
+    return <Object?>[
+      lineNumber,
+      message,
+      level.index,
+      sourceId,
+    ];
+  }
+
+  static ConsoleMessage decode(Object result) {
+    result as List<Object?>;
+    return ConsoleMessage(
+      lineNumber: result[0]! as int,
+      message: result[1]! as String,
+      level: ConsoleMessageLevel.values[result[2]! as int],
+      sourceId: result[3]! as String,
+    );
+  }
+}
+
 /// Host API for managing the native `InstanceManager`.
 class InstanceManagerHostApi {
   /// Constructor for [InstanceManagerHostApi].  The [binaryMessenger] named argument is
@@ -1943,6 +2018,30 @@
       return;
     }
   }
+
+  Future<void> setSynchronousReturnValueForOnConsoleMessage(
+      int arg_instanceId, bool arg_value) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel
+        .send(<Object?>[arg_instanceId, arg_value]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
 }
 
 class FlutterAssetManagerHostApi {
@@ -2012,8 +2111,31 @@
   }
 }
 
+class _WebChromeClientFlutterApiCodec extends StandardMessageCodec {
+  const _WebChromeClientFlutterApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is ConsoleMessage) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return ConsoleMessage.decode(readValue(buffer)!);
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
 abstract class WebChromeClientFlutterApi {
-  static const MessageCodec<Object?> codec = StandardMessageCodec();
+  static const MessageCodec<Object?> codec = _WebChromeClientFlutterApiCodec();
 
   void onProgressChanged(int instanceId, int webViewInstanceId, int progress);
 
@@ -2037,6 +2159,9 @@
   /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`.
   void onGeolocationPermissionsHidePrompt(int identifier);
 
+  /// Callback to Dart function `WebChromeClient.onConsoleMessage`.
+  void onConsoleMessage(int instanceId, ConsoleMessage message);
+
   static void setup(WebChromeClientFlutterApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
@@ -2210,6 +2335,29 @@
         });
       }
     }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage',
+          codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final int? arg_instanceId = (args[0] as int?);
+          assert(arg_instanceId != null,
+              'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage was null, expected non-null int.');
+          final ConsoleMessage? arg_message = (args[1] as ConsoleMessage?);
+          assert(arg_message != null,
+              'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage was null, expected non-null ConsoleMessage.');
+          api.onConsoleMessage(arg_instanceId!, arg_message!);
+          return;
+        });
+      }
+    }
   }
 }
 
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
index 2c773fd..c9191e0 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
@@ -11,7 +11,8 @@
 import 'android_webview.g.dart';
 import 'instance_manager.dart';
 
-export 'android_webview.g.dart' show FileChooserMode;
+export 'android_webview.g.dart'
+    show ConsoleMessage, ConsoleMessageLevel, FileChooserMode;
 
 /// Converts [WebResourceRequestData] to [WebResourceRequest]
 WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) {
@@ -892,6 +893,17 @@
       value,
     );
   }
+
+  /// Helper method to convert instances ids to objects.
+  Future<void> setSynchronousReturnValueForOnConsoleMessageFromInstance(
+    WebChromeClient instance,
+    bool value,
+  ) {
+    return setSynchronousReturnValueForOnConsoleMessage(
+      instanceManager.getIdentifier(instance)!,
+      value,
+    );
+  }
 }
 
 /// Flutter api implementation for [DownloadListener].
@@ -1017,6 +1029,13 @@
       );
     }
   }
+
+  @override
+  void onConsoleMessage(int instanceId, ConsoleMessage message) {
+    final WebChromeClient instance =
+        instanceManager.getInstanceWithWeakReference(instanceId)!;
+    instance.onConsoleMessage?.call(instance, message);
+  }
 }
 
 /// Host api implementation for [WebStorage].
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart
index bc1da74..d0559b4 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart
@@ -13,6 +13,7 @@
 
 import 'android_proxy.dart';
 import 'android_webview.dart' as android_webview;
+import 'android_webview_api_impls.dart';
 import 'instance_manager.dart';
 import 'platform_views_service_proxy.dart';
 import 'weak_reference_utils.dart';
@@ -188,6 +189,42 @@
         };
       },
     ),
+    onConsoleMessage: withWeakReferenceTo(
+      this,
+      (WeakReference<AndroidWebViewController> weakReference) {
+        return (android_webview.WebChromeClient webChromeClient,
+            android_webview.ConsoleMessage consoleMessage) async {
+          final void Function(JavaScriptConsoleMessage)? callback =
+              weakReference.target?._onConsoleLogCallback;
+          if (callback != null) {
+            JavaScriptLogLevel logLevel;
+            switch (consoleMessage.level) {
+              // Android maps `console.debug` to `MessageLevel.TIP`, it seems
+              // `MessageLevel.DEBUG` if not being used.
+              case ConsoleMessageLevel.debug:
+              case ConsoleMessageLevel.tip:
+                logLevel = JavaScriptLogLevel.debug;
+                break;
+              case ConsoleMessageLevel.error:
+                logLevel = JavaScriptLogLevel.error;
+                break;
+              case ConsoleMessageLevel.warning:
+                logLevel = JavaScriptLogLevel.warning;
+                break;
+              case ConsoleMessageLevel.unknown:
+              case ConsoleMessageLevel.log:
+                logLevel = JavaScriptLogLevel.log;
+                break;
+            }
+
+            callback(JavaScriptConsoleMessage(
+              level: logLevel,
+              message: consoleMessage.message,
+            ));
+          }
+        };
+      },
+    ),
     onPermissionRequest: withWeakReferenceTo(
       this,
       (WeakReference<AndroidWebViewController> weakReference) {
@@ -255,6 +292,8 @@
 
   void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback;
 
+  void Function(JavaScriptConsoleMessage consoleMessage)? _onConsoleLogCallback;
+
   /// Whether to enable the platform's webview content debugging tools.
   ///
   /// Defaults to false.
@@ -566,6 +605,18 @@
     _onShowCustomWidgetCallback = onShowCustomWidget;
     _onHideCustomWidgetCallback = onHideCustomWidget;
   }
+
+  /// Sets a callback that notifies the host application of any log messages
+  /// written to the JavaScript console.
+  @override
+  Future<void> setOnConsoleMessage(
+      void Function(JavaScriptConsoleMessage consoleMessage)
+          onConsoleMessage) async {
+    _onConsoleLogCallback = onConsoleMessage;
+
+    return _webChromeClient.setSynchronousReturnValueForOnConsoleMessage(
+        _onConsoleLogCallback != null);
+  }
 }
 
 /// Android implementation of [PlatformWebViewPermissionRequest].
diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart
index c19a2b2..3676053 100644
--- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart
+++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart
@@ -57,6 +57,42 @@
   save,
 }
 
+/// Indicates the type of message logged to the console.
+///
+/// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel.
+enum ConsoleMessageLevel {
+  /// Indicates a message is logged for debugging.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG.
+  debug,
+
+  /// Indicates a message is provided as an error.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR.
+  error,
+
+  /// Indicates a message is provided as a basic log message.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG.
+  log,
+
+  /// Indicates a message is provided as a tip.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP.
+  tip,
+
+  /// Indicates a message is provided as a warning.
+  ///
+  /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING.
+  warning,
+
+  /// Indicates a message with an unknown level.
+  ///
+  /// This does not represent an actual value provided by the platform and only
+  /// indicates a value was provided that isn't currently supported.
+  unknown,
+}
+
 class WebResourceRequestData {
   WebResourceRequestData(
     this.url,
@@ -89,6 +125,16 @@
   int y;
 }
 
+/// Represents a JavaScript console message from WebCore.
+///
+/// See https://developer.android.com/reference/android/webkit/ConsoleMessage
+class ConsoleMessage {
+  late int lineNumber;
+  late String message;
+  late ConsoleMessageLevel level;
+  late String sourceId;
+}
+
 /// Handles methods calls to the native Java Object class.
 ///
 /// Also handles calls to remove the reference to an instance with `dispose`.
@@ -337,6 +383,11 @@
     int instanceId,
     bool value,
   );
+
+  void setSynchronousReturnValueForOnConsoleMessage(
+    int instanceId,
+    bool value,
+  );
 }
 
 @HostApi(dartHostTestHandler: 'TestAssetManagerHostApi')
@@ -379,6 +430,9 @@
 
   /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`.
   void onGeolocationPermissionsHidePrompt(int identifier);
+
+  /// Callback to Dart function `WebChromeClient.onConsoleMessage`.
+  void onConsoleMessage(int instanceId, ConsoleMessage message);
 }
 
 @HostApi(dartHostTestHandler: 'TestWebStorageHostApi')
diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
index 21acae2..8242abd 100644
--- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin that provides a WebView widget on Android.
 repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
-version: 3.10.1
+version: 3.11.0
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -20,7 +20,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  webview_flutter_platform_interface: ^2.4.0
+  webview_flutter_platform_interface: ^2.6.0
 
 dev_dependencies:
   build_runner: ^2.1.4
diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart
index 0693ef5..269123e 100644
--- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart
@@ -520,6 +520,7 @@
     super.onShowCustomView,
     super.onHideCustomView,
     super.onPermissionRequest,
+    super.onConsoleMessage,
     super.binaryMessenger,
     super.instanceManager,
   }) : super.detached() {
diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart
index c350be6..3f3bf73 100644
--- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart
@@ -73,6 +73,9 @@
               android_webview.CustomViewCallback callback)?
           onShowCustomView,
       void Function(android_webview.WebChromeClient instance)? onHideCustomView,
+      void Function(android_webview.WebChromeClient instance,
+              android_webview.ConsoleMessage message)?
+          onConsoleMessage,
     })? createWebChromeClient,
     android_webview.WebView? mockWebView,
     android_webview.WebViewClient? mockWebViewClient,
@@ -111,6 +114,9 @@
                         onShowCustomView,
                     void Function(android_webview.WebChromeClient instance)?
                         onHideCustomView,
+                    void Function(android_webview.WebChromeClient instance,
+                            android_webview.ConsoleMessage message)?
+                        onConsoleMessage,
                   }) =>
                       MockWebChromeClient(),
               createAndroidWebView: () => nonNullMockWebView,
@@ -606,6 +612,7 @@
           dynamic onPermissionRequest,
           dynamic onShowCustomView,
           dynamic onHideCustomView,
+          dynamic onConsoleMessage,
         }) {
           onShowFileChooserCallback = onShowFileChooser!;
           return mockWebChromeClient;
@@ -676,6 +683,7 @@
           dynamic onPermissionRequest,
           dynamic onShowCustomView,
           dynamic onHideCustomView,
+          dynamic onConsoleMessage,
         }) {
           onGeoPermissionHandle = onGeolocationPermissionsShowPrompt!;
           onGeoPermissionHidePromptHandle = onGeolocationPermissionsHidePrompt!;
@@ -750,6 +758,7 @@
               onShowCustomView,
           void Function(android_webview.WebChromeClient instance)?
               onHideCustomView,
+          dynamic onConsoleMessage,
         }) {
           onShowCustomViewHandle = onShowCustomView!;
           onHideCustomViewHandle = onHideCustomView!;
@@ -802,6 +811,7 @@
           )? onPermissionRequest,
           dynamic onShowCustomView,
           dynamic onHideCustomView,
+          dynamic onConsoleMessage,
         }) {
           onPermissionRequestCallback = onPermissionRequest!;
           return mockWebChromeClient;
@@ -856,6 +866,7 @@
           )? onPermissionRequest,
           dynamic onShowCustomView,
           dynamic onHideCustomView,
+          dynamic onConsoleMessage,
         }) {
           onPermissionRequestCallback = onPermissionRequest!;
           return mockWebChromeClient;
@@ -881,6 +892,104 @@
       expect(callbackCalled, isFalse);
     });
 
+    test('setOnConsoleLogCallback', () async {
+      late final void Function(
+        android_webview.WebChromeClient instance,
+        android_webview.ConsoleMessage message,
+      ) onConsoleMessageCallback;
+
+      final MockWebChromeClient mockWebChromeClient = MockWebChromeClient();
+      final AndroidWebViewController controller = createControllerWithMocks(
+        createWebChromeClient: ({
+          dynamic onProgressChanged,
+          dynamic onShowFileChooser,
+          dynamic onGeolocationPermissionsShowPrompt,
+          dynamic onGeolocationPermissionsHidePrompt,
+          dynamic onPermissionRequest,
+          dynamic onShowCustomView,
+          dynamic onHideCustomView,
+          void Function(
+            android_webview.WebChromeClient,
+            android_webview.ConsoleMessage,
+          )? onConsoleMessage,
+        }) {
+          onConsoleMessageCallback = onConsoleMessage!;
+          return mockWebChromeClient;
+        },
+      );
+
+      final Map<String, JavaScriptLogLevel> logs =
+          <String, JavaScriptLogLevel>{};
+      await controller.setOnConsoleMessage(
+        (JavaScriptConsoleMessage message) async {
+          logs[message.message] = message.level;
+        },
+      );
+
+      onConsoleMessageCallback(
+        mockWebChromeClient,
+        ConsoleMessage(
+          lineNumber: 42,
+          message: 'Debug message',
+          level: ConsoleMessageLevel.debug,
+          sourceId: 'source',
+        ),
+      );
+      onConsoleMessageCallback(
+        mockWebChromeClient,
+        ConsoleMessage(
+          lineNumber: 42,
+          message: 'Error message',
+          level: ConsoleMessageLevel.error,
+          sourceId: 'source',
+        ),
+      );
+      onConsoleMessageCallback(
+        mockWebChromeClient,
+        ConsoleMessage(
+          lineNumber: 42,
+          message: 'Log message',
+          level: ConsoleMessageLevel.log,
+          sourceId: 'source',
+        ),
+      );
+      onConsoleMessageCallback(
+        mockWebChromeClient,
+        ConsoleMessage(
+          lineNumber: 42,
+          message: 'Tip message',
+          level: ConsoleMessageLevel.tip,
+          sourceId: 'source',
+        ),
+      );
+      onConsoleMessageCallback(
+        mockWebChromeClient,
+        ConsoleMessage(
+          lineNumber: 42,
+          message: 'Warning message',
+          level: ConsoleMessageLevel.warning,
+          sourceId: 'source',
+        ),
+      );
+      onConsoleMessageCallback(
+        mockWebChromeClient,
+        ConsoleMessage(
+          lineNumber: 42,
+          message: 'Unknown message',
+          level: ConsoleMessageLevel.unknown,
+          sourceId: 'source',
+        ),
+      );
+
+      expect(logs.length, 6);
+      expect(logs['Debug message'], JavaScriptLogLevel.debug);
+      expect(logs['Error message'], JavaScriptLogLevel.error);
+      expect(logs['Log message'], JavaScriptLogLevel.log);
+      expect(logs['Tip message'], JavaScriptLogLevel.debug);
+      expect(logs['Warning message'], JavaScriptLogLevel.warning);
+      expect(logs['Unknown message'], JavaScriptLogLevel.log);
+    });
+
     test('runJavaScript', () async {
       final MockWebView mockWebView = MockWebView();
       final AndroidWebViewController controller = createControllerWithMocks(
@@ -1334,6 +1443,7 @@
                   android_webview.CustomViewCallback callback)?
               onShowCustomView,
           dynamic onHideCustomView,
+          dynamic onConsoleMessage,
         }) {
           onShowCustomViewCallback = onShowCustomView;
           return mockWebChromeClient;
diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart
index b6d6e2c..98d7879 100644
--- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart
@@ -739,8 +739,8 @@
       ) as _i9.Future<void>);
   @override
   _i9.Future<void> setCustomWidgetCallbacks({
-    _i8.OnShowCustomWidgetCallback? onShowCustomWidget,
-    _i8.OnHideCustomWidgetCallback? onHideCustomWidget,
+    required _i8.OnShowCustomWidgetCallback? onShowCustomWidget,
+    required _i8.OnHideCustomWidgetCallback? onHideCustomWidget,
   }) =>
       (super.noSuchMethod(
         Invocation.method(
@@ -754,6 +754,26 @@
         returnValue: _i9.Future<void>.value(),
         returnValueForMissingStub: _i9.Future<void>.value(),
       ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> setOnConsoleMessage(
+          void Function(_i3.JavaScriptConsoleMessage)? onConsoleMessage) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnConsoleMessage,
+          [onConsoleMessage],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<String?> getUserAgent() => (super.noSuchMethod(
+        Invocation.method(
+          #getUserAgent,
+          [],
+        ),
+        returnValue: _i9.Future<String?>.value(),
+        returnValueForMissingStub: _i9.Future<String?>.value(),
+      ) as _i9.Future<String?>);
 }
 
 /// A class which mocks [AndroidWebViewProxy].
@@ -775,6 +795,10 @@
       ) as _i2.WebView Function());
   @override
   _i2.WebChromeClient Function({
+    void Function(
+      _i2.WebChromeClient,
+      _i2.ConsoleMessage,
+    )? onConsoleMessage,
     void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt,
     _i9.Future<void> Function(
       String,
@@ -801,6 +825,10 @@
   }) get createAndroidWebChromeClient => (super.noSuchMethod(
         Invocation.getter(#createAndroidWebChromeClient),
         returnValue: ({
+          void Function(
+            _i2.WebChromeClient,
+            _i2.ConsoleMessage,
+          )? onConsoleMessage,
           void Function(_i2.WebChromeClient)?
               onGeolocationPermissionsHidePrompt,
           _i9.Future<void> Function(
@@ -831,6 +859,10 @@
           Invocation.getter(#createAndroidWebChromeClient),
         ),
         returnValueForMissingStub: ({
+          void Function(
+            _i2.WebChromeClient,
+            _i2.ConsoleMessage,
+          )? onConsoleMessage,
           void Function(_i2.WebChromeClient)?
               onGeolocationPermissionsHidePrompt,
           _i9.Future<void> Function(
@@ -861,6 +893,10 @@
           Invocation.getter(#createAndroidWebChromeClient),
         ),
       ) as _i2.WebChromeClient Function({
+        void Function(
+          _i2.WebChromeClient,
+          _i2.ConsoleMessage,
+        )? onConsoleMessage,
         void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt,
         _i9.Future<void> Function(
           String,
@@ -1780,6 +1816,16 @@
         returnValueForMissingStub: _i9.Future<void>.value(),
       ) as _i9.Future<void>);
   @override
+  _i9.Future<void> setSynchronousReturnValueForOnConsoleMessage(bool? value) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setSynchronousReturnValueForOnConsoleMessage,
+          [value],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
   _i2.WebChromeClient copy() => (super.noSuchMethod(
         Invocation.method(
           #copy,
diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart
index b23bcaa..5213793 100644
--- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart
@@ -475,8 +475,8 @@
       ) as _i5.Future<void>);
   @override
   _i5.Future<void> setCustomWidgetCallbacks({
-    _i6.OnShowCustomWidgetCallback? onShowCustomWidget,
-    _i6.OnHideCustomWidgetCallback? onHideCustomWidget,
+    required _i6.OnShowCustomWidgetCallback? onShowCustomWidget,
+    required _i6.OnHideCustomWidgetCallback? onHideCustomWidget,
   }) =>
       (super.noSuchMethod(
         Invocation.method(
@@ -490,6 +490,25 @@
         returnValue: _i5.Future<void>.value(),
         returnValueForMissingStub: _i5.Future<void>.value(),
       ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOnConsoleMessage(
+          void Function(_i3.JavaScriptConsoleMessage)? onConsoleMessage) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnConsoleMessage,
+          [onConsoleMessage],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<String?> getUserAgent() => (super.noSuchMethod(
+        Invocation.method(
+          #getUserAgent,
+          [],
+        ),
+        returnValue: _i5.Future<String?>.value(),
+      ) as _i5.Future<String?>);
 }
 
 /// A class which mocks [TestInstanceManagerHostApi].
diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart
index da03052..2629d56 100644
--- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart
@@ -1120,6 +1120,81 @@
         expect(callbackParameters, <Object?>[instance]);
       });
 
+      test('onConsoleMessage', () async {
+        late final List<Object> result;
+        when(mockWebChromeClient.onConsoleMessage).thenReturn(
+          (WebChromeClient instance, ConsoleMessage message) {
+            result = <Object>[instance, message];
+          },
+        );
+
+        final ConsoleMessage message = ConsoleMessage(
+          lineNumber: 0,
+          message: 'message',
+          level: ConsoleMessageLevel.error,
+          sourceId: 'sourceId',
+        );
+
+        flutterApi.onConsoleMessage(
+          mockWebChromeClientInstanceId,
+          message,
+        );
+        expect(result[0], mockWebChromeClient);
+        expect(result[1], message);
+      });
+
+      test('setSynchronousReturnValueForOnConsoleMessage', () {
+        final MockTestWebChromeClientHostApi mockHostApi =
+            MockTestWebChromeClientHostApi();
+        TestWebChromeClientHostApi.setup(mockHostApi);
+
+        WebChromeClient.api =
+            WebChromeClientHostApiImpl(instanceManager: instanceManager);
+
+        final WebChromeClient webChromeClient = WebChromeClient.detached();
+        instanceManager.addHostCreatedInstance(webChromeClient, 2);
+
+        webChromeClient.setSynchronousReturnValueForOnConsoleMessage(false);
+
+        verify(
+          mockHostApi.setSynchronousReturnValueForOnConsoleMessage(2, false),
+        );
+      });
+
+      test(
+          'setSynchronousReturnValueForOnConsoleMessage throws StateError when onConsoleMessage is null',
+          () {
+        final MockTestWebChromeClientHostApi mockHostApi =
+            MockTestWebChromeClientHostApi();
+        TestWebChromeClientHostApi.setup(mockHostApi);
+
+        WebChromeClient.api =
+            WebChromeClientHostApiImpl(instanceManager: instanceManager);
+
+        final WebChromeClient clientWithNullCallback =
+            WebChromeClient.detached();
+        instanceManager.addHostCreatedInstance(clientWithNullCallback, 2);
+
+        expect(
+          () => clientWithNullCallback
+              .setSynchronousReturnValueForOnConsoleMessage(true),
+          throwsStateError,
+        );
+
+        final WebChromeClient clientWithNonnullCallback =
+            WebChromeClient.detached(
+          onConsoleMessage: (_, __) async {},
+        );
+        instanceManager.addHostCreatedInstance(clientWithNonnullCallback, 3);
+
+        clientWithNonnullCallback
+            .setSynchronousReturnValueForOnConsoleMessage(true);
+
+        verify(
+          mockHostApi.setSynchronousReturnValueForOnConsoleMessage(3, true),
+        );
+      });
+
       test('copy', () {
         expect(WebChromeClient.detached().copy(), isA<WebChromeClient>());
       });
diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart
index fecf814..717cc14 100644
--- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart
@@ -480,6 +480,21 @@
         ),
         returnValueForMissingStub: null,
       );
+  @override
+  void setSynchronousReturnValueForOnConsoleMessage(
+    int? instanceId,
+    bool? value,
+  ) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #setSynchronousReturnValueForOnConsoleMessage,
+          [
+            instanceId,
+            value,
+          ],
+        ),
+        returnValueForMissingStub: null,
+      );
 }
 
 /// A class which mocks [TestWebSettingsHostApi].
@@ -1181,6 +1196,16 @@
         returnValueForMissingStub: _i5.Future<void>.value(),
       ) as _i5.Future<void>);
   @override
+  _i5.Future<void> setSynchronousReturnValueForOnConsoleMessage(bool? value) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setSynchronousReturnValueForOnConsoleMessage,
+          [value],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
   _i2.WebChromeClient copy() => (super.noSuchMethod(
         Invocation.method(
           #copy,
diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart
index c527038..150ea5a 100644
--- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart
@@ -780,6 +780,16 @@
         returnValueForMissingStub: _i5.Future<void>.value(),
       ) as _i5.Future<void>);
   @override
+  _i5.Future<void> setSynchronousReturnValueForOnConsoleMessage(bool? value) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setSynchronousReturnValueForOnConsoleMessage,
+          [value],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
   _i2.WebChromeClient copy() => (super.noSuchMethod(
         Invocation.method(
           #copy,
diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart
index b88ea8a..a92c054 100644
--- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart
+++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart
@@ -1516,6 +1516,8 @@
   void setSynchronousReturnValueForOnShowFileChooser(
       int instanceId, bool value);
 
+  void setSynchronousReturnValueForOnConsoleMessage(int instanceId, bool value);
+
   static void setup(TestWebChromeClientHostApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
@@ -1568,6 +1570,33 @@
         });
       }
     }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage',
+          codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel, null);
+      } else {
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel,
+                (Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final int? arg_instanceId = (args[0] as int?);
+          assert(arg_instanceId != null,
+              'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null, expected non-null int.');
+          final bool? arg_value = (args[1] as bool?);
+          assert(arg_value != null,
+              'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null, expected non-null bool.');
+          api.setSynchronousReturnValueForOnConsoleMessage(
+              arg_instanceId!, arg_value!);
+          return <Object?>[];
+        });
+      }
+    }
   }
 }