diff --git a/.github/labeler.yml b/.github/labeler.yml index aeae90a65..99e38ff27 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -63,6 +63,8 @@ - packages/wakelock/**/* "p: wearable_rotary": - packages/wearable_rotary/**/* +"p: webview_flutter": + - packages/webview_flutter/**/* "p: webview_flutter_lwe": - packages/webview_flutter_lwe/**/* diff --git a/.github/recipe.yaml b/.github/recipe.yaml index 0017d67c4..f4916b514 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -24,6 +24,7 @@ plugins: camera: [] google_maps_flutter: [] network_info_plus: [] + webview_flutter: [] webview_flutter_lwe: [] # No tests. diff --git a/README.md b/README.md index fa4747de8..b6296245f 100644 --- a/README.md +++ b/README.md @@ -84,5 +84,6 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**video_player_tizen**](packages/video_player) | 4.0 | ✔️ | ✔️ | ⚠️ | ❌ | Functional limitations,
TV emulator issue | | [**wakelock_tizen**](packages/wakelock) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Cannot override system settings | | [**wearable_rotary**](packages/wearable_rotary) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Not applicable for TV | +| [**webview_flutter_tizen**](packages/webview_flutter) | 5.5 | ❌ | ❌ | ✔️ | ❌ | | [**webview_flutter_lwe**](packages/webview_flutter_lwe) | 5.5 | ✔️ | ✔️ | ✔️ | ✔️ | Not for production use | diff --git a/packages/webview_flutter/.gitignore b/packages/webview_flutter/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/packages/webview_flutter/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md new file mode 100644 index 000000000..c8e1d2318 --- /dev/null +++ b/packages/webview_flutter/CHANGELOG.md @@ -0,0 +1,134 @@ +## 0.6.0 + +* Change the backing web engine from LWE to EFL WebKit (EWK). + +## 0.5.6 + +* Update LWE binary (9af6ea4101d173935fe6e6cd3f2c91ca17ed451e). + +## 0.5.5 + +* Update README. +* Add `-Wl,-rpath=$ORIGIN` linker option. + +## 0.5.4 + +* Change the project type to sharedLib. + +## 0.5.3 + +* Apply new texture APIs. + +## 0.5.2 + +* Add back key handling. + +## 0.5.1 + +* Apply PlatformView API change. +* Code refactoring. + +## 0.5.0 + +* Code refactoring. +* Update the example app and integration_test. +* Sync with the latest framework code. +* Migrate to new analysis options. +* Update LWE binary (f0ca15ee41d2fc96b59fd57b63b6c32cf6c1906b). + +## 0.4.4 + +* Update LWE binary (645719ed084d899ec7d53de1758db71a8974e446). + +## 0.4.3 + +* Remove unused things. +* Fix build warnings. + +## 0.4.2 + +* Support background color. + +## 0.4.1 + +* Apply texture api change. + +## 0.4.0 + +* Support emulator. +* Update LWE binary (b22fd0c4e50cde2b9203150d80e9d0bd1a1b0602). +* Update webivew_flutter to 3.0.1. + +## 0.3.11 + +* Organize includes. + +## 0.3.10 + +* Apply `PlatformView` and `PlatformViewFactory` API changes. + +## 0.3.9 + +* Update LWE binary (6bae13cb915bd41c5aac4aaaae72865f20924c03). + +## 0.3.8 + +* Update webivew_flutter to 2.3.0. + +## 0.3.7 + +* Update webivew_flutter to 2.1.1. + +## 0.3.6 + +* Update LWE binary (3dff8724bfb4b2b0b9e7c4e3976a9b02e74ee13c). +* Fix various issues. + +## 0.3.5 + +* Update LWE binary (b2fad69f50d693c86abc45b363a39b0625f5e95f). +* Fix crash issue. + +## 0.3.4 + +* Fix buffer synchronization issue. + +## 0.3.3 + +* Update LWE binary (c57d045a513455115a8a4c66517e5e51f5a4dfbd). +* Fix issue of multiple webviews. + +## 0.3.2 + +* Update LWE binary (2226c28429391407d7c875c3af7531f5e1d5dfa7) for supporting google_map_flutter_tizen. + +## 0.3.1 + +* Update lightweight web engine binary (ad0e77631f96180e19a11c3dc80b6b72c32bdffb). +* Fix bug on handling parameter of `loadUrl` API. + +## 0.3.0 + +* Apply `PlatformView` and `PlatformViewFactory` API changes. + +## 0.2.2 + +* Update lightweight web engine binary & header file (6263be6c888d5cb9dcca5466dfc3941a70b424eb). +* Activate resizing function. +* Apply embedder's texture API changes. + +## 0.2.1 + +* Add lightweight web engine binary for arm64. + +## 0.2.0 + +* Update Dart and Flutter SDK constraints. +* Update Flutter and Samsung copyright information. +* Update webview_flutter_tizen to use platform view interface. +* Update example and integration_test. +* Update webivew_flutter to 2.0.4. + +## 0.1.0 + +* Initial release. diff --git a/packages/webview_flutter/LICENSE b/packages/webview_flutter/LICENSE new file mode 100644 index 000000000..934dd180c --- /dev/null +++ b/packages/webview_flutter/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2021 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md new file mode 100644 index 000000000..5e9a7c2c8 --- /dev/null +++ b/packages/webview_flutter/README.md @@ -0,0 +1,50 @@ +# webview_flutter_tizen + +[![pub package](https://img.shields.io/pub/v/webview_flutter_tizen.svg)](https://pub.dev/packages/webview_flutter_tizen) + +The Tizen implementation of [`webview_flutter`](https://github.com/flutter/plugins/tree/main/packages/webview_flutter) only for Tizen TV devices. +The WebView widget is backed by the EFL WebKit (EWK) on Tizen. + +## Required privileges + +To use this plugin, add below lines under the `` section in your `tizen-manifest.xml` file. + +```xml + + http://tizen.org/privilege/internet + +``` + +## Usage + +This package is not an _endorsed_ implementation of `webview_flutter`. Therefore, you have to include `webview_flutter_tizen` alongside `webview_flutter` as dependencies in your `pubspec.yaml` file. + +```yaml +dependencies: + webview_flutter: ^3.0.4 + webview_flutter_tizen: ^0.6.0 +``` + +## Example + +```dart +import 'package:webview_flutter/webview_flutter.dart'; + +class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key}) : super(key: key); + + @override + WebViewExampleState createState() => WebViewExampleState(); +} + +class WebViewExampleState extends State { + @override + Widget build(BuildContext context) { + return WebView(initialUrl: 'https://flutter.dev'); + } +} +``` + +## Supported devices + +This plugin is supported on Tizen TV devices running Tizen 5.5 or later. diff --git a/packages/webview_flutter/example/.gitignore b/packages/webview_flutter/example/.gitignore new file mode 100644 index 000000000..9d532b18a --- /dev/null +++ b/packages/webview_flutter/example/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/webview_flutter/example/README.md b/packages/webview_flutter/example/README.md new file mode 100644 index 000000000..2f0f82a64 --- /dev/null +++ b/packages/webview_flutter/example/README.md @@ -0,0 +1,7 @@ +# webview_flutter_tizen_example + +Demonstrates how to use the webview_flutter_tizen plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). \ No newline at end of file diff --git a/packages/webview_flutter/example/assets/sample_audio.ogg b/packages/webview_flutter/example/assets/sample_audio.ogg new file mode 100644 index 000000000..27e171042 Binary files /dev/null and b/packages/webview_flutter/example/assets/sample_audio.ogg differ diff --git a/packages/webview_flutter/example/assets/sample_video.mp4 b/packages/webview_flutter/example/assets/sample_video.mp4 new file mode 100644 index 000000000..a203d0cdf Binary files /dev/null and b/packages/webview_flutter/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter/example/assets/www/index.html b/packages/webview_flutter/example/assets/www/index.html new file mode 100644 index 000000000..9895dd3ce --- /dev/null +++ b/packages/webview_flutter/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/example/assets/www/styles/style.css b/packages/webview_flutter/example/assets/www/styles/style.css new file mode 100644 index 000000000..c2140b8b0 --- /dev/null +++ b/packages/webview_flutter/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000..73827ce66 --- /dev/null +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,640 @@ +// 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. + +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const bool _skipDueToIssue86757 = true; + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('initialUrl', (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); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('loadUrl', (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); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); + + 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, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + 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) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + await controller.runJavascript('Echo.postMessage("hello");'); + expect(messagesReceived, equals(['hello'])); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + + testWidgets('resize webview', (WidgetTester tester) async { + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } + }, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(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 String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + const String getTitleTest = ''' + + Some title + + + + + '''; + 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; + 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;'); + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + 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 = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // 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)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, 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); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); + + group('NavigationDelegate', () { + const String blankPage = ''; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${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), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) 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 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', + (WidgetTester tester) async { + final Completer errorCompleter = + 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(), + ), + ), + ); + + 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')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + 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 = "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 + .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 { + 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 pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + 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 controller.runJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion(primaryUrl)); + }, + skip: _skipDueToIssue86757, + ); +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); +} + +Future _runJavascriptReturningResult( + WebViewController controller, String js) async { + return await controller.runJavascriptReturningResult(js); +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @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, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } +} diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart new file mode 100644 index 000000000..a6298219c --- /dev/null +++ b/packages/webview_flutter/example/lib/main.dart @@ -0,0 +1,501 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +void main() => runApp(const MaterialApp(home: WebViewExample())); + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

+The navigation delegate is set to block navigation to the youtube website. +

+ + + +'''; + +const String kLocalExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + +'''; + +const String kTransparentBackgroundPage = ''' + + + + Transparent background test + + + +
+

Transparent background test

+
+
+ + +'''; + +class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key, this.cookieManager}) : super(key: key); + + final CookieManager? cookieManager; + + @override + State createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.green, + appBar: AppBar( + 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), + ], + ), + 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), + ), + 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(); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + controller.hasData + ? 'Favorited $url' + : 'Unable to favorite', + ), + ), + ); + }, + child: const Icon(Icons.favorite), + ); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, + doPostRequest, + loadLocalFile, + loadFlutterAsset, + loadHtmlString, + transparentBackground, + setCookie, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller, CookieManager? cookieManager, {Key? key}) + : cookieManager = cookieManager ?? CookieManager(), + super(key: key); + + final Future controller; + late final CookieManager cookieManager; + + @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'), + ), + ], + ); + }, + ); + } + + Future _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // 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);'); + } + + Future _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.runJavascriptReturningResult('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + Future _onAddToCache( + WebViewController controller, BuildContext context) async { + await controller.runJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + Future _onListCache( + WebViewController controller, BuildContext context) async { + await controller.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(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } + + Future _onClearCookies(BuildContext context) async { + final bool hadCookies = await cookieManager.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + Future _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await cookieManager.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('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, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + + Future _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + + Future _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalExamplePage); + } + + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File( + {tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); + + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); + + return indexFile.path; + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture, {Key? key}) + : assert(_webViewControllerFuture != null), + super(key: key); + + final Future _webViewControllerFuture; + + @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 { + 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 { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No forward history item')), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller!.reload(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml new file mode 100644 index 000000000..31a1e34be --- /dev/null +++ b/packages/webview_flutter/example/pubspec.yaml @@ -0,0 +1,37 @@ +name: webview_flutter_tizen_example +description: Demonstrates how to use the webview_flutter_tizen plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + path_provider: ^2.0.7 + path_provider_tizen: + path: ../../path_provider/ + webview_flutter: ^3.0.4 + webview_flutter_tizen: + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css diff --git a/packages/webview_flutter/example/test_driver/integration_test.dart b/packages/webview_flutter/example/test_driver/integration_test.dart new file mode 100644 index 000000000..4f10f2a52 --- /dev/null +++ b/packages/webview_flutter/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// 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:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter/example/tizen/.gitignore b/packages/webview_flutter/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/webview_flutter/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/webview_flutter/example/tizen/App.cs b/packages/webview_flutter/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/webview_flutter/example/tizen/App.cs @@ -0,0 +1,20 @@ +using Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + } + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/webview_flutter/example/tizen/Runner.csproj b/packages/webview_flutter/example/tizen/Runner.csproj new file mode 100644 index 000000000..f4e369d0c --- /dev/null +++ b/packages/webview_flutter/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/webview_flutter/example/tizen/shared/res/ic_launcher.png b/packages/webview_flutter/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/webview_flutter/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/webview_flutter/example/tizen/tizen-manifest.xml b/packages/webview_flutter/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..3f9e1a889 --- /dev/null +++ b/packages/webview_flutter/example/tizen/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + + http://tizen.org/privilege/internet + + + diff --git a/packages/webview_flutter/lib/webview_flutter_tizen.dart b/packages/webview_flutter/lib/webview_flutter_tizen.dart new file mode 100644 index 000000000..70ae2ebf5 --- /dev/null +++ b/packages/webview_flutter/lib/webview_flutter_tizen.dart @@ -0,0 +1,95 @@ +// Copyright 2021 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/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 TizenWebView implements WebViewPlatform { + /// Sets a tizen [WebViewPlatform]. + static void register() { + WebView.platform = TizenWebView(); + 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; + } +} diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml new file mode 100644 index 000000000..777933d6d --- /dev/null +++ b/packages/webview_flutter/pubspec.yaml @@ -0,0 +1,28 @@ +name: webview_flutter_tizen +description: Tizen implementation of the webview plugin +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/webview_flutter +version: 0.6.0 + +environment: + sdk: ">=2.17.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: WebviewFlutterTizenPlugin + fileName: webview_flutter_tizen_plugin.h + dartPluginClass: TizenWebView + +dependencies: + flutter: + sdk: flutter + flutter_tizen: ^0.2.0 + webview_flutter: ^3.0.4 + webview_flutter_platform_interface: ^1.8.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/webview_flutter/tizen/.gitignore b/packages/webview_flutter/tizen/.gitignore new file mode 100644 index 000000000..a2a7d62b1 --- /dev/null +++ b/packages/webview_flutter/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/webview_flutter/tizen/inc/webview_flutter_tizen_plugin.h b/packages/webview_flutter/tizen/inc/webview_flutter_tizen_plugin.h new file mode 100644 index 000000000..ecaada214 --- /dev/null +++ b/packages/webview_flutter/tizen/inc/webview_flutter_tizen_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void WebviewFlutterTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_PLUGIN_H_ diff --git a/packages/webview_flutter/tizen/project_def.prop b/packages/webview_flutter/tizen/project_def.prop new file mode 100644 index 000000000..ecd814012 --- /dev/null +++ b/packages/webview_flutter/tizen/project_def.prop @@ -0,0 +1,29 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = webview_flutter_tizen_plugin +type = sharedLib +profile = common-5.5 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Compiler flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = + +# Linker options +USER_LIBS = +USER_LIB_DIRS = +USER_LFLAGS = diff --git a/packages/webview_flutter/tizen/src/buffer_pool.cc b/packages/webview_flutter/tizen/src/buffer_pool.cc new file mode 100644 index 000000000..ed8fde021 --- /dev/null +++ b/packages/webview_flutter/tizen/src/buffer_pool.cc @@ -0,0 +1,151 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffer_pool.h" + +#include "log.h" + +BufferUnit::BufferUnit(int32_t width, int32_t height) { Reset(width, height); } + +BufferUnit::~BufferUnit() { + if (tbm_surface_ && !use_external_buffer_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + if (gpu_surface_) { + delete gpu_surface_; + gpu_surface_ = nullptr; + } +} + +void BufferUnit::UseExternalBuffer() { + if (!use_external_buffer_) { + use_external_buffer_ = true; + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + } +} + +void BufferUnit::SetExternalBuffer(tbm_surface_h tbm_surface) { + if (use_external_buffer_) { + tbm_surface_ = tbm_surface; + gpu_surface_->handle = tbm_surface_; + } +} + +bool BufferUnit::MarkInUse() { + if (!is_used_) { + is_used_ = true; + return true; + } + return false; +} + +void BufferUnit::UnmarkInUse() { is_used_ = false; } + +tbm_surface_h BufferUnit::Surface() { + if (IsUsed()) { + return tbm_surface_; + } + return nullptr; +} + +void BufferUnit::Reset(int32_t width, int32_t height) { + if (width_ == width && height_ == height) { + return; + } + width_ = width; + height_ = height; + + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + if (gpu_surface_) { + delete gpu_surface_; + gpu_surface_ = nullptr; + } + + tbm_surface_ = tbm_surface_create(width_, height_, TBM_FORMAT_ARGB8888); + gpu_surface_ = new FlutterDesktopGpuSurfaceDescriptor(); + gpu_surface_->width = width_; + gpu_surface_->height = height_; + gpu_surface_->handle = tbm_surface_; + gpu_surface_->release_callback = [](void* release_context) { + BufferUnit* buffer = reinterpret_cast(release_context); + buffer->UnmarkInUse(); + }; + gpu_surface_->release_context = this; +} + +BufferPool::BufferPool(int32_t width, int32_t height, size_t pool_size) { + for (size_t index = 0; index < pool_size; index++) { + pool_.emplace_back(std::make_unique(width, height)); + } + Prepare(width, height); +} + +BufferPool::~BufferPool() {} + +BufferUnit* BufferPool::GetAvailableBuffer() { + std::lock_guard lock(mutex_); + for (size_t index = 0; index < pool_.size(); index++) { + size_t current = (index + last_index_) % pool_.size(); + BufferUnit* buffer = pool_[current].get(); + if (buffer->MarkInUse()) { + last_index_ = current; + return buffer; + } + } + return nullptr; +} + +void BufferPool::Release(BufferUnit* buffer) { + std::lock_guard lock(mutex_); + buffer->UnmarkInUse(); +} + +void BufferPool::Prepare(int32_t width, int32_t height) { + std::lock_guard lock(mutex_); + for (size_t index = 0; index < pool_.size(); index++) { + BufferUnit* buffer = pool_[index].get(); + buffer->Reset(width, height); + } +} + +SingleBufferPool::SingleBufferPool(int32_t width, int32_t height) + : BufferPool(width, height, 1) {} + +SingleBufferPool::~SingleBufferPool() {} + +BufferUnit* SingleBufferPool::GetAvailableBuffer() { + BufferUnit* buffer = pool_[0].get(); + buffer->MarkInUse(); + return buffer; +} + +void SingleBufferPool::Release(BufferUnit* buffer) {} + +#ifndef NDEBUG +#include +void BufferUnit::DumpToPng(int file_name) { + char file_path[256]; + sprintf(file_path, "/tmp/dump%d.png", file_name); + + tbm_surface_info_s surface_info; + tbm_surface_map(tbm_surface_, TBM_SURF_OPTION_WRITE, &surface_info); + + unsigned char* buffer = surface_info.planes[0].ptr; + cairo_surface_t* png_buffer = cairo_image_surface_create_for_data( + buffer, CAIRO_FORMAT_ARGB32, width_, height_, + surface_info.planes[0].stride); + + cairo_surface_write_to_png(png_buffer, file_path); + + tbm_surface_unmap(tbm_surface_); + cairo_surface_destroy(png_buffer); +} +#endif diff --git a/packages/webview_flutter/tizen/src/buffer_pool.h b/packages/webview_flutter/tizen/src/buffer_pool.h new file mode 100644 index 000000000..0128c63ce --- /dev/null +++ b/packages/webview_flutter/tizen/src/buffer_pool.h @@ -0,0 +1,75 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BUFFER_POOL_H_ +#define FLUTTER_PLUGIN_BUFFER_POOL_H_ + +#include +#include + +#include +#include +#include + +class BufferUnit { + public: + explicit BufferUnit(int32_t width, int32_t height); + ~BufferUnit(); + + void Reset(int32_t width, int32_t height); + + bool MarkInUse(); + void UnmarkInUse(); + + bool IsUsed() { return is_used_ && tbm_surface_; } + + void UseExternalBuffer(); + void SetExternalBuffer(tbm_surface_h tbm_surface); + + tbm_surface_h Surface(); + + FlutterDesktopGpuSurfaceDescriptor* GpuSurface() { return gpu_surface_; } + +#ifndef NDEBUG + // TODO: Unused code. + void DumpToPng(int file_name); +#endif + + private: + bool is_used_ = false; + bool use_external_buffer_ = false; + int32_t width_ = 0; + int32_t height_ = 0; + tbm_surface_h tbm_surface_ = nullptr; + FlutterDesktopGpuSurfaceDescriptor* gpu_surface_ = nullptr; +}; + +class BufferPool { + public: + explicit BufferPool(int32_t width, int32_t height, size_t pool_size); + virtual ~BufferPool(); + + virtual BufferUnit* GetAvailableBuffer(); + virtual void Release(BufferUnit* buffer); + + void Prepare(int32_t with, int32_t height); + + protected: + std::vector> pool_; + + private: + size_t last_index_ = 0; + std::mutex mutex_; +}; + +class SingleBufferPool : public BufferPool { + public: + explicit SingleBufferPool(int32_t width, int32_t height); + ~SingleBufferPool(); + + virtual BufferUnit* GetAvailableBuffer() override; + virtual void Release(BufferUnit* buffer) override; +}; + +#endif // FLUTTER_PLUGIN_BUFFER_POOL_H_ diff --git a/packages/webview_flutter/tizen/src/ewk_internal_api_binding.cc b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.cc new file mode 100644 index 000000000..82d7757a6 --- /dev/null +++ b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.cc @@ -0,0 +1,64 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ewk_internal_api_binding.h" + +#include + +EwkInternalApiBinding::EwkInternalApiBinding() { + handle_ = dlopen("libchromium-ewk.so", RTLD_LAZY); +} + +EwkInternalApiBinding::~EwkInternalApiBinding() { + if (handle_) { + dlclose(handle_); + } +} + +bool EwkInternalApiBinding::Initialize() { + if (handle_ == nullptr) { + return false; + } + + // ewk_view + view.SetBackgroundColor = reinterpret_cast( + dlsym(handle_, "ewk_view_bg_color_set")); + view.FeedTouchEvent = reinterpret_cast( + dlsym(handle_, "ewk_view_feed_touch_event")); + view.SendKeyEvent = reinterpret_cast( + dlsym(handle_, "ewk_view_send_key_event")); + view.OffscreenRenderingEnabledSet = + reinterpret_cast( + dlsym(handle_, "ewk_view_offscreen_rendering_enabled_set")); + view.ImeWindowSet = reinterpret_cast( + dlsym(handle_, "ewk_view_ime_window_set")); + view.KeyEventsEnabledSet = reinterpret_cast( + dlsym(handle_, "ewk_view_key_events_enabled_set")); + + // ewk_main + main.SetArguments = reinterpret_cast( + dlsym(handle_, "ewk_set_arguments")); + + // ewk_settings + settings.ImePanelEnabledSet = + reinterpret_cast( + dlsym(handle_, "ewk_settings_ime_panel_enabled_set")); + + // ewk_console_message + console_message.LevelGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_level_get")); + console_message.TextGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_text_get")); + console_message.LineGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_line_get")); + console_message.SourceGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_source_get")); + + return view.SetBackgroundColor && view.FeedTouchEvent && view.SendKeyEvent && + view.OffscreenRenderingEnabledSet && view.ImeWindowSet && + view.KeyEventsEnabledSet && main.SetArguments && + settings.ImePanelEnabledSet && console_message.LevelGet && + console_message.TextGet && console_message.LineGet && + console_message.SourceGet; +} diff --git a/packages/webview_flutter/tizen/src/ewk_internal_api_binding.h b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.h new file mode 100644 index 000000000..f4a8971f4 --- /dev/null +++ b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.h @@ -0,0 +1,117 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_EWK_INTERNAL_API_BINDING_H_ +#define FLUTTER_PLUGIN_EWK_INTERNAL_API_BINDING_H_ + +#include + +typedef enum { + EWK_TOUCH_START, + EWK_TOUCH_MOVE, + EWK_TOUCH_END, + EWK_TOUCH_CANCEL +} Ewk_Touch_Event_Type; + +typedef struct _Ewk_Touch_Point Ewk_Touch_Point; + +struct _Ewk_Touch_Point { + int id; + int x; + int y; + Evas_Touch_Point_State state; +}; + +typedef Eina_Bool (*EwkViewBgColorSetFnPtr)(Evas_Object* obj, int r, int g, + int b, int a); +typedef Eina_Bool (*EwkViewFeedTouchEventFnPtr)(Evas_Object* obj, + Ewk_Touch_Event_Type type, + const Eina_List* points, + const Evas_Modifier* modifiers); +typedef Eina_Bool (*EwkViewSendKeyEventFnPtr)(Evas_Object* obj, void* key_event, + Eina_Bool is_press); +typedef void (*EwkViewOffscreenRenderingEnabledSetFnPtr)(Evas_Object* obj, + Eina_Bool enabled); +typedef void (*EwkViewImeWindowSetFnPtr)(Evas_Object* obj, void* window); +typedef Eina_Bool (*EwkViewKeyEventsEnabledSetFnPtr)(Evas_Object* obj, + Eina_Bool enabled); + +typedef struct { + EwkViewBgColorSetFnPtr SetBackgroundColor = nullptr; + EwkViewFeedTouchEventFnPtr FeedTouchEvent = nullptr; + EwkViewSendKeyEventFnPtr SendKeyEvent = nullptr; + EwkViewOffscreenRenderingEnabledSetFnPtr OffscreenRenderingEnabledSet = + nullptr; + EwkViewImeWindowSetFnPtr ImeWindowSet = nullptr; + EwkViewKeyEventsEnabledSetFnPtr KeyEventsEnabledSet = nullptr; +} EwkViewProcTable; + +typedef void (*EwkSetArgumentsFnPtr)(int argc, char** argv); + +typedef struct { + EwkSetArgumentsFnPtr SetArguments = nullptr; +} EwkMainProcTable; + +typedef struct Ewk_Settings Ewk_Settings; +typedef void (*EwkSettingsImePanelEnabledSetFnPtr)(Ewk_Settings* settings, + Eina_Bool enabled); + +typedef struct { + EwkSettingsImePanelEnabledSetFnPtr ImePanelEnabledSet = nullptr; +} EWKSettingsProcTable; + +typedef struct _Ewk_Console_Message Ewk_Console_Message; + +typedef enum { + EWK_CONSOLE_MESSAGE_LEVEL_NULL, + EWK_CONSOLE_MESSAGE_LEVEL_LOG, + EWK_CONSOLE_MESSAGE_LEVEL_WARNING, + EWK_CONSOLE_MESSAGE_LEVEL_ERROR, + EWK_CONSOLE_MESSAGE_LEVEL_DEBUG, + EWK_CONSOLE_MESSAGE_LEVEL_INFO, +} Ewk_Console_Message_Level; + +typedef Ewk_Console_Message_Level (*EwkConsoleMessageLevelGetFnPtr)( + const Ewk_Console_Message* message); +typedef Eina_Stringshare* (*EwkConsoleMessageTextGetFnPtr)( + const Ewk_Console_Message* message); +typedef unsigned (*EwkConsoleMessageLineGetFnPtr)( + const Ewk_Console_Message* message); +typedef Eina_Stringshare* (*EwkConsoleMessageSourceGetFnPtr)( + const Ewk_Console_Message* message); + +typedef struct { + EwkConsoleMessageLevelGetFnPtr LevelGet = nullptr; + EwkConsoleMessageTextGetFnPtr TextGet = nullptr; + EwkConsoleMessageLineGetFnPtr LineGet = nullptr; + EwkConsoleMessageSourceGetFnPtr SourceGet = nullptr; +} EWKConsoleMessageProcTable; + +class EwkInternalApiBinding { + public: + static EwkInternalApiBinding& GetInstance() { + static EwkInternalApiBinding instance = EwkInternalApiBinding(); + return instance; + } + + ~EwkInternalApiBinding(); + + EwkInternalApiBinding(const EwkInternalApiBinding&) = delete; + + EwkInternalApiBinding& operator=(const EwkInternalApiBinding&) = delete; + + bool Initialize(); + + EwkViewProcTable view; + EwkMainProcTable main; + EWKSettingsProcTable settings; + EWKConsoleMessageProcTable console_message; + + private: + EwkInternalApiBinding(); + + void* handle_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_EWK_INTERNAL_API_BINDING_H_ diff --git a/packages/webview_flutter/tizen/src/log.h b/packages/webview_flutter/tizen/src/log.h new file mode 100644 index 000000000..286d42cba --- /dev/null +++ b/packages/webview_flutter/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "WebviewFlutterTizenPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/webview_flutter/tizen/src/webview.cc b/packages/webview_flutter/tizen/src/webview.cc new file mode 100644 index 000000000..7144bbade --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview.cc @@ -0,0 +1,684 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webview.h" + +#include +#include +#include +#include +#include + +#include "buffer_pool.h" +#include "ewk_internal_api_binding.h" +#include "log.h" +#include "webview_factory.h" + +namespace { + +typedef flutter::MethodCall FlMethodCall; +typedef flutter::MethodResult FlMethodResult; +typedef flutter::MethodChannel FlMethodChannel; + +constexpr size_t kBufferPoolSize = 5; +constexpr char kEwkInstance[] = "ewk_instance"; + +class NavigationRequestResult + : public flutter::MethodResult { + public: + NavigationRequestResult(WebView* webview) : webview_(webview) {} + + void SuccessInternal(const flutter::EncodableValue* should_load) override { + if (std::holds_alternative(*should_load)) { + if (std::get(*should_load)) { + webview_->Resume(); + return; + } + } + webview_->Stop(); + } + + void ErrorInternal(const std::string& error_code, + const std::string& error_message, + const flutter::EncodableValue* error_details) override { + webview_->Stop(); + LOG_ERROR("The request unexpectedly completed with an error."); + } + + void NotImplementedInternal() override { + webview_->Stop(); + LOG_ERROR("The target method was unexpectedly unimplemented."); + } + + private: + WebView* webview_; +}; + +std::string ErrorCodeToString(int error_code) { + switch (error_code) { + case EWK_ERROR_CODE_AUTHENTICATION: + return "authentication"; + case EWK_ERROR_CODE_BAD_URL: + return "badUrl"; + case EWK_ERROR_CODE_FAILED_TLS_HANDSHAKE: + return "failedSslHandshake"; + case EWK_ERROR_CODE_FAILED_FILE_IO: + return "file"; + case EWK_ERROR_CODE_CANT_LOOKUP_HOST: + return "hostLookup"; + case EWK_ERROR_CODE_REQUEST_TIMEOUT: + return "timeout"; + case EWK_ERROR_CODE_TOO_MANY_REQUESTS: + return "tooManyRequests"; + case EWK_ERROR_CODE_UNKNOWN: + return "unknown"; + case EWK_ERROR_CODE_UNSUPPORTED_SCHEME: + return "unsupportedScheme"; + default: + LOG_ERROR("Unknown error type: %d", error_code); + return std::to_string(error_code); + } +} + +template +bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, + std::string key, T* out) { + if (auto* map = std::get_if(arguments)) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto* value = std::get_if(&iter->second)) { + *out = *value; + return true; + } + } + } + return false; +} + +} // namespace + +WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params, + void* win) + : PlatformView(registrar, view_id, nullptr), + texture_registrar_(texture_registrar), + width_(width), + height_(height), + win_(win) { + if (!EwkInternalApiBinding::GetInstance().Initialize()) { + LOG_ERROR("Failed to Initialize EWK internal APIs."); + return; + } + + tbm_pool_ = std::make_unique(width, height); + + texture_variant_ = + std::make_unique(flutter::GpuSurfaceTexture( + kFlutterDesktopGpuSurfaceTypeNone, + [this](size_t width, + size_t height) -> const FlutterDesktopGpuSurfaceDescriptor* { + return ObtainGpuSurface(width, height); + })); + SetTextureId(texture_registrar_->RegisterTexture(texture_variant_.get())); + + InitWebView(); + + channel_ = std::make_unique( + GetPluginRegistrar()->messenger(), GetChannelName(), + &flutter::StandardMethodCodec::GetInstance()); + channel_->SetMethodCallHandler( + [webview = this](const auto& call, auto result) { + webview->HandleMethodCall(call, std::move(result)); + }); + + auto cookie_channel = std::make_unique( + GetPluginRegistrar()->messenger(), "plugins.flutter.io/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"; + } + + int color; + if (GetValueFromEncodableMap(¶ms, "backgroundColor", &color)) { + EwkInternalApiBinding::GetInstance().view.SetBackgroundColor( + webview_instance_, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, + color >> 24 & 0xff); + } + + 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)) { + ewk_view_user_agent_set(webview_instance_, user_agent.c_str()); + } + ewk_view_url_set(webview_instance_, url.c_str()); +} + +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") { + } 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") { + } else if (string_key == "gestureNavigationEnabled") { + } else if (string_key == "allowsInlineMediaPlayback") { + } else if (string_key == "userAgent") { + if (std::holds_alternative(value)) { + ewk_view_user_agent_set(webview_instance_, + std::get(value).c_str()); + } + } else if (string_key == "zoomEnabled") { + } else { + LOG_WARN("Unknown settings key: %s", string_key.c_str()); + } + } + } +} + +/** + * Added as a JavaScript interface to the WebView for any JavaScript channel + * that the Dart code sets up. + * + * Exposes a single method named `postMessage` to JavaScript, which sends a + * 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()); + ewk_view_javascript_message_handler_add( + webview_instance_, &WebView::OnJavaScriptMessage, name.c_str()); +} + +WebView::~WebView() { Dispose(); } + +std::string WebView::GetChannelName() { + return "plugins.flutter.io/webview_" + std::to_string(GetViewId()); +} + +void WebView::Dispose() { + evas_object_smart_callback_del(webview_instance_, "offscreen,frame,rendered", + &WebView::OnFrameRendered); + evas_object_smart_callback_del(webview_instance_, "load,started", + &WebView::OnLoadStarted); + evas_object_smart_callback_del(webview_instance_, "load,finished", + &WebView::OnLoadFinished); + evas_object_smart_callback_del(webview_instance_, "load,error", + &WebView::OnLoadError); + evas_object_smart_callback_del(webview_instance_, "console,message", + &WebView::OnConsoleMessage); + evas_object_smart_callback_del(webview_instance_, "policy,navigation,decide", + &WebView::OnNavigationPolicy); + texture_registrar_->UnregisterTexture(GetTextureId()); +} + +void WebView::Resize(double width, double height) { + width_ = width; + height_ = height; + + if (candidate_surface_) { + candidate_surface_ = nullptr; + } + + tbm_pool_->Prepare(width_, height_); + evas_object_resize(webview_instance_, width_, height_); +} + +void WebView::Touch(int type, int button, double x, double y, double dx, + double dy) { + Ewk_Touch_Event_Type mouse_event_type = EWK_TOUCH_START; + Evas_Touch_Point_State state = EVAS_TOUCH_POINT_DOWN; + if (type == 0) { // down event + mouse_event_type = EWK_TOUCH_START; + state = EVAS_TOUCH_POINT_DOWN; + } else if (type == 1) { // move event + mouse_event_type = EWK_TOUCH_MOVE; + state = EVAS_TOUCH_POINT_MOVE; + + } else if (type == 2) { // up event + mouse_event_type = EWK_TOUCH_END; + state = EVAS_TOUCH_POINT_UP; + } else { + // TODO: Not implemented + } + Eina_List* pointList = 0; + Ewk_Touch_Point* point = new Ewk_Touch_Point; + point->id = 0; + point->x = x; + point->y = y; + point->state = state; + pointList = eina_list_append(pointList, point); + + EwkInternalApiBinding::GetInstance().view.FeedTouchEvent( + webview_instance_, mouse_event_type, pointList, 0); + eina_list_free(pointList); +} + +bool WebView::SendKey(const char* key, const char* string, const char* compose, + uint32_t modifiers, uint32_t scan_code, bool is_down) { + if (!IsFocused()) { + return false; + } + + if (is_down) { + Evas_Event_Key_Down downEvent; + memset(&downEvent, 0, sizeof(Evas_Event_Key_Down)); + downEvent.key = key; + downEvent.string = string; + void* evasKeyEvent = static_cast(&downEvent); + EwkInternalApiBinding::GetInstance().view.SendKeyEvent( + webview_instance_, evasKeyEvent, is_down); + return true; + } else { + Evas_Event_Key_Up upEvent; + memset(&upEvent, 0, sizeof(Evas_Event_Key_Up)); + upEvent.key = key; + upEvent.string = string; + void* evasKeyEvent = static_cast(&upEvent); + EwkInternalApiBinding::GetInstance().view.SendKeyEvent( + webview_instance_, evasKeyEvent, is_down); + return true; + } + + return false; +} + +void WebView::Resume() { ewk_view_resume(webview_instance_); } + +void WebView::Stop() { ewk_view_stop(webview_instance_); } + +void WebView::SetDirection(int direction) { + // TODO: Implement if necessary. +} + +void WebView::InitWebView() { + char* chromium_argv[] = { + const_cast("--disable-pinch"), + const_cast("--js-flags=--expose-gc"), + const_cast("--single-process"), + const_cast("--no-zygote"), + }; + int chromium_argc = sizeof(chromium_argv) / sizeof(chromium_argv[0]); + EwkInternalApiBinding::GetInstance().main.SetArguments(chromium_argc, + chromium_argv); + + ewk_init(); + Ecore_Evas* evas = ecore_evas_new("wayland_egl", 0, 0, 1, 1, 0); + + webview_instance_ = ewk_view_add(ecore_evas_get(evas)); + ecore_evas_focus_set(evas, true); + ewk_view_focus_set(webview_instance_, true); + EwkInternalApiBinding::GetInstance().view.OffscreenRenderingEnabledSet( + webview_instance_, true); + + Ewk_Settings* settings = ewk_view_settings_get(webview_instance_); + + Ewk_Context* context = ewk_view_context_get(webview_instance_); + Ewk_Cookie_Manager* manager = ewk_context_cookie_manager_get(context); + if (manager) { + ewk_cookie_manager_accept_policy_set( + manager, EWK_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY); + } + ewk_settings_viewport_meta_tag_set(settings, false); + EwkInternalApiBinding::GetInstance().settings.ImePanelEnabledSet(settings, + true); + ewk_settings_javascript_enabled_set(settings, true); + + EwkInternalApiBinding::GetInstance().view.ImeWindowSet(webview_instance_, + win_); + EwkInternalApiBinding::GetInstance().view.KeyEventsEnabledSet( + webview_instance_, true); + ewk_context_cache_model_set(context, EWK_CACHE_MODEL_PRIMARY_WEBBROWSER); + + evas_object_smart_callback_add(webview_instance_, "offscreen,frame,rendered", + &WebView::OnFrameRendered, this); + evas_object_smart_callback_add(webview_instance_, "load,started", + &WebView::OnLoadStarted, this); + evas_object_smart_callback_add(webview_instance_, "load,finished", + &WebView::OnLoadFinished, this); + evas_object_smart_callback_add(webview_instance_, "load,error", + &WebView::OnLoadError, this); + evas_object_smart_callback_add(webview_instance_, "console,message", + &WebView::OnConsoleMessage, this); + evas_object_smart_callback_add(webview_instance_, "policy,navigation,decide", + &WebView::OnNavigationPolicy, this); + Resize(width_, height_); + evas_object_show(webview_instance_); + + evas_object_data_set(webview_instance_, kEwkInstance, this); +} + +void WebView::HandleMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { + if (!webview_instance_) { + result->Error("Invalid operation", + "The webview instance has not been initialized."); + return; + } + + const std::string& method_name = method_call.method_name(); + const flutter::EncodableValue* arguments = method_call.arguments(); + + if (method_name == "loadUrl") { + std::string url; + if (GetValueFromEncodableMap(arguments, "url", &url)) { + ewk_view_url_set(webview_instance_, url.c_str()); + result->Success(); + } 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(ewk_view_back_possible(webview_instance_))); + } else if (method_name == "canGoForward") { + result->Success( + flutter::EncodableValue(ewk_view_forward_possible(webview_instance_))); + } else if (method_name == "goBack") { + ewk_view_back(webview_instance_); + result->Success(); + } else if (method_name == "goForward") { + ewk_view_forward(webview_instance_); + result->Success(); + } else if (method_name == "reload") { + ewk_view_reload(webview_instance_); + result->Success(); + } else if (method_name == "currentUrl") { + result->Success( + flutter::EncodableValue(ewk_view_url_get(webview_instance_))); + } else if (method_name == "evaluateJavascript" || + method_name == "runJavascriptReturningResult" || + method_name == "runJavascript") { + const auto* javascript = std::get_if(arguments); + if (javascript) { + auto p_result = result.release(); + ewk_view_script_execute(webview_instance_, javascript->c_str(), + &WebView::OnEvaluateJavaScript, p_result); + } 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") { + result->NotImplemented(); + } else if (method_name == "clearCache") { + result->NotImplemented(); + } else if (method_name == "getTitle") { + result->Success(flutter::EncodableValue( + std::string(ewk_view_title_get(webview_instance_)))); + } else if (method_name == "scrollTo") { + int x = 0, y = 0; + if (GetValueFromEncodableMap(arguments, "x", &x) && + GetValueFromEncodableMap(arguments, "y", &y)) { + ewk_view_scroll_set(webview_instance_, x, y); + result->Success(); + } else { + result->Error("Invalid argument", "No x or y provided."); + } + } else if (method_name == "scrollBy") { + int x = 0, y = 0; + if (GetValueFromEncodableMap(arguments, "x", &x) && + GetValueFromEncodableMap(arguments, "y", &y)) { + ewk_view_scroll_by(webview_instance_, x, y); + result->Success(); + } else { + result->Error("Invalid argument", "No x or y provided."); + } + } else if (method_name == "getScrollX") { + int x = 0; + ewk_view_scroll_pos_get(webview_instance_, &x, nullptr); + result->Success(flutter::EncodableValue(x)); + } else if (method_name == "getScrollY") { + int y = 0; + ewk_view_scroll_pos_get(webview_instance_, nullptr, &y); + result->Success(flutter::EncodableValue(y)); + } else if (method_name == "loadFlutterAsset") { + const auto* key = std::get_if(arguments); + if (key) { + char* res_path = app_get_resource_path(); + if (res_path) { + std::string url = + std::string("file://") + res_path + "flutter_assets/" + *key; + free(res_path); + ewk_view_url_set(webview_instance_, url.c_str()); + result->Success(); + } else { + result->Error("Operation failed", + "Could not get the flutter_assets path."); + } + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "loadHtmlString") { + std::string html, base_url; + if (!GetValueFromEncodableMap(arguments, "html", &html)) { + result->Error("Invalid argument", "No html provided."); + return; + } + if (GetValueFromEncodableMap(arguments, "baseUrl", &base_url)) { + LOG_WARN("loadHtmlString: baseUrl is not supported and will be ignored."); + } + ewk_view_html_string_load(webview_instance_, html.c_str(), base_url.c_str(), + nullptr); + result->Success(); + } else if (method_name == "loadFile") { + const auto* file_path = std::get_if(arguments); + if (file_path) { + std::string url = std::string("file://") + *file_path; + ewk_view_url_set(webview_instance_, url.c_str()); + result->Success(); + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "loadRequest") { + result->NotImplemented(); + } else if (method_name == "setCookie") { + result->NotImplemented(); + } else { + result->NotImplemented(); + } +} + +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."); + return; + } + + const std::string& method_name = method_call.method_name(); + + if (method_name == "clearCookies") { + Ewk_Cookie_Manager* manager = + ewk_context_cookie_manager_get(ewk_view_context_get(webview_instance_)); + if (manager) { + ewk_cookie_manager_cookies_clear(manager); + result->Success(flutter::EncodableValue(true)); + } else { + result->Error("Operation failed", "Failed to get cookie manager"); + } + } else { + result->NotImplemented(); + } +} + +FlutterDesktopGpuSurfaceDescriptor* WebView::ObtainGpuSurface(size_t width, + size_t height) { + std::lock_guard lock(mutex_); + if (!candidate_surface_) { + if (rendered_surface_) { + return rendered_surface_->GpuSurface(); + } + return nullptr; + } + if (rendered_surface_ && rendered_surface_->IsUsed()) { + tbm_pool_->Release(rendered_surface_); + } + rendered_surface_ = candidate_surface_; + candidate_surface_ = nullptr; + return rendered_surface_->GpuSurface(); +} + +void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { + if (event_info) { + WebView* webview = static_cast(data); + + std::lock_guard lock(webview->mutex_); + if (!webview->working_surface_) { + webview->working_surface_ = webview->tbm_pool_->GetAvailableBuffer(); + webview->working_surface_->UseExternalBuffer(); + } + webview->working_surface_->SetExternalBuffer( + static_cast(event_info)); + + if (webview->candidate_surface_) { + webview->tbm_pool_->Release(webview->candidate_surface_); + webview->candidate_surface_ = nullptr; + } + webview->candidate_surface_ = webview->working_surface_; + webview->working_surface_ = nullptr; + webview->texture_registrar_->MarkTextureFrameAvailable( + webview->GetTextureId()); + } +} + +void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + std::string url = std::string(ewk_view_url_get(webview->webview_instance_)); + flutter::EncodableMap args; + args.insert(std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + webview->channel_->InvokeMethod( + "onPageStarted", std::make_unique(args)); +} + +void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + std::string url = std::string(ewk_view_url_get(webview->webview_instance_)); + flutter::EncodableMap args; + args.insert(std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + webview->channel_->InvokeMethod( + "onPageFinished", std::make_unique(args)); +} + +void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + Ewk_Error* error = static_cast(event_info); + flutter::EncodableMap args = { + {flutter::EncodableValue("errorCode"), + flutter::EncodableValue(ewk_error_code_get(error))}, + {flutter::EncodableValue("description"), + flutter::EncodableValue(ewk_error_description_get(error))}, + {flutter::EncodableValue("errorType"), + flutter::EncodableValue(ErrorCodeToString(ewk_error_code_get(error)))}, + {flutter::EncodableValue("failingUrl"), + flutter::EncodableValue(ewk_error_url_get(error))}, + }; + webview->channel_->InvokeMethod( + "onWebResourceError", std::make_unique(args)); +} + +void WebView::OnConsoleMessage(void* data, Evas_Object* obj, void* event_info) { + Ewk_Console_Message* message = static_cast(event_info); + LOG_INFO( + "console message:%s: %d: %d: %s", + EwkInternalApiBinding::GetInstance().console_message.SourceGet(message), + EwkInternalApiBinding::GetInstance().console_message.LineGet(message), + EwkInternalApiBinding::GetInstance().console_message.LevelGet(message), + EwkInternalApiBinding::GetInstance().console_message.TextGet(message)); +} + +void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, + void* event_info) { + WebView* webview = static_cast(data); + Ewk_Policy_Decision* policy_decision = + static_cast(event_info); + + const char* url = ewk_policy_decision_url_get(policy_decision); + if (!webview->has_navigation_delegate_) { + ewk_policy_decision_use(policy_decision); + return; + } + ewk_view_suspend(webview->webview_instance_); + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), flutter::EncodableValue(url)}, + {flutter::EncodableValue("isForMainFrame"), + flutter::EncodableValue(true)}, + }; + auto result = std::make_unique(webview); + webview->channel_->InvokeMethod( + "navigationRequest", std::make_unique(args), + std::move(result)); +} + +void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, + void* user_data) { + FlMethodResult* result = static_cast(user_data); + result->Success(flutter::EncodableValue(result_value)); + delete result; +} + +void WebView::OnJavaScriptMessage(Evas_Object* obj, + Ewk_Script_Message message) { + if (obj) { + WebView* webview = + static_cast(evas_object_data_get(obj, kEwkInstance)); + if (webview->channel_) { + std::string channel_name(message.name); + std::string channel_message(static_cast(message.body)); + + flutter::EncodableMap args = { + {flutter::EncodableValue("channel"), + flutter::EncodableValue(channel_name)}, + {flutter::EncodableValue("message"), + flutter::EncodableValue(channel_message)}, + }; + 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 new file mode 100644 index 000000000..a0a528a6a --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview.h @@ -0,0 +1,95 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_WEBVIEW_H_ +#define FLUTTER_PLUGIN_WEBVIEW_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class BufferPool; +class BufferUnit; + +class WebView : public PlatformView { + public: + WebView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params, void* win); + ~WebView(); + + virtual void Dispose() override; + + virtual void Resize(double width, double height) override; + virtual void Touch(int type, int button, double x, double y, double dx, + double dy) override; + virtual void SetDirection(int direction) override; + + virtual void ClearFocus() override {} + + virtual bool SendKey(const char* key, const char* string, const char* compose, + uint32_t modifiers, uint32_t scan_code, + bool is_down) override; + + void Resume(); + + void Stop(); + + Evas_Object* GetWebViewInstance() { return webview_instance_; } + + FlutterDesktopGpuSurfaceDescriptor* ObtainGpuSurface(size_t width, + 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 RegisterJavaScriptChannelName(const std::string& name); + std::string GetChannelName(); + + void InitWebView(); + + static void OnFrameRendered(void* data, Evas_Object* obj, void* event_info); + static void OnLoadStarted(void* data, Evas_Object* obj, void* event_info); + static void OnLoadFinished(void* data, Evas_Object* obj, void* event_info); + static void OnLoadError(void* data, Evas_Object* obj, void* event_info); + static void OnConsoleMessage(void* data, Evas_Object* obj, void* event_info); + static void OnNavigationPolicy(void* data, Evas_Object* obj, + void* event_info); + static void OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, + void* user_data); + static void OnJavaScriptMessage(Evas_Object* obj, Ewk_Script_Message message); + + Evas_Object* webview_instance_ = nullptr; + void* win_ = nullptr; + flutter::TextureRegistrar* texture_registrar_; + double width_ = 0.0; + double height_ = 0.0; + BufferUnit* working_surface_ = nullptr; + BufferUnit* candidate_surface_ = nullptr; + BufferUnit* rendered_surface_ = nullptr; + bool has_navigation_delegate_ = false; + bool has_progress_tracking_ = false; + std::unique_ptr> channel_; + std::unique_ptr texture_variant_; + std::mutex mutex_; + std::unique_ptr tbm_pool_; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_H_ diff --git a/packages/webview_flutter/tizen/src/webview_factory.cc b/packages/webview_flutter/tizen/src/webview_factory.cc new file mode 100644 index 000000000..13fbec833 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_factory.cc @@ -0,0 +1,28 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webview_factory.h" + +#include +#include +#include + +#include +#include + +#include "log.h" +#include "webview.h" + +WebViewFactory::WebViewFactory(flutter::PluginRegistrar* registrar, void* win) + : PlatformViewFactory(registrar), win_(win) { + texture_registrar_ = registrar->texture_registrar(); +} + +PlatformView* WebViewFactory::Create(int view_id, double width, double height, + const ByteMessage& params) { + return new WebView(GetPluginRegistrar(), view_id, texture_registrar_, width, + height, *GetCodec().DecodeMessage(params), win_); +} + +void WebViewFactory::Dispose() {} diff --git a/packages/webview_flutter/tizen/src/webview_factory.h b/packages/webview_flutter/tizen/src/webview_factory.h new file mode 100644 index 000000000..ebb7c96d2 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_factory.h @@ -0,0 +1,28 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ + +#include +#include +#include + +#include + +class WebViewFactory : public PlatformViewFactory { + public: + WebViewFactory(flutter::PluginRegistrar* registrar, void* win); + + virtual PlatformView* Create(int view_id, double width, double height, + const ByteMessage& params) override; + + virtual void Dispose() override; + + private: + flutter::TextureRegistrar* texture_registrar_; + void* win_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ diff --git a/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc b/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc new file mode 100644 index 000000000..749f51a9a --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc @@ -0,0 +1,44 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webview_flutter_tizen_plugin.h" + +#include +#include + +#include + +#include "webview_factory.h" + +namespace { + +constexpr char kViewType[] = "plugins.flutter.io/webview"; + +class WebviewFlutterTizenPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto plugin = std::make_unique(); + registrar->AddPlugin(std::move(plugin)); + } + + WebviewFlutterTizenPlugin() {} + + virtual ~WebviewFlutterTizenPlugin() {} +}; + +} // namespace + +void WebviewFlutterTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef core_registrar) { + flutter::PluginRegistrar* registrar = + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(core_registrar); + FlutterDesktopViewRef view = + FlutterDesktopPluginRegistrarGetView(core_registrar); + FlutterDesktopRegisterViewFactory( + core_registrar, kViewType, + std::make_unique( + registrar, FlutterDesktopViewGetNativeHandle(view))); + WebviewFlutterTizenPlugin::RegisterWithRegistrar(registrar); +}