diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
index 4608d1caa44..4fb48de10e3 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 567e201b859..d4cfc698f03 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 @@ private FileChooserMode(final int index) {
}
}
+ /**
+ * Indicates the type of message logged to the console.
+ *
+ *
See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel.
+ */
+ public enum ConsoleMessageLevel {
+ /**
+ * Indicates a message is logged for debugging.
+ *
+ *
See
+ * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG.
+ */
+ DEBUG(0),
+ /**
+ * Indicates a message is provided as an error.
+ *
+ *
See
+ * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR.
+ */
+ ERROR(1),
+ /**
+ * Indicates a message is provided as a basic log message.
+ *
+ *
See
+ * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG.
+ */
+ LOG(2),
+ /**
+ * Indicates a message is provided as a tip.
+ *
+ *
See
+ * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP.
+ */
+ TIP(3),
+ /**
+ * Indicates a message is provided as a warning.
+ *
+ *
See
+ * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING.
+ */
+ WARNING(4),
+ /**
+ * 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(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 @@ ArrayList toList() {
}
}
+ /**
+ * Represents a JavaScript console message from WebCore.
+ *
+ * See https://developer.android.com/reference/android/webkit/ConsoleMessage
+ *
+ *
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 toList() {
+ ArrayList toListResult = new ArrayList(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 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 {
@SuppressWarnings("UnknownNullness")
void success(T result);
@@ -2401,6 +2587,9 @@ public interface WebChromeClientHostApi {
void setSynchronousReturnValueForOnShowFileChooser(
@NonNull Long instanceId, @NonNull Boolean value);
+ void setSynchronousReturnValueForOnConsoleMessage(
+ @NonNull Long instanceId, @NonNull Boolean value);
+
/** The codec used by WebChromeClientHostApi. */
static @NonNull MessageCodec getCodec() {
return new StandardMessageCodec();
@@ -2463,6 +2652,33 @@ static void setup(
channel.setMessageHandler(null);
}
}
+ {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage",
+ getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ ArrayList wrapped = new ArrayList();
+ ArrayList args = (ArrayList) 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 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 @@ static void setup(
}
}
}
+
+ 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) 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 @@ public interface Reply {
}
/** The codec used by WebChromeClientFlutterApi. */
static @NonNull MessageCodec getCodec() {
- return new StandardMessageCodec();
+ return WebChromeClientFlutterApiCodec.INSTANCE;
}
public void onProgressChanged(
@@ -2656,6 +2900,20 @@ public void onGeolocationPermissionsHidePrompt(
new ArrayList(Collections.singletonList(identifierArg)),
channelReply -> callback.reply(null));
}
+ /** Callback to Dart function `WebChromeClient.onConsoleMessage`. */
+ public void onConsoleMessage(
+ @NonNull Long instanceIdArg,
+ @NonNull ConsoleMessage messageArg,
+ @NonNull Reply callback) {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage",
+ getCodec());
+ channel.send(
+ new ArrayList(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 f5097b6393c..b383dfdba11 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 @@ public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi {
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 @@ public void onHideCustomView(
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 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 635c6c30ee9..cb382d51f2b 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 class WebChromeClientHostApiImpl implements WebChromeClientHostApi {
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 @@ public void onPermissionRequest(@NonNull PermissionRequest request) {
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 @@ public void setSynchronousReturnValueForOnShowFileChooser(
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 4e09b2e4b45..ef49dded9b5 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 @@ public void onGeolocationPermissionsHidePrompt() {
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 0e805dd2aef..017e6961e60 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 @@ Future main() async {
);
},
);
+
+ group('Logging', () {
+ testWidgets('can receive console log messages',
+ (WidgetTester tester) async {
+ const String testPage = '''
+
+
+
+ WebResourceError test
+
+
+ Test page
+
+
+ ''';
+
+ final Completer debugMessageReceived = Completer();
+ 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 781a98f87f7..92a8fe2bd77 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 @@ const String kTransparentBackgroundPage = '''