diff --git a/packages/webview_flutter/lib/src/tizen_webview_controller.dart b/packages/webview_flutter/lib/src/tizen_webview_controller.dart index 707c2dbc1..389177398 100644 --- a/packages/webview_flutter/lib/src/tizen_webview_controller.dart +++ b/packages/webview_flutter/lib/src/tizen_webview_controller.dart @@ -16,7 +16,7 @@ import 'tizen_webview.dart'; const String kTizenNavigationDelegateChannelName = 'plugins.flutter.io/tizen_webview_navigation_delegate_'; -/// An implementation of [PlatformWebViewController] the Tizen WebView API. +/// An implementation of [PlatformWebViewController] using the Tizen WebView API. class TizenWebViewController extends PlatformWebViewController { /// Constructs a [TizenWebViewController]. TizenWebViewController(super.params) diff --git a/packages/webview_flutter/tizen/src/webview.cc b/packages/webview_flutter/tizen/src/webview.cc index 5ae7d252e..6f5282142 100644 --- a/packages/webview_flutter/tizen/src/webview.cc +++ b/packages/webview_flutter/tizen/src/webview.cc @@ -19,10 +19,6 @@ namespace { -typedef flutter::MethodCall FlMethodCall; -typedef flutter::MethodResult FlMethodResult; -typedef flutter::MethodChannel FlMethodChannel; - constexpr size_t kBufferPoolSize = 5; constexpr char kEwkInstance[] = "ewk_instance"; constexpr char kTizenWebViewChannelName[] = "plugins.flutter.io/tizen_webview_"; @@ -133,13 +129,13 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, InitWebView(); - tizen_webview_channel_ = std::make_unique( - GetPluginRegistrar()->messenger(), GetTizenWebViewChannelName(), + webview_channel_ = std::make_unique( + GetPluginRegistrar()->messenger(), GetWebViewChannelName(), &flutter::StandardMethodCodec::GetInstance()); - tizen_webview_channel_->SetMethodCallHandler( + webview_channel_->SetMethodCallHandler( [webview = this](const auto& call, auto result) { - webview->HandleTizenWebViewMethodCall(call, std::move(result)); + webview->HandleWebViewMethodCall(call, std::move(result)); }); navigation_delegate_channel_ = std::make_unique( @@ -170,7 +166,7 @@ void WebView::RegisterJavaScriptChannelName(const std::string& name) { WebView::~WebView() { Dispose(); } -std::string WebView::GetTizenWebViewChannelName() { +std::string WebView::GetWebViewChannelName() { return std::string(kTizenWebViewChannelName) + std::to_string(GetViewId()); } @@ -338,8 +334,8 @@ void WebView::InitWebView() { evas_object_data_set(webview_instance_, kEwkInstance, this); } -void WebView::HandleTizenWebViewMethodCall( - const FlMethodCall& method_call, std::unique_ptr result) { +void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { if (!webview_instance_) { result->Error("Invalid operation", "The webview instance has not been initialized."); @@ -668,7 +664,7 @@ void WebView::OnJavaScriptMessage(Evas_Object* obj, if (obj) { WebView* webview = static_cast(evas_object_data_get(obj, kEwkInstance)); - if (webview->tizen_webview_channel_) { + if (webview->webview_channel_) { std::string channel_name(message.name); std::string message_body(static_cast(message.body)); @@ -678,7 +674,7 @@ void WebView::OnJavaScriptMessage(Evas_Object* obj, {flutter::EncodableValue("message"), flutter::EncodableValue(message_body)}, }; - webview->tizen_webview_channel_->InvokeMethod( + webview->webview_channel_->InvokeMethod( "javaScriptChannelMessage", std::make_unique(args)); } diff --git a/packages/webview_flutter/tizen/src/webview.h b/packages/webview_flutter/tizen/src/webview.h index c268ae9f4..7f5a1d0b0 100644 --- a/packages/webview_flutter/tizen/src/webview.h +++ b/packages/webview_flutter/tizen/src/webview.h @@ -19,6 +19,10 @@ #include #include +typedef flutter::MethodCall FlMethodCall; +typedef flutter::MethodResult FlMethodResult; +typedef flutter::MethodChannel FlMethodChannel; + class BufferPool; class BufferUnit; @@ -52,15 +56,13 @@ class WebView : public PlatformView { size_t height); private: - void HandleTizenWebViewMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - void HandleCookieMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); + void HandleWebViewMethodCall(const FlMethodCall& method_call, + std::unique_ptr result); + void HandleCookieMethodCall(const FlMethodCall& method_call, + std::unique_ptr result); void RegisterJavaScriptChannelName(const std::string& name); - std::string GetTizenWebViewChannelName(); + std::string GetWebViewChannelName(); std::string GetNavigationDelegateChannelName(); void InitWebView(); @@ -86,10 +88,8 @@ class WebView : public PlatformView { BufferUnit* candidate_surface_ = nullptr; BufferUnit* rendered_surface_ = nullptr; bool has_navigation_delegate_ = false; - std::unique_ptr> - tizen_webview_channel_; - std::unique_ptr> - navigation_delegate_channel_; + std::unique_ptr webview_channel_; + std::unique_ptr navigation_delegate_channel_; std::unique_ptr texture_variant_; std::mutex mutex_; std::unique_ptr tbm_pool_; diff --git a/packages/webview_flutter_lwe/CHANGELOG.md b/packages/webview_flutter_lwe/CHANGELOG.md index c46a87856..17458954e 100644 --- a/packages/webview_flutter_lwe/CHANGELOG.md +++ b/packages/webview_flutter_lwe/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.0 + +* Update webivew_flutter to 4.0.2. +* Update webview_flutter_platform_interface to 2.0.1. + ## 0.1.1 * Use only error type names defined in `web_resource_error.dart`. diff --git a/packages/webview_flutter_lwe/README.md b/packages/webview_flutter_lwe/README.md index 240474d5b..918637efd 100644 --- a/packages/webview_flutter_lwe/README.md +++ b/packages/webview_flutter_lwe/README.md @@ -20,8 +20,8 @@ This package is not an _endorsed_ implementation of `webview_flutter`. Therefore ```yaml dependencies: - webview_flutter: ^3.0.4 - webview_flutter_lwe: ^0.1.1 + webview_flutter: ^4.0.2 + webview_flutter_lwe: ^0.2.0 ``` ## Example @@ -30,16 +30,27 @@ dependencies: import 'package:webview_flutter/webview_flutter.dart'; class WebViewExample extends StatefulWidget { - const WebViewExample({Key? key}) : super(key: key); + const WebViewExample({super.key}); @override - WebViewExampleState createState() => WebViewExampleState(); + State createState() => _WebViewExampleState(); } -class WebViewExampleState extends State { +class _WebViewExampleState extends State { + final WebViewController _controller = WebViewController(); + + @override + void initState() { + super.initState(); + + _controller.loadRequest(Uri.parse('https://flutter.dev')); + } + @override Widget build(BuildContext context) { - return WebView(initialUrl: 'https://flutter.dev'); + return Scaffold( + body: WebViewWidget(controller: _controller), + ); } } ``` diff --git a/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart index c3710f944..e92333058 100644 --- a/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart @@ -10,6 +10,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -37,123 +38,68 @@ Future main() async { final String primaryUrl = '$prefixUrl/hello.txt'; final String secondaryUrl = '$prefixUrl/secondary.txt'; - testWidgets('initialUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageFinishedCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: pageFinishedCompleter.complete, - ), - ), - ); + testWidgets('loadRequest', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + + final WebViewController controller = WebViewController() + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + ) + ..loadRequest(Uri.parse(primaryUrl)); - final WebViewController controller = await controllerCompleter.future; - await pageFinishedCompleter.future; + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); - testWidgets('loadUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoads.add(url); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + final Completer pageFinished = Completer(); - await controller.loadUrl(secondaryUrl); - await expectLater( - pageLoads.stream.firstWhere((String url) => url == secondaryUrl), - completion(secondaryUrl), - ); - }); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + ) + ..loadRequest(Uri.parse(primaryUrl)); - testWidgets('evaluateJavascript', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - ), - ), + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; + + await expectLater( + controller.runJavaScriptReturningResult('1 + 1'), + completion(2), ); - final WebViewController controller = await controllerCompleter.future; - // ignore: deprecated_member_use - final String result = await controller.evaluateJavascript('1 + 1'); - expect(result, equals('2')); }); testWidgets('JavascriptChannel', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); + final Completer pageFinished = Completer(); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + ); + final Completer channelCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - // This is the data URL for: '' - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'Echo', - onMessageReceived: (JavascriptMessage message) { - channelCompleter.complete(message.message); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), + await controller.addJavaScriptChannel( + 'Echo', + onMessageReceived: (JavaScriptMessage message) { + channelCompleter.complete(message.message); + }, ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - expect(channelCompleter.isCompleted, isFalse); - await controller.runJavascript('Echo.postMessage("hello");'); + await controller.loadHtmlString( + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; + await controller.runJavaScript('Echo.postMessage("hello");'); await expectLater(channelCompleter.future, completion('hello')); }); @@ -164,7 +110,7 @@ Future main() async { bool resizeButtonTapped = false; await tester.pumpWidget(ResizableWebView( - onResize: (_) { + onResize: () { if (resizeButtonTapped) { buttonTapResizeCompleter.complete(); } else { @@ -173,6 +119,7 @@ Future main() async { }, onPageFinished: () => onPageFinished.complete(), )); + await onPageFinished.future; // Wait for a potential call to resize after page is loaded. await initialResizeCompleter.future.timeout( @@ -181,47 +128,30 @@ Future main() async { ); resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); await tester.pumpAndSettle(); - expect(buttonTapResizeCompleter.future, completes); + + await expectLater(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { - final Completer controllerCompleter1 = - Completer(); - final GlobalKey globalKey = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent1', - onWebViewCreated: (WebViewController controller) { - controllerCompleter1.complete(controller); - }, - ), - ), - ); - final WebViewController controller1 = await controllerCompleter1.future; - final String customUserAgent1 = await _getUserAgent(controller1); - expect(customUserAgent1, 'Custom_User_Agent1'); - // rebuild the WebView with a different user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent2', - ), - ), - ); + final Completer pageFinished = Completer(); - final String customUserAgent2 = await _getUserAgent(controller1); - expect(customUserAgent2, 'Custom_User_Agent2'); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageFinished.complete(), + )) + ..setUserAgent('Custom_User_Agent1') + ..loadRequest(Uri.parse('about:blank')); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; + + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent1'); }); testWidgets('getTitle', (WidgetTester tester) async { @@ -235,39 +165,26 @@ Future main() async { '''; final String getTitleTestBase64 = base64Encode(const Utf8Encoder().convert(getTitleTest)); - final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + )) + ..loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$getTitleTestBase64'), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + await pageLoaded.future; // On at least iOS, it does not appear to be guaranteed that the native // code has the title when the page load completes. Execute some JavaScript // before checking the title to ensure that the page has been fully parsed // and processed. - await controller.runJavascript('1;'); + await controller.runJavaScript('1;'); final String? title = await controller.getTitle(); expect(title, 'Some title'); @@ -300,32 +217,22 @@ Future main() async { base64Encode(const Utf8Encoder().convert(scrollTestPage)); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + )) + ..loadRequest(Uri.parse( + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + )); + + await tester.pumpWidget(WebViewWidget(controller: controller)); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); + Offset scrollPos = await controller.getScrollPosition(); // Check scrollTo() const int X_SCROLL = 123; @@ -333,21 +240,19 @@ Future main() async { // Get the initial position; this ensures that scrollTo is actually // changing something, but also gives the native view's scroll position // time to settle. - expect(scrollPosX, isNot(X_SCROLL)); - expect(scrollPosX, isNot(Y_SCROLL)); + expect(scrollPos.dx, isNot(X_SCROLL)); + expect(scrollPos.dy, isNot(Y_SCROLL)); await controller.scrollTo(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL); - expect(scrollPosY, Y_SCROLL); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL); + expect(scrollPos.dy, Y_SCROLL); // Check scrollBy() (on top of scrollTo()) await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL * 2); - expect(scrollPosY, Y_SCROLL * 2); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL * 2); + expect(scrollPos.dy, Y_SCROLL * 2); }); }); @@ -357,35 +262,29 @@ Future main() async { '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) - ? NavigationDecision.prevent - : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + )); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await tester.pumpWidget(WebViewWidget(controller: controller)); + + controller.loadRequest(Uri.parse(blankPageEncoded)); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for the next page load. - await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -394,30 +293,18 @@ Future main() async { final Completer errorCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://www.notawebsite..com', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + })) + ..loadRequest(Uri.parse('https://www.notawebsite..com')); + + await tester.pumpWidget(WebViewWidget(controller: controller)); final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); - - if (Platform.isIOS) { - expect(error.domain, isNotNull); - expect(error.failingUrl, isNull); - } else if (Platform.isAndroid) { - expect(error.errorType, isNotNull); - expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), - isTrue); - } }); testWidgets('onWebResourceError is not called with valid url', @@ -426,139 +313,85 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageFinishCompleter.complete(), + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + )) + ..loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+'), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }); testWidgets('can block requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) + Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + })); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + controller.loadRequest(Uri.parse(blankPageEncoded)); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller - .runJavascript('location.href = "https://www.youtube.com/"'); + .runJavaScript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. - await pageLoads.stream.first + await pageLoaded.future .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) async { + Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) async { NavigationDecision decision = NavigationDecision.prevent; decision = await Future.delayed( const Duration(milliseconds: 10), () => NavigationDecision.navigate); return decision; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + })); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await tester.pumpWidget(WebViewWidget(controller: controller)); - await pageLoads.stream.first; // Wait for second page to load. - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, secondaryUrl); - }); - }); + controller.loadRequest(Uri.parse(blankPageEncoded)); - testWidgets( - 'can open new window and go back', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(); - }, - initialUrl: primaryUrl, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion(primaryUrl)); - await pageLoaded.future; - pageLoaded = Completer(); + await pageLoaded.future; // Wait for initial page load. - await controller.runJavascript('window.open("$secondaryUrl")'); - await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion(secondaryUrl)); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for second page to load. - expect(controller.canGoBack(), completion(true)); - await controller.goBack(); - await pageLoaded.future; - await expectLater(controller.currentUrl(), completion(primaryUrl)); - }, - ); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); } /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. @@ -567,16 +400,25 @@ Future _getUserAgent(WebViewController controller) async { } Future _runJavascriptReturningResult( - WebViewController controller, String js) async { - return controller.runJavascriptReturningResult(js); + WebViewController controller, + String js, +) async { + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.linux) { + return await controller.runJavaScriptReturningResult(js) as String; + } + return jsonDecode(await controller.runJavaScriptReturningResult(js) as String) + as String; } class ResizableWebView extends StatefulWidget { - const ResizableWebView( - {Key? key, required this.onResize, required this.onPageFinished}) - : super(key: key); + const ResizableWebView({ + super.key, + required this.onResize, + required this.onPageFinished, + }); - final JavascriptMessageHandler onResize; + final VoidCallback onResize; final VoidCallback onPageFinished; @override @@ -584,6 +426,23 @@ class ResizableWebView extends StatefulWidget { } class ResizableWebViewState extends State { + late final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => widget.onPageFinished(), + )) + ..addJavaScriptChannel( + 'Resize', + onMessageReceived: (_) { + widget.onResize(); + }, + ) + ..loadRequest( + Uri.parse( + 'data:text/html;charset=utf-8;base64,${base64Encode(const Utf8Encoder().convert(resizePage))}', + ), + ); + double webViewWidth = 200; double webViewHeight = 200; @@ -606,28 +465,14 @@ class ResizableWebViewState extends State { @override Widget build(BuildContext context) { - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizePage)); return Directionality( textDirection: TextDirection.ltr, child: Column( children: [ SizedBox( - width: webViewWidth, - height: webViewHeight, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: widget.onResize, - ), - }, - onPageFinished: (_) => widget.onPageFinished(), - javascriptMode: JavascriptMode.unrestricted, - ), - ), + width: webViewWidth, + height: webViewHeight, + child: WebViewWidget(controller: controller)), TextButton( key: const Key('resizeButton'), onPressed: () { diff --git a/packages/webview_flutter_lwe/example/lib/main.dart b/packages/webview_flutter_lwe/example/lib/main.dart index 9a586088e..d4c5f846b 100644 --- a/packages/webview_flutter_lwe/example/lib/main.dart +++ b/packages/webview_flutter_lwe/example/lib/main.dart @@ -71,17 +71,63 @@ const String kTransparentBackgroundPage = ''' '''; class WebViewExample extends StatefulWidget { - const WebViewExample({Key? key, this.cookieManager}) : super(key: key); - - final CookieManager? cookieManager; + const WebViewExample({super.key}); @override State createState() => _WebViewExampleState(); } class _WebViewExampleState extends State { - final Completer _controller = - Completer(); + final WebViewController _controller = WebViewController(); + + @override + void initState() { + super.initState(); + + _controller + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + debugPrint('WebView is loading (progress : $progress%)'); + }, + onPageStarted: (String url) { + debugPrint('Page started loading: $url'); + }, + onPageFinished: (String url) { + debugPrint('Page finished loading: $url'); + }, + onWebResourceError: (WebResourceError error) { + debugPrint(''' +Page resource error: + code: ${error.errorCode} + description: ${error.description} + errorType: ${error.errorType} + isForMainFrame: ${error.isForMainFrame} + '''); + }, + onNavigationRequest: (NavigationRequest request) { + debugPrint('On onNavigationReques'); + if (request.url.startsWith('https://www.youtube.com/')) { + debugPrint('blocking navigation to ${request.url}'); + return NavigationDecision.prevent; + } + debugPrint('allowing navigation to ${request.url}'); + return NavigationDecision.navigate; + }, + ), + ) + ..addJavaScriptChannel( + 'Toaster', + onMessageReceived: (JavaScriptMessage message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }, + ) + ..loadRequest(Uri.parse('https://flutter.dev')); + } @override Widget build(BuildContext context) { @@ -91,79 +137,27 @@ class _WebViewExampleState extends State { title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. actions: [ - NavigationControls(_controller.future), - SampleMenu(_controller.future, widget.cookieManager), + NavigationControls(webViewController: _controller), + SampleMenu(webViewController: _controller), ], ), - body: WebView( - initialUrl: 'https://flutter.dev', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - _controller.complete(webViewController); - }, - onProgress: (int progress) { - print('WebView is loading (progress : $progress%)'); - }, - javascriptChannels: { - _toasterJavascriptChannel(context), - }, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - gestureNavigationEnabled: true, - backgroundColor: const Color(0x00000000), - ), + body: WebViewWidget(controller: _controller), floatingActionButton: favoriteButton(), ); } - JavascriptChannel _toasterJavascriptChannel(BuildContext context) { - return JavascriptChannel( - name: 'Toaster', - onMessageReceived: (JavascriptMessage message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message.message)), - ); - }); - } - Widget favoriteButton() { - return FutureBuilder( - future: _controller.future, - builder: (BuildContext context, - AsyncSnapshot controller) { - return FloatingActionButton( - onPressed: () async { - String? url; - if (controller.hasData) { - url = await controller.data!.currentUrl(); - } - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - controller.hasData - ? 'Favorited $url' - : 'Unable to favorite', - ), - ), - ); - } - }, - child: const Icon(Icons.favorite), + return FloatingActionButton( + onPressed: () async { + final String? url = await _controller.currentUrl(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), ); - }); + } + }, + child: const Icon(Icons.favorite), + ); } } @@ -184,137 +178,130 @@ enum MenuOptions { } class SampleMenu extends StatelessWidget { - SampleMenu(this.controller, CookieManager? cookieManager, {Key? key}) - : cookieManager = cookieManager ?? CookieManager(), - super(key: key); + SampleMenu({ + super.key, + required this.webViewController, + }); - final Future controller; - late final CookieManager cookieManager; + final WebViewController webViewController; + late final WebViewCookieManager cookieManager = WebViewCookieManager(); @override Widget build(BuildContext context) { - return FutureBuilder( - future: controller, - builder: - (BuildContext context, AsyncSnapshot controller) { - return PopupMenuButton( - key: const ValueKey('ShowPopupMenu'), - onSelected: (MenuOptions value) { - switch (value) { - case MenuOptions.showUserAgent: - _onShowUserAgent(controller.data!, context); - break; - case MenuOptions.listCookies: - _onListCookies(controller.data!, context); - break; - case MenuOptions.clearCookies: - _onClearCookies(context); - break; - case MenuOptions.addToCache: - _onAddToCache(controller.data!, context); - break; - case MenuOptions.listCache: - _onListCache(controller.data!, context); - break; - case MenuOptions.clearCache: - _onClearCache(controller.data!, context); - break; - case MenuOptions.navigationDelegate: - _onNavigationDelegateExample(controller.data!, context); - break; - case MenuOptions.doPostRequest: - _onDoPostRequest(controller.data!, context); - break; - case MenuOptions.loadLocalFile: - _onLoadLocalFileExample(controller.data!, context); - break; - case MenuOptions.loadFlutterAsset: - _onLoadFlutterAssetExample(controller.data!, context); - break; - case MenuOptions.loadHtmlString: - _onLoadHtmlStringExample(controller.data!, context); - break; - case MenuOptions.transparentBackground: - _onTransparentBackground(controller.data!, context); - break; - case MenuOptions.setCookie: - _onSetCookie(controller.data!, context); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: MenuOptions.showUserAgent, - enabled: controller.hasData, - child: const Text('Show user agent'), - ), - const PopupMenuItem( - value: MenuOptions.listCookies, - child: Text('List cookies'), - ), - const PopupMenuItem( - value: MenuOptions.clearCookies, - child: Text('Clear cookies'), - ), - const PopupMenuItem( - value: MenuOptions.addToCache, - child: Text('Add to cache'), - ), - const PopupMenuItem( - value: MenuOptions.listCache, - child: Text('List cache'), - ), - const PopupMenuItem( - value: MenuOptions.clearCache, - child: Text('Clear cache'), - ), - const PopupMenuItem( - value: MenuOptions.navigationDelegate, - child: Text('Navigation Delegate example'), - ), - const PopupMenuItem( - value: MenuOptions.doPostRequest, - child: Text('Post Request'), - ), - const PopupMenuItem( - value: MenuOptions.loadHtmlString, - child: Text('Load HTML string'), - ), - const PopupMenuItem( - value: MenuOptions.loadLocalFile, - child: Text('Load local file'), - ), - const PopupMenuItem( - value: MenuOptions.loadFlutterAsset, - child: Text('Load Flutter Asset'), - ), - const PopupMenuItem( - key: ValueKey('ShowTransparentBackgroundExample'), - value: MenuOptions.transparentBackground, - child: Text('Transparent background example'), - ), - const PopupMenuItem( - value: MenuOptions.setCookie, - child: Text('Set cookie'), - ), - ], - ); + return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(); + break; + case MenuOptions.listCookies: + _onListCookies(context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(context); + break; + case MenuOptions.listCache: + _onListCache(); + break; + case MenuOptions.clearCache: + _onClearCache(context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(); + break; + case MenuOptions.doPostRequest: + _onDoPostRequest(); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(); + break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(); + break; + case MenuOptions.setCookie: + _onSetCookie(); + break; + } }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: MenuOptions.showUserAgent, + child: Text('Show user agent'), + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), + ], ); } - Future _onShowUserAgent( - WebViewController controller, BuildContext context) async { + Future _onShowUserAgent() { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.runJavascript( - 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + return webViewController.runJavaScript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);', + ); } - Future _onListCookies( - WebViewController controller, BuildContext context) async { - final String cookies = - await controller.runJavascriptReturningResult('document.cookie'); + Future _onListCookies(BuildContext context) async { + final String cookies = await webViewController + .runJavaScriptReturningResult('document.cookie') as String; if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( @@ -329,10 +316,10 @@ class SampleMenu extends StatelessWidget { } } - Future _onAddToCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript( - 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + Future _onAddToCache(BuildContext context) async { + await webViewController.runJavaScript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), @@ -340,17 +327,17 @@ class SampleMenu extends StatelessWidget { } } - Future _onListCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript('caches.keys()' + Future _onListCache() { + return webViewController.runJavaScript('caches.keys()' // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } - Future _onClearCache( - WebViewController controller, BuildContext context) async { - await controller.clearCache(); + Future _onClearCache(BuildContext context) async { + await webViewController.clearCache(); + // This is unimplemented in webview_flutter_lwe. + // await webViewController.clearLocalStorage(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Cache cleared.'), @@ -371,53 +358,53 @@ class SampleMenu extends StatelessWidget { } } - Future _onNavigationDelegateExample( - WebViewController controller, BuildContext context) async { - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - await controller.loadUrl('data:text/html;base64,$contentBase64'); + Future _onNavigationDelegateExample() { + final String contentBase64 = base64Encode( + const Utf8Encoder().convert(kNavigationExamplePage), + ); + return webViewController.loadRequest( + Uri.parse('data:text/html;base64,$contentBase64'), + ); } - Future _onSetCookie( - WebViewController controller, BuildContext context) async { + Future _onSetCookie() async { await cookieManager.setCookie( const WebViewCookie( - name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + name: 'foo', + value: 'bar', + domain: 'httpbin.org', + path: '/anything', + ), ); - await controller.loadUrl('https://httpbin.org/anything'); + await webViewController.loadRequest(Uri.parse( + 'https://httpbin.org/anything', + )); } - Future _onDoPostRequest( - WebViewController controller, BuildContext context) async { - final WebViewRequest request = WebViewRequest( - uri: Uri.parse('https://httpbin.org/post'), - method: WebViewRequestMethod.post, + Future _onDoPostRequest() { + return webViewController.loadRequest( + Uri.parse('https://httpbin.org/post'), + method: LoadRequestMethod.post, headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, body: Uint8List.fromList('Test Body'.codeUnits), ); - await controller.loadRequest(request); } - Future _onLoadLocalFileExample( - WebViewController controller, BuildContext context) async { + Future _onLoadLocalFileExample() async { final String pathToIndex = await _prepareLocalFile(); - - await controller.loadFile(pathToIndex); + await webViewController.loadFile(pathToIndex); } - Future _onLoadFlutterAssetExample( - WebViewController controller, BuildContext context) async { - await controller.loadFlutterAsset('assets/www/index.html'); + Future _onLoadFlutterAssetExample() { + return webViewController.loadFlutterAsset('assets/www/index.html'); } - Future _onLoadHtmlStringExample( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kLocalExamplePage); + Future _onLoadHtmlStringExample() { + return webViewController.loadHtmlString(kLocalExamplePage); } - Future _onTransparentBackground( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kTransparentBackgroundPage); + Future _onTransparentBackground() { + return webViewController.loadHtmlString(kTransparentBackgroundPage); } Widget _getCookieList(String cookies) { @@ -447,70 +434,47 @@ class SampleMenu extends StatelessWidget { } class NavigationControls extends StatelessWidget { - const NavigationControls(this._webViewControllerFuture, {Key? key}) - : assert(_webViewControllerFuture != null), - super(key: key); + const NavigationControls({super.key, required this.webViewController}); - final Future _webViewControllerFuture; + final WebViewController webViewController; @override Widget build(BuildContext context) { - return FutureBuilder( - future: _webViewControllerFuture, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - final bool webViewReady = - snapshot.connectionState == ConnectionState.done; - final WebViewController? controller = snapshot.data; - return Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoBack()) { - await controller.goBack(); - } else { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No back history item')), - ); - } - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.arrow_forward_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoForward()) { - await controller.goForward(); - } else { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No forward history item')), - ); - } - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.replay), - onPressed: !webViewReady - ? null - : () { - controller!.reload(); - }, - ), - ], - ); - }, + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () async { + if (await webViewController.canGoBack()) { + await webViewController.goBack(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: () async { + if (await webViewController.canGoForward()) { + await webViewController.goForward(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: () => webViewController.reload(), + ), + ], ); } } diff --git a/packages/webview_flutter_lwe/example/pubspec.yaml b/packages/webview_flutter_lwe/example/pubspec.yaml index e2cc16af9..3cb7265ad 100644 --- a/packages/webview_flutter_lwe/example/pubspec.yaml +++ b/packages/webview_flutter_lwe/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the webview_flutter_lwe plugin. publish_to: none environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -12,7 +12,7 @@ dependencies: path_provider: ^2.0.7 path_provider_tizen: path: ../../path_provider/ - webview_flutter: ^3.0.4 + webview_flutter: ^4.0.2 webview_flutter_lwe: path: ../ diff --git a/packages/webview_flutter_lwe/lib/src/lwe_webview.dart b/packages/webview_flutter_lwe/lib/src/lwe_webview.dart new file mode 100644 index 000000000..6f7f38e4d --- /dev/null +++ b/packages/webview_flutter_lwe/lib/src/lwe_webview.dart @@ -0,0 +1,199 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// 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. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_tizen/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// The channel name of [LweWebView]. +const String kLweWebViewChannelName = 'plugins.flutter.io/lwe_webview_'; + +/// A lwe webview that displays web pages. +class LweWebView { + /// Whether the [LweNavigationDelegate] is set by the [PlatformWebViewController]. + /// + /// Defaults to false. + bool hasNavigationDelegate = false; + + late final MethodChannel _lweWebViewChannel; + bool _isCreated = false; + + final Map _javaScriptChannelParams = + {}; + final Map _pendingMethodCalls = {}; + + Future _onMethodCall(MethodCall call) async { + switch (call.method) { + case 'javaScriptChannelMessage': + final Map arguments = + (call.arguments as Map).cast(); + final String channel = arguments['channel']! as String; + final String message = arguments['message']! as String; + if (_javaScriptChannelParams.containsKey(channel)) { + _javaScriptChannelParams[channel] + ?.onMessageReceived(JavaScriptMessage(message: message)); + } + + return true; + } + + throw MissingPluginException( + '${call.method} was invoked but has no handler', + ); + } + + Future _invokeChannelMethod(String method, [dynamic arguments]) async { + if (!_isCreated) { + _pendingMethodCalls[method] = arguments; + return null; + } + + return _lweWebViewChannel.invokeMethod(method, arguments); + } + + /// Called when [TizenView] is created. Invokes the requested method call before [LweWebView] is created. + void onCreate(int viewId) { + _isCreated = true; + _lweWebViewChannel = + MethodChannel(kLweWebViewChannelName + viewId.toString()); + _lweWebViewChannel.setMethodCallHandler(_onMethodCall); + + _callPendingMethodCalls(); + } + + /// Applies the requested settings before [TizenView] is created. + void _callPendingMethodCalls() { + if (hasNavigationDelegate) { + _invokeChannelMethod( + 'hasNavigationDelegate', hasNavigationDelegate); + } + + _pendingMethodCalls.forEach((String method, dynamic arguments) { + _lweWebViewChannel.invokeMethod(method, arguments); + }); + _pendingMethodCalls.clear(); + } + + /// Loads the file located on the specified [absoluteFilePath]. + Future loadFile(String absoluteFilePath) { + return _invokeChannelMethod('loadFile', absoluteFilePath); + } + + /// Loads the Flutter asset specified in the pubspec.yaml file. + Future loadFlutterAsset(String key) { + return _invokeChannelMethod('loadFlutterAsset', key); + } + + /// Loads the supplied HTML string. + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + return _invokeChannelMethod('loadHtmlString', { + 'html': html, + 'baseUrl': baseUrl, + }); + } + + /// Makes a specific HTTP request ands loads the response in the webview. + Future loadRequest(String uri) { + return _invokeChannelMethod( + 'loadRequest', {'url': uri}); + } + + /// Accessor to the current URL that the WebView is displaying. + Future currentUrl() => _invokeChannelMethod('currentUrl'); + + /// Checks whether there's a back history item. + Future canGoBack() async { + return await _invokeChannelMethod('canGoBack') ?? false; + } + + /// Checks whether there's a forward history item. + Future canGoForward() async { + return await _invokeChannelMethod('canGoForward') ?? false; + } + + /// Goes back in the history of this WebView. + Future goBack() => _invokeChannelMethod('goBack'); + + /// Goes forward in the history of this WebView. + Future goForward() => _invokeChannelMethod('goForward'); + + /// Reloads the current URL. + Future reload() => _invokeChannelMethod('reload'); + + /// Clears all caches used by the [WebView]. + Future clearCache() => _invokeChannelMethod('clearCache'); + + /// Sets the JavaScript execution mode to be used by the webview. + Future setJavaScriptMode(int javaScriptMode) => + _invokeChannelMethod('javaScriptMode', javaScriptMode); + + /// Returns the title of the currently loaded page. + Future getTitle() => _invokeChannelMethod('getTitle'); + + /// Sets the scrolled position of this view. + Future scrollTo(int x, int y) => + _invokeChannelMethod('scrollTo', { + 'x': x, + 'y': y, + }); + + /// Moves the scrolled position of this view. + Future scrollBy(int x, int y) => + _invokeChannelMethod('scrollBy', { + 'x': x, + 'y': y, + }); + + /// Returns the current scroll position of this view. + Future getScrollPosition() async { + final Map? position = + (await _invokeChannelMethod>('getScrollPosition')) + ?.cast(); + if (position == null) { + return Offset.zero; + } + return Offset(position['x']! as double, position['y']! as double); + } + + /// Sets the background color of this WebView. + Future setBackgroundColor(Color color) => + _invokeChannelMethod('backgroundColor', color.value); + + /// Adds a new JavaScript channel to the set of enabled channels. + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams) { + _javaScriptChannelParams[javaScriptChannelParams.name] = + javaScriptChannelParams; + return _invokeChannelMethod( + 'addJavaScriptChannel', javaScriptChannelParams.name); + } + + /// Runs the given JavaScript in the context of the current page. + Future runJavaScript(String javaScript) => + _invokeChannelMethod('runJavaScript', javaScript); + + /// Runs the given JavaScript in the context of the current page, and returns the result. + Future runJavaScriptReturningResult(String javaScript) async { + final String? result = await _invokeChannelMethod( + 'runJavaScriptReturningResult', javaScript); + if (result == null) { + return ''; + } else if (result == 'true') { + return true; + } else if (result == 'false') { + return false; + } + return num.tryParse(result) ?? result; + } + + /// Sets the value used for the HTTP `User-Agent:` request header. + Future setUserAgent(String? userAgent) => + _invokeChannelMethod('userAgent', userAgent); +} diff --git a/packages/webview_flutter_lwe/lib/src/lwe_webview_controller.dart b/packages/webview_flutter_lwe/lib/src/lwe_webview_controller.dart new file mode 100644 index 000000000..8ea9a13ba --- /dev/null +++ b/packages/webview_flutter_lwe/lib/src/lwe_webview_controller.dart @@ -0,0 +1,405 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// 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. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_tizen/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'lwe_webview.dart'; + +/// The channel name of [LweNavigationDelegate]. +const String kLweNavigationDelegateChannelName = + 'plugins.flutter.io/lwe_webview_navigation_delegate_'; + +/// An implementation of [PlatformWebViewController] using the Lightweight Web Engine. +class LweWebViewController extends PlatformWebViewController { + /// Constructs a [LweWebViewController]. + LweWebViewController(super.params) + : _webview = LweWebView(), + super.implementation(); + + final LweWebView _webview; + late LweNavigationDelegate _lweNavigationDelegate; + + /// Called when [TizenView] is created. + void onCreate(int viewId) { + if (_webview.hasNavigationDelegate) { + _lweNavigationDelegate.onCreate(viewId); + } + _webview.onCreate(viewId); + } + + @override + Future loadFile(String absoluteFilePath) { + assert(absoluteFilePath != null); + return _webview.loadFile(absoluteFilePath); + } + + @override + Future loadFlutterAsset(String key) { + assert(key.isNotEmpty); + return _webview.loadFlutterAsset(key); + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + assert(html != null); + return _webview.loadHtmlString(html, baseUrl: baseUrl); + } + + @override + Future loadRequest(LoadRequestParams params) { + if (!params.uri.hasScheme) { + throw ArgumentError( + 'LoadRequestParams#uri is required to have a scheme.'); + } + + if (params.headers.isNotEmpty) { + throw ArgumentError('LoadRequestParams#headers is not supported.'); + } + + if (params.body != null) { + throw ArgumentError('LoadRequestParams#body is not supported.'); + } + + switch (params.method) { + case LoadRequestMethod.get: + return _webview.loadRequest(params.uri.toString()); + case LoadRequestMethod.post: + break; + } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError( + 'This version of `LweWebViewController` currently has no ' + 'implementation for HTTP method ${params.method.serialize()} in ' + 'loadRequest.'); + } + + @override + Future currentUrl() => _webview.currentUrl(); + + @override + Future canGoBack() => _webview.canGoBack(); + + @override + Future canGoForward() => _webview.canGoForward(); + + @override + Future goBack() => _webview.goBack(); + + @override + Future goForward() => _webview.goForward(); + + @override + Future reload() => _webview.reload(); + + @override + Future clearCache() => _webview.clearCache(); + + @override + Future clearLocalStorage() { + throw UnimplementedError( + 'This version of `LweWebViewController` currently has no ' + 'implementation.'); + } + + @override + Future setJavaScriptMode(JavaScriptMode javaScriptMode) => + _webview.setJavaScriptMode(javaScriptMode.index); + + @override + Future getTitle() => _webview.getTitle(); + + @override + Future scrollTo(int x, int y) => _webview.scrollTo(x, y); + + @override + Future scrollBy(int x, int y) => _webview.scrollBy(x, y); + + @override + Future getScrollPosition() => _webview.getScrollPosition(); + + @override + Future setBackgroundColor(Color color) => + _webview.setBackgroundColor(color); + + @override + Future setPlatformNavigationDelegate( + covariant LweNavigationDelegate handler) async { + _lweNavigationDelegate = handler; + _webview.hasNavigationDelegate = true; + } + + @override + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams) => + _webview.addJavaScriptChannel(javaScriptChannelParams); + + @override + Future runJavaScript(String javaScript) => + _webview.runJavaScript(javaScript); + + @override + Future runJavaScriptReturningResult(String javaScript) => + _webview.runJavaScriptReturningResult(javaScript); + + @override + Future setUserAgent(String? userAgent) => + _webview.setUserAgent(userAgent); +} + +/// An implementation of [PlatformWebViewWidget] with the Lightweight Web Engine. +class LweWebViewWidget extends PlatformWebViewWidget { + /// Constructs a [LweWebViewWidget]. + LweWebViewWidget(super.params) : super.implementation(); + + @override + Widget build(BuildContext context) { + return TizenView( + key: params.key, + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + final LweWebViewController controller = + params.controller as LweWebViewController; + controller.onCreate(id); + }, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + ); + } +} + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +@immutable +class LweWebResourceError extends WebResourceError { + /// Creates a new [LweWebResourceError]. + LweWebResourceError._({ + required super.errorCode, + required super.description, + super.isForMainFrame, + this.failingUrl, + }) : super(errorType: _errorCodeToErrorType(errorCode)); + + /// Unknown error. + static const int unknown = 1; + + /// Server or proxy hostname lookup failed. + static const int hostLookup = 2; + + /// Unsupported authentication scheme (not basic or digest). + static const int unsupportedAuthScheme = 3; + + /// User authentication failed on server. + static const int authentication = 4; + + /// User authentication failed on proxy. + static const int proxyAuthentication = 5; + + /// Failed to connect to the server. + static const int connect = 6; + + /// Failed to read or write to the server. + static const int io = 7; + + /// Connection timed out. + static const int timeout = 8; + + /// Too many redirects. + static const int redirectLoop = 9; + + /// Unsupported URI scheme. + static const int unsupportedScheme = 10; + + /// Failed to perform SSL handshake. + static const int failedSSLHandshake = 11; + + /// Malformed URL. + static const int badURL = 12; + + /// Generic file error. + static const int file = 13; + + /// File not found. + static const int fileNotFound = 14; + + /// Too many requests during this load. + static const int tooManyRequests = 15; + + /// Gets the URL for which the failing resource request was made. + final String? failingUrl; + + static WebResourceErrorType? _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case unknown: + return WebResourceErrorType.unknown; + case hostLookup: + return WebResourceErrorType.hostLookup; + case unsupportedAuthScheme: + return WebResourceErrorType.unsupportedAuthScheme; + case authentication: + return WebResourceErrorType.authentication; + case proxyAuthentication: + return WebResourceErrorType.proxyAuthentication; + case connect: + return WebResourceErrorType.connect; + case io: + return WebResourceErrorType.io; + case timeout: + return WebResourceErrorType.timeout; + case redirectLoop: + return WebResourceErrorType.redirectLoop; + case unsupportedScheme: + return WebResourceErrorType.unsupportedScheme; + case failedSSLHandshake: + return WebResourceErrorType.failedSslHandshake; + case badURL: + return WebResourceErrorType.badUrl; + case file: + return WebResourceErrorType.file; + case fileNotFound: + return WebResourceErrorType.fileNotFound; + case tooManyRequests: + return WebResourceErrorType.tooManyRequests; + } + + throw ArgumentError( + 'Could not find a WebResourceErrorType for errorCode: $errorCode', + ); + } +} + +/// A place to register callback methods responsible to handle navigation events +/// triggered by the [LweWebView]. +class LweNavigationDelegate extends PlatformNavigationDelegate { + /// Creates a new [LweNavigationDelegate]. + LweNavigationDelegate(super.params) : super.implementation(); + + late final MethodChannel _navigationDelegateChannel; + PageEventCallback? _onPageFinished; + PageEventCallback? _onPageStarted; + ProgressCallback? _onProgress; + WebResourceErrorCallback? _onWebResourceError; + NavigationRequestCallback? _onNavigationRequest; + + /// Called when [TizenView] is created. + void onCreate(int viewId) { + _navigationDelegateChannel = + MethodChannel(kLweNavigationDelegateChannelName + viewId.toString()); + _navigationDelegateChannel.setMethodCallHandler((MethodCall call) async { + final Map arguments = + (call.arguments as Map).cast(); + switch (call.method) { + case 'navigationRequest': + return _handleNavigation(arguments['url']! as String, + isForMainFrame: arguments['isForMainFrame']! as bool); + case 'onPageFinished': + if (_onPageFinished != null) { + _onPageFinished!(arguments['url']! as String); + } + return null; + case 'onProgress': + if (_onProgress != null) { + _onProgress!(arguments['progress']! as int); + } + return null; + case 'onPageStarted': + if (_onPageStarted != null) { + _onPageStarted!(arguments['url']! as String); + } + return null; + case 'onWebResourceError': + if (_onWebResourceError != null) { + _onWebResourceError!(LweWebResourceError._( + errorCode: arguments['errorCode']! as int, + description: arguments['description']! as String, + failingUrl: arguments['failingUrl']! as String, + isForMainFrame: true, + )); + } + return null; + } + + throw MissingPluginException( + '${call.method} was invoked but has no handler', + ); + }); + } + + Future _handleNavigation( + String url, { + required bool isForMainFrame, + }) async { + final NavigationRequestCallback? onNavigationRequest = _onNavigationRequest; + + if (onNavigationRequest == null) { + return true; + } + + final FutureOr returnValue = + onNavigationRequest(NavigationRequest( + url: url, + isMainFrame: isForMainFrame, + )); + + if (returnValue is NavigationDecision && + returnValue == NavigationDecision.navigate) { + return true; + } else if (returnValue is Future) { + return returnValue.then((NavigationDecision shouldLoadUrl) { + if (shouldLoadUrl == NavigationDecision.navigate) { + return true; + } + return false; + }); + } + return false; + } + + @override + Future setOnNavigationRequest( + NavigationRequestCallback onNavigationRequest, + ) async { + _onNavigationRequest = onNavigationRequest; + } + + @override + Future setOnPageStarted( + PageEventCallback onPageStarted, + ) async { + _onPageStarted = onPageStarted; + } + + @override + Future setOnPageFinished( + PageEventCallback onPageFinished, + ) async { + _onPageFinished = onPageFinished; + } + + @override + Future setOnProgress( + ProgressCallback onProgress, + ) async { + _onProgress = onProgress; + } + + @override + Future setOnWebResourceError( + WebResourceErrorCallback onWebResourceError, + ) async { + _onWebResourceError = onWebResourceError; + } +} diff --git a/packages/webview_flutter_lwe/lib/src/lwe_webview_cookie_manager.dart b/packages/webview_flutter_lwe/lib/src/lwe_webview_cookie_manager.dart new file mode 100644 index 000000000..f9684a09a --- /dev/null +++ b/packages/webview_flutter_lwe/lib/src/lwe_webview_cookie_manager.dart @@ -0,0 +1,44 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// 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. + +import 'package:flutter/services.dart'; + +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class LweWebViewCookieManager extends PlatformWebViewCookieManager { + /// Creates a new [LweWebViewCookieManager]. + LweWebViewCookieManager(super.params) : super.implementation(); + + static const MethodChannel _cookieManagerChannel = + MethodChannel('plugins.flutter.io/lwe_cookie_manager'); + + @override + Future clearCookies() async { + return await _cookieManagerChannel.invokeMethod('clearCookies') ?? + false; + } + + @override + Future setCookie(WebViewCookie cookie) async { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + throw UnimplementedError( + 'This version of `LweWebViewCookieManager` currently has no ' + 'implementation for setCookie method.'); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +} diff --git a/packages/webview_flutter_lwe/lib/src/lwe_webview_platform.dart b/packages/webview_flutter_lwe/lib/src/lwe_webview_platform.dart new file mode 100644 index 000000000..a2d30eaaa --- /dev/null +++ b/packages/webview_flutter_lwe/lib/src/lwe_webview_platform.dart @@ -0,0 +1,45 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// 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. + +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'lwe_webview_controller.dart'; +import 'lwe_webview_cookie_manager.dart'; + +/// An implementation of [WebViewPlatform] using the Lightweight Web Engine. +class LweWebViewPlatform extends WebViewPlatform { + @override + PlatformWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + return LweWebViewController(params); + } + + @override + PlatformNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + return LweNavigationDelegate(params); + } + + @override + PlatformWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return LweWebViewWidget(params); + } + + @override + PlatformWebViewCookieManager createPlatformCookieManager( + PlatformWebViewCookieManagerCreationParams params, + ) { + return LweWebViewCookieManager(params); + } + + /// Gets called when the plugin is registered. + static void register() { + WebViewPlatform.instance = LweWebViewPlatform(); + } +} diff --git a/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart b/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart index 95858ec0b..e84611276 100644 --- a/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart +++ b/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart @@ -1,94 +1,10 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. // 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. -import 'dart:async'; +library webview_flutter_lwe; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_tizen/widgets.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; - -/// Builds a Tizen webview. -/// -/// This is used as the default implementation for [WebView.platform] on Tizen. It uses a method channel to -/// communicate with the platform code. -class LweWebView implements WebViewPlatform { - /// Sets a tizen [WebViewPlatform]. - static void register() { - WebView.platform = LweWebView(); - WebViewCookieManagerPlatform.instance = WebViewTizenCookieManager(); - } - - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - required JavascriptChannelRegistry javascriptChannelRegistry, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - onLongPress: () {}, - excludeFromSemantics: true, - child: TizenView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, - webViewPlatformCallbacksHandler, - javascriptChannelRegistry, - )); - }, - gestureRecognizers: gestureRecognizers, - layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } - - @override - Future clearCookies() { - if (WebViewCookieManagerPlatform.instance == null) { - throw Exception( - 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); - } - return WebViewCookieManagerPlatform.instance!.clearCookies(); - } -} - -/// Handles all cookie operations for the current platform. -class WebViewTizenCookieManager extends WebViewCookieManagerPlatform { - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); - - @override - Future setCookie(WebViewCookie cookie) async { - if (!_isValidPath(cookie.path)) { - throw ArgumentError( - 'The path property for the provided cookie was not given a legal value.'); - } - return MethodChannelWebViewPlatform.setCookie(cookie); - } - - bool _isValidPath(String path) { - // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 - for (final int char in path.codeUnits) { - if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { - return false; - } - } - return true; - } -} +export 'src/lwe_webview_controller.dart'; +export 'src/lwe_webview_cookie_manager.dart'; +export 'src/lwe_webview_platform.dart'; diff --git a/packages/webview_flutter_lwe/pubspec.yaml b/packages/webview_flutter_lwe/pubspec.yaml index b8273f132..5d55e8c1c 100644 --- a/packages/webview_flutter_lwe/pubspec.yaml +++ b/packages/webview_flutter_lwe/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_flutter_lwe description: Tizen implementation of the webview_flutter plugin backed by Lightweight Web Engine. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/webview_flutter_lwe -version: 0.1.1 +version: 0.2.0 environment: sdk: ">=2.17.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=3.0.0" flutter: plugin: @@ -14,11 +14,11 @@ flutter: tizen: pluginClass: WebviewFlutterLwePlugin fileName: webview_flutter_lwe_plugin.h - dartPluginClass: LweWebView + dartPluginClass: LweWebViewPlatform dependencies: flutter: sdk: flutter - flutter_tizen: ^0.2.0 - webview_flutter: ^3.0.4 - webview_flutter_platform_interface: ^1.8.0 + flutter_tizen: ^0.2.1 + webview_flutter: ^4.0.2 + webview_flutter_platform_interface: ^2.0.1 diff --git a/packages/webview_flutter_lwe/tizen/src/webview.cc b/packages/webview_flutter_lwe/tizen/src/webview.cc index fd4124dee..5187f0fb3 100644 --- a/packages/webview_flutter_lwe/tizen/src/webview.cc +++ b/packages/webview_flutter_lwe/tizen/src/webview.cc @@ -19,7 +19,12 @@ #include "lwe/PlatformIntegrationData.h" #include "webview_factory.h" -static constexpr size_t kBufferPoolSize = 5; +namespace { + +constexpr size_t kBufferPoolSize = 5; +constexpr char kLweWebViewChannelName[] = "plugins.flutter.io/lwe_webview_"; +constexpr char kLweNavigationDelegateChannelName[] = + "plugins.flutter.io/lwe_webview_navigation_delegate_"; extern "C" size_t LWE_EXPORT createWebViewInstance( unsigned x, unsigned y, unsigned width, unsigned height, @@ -30,8 +35,7 @@ extern "C" size_t LWE_EXPORT createWebViewInstance( const std::function& flushCb, bool useSWBackend); -class NavigationRequestResult - : public flutter::MethodResult { +class NavigationRequestResult : public FlMethodResult { public: NavigationRequestResult(std::string url, WebView* webview) : url_(url), webview_(webview) {} @@ -65,63 +69,6 @@ class NavigationRequestResult WebView* webview_; }; -enum class ResourceErrorType { - NoError, - UnknownError, - HostLookupError, - UnsupportedAuthSchemeError, - AuthenticationError, - ProxyAuthenticationError, - ConnectError, - IOError, - TimeoutError, - RedirectLoopError, - UnsupportedSchemeError, - FailedSSLHandshakeError, - BadURLError, - FileError, - FileNotFoundError, - TooManyRequestError, -}; - -static std::string ErrorCodeToString(int error_code) { - switch (ResourceErrorType(error_code)) { - case ResourceErrorType::AuthenticationError: - return "authentication"; - case ResourceErrorType::BadURLError: - return "badUrl"; - case ResourceErrorType::ConnectError: - return "connect"; - case ResourceErrorType::FailedSSLHandshakeError: - return "failedSslHandshake"; - case ResourceErrorType::FileError: - return "file"; - case ResourceErrorType::FileNotFoundError: - return "fileNotFound"; - case ResourceErrorType::HostLookupError: - return "hostLookup"; - case ResourceErrorType::IOError: - return "io"; - case ResourceErrorType::ProxyAuthenticationError: - return "proxyAuthentication"; - case ResourceErrorType::RedirectLoopError: - return "redirectLoop"; - case ResourceErrorType::TimeoutError: - return "timeout"; - case ResourceErrorType::TooManyRequestError: - return "tooManyRequests"; - case ResourceErrorType::UnknownError: - return "unknown"; - case ResourceErrorType::UnsupportedAuthSchemeError: - return "unsupportedAuthScheme"; - case ResourceErrorType::UnsupportedSchemeError: - return "unsupportedScheme"; - default: - LOG_ERROR("Unknown error type: %d", error_code); - return "unknown"; - } -} - template static bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, std::string key, T* out) { @@ -151,6 +98,8 @@ static bool IsRunningOnEmulator() { return result; } +} // namespace + WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, flutter::TextureRegistrar* texture_registrar, double width, double height, const flutter::EncodableValue& params) @@ -176,83 +125,47 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, InitWebView(); - channel_ = std::make_unique>( - GetPluginRegistrar()->messenger(), GetChannelName(), + webview_channel_ = std::make_unique( + GetPluginRegistrar()->messenger(), GetWebViewChannelName(), &flutter::StandardMethodCodec::GetInstance()); - channel_->SetMethodCallHandler( + webview_channel_->SetMethodCallHandler( [webview = this](const auto& call, auto result) { - webview->HandleMethodCall(call, std::move(result)); + webview->HandleWebViewMethodCall(call, std::move(result)); }); - auto cookie_channel = - std::make_unique>( - GetPluginRegistrar()->messenger(), - "plugins.flutter.io/cookie_manager", - &flutter::StandardMethodCodec::GetInstance()); + navigation_delegate_channel_ = std::make_unique( + GetPluginRegistrar()->messenger(), GetNavigationDelegateChannelName(), + &flutter::StandardMethodCodec::GetInstance()); + + auto cookie_channel = std::make_unique( + GetPluginRegistrar()->messenger(), + "plugins.flutter.io/lwe_cookie_manager", + &flutter::StandardMethodCodec::GetInstance()); cookie_channel->SetMethodCallHandler( [webview = this](const auto& call, auto result) { webview->HandleCookieMethodCall(call, std::move(result)); }); - std::string url; - if (!GetValueFromEncodableMap(¶ms, "initialUrl", &url)) { - url = "about:blank"; - } - - int32_t color; - if (GetValueFromEncodableMap(¶ms, "backgroundColor", &color)) { - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetBaseBackgroundColor(color >> 16 & 0xff, color >> 8 & 0xff, - color & 0xff, color >> 24 & 0xff); - webview_instance_->SetSettings(settings); - } - - flutter::EncodableMap settings; - if (GetValueFromEncodableMap(¶ms, "settings", &settings)) { - ApplySettings(settings); - } - - flutter::EncodableList names; - if (GetValueFromEncodableMap(¶ms, "javascriptChannelNames", &names)) { - for (flutter::EncodableValue name : names) { - if (std::holds_alternative(name)) { - RegisterJavaScriptChannelName(std::get(name)); - } - } - } - - // TODO: Implement autoMediaPlaybackPolicy. - - std::string user_agent; - if (GetValueFromEncodableMap(¶ms, "userAgent", &user_agent)) { - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetUserAgentString(user_agent); - webview_instance_->SetSettings(settings); - } - webview_instance_->RegisterOnPageStartedHandler( [this](LWE::WebContainer* container, const std::string& url) { flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; - channel_->InvokeMethod("onPageStarted", - std::make_unique(args)); + navigation_delegate_channel_->InvokeMethod( + "onPageStarted", std::make_unique(args)); }); webview_instance_->RegisterOnPageLoadedHandler( [this](LWE::WebContainer* container, const std::string& url) { flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; - channel_->InvokeMethod("onPageFinished", - std::make_unique(args)); + navigation_delegate_channel_->InvokeMethod( + "onPageFinished", std::make_unique(args)); }); webview_instance_->RegisterOnProgressChangedHandler( [this](LWE::WebContainer* container, int progress) { - if (!has_progress_tracking_) { - return; - } flutter::EncodableMap args = {{flutter::EncodableValue("progress"), flutter::EncodableValue(progress)}}; - channel_->InvokeMethod("onProgress", - std::make_unique(args)); + navigation_delegate_channel_->InvokeMethod( + "onProgress", std::make_unique(args)); }); webview_instance_->RegisterOnReceivedErrorHandler( [this](LWE::WebContainer* container, LWE::ResourceError error) { @@ -261,13 +174,12 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, flutter::EncodableValue(error.GetErrorCode())}, {flutter::EncodableValue("description"), flutter::EncodableValue(error.GetDescription())}, - {flutter::EncodableValue("errorType"), - flutter::EncodableValue(ErrorCodeToString(error.GetErrorCode()))}, {flutter::EncodableValue("failingUrl"), flutter::EncodableValue(error.GetUrl())}, }; - channel_->InvokeMethod("onWebResourceError", - std::make_unique(args)); + navigation_delegate_channel_->InvokeMethod( + "onWebResourceError", + std::make_unique(args)); }); webview_instance_->RegisterShouldOverrideUrlLoadingHandler( [this](LWE::WebContainer* view, const std::string& url) -> bool { @@ -280,48 +192,11 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, flutter::EncodableValue(true)}, }; auto result = std::make_unique(url, this); - channel_->InvokeMethod("navigationRequest", - std::make_unique(args), - std::move(result)); + navigation_delegate_channel_->InvokeMethod( + "navigationRequest", + std::make_unique(args), std::move(result)); return true; }); - - webview_instance_->LoadURL(url); -} - -void WebView::ApplySettings(const flutter::EncodableMap& settings) { - for (const auto& [key, value] : settings) { - if (std::holds_alternative(key)) { - std::string string_key = std::get(key); - if (string_key == "jsMode") { - // NOTE: Not supported by LWE on Tizen. - } else if (string_key == "hasNavigationDelegate") { - if (std::holds_alternative(value)) { - has_navigation_delegate_ = std::get(value); - } - } else if (string_key == "hasProgressTracking") { - if (std::holds_alternative(value)) { - has_progress_tracking_ = std::get(value); - } - } else if (string_key == "debuggingEnabled") { - // NOTE: Not supported by LWE on Tizen. - } else if (string_key == "gestureNavigationEnabled") { - // NOTE: Not supported by LWE on Tizen. - } else if (string_key == "allowsInlineMediaPlayback") { - // no-op inline media playback is always allowed on Tizen. - } else if (string_key == "userAgent") { - if (std::holds_alternative(value)) { - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetUserAgentString(std::get(value)); - webview_instance_->SetSettings(settings); - } - } else if (string_key == "zoomEnabled") { - // NOTE: Not supported by LWE on Tizen. - } else { - LOG_WARN("Unknown settings key: %s", string_key.c_str()); - } - } - } } /** @@ -332,16 +207,14 @@ void WebView::ApplySettings(const flutter::EncodableMap& settings) { * message over a method channel to the Dart code. */ void WebView::RegisterJavaScriptChannelName(const std::string& name) { - LOG_DEBUG("Register a JavaScript channel: %s", name.c_str()); - auto on_message = [this, name](const std::string& message) -> std::string { - LOG_DEBUG("JavaScript channel message: %s", message.c_str()); flutter::EncodableMap args = { {flutter::EncodableValue("channel"), flutter::EncodableValue(name)}, {flutter::EncodableValue("message"), flutter::EncodableValue(message)}, }; - channel_->InvokeMethod("javascriptChannelMessage", - std::make_unique(args)); + webview_channel_->InvokeMethod( + "javaScriptChannelMessage", + std::make_unique(args)); return "success"; }; webview_instance_->AddJavaScriptInterface(name, "postMessage", on_message); @@ -349,8 +222,13 @@ void WebView::RegisterJavaScriptChannelName(const std::string& name) { WebView::~WebView() { Dispose(); } -std::string WebView::GetChannelName() { - return "plugins.flutter.io/webview_" + std::to_string(GetViewId()); +std::string WebView::GetWebViewChannelName() { + return std::string(kLweWebViewChannelName) + std::to_string(GetViewId()); +} + +std::string WebView::GetNavigationDelegateChannelName() { + return std::string(kLweNavigationDelegateChannelName) + + std::to_string(GetViewId()); } void WebView::Dispose() { @@ -684,9 +562,8 @@ void WebView::InitWebView() { #endif } -void WebView::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { +void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { if (!webview_instance_) { result->Error("Invalid operation", "The webview instance has not been initialized."); @@ -696,9 +573,16 @@ void WebView::HandleMethodCall( const std::string& method_name = method_call.method_name(); const flutter::EncodableValue* arguments = method_call.arguments(); - LOG_DEBUG("Handle a method call: %s", method_name.c_str()); - - if (method_name == "loadUrl") { + if (method_name == "javaScriptMode") { + // NOTE: Not supported by LWE on Tizen. + result->Success(); + } else if (method_name == "hasNavigationDelegate") { + const auto* has_navigation_delegate = std::get_if(arguments); + if (has_navigation_delegate) { + has_navigation_delegate_ = *has_navigation_delegate; + } + result->Success(); + } else if (method_name == "loadRequest") { std::string url; if (GetValueFromEncodableMap(arguments, "url", &url)) { webview_instance_->LoadURL(url); @@ -706,12 +590,6 @@ void WebView::HandleMethodCall( } else { result->Error("Invalid argument", "No url provided."); } - } else if (method_name == "updateSettings") { - const auto* settings = std::get_if(arguments); - if (settings) { - ApplySettings(*settings); - } - result->Success(); } else if (method_name == "canGoBack") { result->Success(flutter::EncodableValue(webview_instance_->CanGoBack())); } else if (method_name == "canGoForward") { @@ -727,15 +605,14 @@ void WebView::HandleMethodCall( result->Success(); } else if (method_name == "currentUrl") { result->Success(flutter::EncodableValue(webview_instance_->GetURL())); - } else if (method_name == "evaluateJavascript" || - method_name == "runJavascriptReturningResult" || - method_name == "runJavascript") { + } else if (method_name == "evaluateJavaScript" || + method_name == "runJavaScriptReturningResult" || + method_name == "runJavaScript") { const auto* javascript = std::get_if(arguments); if (javascript) { - bool should_return = method_name != "runJavascript"; + bool should_return = method_name != "runJavaScript"; auto on_result = [result = result.release(), should_return](std::string value) { - LOG_DEBUG("JavaScript evaluation result: %s", value.c_str()); if (result) { if (should_return) { result->Success(flutter::EncodableValue(value)); @@ -749,25 +626,10 @@ void WebView::HandleMethodCall( } else { result->Error("Invalid argument", "The argument must be a string."); } - } else if (method_name == "addJavascriptChannels") { - const auto* channels = std::get_if(arguments); - if (channels) { - for (flutter::EncodableValue channel : *channels) { - if (std::holds_alternative(channel)) { - RegisterJavaScriptChannelName(std::get(channel)); - } - } - } - result->Success(); - } else if (method_name == "removeJavascriptChannels") { - const auto* channels = std::get_if(arguments); - if (channels) { - for (flutter::EncodableValue channel : *channels) { - if (std::holds_alternative(channel)) { - webview_instance_->RemoveJavascriptInterface( - std::get(channel), "postMessage"); - } - } + } else if (method_name == "addJavaScriptChannel") { + const auto* channel = std::get_if(arguments); + if (channel) { + RegisterJavaScriptChannelName(*channel); } result->Success(); } else if (method_name == "clearCache") { @@ -793,10 +655,15 @@ void WebView::HandleMethodCall( } else { result->Error("Invalid argument", "No x or y provided."); } - } else if (method_name == "getScrollX") { - result->Success(flutter::EncodableValue(webview_instance_->GetScrollX())); - } else if (method_name == "getScrollY") { - result->Success(flutter::EncodableValue(webview_instance_->GetScrollY())); + } else if (method_name == "getScrollPosition") { + int x = webview_instance_->GetScrollX(); + int y = webview_instance_->GetScrollY(); + flutter::EncodableMap args = { + {flutter::EncodableValue("x"), + flutter::EncodableValue(static_cast(x))}, + {flutter::EncodableValue("y"), + flutter::EncodableValue(static_cast(y))}}; + result->Success(flutter::EncodableValue(args)); } else if (method_name == "loadFlutterAsset") { const auto* key = std::get_if(arguments); if (key) { @@ -834,8 +701,23 @@ void WebView::HandleMethodCall( } else { result->Error("Invalid argument", "The argument must be a string."); } - } else if (method_name == "loadRequest") { - result->NotImplemented(); + } else if (method_name == "backgroundColor") { + const auto* color = std::get_if(arguments); + if (color) { + LWE::Settings settings = webview_instance_->GetSettings(); + settings.SetBaseBackgroundColor(*color >> 16 & 0xff, *color >> 8 & 0xff, + *color & 0xff, *color >> 24 & 0xff); + webview_instance_->SetSettings(settings); + result->Success(); + } + } else if (method_name == "userAgent") { + const auto* user_agent = std::get_if(arguments); + if (user_agent) { + LWE::Settings settings = webview_instance_->GetSettings(); + settings.SetUserAgentString(*user_agent); + webview_instance_->SetSettings(settings); + } + result->Success(); } else if (method_name == "setCookie") { result->NotImplemented(); } else { @@ -843,9 +725,8 @@ void WebView::HandleMethodCall( } } -void WebView::HandleCookieMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { +void WebView::HandleCookieMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { if (!webview_instance_) { result->Error("Invalid operation", "The webview instance has not been initialized."); diff --git a/packages/webview_flutter_lwe/tizen/src/webview.h b/packages/webview_flutter_lwe/tizen/src/webview.h index f8f8b21e9..19cbe703e 100644 --- a/packages/webview_flutter_lwe/tizen/src/webview.h +++ b/packages/webview_flutter_lwe/tizen/src/webview.h @@ -17,6 +17,10 @@ #include #include +typedef flutter::MethodCall FlMethodCall; +typedef flutter::MethodResult FlMethodResult; +typedef flutter::MethodChannel FlMethodChannel; + namespace LWE { class WebContainer; } @@ -50,16 +54,14 @@ class WebView : public PlatformView { size_t height); private: - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - void HandleCookieMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - void ApplySettings(const flutter::EncodableMap& settings); + void HandleWebViewMethodCall(const FlMethodCall& method_call, + std::unique_ptr result); + void HandleCookieMethodCall(const FlMethodCall& method_call, + std::unique_ptr result); + void RegisterJavaScriptChannelName(const std::string& name); - std::string GetChannelName(); + std::string GetWebViewChannelName(); + std::string GetNavigationDelegateChannelName(); void InitWebView(); @@ -72,8 +74,8 @@ class WebView : public PlatformView { BufferUnit* rendered_surface_ = nullptr; bool is_mouse_lbutton_down_ = false; bool has_navigation_delegate_ = false; - bool has_progress_tracking_ = false; - std::unique_ptr> channel_; + std::unique_ptr webview_channel_; + std::unique_ptr navigation_delegate_channel_; std::unique_ptr texture_variant_; std::mutex mutex_; std::unique_ptr tbm_pool_;