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/.metadata b/packages/webview_flutter/.metadata new file mode 100644 index 000000000..9a4695d28 --- /dev/null +++ b/packages/webview_flutter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8b3760638a189741cd9ca881aa2dd237c1df1be5 + channel: unknown + +project_type: plugin diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/packages/webview_flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/webview_flutter/LICENSE b/packages/webview_flutter/LICENSE new file mode 100644 index 000000000..4e5cfe14e --- /dev/null +++ b/packages/webview_flutter/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2020 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..bc4cfe7e6 --- /dev/null +++ b/packages/webview_flutter/README.md @@ -0,0 +1,52 @@ +# webview_flutter_tizen + +The Tizen implementation of [`webview_flutter`](https://github.com/flutter/plugins/tree/master/packages/webview_flutter). + +## Supported devices + +This plugin is available on these types of devices: + +- Galaxy Watch or TV (running Tizen 5.5 or later) + +## Usage + +```yaml +dependencies: + webview_flutter: ^1.0.6 + webview_flutter_tizen: ^1.0.0 +``` + +To enable tizen implementation, set `WebView.platform = TizenWebView();` in `initState()`. +For example: + +```dart +import 'dart:io'; + +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_tizen/webview_flutter_tizen.dart'; + +class WebViewExample extends StatefulWidget { + @override + WebViewExampleState createState() => WebViewExampleState(); +} + +class WebViewExampleState extends State { + @override + void initState() { + super.initState(); + WebView.platform = TizenWebView(); + } + + @override + Widget build(BuildContext context) { + return WebView( + initialUrl: 'https://flutter.dev', + ); + } +} +``` + +## Limitations +- This plugin is only supported on **Galaxy Watch and TV** devices running Tizen 5.5 or later. +- This is an initial webview plugin for Tizen and is implemented based on Tizen Lightweight Web Engine (LWE). If you would like to know detailed specifications that the LWE supports, please refer to the following link : +https://review.tizen.org/gerrit/gitweb?p=platform/upstream/lightweight-web-engine.git;a=blob;f=docs/Spec.md;h=ecb8f437c5a1facc77d3435e1a8aad6a267f12f3;hb=refs/heads/tizen 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/.metadata b/packages/webview_flutter/example/.metadata new file mode 100644 index 000000000..da892bb87 --- /dev/null +++ b/packages/webview_flutter/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8b3760638a189741cd9ca881aa2dd237c1df1be5 + channel: unknown + +project_type: app diff --git a/packages/webview_flutter/example/README.md b/packages/webview_flutter/example/README.md new file mode 100644 index 000000000..ebff2bd76 --- /dev/null +++ b/packages/webview_flutter/example/README.md @@ -0,0 +1,16 @@ +# webview_flutter_tizen_example + +Demonstrates how to use the webview_flutter_tizen plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. 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/integration_test/webview_flutter_tizen_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_tizen_test.dart new file mode 100644 index 000000000..f3ba4f867 --- /dev/null +++ b/packages/webview_flutter/example/integration_test/webview_flutter_tizen_test.dart @@ -0,0 +1,1216 @@ +// Copyright 2018 The Chromium 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:math'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter/src/foundation/basic_types.dart'; +import 'package:flutter/src/gestures/recognizer.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_tizen/webview_flutter_tizen.dart'; + +typedef void VoidCallback(); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final _FakePlatformViewsController fakePlatformViewsController = + _FakePlatformViewsController(); + + final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + SystemChannels.platform + .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); + }); + + setUp(() { + WebView.platform = TizenWebView(); + fakePlatformViewsController.reset(); + _fakeCookieManager.reset(); + }); + + testWidgets('Create WebView', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + }); + + testWidgets('Initial url', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(await controller.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Javascript mode', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.disabled, + )); + expect(platformWebView.javascriptMode, JavascriptMode.disabled); + }); + + testWidgets('Load url', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller.loadUrl('https://flutter.io'); + + expect(await controller.currentUrl(), 'https://flutter.io'); + }); + + testWidgets('Invalid urls', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller.loadUrl(null), throwsA(anything)); + expect(await controller.currentUrl(), isNull); + + expect(() => controller.loadUrl(''), throwsA(anything)); + expect(await controller.currentUrl(), isNull); + + // Missing schema. + expect(() => controller.loadUrl('flutter.io'), throwsA(anything)); + expect(await controller.currentUrl(), isNull); + }); + + testWidgets('Headers in loadUrl', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final Map headers = { + 'CACHE-CONTROL': 'ABC' + }; + await controller.loadUrl('https://flutter.io', headers: headers); + expect(await controller.currentUrl(), equals('https://flutter.io')); + }); + + testWidgets("Can't go back before loading a page", + (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final bool canGoBackNoPageLoaded = await controller.canGoBack(); + + expect(canGoBackNoPageLoaded, false); + }); + + testWidgets("Clear Cache", (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + expect(fakePlatformViewsController.lastCreatedView.hasCache, true); + + await controller.clearCache(); + + expect(fakePlatformViewsController.lastCreatedView.hasCache, false); + }); + + testWidgets("Can't go back with no history", (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + final bool canGoBackFirstPageLoaded = await controller.canGoBack(); + + expect(canGoBackFirstPageLoaded, false); + }); + + testWidgets('Can go back', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller.loadUrl('https://www.google.com'); + final bool canGoBackSecondPageLoaded = await controller.canGoBack(); + + expect(canGoBackSecondPageLoaded, true); + }); + + testWidgets("Can't go forward before loading a page", + (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final bool canGoForwardNoPageLoaded = await controller.canGoForward(); + + expect(canGoForwardNoPageLoaded, false); + }); + + testWidgets("Can't go forward with no history", (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + final bool canGoForwardFirstPageLoaded = await controller.canGoForward(); + + expect(canGoForwardFirstPageLoaded, false); + }); + + testWidgets('Can go forward', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller.loadUrl('https://youtube.com'); + await controller.goBack(); + final bool canGoForwardFirstPageBacked = await controller.canGoForward(); + + expect(canGoForwardFirstPageBacked, true); + }); + + testWidgets('Go back', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(await controller.currentUrl(), 'https://youtube.com'); + + await controller.loadUrl('https://flutter.io'); + + expect(await controller.currentUrl(), 'https://flutter.io'); + + await controller.goBack(); + + expect(await controller.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Go forward', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(await controller.currentUrl(), 'https://youtube.com'); + + await controller.loadUrl('https://flutter.io'); + + expect(await controller.currentUrl(), 'https://flutter.io'); + + await controller.goBack(); + + expect(await controller.currentUrl(), 'https://youtube.com'); + + await controller.goForward(); + + expect(await controller.currentUrl(), 'https://flutter.io'); + }); + + testWidgets('Current URL', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + // Test a WebView without an explicitly set first URL. + expect(await controller.currentUrl(), isNull); + + await controller.loadUrl('https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); + + await controller.loadUrl('https://flutter.io'); + expect(await controller.currentUrl(), 'https://flutter.io'); + + await controller.goBack(); + expect(await controller.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Reload url', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + + await controller.reload(); + + expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); + + await controller.loadUrl('https://youtube.com'); + + expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + }); + + testWidgets('evaluate Javascript', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + await controller.evaluateJavascript("fake js string"), "fake js string", + reason: 'should get the argument'); + expect( + () => controller.evaluateJavascript(null), + throwsA(anything), + ); + }); + + testWidgets('evaluate Javascript with JavascriptMode disabled', + (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.evaluateJavascript('fake js string'), + throwsA(anything), + ); + expect( + () => controller.evaluateJavascript(null), + throwsA(anything), + ); + }); + + testWidgets('Cookies can be cleared once', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + final bool hasCookies = await cookieManager.clearCookies(); + expect(hasCookies, true); + }); + + testWidgets('Second cookie clear does not have cookies', + (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + final bool hasCookies = await cookieManager.clearCookies(); + expect(hasCookies, true); + final bool hasCookiesSecond = await cookieManager.clearCookies(); + expect(hasCookiesSecond, false); + }); + + testWidgets('Initial JavaScript channels', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm'])); + }); + + test('Only valid JavaScript channel names are allowed', () { + final JavascriptMessageHandler noOp = (JavascriptMessage msg) {}; + JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); + JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); + JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); + + VoidCallback createChannel(String name) { + return () { + JavascriptChannel(name: name, onMessageReceived: noOp); + }; + } + + expect(createChannel('1Alarm'), throwsAssertionError); + expect(createChannel('foo.bar'), throwsAssertionError); + expect(createChannel(''), throwsAssertionError); + }); + + testWidgets('Unique JavaScript channel names are required', + (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + expect(tester.takeException(), isNot(null)); + }); + + testWidgets('JavaScript channels update', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + }); + + testWidgets('Remove all JavaScript channels and then add', + (WidgetTester tester) async { + // This covers a specific bug we had where after updating javascriptChannels to null, + // updating it again with a subset of the previously registered channels fails as the + // widget's cache of current channel wasn't properly updated when updating javascriptChannels to + // null. + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts'])); + }); + + testWidgets('JavaScript channel messages', (WidgetTester tester) async { + final List ttsMessagesReceived = []; + final List alarmMessagesReceived = []; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Tts', + onMessageReceived: (JavascriptMessage msg) { + ttsMessagesReceived.add(msg.message); + }), + JavascriptChannel( + name: 'Alarm', + onMessageReceived: (JavascriptMessage msg) { + alarmMessagesReceived.add(msg.message); + }), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(ttsMessagesReceived, isEmpty); + expect(alarmMessagesReceived, isEmpty); + + platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); + platformWebView.fakeJavascriptPostMessage('Tts', 'World'); + + expect(ttsMessagesReceived, ['Hello', 'World']); + }); + + group('$PageStartedCallback', () { + testWidgets('onPageStarted is not null', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + + testWidgets('onPageStarted is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageStarted: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + // The platform side will always invoke a call for onPageStarted. This is + // to test that it does not crash on a null callback. + platformWebView.fakeOnPageStartedCallback(); + }); + + testWidgets('onPageStarted changed', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + }); + + group('$PageFinishedCallback', () { + testWidgets('onPageFinished is not null', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageFinishedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + + testWidgets('onPageFinished is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageFinished: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + // The platform side will always invoke a call for onPageFinished. This is + // to test that it does not crash on a null callback. + platformWebView.fakeOnPageFinishedCallback(); + }); + + testWidgets('onPageFinished changed', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageFinishedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + }); + + group('navigationDelegate', () { + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.hasNavigationDelegate, false); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest r) => null, + )); + + expect(platformWebView.hasNavigationDelegate, true); + }); + + testWidgets('Block navigation', (WidgetTester tester) async { + final List navigationRequests = []; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest request) { + navigationRequests.add(request); + // Only allow navigating to https://flutter.dev + return request.url == 'https://flutter.dev' + ? NavigationDecision.navigate + : NavigationDecision.prevent; + })); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.hasNavigationDelegate, true); + + platformWebView.fakeNavigate('https://www.google.com'); + // The navigation delegate only allows navigation to https://flutter.dev + // so we should still be in https://youtube.com. + expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(navigationRequests.length, 1); + expect(navigationRequests[0].url, 'https://www.google.com'); + expect(navigationRequests[0].isForMainFrame, true); + + platformWebView.fakeNavigate('https://flutter.dev'); + await tester.pump(); + expect(platformWebView.currentUrl, 'https://flutter.dev'); + }); + }); + + group('debuggingEnabled', () { + testWidgets('enable debugging', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + debuggingEnabled: true, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.debuggingEnabled, true); + }); + + testWidgets('defaults to false', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.debuggingEnabled, false); + }); + + testWidgets('can be changed', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(WebView(key: key)); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + await tester.pumpWidget(WebView( + key: key, + debuggingEnabled: true, + )); + + expect(platformWebView.debuggingEnabled, true); + + await tester.pumpWidget(WebView( + key: key, + debuggingEnabled: false, + )); + + expect(platformWebView.debuggingEnabled, false); + }); + }); + + // Currently, webview for tizen cannot satisfy this test due to its implementation limitations. + /* + group('Custom platform implementation', () { + setUpAll(() { + WebView.platform = MyWebViewPlatform(); + }); + tearDownAll(() { + WebView.platform = null; + }); + + testWidgets('creation', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + gestureNavigationEnabled: true, + ), + ); + + final MyWebViewPlatform builder = WebView.platform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt; + + expect( + platform.creationParams, + MatchesCreationParams(CreationParams( + initialUrl: 'https://youtube.com', + webSettings: WebSettings( + javascriptMode: JavascriptMode.disabled, + hasNavigationDelegate: false, + debuggingEnabled: false, + userAgent: WebSetting.of(null), + gestureNavigationEnabled: true, + ), + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannelNames: Set(), + ))); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final MyWebViewPlatform builder = WebView.platform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt; + + final Map headers = { + 'header': 'value', + }; + + await controller.loadUrl('https://google.com', headers: headers); + + expect(platform.lastUrlLoaded, 'https://google.com'); + expect(platform.lastRequestHeaders, headers); + }); + }); + */ + testWidgets('Set UserAgent', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.userAgent, isNull); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA', + )); + + expect(platformWebView.userAgent, 'UA'); + }); +} + +class FakePlatformWebView { + FakePlatformWebView(int id, Map params) { + if (params.containsKey('initialUrl')) { + final String initialUrl = params['initialUrl']; + if (initialUrl != null) { + history.add(initialUrl); + currentPosition++; + } + } + if (params.containsKey('javascriptChannelNames')) { + javascriptChannelNames = + List.from(params['javascriptChannelNames']); + } + javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; + hasNavigationDelegate = + params['settings']['hasNavigationDelegate'] ?? false; + debuggingEnabled = params['settings']['debuggingEnabled']; + userAgent = params['settings']['userAgent']; + channel = MethodChannel( + 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); + channel.setMockMethodCallHandler(onMethodCall); + } + + MethodChannel channel; + + List history = []; + int currentPosition = -1; + int amountOfReloadsOnCurrentUrl = 0; + bool hasCache = true; + + String get currentUrl => history.isEmpty ? null : history[currentPosition]; + JavascriptMode javascriptMode; + List javascriptChannelNames; + + bool hasNavigationDelegate; + bool debuggingEnabled; + String userAgent; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case 'loadUrl': + final Map request = call.arguments; + _loadUrl(request['url']); + return Future.sync(() {}); + case 'updateSettings': + if (call.arguments['jsMode'] != null) { + javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; + } + if (call.arguments['hasNavigationDelegate'] != null) { + hasNavigationDelegate = call.arguments['hasNavigationDelegate']; + } + if (call.arguments['debuggingEnabled'] != null) { + debuggingEnabled = call.arguments['debuggingEnabled']; + } + userAgent = call.arguments['userAgent']; + break; + case 'canGoBack': + return Future.sync(() => currentPosition > 0); + break; + case 'canGoForward': + return Future.sync(() => currentPosition < history.length - 1); + break; + case 'goBack': + currentPosition = max(-1, currentPosition - 1); + return Future.sync(() {}); + break; + case 'goForward': + currentPosition = min(history.length - 1, currentPosition + 1); + return Future.sync(() {}); + case 'reload': + amountOfReloadsOnCurrentUrl++; + return Future.sync(() {}); + break; + case 'currentUrl': + return Future.value(currentUrl); + break; + case 'evaluateJavascript': + return Future.value(call.arguments); + break; + case 'addJavascriptChannels': + final List channelNames = List.from(call.arguments); + javascriptChannelNames.addAll(channelNames); + break; + case 'removeJavascriptChannels': + final List channelNames = List.from(call.arguments); + javascriptChannelNames + .removeWhere((String channel) => channelNames.contains(channel)); + break; + case 'clearCache': + hasCache = false; + return Future.sync(() {}); + } + return Future.sync(() {}); + } + + void fakeJavascriptPostMessage(String jsChannel, String message) { + final StandardMethodCodec codec = const StandardMethodCodec(); + final Map arguments = { + 'channel': jsChannel, + 'message': message + }; + final ByteData data = codec + .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); + ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData data) {}); + } + + // Fakes a main frame navigation that was initiated by the webview, e.g when + // the user clicks a link in the currently loaded page. + void fakeNavigate(String url) { + if (!hasNavigationDelegate) { + print('no navigation delegate'); + _loadUrl(url); + return; + } + final StandardMethodCodec codec = const StandardMethodCodec(); + final Map arguments = { + 'url': url, + 'isForMainFrame': true + }; + final ByteData data = + codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); + ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData data) { + final bool allow = codec.decodeEnvelope(data); + if (allow) { + _loadUrl(url); + } + }); + } + + void fakeOnPageStartedCallback() { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageStarted', + {'url': currentUrl}, + )); + + ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + channel.name, + data, + (ByteData data) {}, + ); + } + + void fakeOnPageFinishedCallback() { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageFinished', + {'url': currentUrl}, + )); + + ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + channel.name, + data, + (ByteData data) {}, + ); + } + + void _loadUrl(String url) { + history = history.sublist(0, currentPosition + 1); + history.add(url); + currentPosition++; + amountOfReloadsOnCurrentUrl = 0; + } +} + +class _FakePlatformViewsController { + FakePlatformWebView lastCreatedView; + + Future fakePlatformViewsMethodHandler(MethodCall call) { + switch (call.method) { + case 'create': + final Map args = call.arguments; + final Map params = _decodeParams(args['params']); + lastCreatedView = FakePlatformWebView( + args['id'], + params, + ); + return Future.sync(() => 1); + default: + return Future.sync(() {}); + } + } + + void reset() { + lastCreatedView = null; + } +} + +Map _decodeParams(Uint8List paramsMessage) { + final ByteBuffer buffer = paramsMessage.buffer; + final ByteData messageBytes = buffer.asByteData( + paramsMessage.offsetInBytes, + paramsMessage.lengthInBytes, + ); + return const StandardMessageCodec().decodeMessage(messageBytes); +} + +class _FakeCookieManager { + _FakeCookieManager() { + final MethodChannel channel = const MethodChannel( + 'plugins.flutter.io/cookie_manager', + StandardMethodCodec(), + ); + channel.setMockMethodCallHandler(onMethodCall); + } + + bool hasCookies = true; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case 'clearCookies': + bool hadCookies = false; + if (hasCookies) { + hadCookies = true; + hasCookies = false; + } + return Future.sync(() { + return hadCookies; + }); + break; + } + return Future.sync(() => null); + } + + void reset() { + hasCookies = true; + } +} + +class MyWebViewPlatform implements WebViewPlatform { + MyWebViewPlatformController lastPlatformBuilt; + + @override + Widget build({ + BuildContext context, + CreationParams creationParams, + @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + @required WebViewPlatformCreatedCallback onWebViewPlatformCreated, + Set> gestureRecognizers, + }) { + assert(onWebViewPlatformCreated != null); + lastPlatformBuilt = MyWebViewPlatformController( + creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); + onWebViewPlatformCreated(lastPlatformBuilt); + return Container(); + } + + @override + Future clearCookies() { + return Future.sync(() => null); + } +} + +class MyWebViewPlatformController extends WebViewPlatformController { + MyWebViewPlatformController(this.creationParams, this.gestureRecognizers, + WebViewPlatformCallbacksHandler platformHandler) + : super(platformHandler); + + CreationParams creationParams; + Set> gestureRecognizers; + + String lastUrlLoaded; + Map lastRequestHeaders; + + @override + Future loadUrl(String url, Map headers) { + equals(1, 1); + lastUrlLoaded = url; + lastRequestHeaders = headers; + return null; + } +} + +class MatchesWebSettings extends Matcher { + MatchesWebSettings(this._webSettings); + + final WebSettings _webSettings; + + @override + Description describe(Description description) => + description.add('$_webSettings'); + + @override + bool matches( + covariant WebSettings webSettings, Map matchState) { + return _webSettings.javascriptMode == webSettings.javascriptMode && + _webSettings.hasNavigationDelegate == + webSettings.hasNavigationDelegate && + _webSettings.debuggingEnabled == webSettings.debuggingEnabled && + _webSettings.gestureNavigationEnabled == + webSettings.gestureNavigationEnabled && + _webSettings.userAgent == webSettings.userAgent; + } +} + +class MatchesCreationParams extends Matcher { + MatchesCreationParams(this._creationParams); + + final CreationParams _creationParams; + + @override + Description describe(Description description) => + description.add('$_creationParams'); + + @override + bool matches(covariant CreationParams creationParams, + Map matchState) { + return _creationParams.initialUrl == creationParams.initialUrl && + MatchesWebSettings(_creationParams.webSettings) + .matches(creationParams.webSettings, matchState) && + orderedEquals(_creationParams.javascriptChannelNames) + .matches(creationParams.javascriptChannelNames, matchState); + } +} diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart new file mode 100644 index 000000000..5e26b5dac --- /dev/null +++ b/packages/webview_flutter/example/lib/main.dart @@ -0,0 +1,346 @@ +// Copyright 2018 The Chromium 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 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_tizen/webview_flutter_tizen.dart'; + +void main() => runApp(MaterialApp(home: WebViewExample())); + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

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

+ + + +'''; + +class WebViewExample extends StatefulWidget { + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State { + final Completer _controller = + Completer(); + + @override + void initState() { + super.initState(); + WebView.platform = TizenWebView(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + 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), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (BuildContext context) { + return WebView( + initialUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + _toasterJavascriptChannel(context), + ].toSet(), + 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, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + Scaffold.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = await controller.data.currentUrl(); + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller); + + final Future controller; + final CookieManager cookieManager = CookieManager(); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton( + 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; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + 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'), + ), + ], + ); + }, + ); + } + + void _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.evaluateJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Toaster.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _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.'; + } + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + 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(), + ); + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + 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 { + Scaffold.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 { + Scaffold.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..80c7abe7b --- /dev/null +++ b/packages/webview_flutter/example/pubspec.yaml @@ -0,0 +1,23 @@ +name: webview_flutter_tizen_example +description: Demonstrates how to use the webview_flutter_tizen plugin. + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter: ^1.0.6 + webview_flutter_tizen: + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: ^1.0.1 + integration_test_tizen: + path: ../../integration_test/ + +flutter: + uses-material-design: true 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..b38629cca --- /dev/null +++ b/packages/webview_flutter/example/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +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/NuGet.Config b/packages/webview_flutter/example/tizen/NuGet.Config new file mode 100644 index 000000000..c4ea70c17 --- /dev/null +++ b/packages/webview_flutter/example/tizen/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/webview_flutter/example/tizen/Runner.csproj b/packages/webview_flutter/example/tizen/Runner.csproj new file mode 100644 index 000000000..8ebc2abdf --- /dev/null +++ b/packages/webview_flutter/example/tizen/Runner.csproj @@ -0,0 +1,26 @@ + + + + Exe + tizen60 + + + + portable + + + none + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/webview_flutter/example/tizen/lib/arm/liblightweight-web-engine.flutter.so b/packages/webview_flutter/example/tizen/lib/arm/liblightweight-web-engine.flutter.so new file mode 100755 index 000000000..3d1178bc0 Binary files /dev/null and b/packages/webview_flutter/example/tizen/lib/arm/liblightweight-web-engine.flutter.so differ 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/shared/res/input.html b/packages/webview_flutter/example/tizen/shared/res/input.html new file mode 100644 index 000000000..1725547d7 --- /dev/null +++ b/packages/webview_flutter/example/tizen/shared/res/input.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + 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..42e10c4fe --- /dev/null +++ b/packages/webview_flutter/example/tizen/tizen-manifest.xml @@ -0,0 +1,20 @@ + + + + + + ic_launcher.png + + + + http://tizen.org/privilege/network.get + http://tizen.org/privilege/appmanager.launch + http://tizen.org/privilege/packagemanager.info + http://tizen.org/privilege/datasharing + http://tizen.org/privilege/internet + http://tizen.org/privilege/wifidirect + http://tizen.org/privilege/filesystem.read + http://tizen.org/privilege/filesystem.write + + + 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..a2308dfdb --- /dev/null +++ b/packages/webview_flutter/lib/webview_flutter_tizen.dart @@ -0,0 +1,849 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/rendering.dart'; + +import 'package:webview_flutter/platform_interface.dart'; +import 'package:webview_flutter/src/webview_method_channel.dart'; + +enum _TizenViewState { + waitingForSize, + creating, + created, + disposed, +} + +enum PlatformViewHitTestBehavior { + opaque, + translucent, + transparent, +} + +enum _PlatformViewState { + uninitialized, + resizing, + ready, +} + +class TizenViewController extends PlatformViewController { + TizenViewController._({ + @required this.viewId, + @required String viewType, + @required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec creationParamsCodec, + bool waitingForSize = true, + }) : assert(viewId != null), + assert(viewType != null), + assert(layoutDirection != null), + assert(creationParams == null || creationParamsCodec != null), + _viewType = viewType, + _layoutDirection = layoutDirection, + _creationParams = creationParams, + _creationParamsCodec = creationParamsCodec, + _state = waitingForSize + ? _TizenViewState.waitingForSize + : _TizenViewState.creating; + + @override + final int viewId; + + final String _viewType; + + TextDirection _layoutDirection; + + _TizenViewState _state; + + final dynamic _creationParams; + + final MessageCodec _creationParamsCodec; + + final List _platformViewCreatedCallbacks = + []; + + static int pointerAction(int pointerId, int action) { + return ((pointerId << 8) & 0xff00) | (action & 0xff); + } + + int _textureId; + + int get textureId => _textureId; + + Size _size; + + Future setSize(Size size) async { + assert(_state != _TizenViewState.disposed, + 'trying to size a disposed Tizen View. View id: $viewId'); + + assert(size != null); + assert(!size.isEmpty); + + if (_state == _TizenViewState.waitingForSize) { + _size = size; + return create(); + } + await SystemChannels.platform_views + .invokeMethod('resize', { + 'id': viewId, + 'width': size.width, + 'height': size.height, + }); + } + + Future _sendCreateMessage() async { + assert(!_size.isEmpty, + 'trying to create $TizenViewController without setting a valid size.'); + + final Map args = { + 'id': viewId, + 'viewType': _viewType, + 'width': _size.width, + 'height': _size.height, + 'direction': _layoutDirection == TextDirection.ltr ? 0 : 1, + }; + if (_creationParams != null) { + final ByteData paramsByteData = + _creationParamsCodec.encodeMessage(_creationParams); + args['params'] = Uint8List.view( + paramsByteData.buffer, + 0, + paramsByteData.lengthInBytes, + ); + } + _textureId = + await SystemChannels.platform_views.invokeMethod('create', args); + } + + Future _sendDisposeMessage() { + return SystemChannels.platform_views + .invokeMethod('dispose', { + 'id': viewId, + 'hybrid': false, + }); + } + + Future create() async { + assert(_state != _TizenViewState.disposed, + 'trying to create a disposed Tizen view'); + await _sendCreateMessage(); + + _state = _TizenViewState.created; + for (final PlatformViewCreatedCallback callback + in _platformViewCreatedCallbacks) { + callback(viewId); + } + } + + @Deprecated('Call `controller.viewId` instead. ' + 'This feature was deprecated after v1.20.0-2.0.pre.') + int get id => viewId; + + set pointTransformer(PointTransformer transformer) { + assert(transformer != null); + } + + bool get isCreated => _state == _TizenViewState.created; + + void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { + assert(listener != null); + assert(_state != _TizenViewState.disposed); + _platformViewCreatedCallbacks.add(listener); + } + + /// Removes a callback added with [addOnPlatformViewCreatedListener]. + void removeOnPlatformViewCreatedListener( + PlatformViewCreatedCallback listener) { + assert(_state != _TizenViewState.disposed); + _platformViewCreatedCallbacks.remove(listener); + } + + Future setLayoutDirection(TextDirection layoutDirection) async { + assert(_state != _TizenViewState.disposed, + 'trying to set a layout direction for a disposed UIView. View id: $viewId'); + + if (layoutDirection == _layoutDirection) { + return; + } + + assert(layoutDirection != null); + _layoutDirection = layoutDirection; + + if (_state == _TizenViewState.waitingForSize) { + return; + } + + await SystemChannels.platform_views + .invokeMethod('setDirection', { + 'id': viewId, + 'direction': layoutDirection == TextDirection.ltr ? 0 : 1, + }); + } + + @override + Future dispatchPointerEvent(PointerEvent event) async { + if (event is PointerHoverEvent) { + return; + } + + int eventType = 0; + if (event is PointerDownEvent) { + eventType = 0; + } else if (event is PointerMoveEvent) { + eventType = 1; + } else if (event is PointerUpEvent) { + eventType = 2; + } else { + throw UnimplementedError('Not Implemented'); + } + await SystemChannels.platform_views + .invokeMethod('touch', { + 'id': viewId, + 'event': [ + eventType, // int, pointer event type + event.buttons, // int, mouse button type (left, right, middle) + event.localPosition.dx, // double, global position x + event.localPosition.dy, // double, global position y + event.localDelta.dx, // double, moved position x + event.localDelta.dy, // double, moved position y + ] + }); + } + + @override + Future clearFocus() { + if (_state != _TizenViewState.created) { + return Future.value(); + } + print('TizenViewController::clearFocus() : $viewId'); + return SystemChannels.platform_views + .invokeMethod('clearFocus', viewId); + } + + @override + Future dispose() async { + print('TizenViewController::dispose()'); + if (_state == _TizenViewState.creating || _state == _TizenViewState.created) + await _sendDisposeMessage(); + _platformViewCreatedCallbacks.clear(); + _state = _TizenViewState.disposed; + PlatformViewsServiceTizen._instance._focusCallbacks.remove(viewId); + } +} + +class TizenView extends StatefulWidget { + const TizenView({ + Key key, + @required this.viewType, + this.onPlatformViewCreated, + this.hitTestBehavior = PlatformViewHitTestBehavior.opaque, + this.layoutDirection, + this.gestureRecognizers, + this.creationParams, + this.creationParamsCodec, + }) : assert(viewType != null), + assert(hitTestBehavior != null), + assert(creationParams == null || creationParamsCodec != null), + super(key: key); + + final String viewType; + final PlatformViewCreatedCallback onPlatformViewCreated; + final PlatformViewHitTestBehavior hitTestBehavior; + final TextDirection layoutDirection; + final Set> gestureRecognizers; + final dynamic creationParams; + final MessageCodec creationParamsCodec; + + @override + State createState() => _TizenWebViewState(); +} + +class PlatformViewsServiceTizen { + PlatformViewsServiceTizen._() { + SystemChannels.platform_views.setMethodCallHandler(_onMethodCall); + } + static final PlatformViewsServiceTizen _instance = + PlatformViewsServiceTizen._(); + + Future _onMethodCall(MethodCall call) { + print('TizenView::_onMethodCall() - ${call.method}'); + switch (call.method) { + case 'viewFocused': + final int id = call.arguments as int; + print('viewFocused: id - $id'); + if (_focusCallbacks.containsKey(id)) { + if (_focusCallbacks[id] != null) { + _focusCallbacks[id](); + } else { + throw FlutterError('FocusCallbacks[$id] must not be null.'); + } + } + break; + default: + throw UnimplementedError( + "${call.method} was invoked but isn't implemented by PlatformViewsService"); + } + return Future.value(); + } + + final Map _focusCallbacks = {}; + + static TizenViewController initTizenView({ + @required int id, + @required String viewType, + @required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec creationParamsCodec, + VoidCallback onFocus, + }) { + assert(id != null); + assert(viewType != null); + assert(layoutDirection != null); + assert(creationParams == null || creationParamsCodec != null); + + print( + 'PlatformViewsServiceTizen::initTizenView [id:$id] [onFocus:$onFocus]'); + + final TizenViewController controller = TizenViewController._( + viewId: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: creationParamsCodec, + ); + + _instance._focusCallbacks[id] = onFocus ?? () {}; + return controller; + } +} + +typedef _HandlePointerEvent = Future Function(PointerEvent event); + +// This recognizer constructs gesture recognizers from a set of gesture recognizer factories +// it was give, adds all of them to a gesture arena team with the _PlatformViewGestureRecognizer +// as the team captain. +// As long as the gesture arena is unresolved, the recognizer caches all pointer events. +// When the team wins, the recognizer sends all the cached pointer events to `_handlePointerEvent`, and +// sets itself to a "forwarding mode" where it will forward any new pointer event to `_handlePointerEvent`. +class _PlatformViewGestureRecognizer extends OneSequenceGestureRecognizer { + _PlatformViewGestureRecognizer( + _HandlePointerEvent handlePointerEvent, + this.gestureRecognizerFactories, { + PointerDeviceKind kind, + }) : super(kind: kind) { + team = GestureArenaTeam(); + team.captain = this; + _gestureRecognizers = gestureRecognizerFactories.map( + (Factory recognizerFactory) { + final OneSequenceGestureRecognizer gestureRecognizer = + recognizerFactory.constructor(); + gestureRecognizer.team = team; + // The below gesture recognizers requires at least one non-empty callback to + // compete in the gesture arena. + // https://github.com/flutter/flutter/issues/35394#issuecomment-562285087 + if (gestureRecognizer is LongPressGestureRecognizer) { + gestureRecognizer.onLongPress ??= () {}; + } else if (gestureRecognizer is DragGestureRecognizer) { + gestureRecognizer.onDown ??= (_) {}; + } else if (gestureRecognizer is TapGestureRecognizer) { + gestureRecognizer.onTapDown ??= (_) {}; + } + return gestureRecognizer; + }, + ).toSet(); + _handlePointerEvent = handlePointerEvent; + } + + _HandlePointerEvent _handlePointerEvent; + + // Maps a pointer to a list of its cached pointer events. + // Before the arena for a pointer is resolved all events are cached here, if we win the arena + // the cached events are dispatched to `_handlePointerEvent`, if we lose the arena we clear the cache for + // the pointer. + final Map> cachedEvents = >{}; + + // Pointer for which we have already won the arena, events for pointers in this set are + // immediately dispatched to `_handlePointerEvent`. + final Set forwardedPointers = {}; + + // We use OneSequenceGestureRecognizers as they support gesture arena teams. + // TODO(amirh): get a list of GestureRecognizers here. + // https://github.com/flutter/flutter/issues/20953 + final Set> gestureRecognizerFactories; + Set _gestureRecognizers; + + @override + void addAllowedPointer(PointerDownEvent event) { + startTrackingPointer(event.pointer, event.transform); + for (final OneSequenceGestureRecognizer recognizer in _gestureRecognizers) { + recognizer.addPointer(event); + } + } + + @override + String get debugDescription => 'Platform view'; + + @override + void didStopTrackingLastPointer(int pointer) {} + + @override + void handleEvent(PointerEvent event) { + if (!forwardedPointers.contains(event.pointer)) { + _cacheEvent(event); + } else { + _handlePointerEvent(event); + } + stopTrackingIfPointerNoLongerDown(event); + } + + @override + void acceptGesture(int pointer) { + _flushPointerCache(pointer); + forwardedPointers.add(pointer); + } + + @override + void rejectGesture(int pointer) { + stopTrackingPointer(pointer); + cachedEvents.remove(pointer); + } + + void _cacheEvent(PointerEvent event) { + if (!cachedEvents.containsKey(event.pointer)) { + cachedEvents[event.pointer] = []; + } + cachedEvents[event.pointer].add(event); + } + + void _flushPointerCache(int pointer) { + cachedEvents.remove(pointer)?.forEach(_handlePointerEvent); + } + + @override + void stopTrackingPointer(int pointer) { + super.stopTrackingPointer(pointer); + forwardedPointers.remove(pointer); + } + + void reset() { + forwardedPointers.forEach(super.stopTrackingPointer); + forwardedPointers.clear(); + cachedEvents.keys.forEach(super.stopTrackingPointer); + cachedEvents.clear(); + resolve(GestureDisposition.rejected); + } +} + +class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { + /// Creates a render object for an Tizen view. + RenderTizenView({ + @required TizenViewController viewController, + @required PlatformViewHitTestBehavior hitTestBehavior, + @required Set> gestureRecognizers, + }) : assert(viewController != null), + assert(hitTestBehavior != null), + assert(gestureRecognizers != null), + _viewController = viewController { + _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); + updateGestureRecognizers(gestureRecognizers); + _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); + this.hitTestBehavior = hitTestBehavior; + } + + _PlatformViewState _state = _PlatformViewState.uninitialized; + + TizenViewController get viewcontroller => _viewController; + TizenViewController _viewController; + + set viewController(TizenViewController viewController) { + assert(_viewController != null); + assert(viewController != null); + if (_viewController == viewController) { + return; + } + _viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); + _viewController = viewController; + _sizePlatformView(); + if (_viewController.isCreated) { + markNeedsSemanticsUpdate(); + } + _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); + } + + void _onPlatformViewCreated(int id) { + markNeedsSemanticsUpdate(); + } + + void updateGestureRecognizers( + Set> gestureRecognizers) { + _updateGestureRecognizersWithCallBack( + gestureRecognizers, _viewController.dispatchPointerEvent); + } + + @override + bool get sizedByParent => true; + + @override + bool get alwaysNeedsCompositing => true; + + @override + bool get isRepaintBoundary => true; + + @override + void performResize() { + size = constraints.biggest; + _sizePlatformView(); + } + + Size _currentTizenViewSize; + + Future _sizePlatformView() async { + if (_state == _PlatformViewState.resizing || size.isEmpty) { + return; + } + _state = _PlatformViewState.resizing; + markNeedsPaint(); + + Size targetSize; + do { + targetSize = size; + await _viewController.setSize(targetSize); + _currentTizenViewSize = targetSize; + } while (size != targetSize); + + _state = _PlatformViewState.ready; + markNeedsPaint(); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (_viewController.textureId == null) { + return; + } + if (size.width < _currentTizenViewSize.width || + size.height < _currentTizenViewSize.height) { + context.pushClipRect(true, offset, offset & size, _paintTexture); + return; + } + + _paintTexture(context, offset); + } + + void _paintTexture(PaintingContext context, Offset offset) { + context.addLayer(TextureLayer( + rect: offset & _currentTizenViewSize, + textureId: _viewController.textureId, + freeze: _state == _PlatformViewState.resizing, + )); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + + config.isSemanticBoundary = true; + + if (_viewController.isCreated) { + config.platformViewId = _viewController.viewId; + } + } +} + +class _TizenPlatformTextureView extends LeafRenderObjectWidget { + const _TizenPlatformTextureView({ + Key key, + @required this.controller, + @required this.hitTestBehavior, + @required this.gestureRecognizers, + @required this.textureId, + }) : assert(controller != null), + assert(hitTestBehavior != null), + assert(gestureRecognizers != null), + super(key: key); + + final TizenViewController controller; + final PlatformViewHitTestBehavior hitTestBehavior; + final Set> gestureRecognizers; + final int textureId; + + @override + RenderObject createRenderObject(BuildContext context) => RenderTizenView( + viewController: controller, + hitTestBehavior: hitTestBehavior, + gestureRecognizers: gestureRecognizers, + ); + + @override + void updateRenderObject(BuildContext context, RenderTizenView renderObject) { + renderObject.viewController = controller; + renderObject.hitTestBehavior = hitTestBehavior; + renderObject.updateGestureRecognizers(gestureRecognizers); + } +} + +class _TizenWebViewState extends State { + int _id; + TizenViewController _controller; + TextDirection _layoutDirection; + bool _initialized = false; + FocusNode _focusNode; + int _textureId; + int get textureId => _textureId; + + static final Set> _emptyRecognizersSet = + >{}; + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: _focusNode, + onFocusChange: _onFocusChange, + child: _TizenPlatformTextureView( + controller: _controller, + hitTestBehavior: widget.hitTestBehavior, + gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet, + textureId: textureId), + ); + } + + void _initializeOnce() { + if (_initialized) { + return; + } + _initialized = true; + _createNewTizenWebView(); + _focusNode = FocusNode(debugLabel: 'TizenWebView(id: $_id)'); + } + + @override + void didChangeDependencies() { + print('_TizenWebViewState::didChangeDependencies()'); + super.didChangeDependencies(); + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = + _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + _initializeOnce(); + if (didChangeLayoutDirection) { + _controller.setLayoutDirection(_layoutDirection); + } + } + + @override + void didUpdateWidget(TizenView oldWidget) { + print('_TizenWebViewState::didUpdateWidget()'); + super.didUpdateWidget(oldWidget); + + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = + _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + if (widget.viewType != oldWidget.viewType) { + _controller.dispose(); + _createNewTizenWebView(); + return; + } + + if (didChangeLayoutDirection) { + _controller.setLayoutDirection(_layoutDirection); + } + } + + TextDirection _findLayoutDirection() { + assert( + widget.layoutDirection != null || debugCheckHasDirectionality(context)); + return widget.layoutDirection ?? Directionality.of(context); + } + + @override + void dispose() { + print('_TizenWebViewState::dispose()'); + _controller.dispose(); + super.dispose(); + } + + void _createNewTizenWebView() { + _id = platformViewsRegistry.getNextPlatformViewId(); + _controller = PlatformViewsServiceTizen.initTizenView( + id: _id, + viewType: widget.viewType, + layoutDirection: _layoutDirection, + creationParams: widget.creationParams, + creationParamsCodec: widget.creationParamsCodec, + onFocus: () { + print('_TizenWebViewState::_createNewTizenWebView() - onFocus()'); + _focusNode.requestFocus(); + }, + ); + if (widget.onPlatformViewCreated != null) { + _controller + .addOnPlatformViewCreatedListener(widget.onPlatformViewCreated); + } + } + + void _onFocusChange(bool isFocused) { + print('_TizenWebViewState::_onFocusChange(isFocused:$isFocused)'); + if (!_controller.isCreated) { + return; + } + + if (!isFocused) { + _controller.clearFocus().catchError((dynamic e) { + if (e is MissingPluginException) { + return; + } + }); + return; + } + SystemChannels.textInput + .invokeMethod( + 'TextInput.setPlatformViewClient', + _id, + ) + .catchError((dynamic e) { + if (e is MissingPluginException) { + return; + } + }); + } +} + +class TizenWebView implements WebViewPlatform { + @override + Widget build({ + BuildContext context, + CreationParams creationParams, + @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + 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)); + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: TextDirection.rtl, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} + +bool _factoryTypesSetEquals(Set> a, Set> b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return setEquals(_factoriesTypeSet(a), _factoriesTypeSet(b)); +} + +Set _factoriesTypeSet(Set> factories) { + return factories.map((Factory factory) => factory.type).toSet(); +} + +/// The Mixin handling the pointer events and gestures of a platform view render box. +mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { + /// How to behave during hit testing. + // Changing _hitTestBehavior might affect which objects are considered hovered over. + set hitTestBehavior(PlatformViewHitTestBehavior value) { + if (value != _hitTestBehavior) { + _hitTestBehavior = value; + if (owner != null) { + markNeedsPaint(); + } + } + } + + PlatformViewHitTestBehavior _hitTestBehavior; + + _HandlePointerEvent _handlePointerEvent; + + /// {@macro flutter.rendering.platformView.updateGestureRecognizers} + /// + /// Any active gesture arena the `PlatformView` participates in is rejected when the + /// set of gesture recognizers is changed. + void _updateGestureRecognizersWithCallBack( + Set> gestureRecognizers, + _HandlePointerEvent handlePointerEvent) { + assert(gestureRecognizers != null); + assert( + _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, + 'There were multiple gesture recognizer factories for the same type, there must only be a single ' + 'gesture recognizer factory for each gesture recognizer type.', + ); + if (_factoryTypesSetEquals( + gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { + return; + } + _gestureRecognizer?.dispose(); + _gestureRecognizer = + _PlatformViewGestureRecognizer(handlePointerEvent, gestureRecognizers); + _handlePointerEvent = handlePointerEvent; + } + + _PlatformViewGestureRecognizer _gestureRecognizer; + + @override + bool hitTest(BoxHitTestResult result, {Offset position}) { + if (_hitTestBehavior == PlatformViewHitTestBehavior.transparent || + !size.contains(position)) { + return false; + } + result.add(BoxHitTestEntry(this, position)); + return _hitTestBehavior == PlatformViewHitTestBehavior.opaque; + } + + @override + bool hitTestSelf(Offset position) => + _hitTestBehavior != PlatformViewHitTestBehavior.transparent; + + @override + PointerEnterEventListener get onEnter => null; + + @override + PointerExitEventListener get onExit => null; + + @override + MouseCursor get cursor => MouseCursor.uncontrolled; + + @override + bool get validForMouseTracker => true; + + @override + void handleEvent(PointerEvent event, HitTestEntry entry) { + if (event is PointerDownEvent) { + _gestureRecognizer.addPointer(event); + } + if (event is PointerHoverEvent) { + _handlePointerEvent?.call(event); + } + } + + @override + void detach() { + _gestureRecognizer.reset(); + super.detach(); + } +} diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml new file mode 100644 index 000000000..21f2a8903 --- /dev/null +++ b/packages/webview_flutter/pubspec.yaml @@ -0,0 +1,24 @@ +name: webview_flutter_tizen +description: Tizen implementation of the webview plugin +homepage: https://github.com/flutter-tizen/plugins +version: 1.0.0 + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter: ^1.0.6 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + tizen: + pluginClass: WebviewFlutterTizenPlugin + fileName: webview_flutter_tizen_plugin.h 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/lwe/LWEWebView.h b/packages/webview_flutter/tizen/inc/lwe/LWEWebView.h new file mode 100644 index 000000000..4457870d5 --- /dev/null +++ b/packages/webview_flutter/tizen/inc/lwe/LWEWebView.h @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2018-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#ifndef __LWEWebView__ +#define __LWEWebView__ + +#ifndef LWE_EXPORT +#ifdef _MSC_VER +#define LWE_EXPORT __declspec(dllexport) +#else +#define LWE_EXPORT __attribute__((visibility("default"))) +#endif +#endif + +#include "PlatformIntegrationData.h" + +#include +#include +#include +#include + +namespace LWE { + +class LWE_EXPORT LWE { +public: + // You must call Initialize function before using WebContainer or WebView + static void Initialize(const char* localStorageDataFilePath, + const char* cookieStoreDataFilePath, + const char* httpCacheDataDirectorypath); + static bool IsInitialized(); + static void Finalize(); +}; + +#define LWE_DEFAULT_FONT_SIZE 16 +#define LWE_MIN_FONT_SIZE 1 +#define LWE_MAX_FONT_SIZE 72 + +enum class WebSecurityMode; + +class LWE_EXPORT CookieManager { +public: + std::string GetCookie(std::string url); + bool HasCookies(); + void ClearCookies(); + + static CookieManager* GetInstance(); + static void Destroy(); + +private: + CookieManager(); + ~CookieManager(); +}; + +class LWE_EXPORT Settings { +public: + Settings(const std::string& defaultUA, const std::string& ua); + std::string GetDefaultUserAgent() const; + std::string GetUserAgentString() const; + std::string GetProxyURL() const; + int GetCacheMode() const; + TTSMode GetTTSMode() const; + WebSecurityMode GetWebSecurityMode() const; + IdleModeJob GetIdleModeJob() const; + uint32_t GetIdleModeCheckIntervalInMS() const; + void GetBaseBackgroundColor(unsigned char& r, unsigned char& g, + unsigned char& b, unsigned char& a) const; + void GetBaseForegroundColor(unsigned char& r, unsigned char& g, + unsigned char& b, unsigned char& a) const; + bool NeedsDownloadWebFontsEarly() const; + uint32_t NeedsDownScaleImageResourceLargerThan() const; + void SetUserAgentString(const std::string& ua); + void SetCacheMode(int mode); + void SetProxyURL(const std::string& proxyURL); + void setDefaultFontSize(int size); + void SetTTSMode(TTSMode value); + void SetBaseBackgroundColor(unsigned char r, unsigned char g, + unsigned char b, unsigned char a); + void SetBaseForegroundColor(unsigned char r, unsigned char g, + unsigned char b, unsigned char a); + void SetWebSecurityMode(WebSecurityMode value); + void SetIdleModeJob(IdleModeJob j); + void SetIdleModeCheckIntervalInMS(uint32_t intervalInMS); + void SetNeedsDownloadWebFontsEarly(bool b); + void SetNeedsDownScaleImageResourceLargerThan( + uint32_t demention); // Experimental + +private: + std::string m_defaultUserAgent; + std::string m_userAgent; + std::string m_proxyURL; + int m_cacheMode; + uint32_t m_defaultFontSize; + TTSMode m_ttsMode; + unsigned char m_bgR, m_bgG, m_bgB, m_bgA; + unsigned char m_fgR, m_fgG, m_fgB, m_fgA; + WebSecurityMode m_webSecurityMode; + IdleModeJob m_idleModeJob; // default value is IdleModeJob::IdleModeFull + uint32_t m_idleModeCheckIntervalInMS; // default value is 3000(ms) + bool m_needsDownloadWebFontsEarly; + uint32_t m_needsDownScaleImageResourceLargerThan; +}; + +class LWE_EXPORT ResourceError { +public: + ResourceError(int code, const std::string& description, + const std::string& url); + int GetErrorCode(); + std::string GetDescription(); + std::string GetUrl(); + +private: + int m_errorCode; + std::string m_description; + std::string m_url; +}; + +class LWE_EXPORT WebContainer { +private: + // use Destroy function instead of using delete operator + ~WebContainer() + { + } + +public: + // Function set for render to buffer + static WebContainer* Create(unsigned width, unsigned height, + float devicePixelRatio, + const char* defaultFontName, const char* locale, + const char* timezoneID); + struct RenderInfo { + void* updatedBufferAddress; + size_t bufferStride; + }; + + struct RenderResult { + size_t updatedX; + size_t updatedY; + size_t updatedWidth; + size_t updatedHeight; + + void* updatedBufferAddress; + size_t bufferImageWidth; + size_t bufferImageHeight; + }; + void RegisterPreRenderingHandler(const std::function& cb); + void RegisterOnRenderedHandler( + const std::function& cb); + // <--- end of function set for render to buffer + + // Function set for render with OpenGL + static WebContainer* CreateGL( + unsigned width, unsigned height, + const std::function& onGLMakeCurrent, + const std::function& + onGLSwapBuffers, + float devicePixelRatio, const char* defaultFontName, const char* locale, + const char* timezoneID); + + struct ExternalImageInfo { + void* imageAddress; + }; + + static WebContainer* CreateGLWithPlatformImage( + unsigned width, unsigned height, + const std::function& onGLMakeCurrent, + const std::function& + onGLSwapBuffers, + const std::function& prepareImageCb, + const std::function& flushCb, + float devicePixelRatio, const char* defaultFontName, const char* locale, + const char* timezoneID); + // <--- end of function set for render with OpenGL + + // Function set for headless + static WebContainer* CreateHeadless(unsigned width, unsigned height, + float devicePixelRatio, + const char* defaultFontName, + const char* locale, + const char* timezoneID); + // <--- end of function set for headless + + void AddIdleCallback(void (*callback)(void*), void* data); + size_t AddTimeout(void (*callback)(void*), void* data, size_t timeoutInMS); + void ClearTimeout(size_t handle); + + void RegisterCanRenderingHandler( + const std::function& cb); + + Settings GetSettings(); + void LoadURL(const std::string& url); + std::string GetURL(); + void LoadData(const std::string& data); + void Reload(); + void StopLoading(); + void GoBack(); + void GoForward(); + bool CanGoBack(); + bool CanGoForward(); + void AddJavaScriptInterface( + const std::string& exposedObjectName, const std::string& jsFunctionName, + std::function cb); + std::string EvaluateJavaScript(const std::string& script); + void EvaluateJavaScript(const std::string& script, + std::function cb); + void ClearHistory(); + void Destroy(); + void Pause(); + void Resume(); + + void ResizeTo(size_t width, size_t height); + + void Focus(); + void Blur(); + + void SetSettings(const Settings& setttings); + void RemoveJavascriptInterface(const std::string& exposedObjectName, + const std::string& jsFunctionName); + void ClearCache(); + + void RegisterOnReceivedErrorHandler( + const std::function& cb); + void RegisterOnPageParsedHandler( + std::function cb); + void RegisterOnPageLoadedHandler( + std::function cb); + void RegisterOnPageStartedHandler( + const std::function& cb); + void RegisterOnLoadResourceHandler( + const std::function& cb); + void RegisterShouldOverrideUrlLoadingHandler( + const std::function& cb); + void RegisterOnProgressChangedHandler( + const std::function& cb); + void RegisterOnDownloadStartHandler( + const std::function& cb); + + void RegisterShowDropdownMenuHandler( + const std::function*, + int)>& cb); + void RegisterShowAlertHandler( + const std::function& cb); + + void RegisterCustomFileResourceRequestHandlers( + std::function resolveFilePathCallback, + std::function fileOpenCallback, + std::function + fileReadCallback, + std::function fileLengthCallback, + std::function fileCloseCallback); + + void CallHandler(const std::string& handler, void* param); + + void SetUserAgentString(const std::string& userAgent); + std::string GetUserAgentString(); + void SetCacheMode(int mode); + int GetCacheMode(); + void SetDefaultFontSize(uint32_t size); + uint32_t GetDefaultFontSize(); + + void DispatchMouseMoveEvent(MouseButtonValue button, + MouseButtonsValue buttons, double x, double y); + void DispatchMouseDownEvent(MouseButtonValue button, + MouseButtonsValue buttons, double x, double y); + void DispatchMouseUpEvent(MouseButtonValue button, + MouseButtonsValue buttons, double x, double y); + void DispatchMouseWheelEvent(double x, double y, int delta); + void DispatchKeyDownEvent(KeyValue keyCode); + void DispatchKeyPressEvent(KeyValue keyCode); + void DispatchKeyUpEvent(KeyValue keyCode); + + void DispatchCompositionStartEvent( + const std::string& soFarCompositiedString); + void DispatchCompositionUpdateEvent( + const std::string& soFarCompositiedString); + void DispatchCompositionEndEvent(const std::string& soFarCompositiedString); + void RegisterOnShowSoftwareKeyboardIfPossibleHandler( + const std::function& cb); + void RegisterOnHideSoftwareKeyboardIfPossibleHandler( + const std::function& cb); + + void SetUserData(const std::string& key, void* data); + void* GetUserData(const std::string& key); + + std::string GetTitle(); + void ScrollTo(int x, int y); + void ScrollBy(int x, int y); + int GetScrollX(); + int GetScrollY(); + + size_t Width(); + size_t Height(); + + // You can control rendering flow through this function + // If you got callback, you must call `doRenderingFunction` after + void RegisterSetNeedsRenderingCallback( + const std::function& + doRenderingFunction)>& cb); + +protected: + WebContainer(void* webView); + +private: + void* m_impl; +}; + +class LWE_EXPORT WebView { +protected: + // use Destroy function instead of using delete operator + virtual ~WebView() + { + } + +public: + static WebView* Create(void* win, unsigned x, unsigned y, unsigned width, + unsigned height, float devicePixelRatio, + const char* defaultFontName, const char* locale, + const char* timezoneID); + + virtual void Destroy(); + + Settings GetSettings(); + virtual void LoadURL(const std::string& url); + std::string GetURL(); + void LoadData(const std::string& data); + void Reload(); + void StopLoading(); + void GoBack(); + void GoForward(); + bool CanGoBack(); + bool CanGoForward(); + void Pause(); + void Resume(); + void AddJavaScriptInterface( + const std::string& exposedObjectName, const std::string& jsFunctionName, + std::function cb); + std::string EvaluateJavaScript(const std::string& script); + void EvaluateJavaScript(const std::string& script, + std::function cb); + void ClearHistory(); + void SetSettings(const Settings& setttings); + void RemoveJavascriptInterface(const std::string& exposedObjectName, + const std::string& jsFunctionName); + void ClearCache(); + void RegisterOnReceivedErrorHandler( + std::function cb); + void RegisterOnPageParsedHandler( + std::function cb); + void RegisterOnPageLoadedHandler( + std::function cb); + void RegisterOnPageStartedHandler( + std::function cb); + void RegisterOnLoadResourceHandler( + std::function cb); + + void RegisterCustomFileResourceRequestHandlers( + std::function resolveFilePathCallback, + std::function fileOpenCallback, + std::function + fileReadCallback, + std::function fileLengthCallback, + std::function fileCloseCallback); + + void SetUserData(const std::string& key, void* data); + void* GetUserData(const std::string& key); + + std::string GetTitle(); + void ScrollTo(int x, int y); + void ScrollBy(int x, int y); + int GetScrollX(); + int GetScrollY(); + + virtual void* Unwrap() + { + // Some platform returns associated native handle ex) Evas_Object* + return nullptr; + } + virtual void Focus(); + virtual void Blur(); + +protected: + WebView(void* impl) + : m_impl(impl) + { + } + + virtual WebContainer* FetchWebContainer() = 0; + + void* m_impl; +}; + +} // namespace LWE + +#endif diff --git a/packages/webview_flutter/tizen/inc/lwe/PlatformIntegrationData.h b/packages/webview_flutter/tizen/inc/lwe/PlatformIntegrationData.h new file mode 100644 index 000000000..7d9b10f0e --- /dev/null +++ b/packages/webview_flutter/tizen/inc/lwe/PlatformIntegrationData.h @@ -0,0 +1,275 @@ +#ifndef __PlatformIntegrationData__ +#define __PlatformIntegrationData__ + +namespace LWE { +// This table has same chars with +// ASCII printable char(32-126) +enum KeyValue { + UnidentifiedKey, + AltLeftKey, + AltRightKey, + ControlLeftKey, + ControlRightKey, + CapsLockKey, + FnKey, + FnLockKey, + HyperKey, + MetaKey, + NumLockKey, + ScrollLockKey, + ShiftLeftKey, + ShiftRightKey, + SuperKey, + SymbolKey, + SymbolLockKey, + EnterKey, + TabKey, + ArrowDownKey, + ArrowUpKey, + ArrowLeftKey, + ArrowRightKey, + EndKey, + HomeKey, + PageDownKey, + PageUpKey, + BackspaceKey, + DeleteKey, + InsertKey, + ContextMenuKey, + EscapeKey, + SpaceKey = 32, + ExclamationMarkKey = 33, + DoubleQuoteMarkKey = 34, + SharpMarkKey = 35, + DollarMarkKey = 36, + PercentMarkKey = 37, + AmpersandMarkKey = 38, + SingleQuoteMarkKey = 39, + LeftParenthesisMarkKey = 40, + RightParenthesisMarkKey = 41, + AsteriskMarkKey = 42, + PlusMarkKey = 43, + CommaMarkKey = 44, + MinusMarkKey = 45, + PeriodKey = 46, + SlashKey = 47, + Digit0Key = 48, + Digit1Key, + Digit2Key, + Digit3Key, + Digit4Key, + Digit5Key, + Digit6Key, + Digit7Key, + Digit8Key, + Digit9Key, + ColonMarkKey = 58, + SemiColonMarkKey = 59, + LessThanMarkKey = 60, + EqualitySignKey = 61, + GreaterThanSignKey = 62, + QuestionMarkKey = 63, + AtMarkKey = 64, + AKey = 65, + BKey, + CKey, + DKey, + EKey, + FKey, + GKey, + HKey, + IKey, + JKey, + KKey, + LKey, + MKey, + NKey, + OKey, + PKey, + QKey, + RKey, + SKey, + TKey, + UKey, + VKey, + WKey, + XKey, + YKey, + ZKey, + LeftSquareBracketKey = 91, + BackslashKey = 92, + RightSquareBracketKey = 93, + CaretMarkKey = 94, + UnderScoreMarkKey = 95, + AccentMarkKey = 96, + LowerAKey = 97, + LowerBKey, + LowerCKey, + LowerDKey, + LowerEKey, + LowerFKey, + LowerGKey, + LowerHKey, + LowerIKey, + LowerJKey, + LowerKKey, + LowerLKey, + LowerMKey, + LowerNKey, + LowerOKey, + LowerPKey, + LowerQKey, + LowerRKey, + LowerSKey, + LowerTKey, + LowerUKey, + LowerVKey, + LowerWKey, + LowerXKey, + LowerYKey, + LowerZKey, + LeftCurlyBracketMarkKey = 123, + VerticalBarMarkKey = 124, + RightCurlyBracketMarkKey = 125, + TildeMarkKey = 126, + F1Key, + F2Key, + F3Key, + F4Key, + F5Key, + F6Key, + F7Key, + F8Key, + F9Key, + F10Key, + F11Key, + F12Key, + F13Key, + F14Key, + F15Key, + F16Key, + F17Key, + F18Key, + F19Key, + F20Key, + TVPreviousChannel, + TVChannelDownKey, + TVChannelUpKey, + TVVolumeUpKey, + TVVolumeDownKey, + TVMuteKey, + TVChannelList, + TVChannelGuide, + TVSimpleMenu, + TVReturnKey, + TVExitKey, + TVInfoKey, + TVRedKey, + TVGreenKey, + TVYellowKey, + TVBlueKey, + TVEManual, + TVExtraApp, + TVSearch, + TVPictureSize, + TVSleep, + TVCaption, + TVMore, + TVBTVoice, + TVColor, + TVPlayBack, + TVMenuKey, + MediaFastForwardKey, + MediaPauseKey, + MediaPlayKey, + MediaPlayPauseKey, + MediaRecordKey, + MediaRewindKey, + MediaStopKey, + MediaTrackNextKey, + MediaTrackPreviousKey, + TVKey, + TV3DModeKey, + TVAntennaCableKey, + TVAudioDescriptionKey, + TVAudioDescriptionMixDownKey, + TVAudioDescriptionMixUpKey, + TVContentsMenuKey, + TVDataServiceKey, + TVInputKey, + TVInputComponent1Key, + TVInputComponent2Key, + TVInputComposite1Key, + TVInputComposite2Key, + TVInputHDMI1Key, + TVInputHDMI2Key, + TVInputHDMI3Key, + TVInputHDMI4Key, + TVInputVGA1Key, + TVMediaContextKey, + TVNetworkKey, + TVNumberEntryKey, + TVPowerKey, + TVRadioServiceKey, + TVSatelliteKey, + TVSatelliteBSKey, + TVSatelliteCSKey, + TVSatelliteToggleKey, + TVTerrestrialAnalogKey, + TVTerrestrialDigitalKey, + TVTimerKey, + TVHomeKey, + MediaAppsKey, + MediaAudioTrackKey, + MediaLastKey, + MediaSkipBackwardKey, + MediaSkipForwardKey, + MediaStepBackwardKey, + MediaStepForwardKey, + MediaTopMenuKey, + BrowserBackKey, + BrowserFavoritesKey, + BrowserForwardKey, + BrowserHomeKey, + BrowserRefreshKey, + BrowserSearchKey, + BrowserStopKey, +}; + +enum MouseButtonValue { + NoButton = 0, + LeftButton = 0, + MiddleButton = 1, + RightButton = 2 +}; + +enum MouseButtonsValue { + NoButtonDown = 0, + LeftButtonDown = 1, + RightButtonDown = 1 << 1, + MiddleButtonDown = 1 << 2, +}; + +enum TTSMode { + Default = 0, + Forced = 1, +}; + +enum class WebSecurityMode { Enable = 0, Disable = 1 }; + +enum class IdleModeJob { + ClearDrawnBuffers = 1, + ForceGC = 1 << 1, // it also includes calling malloc_trim(0) + DropDecodedImageBuffer = 1 << 2, + + IdleModeFull = ClearDrawnBuffers | ForceGC | DropDecodedImageBuffer, + IdleModeMiddle = ForceGC, + IdleModeNone = 0, + + IdleModeDefault = IdleModeFull +}; + +constexpr int IdleModeCheckDefaultIntervalInMS{ 3000 }; +} // namespace LWE + +#endif 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/lib/liblightweight-web-engine.flutter.so b/packages/webview_flutter/tizen/lib/liblightweight-web-engine.flutter.so new file mode 100755 index 000000000..3d1178bc0 Binary files /dev/null and b/packages/webview_flutter/tizen/lib/liblightweight-web-engine.flutter.so differ diff --git a/packages/webview_flutter/tizen/project_def.prop b/packages/webview_flutter/tizen/project_def.prop new file mode 100644 index 000000000..496f78e97 --- /dev/null +++ b/packages/webview_flutter/tizen/project_def.prop @@ -0,0 +1,30 @@ +# 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/webview_flutter_tizen_plugin.cc src/webview.cc src/webview_factory.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = TIZEN_DEPRECATION DEPRECATION_WARNING FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Compiler/linker flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = -c -fmessage-length=0 +USER_LFLAGS = -llightweight-web-engine.flutter -Wl,-rpath='$$ORIGIN' + +# Libraries and objects +USER_LIB_DIRS = lib +USER_LIBS = lightweight-web-engine.flutter +USER_OBJS = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/webview_flutter/tizen/src/log.h b/packages/webview_flutter/tizen/src/log.h new file mode 100644 index 000000000..a356ed39e --- /dev/null +++ b/packages/webview_flutter/tizen/src/log.h @@ -0,0 +1,20 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "WebviewFlutterTizenPlugin" + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __FILE__, __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..138d66348 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview.cc @@ -0,0 +1,880 @@ + +#include "webview.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "log.h" +#include "lwe/LWEWebView.h" +#include "lwe/PlatformIntegrationData.h" +#include "webview_factory.h" + +#define LWE_EXPORT +extern "C" size_t LWE_EXPORT createWebViewInstance( + unsigned x, unsigned y, unsigned width, unsigned height, + float devicePixelRatio, const char* defaultFontName, const char* locale, + const char* timezoneID, + const std::function<::LWE::WebContainer::ExternalImageInfo(void)>& + prepareImageCb, + const std::function& + renderedCb); + +template +class NavigationRequestResult : public flutter::MethodResult { + public: + NavigationRequestResult(std::string url, WebView* webview) + : url_(url), webview_(webview) {} + + void SuccessInternal(const T* should_load) override { + if (std::holds_alternative(*should_load)) { + if (std::get(*should_load)) { + LoadUrl(); + } + } + } + + void ErrorInternal(const std::string& error_code, + const std::string& error_message, + const T* error_details) override { + throw std::invalid_argument("navigationRequest calls must succeed [code:" + + error_code + "][msg:" + error_message + "]"); + } + + void NotImplementedInternal() override { + throw std::invalid_argument( + "navigationRequest must be implemented by the webview method channel"); + } + + private: + void LoadUrl() { + if (webview_ && webview_->GetWebViewInstance()) { + webview_->GetWebViewInstance()->LoadURL(url_); + } + } + + std::string url_; + WebView* webview_; +}; + +enum RequestErrorType { + NoError, + UnknownError, + HostLookupError, + UnsupportedAuthSchemeError, + AuthenticationError, + ProxyAuthenticationError, + ConnectError, + IOError, + TimeoutError, + RedirectLoopError, + UnsupportedSchemeError, + FailedSSLHandshakeError, + BadURLError, + FileError, + FileNotFoundError, + TooManyRequestError, +}; + +static std::string ErrorCodeToString(int error_code) { + switch (error_code) { + case RequestErrorType::AuthenticationError: + return "authentication"; + case RequestErrorType::BadURLError: + return "badUrl"; + case RequestErrorType::ConnectError: + return "connect"; + case RequestErrorType::FailedSSLHandshakeError: + return "failedSslHandshake"; + case RequestErrorType::FileError: + return "file"; + case RequestErrorType::FileNotFoundError: + return "fileNotFound"; + case RequestErrorType::HostLookupError: + return "hostLookup"; + case RequestErrorType::IOError: + return "io"; + case RequestErrorType::ProxyAuthenticationError: + return "proxyAuthentication"; + case RequestErrorType::RedirectLoopError: + return "redirectLoop"; + case RequestErrorType::TimeoutError: + return "timeout"; + case RequestErrorType::TooManyRequestError: + return "tooManyRequests"; + case RequestErrorType::UnknownError: + return "unknown"; + case RequestErrorType::UnsupportedAuthSchemeError: + return "unsupportedAuthScheme"; + case RequestErrorType::UnsupportedSchemeError: + return "unsupportedScheme"; + } + + std::string message = + "Could not find a string for errorCode: " + std::to_string(error_code); + throw std::invalid_argument(message); +} + +std::string ExtractStringFromMap(const flutter::EncodableValue& arguments, + const char* key) { + if (std::holds_alternative(arguments)) { + flutter::EncodableMap values = std::get(arguments); + flutter::EncodableValue value = values[flutter::EncodableValue(key)]; + if (std::holds_alternative(value)) + return std::get(value); + } + return std::string(); +} +int ExtractIntFromMap(const flutter::EncodableValue& arguments, + const char* key) { + if (std::holds_alternative(arguments)) { + flutter::EncodableMap values = std::get(arguments); + flutter::EncodableValue value = values[flutter::EncodableValue(key)]; + if (std::holds_alternative(value)) return std::get(value); + } + return -1; +} +double ExtractDoubleFromMap(const flutter::EncodableValue& arguments, + const char* key) { + if (std::holds_alternative(arguments)) { + flutter::EncodableMap values = std::get(arguments); + flutter::EncodableValue value = values[flutter::EncodableValue(key)]; + if (std::holds_alternative(value)) return std::get(value); + } + return -1; +} + +WebView::WebView(flutter::PluginRegistrar* registrar, int viewId, + FlutterTextureRegistrar* texture_registrar, double width, + double height, flutter::EncodableMap& params) + : PlatformView(registrar, viewId), + texture_registrar_(texture_registrar), + webview_instance_(nullptr), + width_(width), + height_(height), + tbm_surface_(nullptr), + is_mouse_lbutton_down_(false), + has_navigation_delegate_(false), + context_(nullptr) { + SetTextureId(FlutterRegisterExternalTexture(texture_registrar_)); + 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; + auto initial_url = params[flutter::EncodableValue("initialUrl")]; + if (std::holds_alternative(initial_url)) { + url = std::get(initial_url); + } else { + url = "about:blank"; + } + + auto settings = params[flutter::EncodableValue("settings")]; + if (std::holds_alternative(settings)) { + auto settingList = std::get(settings); + if (settingList.size() > 0) { + ApplySettings(settingList); + } + } + + auto names = params[flutter::EncodableValue("javascriptChannelNames")]; + if (std::holds_alternative(names)) { + auto name_list = std::get(names); + for (size_t i = 0; i < name_list.size(); i++) { + if (std::holds_alternative(name_list[i])) { + RegisterJavaScriptChannelName(std::get(name_list[i])); + } + } + } + + // TODO: Not implemented + // auto media = params[flutter::EncodableValue("autoMediaPlaybackPolicy")]; + + auto user_agent = params[flutter::EncodableValue("userAgent")]; + if (std::holds_alternative(user_agent)) { + auto settings = webview_instance_->GetSettings(); + settings.SetUserAgentString(std::get(user_agent)); + webview_instance_->SetSettings(settings); + } + + webview_instance_->RegisterOnPageStartedHandler( + [this](LWE::WebContainer* container, const std::string& url) { + LOG_DEBUG("RegisterOnPageStartedHandler(url: %s)\n", url.c_str()); + flutter::EncodableMap map; + map.insert( + std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + auto args = std::make_unique(map); + channel_->InvokeMethod("onPageStarted", std::move(args)); + }); + webview_instance_->RegisterOnPageLoadedHandler( + [this](LWE::WebContainer* container, const std::string& url) { + LOG_DEBUG("RegisterOnPageLoadedHandler(url: %s)(title:%s)\n", + url.c_str(), container->GetTitle().c_str()); + flutter::EncodableMap map; + map.insert( + std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + auto args = std::make_unique(map); + channel_->InvokeMethod("onPageFinished", std::move(args)); + }); + webview_instance_->RegisterOnReceivedErrorHandler( + [this](LWE::WebContainer* container, LWE::ResourceError e) { + flutter::EncodableMap map; + map.insert( + std::make_pair( + flutter::EncodableValue("errorCode"), + flutter::EncodableValue(e.GetErrorCode()))); + map.insert( + std::make_pair( + flutter::EncodableValue("description"), + flutter::EncodableValue(e.GetDescription()))); + map.insert( + std::make_pair( + flutter::EncodableValue("errorType"), + flutter::EncodableValue(ErrorCodeToString(e.GetErrorCode())))); + map.insert( + std::make_pair( + flutter::EncodableValue("failingUrl"), + flutter::EncodableValue(e.GetUrl()))); + auto args = std::make_unique(map); + channel_->InvokeMethod("onWebResourceError", std::move(args)); + }); + + webview_instance_->RegisterShouldOverrideUrlLoadingHandler( + [this](LWE::WebContainer* view, const std::string& url) -> bool { + if (!has_navigation_delegate_) { + return false; + } + flutter::EncodableMap map; + map.insert( + std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + map.insert( + std::make_pair( + flutter::EncodableValue("isForMainFrame"), + flutter::EncodableValue(true))); + auto args = std::make_unique(map); + auto on_result = + std::make_unique>( + url, this); + channel_->InvokeMethod("navigationRequest", std::move(args), + std::move(on_result)); + + return true; + }); + + webview_instance_->LoadURL(url); +} + +void WebView::ApplySettings(flutter::EncodableMap settings) { + for (auto const& [key, val] : settings) { + if (std::holds_alternative(key)) { + std::string k = std::get(key); + if ("jsMode" == k) { + // NOTE: Not supported by Lightweight Web Engine (LWE) on Tizen. + } else if ("hasNavigationDelegate" == k) { + if (std::holds_alternative(val)) { + has_navigation_delegate_ = std::get(val); + } + } else if ("debuggingEnabled" == k) { + // NOTE: Not supported by LWE on Tizen. + } else if ("gestureNavigationEnabled" == k) { + // NOTE: Not supported by LWE on Tizen. + } else if ("userAgent" == k) { + if (std::holds_alternative(val)) { + auto settings = webview_instance_->GetSettings(); + settings.SetUserAgentString(std::get(val)); + webview_instance_->SetSettings(settings); + } + } else { + throw std::invalid_argument("Unknown WebView setting: " + k); + } + } + } +} + +/** + * 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("RegisterJavaScriptChannelName(channelName: %s)\n", name.c_str()); + + std::function cb = + [this, name](const std::string& message) -> std::string { + LOG_DEBUG("Invoke JavaScriptChannel(message: %s)\n", message.c_str()); + flutter::EncodableMap map; + map.insert(std::make_pair( + flutter::EncodableValue("channel"), flutter::EncodableValue(name))); + map.insert(std::make_pair( + flutter::EncodableValue("message"), flutter::EncodableValue(message))); + + std::unique_ptr args = + std::make_unique(map); + channel_->InvokeMethod("javascriptChannelMessage", std::move(args)); + return "success"; + }; + + webview_instance_->AddJavaScriptInterface(name, "postMessage", cb); +} + +WebView::~WebView() { Dispose(); } + +std::string WebView::GetChannelName() { + return "plugins.flutter.io/webview_" + std::to_string(GetViewId()); +} + +void WebView::Dispose() { + FlutterUnregisterExternalTexture(texture_registrar_, GetTextureId()); + + if (webview_instance_) { + webview_instance_->Destroy(); + webview_instance_ = nullptr; + } +} + +void WebView::Resize(double width, double height) { + LOG_DEBUG("WebView::Resize width: %f height: %f \n", width, height); + // NOTE: Not supported by LWE on Tizen. +} + +void WebView::Touch(int type, int button, double x, double y, double dx, + double dy) { + if (type == 0) { // down event + webview_instance_->DispatchMouseDownEvent( + LWE::MouseButtonValue::LeftButton, + LWE::MouseButtonsValue::LeftButtonDown, x, y); + is_mouse_lbutton_down_ = true; + } else if (type == 1) { // move event + webview_instance_->DispatchMouseMoveEvent( + is_mouse_lbutton_down_ ? LWE::MouseButtonValue::LeftButton + : LWE::MouseButtonValue::NoButton, + is_mouse_lbutton_down_ ? LWE::MouseButtonsValue::LeftButtonDown + : LWE::MouseButtonsValue::NoButtonDown, + x, y); + } else if (type == 2) { // up event + webview_instance_->DispatchMouseUpEvent( + LWE::MouseButtonValue::NoButton, LWE::MouseButtonsValue::NoButtonDown, + x, y); + is_mouse_lbutton_down_ = false; + } else { + // TODO: Not implemented + } +} + +static LWE::KeyValue EcoreEventKeyToKeyValue(const char* ecore_key_string, + bool is_shift_pressed) { + if (strcmp("Left", ecore_key_string) == 0) { + return LWE::KeyValue::ArrowLeftKey; + } else if (strcmp("Right", ecore_key_string) == 0) { + return LWE::KeyValue::ArrowRightKey; + } else if (strcmp("Up", ecore_key_string) == 0) { + return LWE::KeyValue::ArrowUpKey; + } else if (strcmp("Down", ecore_key_string) == 0) { + return LWE::KeyValue::ArrowDownKey; + } else if (strcmp("space", ecore_key_string) == 0) { + return LWE::KeyValue::SpaceKey; + } else if (strcmp("Return", ecore_key_string) == 0) { + return LWE::KeyValue::EnterKey; + } else if (strcmp("Tab", ecore_key_string) == 0) { + return LWE::KeyValue::TabKey; + } else if (strcmp("BackSpace", ecore_key_string) == 0) { + return LWE::KeyValue::BackspaceKey; + } else if (strcmp("Escape", ecore_key_string) == 0) { + return LWE::KeyValue::EscapeKey; + } else if (strcmp("Delete", ecore_key_string) == 0) { + return LWE::KeyValue::DeleteKey; + } else if (strcmp("at", ecore_key_string) == 0) { + return LWE::KeyValue::AtMarkKey; + } else if (strcmp("minus", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::UnderScoreMarkKey; + } else { + return LWE::KeyValue::MinusMarkKey; + } + } else if (strcmp("equal", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::PlusMarkKey; + } else { + return LWE::KeyValue::EqualitySignKey; + } + } else if (strcmp("bracketleft", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::LeftCurlyBracketMarkKey; + } else { + return LWE::KeyValue::LeftSquareBracketKey; + } + } else if (strcmp("bracketright", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::RightCurlyBracketMarkKey; + } else { + return LWE::KeyValue::RightSquareBracketKey; + } + } else if (strcmp("semicolon", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::ColonMarkKey; + } else { + return LWE::KeyValue::SemiColonMarkKey; + } + } else if (strcmp("apostrophe", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::DoubleQuoteMarkKey; + } else { + return LWE::KeyValue::SingleQuoteMarkKey; + } + } else if (strcmp("comma", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::LessThanMarkKey; + } else { + return LWE::KeyValue::CommaMarkKey; + } + } else if (strcmp("period", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::GreaterThanSignKey; + } else { + return LWE::KeyValue::PeriodKey; + } + } else if (strcmp("slash", ecore_key_string) == 0) { + if (is_shift_pressed) { + return LWE::KeyValue::QuestionMarkKey; + } else { + return LWE::KeyValue::SlashKey; + } + } else if (strlen(ecore_key_string) == 1) { + char ch = ecore_key_string[0]; + if (ch >= '0' && ch <= '9') { + if (is_shift_pressed) { + switch (ch) { + case '1': + return LWE::KeyValue::ExclamationMarkKey; + case '2': + return LWE::KeyValue::AtMarkKey; + case '3': + return LWE::KeyValue::SharpMarkKey; + case '4': + return LWE::KeyValue::DollarMarkKey; + case '5': + return LWE::KeyValue::PercentMarkKey; + case '6': + return LWE::KeyValue::CaretMarkKey; + case '7': + return LWE::KeyValue::AmpersandMarkKey; + case '8': + return LWE::KeyValue::AsteriskMarkKey; + case '9': + return LWE::KeyValue::LeftParenthesisMarkKey; + case '0': + return LWE::KeyValue::RightParenthesisMarkKey; + } + } + return (LWE::KeyValue)(LWE::KeyValue::Digit0Key + ch - '0'); + } else if (ch >= 'a' && ch <= 'z') { + return (LWE::KeyValue)(LWE::KeyValue::LowerAKey + ch - 'a'); + } else if (ch >= 'A' && ch <= 'Z') { + return (LWE::KeyValue)(LWE::KeyValue::AKey + ch - 'A'); + } + } else if (strcmp("XF86AudioRaiseVolume", ecore_key_string) == 0) { + return LWE::KeyValue::TVVolumeUpKey; + } else if (strcmp("XF86AudioLowerVolume", ecore_key_string) == 0) { + return LWE::KeyValue::TVVolumeDownKey; + } else if (strcmp("XF86AudioMute", ecore_key_string) == 0) { + return LWE::KeyValue::TVMuteKey; + } else if (strcmp("XF86RaiseChannel", ecore_key_string) == 0) { + return LWE::KeyValue::TVChannelUpKey; + } else if (strcmp("XF86LowerChannel", ecore_key_string) == 0) { + return LWE::KeyValue::TVChannelDownKey; + } else if (strcmp("XF86AudioRewind", ecore_key_string) == 0) { + return LWE::KeyValue::MediaTrackPreviousKey; + } else if (strcmp("XF86AudioNext", ecore_key_string) == 0) { + return LWE::KeyValue::MediaTrackNextKey; + } else if (strcmp("XF86AudioPause", ecore_key_string) == 0) { + return LWE::KeyValue::MediaPauseKey; + } else if (strcmp("XF86AudioRecord", ecore_key_string) == 0) { + return LWE::KeyValue::MediaRecordKey; + } else if (strcmp("XF86AudioPlay", ecore_key_string) == 0) { + return LWE::KeyValue::MediaPlayKey; + } else if (strcmp("XF86AudioStop", ecore_key_string) == 0) { + return LWE::KeyValue::MediaStopKey; + } else if (strcmp("XF86Info", ecore_key_string) == 0) { + return LWE::KeyValue::TVInfoKey; + } else if (strcmp("XF86Back", ecore_key_string) == 0) { + return LWE::KeyValue::TVReturnKey; + } else if (strcmp("XF86Red", ecore_key_string) == 0) { + return LWE::KeyValue::TVRedKey; + } else if (strcmp("XF86Green", ecore_key_string) == 0) { + return LWE::KeyValue::TVGreenKey; + } else if (strcmp("XF86Yellow", ecore_key_string) == 0) { + return LWE::KeyValue::TVYellowKey; + } else if (strcmp("XF86Blue", ecore_key_string) == 0) { + return LWE::KeyValue::TVBlueKey; + } else if (strcmp("XF86SysMenu", ecore_key_string) == 0) { + return LWE::KeyValue::TVMenuKey; + } else if (strcmp("XF86Home", ecore_key_string) == 0) { + return LWE::KeyValue::TVHomeKey; + } else if (strcmp("XF86Exit", ecore_key_string) == 0) { + return LWE::KeyValue::TVExitKey; + } else if (strcmp("XF86PreviousChannel", ecore_key_string) == 0) { + return LWE::KeyValue::TVPreviousChannel; + } else if (strcmp("XF86ChannelList", ecore_key_string) == 0) { + return LWE::KeyValue::TVChannelList; + } else if (strcmp("XF86ChannelGuide", ecore_key_string) == 0) { + return LWE::KeyValue::TVChannelGuide; + } else if (strcmp("XF86SimpleMenu", ecore_key_string) == 0) { + return LWE::KeyValue::TVSimpleMenu; + } else if (strcmp("XF86EManual", ecore_key_string) == 0) { + return LWE::KeyValue::TVEManual; + } else if (strcmp("XF86ExtraApp", ecore_key_string) == 0) { + return LWE::KeyValue::TVExtraApp; + } else if (strcmp("XF86Search", ecore_key_string) == 0) { + return LWE::KeyValue::TVSearch; + } else if (strcmp("XF86PictureSize", ecore_key_string) == 0) { + return LWE::KeyValue::TVPictureSize; + } else if (strcmp("XF86Sleep", ecore_key_string) == 0) { + return LWE::KeyValue::TVSleep; + } else if (strcmp("XF86Caption", ecore_key_string) == 0) { + return LWE::KeyValue::TVCaption; + } else if (strcmp("XF86More", ecore_key_string) == 0) { + return LWE::KeyValue::TVMore; + } else if (strcmp("XF86BTVoice", ecore_key_string) == 0) { + return LWE::KeyValue::TVBTVoice; + } else if (strcmp("XF86Color", ecore_key_string) == 0) { + return LWE::KeyValue::TVColor; + } else if (strcmp("XF86PlayBack", ecore_key_string) == 0) { + return LWE::KeyValue::TVPlayBack; + } + + LOG_DEBUG("WebViewEFL - unimplemented key %s\n", ecore_key_string); + return LWE::KeyValue::UnidentifiedKey; +} + +void WebView::DispatchKeyDownEvent(Ecore_Event_Key* key_event) { + std::string key_name = key_event->keyname; + LOG_DEBUG("ECORE_EVENT_KEY_DOWN [%s, %d]\n", key_name.data(), + (key_event->modifiers & 1) || (key_event->modifiers & 2)); + + if (!IsFocused()) { + LOG_DEBUG("ignore keydown because we dont have focus"); + return; + } + +#ifdef TV_PROFILE + if ((strncmp(key_name.data(), "XF86Back", 8) == 0)) { + key_name = "Escape"; + } +#endif + + if ((strcmp(key_name.data(), "XF86Exit") == 0) || + (strcmp(key_name.data(), "Select") == 0) || + (strcmp(key_name.data(), "Cancel") == 0)) { + if (strcmp(key_name.data(), "Select") == 0) { + webview_instance_->AddIdleCallback( + [](void* data) { + WebView* view = (WebView*)data; + LWE::WebContainer* self = view->GetWebViewInstance(); + LWE::KeyValue kv = LWE::KeyValue::EnterKey; + self->DispatchKeyDownEvent(kv); + self->DispatchKeyPressEvent(kv); + self->DispatchKeyUpEvent(kv); + view->HidePanel(); + }, + this); + } else { + webview_instance_->AddIdleCallback( + [](void* data) { + WebView* view = (WebView*)data; + view->HidePanel(); + }, + this); + } + } + + struct Param { + LWE::WebContainer* webview_instance; + LWE::KeyValue key_value; + }; + Param* p = new Param(); + p->webview_instance = webview_instance_; + p->key_value = EcoreEventKeyToKeyValue(key_name.data(), false); + + webview_instance_->AddIdleCallback( + [](void* data) { + Param* p = (Param*)data; + p->webview_instance->DispatchKeyDownEvent(p->key_value); + p->webview_instance->DispatchKeyPressEvent(p->key_value); + delete p; + }, + p); +} + +void WebView::DispatchKeyUpEvent(Ecore_Event_Key* key_event) { + std::string key_name = key_event->keyname; + LOG_DEBUG("ECORE_EVENT_KEY_UP [%s, %d]\n", key_name.data(), + (key_event->modifiers & 1) || (key_event->modifiers & 2)); + + if (!IsFocused()) { + LOG_DEBUG("ignore keyup because we dont have focus"); + return; + } + +#ifdef TV_PROFILE + if ((strncmp(key_name.data(), "XF86Back", 8) == 0)) { + key_name = "Escape"; + } +#endif + struct Param { + LWE::WebContainer* webview_instance; + LWE::KeyValue key_value; + }; + Param* p = new Param(); + p->webview_instance = webview_instance_; + p->key_value = EcoreEventKeyToKeyValue(key_name.data(), false); + + webview_instance_->AddIdleCallback( + [](void* data) { + Param* p = (Param*)data; + p->webview_instance->DispatchKeyUpEvent(p->key_value); + delete p; + }, + p); +} + +void WebView::DispatchCompositionUpdateEvent(const char* str, int size) { + if (str) { + LOG_DEBUG("WebView::DispatchCompositionUpdateEvent [%s]", str); + webview_instance_->DispatchCompositionUpdateEvent(std::string(str, size)); + } +} + +void WebView::DispatchCompositionEndEvent(const char* str, int size) { + if (str) { + LOG_DEBUG("WebView::DispatchCompositionEndEvent [%s]", str); + webview_instance_->DispatchCompositionEndEvent(std::string(str, size)); + } +} + +void WebView::ShowPanel() { + LOG_DEBUG("WebView::ShowPanel()"); + if (!context_) { + LOG_ERROR("Ecore_IMF_Context NULL"); + return; + } + ecore_imf_context_input_panel_show(context_); + ecore_imf_context_focus_in(context_); +} + +void WebView::HidePanel() { + LOG_DEBUG("WebView::HidePanel()"); + if (!context_) { + LOG_ERROR("Ecore_IMF_Context NULL"); + return; + } + ecore_imf_context_reset(context_); + ecore_imf_context_focus_out(context_); + ecore_imf_context_input_panel_hide(context_); +} + +void WebView::SetSoftwareKeyboardContext(Ecore_IMF_Context* context) { + context_ = context; + + webview_instance_->RegisterOnShowSoftwareKeyboardIfPossibleHandler( + [this](LWE::WebContainer* v) { ShowPanel(); }); + + webview_instance_->RegisterOnHideSoftwareKeyboardIfPossibleHandler( + [this](LWE::WebContainer*) { HidePanel(); }); +} + +void WebView::ClearFocus() { + LOG_DEBUG("WebView::ClearFocus()"); + HidePanel(); +} + +void WebView::SetDirection(int direction) { + LOG_DEBUG("WebView::SetDirection direction: %d\n", direction); + // TODO: implement this if necessary +} + +void WebView::InitWebView() { + if (webview_instance_ != nullptr) { + webview_instance_->Destroy(); + webview_instance_ = nullptr; + } + float scale_factor = 1; + + webview_instance_ = (LWE::WebContainer*)createWebViewInstance( + 0, 0, width_, height_, scale_factor, "SamsungOneUI", "ko-KR", + "Asia/Seoul", + [this]() -> LWE::WebContainer::ExternalImageInfo { + LWE::WebContainer::ExternalImageInfo result; + if (!tbm_surface_) { + tbm_surface_ = + tbm_surface_create(width_, height_, TBM_FORMAT_ARGB8888); + } + result.imageAddress = (void*)tbm_surface_; + return result; + }, + [this](LWE::WebContainer* c, bool isRendered) { + if (isRendered) { + FlutterMarkExternalTextureFrameAvailable( + texture_registrar_, GetTextureId(), tbm_surface_); + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + }); +#ifndef TV_PROFILE + auto settings = webview_instance_->GetSettings(); + settings.SetUserAgentString( + "Mozilla/5.0 (like Gecko/54.0 Firefox/54.0) Mobile"); + webview_instance_->SetSettings(settings); +#endif +} + +void WebView::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (!webview_instance_) { + return; + } + const auto method_name = method_call.method_name(); + const auto& arguments = *method_call.arguments(); + + LOG_DEBUG("WebView::HandleMethodCall : %s \n ", method_name.c_str()); + + if (method_name.compare("loadUrl") == 0) { + std::string url = ExtractStringFromMap(arguments, "url"); + webview_instance_->LoadURL(url); + result->Success(); + } else if (method_name.compare("updateSettings") == 0) { + if (std::holds_alternative(arguments)) { + auto settings = std::get(arguments); + if (settings.size() > 0) { + try { + ApplySettings(settings); + } catch (const std::invalid_argument& ex) { + LOG_ERROR("[Exception] %s\n", ex.what()); + result->Error(ex.what()); + return; + } + } + } + result->Success(); + } else if (method_name.compare("canGoBack") == 0) { + result->Success(flutter::EncodableValue(webview_instance_->CanGoBack())); + } else if (method_name.compare("canGoForward") == 0) { + result->Success(flutter::EncodableValue(webview_instance_->CanGoForward())); + } else if (method_name.compare("goBack") == 0) { + webview_instance_->GoBack(); + result->Success(); + } else if (method_name.compare("goForward") == 0) { + webview_instance_->GoForward(); + result->Success(); + } else if (method_name.compare("reload") == 0) { + webview_instance_->Reload(); + result->Success(); + } else if (method_name.compare("currentUrl") == 0) { + result->Success(flutter::EncodableValue(webview_instance_->GetURL())); + } else if (method_name.compare("evaluateJavascript") == 0) { + if (std::holds_alternative(arguments)) { + std::string js_string = std::get(arguments); + webview_instance_->EvaluateJavaScript( + js_string, [res = result.release()](std::string value) { + LOG_DEBUG("value: %s\n", value.c_str()); + if (res) { + res->Success(flutter::EncodableValue(value)); + delete res; + } + }); + } else { + result->Error("Invalid Arguments", "Invalid Arguments"); + } + } else if (method_name.compare("addJavascriptChannels") == 0) { + if (std::holds_alternative(arguments)) { + auto name_list = std::get(arguments); + for (size_t i = 0; i < name_list.size(); i++) { + if (std::holds_alternative(name_list[i])) { + RegisterJavaScriptChannelName(std::get(name_list[i])); + } + } + } + result->Success(); + } else if (method_name.compare("removeJavascriptChannels") == 0) { + if (std::holds_alternative(arguments)) { + auto name_list = std::get(arguments); + for (size_t i = 0; i < name_list.size(); i++) { + if (std::holds_alternative(name_list[i])) { + webview_instance_->RemoveJavascriptInterface( + std::get(name_list[i]), "postMessage"); + } + } + } + result->Success(); + } else if (method_name.compare("clearCache") == 0) { + webview_instance_->ClearCache(); + result->Success(); + } else if (method_name.compare("getTitle") == 0) { + result->Success(flutter::EncodableValue(webview_instance_->GetTitle())); + } else if (method_name.compare("scrollTo") == 0) { + int x = ExtractIntFromMap(arguments, "x"); + int y = ExtractIntFromMap(arguments, "y"); + webview_instance_->ScrollTo(x, y); + result->Success(); + } else if (method_name.compare("scrollBy") == 0) { + int x = ExtractIntFromMap(arguments, "x"); + int y = ExtractIntFromMap(arguments, "y"); + webview_instance_->ScrollBy(x, y); + result->Success(); + } else if (method_name.compare("getScrollX") == 0) { + result->Success(flutter::EncodableValue(webview_instance_->GetScrollX())); + } else if (method_name.compare("getScrollY") == 0) { + result->Success(flutter::EncodableValue(webview_instance_->GetScrollY())); + } else { + result->NotImplemented(); + } +} + +void WebView::HandleCookieMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (webview_instance_ == nullptr) { + result->Error("Not Webview created"); + return; + } + + const auto method_name = method_call.method_name(); + const auto& arguments = *method_call.arguments(); + + LOG_DEBUG("WebView::HandleMethodCall : %s \n ", method_name.c_str()); + + if (method_name.compare("clearCookies") == 0) { + LWE::CookieManager* cookie = LWE::CookieManager::GetInstance(); + cookie->ClearCookies(); + result->Success(flutter::EncodableValue(true)); + } else { + result->NotImplemented(); + } +} diff --git a/packages/webview_flutter/tizen/src/webview.h b/packages/webview_flutter/tizen/src/webview.h new file mode 100644 index 000000000..0dbcb2c07 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview.h @@ -0,0 +1,69 @@ +#ifndef FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace LWE { +class WebContainer; +} + +class TextInputChannel; + +class WebView : public PlatformView { + public: + WebView(flutter::PluginRegistrar* registrar, int viewId, + FlutterTextureRegistrar* textureRegistrar, double width, + double height, flutter::EncodableMap& params); + ~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; + + // Key input event + virtual void DispatchKeyDownEvent(Ecore_Event_Key* key) override; + virtual void DispatchKeyUpEvent(Ecore_Event_Key* key) override; + virtual void DispatchCompositionUpdateEvent(const char* str, + int size) override; + virtual void DispatchCompositionEndEvent(const char* str, int size) override; + + virtual void SetSoftwareKeyboardContext(Ecore_IMF_Context* context) override; + + LWE::WebContainer* GetWebViewInstance() { return webview_instance_; } + + void HidePanel(); + void ShowPanel(); + + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + void HandleCookieMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + std::string GetChannelName(); + void InitWebView(); + + void RegisterJavaScriptChannelName(const std::string& name); + void ApplySettings(flutter::EncodableMap); + + FlutterTextureRegistrar* texture_registrar_; + LWE::WebContainer* webview_instance_; + double width_; + double height_; + tbm_surface_h tbm_surface_; + bool is_mouse_lbutton_down_; + bool has_navigation_delegate_; + std::unique_ptr> channel_; + Ecore_IMF_Context* context_; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_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..26aa73b1f --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_factory.cc @@ -0,0 +1,49 @@ +#include "webview_factory.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "log.h" +#include "lwe/LWEWebView.h" +#include "webview_flutter_tizen_plugin.h" + +WebViewFactory::WebViewFactory(flutter::PluginRegistrar* registrar, + FlutterTextureRegistrar* textureRegistrar) + : PlatformViewFactory(registrar), textureRegistrar_(textureRegistrar) { + // temporlal soluation + std::string localstoragePath = + "/tmp/" + std::string("StarFish_localStorage.db"); + std::string cookiePath = "/tmp/" + std::string("StarFish_cookies.db"); + std::string cachePath = "/tmp/" + std::string("Starfish_cache.db"); + + LWE::LWE::Initialize(localstoragePath.c_str(), cookiePath.c_str(), + cachePath.c_str()); +} + +PlatformView* WebViewFactory::Create(int viewId, double width, double height, + const std::vector& createParams) { + flutter::EncodableMap params; + auto decodedValue = *GetCodec().DecodeMessage(createParams); + if (std::holds_alternative(decodedValue)) { + params = std::get(decodedValue); + } + + try { + return new WebView(GetPluginRegistrar(), viewId, textureRegistrar_, width, + height, params); + } catch (const std::invalid_argument& ex) { + LOG_ERROR("[Exception] %s\n", ex.what()); + return nullptr; + } +} + +void WebViewFactory::Dispose() { LWE::LWE::Finalize(); } 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..a8ebda4e0 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_factory.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_FACTORY_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_FACTORY_H_ + +#include "webview.h" +class WebViewFactory : public PlatformViewFactory { + public: + WebViewFactory(flutter::PluginRegistrar* registrar, + FlutterTextureRegistrar* textureRegistrar); + virtual void Dispose() override; + virtual PlatformView* Create( + int viewId, double width, double height, + const std::vector& createParams) override; + + private: + FlutterTextureRegistrar* textureRegistrar_; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_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..6539cd74b --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc @@ -0,0 +1,40 @@ +#include "webview_flutter_tizen_plugin.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "log.h" +#include "webview.h" +#include "webview_factory.h" + +static 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() {} +}; + +void WebviewFlutterTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + flutter::PluginRegistrar* core_registrar = + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar); + auto factory = std::make_unique( + core_registrar, FlutterPluginRegistrarGetTexture(registrar)); + FlutterRegisterViewFactory(registrar, kViewType, std::move(factory)); + WebviewFlutterTizenPlugin::RegisterWithRegistrar(core_registrar); +}