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..e601e9eb8 --- /dev/null +++ b/packages/webview_flutter/README.md @@ -0,0 +1,48 @@ + +# 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 (running Tizen 5.5 or later) + +## Usage + +```yaml +dependencies: + webview_flutter: ^1.0.6 + webview_flutter_tizen: ^0.0.1 +``` + +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', + ); + } +} +``` \ No newline at end of file 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/lib/main.dart b/packages/webview_flutter/example/lib/main.dart new file mode 100644 index 000000000..55f1c9975 --- /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: 'http://www.naver.com', + 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..4fa7f095d --- /dev/null +++ b/packages/webview_flutter/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: webview_flutter_tizen_example +description: Demonstrates how to use the webview_flutter_tizen plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter: + webview_flutter_tizen: + path: ../ + + cupertino_icons: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg diff --git a/packages/webview_flutter/example/test/widget_test.dart b/packages/webview_flutter/example/test/widget_test.dart new file mode 100644 index 000000000..acc2bf771 --- /dev/null +++ b/packages/webview_flutter/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:webview_flutter_tizen_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} 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/shared/res/ic_launcher.png b/packages/webview_flutter/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/webview_flutter/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/webview_flutter/example/tizen/tizen-manifest.xml b/packages/webview_flutter/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..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..a8b507c64 --- /dev/null +++ b/packages/webview_flutter/lib/webview_flutter_tizen.dart @@ -0,0 +1,844 @@ +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:webview_flutter/platform_interface.dart'; +import 'package:webview_flutter/src/webview_method_channel.dart'; + +import 'package:flutter/src/rendering/binding.dart'; +import 'package:flutter/src/rendering/box.dart'; +import 'package:flutter/src/rendering/layer.dart'; +import 'package:flutter/src/rendering/mouse_cursor.dart'; +import 'package:flutter/src/rendering/mouse_tracking.dart'; +import 'package:flutter/src/rendering/object.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; + + @override + int get textureId => _textureId; + + Size _size; + + @override + 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, + }); + } + + @override + 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, + }; + 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); + } + + @override + 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, + }); + } + + @override + Future dispatchPointerEvent(PointerEvent event) async { + int eventType = 0; + if (event is PointerDownEvent) { + eventType = 0; + } else if (event is PointerMoveEvent) { + eventType = 1; + } else if (event is PointerUpEvent) { + eventType = 2; + } else { + // TODO: Not implemented. + return; + } + 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(); + } + return SystemChannels.platform_views + .invokeMethod('clearFocus', viewId); + } + + @override + Future dispose() async { + if (_state == _TizenViewState.creating || _state == _TizenViewState.created) + await _sendDisposeMessage(); + _platformViewCreatedCallbacks.clear(); + _state = _TizenViewState.disposed; + PlatformViewsServiceTizen._instance._focusCallbacks.remove(viewId); + } +} + +class TizenWebView_ extends StatefulWidget { + const TizenWebView_({ + 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) { + switch (call.method) { + case 'viewFocused': + final int id = call.arguments as int; + if (_focusCallbacks.containsKey(id)) { + _focusCallbacks[id](); + } + 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); + + 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; + + 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() { + super.didChangeDependencies(); + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = + _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + _initializeOnce(); + if (didChangeLayoutDirection) { + _controller.setLayoutDirection(_layoutDirection); + } + } + + @override + void didUpdateWidget(TizenWebView_ oldWidget) { + 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() { + _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: () { + _focusNode.requestFocus(); + }, + ); + if (widget.onPlatformViewCreated != null) { + _controller + .addOnPlatformViewCreatedListener(widget.onPlatformViewCreated); + } + } + + void _onFocusChange(bool 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 TizenMethodChannelWebViewPlatform extends MethodChannelWebViewPlatform { +// TizenMethodChannelWebViewPlatform( +// int id, WebViewPlatformCallbacksHandler handler) +// : super(id, handler) { +// print("[MONG] TizenMethodChannelWebViewPlatform : " + id.toString()); +// } +// static Map creationParamsToMap( +// CreationParams creationParams) { +// return MethodChannelWebViewPlatform.creationParamsToMap(creationParams); +// } +// } + +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: TizenWebView_( + 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) { + //RendererBinding.instance.mouseTracker.schedulePostFrameCheck(); + } + } + } + + 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 + PointerHoverEventListener get onHover => _handleHover; + void _handleHover(PointerHoverEvent event) { + if (_handlePointerEvent != null) _handlePointerEvent(event); + } + + @override + PointerExitEventListener get onExit => null; + + @override + MouseCursor get cursor => MouseCursor.uncontrolled; + + @override + void handleEvent(PointerEvent event, HitTestEntry entry) { + if (event is PointerDownEvent) { + _gestureRecognizer.addPointer(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..204c6472f --- /dev/null +++ b/packages/webview_flutter/pubspec.yaml @@ -0,0 +1,22 @@ +name: webview_flutter_tizen +description: Tizen implementation of the webview plugin +version: 0.0.1 + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + tizen: + pluginClass: WebviewFlutterTizenPlugin + fileName: webview_flutter_tizen_plugin.h diff --git a/packages/webview_flutter/test/webview_flutter_tizen_test.dart b/packages/webview_flutter/test/webview_flutter_tizen_test.dart new file mode 100644 index 000000000..c7cf46a08 --- /dev/null +++ b/packages/webview_flutter/test/webview_flutter_tizen_test.dart @@ -0,0 +1,1211 @@ +// 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'; + +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(() { + 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); + }); + }); + + 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/tizen/inc/lwe/LWEWebView.h b/packages/webview_flutter/tizen/inc/lwe/LWEWebView.h new file mode 100644 index 000000000..59dc7b954 --- /dev/null +++ b/packages/webview_flutter/tizen/inc/lwe/LWEWebView.h @@ -0,0 +1,369 @@ +/* + * 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 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; + 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); + +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; +}; + +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); + // <--- 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); + + 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); + + 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..5167094f2 --- /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 }; +} + +#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.so b/packages/webview_flutter/tizen/lib/liblightweight-web-engine.so new file mode 100644 index 000000000..a85c1b917 Binary files /dev/null and b/packages/webview_flutter/tizen/lib/liblightweight-web-engine.so differ diff --git a/packages/webview_flutter/tizen/liblightweight-web-engine.so b/packages/webview_flutter/tizen/liblightweight-web-engine.so new file mode 100644 index 000000000..a85c1b917 Binary files /dev/null and b/packages/webview_flutter/tizen/liblightweight-web-engine.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..60838aab7 --- /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 + +# Libraries and objects +USER_LIB_DIRS = lib +USER_LIBS = lightweight-web-engine +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..62ad2972a --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview.cc @@ -0,0 +1,214 @@ + +#include +#include +#include +#include +#include +#include +#include "webview_flutter_tizen_plugin.h" +#include "webview.h" + +#include +#include +#include +#include + +#include "log.h" +#include "lwe/LWEWebView.h" +#include "lwe/PlatformIntegrationData.h" +#include "webview_factory.h" + +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* textureRegistrar, double width, + double height, const std::string initialUrl) + : PlatformView(registrar, viewId), + textureRegistrar_(textureRegistrar), + webViewInstance_(nullptr), + currentUrl_(initialUrl), + width_(width), + height_(height) { + setTextureId(FlutterRegisterExternalTexture(textureRegistrar_)); + initWebView(); + auto 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)); + }); + webViewInstance_->LoadURL(currentUrl_); +} + +WebView::~WebView() { dispose(); } + +std::string WebView::getChannelName() { + return "plugins.flutter.io/webview_" + std::to_string(getViewId()); +} + +void WebView::dispose() { + FlutterUnregisterExternalTexture(textureRegistrar_, getTextureId()); + + webViewInstance_->Destroy(); + webViewInstance_ = nullptr; +} + +void WebView::resize(double width, double height) { + LOG_DEBUG("WebView::resize width: %f height: %f \n", width, height); +} + +void WebView::touch(int type, int button, double x, double y, double dx, + double dy) { + // LOG_DEBUG( + // "Widget::Native::Touch type[%s],btn[%d],x[%f],y[%f],dx[%f],dy[%f]", + // type == 0 ? "DownEvent" : type == 1 ? "MoveEvent" : "UpEvent", + // button, x, y, dx, dy); + if (type == 0) { // down event + webViewInstance_->DispatchMouseDownEvent( + LWE::MouseButtonValue::LeftButton, + LWE::MouseButtonsValue::LeftButtonDown, x, y); + isMouseLButtonDown_ = true; + } else if (type == 1) { // move event + webViewInstance_->DispatchMouseMoveEvent( + isMouseLButtonDown_ ? LWE::MouseButtonValue::LeftButton + : LWE::MouseButtonValue::NoButton, + isMouseLButtonDown_ ? LWE::MouseButtonsValue::LeftButtonDown + : LWE::MouseButtonsValue::NoButtonDown, + x, y); + } else if (type == 2) { // up event + webViewInstance_->DispatchMouseUpEvent(LWE::MouseButtonValue::NoButton, + LWE::MouseButtonsValue::NoButtonDown, + x, y); + isMouseLButtonDown_ = false; + } else { + // TODO: Not implemented + } +} + +void WebView::clearFocus() { LOG_DEBUG("WebView::clearFocus \n"); } + +void WebView::setDirection(int direction) { + LOG_DEBUG("WebView::setDirection direction: %d\n", direction); +} + +void WebView::initWebView() { + if (webViewInstance_ != nullptr) { + webViewInstance_->Destroy(); + webViewInstance_ = nullptr; + } + float scaleFactor = 1; + webViewInstance_ = LWE::WebContainer::Create( + width_, height_, scaleFactor, "SamsungOneUI", "ko-KR", "Asia/Seoul"); + webViewInstance_->RegisterPreRenderingHandler( + [this]() -> LWE::WebContainer::RenderInfo { + LWE::WebContainer::RenderInfo result; + { + tbmSurface_ = + tbm_surface_create(width_, height_, TBM_FORMAT_ARGB8888); + tbm_surface_info_s tbmSurfaceInfo; + if (tbm_surface_map(tbmSurface_, TBM_SURF_OPTION_WRITE, + &tbmSurfaceInfo) == TBM_SURFACE_ERROR_NONE) { + result.updatedBufferAddress = tbmSurfaceInfo.planes[0].ptr; + result.bufferStride = tbmSurfaceInfo.planes[0].stride; + } + } + return result; + }); + webViewInstance_->RegisterOnRenderedHandler( + [this](LWE::WebContainer* c, LWE::WebContainer::RenderResult r) { + FlutterMarkExternalTextureFrameAvailable(textureRegistrar_, + getTextureId(), tbmSurface_); + tbm_surface_destroy(tbmSurface_); + tbmSurface_ = nullptr; + }); +#ifndef TV_PROFILE + auto settings = webViewInstance_->GetSettings(); + settings.SetUserAgentString( + "Mozilla/5.0 (like Gecko/54.0 Firefox/54.0) Mobile"); + webViewInstance_->SetSettings(settings); +#endif +} + +void WebView::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (webViewInstance_ == nullptr) return; + + const auto methodName = method_call.method_name(); + const auto& arguments = *method_call.arguments(); + + LOG_DEBUG("HandleMethodCall : %s \n ", methodName.c_str()); + + if (methodName.compare("loadUrl") == 0) { + currentUrl_ = extractStringFromMap(arguments, "url"); + webViewInstance_->LoadURL(getCurrentUrl()); + result->Success(); + } else if (methodName.compare("updateSettings") == 0) { + result->NotImplemented(); + } else if (methodName.compare("canGoBack") == 0) { + result->Success(flutter::EncodableValue(webViewInstance_->CanGoBack())); + } else if (methodName.compare("canGoForward") == 0) { + result->Success(flutter::EncodableValue(webViewInstance_->CanGoForward())); + } else if (methodName.compare("goBack") == 0) { + webViewInstance_->GoBack(); + result->Success(); + } else if (methodName.compare("goForward") == 0) { + webViewInstance_->GoForward(); + result->Success(); + } else if (methodName.compare("reload") == 0) { + webViewInstance_->Reload(); + result->Success(); + } else if (methodName.compare("currentUrl") == 0) { + result->Success(flutter::EncodableValue(getCurrentUrl().c_str())); + } else if (methodName.compare("evaluateJavascript") == 0) { + result->NotImplemented(); + } else if (methodName.compare("addJavascriptChannels") == 0) { + result->NotImplemented(); + } else if (methodName.compare("removeJavascriptChannels") == 0) { + result->NotImplemented(); + } else if (methodName.compare("clearCache") == 0) { + webViewInstance_->ClearCache(); + result->Success(); + } else if (methodName.compare("getTitle") == 0) { + result->NotImplemented(); + } else if (methodName.compare("scrollTo") == 0) { + result->NotImplemented(); + } else if (methodName.compare("scrollBy") == 0) { + result->NotImplemented(); + } else if (methodName.compare("getScrollX") == 0) { + result->NotImplemented(); + } else if (methodName.compare("getScrollY") == 0) { + result->NotImplemented(); + } 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..92077c211 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview.h @@ -0,0 +1,43 @@ +#ifndef FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FLUTTER_TIZEN_WEVIEW_H_ + +#include +#include +#include + +// #include "lwe/LWEWebView.h" +// #include "lwe/PlatformIntegrationData.h" +namespace LWE { +class WebContainer; +} + +class WebView : public PlatformView { + public: + WebView(flutter::PluginRegistrar* registrar, int viewId, + FlutterTextureRegistrar* textureRegistrar, double width, + double height, const std::string initialUrl); + ~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; + + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + std::string getChannelName(); + const std::string& getCurrentUrl() { return currentUrl_; } + void initWebView(); + FlutterTextureRegistrar* textureRegistrar_; + LWE::WebContainer* webViewInstance_; + std::string currentUrl_; + double width_; + double height_; + tbm_surface_h tbmSurface_; + bool isMouseLButtonDown_; +}; + +#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..ed4bdbd06 --- /dev/null +++ b/packages/webview_flutter/tizen/src/webview_factory.cc @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "log.h" +#include "webview_flutter_tizen_plugin.h" +#include "webview_factory.h" +#include "lwe/LWEWebView.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) { + std::string initialUrl = "about:blank"; + auto decoded_value = *getCodec().DecodeMessage(createParams); + if (std::holds_alternative(decoded_value)) { + flutter::EncodableMap createParams = + std::get(decoded_value); + flutter::EncodableValue initialUrlValue = + createParams[flutter::EncodableValue("initialUrl")]; + if (std::holds_alternative(initialUrlValue)) { + initialUrl = std::get(initialUrlValue); + } + } + return new WebView(getPluginRegistrar(), viewId, textureRegistrar_, width, + height, initialUrl); +} + +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..aa0330363 --- /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..bf0f21b62 --- /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); +}