diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index a2bed900ad3f..a6de2939ec1e 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -63,6 +63,9 @@ public void onMethodCall(MethodCall methodCall, Result result) { case "loadUrl": loadUrl(methodCall, result); break; + case "getUserAgent": + getUserAgent(methodCall, result); + break; case "updateSettings": updateSettings(methodCall, result); break; @@ -144,6 +147,11 @@ private void currentUrl(Result result) { result.success(webView.getUrl()); } + private void getUserAgent(MethodCall methodCall, Result result) { + String userAgent = webView.getSettings().getUserAgentString(); + result.success(userAgent); + } + @SuppressWarnings("unchecked") private void updateSettings(MethodCall methodCall, Result result) { applySettings((Map) methodCall.arguments); @@ -207,6 +215,9 @@ private void applySettings(Map settings) { webView.setWebContentsDebuggingEnabled(debuggingEnabled); break; + case "userAgent": + updateUserAgent((String) settings.get(key)); + break; default: throw new IllegalArgumentException("Unknown WebView setting: " + key); } @@ -233,6 +244,10 @@ private void registerJavaScriptChannelNames(List channelNames) { } } + private void updateUserAgent(String userAgent) { + webView.getSettings().setUserAgentString(userAgent); + } + @Override public void dispose() { methodChannel.setMethodCallHandler(null); diff --git a/packages/webview_flutter/example/test_driver/webview.dart b/packages/webview_flutter/example/test_driver/webview.dart index 7a290a05bd5b..179348201c1b 100644 --- a/packages/webview_flutter/example/test_driver/webview.dart +++ b/packages/webview_flutter/example/test_driver/webview.dart @@ -132,6 +132,49 @@ void main() { await controller.evaluateJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }); + + test('userAgent', () async { + final Completer controllerCompleter01 = + Completer(); + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA-01', + onWebViewCreated: (WebViewController controller) { + controllerCompleter01.complete(controller); + }, + ), + ), + ); + final WebViewController controller01 = await controllerCompleter01.future; + final String userAgent01 = await controller01.getUserAgent(); + expect(userAgent01, 'UA-01'); + + // rebuilds a WebView with a different user agent. + final Completer controllerCompleter02 = + Completer(); + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA-02', + onWebViewCreated: (WebViewController controller) { + controllerCompleter02.complete(controller); + }, + ), + ), + ); + final WebViewController controller02 = await controllerCompleter02.future; + final String userAgent02 = await controller02.getUserAgent(); + expect(userAgent02, 'UA-02'); + }); } Future pumpWidget(Widget widget) { diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index afc2f9921d05..dc5948164fc3 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -112,6 +112,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onRemoveJavaScriptChannels:call result:result]; } else if ([[call method] isEqualToString:@"clearCache"]) { [self clearCache:result]; + } else if ([[call method] isEqualToString:@"getUserAgent"]) { + [self onGetUserAgent:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -228,6 +230,22 @@ - (void)clearCache:(FlutterResult)result { } } +- (void)onGetUserAgent:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView evaluateJavaScript:@"navigator.userAgent" + completionHandler:^(NSString* userAgent, NSError* error) { + if (error) { + result([FlutterError + errorWithCode:@"userAgent_failed" + message:@"Failed getting UserAgent" + details:[NSString stringWithFormat: + @"webview_flutter: fail evaluating JavaScript: %@", + [error localizedDescription]]]); + } else { + result(userAgent); + } + }]; +} + - (void)applySettings:(NSDictionary*)settings { for (NSString* key in settings) { if ([key isEqualToString:@"jsMode"]) { @@ -236,6 +254,9 @@ - (void)applySettings:(NSDictionary*)settings { } else if ([key isEqualToString:@"hasNavigationDelegate"]) { NSNumber* hasDartNavigationDelegate = settings[key]; _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; + } else if ([key isEqualToString:@"userAgent"]) { + NSString* userAgent = settings[key]; + [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; } else { NSLog(@"webview_flutter: unknown setting key: %@", key); } @@ -274,6 +295,14 @@ - (bool)loadRequest:(NSDictionary*)request { return false; } +- (void)updateUserAgent:(NSString*)userAgent { + if (@available(iOS 9.0, *)) { + [_webView setCustomUserAgent:userAgent]; + } else { + NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9."); + } +} + - (bool)loadUrl:(NSString*)url { return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 432de6e95d5a..f2e4c13ad1dd 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -119,6 +119,7 @@ class WebView extends StatefulWidget { this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, + this.userAgent, this.gestureRecognizers, this.onPageFinished, this.debuggingEnabled = false, @@ -256,6 +257,9 @@ class WebView extends StatefulWidget { /// By default `debuggingEnabled` is false. final bool debuggingEnabled; + /// The custom UserAgent. + final String userAgent; + @override State createState() => _WebViewState(); } @@ -349,6 +353,7 @@ class _WebSettings { this.javascriptMode, this.hasNavigationDelegate, this.debuggingEnabled, + this.userAgent, }); static _WebSettings fromWidget(WebView widget) { @@ -356,18 +361,21 @@ class _WebSettings { javascriptMode: widget.javascriptMode, hasNavigationDelegate: widget.navigationDelegate != null, debuggingEnabled: widget.debuggingEnabled, + userAgent: widget.userAgent, ); } final JavascriptMode javascriptMode; final bool hasNavigationDelegate; final bool debuggingEnabled; + final String userAgent; Map toMap() { return { 'jsMode': javascriptMode.index, 'hasNavigationDelegate': hasNavigationDelegate, 'debuggingEnabled': debuggingEnabled, + 'userAgent': userAgent, }; } @@ -384,6 +392,9 @@ class _WebSettings { updates['debuggingEnabled'] = newSettings.debuggingEnabled; } + if (userAgent != newSettings.userAgent) { + updates['userAgent'] = newSettings.userAgent; + } return updates; } } @@ -529,6 +540,11 @@ class WebViewController { return _channel.invokeMethod("reload"); } + /// Returns the User-Agent value that will be used for subsequent HTTP requests. + Future getUserAgent() async { + return await _channel.invokeMethod('getUserAgent'); + } + /// Clears all caches used by the [WebView]. /// /// The following caches are cleared: diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index a973e43e6081..7a7a98c80c2c 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -70,6 +70,42 @@ void main() { expect(platformWebView.javascriptMode, JavascriptMode.disabled); }); + testWidgets('Set UserAgent', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.userAgent, isNull); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA', + )); + + expect(platformWebView.userAgent, 'UA'); + }); + + testWidgets('Get UserAgent', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(controller, isNotNull); + expect(await controller.getUserAgent(), 'UA'); + }); + testWidgets('Load url', (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( @@ -823,6 +859,7 @@ class FakePlatformWebView { hasNavigationDelegate = params['settings']['hasNavigationDelegate'] ?? false; debuggingEnabled = params['settings']['debuggingEnabled']; + userAgent = params['settings']['userAgent']; channel = MethodChannel( 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); @@ -842,6 +879,7 @@ class FakePlatformWebView { bool hasNavigationDelegate; bool debuggingEnabled; + String userAgent; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -849,6 +887,8 @@ class FakePlatformWebView { final Map request = call.arguments; _loadUrl(request['url']); return Future.sync(() {}); + case 'userAgent': + return Future.value(userAgent); case 'updateSettings': if (call.arguments['jsMode'] != null) { javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; @@ -859,6 +899,9 @@ class FakePlatformWebView { if (call.arguments['debuggingEnabled'] != null) { debuggingEnabled = call.arguments['debuggingEnabled']; } + if (call.arguments['userAgent'] != null) { + userAgent = call.arguments['userAgent']; + } break; case 'canGoBack': return Future.sync(() => currentPosition > 0);