diff --git a/.github/labeler.yml b/.github/labeler.yml index 6b1a79cd4..61ee87b3f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -9,6 +9,8 @@ - packages/connectivity_plus/**/* "p: device_info_plus": - packages/device_info_plus/**/* +"p: flutter_app_badger": + - packages/flutter_app_badger/**/* "p: flutter_secure_storage": - packages/flutter_secure_storage/**/* "p: flutter_tts": @@ -65,6 +67,8 @@ - packages/wearable_rotary/**/* "p: webview_flutter": - packages/webview_flutter/**/* +"p: webview_flutter_lwe": + - packages/webview_flutter_lwe/**/* # Tools "tools": diff --git a/.github/recipe.yaml b/.github/recipe.yaml index 8c93c41b8..e26a107fe 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -3,9 +3,9 @@ plugins: battery_plus: ["wearable-5.5"] connectivity_plus: ["wearable-5.5", "tv-6.5"] device_info_plus: ["wearable-5.5", "tv-6.5"] + flutter_app_badger: ["wearable-5.5"] flutter_secure_storage: ["wearable-5.5", "tv-6.5"] flutter_tts: ["wearable-5.5", "tv-6.5"] - google_sign_in: ["wearable-5.5", "tv-6.5"] integration_test: ["wearable-5.5", "tv-6.5"] messageport: ["wearable-5.5", "tv-6.5"] package_info_plus: ["wearable-5.5", "tv-6.5"] @@ -26,8 +26,10 @@ plugins: google_maps_flutter: [] network_info_plus: [] webview_flutter: [] + webview_flutter_lwe: [] # No tests. + google_sign_in: [] image_picker: [] tizen_log: [] tizen_notification: [] diff --git a/README.md b/README.md index 3a3062e28..5049dacf8 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,24 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina |-|-|:-:|:-:| | [**audioplayers_tizen**](packages/audioplayers) | [audioplayers](https://github.com/luanpotter/audioplayers) (3rd-party) | [![pub package](https://img.shields.io/pub/v/audioplayers_tizen.svg)](https://pub.dev/packages/audioplayers_tizen) | No | | [**battery_plus_tizen**](packages/battery_plus) | [battery_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/battery_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/battery_plus_tizen.svg)](https://pub.dev/packages/battery_plus_tizen) | No | -| [**camera_tizen**](packages/camera) | [camera](https://github.com/flutter/plugins/tree/master/packages/camera) (1st-party) | [![pub package](https://img.shields.io/pub/v/camera_tizen.svg)](https://pub.dev/packages/camera_tizen) | No | +| [**camera_tizen**](packages/camera) | [camera](https://github.com/flutter/plugins/tree/main/packages/camera) (1st-party) | [![pub package](https://img.shields.io/pub/v/camera_tizen.svg)](https://pub.dev/packages/camera_tizen) | No | | [**connectivity_plus_tizen**](packages/connectivity_plus) | [connectivity_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/connectivity_plus_tizen.svg)](https://pub.dev/packages/connectivity_plus_tizen) | No | | [**device_info_plus_tizen**](packages/device_info_plus) | [device_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/device_info_plus_tizen.svg)](https://pub.dev/packages/device_info_plus_tizen) | No | +| [**flutter_app_badger_tizen**](packages/flutter_app_badger) | [flutter_app_badger](https://github.com/g123k/flutter_app_badger) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_app_badger_tizen.svg)](https://pub.dev/packages/flutter_app_badger_tizen) | No | | [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | [flutter_secure_storage](https://github.com/mogol/flutter_secure_storage) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_secure_storage_tizen.svg)](https://pub.dev/packages/flutter_secure_storage_tizen) | No | | [**flutter_tts_tizen**](packages/flutter_tts) | [flutter_tts](https://github.com/dlutton/flutter_tts) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_tts_tizen.svg)](https://pub.dev/packages/flutter_tts_tizen) | No | | [**geolocator_tizen**](packages/geolocator) | [geolocator](https://github.com/Baseflow/flutter-geolocator/tree/master/geolocator) (3rd-party) | [![pub package](https://img.shields.io/pub/v/geolocator_tizen.svg)](https://pub.dev/packages/geolocator_tizen) | No | -| [**google_maps_flutter_tizen**](packages/google_maps_flutter) | [google_maps_flutter](https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter_tizen.svg)](https://pub.dev/packages/google_maps_flutter_tizen) | No | -| [**google_sign_in_tizen**](packages/google_sign_in) | [google_sign_in](https://github.com/flutter/plugins/tree/master/packages/google_sign_in) (1st-party) | [![pub package](https://img.shields.io/pub/v/google_sign_in_tizen.svg)](https://pub.dev/packages/google_sign_in_tizen) | No | -| [**image_picker_tizen**](packages/image_picker) | [image_picker](https://github.com/flutter/plugins/tree/master/packages/image_picker) (1st-party) | [![pub package](https://img.shields.io/pub/v/image_picker_tizen.svg)](https://pub.dev/packages/image_picker_tizen) | No | -| [**integration_test_tizen**](packages/integration_test) | [integration_test](https://github.com/flutter/flutter/tree/master/packages/integration_test) (1st-party) | [![pub package](https://img.shields.io/pub/v/integration_test_tizen.svg)](https://pub.dev/packages/integration_test_tizen) | No | +| [**google_maps_flutter_tizen**](packages/google_maps_flutter) | [google_maps_flutter](https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter_tizen.svg)](https://pub.dev/packages/google_maps_flutter_tizen) | No | +| [**google_sign_in_tizen**](packages/google_sign_in) | [google_sign_in](https://github.com/flutter/plugins/tree/main/packages/google_sign_in) (1st-party) | [![pub package](https://img.shields.io/pub/v/google_sign_in_tizen.svg)](https://pub.dev/packages/google_sign_in_tizen) | No | +| [**image_picker_tizen**](packages/image_picker) | [image_picker](https://github.com/flutter/plugins/tree/main/packages/image_picker) (1st-party) | [![pub package](https://img.shields.io/pub/v/image_picker_tizen.svg)](https://pub.dev/packages/image_picker_tizen) | No | +| [**integration_test_tizen**](packages/integration_test) | [integration_test](https://github.com/flutter/flutter/tree/main/packages/integration_test) (1st-party) | [![pub package](https://img.shields.io/pub/v/integration_test_tizen.svg)](https://pub.dev/packages/integration_test_tizen) | No | | [**messageport_tizen**](packages/messageport) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/messageport_tizen.svg)](https://pub.dev/packages/messageport_tizen) | N/A | | [**network_info_plus_tizen**](packages/network_info_plus) | [network_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/network_info_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/network_info_plus_tizen.svg)](https://pub.dev/packages/network_info_plus_tizen) | No | | [**package_info_plus_tizen**](packages/package_info_plus) | [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/package_info_plus_tizen.svg)](https://pub.dev/packages/package_info_plus_tizen) | No | -| [**path_provider_tizen**](packages/path_provider) | [path_provider](https://github.com/flutter/plugins/tree/master/packages/path_provider) (1st-party) | [![pub package](https://img.shields.io/pub/v/path_provider_tizen.svg)](https://pub.dev/packages/path_provider_tizen) | No | +| [**path_provider_tizen**](packages/path_provider) | [path_provider](https://github.com/flutter/plugins/tree/main/packages/path_provider) (1st-party) | [![pub package](https://img.shields.io/pub/v/path_provider_tizen.svg)](https://pub.dev/packages/path_provider_tizen) | No | | [**permission_handler_tizen**](packages/permission_handler) | [permission_handler](https://github.com/Baseflow/flutter-permission-handler) (3rd-party) | [![pub package](https://img.shields.io/pub/v/permission_handler_tizen.svg)](https://pub.dev/packages/permission_handler_tizen) | No | | [**sensors_plus_tizen**](packages/sensors_plus) | [sensors_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/sensors_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/sensors_plus_tizen.svg)](https://pub.dev/packages/sensors_plus_tizen) | No | -| [**shared_preferences_tizen**](packages/shared_preferences) | [shared_preferences](https://github.com/flutter/plugins/tree/master/packages/shared_preferences) (1st-party) | [![pub package](https://img.shields.io/pub/v/shared_preferences_tizen.svg)](https://pub.dev/packages/shared_preferences_tizen) | No | +| [**shared_preferences_tizen**](packages/shared_preferences) | [shared_preferences](https://github.com/flutter/plugins/tree/main/packages/shared_preferences) (1st-party) | [![pub package](https://img.shields.io/pub/v/shared_preferences_tizen.svg)](https://pub.dev/packages/shared_preferences_tizen) | No | | [**sqflite_tizen**](packages/sqflite) | [sqflite](https://github.com/tekartik/sqflite) (3rd-party) | [![pub package](https://img.shields.io/pub/v/sqflite_tizen.svg)](https://pub.dev/packages/sqflite_tizen) | No | | [**tizen_app_control**](packages/tizen_app_control) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_app_control.svg)](https://pub.dev/packages/tizen_app_control) | N/A | | [**tizen_app_manager**](packages/tizen_app_manager) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_app_manager.svg)](https://pub.dev/packages/tizen_app_manager) | N/A | @@ -42,11 +43,12 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**tizen_notification**](packages/tizen_notification) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_notification.svg)](https://pub.dev/packages/tizen_notification) | N/A | | [**tizen_package_manager**](packages/tizen_package_manager) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_package_manager.svg)](https://pub.dev/packages/tizen_package_manager) | N/A | | [**tizen_rpc_port**](packages/tizen_rpc_port) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_rpc_port.svg)](https://pub.dev/packages/tizen_rpc_port) | N/A | -| [**url_launcher_tizen**](packages/url_launcher) | [url_launcher](https://github.com/flutter/plugins/tree/master/packages/url_launcher) (1st-party) | [![pub package](https://img.shields.io/pub/v/url_launcher_tizen.svg)](https://pub.dev/packages/url_launcher_tizen) | No | -| [**video_player_tizen**](packages/video_player) | [video_player](https://github.com/flutter/plugins/tree/master/packages/video_player) (1st-party) | [![pub package](https://img.shields.io/pub/v/video_player_tizen.svg)](https://pub.dev/packages/video_player_tizen) | No | +| [**url_launcher_tizen**](packages/url_launcher) | [url_launcher](https://github.com/flutter/plugins/tree/main/packages/url_launcher) (1st-party) | [![pub package](https://img.shields.io/pub/v/url_launcher_tizen.svg)](https://pub.dev/packages/url_launcher_tizen) | No | +| [**video_player_tizen**](packages/video_player) | [video_player](https://github.com/flutter/plugins/tree/main/packages/video_player) (1st-party) | [![pub package](https://img.shields.io/pub/v/video_player_tizen.svg)](https://pub.dev/packages/video_player_tizen) | No | | [**wakelock_tizen**](packages/wakelock) | [wakelock](https://github.com/creativecreatorormaybenot/wakelock) (3rd-party) | [![pub package](https://img.shields.io/pub/v/wakelock_tizen.svg)](https://pub.dev/packages/wakelock_tizen) | No | | [**wearable_rotary**](packages/wearable_rotary) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/wearable_rotary.svg)](https://pub.dev/packages/wearable_rotary) | N/A | -| [**webview_flutter_tizen**](packages/webview_flutter) | [webview_flutter](https://github.com/flutter/plugins/tree/master/packages/webview_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/webview_flutter_tizen.svg)](https://pub.dev/packages/webview_flutter_tizen) | No | +| [**webview_flutter_lwe**](packages/webview_flutter_lwe) | [webview_flutter](https://github.com/flutter/plugins/tree/main/packages/webview_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/webview_flutter_lwe.svg)](https://pub.dev/packages/webview_flutter_lwe) | No | +| [**webview_flutter_tizen**](packages/webview_flutter) | [webview_flutter](https://github.com/flutter/plugins/tree/main/packages/webview_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/webview_flutter_tizen.svg)](https://pub.dev/packages/webview_flutter_tizen) | No | ## Device limitations @@ -57,6 +59,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**camera_tizen**](packages/camera) | 4.0 | ❌ | ❌ | ❌ | ❌ | No camera | | [**connectivity_plus_tizen**](packages/connectivity_plus) | 4.0 | ✔️ | ⚠️ | ✔️ | ✔️ | Returns incorrect connection status | | [**device_info_plus_tizen**](packages/device_info_plus) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | +| [**flutter_app_badger_tizen**](packages/flutter_app_badger) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | API not supported | | [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**flutter_tts_tizen**](packages/flutter_tts) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**geolocator_tizen**](packages/geolocator) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Not applicable for TV | @@ -84,4 +87,6 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**video_player_tizen**](packages/video_player) | 4.0 | ✔️ | ✔️ | ⚠️ | ❌ | Functional limitations,
TV emulator issue | | [**wakelock_tizen**](packages/wakelock) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Cannot override system settings | | [**wearable_rotary**](packages/wearable_rotary) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Not applicable for TV | -| [**webview_flutter_tizen**](packages/webview_flutter) | 5.5 | ✔️ | ✔️ | ✔️ | ✔️ | Not for production use | +| [**webview_flutter_lwe**](packages/webview_flutter_lwe) | 5.5 | ✔️ | ✔️ | ✔️ | ✔️ | Not for production use | +| [**webview_flutter_tizen**](packages/webview_flutter) | 5.5 | ❌ | ❌ | ✔️ | ✔️ | API not supported | + diff --git a/packages/camera/README.md b/packages/camera/README.md index b66275d00..8e2747413 100644 --- a/packages/camera/README.md +++ b/packages/camera/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/camera_tizen.svg)](https://pub.dev/packages/camera_tizen) -The Tizen implementation of [`camera`](https://github.com/flutter/plugins/tree/master/packages/camera). +The Tizen implementation of [`camera`](https://github.com/flutter/plugins/tree/main/packages/camera). ## Supported devices diff --git a/packages/flutter_app_badger/.gitignore b/packages/flutter_app_badger/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/packages/flutter_app_badger/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/flutter_app_badger/CHANGELOG.md b/packages/flutter_app_badger/CHANGELOG.md new file mode 100644 index 000000000..607323422 --- /dev/null +++ b/packages/flutter_app_badger/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/flutter_app_badger/LICENSE b/packages/flutter_app_badger/LICENSE new file mode 100644 index 000000000..487f7b1dc --- /dev/null +++ b/packages/flutter_app_badger/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2022 Samsung Electronics Co., Ltd. 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/flutter_app_badger/README.md b/packages/flutter_app_badger/README.md new file mode 100644 index 000000000..0a3d8cc29 --- /dev/null +++ b/packages/flutter_app_badger/README.md @@ -0,0 +1,45 @@ +# flutter_app_badger_tizen + +[![pub package](https://img.shields.io/pub/v/flutter_app_badger_tizen.svg)](https://pub.dev/packages/flutter_app_badger_tizen) + +The Tizen implementation of [`flutter_app_badger`](https://github.com/g123k/flutter_app_badger). + +## Usage + + This package is not an _endorsed_ implementation of `flutter_app_badger`. Therefore, you have to include `flutter_app_badger_tizen` alongside `flutter_app_badger` as dependencies in your `pubspec.yaml` file. + + ```yaml +dependencies: + flutter_app_badger: ^1.5.0 + flutter_app_badger_tizen: ^0.1.0 +``` + +Then you can import `flutter_app_badger` in your Dart code: + +```dart +import 'package:flutter_app_badger/flutter_app_badger.dart'; +``` + +For detailed usage, see https://pub.dev/packages/flutter_app_badger#getting-started. + +## Required privileges + +To use this plugin, you need to declare privileges in `tizen-manifest.xml` of your application. + +``` xml + + http://tizen.org/privilege/notification + +``` + +## Supported devices + +- Galaxy Watch series (running Tizen 4.0 or later) + +## Notes + +You need to declare the following feature in your `tizen-manifest.xml` if you plan to release your app on the app store (to enable [feature-based filtering](https://docs.tizen.org/application/native/tutorials/details/app-filtering)). + +```xml + +``` diff --git a/packages/flutter_app_badger/example/.gitignore b/packages/flutter_app_badger/example/.gitignore new file mode 100644 index 000000000..ad3c2ca04 --- /dev/null +++ b/packages/flutter_app_badger/example/.gitignore @@ -0,0 +1,42 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/flutter_app_badger/example/LICENSE b/packages/flutter_app_badger/example/LICENSE new file mode 100644 index 000000000..e72929ee9 --- /dev/null +++ b/packages/flutter_app_badger/example/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + \ No newline at end of file diff --git a/packages/flutter_app_badger/example/README.md b/packages/flutter_app_badger/example/README.md new file mode 100644 index 000000000..6de52f167 --- /dev/null +++ b/packages/flutter_app_badger/example/README.md @@ -0,0 +1,7 @@ +# flutter_app_badger_tizen_example + +Demonstrates how to use the flutter_app_badger_tizen plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). diff --git a/packages/flutter_app_badger/example/analysis_options.yaml b/packages/flutter_app_badger/example/analysis_options.yaml new file mode 100644 index 000000000..f9b303465 --- /dev/null +++ b/packages/flutter_app_badger/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/flutter_app_badger/example/integration_test/flutter_app_badger_test.dart b/packages/flutter_app_badger/example/integration_test/flutter_app_badger_test.dart new file mode 100644 index 000000000..04fb84475 --- /dev/null +++ b/packages/flutter_app_badger/example/integration_test/flutter_app_badger_test.dart @@ -0,0 +1,23 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('get supported', (tester) async { + expect(await FlutterAppBadger.isAppBadgeSupported(), true); + }); + + testWidgets('update badge count', (tester) async { + await expectLater(FlutterAppBadger.updateBadgeCount(1), completes); + }); + + testWidgets('remove badge count', (tester) async { + await expectLater(FlutterAppBadger.removeBadge(), completes); + }); +} diff --git a/packages/flutter_app_badger/example/lib/main.dart b/packages/flutter_app_badger/example/lib/main.dart new file mode 100644 index 000000000..cbec33c2a --- /dev/null +++ b/packages/flutter_app_badger/example/lib/main.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _appBadgeSupported = 'Unknown'; + + @override + initState() { + super.initState(); + initPlatformState(); + } + + initPlatformState() async { + String appBadgeSupported; + try { + bool res = await FlutterAppBadger.isAppBadgeSupported(); + if (res) { + appBadgeSupported = 'Supported'; + } else { + appBadgeSupported = 'Not supported'; + } + } on Exception { + appBadgeSupported = 'Failed to get badge support.'; + } + + setState(() { + _appBadgeSupported = appBadgeSupported; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: SizedBox.expand( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Badge supported: $_appBadgeSupported\n'), + ElevatedButton( + child: const Text('Add badge'), + onPressed: () { + _addBadge(); + }, + ), + ElevatedButton( + child: const Text('Remove badge'), + onPressed: () { + _removeBadge(); + }), + ], + ), + ), + ), + ); + } + + void _addBadge() { + FlutterAppBadger.updateBadgeCount(1); + } + + void _removeBadge() { + FlutterAppBadger.removeBadge(); + } +} diff --git a/packages/flutter_app_badger/example/pubspec.yaml b/packages/flutter_app_badger/example/pubspec.yaml new file mode 100644 index 000000000..955965eb4 --- /dev/null +++ b/packages/flutter_app_badger/example/pubspec.yaml @@ -0,0 +1,26 @@ +name: flutter_app_badger_tizen_example +description: Demonstrates how to use the flutter_app_badger_tizen plugin. +publish_to: "none" + +dependencies: + flutter: + sdk: flutter + flutter_app_badger: ^1.5.0 + flutter_app_badger_tizen: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + +flutter: + uses-material-design: true + +environment: + sdk: ">=2.17.0 <3.0.0" + flutter: ">=2.0.0" diff --git a/packages/flutter_app_badger/example/tizen/.gitignore b/packages/flutter_app_badger/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/flutter_app_badger/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/flutter_app_badger/example/tizen/App.cs b/packages/flutter_app_badger/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/flutter_app_badger/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/flutter_app_badger/example/tizen/Runner.csproj b/packages/flutter_app_badger/example/tizen/Runner.csproj new file mode 100644 index 000000000..f4e369d0c --- /dev/null +++ b/packages/flutter_app_badger/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/flutter_app_badger/example/tizen/shared/res/ic_launcher.png b/packages/flutter_app_badger/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/flutter_app_badger/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/flutter_app_badger/example/tizen/tizen-manifest.xml b/packages/flutter_app_badger/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..cc1a70e7a --- /dev/null +++ b/packages/flutter_app_badger/example/tizen/tizen-manifest.xml @@ -0,0 +1,14 @@ + + + + + + ic_launcher.png + + + + + + http://tizen.org/privilege/notification + + diff --git a/packages/flutter_app_badger/pubspec.yaml b/packages/flutter_app_badger/pubspec.yaml new file mode 100644 index 000000000..364ad8be8 --- /dev/null +++ b/packages/flutter_app_badger/pubspec.yaml @@ -0,0 +1,20 @@ +name: flutter_app_badger_tizen +description: Tizen implementation of the flutter_app_badger plugin +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/flutter_app_badger_tizen +version: 0.1.0 + +flutter: + plugin: + platforms: + tizen: + pluginClass: FlutterAppBadgerTizenPlugin + fileName: flutter_app_badger_tizen_plugin.h + +dependencies: + flutter: + sdk: flutter + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/packages/google_sign_in/tizen/.gitignore b/packages/flutter_app_badger/tizen/.gitignore similarity index 100% rename from packages/google_sign_in/tizen/.gitignore rename to packages/flutter_app_badger/tizen/.gitignore diff --git a/packages/flutter_app_badger/tizen/inc/flutter_app_badger_tizen_plugin.h b/packages/flutter_app_badger/tizen/inc/flutter_app_badger_tizen_plugin.h new file mode 100644 index 000000000..9e74f5382 --- /dev/null +++ b/packages/flutter_app_badger/tizen/inc/flutter_app_badger_tizen_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FLUTTER_APP_BADGER_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_APP_BADGER_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 FlutterAppBadgerTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FLUTTER_APP_BADGER_TIZEN_PLUGIN_H_ diff --git a/packages/flutter_app_badger/tizen/project_def.prop b/packages/flutter_app_badger/tizen/project_def.prop new file mode 100644 index 000000000..cf0d42916 --- /dev/null +++ b/packages/flutter_app_badger/tizen/project_def.prop @@ -0,0 +1,24 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = flutter_app_badger_tizen_plugin +type = staticLib +profile = common-4.0 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Compiler flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/flutter_app_badger/tizen/src/flutter_app_badger_tizen_plugin.cc b/packages/flutter_app_badger/tizen/src/flutter_app_badger_tizen_plugin.cc new file mode 100644 index 000000000..8d69346f1 --- /dev/null +++ b/packages/flutter_app_badger/tizen/src/flutter_app_badger_tizen_plugin.cc @@ -0,0 +1,118 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_app_badger_tizen_plugin.h" + +#include +#include +#include + +#include +#include + +#include "log.h" +#include "tizen_badge.h" + +namespace { + +typedef flutter::MethodChannel FlMethodChannel; +typedef flutter::MethodResult FlMethodResult; + +template +static bool GetValueFromEncodableMap(const flutter::EncodableMap *map, + const char *key, T &out) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto *value = std::get_if(&iter->second)) { + out = *value; + return true; + } + } + return false; +} + +class FlutterAppBadgerTizenPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar *registrar) { + auto plugin = std::make_unique(registrar); + registrar->AddPlugin(std::move(plugin)); + } + + FlutterAppBadgerTizenPlugin(flutter::PluginRegistrar *registrar) { + // FlutterAppBadgerPlugin : https://pub.dev/packages/flutter_app_badger + channel_ = std::make_unique( + registrar->messenger(), "g123k/flutter_app_badger", + &flutter::StandardMethodCodec::GetInstance()); + channel_->SetMethodCallHandler([this](const auto &call, auto result) { + this->HandleMethodCall(call, std::move(result)); + }); + + badge_ = std::make_unique(); + if (!badge_->Initialize()) { + badge_ = nullptr; + } + } + + virtual ~FlutterAppBadgerTizenPlugin() {} + + private: + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr result) { + const auto &method_name = method_call.method_name(); + + if (!badge_) { + result->Error(std::to_string(badge_->GetLastError()), + badge_->GetLastErrorString()); + return; + } + + if (method_name == "isAppBadgeSupported") { + if (badge_) { + result->Success(flutter::EncodableValue(true)); + } else { + result->Success(flutter::EncodableValue(false)); + } + } else if (method_name == "updateBadgeCount") { + const auto *arguments = + std::get_if(method_call.arguments()); + if (!arguments) { + result->Error("Invalid arguments", "Invalid arguments."); + return; + } + int32_t count = 0; + if (GetValueFromEncodableMap(arguments, "count", count)) { + if (badge_->UpdateBadgeCount(count)) { + result->Success(flutter::EncodableValue(true)); + } else { + result->Error(std::to_string(badge_->GetLastError()), + badge_->GetLastErrorString()); + } + } else { + result->Error("Invalid arguments", "No count provided."); + } + } else if (method_name == "removeBadge") { + if (badge_->RemoveBadge()) { + result->Success(flutter::EncodableValue(true)); + } else { + result->Error(std::to_string(badge_->GetLastError()), + badge_->GetLastErrorString()); + } + } else { + result->NotImplemented(); + } + } + + std::unique_ptr badge_; + std::unique_ptr channel_; +}; + +} // namespace + +void FlutterAppBadgerTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FlutterAppBadgerTizenPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/flutter_app_badger/tizen/src/log.h b/packages/flutter_app_badger/tizen/src/log.h new file mode 100644 index 000000000..c0b5b3ede --- /dev/null +++ b/packages/flutter_app_badger/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "FlutterAppBadgerTizenPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/flutter_app_badger/tizen/src/tizen_badge.cc b/packages/flutter_app_badger/tizen/src/tizen_badge.cc new file mode 100644 index 000000000..c4a00b7b2 --- /dev/null +++ b/packages/flutter_app_badger/tizen/src/tizen_badge.cc @@ -0,0 +1,89 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tizen_badge.h" + +#include "log.h" + +bool TizenBadge::Initialize() { + char *app_id = nullptr; + int ret = app_get_id(&app_id); + if (ret != APP_ERROR_NONE) { + LOG_ERROR("app_get_id() failed with error: %s", get_error_message(ret)); + last_error_ = ret; + return false; + } + app_id_ = app_id; + free(app_id); + + ret = badge_add(app_id_.c_str()); + if (ret == BADGE_ERROR_NOT_SUPPORTED) { + LOG_INFO("Badge is not supported."); + is_supported_ = false; + last_error_ = ret; + } else if (ret != BADGE_ERROR_NONE && ret != BADGE_ERROR_ALREADY_EXIST) { + LOG_ERROR("badge_add() failed with error: %s", get_error_message(ret)); + last_error_ = ret; + return false; + } else { + is_supported_ = true; + } + + is_added_ = true; + return true; +} + +bool TizenBadge::UpdateBadgeCount(int32_t count) { + if (!is_supported_) { + LOG_ERROR("Badge is not supported."); + last_error_ = BADGE_ERROR_NOT_SUPPORTED; + return false; + } + int ret = BADGE_ERROR_NONE; + if (!is_added_) { + ret = badge_add(app_id_.c_str()); + if (ret != BADGE_ERROR_NONE && ret != BADGE_ERROR_ALREADY_EXIST) { + LOG_ERROR("badge_add() failed with error: %s", get_error_message(ret)); + last_error_ = ret; + return false; + } + is_added_ = true; + } + + ret = badge_set_count(app_id_.c_str(), count); + if (ret != BADGE_ERROR_NONE) { + LOG_ERROR("badge_set_count() failed with error: %s", + get_error_message(ret)); + last_error_ = ret; + return false; + } + + ret = badge_set_display(app_id_.c_str(), 1); + if (ret != BADGE_ERROR_NONE) { + LOG_ERROR("badge_set_display() failed with error: %s", + get_error_message(ret)); + last_error_ = ret; + return false; + } + return true; +} + +bool TizenBadge::RemoveBadge() { + if (!is_supported_) { + LOG_ERROR("Badge is not supported."); + last_error_ = BADGE_ERROR_NOT_SUPPORTED; + return false; + } + + if (is_added_) { + int ret = badge_remove(app_id_.c_str()); + if (ret != BADGE_ERROR_NONE) { + LOG_ERROR("badge_remove() failed with error: %s", get_error_message(ret)); + last_error_ = ret; + return false; + } + is_added_ = false; + } + return true; +} diff --git a/packages/flutter_app_badger/tizen/src/tizen_badge.h b/packages/flutter_app_badger/tizen/src/tizen_badge.h new file mode 100644 index 000000000..b635ed91a --- /dev/null +++ b/packages/flutter_app_badger/tizen/src/tizen_badge.h @@ -0,0 +1,36 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_TIZEN_BADGE_H_ +#define FLUTTER_PLUGIN_TIZEN_BADGE_H_ + +#include +#include + +#include + +class TizenBadge { + public: + ~TizenBadge() = default; + + int GetLastError() { return last_error_; } + + std::string GetLastErrorString() { return get_error_message(last_error_); } + + bool IsSupported() { return is_supported_; }; + + bool Initialize(); + + bool UpdateBadgeCount(int32_t count); + + bool RemoveBadge(); + + private: + std::string app_id_; + bool is_added_ = false; + bool is_supported_ = false; + int last_error_ = TIZEN_ERROR_NONE; +}; + +#endif // FLUTTER_PLUGIN_TIZEN_BADGE_H_ diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index a2c5c12c7..12c93da5a 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.1.4 +* Change dependency from webview_flutter_tizen to webview_flutter_lwe. * Fix integration test failures ## 0.1.3 diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index b9098350f..186f81006 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_tizen description: Tizen platform implementation of google_maps_flutter homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/google_maps_flutter -version: 0.1.3 +version: 0.1.4 flutter: plugin: @@ -19,7 +19,7 @@ dependencies: meta: ^1.3.0 stream_transform: ^2.0.0 webview_flutter: ^3.0.4 - webview_flutter_tizen: ^0.5.3 + webview_flutter_lwe: ^0.1.0 dev_dependencies: flutter_test: diff --git a/packages/google_sign_in/CHANGELOG.md b/packages/google_sign_in/CHANGELOG.md index 541fbeb16..e5fb29ddf 100644 --- a/packages/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.3 + +* Add dependency on flutter_secure_storage_tizen. + ## 0.1.2 * Resolve linter warnings. diff --git a/packages/google_sign_in/README.md b/packages/google_sign_in/README.md index 1a1c78881..996d7fcb1 100644 --- a/packages/google_sign_in/README.md +++ b/packages/google_sign_in/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/google_sign_in_tizen.svg)](https://pub.dev/packages/google_sign_in_tizen) -The Tizen implementation of [`google_sign_in`](https://github.com/flutter/plugins/tree/master_archive/packages/google_sign_in/google_sign_in). +The Tizen implementation of [`google_sign_in`](https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in). ## Usage @@ -11,7 +11,7 @@ This package is not an _endorsed_ implementation of `google_sign_in`. Therefore, ```yaml dependencies: google_sign_in: ^5.4.1 - google_sign_in_tizen: ^0.1.2 + google_sign_in_tizen: ^0.1.3 ``` For detailed usage on how to use `google_sign_in`, see https://pub.dev/packages/google_sign_in#usage. diff --git a/packages/google_sign_in/example/integration_test/secure_storage_test.dart b/packages/google_sign_in/example/integration_test/secure_storage_test.dart deleted file mode 100644 index d13dd768c..000000000 --- a/packages/google_sign_in/example/integration_test/secure_storage_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_tizen/google_sign_in_tizen.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - late SecureStorage storage; - late Map data1; - late Map data2; - - setUpAll(() async { - storage = SecureStorage(); - data1 = { - 'key11': 'value11', - 'key12': 12, - }; - data2 = { - 'key21': 'value21', - 'key22': 22, - }; - }); - - tearDown(() async { - await storage.destroy(); - }); - - testWidgets('Can save get and remove', (WidgetTester tester) async { - await storage.saveJson('a', data1); - await storage.saveJson('b', data2); - - final Map? retrievedData1 = await storage.getJson('a'); - final Map? retrievedData2 = await storage.getJson('b'); - expect(retrievedData1, isNotNull); - expect(retrievedData2, isNotNull); - - expect(true, mapEquals(data1, retrievedData1)); - expect(true, mapEquals(data2, retrievedData2)); - - await storage.remove('a'); - await storage.remove('b'); - - expect(await storage.getJson('a'), isNull); - expect(await storage.getJson('b'), isNull); - }); -} diff --git a/packages/google_sign_in/lib/google_sign_in_tizen.dart b/packages/google_sign_in/lib/google_sign_in_tizen.dart index a2af24df3..fd602951a 100644 --- a/packages/google_sign_in/lib/google_sign_in_tizen.dart +++ b/packages/google_sign_in/lib/google_sign_in_tizen.dart @@ -2,18 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert' as convert; +import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'src/device_flow_widget.dart' as device_flow_widget; import 'src/oauth2.dart'; -import 'src/secure_storage.dart'; export 'src/authorization_exception.dart'; -export 'src/secure_storage.dart'; /// Holds authentication data after Google sign in for Tizen. class _GoogleSignInTokenDataTizen extends GoogleSignInTokenData { @@ -50,8 +49,8 @@ class _GoogleSignInTokenDataTizen extends GoogleSignInTokenData { static _GoogleSignInTokenDataTizen fromJson(Map json) { return _GoogleSignInTokenDataTizen( accessToken: json['access_token']! as String, - accessTokenExpirationDate: DateTime.fromMicrosecondsSinceEpoch( - json['access_token_expiration_date']! as int), + accessTokenExpirationDate: + DateTime.parse(json['access_token_expiration_date']! as String), idToken: json['id_token']! as String, refreshToken: json['refresh_token'] as String?, ); @@ -61,8 +60,7 @@ class _GoogleSignInTokenDataTizen extends GoogleSignInTokenData { Map toJson() { return { 'access_token': accessToken, - 'access_token_expiration_date': - accessTokenExpirationDate.microsecondsSinceEpoch, + 'access_token_expiration_date': accessTokenExpirationDate.toString(), 'id_token': idToken, if (refreshToken != null) 'refresh_token': refreshToken!, }; @@ -84,7 +82,7 @@ class _Credentials { class _CachedTokenStorage { // ignore: invalid_use_of_visible_for_testing_member - final SecureStorage _storage = SecureStorage(); + final FlutterSecureStorage _storage = const FlutterSecureStorage(); final String _kToken = 'token'; @@ -92,7 +90,7 @@ class _CachedTokenStorage { _GoogleSignInTokenDataTizen? _token; Future saveToken(_GoogleSignInTokenDataTizen token) async { - await _storage.saveJson(_kToken, token.toJson()); + await _storage.write(key: _kToken, value: jsonEncode(token.toJson())); _token = token; } @@ -100,12 +98,15 @@ class _CachedTokenStorage { if (_token != null) { return _token!; } - final Map? json = await _storage.getJson(_kToken); - return json != null ? _GoogleSignInTokenDataTizen.fromJson(json) : null; + final String? jsonString = await _storage.read(key: _kToken); + return jsonString != null + ? _GoogleSignInTokenDataTizen.fromJson( + jsonDecode(jsonString) as Map) + : null; } Future removeToken() async { - await _storage.remove(_kToken); + await _storage.delete(key: _kToken); _token = null; } } @@ -328,11 +329,10 @@ class GoogleSignInTizen extends GoogleSignInPlatform { if (splitTokens.length != 3) { throw const FormatException('Invalid idToken.'); } - final String normalizedPayload = convert.base64.normalize(splitTokens[1]); - final String payloadString = - convert.utf8.decode(convert.base64.decode(normalizedPayload)); + final String normalizedPayload = base64.normalize(splitTokens[1]); + final String payloadString = utf8.decode(base64.decode(normalizedPayload)); final Map json = - convert.jsonDecode(payloadString) as Map; + jsonDecode(payloadString) as Map; return GoogleSignInUserData( email: json['email']! as String, diff --git a/packages/google_sign_in/lib/src/secure_storage.dart b/packages/google_sign_in/lib/src/secure_storage.dart deleted file mode 100644 index 4f210026d..000000000 --- a/packages/google_sign_in/lib/src/secure_storage.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert' as convert; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -// This value must match `kIvSizeBytes` in `secure_storage.cc`. -const int _kIvSizeBytes = 12; - -/// Storage that encrypts/decrypts saved data. -@visibleForTesting -class SecureStorage { - final MethodChannel _channel = const MethodChannel('tizen/secure_storage'); - - final Random _random = Random.secure(); - - /// Saves json data with [name] to storage, data will be overwritten if it - /// already exists. - Future saveJson(String name, Map json) async { - final Uint8List bytes = - Uint8List.fromList(convert.jsonEncode(json).codeUnits); - final Uint8List initializationVector = Uint8List.fromList( - List.generate(_kIvSizeBytes, (_) => _random.nextInt(256))); - await _channel.invokeMethod('save', { - 'name': name, - 'data': bytes, - 'initializationVector': initializationVector, - }); - } - - /// Gets the data [name] from storage, returns `null` if no data is present. - Future?> getJson(String name) async { - final Uint8List? bytes = await _channel - .invokeMethod('get', {'name': name}); - if (bytes != null) { - return convert.jsonDecode(String.fromCharCodes(bytes.toList())) - as Map; - } - return null; - } - - /// Removes the data [name] from storage. - /// - /// This method is safe to call multiple times, nothing will happen if [name] - /// doesn't exist in storage. - Future remove(String name) => - _channel.invokeMethod('remove', {'name': name}); - - /// Removes all data and key from storage. - Future destroy() => _channel.invokeMethod('destroy'); -} diff --git a/packages/google_sign_in/pubspec.yaml b/packages/google_sign_in/pubspec.yaml index e2f276582..b87f54711 100644 --- a/packages/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/pubspec.yaml @@ -2,21 +2,21 @@ name: google_sign_in_tizen description: Tizen implementation of the google_sign_in plugin homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/google_sign_in -version: 0.1.2 +version: 0.1.3 flutter: plugin: platforms: tizen: dartPluginClass: GoogleSignInTizen - pluginClass: GoogleSignInTizenPlugin - fileName: google_sign_in_tizen_plugin.h assets: - assets/images/qrcode.svg dependencies: flutter: sdk: flutter + flutter_secure_storage: ^6.0.0 + flutter_secure_storage_tizen: ^0.1.0 flutter_svg: ^1.1.5 google_sign_in_platform_interface: ^2.3.0 http: ^0.13.4 diff --git a/packages/google_sign_in/tizen/src/google_sign_in_tizen_plugin.cc b/packages/google_sign_in/tizen/src/google_sign_in_tizen_plugin.cc deleted file mode 100644 index 6d625d4cc..000000000 --- a/packages/google_sign_in/tizen/src/google_sign_in_tizen_plugin.cc +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "google_sign_in_tizen_plugin.h" - -#include -#include -#include - -#include - -#include "secure_storage.h" - -namespace { - -const char *kInvalidArgument = "Invalid argument"; - -template -bool GetValueFromEncodableMap(const flutter::EncodableMap *map, const char *key, - T &out) { - auto iter = map->find(flutter::EncodableValue(key)); - if (iter != map->end() && !iter->second.IsNull()) { - if (auto *value = std::get_if(&iter->second)) { - out = *value; - return true; - } - } - return false; -} - -class GoogleSignInTizenPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrar *registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "tizen/secure_storage", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto &call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); - } - - GoogleSignInTizenPlugin() {} - - virtual ~GoogleSignInTizenPlugin() {} - - private: - void HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result) { - const auto &method_name = method_call.method_name(); - if (method_name == "destroy") { - storage_.Destroy(); - result->Success(); - return; - } - - const auto *arguments = - std::get_if(method_call.arguments()); - if (!arguments) { - result->Error(kInvalidArgument, "No arguments provided."); - return; - } - std::string name; - if (!GetValueFromEncodableMap(arguments, "name", name)) { - result->Error(kInvalidArgument, "No name provided."); - return; - } - - if (method_name == "save") { - std::vector data; - if (!GetValueFromEncodableMap(arguments, "data", data)) { - result->Error(kInvalidArgument, "No data provided."); - return; - } - std::vector initialization_vector; - if (!GetValueFromEncodableMap(arguments, "initializationVector", - initialization_vector)) { - result->Error(kInvalidArgument, "No initialization_vector provided."); - return; - } - storage_.SaveData(name, data, initialization_vector); - result->Success(); - } else if (method_name == "get") { - std::optional> data = storage_.GetData(name); - if (data.has_value()) { - result->Success(flutter::EncodableValue(data.value())); - } else { - // Sends `null` to dart side if data doesn't exist. - result->Success(); - } - } else if (method_name == "remove") { - storage_.RemoveData(name); - result->Success(); - } else { - result->NotImplemented(); - } - } - - SecureStorage storage_; -}; - -} // namespace - -void GoogleSignInTizenPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - GoogleSignInTizenPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); -} diff --git a/packages/google_sign_in/tizen/src/secure_storage.cc b/packages/google_sign_in/tizen/src/secure_storage.cc deleted file mode 100644 index 927bdf669..000000000 --- a/packages/google_sign_in/tizen/src/secure_storage.cc +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "secure_storage.h" - -#include -#include - -#include - -namespace { - -// This value must match `_kIvSizeBytes` in `secure_storage.dart`. -constexpr size_t kIvSizeBytes = 12; - -constexpr char kAesKey[] = "AesKey"; - -const ckmc_policy_s kDefaultKeyPolicy = { - .password = nullptr, - .extractable = false, -}; - -const ckmc_policy_s kDefaultDataPolicy = { - .password = nullptr, - .extractable = true, -}; - -enum class NameType { - kKey, - kData, -}; - -std::vector GetNames(NameType name_type) { - ckmc_alias_list_s *ckmc_alias_list = nullptr; - switch (name_type) { - case NameType::kKey: - ckmc_get_key_alias_list(&ckmc_alias_list); - break; - case NameType::kData: - ckmc_get_data_alias_list(&ckmc_alias_list); - break; - default: - assert(false); - } - - std::vector names; - ckmc_alias_list_s *current = ckmc_alias_list; - while (current != nullptr) { - std::string name = current->alias; - // The ckmc module returns all aliases prefixed with application id. - // For example, if 'some_alias' is saved for 'some_app', - // the returned alias would be 'some_app some_alias'. - // So we need to remove prefix from the returned alias. - names.push_back(name.substr(name.find(' ') + 1)); - current = current->next; - } - - ckmc_alias_list_all_free(ckmc_alias_list); - return names; -} -} // namespace - -SecureStorage::SecureStorage() { - ckmc_generate_new_params(CKMC_ALGO_AES_GCM, ¶ms_); - std::vector names = GetNames(NameType::kKey); - for (const auto &name : names) { - if (name == kAesKey) { - return; - } - } - ckmc_create_key_aes(256, kAesKey, kDefaultKeyPolicy); -} - -SecureStorage::~SecureStorage() { ckmc_param_list_free(params_); } - -void SecureStorage::Destroy() { - std::vector names = GetNames(NameType::kData); - for (const auto &name : names) { - RemoveData(name); - } - ckmc_remove_alias(kAesKey); -} - -void SecureStorage::SaveData( - const std::string &name, const std::vector &data, - const std::vector &initialization_vector) { - std::vector encrypted = EncryptData(data, initialization_vector); - - ckmc_raw_buffer_s *buffer = nullptr; - ckmc_buffer_new(encrypted.data(), encrypted.size(), &buffer); - int ret = ckmc_save_data(name.c_str(), *buffer, kDefaultDataPolicy); - if (ret == CKMC_ERROR_DB_ALIAS_EXISTS) { - RemoveData(name.c_str()); - ckmc_save_data(name.c_str(), *buffer, kDefaultDataPolicy); - } - ckmc_buffer_free(buffer); -} - -std::optional> SecureStorage::GetData( - const std::string &name) { - ckmc_raw_buffer_s *buffer = nullptr; - - int ret = ckmc_get_data(name.c_str(), nullptr, &buffer); - if (ret != CKMC_ERROR_NONE) { - return std::nullopt; - } - std::vector data(buffer->data, buffer->data + buffer->size); - ckmc_buffer_free(buffer); - return DecryptData(data); -} - -void SecureStorage::RemoveData(const std::string &name) { - ckmc_remove_alias(name.c_str()); -} - -std::vector SecureStorage::EncryptData( - std::vector data, std::vector initialization_vector) { - ckmc_raw_buffer_s *buffer = nullptr; - ckmc_buffer_new(initialization_vector.data(), initialization_vector.size(), - &buffer); - ckmc_param_list_set_buffer(params_, CKMC_PARAM_ED_IV, buffer); - ckmc_buffer_free(buffer); - - ckmc_buffer_new(data.data(), data.size(), &buffer); - ckmc_raw_buffer_s *encrypted_buffer = nullptr; - ckmc_encrypt_data(params_, kAesKey, nullptr, *buffer, &encrypted_buffer); - ckmc_buffer_free(buffer); - - std::vector encrypted( - encrypted_buffer->data, encrypted_buffer->data + encrypted_buffer->size); - ckmc_buffer_free(encrypted_buffer); - encrypted.insert(encrypted.end(), initialization_vector.begin(), - initialization_vector.end()); - return encrypted; -} - -std::vector SecureStorage::DecryptData(std::vector data) { - std::vector initialization_vector(data.end() - kIvSizeBytes, - data.end()); - data.erase(data.end() - kIvSizeBytes, data.end()); - - ckmc_raw_buffer_s *buffer = nullptr; - ckmc_buffer_new(initialization_vector.data(), initialization_vector.size(), - &buffer); - ckmc_param_list_set_buffer(params_, CKMC_PARAM_ED_IV, buffer); - ckmc_buffer_free(buffer); - - ckmc_buffer_new(data.data(), data.size(), &buffer); - ckmc_raw_buffer_s *decrypted_buffer = nullptr; - ckmc_decrypt_data(params_, kAesKey, nullptr, *buffer, &decrypted_buffer); - ckmc_buffer_free(buffer); - - std::vector decrypted( - decrypted_buffer->data, decrypted_buffer->data + decrypted_buffer->size); - ckmc_buffer_free(decrypted_buffer); - - return decrypted; -} diff --git a/packages/google_sign_in/tizen/src/secure_storage.h b/packages/google_sign_in/tizen/src/secure_storage.h deleted file mode 100644 index e70b2f670..000000000 --- a/packages/google_sign_in/tizen/src/secure_storage.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_PLUGIN_SECURE_STORAGE_H_ -#define FLUTTER_PLUGIN_SECURE_STORAGE_H_ - -#include - -#include -#include -#include - -class SecureStorage { - public: - SecureStorage(); - - ~SecureStorage(); - - void Destroy(); - - void SaveData(const std::string& name, const std::vector& data, - const std::vector& initialization_vector); - - std::optional> GetData(const std::string& name); - - void RemoveData(const std::string& name); - - private: - ckmc_param_list_h params_; - - std::vector EncryptData(std::vector data, - std::vector initialization_vector); - - std::vector DecryptData(std::vector data); -}; - -#endif // FLUTTER_PLUGIN_SECURE_STORAGE_H_ diff --git a/packages/image_picker/README.md b/packages/image_picker/README.md index c9dea8f44..a8fcaca1c 100644 --- a/packages/image_picker/README.md +++ b/packages/image_picker/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/image_picker_tizen.svg)](https://pub.dev/packages/image_picker_tizen) -The Tizen implementation of [`image_picker`](https://github.com/flutter/plugins/tree/master/packages/image_picker). +The Tizen implementation of [`image_picker`](https://github.com/flutter/plugins/tree/main/packages/image_picker). ## Usage diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index 59034aa6a..900b4a5ab 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/integration_test_tizen.svg)](https://pub.dev/packages/integration_test_tizen) -The Tizen implementation of [`integration_test`](https://github.com/flutter/flutter/tree/master/packages/integration_test). +The Tizen implementation of [`integration_test`](https://github.com/flutter/flutter/tree/main/packages/integration_test). ## Usage diff --git a/packages/path_provider/README.md b/packages/path_provider/README.md index 3c70cee0a..de6b97cee 100644 --- a/packages/path_provider/README.md +++ b/packages/path_provider/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/path_provider_tizen.svg)](https://pub.dev/packages/path_provider_tizen) -The Tizen implementation of [`path_provider`](https://github.com/flutter/plugins/tree/master/packages/path_provider). +The Tizen implementation of [`path_provider`](https://github.com/flutter/plugins/tree/main/packages/path_provider). ## Usage diff --git a/packages/shared_preferences/README.md b/packages/shared_preferences/README.md index 17abac1a7..c57476d97 100644 --- a/packages/shared_preferences/README.md +++ b/packages/shared_preferences/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/shared_preferences_tizen.svg)](https://pub.dev/packages/shared_preferences_tizen) -The Tizen implementation of [`shared_preferences`](https://github.com/flutter/plugins/tree/master/packages/shared_preferences). +The Tizen implementation of [`shared_preferences`](https://github.com/flutter/plugins/tree/main/packages/shared_preferences). ## Usage diff --git a/packages/tizen_app_control/lib/src/app_control.dart b/packages/tizen_app_control/lib/src/app_control.dart index be430168e..21d7fe943 100644 --- a/packages/tizen_app_control/lib/src/app_control.dart +++ b/packages/tizen_app_control/lib/src/app_control.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'ffi.dart'; -import 'utils.dart'; /// Enumeration for the application control launch mode. /// @@ -80,8 +79,7 @@ class AppControl { uri = map['uri'] as String?, mime = map['mime'] as String?, category = map['category'] as String?, - launchMode = - enumFromString(LaunchMode.values, map['launchMode'] as String), + launchMode = LaunchMode.values.byName(map['launchMode'] as String), extraData = Map.from( map['extraData'] as Map) { if (!nativeAttachAppControl(_id, this)) { @@ -180,11 +178,8 @@ class AppControl { await _methodChannel.invokeMethod('sendLaunchRequest', args); final Map responseMap = Map.from(response as Map); - final AppControlReplyResult result = enumFromString( - AppControlReplyResult.values, - responseMap['result'] as String, - AppControlReplyResult.failed, - ); + final AppControlReplyResult result = + AppControlReplyResult.values.byName(responseMap['result'] as String); final Map replyMap = Map.from( responseMap['reply'] as Map); final AppControl reply = AppControl._fromMap(replyMap); diff --git a/packages/tizen_app_control/lib/src/utils.dart b/packages/tizen_app_control/lib/src/utils.dart deleted file mode 100644 index 8ee2f0d95..000000000 --- a/packages/tizen_app_control/lib/src/utils.dart +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: public_member_api_docs - -import 'package:ffi/ffi.dart'; -import 'package:flutter/services.dart'; - -import 'ffi.dart'; - -E enumFromString( - List enumValues, - String stringValue, [ - E? defaultValue, -]) { - return enumValues.firstWhere( - (E e) => e.toString().split('.').last == stringValue, - orElse: () => defaultValue ?? enumValues.first, - ); -} - -void throwOnError(int ret) { - if (ret != 0) { - throw PlatformException( - code: ret.toString(), - message: getErrorMessage(ret).toDartString(), - ); - } -} diff --git a/packages/tizen_notification/CHANGELOG.md b/packages/tizen_notification/CHANGELOG.md index cc0f2b202..7c8c635e5 100644 --- a/packages/tizen_notification/CHANGELOG.md +++ b/packages/tizen_notification/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.2.0 + +* Clean up the API. + * Rename `NotificationImage` to `NotificationIcons`. + * Rename `Property` to `NotificationProperty`. + * Rename `DisplayApplist` to `NotificationStyle`. + * Rename some members of the above classes. +* Update the documentation. +* Code refactoring and bug fixes. + ## 0.1.1 * Update the example app. diff --git a/packages/tizen_notification/README.md b/packages/tizen_notification/README.md index 745925f4d..e0c418df1 100644 --- a/packages/tizen_notification/README.md +++ b/packages/tizen_notification/README.md @@ -6,11 +6,11 @@ Tizen notification APIs. Used to show and delete notifications on a Tizen device ## Usage -To use this plugin, add `tizen_notification` as a dependency in your pubspec.yaml file: +To use this plugin, add `tizen_notification` as a dependency in your `pubspec.yaml` file: ```yaml dependencies: - tizen_notification: ^0.1.1 + tizen_notification: ^0.2.0 ``` Then you can import `tizen_notification` in your Dart code: diff --git a/packages/tizen_notification/example/lib/main.dart b/packages/tizen_notification/example/lib/main.dart index c121dbe17..7a7ade91b 100644 --- a/packages/tizen_notification/example/lib/main.dart +++ b/packages/tizen_notification/example/lib/main.dart @@ -16,25 +16,26 @@ class MyApp extends StatelessWidget { final TizenNotificationPlugin _tizenNotificationPlugin = TizenNotificationPlugin(); - final int notificationId = 1; + final int _notificationId = 1; Future _showNotification() async { final TizenNotificationDetails details = TizenNotificationDetails( - image: NotificationImage(iconPath: 'test.png'), - properties: Property.disableAutoDelete | Property.disableAppLaunch, - vibration: NotificationVibration(type: VibrationType.builtIn), + icons: NotificationIcons(icon: 'test.png'), sound: NotificationSound(type: SoundType.builtIn), + vibration: NotificationVibration(type: VibrationType.builtIn), + properties: NotificationProperty.disableAutoDelete, + appControl: AppControl(appId: 'org.tizen.tizen_notification_example'), ); await _tizenNotificationPlugin.show( - notificationId, - title: 'show Notification Title', - body: 'show Notification Body', + _notificationId, + title: 'Notification title', + body: 'Notification body', notificationDetails: details, ); } Future _cancelNotification() async { - await _tizenNotificationPlugin.cancel(notificationId); + await _tizenNotificationPlugin.cancel(_notificationId); } Future _cancelAllNotifications() async { @@ -45,6 +46,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( + appBar: AppBar(title: const Text('Tizen Notification Example')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -53,10 +55,12 @@ class MyApp extends StatelessWidget { onPressed: _showNotification, child: const Text('Show notification'), ), + const SizedBox(height: 10), ElevatedButton( onPressed: _cancelNotification, child: const Text('Cancel notification'), ), + const SizedBox(height: 10), ElevatedButton( onPressed: _cancelAllNotifications, child: const Text('Cancel all notifications'), diff --git a/packages/tizen_notification/lib/src/enums.dart b/packages/tizen_notification/lib/src/enums.dart deleted file mode 100644 index 328d269b6..000000000 --- a/packages/tizen_notification/lib/src/enums.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// The type of sound. -enum SoundType { - /// No sound. - none, - - /// Default sound. - builtIn, - - /// User sound data. - userData, -} - -/// The type of vibration. -enum VibrationType { - /// No vibration. - none, - - /// Default vibrate pattern. - builtIn, - - /// User vibration data. - userData, -} diff --git a/packages/tizen_notification/lib/src/types.dart b/packages/tizen_notification/lib/src/types.dart index 2e6acf212..8548cc6b8 100644 --- a/packages/tizen_notification/lib/src/types.dart +++ b/packages/tizen_notification/lib/src/types.dart @@ -2,102 +2,199 @@ // 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 'package:tizen_app_control/tizen_app_control.dart'; -import 'enums.dart'; +export 'package:tizen_app_control/tizen_app_control.dart'; -/// Properties that configure how the system handles notification at -/// certain scenarios. -class Property { - /// Display only SIM card inserted. - static const int displayOnlySimMode = 1 << 0; +/// How the system handles the notification in certain scenarios. +class NotificationProperty { + // The constants below are originally defined in notification_type.h as + // the enum type _notification_property. + NotificationProperty._(); - /// Disable application launch when notification is selected. - static const int disableAppLaunch = 1 << 1; + /// Display only if a SIM card is inserted. + static const int onlySimMode = 0x00000001; - /// Disable auto delete when notification is selected. - static const int disableAutoDelete = 1 << 2; + /// Do not perform any operation when the notification is clicked. + static const int disableAppLaunch = 0x00000002; - /// Delete notification when device is rebooted. - static const int volatileDisplay = 1 << 8; + /// Do not dismiss the notification when clicked. + static const int disableAutoDelete = 0x00000004; + + /// Dismiss the notification when the device is rebooted. + static const int volatile = 0x00000100; } -/// The destination app that displays notification. -class DisplayApplist { - /// Notification Tray(Quickpanel). - static const int tray = 1 << 0; +/// Where and how the notification should be presented. +class NotificationStyle { + // The constants below are originally defined in notification_type.h as + // the enum type _notification_display_applist. + NotificationStyle._(); - /// Ticker notification. - static const int ticker = 1 << 1; + /// Display in the notification area of the quick panel. + static const int tray = 0x00000001; - /// Lock screen. - static const int lock = 1 << 2; + /// Display in the lock screen. + static const int lock = 0x00000004; - /// Indicator. - static const int indicator = 1 << 3; + /// Display in the indicator area (the top of the screen). + static const int indicator = 0x00000002 | 0x00000008; - /// All display application. - static const int all = (1 << 4) - 1; + /// All of the above. + static const int all = tray | lock | indicator; } -class NotificationImage { - /// [NotificationImage] specifies image options for notifications. - NotificationImage({ - this.iconPath, - this.indicatorPath, - this.lockPath, +/// A set of icons to be shown in the notification layouts. +class NotificationIcons { + /// Creates a [NotificationIcons] with the given icon paths. + NotificationIcons({ + this.icon, + this.indicatorIcon, + this.lockIcon, }); - /// The path of icon. - final String? iconPath; + /// The path to the icon file. + String? icon; - /// The path of indicator icon. - final String? indicatorPath; + /// The path to the indicator icon file. + String? indicatorIcon; - /// The path of lock screen icon. - final String? lockPath; + /// The path to the lock screen icon file. + String? lockIcon; - /// Returns [NotificationImage] member fields in a map format. - Map toMap() => { - 'icon': iconPath, - 'iconForIndicator': indicatorPath, - 'iconForLock': lockPath, + /// Converts to a map. + Map toMap() => { + 'icon': icon, + 'iconForIndicator': indicatorIcon, + 'iconForLock': lockIcon, }; } +/// The type of sound. +enum SoundType { + /// No sound. + none, + + /// Default sound. + builtIn, + + /// User sound data. + userData, +} + +/// The sound to play when the notification is presented. class NotificationSound { - /// [NotificationSound] specifies sound options for notifications. - /// - /// [path] refers to a file path to custom sound data played on notification, - /// this value is ignored When [type] is not [SoundType.userData]. - NotificationSound({ - required this.type, - this.path, - }); + /// Creates a [NotificationSound] with the given [type] and [path]. + NotificationSound({required this.type, this.path}); + + /// The type of sound. + SoundType type; - final SoundType type; + /// The path to the user sound data file. + /// + /// Only applicable if the [type] is [SoundType.userData]. String? path; - /// Returns [NotificationSound] member fields in a map format. - Map toMap() => { + /// Converts to a map. + Map toMap() => { 'type': type.name, 'path': path, }; } +/// The type of vibration. +enum VibrationType { + /// No vibration. + none, + + /// Default vibration pattern. + builtIn, + + /// User vibration data. + userData, +} + +/// The notification vibration options. class NotificationVibration { - /// [NotificationVibration] specifies vibration options for notifications. - NotificationVibration({ - required this.type, - this.path, - }); + /// Creates a [NotificationVibration] with the given [type] and [path]. + NotificationVibration({required this.type, this.path}); - final VibrationType type; + /// The type of vibration. + VibrationType type; + + /// The path to the user vibration data file. + /// + /// Only applicable if the [type] is [VibrationType.userData]. String? path; - /// Returns [NotificationVibration] member fields in a map format. - Map toMap() => { + /// Converts to a map. + Map toMap() => { 'type': type.name, 'path': path, }; } + +extension _AppControlToMap on AppControl { + /// Converts to a map. + Map toMap() => { + 'appId': appId, + 'operation': operation, + 'uri': uri, + 'mime': mime, + 'extraData': extraData, + }; +} + +/// The notification details. +class TizenNotificationDetails { + /// Constructs a [TizenNotificationDetails] from the given properties. + TizenNotificationDetails({ + this.icons, + this.sound, + this.vibration, + this.properties = 0, + this.style = NotificationStyle.all, + this.ongoing = false, + this.appControl, + }); + + /// A set of icons to be shown in the notification layouts. + NotificationIcons? icons; + + /// The sound to play when the notification is presented. + NotificationSound? sound; + + /// The notification vibration options. + NotificationVibration? vibration; + + /// Specifies how the system handles the notification in certain scenarios. + /// + /// Multiple [NotificationProperty] values can be set using the bitwise OR + /// operator (|). + int properties; + + /// Specifies where and how the notification should be presented. + /// + /// Multiple [NotificationStyle] values can be set using the bitwise OR + /// operator (|). + int style; + + /// Whether the notification can be dismissed by user. + /// + /// Only supported on Raspberry Pi (common profile) devices. + bool ongoing; + + /// A control message to be sent when the notification is clicked. + AppControl? appControl; + + /// Converts to a map. + Map toMap() => { + 'image': icons?.toMap(), + 'sound': sound?.toMap(), + 'vibration': vibration?.toMap(), + 'properties': properties, + 'displayApplist': style, + 'ongoing': ongoing, + 'appControl': appControl?.toMap(), + }; +} diff --git a/packages/tizen_notification/lib/tizen_notification.dart b/packages/tizen_notification/lib/tizen_notification.dart index 6e1341437..3555a66b7 100644 --- a/packages/tizen_notification/lib/tizen_notification.dart +++ b/packages/tizen_notification/lib/tizen_notification.dart @@ -3,103 +3,44 @@ // found in the LICENSE file. import 'package:flutter/services.dart'; -import 'package:tizen_app_control/tizen_app_control.dart'; import 'src/types.dart'; -export 'package:tizen_app_control/tizen_app_control.dart'; - -export 'src/enums.dart'; export 'src/types.dart'; -/// Contains detailed information about a notification being sent. -class TizenNotificationDetails { - /// Constructs a [TizenNotificationDetails] from the given properties. - /// - /// The properties are all optional. - const TizenNotificationDetails({ - this.image, - this.appControl, - this.vibration, - this.sound, - this.properties = 0, - this.displayApplist = DisplayApplist.all, - this.ongoing = false, - }); - - /// [NotificationImage] refers to any image shown on the notification panel. - final NotificationImage? image; - - /// A set of information used by app control to launch other applications. - final AppControl? appControl; - - /// The vibration triggered when a notification is received. - final NotificationVibration? vibration; - - /// The sound played when a notification is received. - final NotificationSound? sound; - - /// Properties that configure how the system handles notification at - /// certain scenarios. - /// - /// [properties] can have multiple [Property] values with the bitwise OR operator. - /// For example, [Property.disableAppLaunch] | [Property.volatileDisplay]. - final int properties; - - /// The destination app that displays notification. - /// - /// The notification can be sent to multiple apps by listing values with the - /// bitwise OR operator. - /// For example, [DisplayApplist.tray] | [DisplayApplist.indicator]. - final int displayApplist; - - /// If [ongoing] is true, the user is not able to delete the notification. - /// - /// Currently, only the common profile supports this operation. - final bool ongoing; - - /// Returns [TizenNotificationDetails] member fields in a map format. - Map toMap() => { - 'displayApplist': displayApplist, - 'properties': properties, - 'vibration': vibration?.toMap(), - 'sound': sound?.toMap(), - 'image': image?.toMap(), - 'ongoing': ongoing, - 'appControl': { - 'appId': appControl?.appId, - 'operation': appControl?.operation, - 'uri': appControl?.uri, - 'mime': appControl?.mime, - 'extraData': appControl?.extraData, - }, - }; -} - -const MethodChannel _channel = MethodChannel('tizen/notification'); - -/// A handle for sending notifications to Tizen. +/// Provides functionality for displaying notifications. class TizenNotificationPlugin { - /// Removes a notification with [id]. - Future cancel(int id) => _channel.invokeMethod('cancel', id.toString()); + final MethodChannel _channel = const MethodChannel('tizen/notification'); - /// Removes all notifications sent. - Future cancelAll() => _channel.invokeMethod('cancelAll'); - - /// Sends the notification with [id], [title], and [body]. [id] is the identifier - /// of the notification which can later be passed to [cancel] to erase the - /// notification. + /// Displays a notification with the given properties. + /// + /// [id] is the identifier of the notification which can later be used to + /// remove the notification. Future show( int id, { - String? title, - String? body, + String title = '', + String body = '', TizenNotificationDetails? notificationDetails, }) { - final Map details = - notificationDetails?.toMap() ?? {}; + final Map details = + notificationDetails?.toMap() ?? {}; details['id'] = id.toString(); - details['title'] = title ?? ''; - details['body'] = body ?? ''; + details['title'] = title; + details['body'] = body; + + // Set disableAppLaunch automatically if appControl is unset. + if (notificationDetails?.appControl == null) { + final int properties = details['properties']! as int; + details['properties'] = + properties | NotificationProperty.disableAppLaunch; + } + return _channel.invokeMethod('show', details); } + + /// Removes a notification with the specified [id]. + Future cancel(int id) => _channel.invokeMethod('cancel', id.toString()); + + /// Removes all notifications. + Future cancelAll() => _channel.invokeMethod('cancelAll'); } diff --git a/packages/tizen_notification/pubspec.yaml b/packages/tizen_notification/pubspec.yaml index 3d00f7121..81fda8007 100644 --- a/packages/tizen_notification/pubspec.yaml +++ b/packages/tizen_notification/pubspec.yaml @@ -2,7 +2,7 @@ name: tizen_notification description: Tizen notification APIs. Used to show and delete notifications on a Tizen device. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/tizen_notification -version: 0.1.1 +version: 0.2.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - tizen_app_control: ^0.2.0 + tizen_app_control: ^0.2.2 flutter: plugin: diff --git a/packages/tizen_notification/tizen/src/tizen_notification_plugin.cc b/packages/tizen_notification/tizen/src/tizen_notification_plugin.cc index 3f47df422..fcaab0150 100644 --- a/packages/tizen_notification/tizen/src/tizen_notification_plugin.cc +++ b/packages/tizen_notification/tizen/src/tizen_notification_plugin.cc @@ -11,11 +11,29 @@ #include #include +#include #include #include +#include +#include #include "log.h" +namespace { + +template +static bool GetValueFromEncodableMap(const flutter::EncodableMap *map, + const char *key, T &out) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto *value = std::get_if(&iter->second)) { + out = *value; + return true; + } + } + return false; +} + class TizenNotificationPlugin : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrar *registrar) { @@ -39,479 +57,377 @@ class TizenNotificationPlugin : public flutter::Plugin { virtual ~TizenNotificationPlugin() {} private: - template - bool GetValueFromArgs(const flutter::EncodableValue *args, const char *key, - T &out) { - if (std::holds_alternative(*args)) { - flutter::EncodableMap map = std::get(*args); - if (map.find(flutter::EncodableValue(key)) != map.end()) { - flutter::EncodableValue value = map[flutter::EncodableValue(key)]; - if (std::holds_alternative(value)) { - out = std::get(value); - return true; - } - } - } - return false; - } - - bool GetEncodableValueFromArgs(const flutter::EncodableValue *args, - const char *key, - flutter::EncodableValue &out) { - if (std::holds_alternative(*args)) { - flutter::EncodableMap map = std::get(*args); - if (map.find(flutter::EncodableValue(key)) != map.end()) { - out = map[flutter::EncodableValue(key)]; - return true; - } - } - return false; - } - - void FreeNotification(notification_h ¬i) { - int ret = notification_free(noti); + void FreeNotification(notification_h handle) { + int ret = notification_free(handle); if (ret != NOTIFICATION_ERROR_NONE) { - LOG_ERROR("notification_free failed : %s", get_error_message(ret)); + LOG_ERROR("notification_free failed: %s", get_error_message(ret)); } } - void DestroyAppControl(app_control_h &app_control) { + void DestroyAppControl(app_control_h app_control) { int ret = app_control_destroy(app_control); if (ret != APP_CONTROL_ERROR_NONE) { - LOG_ERROR("app_control_destroy failed : %s", get_error_message(ret)); + LOG_ERROR("app_control_destroy failed: %s", get_error_message(ret)); + } + } + + notification_image_type_e StringToImageType(const std::string &type) { + if (type == "icon") { + return NOTIFICATION_IMAGE_TYPE_ICON; + } else if (type == "iconForIndicator") { + return NOTIFICATION_IMAGE_TYPE_ICON_FOR_INDICATOR; + } else if (type == "iconForLock") { + return NOTIFICATION_IMAGE_TYPE_ICON_FOR_LOCK; } + return NOTIFICATION_IMAGE_TYPE_ICON; } - notification_sound_type_e StringToSoundType(std::string sound_str) { - notification_sound_type_e ret = NOTIFICATION_SOUND_TYPE_NONE; - if (sound_str.compare("none") == 0) { - ret = NOTIFICATION_SOUND_TYPE_NONE; - } else if (sound_str.compare("builtIn") == 0) { - ret = NOTIFICATION_SOUND_TYPE_DEFAULT; - } else if (sound_str.compare("userData") == 0) { - ret = NOTIFICATION_SOUND_TYPE_USER_DATA; + notification_sound_type_e StringToSoundType(const std::string &type) { + if (type == "none") { + return NOTIFICATION_SOUND_TYPE_NONE; + } else if (type == "builtIn") { + return NOTIFICATION_SOUND_TYPE_DEFAULT; + } else if (type == "userData") { + return NOTIFICATION_SOUND_TYPE_USER_DATA; } - return ret; + return NOTIFICATION_SOUND_TYPE_NONE; } - notification_vibration_type_e StringToVibrationType( - std::string vibration_str) { - notification_vibration_type_e ret = NOTIFICATION_VIBRATION_TYPE_NONE; - if (vibration_str.compare("none") == 0) { - ret = NOTIFICATION_VIBRATION_TYPE_NONE; - } else if (vibration_str.compare("builtIn") == 0) { - ret = NOTIFICATION_VIBRATION_TYPE_DEFAULT; - } else if (vibration_str.compare("userData") == 0) { - ret = NOTIFICATION_VIBRATION_TYPE_USER_DATA; + notification_vibration_type_e StringToVibrationType(const std::string &type) { + if (type == "none") { + return NOTIFICATION_VIBRATION_TYPE_NONE; + } else if (type == "builtIn") { + return NOTIFICATION_VIBRATION_TYPE_DEFAULT; + } else if (type == "userData") { + return NOTIFICATION_VIBRATION_TYPE_USER_DATA; } - return ret; + return NOTIFICATION_VIBRATION_TYPE_NONE; } - notification_image_type_e StringToImageType(std::string image_type) { - notification_image_type_e ret = NOTIFICATION_IMAGE_TYPE_ICON; - if (image_type.compare("icon") == 0) { - ret = NOTIFICATION_IMAGE_TYPE_ICON; - } else if (image_type.compare("iconForIndicator") == 0) { - ret = NOTIFICATION_IMAGE_TYPE_ICON_FOR_INDICATOR; - } else if (image_type.compare("iconForLock") == 0) { - ret = NOTIFICATION_IMAGE_TYPE_ICON_FOR_LOCK; + std::string ToAbsolutePath(const std::string &path) { + std::filesystem::path filesystem_path(path); + if (filesystem_path.is_absolute()) { + return path; } - return ret; + char *res_path = app_get_shared_resource_path(); + std::string result = std::string(res_path) + path; + free(res_path); + return result; } void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { - if (method_call.method_name().compare("show") == 0) { - if (method_call.arguments()) { - const flutter::EncodableValue *arguments = method_call.arguments(); - notification_h noti_handle = nullptr; - app_control_h app_control = nullptr; + const auto &method_name = method_call.method_name(); - int ret = NOTIFICATION_ERROR_NONE; - std::string id; - std::string title; - std::string body; - flutter::EncodableValue app_control_data; - - if (GetValueFromArgs(arguments, "id", id)) { - noti_handle = notification_load_by_tag(id.c_str()); - if (noti_handle) { - ret = notification_delete(noti_handle); - if (ret != NOTIFICATION_ERROR_NONE) { - LOG_ERROR("notification_delete failed : %s", - get_error_message(ret)); - result->Error("notification_delete failed", - std::string(get_error_message(ret))); - return; - } - } + if (method_name == "show") { + const auto *arguments = + std::get_if(method_call.arguments()); + if (!arguments) { + result->Error("Invalid argument", "The argument must be a map."); + } - bool ongoing; - if (GetValueFromArgs(arguments, "ongoing", ongoing)) { - if (ongoing) { - noti_handle = notification_create(NOTIFICATION_TYPE_ONGOING); - if (!noti_handle) { - LOG_ERROR( - "notification_create failed : Fail to create notification"); - result->Error("notification_create failed", - "Fail to create notification"); - return; - } - ret = notification_set_layout(noti_handle, - NOTIFICATION_LY_ONGOING_EVENT); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_layout failed : %s", - get_error_message(ret)); - result->Error("notification_set_layout failed", - std::string(get_error_message(ret))); - return; - } - } else { - noti_handle = notification_create(NOTIFICATION_TYPE_NOTI); - if (!noti_handle) { - LOG_ERROR( - "notification_create failed : Fail to create notification"); - result->Error("notification_create failed", - "Fail to create notification"); - return; - } - } - } - ret = notification_set_tag(noti_handle, id.c_str()); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_tag failed : %s", - get_error_message(ret)); - result->Error("notification_set_tag failed", - std::string(get_error_message(ret))); - return; - } - } + std::string id; + if (!GetValueFromEncodableMap(arguments, "id", id)) { + result->Error("Invalid argument", "No id provided."); + return; + } - if (GetValueFromArgs(arguments, "title", title)) { - ret = notification_set_text(noti_handle, NOTIFICATION_TEXT_TYPE_TITLE, - title.c_str(), nullptr, - NOTIFICATION_VARIABLE_TYPE_NONE); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_text failed : %s", - get_error_message(ret)); - result->Error("notification_set_text failed", - std::string(get_error_message(ret))); - return; - } + notification_h handle = notification_load_by_tag(id.c_str()); + if (handle) { + // The notification with the ID already exists. Delete it. + int ret = notification_delete(handle); + if (ret != NOTIFICATION_ERROR_NONE) { + result->Error("notification_delete failed", get_error_message(ret)); + return; } + } - if (GetValueFromArgs(arguments, "body", body)) { - ret = notification_set_text( - noti_handle, NOTIFICATION_TEXT_TYPE_CONTENT, body.c_str(), - nullptr, NOTIFICATION_VARIABLE_TYPE_NONE); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_text failed : %s", - get_error_message(ret)); - result->Error("notification_set_text failed", - std::string(get_error_message(ret))); - return; - } + bool ongoing = false; + GetValueFromEncodableMap(arguments, "ongoing", ongoing); + if (ongoing) { + handle = notification_create(NOTIFICATION_TYPE_ONGOING); + if (!handle) { + result->Error("notification_create failed", + "Failed to create an ongoing notification."); + return; } - - flutter::EncodableMap images; - if (GetValueFromArgs(arguments, "image", images)) { - for (const auto &image : images) { - std::string type; - std::string path; - char *resource_path = app_get_shared_resource_path(); - if (std::holds_alternative(image.first)) { - type = std::get(image.first); - } - if (std::holds_alternative(image.second)) { - path = std::get(image.second); - path = std::string(resource_path) + path; - } - ret = notification_set_image(noti_handle, StringToImageType(type), - path.c_str()); - free(resource_path); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_image failed : %s", + int ret = + notification_set_layout(handle, NOTIFICATION_LY_ONGOING_EVENT); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_layout failed", get_error_message(ret)); - result->Error("notification_set_image failed", - std::string(get_error_message(ret))); - return; - } - } + return; } + } else { + handle = notification_create(NOTIFICATION_TYPE_NOTI); + if (!handle) { + result->Error("notification_create failed", + "Failed to create a notification."); + return; + } + } - int32_t display_applist; - if (GetValueFromArgs(arguments, "displayApplist", display_applist)) { - ret = notification_set_display_applist(noti_handle, display_applist); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_display_applist failed : %s", - get_error_message(ret)); - result->Error("notification_set_display_applist failed", - std::string(get_error_message(ret))); - return; - } + int ret = notification_set_tag(handle, id.c_str()); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_tag failed", get_error_message(ret)); + return; + } + + std::string title; + if (GetValueFromEncodableMap(arguments, "title", title)) { + ret = notification_set_text(handle, NOTIFICATION_TEXT_TYPE_TITLE, + title.c_str(), nullptr, + NOTIFICATION_VARIABLE_TYPE_NONE); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_text failed", get_error_message(ret)); + return; + } + } + + std::string body; + if (GetValueFromEncodableMap(arguments, "body", body)) { + ret = notification_set_text(handle, NOTIFICATION_TEXT_TYPE_CONTENT, + body.c_str(), nullptr, + NOTIFICATION_VARIABLE_TYPE_NONE); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_text failed", get_error_message(ret)); + return; } + } - int32_t properties; - if (GetValueFromArgs(arguments, "properties", properties)) { - ret = notification_set_property(noti_handle, properties); + flutter::EncodableMap images; + if (GetValueFromEncodableMap(arguments, "image", images)) { + for (const auto &image : images) { + std::string type; + if (std::holds_alternative(image.first)) { + type = std::get(image.first); + } + std::string path; + if (std::holds_alternative(image.second)) { + path = ToAbsolutePath(std::get(image.second)); + } + ret = notification_set_image(handle, StringToImageType(type), + path.c_str()); if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_property failed : %s", - get_error_message(ret)); - result->Error("notification_set_property failed", - std::string(get_error_message(ret))); + FreeNotification(handle); + result->Error("notification_set_image failed", + get_error_message(ret)); return; } } + } - flutter::EncodableValue sound; - if (GetEncodableValueFromArgs(arguments, "sound", sound)) { - std::string type; - if (GetValueFromArgs(&sound, "type", type)) { - std::string path; - char *resource_path = app_get_shared_resource_path(); - if (GetValueFromArgs(&sound, "path", path)) { - path = std::string(resource_path) + path; - } - ret = notification_set_sound(noti_handle, StringToSoundType(type), - path.c_str()); - free(resource_path); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_sound failed : %s", + int32_t display_applist = 0; + if (GetValueFromEncodableMap(arguments, "displayApplist", + display_applist)) { + ret = notification_set_display_applist(handle, display_applist); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_display_applist failed", get_error_message(ret)); - result->Error("notification_set_sound failed", - std::string(get_error_message(ret))); - return; - } - } + return; } + } - flutter::EncodableValue vibration; - if (GetEncodableValueFromArgs(arguments, "vibration", vibration)) { - std::string type; - if (GetValueFromArgs(&vibration, "type", type)) { - std::string path; - char *resource_path = app_get_shared_resource_path(); - if (GetValueFromArgs(&vibration, "path", path)) { - path = std::string(resource_path) + path; - } - ret = notification_set_vibration( - noti_handle, StringToVibrationType(type), path.c_str()); - free(resource_path); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_set_vibration failed : %s", + int32_t properties = 0; + if (GetValueFromEncodableMap(arguments, "properties", properties)) { + ret = notification_set_property(handle, properties); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_property failed", get_error_message(ret)); - result->Error("notification_set_vibration failed", - std::string(get_error_message(ret))); - return; - } - } + return; } + } - if (GetEncodableValueFromArgs(arguments, "appControl", - app_control_data)) { - std::string app_id; - if (GetValueFromArgs(&app_control_data, "appId", app_id)) { - std::string operation; - std::string uri; - std::string category; - std::string mime; - flutter::EncodableMap extras; - flutter::EncodableValue extra_value; - std::string key; - ret = app_control_create(&app_control); - if (ret != APP_CONTROL_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("app_control_create failed : %s", + flutter::EncodableMap sound; + if (GetValueFromEncodableMap(arguments, "sound", sound)) { + std::string type; + GetValueFromEncodableMap(&sound, "type", type); + std::string path; + if (GetValueFromEncodableMap(&sound, "path", path)) { + path = ToAbsolutePath(path); + } + ret = notification_set_sound(handle, StringToSoundType(type), + path.c_str()); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_sound failed", get_error_message(ret)); - result->Error("app_control_create failed", - std::string(get_error_message(ret))); - return; - } + return; + } + } - ret = app_control_set_app_id(app_control, app_id.c_str()); - if (ret != APP_CONTROL_ERROR_NONE) { - DestroyAppControl(app_control); - FreeNotification(noti_handle); - LOG_ERROR("app_control_set_app_id failed : %s", + flutter::EncodableMap vibration; + if (GetValueFromEncodableMap(arguments, "vibration", vibration)) { + std::string type; + GetValueFromEncodableMap(&vibration, "type", type); + std::string path; + if (GetValueFromEncodableMap(&vibration, "path", path)) { + path = ToAbsolutePath(path); + } + ret = notification_set_vibration(handle, StringToVibrationType(type), + path.c_str()); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_set_vibration failed", get_error_message(ret)); - result->Error("app_control_set_app_id failed", - std::string(get_error_message(ret))); - return; - } + return; + } + } - if (GetValueFromArgs(&app_control_data, "operation", operation)) { - ret = app_control_set_operation(app_control, operation.c_str()); - if (ret != APP_CONTROL_ERROR_NONE) { - DestroyAppControl(app_control); - FreeNotification(noti_handle); - LOG_ERROR("app_control_set_operation failed : %s", - get_error_message(ret)); - result->Error("app_control_set_operation failed", - std::string(get_error_message(ret))); - return; - } - } + flutter::EncodableMap app_control_data; + if (GetValueFromEncodableMap(arguments, "appControl", app_control_data)) { + app_control_h app_control = nullptr; + ret = app_control_create(&app_control); + if (ret != APP_CONTROL_ERROR_NONE) { + FreeNotification(handle); + result->Error("app_control_create failed", get_error_message(ret)); + return; + } - if (GetValueFromArgs(&app_control_data, "uri", uri)) { - ret = app_control_set_uri(app_control, uri.c_str()); - if (ret != APP_CONTROL_ERROR_NONE) { - DestroyAppControl(app_control); - FreeNotification(noti_handle); - LOG_ERROR("app_control_set_uri failed : %s", + std::string app_id; + if (GetValueFromEncodableMap(&app_control_data, "appId", app_id)) { + ret = app_control_set_app_id(app_control, app_id.c_str()); + if (ret != APP_CONTROL_ERROR_NONE) { + DestroyAppControl(app_control); + FreeNotification(handle); + result->Error("app_control_set_app_id failed", get_error_message(ret)); - result->Error("app_control_set_uri failed", - std::string(get_error_message(ret))); - return; - } - } + return; + } + } - if (GetValueFromArgs(&app_control_data, "category", category)) { - ret = app_control_set_category(app_control, category.c_str()); - if (ret != APP_CONTROL_ERROR_NONE) { - DestroyAppControl(app_control); - FreeNotification(noti_handle); - LOG_ERROR("app_control_set_category failed : %s", + std::string operation; + if (GetValueFromEncodableMap(&app_control_data, "operation", + operation)) { + ret = app_control_set_operation(app_control, operation.c_str()); + if (ret != APP_CONTROL_ERROR_NONE) { + DestroyAppControl(app_control); + FreeNotification(handle); + result->Error("app_control_set_operation failed", get_error_message(ret)); - result->Error("app_control_set_category failed", - std::string(get_error_message(ret))); - return; - } - } + return; + } + } - if (GetValueFromArgs(&app_control_data, "mime", mime)) { - ret = app_control_set_mime(app_control, mime.c_str()); - if (ret != APP_CONTROL_ERROR_NONE) { - DestroyAppControl(app_control); - FreeNotification(noti_handle); - LOG_ERROR("app_control_set_mime failed : %s", - get_error_message(ret)); - result->Error("app_control_set_mime failed", - std::string(get_error_message(ret))); - return; - } - } + std::string uri; + if (GetValueFromEncodableMap(&app_control_data, "uri", uri)) { + ret = app_control_set_uri(app_control, uri.c_str()); + if (ret != APP_CONTROL_ERROR_NONE) { + DestroyAppControl(app_control); + FreeNotification(handle); + result->Error("app_control_set_uri failed", get_error_message(ret)); + return; + } + } - if (GetValueFromArgs(&app_control_data, "extraData", extras)) { - extra_value = extras; - for (const auto &extra : extras) { - std::string map_key; - if (std::holds_alternative(extra.first)) { - map_key = std::get(extra.first); - flutter::EncodableList value_list; - std::vector values; - std::vector dummy; - std::string value; - if (GetValueFromArgs(&extra_value, map_key.c_str(), - value_list)) { - for (size_t i = 0; i < value_list.size(); i++) { - dummy.push_back(std::get(value_list[i])); - } - for (size_t i = 0; i < value_list.size(); i++) { - values.push_back(dummy[i].c_str()); - } - app_control_add_extra_data_array( - app_control, map_key.c_str(), values.data(), - values.size()); - } else if (GetValueFromArgs(&extra_value, map_key.c_str(), - value)) { - app_control_add_extra_data(app_control, map_key.c_str(), - value.c_str()); - } - } - } - ret = notification_set_launch_option( - noti_handle, NOTIFICATION_LAUNCH_OPTION_APP_CONTROL, - (void *)app_control); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - DestroyAppControl(app_control); - LOG_ERROR("notification_set_launch_option failed : %s", + std::string category; + if (GetValueFromEncodableMap(&app_control_data, "category", category)) { + ret = app_control_set_category(app_control, category.c_str()); + if (ret != APP_CONTROL_ERROR_NONE) { + DestroyAppControl(app_control); + FreeNotification(handle); + result->Error("app_control_set_category failed", get_error_message(ret)); - result->Error("notification_set_launch_option failed", - std::string(get_error_message(ret))); - return; - } - } + return; } } - if (app_control) { - ret = app_control_destroy(app_control); + std::string mime; + if (GetValueFromEncodableMap(&app_control_data, "mime", mime)) { + ret = app_control_set_mime(app_control, mime.c_str()); if (ret != APP_CONTROL_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("app_control_destroy failed : %s", - get_error_message(ret)); - result->Error("app_control_destroy failed", - std::string(get_error_message(ret))); + DestroyAppControl(app_control); + FreeNotification(handle); + result->Error("app_control_set_mime failed", + get_error_message(ret)); return; } } - ret = notification_post(noti_handle); - if (ret != NOTIFICATION_ERROR_NONE) { - FreeNotification(noti_handle); - LOG_ERROR("notification_post failed : %s", get_error_message(ret)); - result->Error("notification_post failed", - std::string(get_error_message(ret))); - return; + + flutter::EncodableMap extras; + if (GetValueFromEncodableMap(&app_control_data, "extraData", extras)) { + for (const auto &extra : extras) { + if (std::holds_alternative(extra.first)) { + const std::string &key = std::get(extra.first); + flutter::EncodableList value_list; + std::string value; + if (GetValueFromEncodableMap(&extras, key.c_str(), value_list)) { + std::vector dummy; + for (const flutter::EncodableValue &value : value_list) { + dummy.push_back(std::get(value)); + } + std::vector values; + for (const std::string &value : dummy) { + values.push_back(value.c_str()); + } + app_control_add_extra_data_array(app_control, key.c_str(), + values.data(), values.size()); + } else if (GetValueFromEncodableMap(&extras, key.c_str(), + value)) { + app_control_add_extra_data(app_control, key.c_str(), + value.c_str()); + } + } + } } - ret = notification_free(noti_handle); + + ret = notification_set_launch_option( + handle, NOTIFICATION_LAUNCH_OPTION_APP_CONTROL, app_control); if (ret != NOTIFICATION_ERROR_NONE) { - LOG_ERROR("notification_free failed : %s", get_error_message(ret)); - result->Error("notification_free failed", - std::string(get_error_message(ret))); + FreeNotification(handle); + DestroyAppControl(app_control); + result->Error("notification_set_launch_option failed", + get_error_message(ret)); return; } - result->Success(); + DestroyAppControl(app_control); + } + + ret = notification_post(handle); + if (ret != NOTIFICATION_ERROR_NONE) { + FreeNotification(handle); + result->Error("notification_post failed", get_error_message(ret)); return; } - result->Error("InvalidArguments", "Please check 'show'"); - } else if (method_call.method_name().compare("cancel") == 0) { - const flutter::EncodableValue *arguments = method_call.arguments(); - if (arguments != nullptr && - std::holds_alternative(*arguments)) { - std::string id = std::get(*arguments); - notification_h noti_handle = nullptr; - noti_handle = notification_load_by_tag(id.c_str()); - if (noti_handle != nullptr) { - int ret = notification_delete(noti_handle); - if (ret != NOTIFICATION_ERROR_NONE) { - LOG_ERROR("notification_delete failed : %s", - get_error_message(ret)); - result->Error("notification_delete failed", - std::string(get_error_message(ret))); - return; - } - } - result->Success(); + FreeNotification(handle); + + result->Success(); + } else if (method_name == "cancel") { + const auto *id = std::get_if(method_call.arguments()); + if (!id) { + result->Error("Invalid argument", "The argument must be a string."); + } + + notification_h handle = notification_load_by_tag(id->c_str()); + if (!handle) { + result->Error("Invalid argument", + "No notification found with the given ID."); + return; + } + + int ret = notification_delete(handle); + if (ret != NOTIFICATION_ERROR_NONE) { + result->Error(std::to_string(ret), get_error_message(ret)); return; } - result->Error("InvalidArguments", "Please check 'cancel'"); - } else if (method_call.method_name().compare("cancelAll") == 0) { - int ret = NOTIFICATION_ERROR_NONE; - ret = notification_delete_all(NOTIFICATION_TYPE_NOTI); + result->Success(); + } else if (method_name == "cancelAll") { + int ret = notification_delete_all(NOTIFICATION_TYPE_NOTI); if (ret != NOTIFICATION_ERROR_NONE) { - LOG_ERROR("notification_delete_all failed : %s", - get_error_message(ret)); - result->Error("notification_delete_all failed", - std::string(get_error_message(ret))); + result->Error(std::to_string(ret), get_error_message(ret)); return; } + ret = notification_delete_all(NOTIFICATION_TYPE_ONGOING); if (ret != NOTIFICATION_ERROR_NONE) { - LOG_ERROR("notification_delete_all failed : %s", - get_error_message(ret)); - result->Error("notification_delete_all failed", - std::string(get_error_message(ret))); + result->Error(std::to_string(ret), get_error_message(ret)); return; } result->Success(); @@ -521,6 +437,8 @@ class TizenNotificationPlugin : public flutter::Plugin { } }; +} // namespace + void TizenNotificationPluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { TizenNotificationPlugin::RegisterWithRegistrar( diff --git a/packages/tizen_view/.gitignore b/packages/tizen_view/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/packages/tizen_view/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/tizen_view/CHANGELOG.md b/packages/tizen_view/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/packages/tizen_view/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/tizen_view/LICENSE b/packages/tizen_view/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/packages/tizen_view/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/tizen_view/README.md b/packages/tizen_view/README.md new file mode 100644 index 000000000..2eb2d8d6b --- /dev/null +++ b/packages/tizen_view/README.md @@ -0,0 +1,18 @@ +# tizen_view + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + +The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. +To add platforms, run `flutter create -t plugin --platforms .` in this directory. +You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. diff --git a/packages/tizen_view/example/.gitignore b/packages/tizen_view/example/.gitignore new file mode 100644 index 000000000..ad3c2ca04 --- /dev/null +++ b/packages/tizen_view/example/.gitignore @@ -0,0 +1,42 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/tizen_view/example/.metadata b/packages/tizen_view/example/.metadata new file mode 100644 index 000000000..0804fd01d --- /dev/null +++ b/packages/tizen_view/example/.metadata @@ -0,0 +1,45 @@ +# 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. + +version: + revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + channel: unknown + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + - platform: android + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + - platform: ios + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + - platform: linux + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + - platform: macos + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + - platform: web + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + - platform: windows + create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/tizen_view/example/README.md b/packages/tizen_view/example/README.md new file mode 100644 index 000000000..cf0b90526 --- /dev/null +++ b/packages/tizen_view/example/README.md @@ -0,0 +1,16 @@ +# tizen_view_example + +Demonstrates how to use the tizen_view 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://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/tizen_view/example/analysis_options.yaml b/packages/tizen_view/example/analysis_options.yaml new file mode 100644 index 000000000..61b6c4de1 --- /dev/null +++ b/packages/tizen_view/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/tizen_view/example/lib/main.dart b/packages/tizen_view/example/lib/main.dart new file mode 100644 index 000000000..200ca3e00 --- /dev/null +++ b/packages/tizen_view/example/lib/main.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:tizen_view/tizen_view.dart'; +import 'package:tizen_log/tizen_log.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + static const logTag = 'ASDF'; + + @override + void initState() { + super.initState(); + Log.error(logTag, 'CJS Init State'); + } + + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + final Map creationParams = {}; + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: TizenView( + viewType: 'plugins.flutter.io/tizenview', + layoutDirection: TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + /*body: Center( + child: TizenView(viewType: 'tizen_view_id'), + ),*/ + ), + ); + } +} diff --git a/packages/tizen_view/example/pubspec.yaml b/packages/tizen_view/example/pubspec.yaml new file mode 100644 index 000000000..769d4750f --- /dev/null +++ b/packages/tizen_view/example/pubspec.yaml @@ -0,0 +1,88 @@ +name: tizen_view_example +description: Demonstrates how to use the tizen_view plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter 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.18.0 <3.0.0' + flutter: ">=2.8.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + path_provider: ^2.0.7 + path_provider_tizen: + path: ../../path_provider/ + tizen_view: + # When depending on this package from a real application you should use: + # tizen_view: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + tizen_log: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/tizen_view/example/test/widget_test.dart b/packages/tizen_view/example/test/widget_test.dart new file mode 100644 index 000000000..c79253e9f --- /dev/null +++ b/packages/tizen_view/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 in the flutter_test package. 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:tizen_view_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const 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/tizen_view/example/tizen/.gitignore b/packages/tizen_view/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/tizen_view/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/tizen_view/example/tizen/App.cs b/packages/tizen_view/example/tizen/App.cs new file mode 100644 index 000000000..f345ce0f4 --- /dev/null +++ b/packages/tizen_view/example/tizen/App.cs @@ -0,0 +1,28 @@ +using Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + + + //registryPlatformView("plugins.flutter.io/tizenview", callback_); + } + + /*Evas_Object* callback_(Evas_Object* parent) + { + return nullptr; + }*/ + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/tizen_view/example/tizen/Runner.csproj b/packages/tizen_view/example/tizen/Runner.csproj new file mode 100644 index 000000000..f4e369d0c --- /dev/null +++ b/packages/tizen_view/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/tizen_view/example/tizen/shared/res/ic_launcher.png b/packages/tizen_view/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/tizen_view/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/tizen_view/example/tizen/tizen-manifest.xml b/packages/tizen_view/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..9649e538f --- /dev/null +++ b/packages/tizen_view/example/tizen/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + + T-INFOLINK2021-1000 + + + diff --git a/packages/webview_flutter/lib/src/platform_view_tizen.dart b/packages/tizen_view/lib/tizen_view.dart similarity index 90% rename from packages/webview_flutter/lib/src/platform_view_tizen.dart rename to packages/tizen_view/lib/tizen_view.dart index 66835e57b..e70c224ff 100644 --- a/packages/webview_flutter/lib/src/platform_view_tizen.dart +++ b/packages/tizen_view/lib/tizen_view.dart @@ -1,4 +1,4 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,7 +12,23 @@ // ignore_for_file: public_member_api_docs -part of '../webview_flutter_tizen.dart'; +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'tizen_view_platform_interface.dart'; + +/* +class TizenView { + Future getPlatformVersion() { + return TizenViewPlatform.instance.getPlatformVersion(); + } +} +*/ enum _PlatformViewState { uninitialized, @@ -21,6 +37,12 @@ enum _PlatformViewState { } class TizenView extends StatefulWidget { + register() {} + + Future getPlatformVersion() { + return TizenViewPlatform.instance.getPlatformVersion(); + } + const TizenView({ super.key, required this.viewType, @@ -36,7 +58,6 @@ class TizenView extends StatefulWidget { assert(creationParams == null || creationParamsCodec != null); final String viewType; - final PlatformViewCreatedCallback? onPlatformViewCreated; final PlatformViewHitTestBehavior hitTestBehavior; final TextDirection? layoutDirection; final Set>? gestureRecognizers; @@ -45,10 +66,12 @@ class TizenView extends StatefulWidget { final Clip clipBehavior; @override - State createState() => _TizenWebViewState(); + State createState() => _TizenViewState(); + + final PlatformViewCreatedCallback? onPlatformViewCreated; } -class _TizenWebViewState extends State { +class _TizenViewState extends State { int? _id; late TizenViewController _controller; TextDirection? _layoutDirection; @@ -77,8 +100,8 @@ class _TizenWebViewState extends State { return; } _initialized = true; - _createNewTizenWebView(); - _focusNode = FocusNode(debugLabel: 'TizenWebView(id: $_id)'); + _createNewTizenView(); + _focusNode = FocusNode(debugLabel: 'TizenView(id: $_id)'); } @override @@ -106,7 +129,7 @@ class _TizenWebViewState extends State { if (widget.viewType != oldWidget.viewType) { _controller.dispose(); - _createNewTizenWebView(); + _createNewTizenView(); return; } @@ -127,7 +150,7 @@ class _TizenWebViewState extends State { super.dispose(); } - void _createNewTizenWebView() { + void _createNewTizenView() { _id = platformViewsRegistry.getNextPlatformViewId(); _controller = PlatformViewsServiceTizen.initTizenView( id: _id!, @@ -168,7 +191,7 @@ class _TizenWebViewState extends State { } } -enum _TizenViewState { +enum _TizenPlatformViewState { waitingForSize, creating, created, @@ -192,8 +215,8 @@ class TizenViewController extends PlatformViewController { _creationParams = creationParams, _creationParamsCodec = creationParamsCodec, _state = waitingForSize - ? _TizenViewState.waitingForSize - : _TizenViewState.creating; + ? _TizenPlatformViewState.waitingForSize + : _TizenPlatformViewState.creating; @override final int viewId; @@ -202,7 +225,7 @@ class TizenViewController extends PlatformViewController { TextDirection _layoutDirection; - _TizenViewState _state; + _TizenPlatformViewState _state; final dynamic _creationParams; @@ -223,9 +246,9 @@ class TizenViewController extends PlatformViewController { Offset _off = Offset.zero; Future setSize(Size size) async { - assert(_state != _TizenViewState.disposed, + assert(_state != _TizenPlatformViewState.disposed, 'Tizen view is disposed. View id: $viewId'); - assert(_state != _TizenViewState.waitingForSize, + assert(_state != _TizenPlatformViewState.waitingForSize, 'Tizen view must have an initial size. View id: $viewId'); assert(size != null); assert(!size.isEmpty); @@ -250,7 +273,7 @@ class TizenViewController extends PlatformViewController { return; } - if (_state != _TizenViewState.created) { + if (_state != _TizenPlatformViewState.created) { return; } @@ -304,34 +327,34 @@ class TizenViewController extends PlatformViewController { @override Future create({Size? size}) async { - assert(_state != _TizenViewState.disposed, + assert(_state != _TizenPlatformViewState.disposed, 'trying to create a disposed Tizen view'); await _sendCreateMessage(size: size); - _state = _TizenViewState.created; + _state = _TizenPlatformViewState.created; for (final PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) { callback(viewId); } } - bool get isCreated => _state == _TizenViewState.created; + bool get isCreated => _state == _TizenPlatformViewState.created; void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { assert(listener != null); - assert(_state != _TizenViewState.disposed); + assert(_state != _TizenPlatformViewState.disposed); _platformViewCreatedCallbacks.add(listener); } /// Removes a callback added with [addOnPlatformViewCreatedListener]. void removeOnPlatformViewCreatedListener( PlatformViewCreatedCallback listener) { - assert(_state != _TizenViewState.disposed); + assert(_state != _TizenPlatformViewState.disposed); _platformViewCreatedCallbacks.remove(listener); } Future setLayoutDirection(TextDirection layoutDirection) async { - assert(_state != _TizenViewState.disposed, + assert(_state != _TizenPlatformViewState.disposed, 'trying to set a layout direction for a disposed UIView. View id: $viewId'); if (layoutDirection == _layoutDirection) { @@ -341,7 +364,7 @@ class TizenViewController extends PlatformViewController { assert(layoutDirection != null); _layoutDirection = layoutDirection; - if (_state == _TizenViewState.waitingForSize) { + if (_state == _TizenPlatformViewState.waitingForSize) { return; } @@ -384,7 +407,7 @@ class TizenViewController extends PlatformViewController { @override Future clearFocus() { - if (_state != _TizenViewState.created) { + if (_state != _TizenPlatformViewState.created) { return Future.value(); } return SystemChannels.platform_views @@ -393,12 +416,12 @@ class TizenViewController extends PlatformViewController { @override Future dispose() async { - if (_state == _TizenViewState.creating || - _state == _TizenViewState.created) { + if (_state == _TizenPlatformViewState.creating || + _state == _TizenPlatformViewState.created) { await _sendDisposeMessage(); } _platformViewCreatedCallbacks.clear(); - _state = _TizenViewState.disposed; + _state = _TizenPlatformViewState.disposed; PlatformViewsServiceTizen._instance._focusCallbacks.remove(viewId); } } diff --git a/packages/tizen_view/lib/tizen_view_method_channel.dart b/packages/tizen_view/lib/tizen_view_method_channel.dart new file mode 100644 index 000000000..894a82c59 --- /dev/null +++ b/packages/tizen_view/lib/tizen_view_method_channel.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'tizen_view_platform_interface.dart'; + +/// An implementation of [TizenViewPlatform] that uses method channels. +class MethodChannelTizenView extends TizenViewPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('tizen_view'); + + @override + Future getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/packages/tizen_view/lib/tizen_view_platform_interface.dart b/packages/tizen_view/lib/tizen_view_platform_interface.dart new file mode 100644 index 000000000..409db0337 --- /dev/null +++ b/packages/tizen_view/lib/tizen_view_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'tizen_view_method_channel.dart'; + +abstract class TizenViewPlatform extends PlatformInterface { + /// Constructs a TizenViewPlatform. + TizenViewPlatform() : super(token: _token); + + static final Object _token = Object(); + + static TizenViewPlatform _instance = MethodChannelTizenView(); + + /// The default instance of [TizenViewPlatform] to use. + /// + /// Defaults to [MethodChannelTizenView]. + static TizenViewPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [TizenViewPlatform] when + /// they register themselves. + static set instance(TizenViewPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/packages/tizen_view/pubspec.yaml b/packages/tizen_view/pubspec.yaml new file mode 100644 index 000000000..d8a6126dd --- /dev/null +++ b/packages/tizen_view/pubspec.yaml @@ -0,0 +1,25 @@ +name: tizen_view +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=2.18.0 <3.0.0' + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + platforms: + tizen: + pluginClass: TizenViewPlugin + fileName: tizen_view_plugin.h diff --git a/packages/tizen_view/test/tizen_view_method_channel_test.dart b/packages/tizen_view/test/tizen_view_method_channel_test.dart new file mode 100644 index 000000000..0de293893 --- /dev/null +++ b/packages/tizen_view/test/tizen_view_method_channel_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tizen_view/tizen_view_method_channel.dart'; + +void main() { + MethodChannelTizenView platform = MethodChannelTizenView(); + const MethodChannel channel = MethodChannel('tizen_view'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/packages/tizen_view/test/tizen_view_test.dart b/packages/tizen_view/test/tizen_view_test.dart new file mode 100644 index 000000000..f3c3b600a --- /dev/null +++ b/packages/tizen_view/test/tizen_view_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tizen_view/tizen_view.dart'; +import 'package:tizen_view/tizen_view_platform_interface.dart'; +import 'package:tizen_view/tizen_view_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockTizenViewPlatform + with MockPlatformInterfaceMixin + implements TizenViewPlatform { + + @override + Future getPlatformVersion() => Future.value('42'); +} + +void main() { + final TizenViewPlatform initialPlatform = TizenViewPlatform.instance; + + test('$MethodChannelTizenView is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + TizenView tizenViewPlugin = TizenView(); + MockTizenViewPlatform fakePlatform = MockTizenViewPlatform(); + TizenViewPlatform.instance = fakePlatform; + + expect(await tizenViewPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/packages/tizen_view/tizen/.gitignore b/packages/tizen_view/tizen/.gitignore new file mode 100644 index 000000000..a2a7d62b1 --- /dev/null +++ b/packages/tizen_view/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/tizen_view/tizen/inc/tizen_view_plugin.h b/packages/tizen_view/tizen/inc/tizen_view_plugin.h new file mode 100644 index 000000000..c8391f24a --- /dev/null +++ b/packages/tizen_view/tizen/inc/tizen_view_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_TIZEN_VIEW_PLUGIN_H_ +#define FLUTTER_PLUGIN_TIZEN_VIEW_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 TizenViewPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_TIZEN_VIEW_PLUGIN_H_ diff --git a/packages/google_sign_in/tizen/project_def.prop b/packages/tizen_view/tizen/project_def.prop similarity index 91% rename from packages/google_sign_in/tizen/project_def.prop rename to packages/tizen_view/tizen/project_def.prop index 6535b3b88..6f8828541 100644 --- a/packages/google_sign_in/tizen/project_def.prop +++ b/packages/tizen_view/tizen/project_def.prop @@ -1,7 +1,7 @@ # See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion # for details. -APPNAME = google_sign_in_tizen_plugin +APPNAME = tizen_view_plugin type = staticLib profile = common-4.0 diff --git a/packages/tizen_view/tizen/src/buffer_pool.cc b/packages/tizen_view/tizen/src/buffer_pool.cc new file mode 100644 index 000000000..3636f9436 --- /dev/null +++ b/packages/tizen_view/tizen/src/buffer_pool.cc @@ -0,0 +1,173 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffer_pool.h" + +#include "log.h" + +BufferUnit::BufferUnit(int32_t width, int32_t height) { Reset(width, height); } + +BufferUnit::~BufferUnit() { + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + if (gpu_surface_) { + delete gpu_surface_; + gpu_surface_ = nullptr; + } +} + +bool BufferUnit::MarkInUse() { + if (!is_used_) { + is_used_ = true; + return true; + } + return false; +} + +void BufferUnit::UnmarkInUse() { is_used_ = false; } + +tbm_surface_h BufferUnit::Surface() { + if (IsUsed()) { + return tbm_surface_; + } + return nullptr; +} + +void BufferUnit::Reset(int32_t width, int32_t height) { + if (width_ == width && height_ == height) { + return; + } + width_ = width; + height_ = height; + + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + if (gpu_surface_) { + delete gpu_surface_; + gpu_surface_ = nullptr; + } + + LOG_ERROR("CJS Reset"); + + tbm_surface_ = tbm_surface_create(width_, height_, TBM_FORMAT_ARGB8888); + + tbm_surface_info_s surface_info; + tbm_surface_map(tbm_surface_, TBM_SURF_OPTION_WRITE, &surface_info); + uint32_t* buffer_ = reinterpret_cast(surface_info.planes[0].ptr); + LOG_ERROR("CJS Reset Buffer %d %d ", width, height); + + for (int w = 0; w < width; w++) { + for (int h = 0; h < height; h++) { + buffer_[h * width + w] = 0x55555555; + } + } + + gpu_surface_ = new FlutterDesktopGpuSurfaceDescriptor(); + gpu_surface_->width = width_; + gpu_surface_->height = height_; + gpu_surface_->handle = tbm_surface_; + gpu_surface_->release_callback = [](void* release_context) { + BufferUnit* buffer = reinterpret_cast(release_context); + buffer->UnmarkInUse(); + LOG_ERROR("CJS release_callback"); + }; + gpu_surface_->release_context = this; +} + +BufferPool::BufferPool(int32_t width, int32_t height, size_t pool_size) { + for (size_t index = 0; index < pool_size; index++) { + pool_.emplace_back(std::make_unique(width, height)); + } + Prepare(width, height); +} + +BufferPool::~BufferPool() {} + +BufferUnit* BufferPool::GetAvailableBuffer() { + std::lock_guard lock(mutex_); + for (size_t index = 0; index < pool_.size(); index++) { + size_t current = (index + last_index_) % pool_.size(); + BufferUnit* buffer = pool_[current].get(); + if (buffer->MarkInUse()) { + last_index_ = current; + LOG_ERROR("CJS GetAvailableBuffer"); + tbm_surface_info_s surface_info; + tbm_surface_map(buffer->Surface(), TBM_SURF_OPTION_WRITE, &surface_info); + uint32_t* buffer_ = + reinterpret_cast(surface_info.planes[0].ptr); + LOG_ERROR("CJS Buffer"); + + for (int w = 0; w < buffer->width_; w++) { + for (int h = 0; h < buffer->height_; h++) { + buffer_[h * buffer->width_ + w] = 0xAAAAAAAA; + } + } + return buffer; + } + } + return nullptr; +} + +void BufferPool::Release(BufferUnit* buffer) { + std::lock_guard lock(mutex_); + buffer->UnmarkInUse(); +} + +void BufferPool::Prepare(int32_t width, int32_t height) { + std::lock_guard lock(mutex_); + for (size_t index = 0; index < pool_.size(); index++) { + BufferUnit* buffer = pool_[index].get(); + buffer->Reset(width, height); + } +} + +SingleBufferPool::SingleBufferPool(int32_t width, int32_t height) + : BufferPool(width, height, 1) {} + +SingleBufferPool::~SingleBufferPool() {} + +BufferUnit* SingleBufferPool::GetAvailableBuffer() { + BufferUnit* buffer = pool_[0].get(); + buffer->MarkInUse(); + + tbm_surface_info_s surface_info; + tbm_surface_map(buffer->Surface(), TBM_SURF_OPTION_WRITE, &surface_info); + uint32_t* buffer_ = reinterpret_cast(surface_info.planes[0].ptr); + LOG_ERROR("CJS Buffer %d %d", buffer->width_, buffer->height_); + + for (int w = 0; w < buffer->width_; w++) { + for (int h = 0; h < buffer->height_; h++) { + buffer_[h * buffer->width_ + w] = 0x55555555; + } + } + + return buffer; +} + +void SingleBufferPool::Release(BufferUnit* buffer) {} + +#ifndef NDEBUG +#include +void BufferUnit::DumpToPng(int file_name) { + char file_path[256]; + sprintf(file_path, "/tmp/dump%d.png", file_name); + + tbm_surface_info_s surface_info; + tbm_surface_map(tbm_surface_, TBM_SURF_OPTION_WRITE, &surface_info); + + unsigned char* buffer = surface_info.planes[0].ptr; + cairo_surface_t* png_buffer = cairo_image_surface_create_for_data( + buffer, CAIRO_FORMAT_ARGB32, width_, height_, + surface_info.planes[0].stride); + + cairo_surface_write_to_png(png_buffer, file_path); + + tbm_surface_unmap(tbm_surface_); + cairo_surface_destroy(png_buffer); +} +#endif diff --git a/packages/tizen_view/tizen/src/buffer_pool.h b/packages/tizen_view/tizen/src/buffer_pool.h new file mode 100644 index 000000000..c1845d50b --- /dev/null +++ b/packages/tizen_view/tizen/src/buffer_pool.h @@ -0,0 +1,72 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BUFFER_POOL_H_ +#define FLUTTER_PLUGIN_BUFFER_POOL_H_ + +#include +#include + +#include +#include +#include + +class BufferUnit { + public: + explicit BufferUnit(int32_t width, int32_t height); + ~BufferUnit(); + + void Reset(int32_t width, int32_t height); + + bool MarkInUse(); + void UnmarkInUse(); + + bool IsUsed() { return is_used_ && tbm_surface_; } + + tbm_surface_h Surface(); + + FlutterDesktopGpuSurfaceDescriptor* GpuSurface() { return gpu_surface_; } + +#ifndef NDEBUG + // TODO: Unused code. + void DumpToPng(int file_name); +#endif + int32_t width_ = 0; + int32_t height_ = 0; + + private: + bool is_used_ = false; + + tbm_surface_h tbm_surface_ = nullptr; + FlutterDesktopGpuSurfaceDescriptor* gpu_surface_ = nullptr; +}; + +class BufferPool { + public: + explicit BufferPool(int32_t width, int32_t height, size_t pool_size); + virtual ~BufferPool(); + + virtual BufferUnit* GetAvailableBuffer(); + virtual void Release(BufferUnit* buffer); + + void Prepare(int32_t with, int32_t height); + + protected: + std::vector> pool_; + + private: + size_t last_index_ = 0; + std::mutex mutex_; +}; + +class SingleBufferPool : public BufferPool { + public: + explicit SingleBufferPool(int32_t width, int32_t height); + ~SingleBufferPool(); + + virtual BufferUnit* GetAvailableBuffer() override; + virtual void Release(BufferUnit* buffer) override; +}; + +#endif // FLUTTER_PLUGIN_BUFFER_POOL_H_ diff --git a/packages/google_sign_in/tizen/src/log.h b/packages/tizen_view/tizen/src/log.h similarity index 93% rename from packages/google_sign_in/tizen/src/log.h rename to packages/tizen_view/tizen/src/log.h index c7bff6212..4fa9f0171 100644 --- a/packages/google_sign_in/tizen/src/log.h +++ b/packages/tizen_view/tizen/src/log.h @@ -6,7 +6,7 @@ #ifdef LOG_TAG #undef LOG_TAG #endif -#define LOG_TAG "GoogleSignInTizenPlugin" +#define LOG_TAG "TizenViewPlugin" #ifndef __MODULE__ #define __MODULE__ strrchr("/" __FILE__, '/') + 1 diff --git a/packages/tizen_view/tizen/src/tizen_view.cc b/packages/tizen_view/tizen/src/tizen_view.cc new file mode 100644 index 000000000..6a1ab2e53 --- /dev/null +++ b/packages/tizen_view/tizen/src/tizen_view.cc @@ -0,0 +1,626 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tizen_view.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "buffer_pool.h" +#include "log.h" +// #include "lwe/LWEWebView.h" +// #include "lwe/PlatformIntegrationData.h" +#include "tizenview_factory.h" + +static constexpr size_t kBufferPoolSize = 5; + +/*extern "C" size_t LWE_EXPORT createTizenViewInstance( + unsigned x, unsigned y, unsigned width, unsigned height, + float devicePixelRatio, const char* defaultFontName, const char* locale, + const char* timezoneID, + const std::function<::LWE::WebContainer::ExternalImageInfo(void)>& + prepareImageCb, + const std::function& flushCb, + bool useSWBackend);*/ + +template +static bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, + std::string key, T* out) { + if (auto* map = std::get_if(arguments)) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto* value = std::get_if(&iter->second)) { + *out = *value; + return true; + } + } + } + return false; +} + +static bool IsRunningOnEmulator() { + bool result = false; + char* value = nullptr; + int ret = system_info_get_platform_string( + "http://tizen.org/system/model_name", &value); + if (ret == SYSTEM_INFO_ERROR_NONE && strcmp(value, "Emulator") == 0) { + result = true; + } + if (value) { + free(value); + } + return result; +} + +TizenView::TizenView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params) + : PlatformView(registrar, view_id, nullptr), + texture_registrar_(texture_registrar), + width_(width), + height_(height) { + use_sw_backend_ = true; // IsRunningOnEmulator(); + if (use_sw_backend_) { + tbm_pool_ = std::make_unique(width, height); + } else { + tbm_pool_ = std::make_unique(width, height, kBufferPoolSize); + } + + texture_variant_ = + std::make_unique(flutter::GpuSurfaceTexture( + kFlutterDesktopGpuSurfaceTypeNone, + [this](size_t width, + size_t height) -> const FlutterDesktopGpuSurfaceDescriptor* { + LOG_ERROR("CJS ObtainDescriptor %d %d", width, height); + return ObtainGpuSurface(width, height); + })); + SetTextureId(texture_registrar_->RegisterTexture(texture_variant_.get())); + + InitTizenView(); + + channel_ = std::make_unique>( + GetPluginRegistrar()->messenger(), GetChannelName(), + &flutter::StandardMethodCodec::GetInstance()); + channel_->SetMethodCallHandler( + [tizenview = this](const auto& call, auto result) { + tizenview->HandleMethodCall(call, std::move(result)); + }); +} + +TizenView::~TizenView() { Dispose(); } + +std::string TizenView::GetChannelName() { + return "plugins.flutter.io/tizenview_" + std::to_string(GetViewId()); +} + +void TizenView::Dispose() { + texture_registrar_->UnregisterTexture(GetTextureId()); + + /*if (webview_instance_) { + webview_instance_->Destroy(); + webview_instance_ = nullptr; + }*/ +} + +void TizenView::Resize(double width, double height) { + width_ = width; + height_ = height; + + if (candidate_surface_) { + candidate_surface_ = nullptr; + } + + tbm_pool_->Prepare(width_, height_); + // webview_instance_->ResizeTo(width_, height_); +} + +void TizenView::Touch(int type, int button, double x, double y, double dx, + double dy) { + LOG_ERROR("CJS: %f %f %d", x, y, type); + if (type == 0) { // down event + /*webview_instance_->DispatchMouseDownEvent( + LWE::MouseButtonValue::LeftButton, + LWE::MouseButtonsValue::LeftButtonDown, x, y);*/ + is_mouse_lbutton_down_ = true; + } else if (type == 1) { // move event + /*webview_instance_->DispatchMouseMoveEvent( + is_mouse_lbutton_down_ ? LWE::MouseButtonValue::LeftButton + : LWE::MouseButtonValue::NoButton, + is_mouse_lbutton_down_ ? LWE::MouseButtonsValue::LeftButtonDown + : LWE::MouseButtonsValue::NoButtonDown, + x, y);*/ + } else if (type == 2) { // up event + /*webview_instance_->DispatchMouseUpEvent( + LWE::MouseButtonValue::NoButton, LWE::MouseButtonsValue::NoButtonDown, + x, y);*/ + is_mouse_lbutton_down_ = false; + } else { + LOG_WARN("Unknown touch event type: %d", type); + } +} +/* +static LWE::KeyValue KeyToKeyValue(const std::string& key, + bool is_shift_pressed) { + if (key == "Left") { + return LWE::KeyValue::ArrowLeftKey; + } else if (key == "Right") { + return LWE::KeyValue::ArrowRightKey; + } else if (key == "Up") { + return LWE::KeyValue::ArrowUpKey; + } else if (key == "Down") { + return LWE::KeyValue::ArrowDownKey; + } else if (key == "space") { + return LWE::KeyValue::SpaceKey; + } else if (key == "Select") { + return LWE::KeyValue::EnterKey; + } else if (key == "Return") { + return LWE::KeyValue::EnterKey; + } else if (key == "Tab") { + return LWE::KeyValue::TabKey; + } else if (key == "BackSpace") { + return LWE::KeyValue::BackspaceKey; + } else if (key == "Escape") { + return LWE::KeyValue::EscapeKey; + } else if (key == "Delete") { + return LWE::KeyValue::DeleteKey; + } else if (key == "at") { + return LWE::KeyValue::AtMarkKey; + } else if (key == "minus") { + if (is_shift_pressed) { + return LWE::KeyValue::UnderScoreMarkKey; + } else { + return LWE::KeyValue::MinusMarkKey; + } + } else if (key == "equal") { + if (is_shift_pressed) { + return LWE::KeyValue::PlusMarkKey; + } else { + return LWE::KeyValue::EqualitySignKey; + } + } else if (key == "bracketleft") { + if (is_shift_pressed) { + return LWE::KeyValue::LeftCurlyBracketMarkKey; + } else { + return LWE::KeyValue::LeftSquareBracketKey; + } + } else if (key == "bracketright") { + if (is_shift_pressed) { + return LWE::KeyValue::RightCurlyBracketMarkKey; + } else { + return LWE::KeyValue::RightSquareBracketKey; + } + } else if (key == "semicolon") { + if (is_shift_pressed) { + return LWE::KeyValue::ColonMarkKey; + } else { + return LWE::KeyValue::SemiColonMarkKey; + } + } else if (key == "apostrophe") { + if (is_shift_pressed) { + return LWE::KeyValue::DoubleQuoteMarkKey; + } else { + return LWE::KeyValue::SingleQuoteMarkKey; + } + } else if (key == "comma") { + if (is_shift_pressed) { + return LWE::KeyValue::LessThanMarkKey; + } else { + return LWE::KeyValue::CommaMarkKey; + } + } else if (key == "period") { + if (is_shift_pressed) { + return LWE::KeyValue::GreaterThanSignKey; + } else { + return LWE::KeyValue::PeriodKey; + } + } else if (key == "slash") { + if (is_shift_pressed) { + return LWE::KeyValue::QuestionMarkKey; + } else { + return LWE::KeyValue::SlashKey; + } + } else if (key.length() == 1) { + const char ch = key.at(0); + if (ch >= '0' && ch <= '9') { + if (is_shift_pressed) { + switch (ch) { + case '1': + return LWE::KeyValue::ExclamationMarkKey; + case '2': + return LWE::KeyValue::AtMarkKey; + case '3': + return LWE::KeyValue::SharpMarkKey; + case '4': + return LWE::KeyValue::DollarMarkKey; + case '5': + return LWE::KeyValue::PercentMarkKey; + case '6': + return LWE::KeyValue::CaretMarkKey; + case '7': + return LWE::KeyValue::AmpersandMarkKey; + case '8': + return LWE::KeyValue::AsteriskMarkKey; + case '9': + return LWE::KeyValue::LeftParenthesisMarkKey; + case '0': + return LWE::KeyValue::RightParenthesisMarkKey; + } + } + return LWE::KeyValue(LWE::KeyValue::Digit0Key + ch - '0'); + } else if (ch >= 'a' && ch <= 'z') { + if (is_shift_pressed) { + return LWE::KeyValue(LWE::KeyValue::LowerAKey + ch - 'a' - 32); + } else { + return LWE::KeyValue(LWE::KeyValue::LowerAKey + ch - 'a'); + } + } else if (ch >= 'A' && ch <= 'Z') { + if (is_shift_pressed) { + return LWE::KeyValue(LWE::KeyValue::AKey + ch - 'A' + 32); + } else { + return LWE::KeyValue(LWE::KeyValue::AKey + ch - 'A'); + } + } + } else if (key == "XF86AudioRaiseVolume") { + return LWE::KeyValue::TVVolumeUpKey; + } else if (key == "XF86AudioLowerVolume") { + return LWE::KeyValue::TVVolumeDownKey; + } else if (key == "XF86AudioMute") { + return LWE::KeyValue::TVMuteKey; + } else if (key == "XF86RaiseChannel") { + return LWE::KeyValue::TVChannelUpKey; + } else if (key == "XF86LowerChannel") { + return LWE::KeyValue::TVChannelDownKey; + } else if (key == "XF86AudioRewind") { + return LWE::KeyValue::MediaTrackPreviousKey; + } else if (key == "XF86AudioNext") { + return LWE::KeyValue::MediaTrackNextKey; + } else if (key == "XF86AudioPause") { + return LWE::KeyValue::MediaPauseKey; + } else if (key == "XF86AudioRecord") { + return LWE::KeyValue::MediaRecordKey; + } else if (key == "XF86AudioPlay") { + return LWE::KeyValue::MediaPlayKey; + } else if (key == "XF86AudioStop") { + return LWE::KeyValue::MediaStopKey; + } else if (key == "XF86Info") { + return LWE::KeyValue::TVInfoKey; + } else if (key == "XF86Back") { + return LWE::KeyValue::TVReturnKey; + } else if (key == "XF86Red") { + return LWE::KeyValue::TVRedKey; + } else if (key == "XF86Green") { + return LWE::KeyValue::TVGreenKey; + } else if (key == "XF86Yellow") { + return LWE::KeyValue::TVYellowKey; + } else if (key == "XF86Blue") { + return LWE::KeyValue::TVBlueKey; + } else if (key == "XF86SysMenu") { + return LWE::KeyValue::TVMenuKey; + } else if (key == "XF86Home") { + return LWE::KeyValue::TVHomeKey; + } else if (key == "XF86Exit") { + return LWE::KeyValue::TVExitKey; + } else if (key == "XF86PreviousChannel") { + return LWE::KeyValue::TVPreviousChannel; + } else if (key == "XF86ChannelList") { + return LWE::KeyValue::TVChannelList; + } else if (key == "XF86ChannelGuide") { + return LWE::KeyValue::TVChannelGuide; + } else if (key == "XF86SimpleMenu") { + return LWE::KeyValue::TVSimpleMenu; + } else if (key == "XF86EManual") { + return LWE::KeyValue::TVEManual; + } else if (key == "XF86ExtraApp") { + return LWE::KeyValue::TVExtraApp; + } else if (key == "XF86Search") { + return LWE::KeyValue::TVSearch; + } else if (key == "XF86PictureSize") { + return LWE::KeyValue::TVPictureSize; + } else if (key == "XF86Sleep") { + return LWE::KeyValue::TVSleep; + } else if (key == "XF86Caption") { + return LWE::KeyValue::TVCaption; + } else if (key == "XF86More") { + return LWE::KeyValue::TVMore; + } else if (key == "XF86BTVoice") { + return LWE::KeyValue::TVBTVoice; + } else if (key == "XF86Color") { + return LWE::KeyValue::TVColor; + } else if (key == "XF86PlayBack") { + return LWE::KeyValue::TVPlayBack; + } + LOG_WARN("Unknown key name: %s", key.c_str()); + return LWE::KeyValue::UnidentifiedKey; +} +*/ + +bool TizenView::SendKey(const char* key, const char* string, + const char* compose, uint32_t modifiers, + uint32_t scan_code, bool is_down) { + if (!IsFocused()) { + return false; + } + + bool is_shift_pressed = modifiers & 1; + LOG_ERROR("CJS: %s", key); + /* + struct Param { + LWE::WebContainer* webview_instance; + LWE::KeyValue key_value; + bool is_down; + }; + + Param* param = new Param(); + param->webview_instance = webview_instance_; + // param->key_value = KeyToKeyValue(key, is_shift_pressed); + param->is_down = is_down; + + if (param->key_value == LWE::KeyValue::TVReturnKey && + webview_instance_->CanGoBack()) { + webview_instance_->GoBack(); + return true; + } + + webview_instance_->AddIdleCallback( + [](void* data) { + Param* param = reinterpret_cast(data); + if (param->is_down) { + param->webview_instance->DispatchKeyDownEvent(param->key_value); + param->webview_instance->DispatchKeyPressEvent(param->key_value); + } else { + param->webview_instance->DispatchKeyUpEvent(param->key_value); + } + delete param; + }, + param); + */ + return false; +} + +void TizenView::SetDirection(int direction) { + // TODO: Implement if necessary. +} + +void TizenView::InitTizenView() { + /*if (webview_instance_) { + webview_instance_->Destroy(); + webview_instance_ = nullptr; + }*/ + + float pixel_ratio = 1.0; + + tbm_pool_->Prepare(315, 505); + rendered_surface_ = candidate_surface_ = tbm_pool_->GetAvailableBuffer(); + LOG_ERROR("CJS Texture ID: %d", GetTextureId()); + texture_registrar_->MarkTextureFrameAvailable(GetTextureId()); + /* + auto on_prepare_image = [this]() -> LWE::WebContainer::ExternalImageInfo { + std::lock_guard lock(mutex_); + LWE::WebContainer::ExternalImageInfo result; + if (!working_surface_) { + working_surface_ = tbm_pool_->GetAvailableBuffer(); + } + if (working_surface_) { + result.imageAddress = working_surface_->Surface(); + } else { + result.imageAddress = nullptr; + } + return result; + }; + * / + /* + auto on_flush = [this](LWE::WebContainer* container, bool is_rendered) { + if (is_rendered) { + std::lock_guard lock(mutex_); + if (candidate_surface_) { + tbm_pool_->Release(candidate_surface_); + candidate_surface_ = nullptr; + } + candidate_surface_ = working_surface_; + working_surface_ = nullptr; + texture_registrar_->MarkTextureFrameAvailable(GetTextureId()); + } + }; +*/ + /* + webview_instance_ = + reinterpret_cast(createWebViewInstance( + 0, 0, width_, height_, pixel_ratio, "SamsungOneUI", "ko-KR", + "Asia/Seoul", on_prepare_image, on_flush, use_sw_backend_)); + + */ +} + +void TizenView::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + tbm_pool_->GetAvailableBuffer(); + LOG_ERROR("CJS Texture ID: %d", GetTextureId()); + texture_registrar_->MarkTextureFrameAvailable(GetTextureId()); + + /* if (!webview_instance_) { + result->Error("Invalid operation", + "The webview instance has not been initialized."); + return; + } + */ + const std::string& method_name = method_call.method_name(); + const flutter::EncodableValue* arguments = method_call.arguments(); + + LOG_DEBUG("Handle a method call: %s", method_name.c_str()); + /* + if (method_name == "loadUrl") { + std::string url; + if (GetValueFromEncodableMap(arguments, "url", &url)) { + webview_instance_->LoadURL(url); + result->Success(); + } else { + result->Error("Invalid argument", "No url provided."); + } + } else if (method_name == "updateSettings") { + const auto* settings = std::get_if(arguments); + if (settings) { + ApplySettings(*settings); + } + result->Success(); + } else if (method_name == "canGoBack") { + result->Success(flutter::EncodableValue(webview_instance_->CanGoBack())); + } else if (method_name == "canGoForward") { + result->Success(flutter::EncodableValue(webview_instance_->CanGoForward())); + } else if (method_name == "goBack") { + webview_instance_->GoBack(); + result->Success(); + } else if (method_name == "goForward") { + webview_instance_->GoForward(); + result->Success(); + } else if (method_name == "reload") { + webview_instance_->Reload(); + result->Success(); + } else if (method_name == "currentUrl") { + result->Success(flutter::EncodableValue(webview_instance_->GetURL())); + } else if (method_name == "evaluateJavascript" || + method_name == "runJavascriptReturningResult" || + method_name == "runJavascript") { + const auto* javascript = std::get_if(arguments); + if (javascript) { + bool should_return = method_name != "runJavascript"; + auto on_result = [result = result.release(), + should_return](std::string value) { + LOG_DEBUG("JavaScript evaluation result: %s", value.c_str()); + if (result) { + if (should_return) { + result->Success(flutter::EncodableValue(value)); + } else { + result->Success(); + } + delete result; + } + }; + webview_instance_->EvaluateJavaScript(*javascript, on_result); + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "addJavascriptChannels") { + const auto* channels = std::get_if(arguments); + if (channels) { + for (flutter::EncodableValue channel : *channels) { + if (std::holds_alternative(channel)) { + RegisterJavaScriptChannelName(std::get(channel)); + } + } + } + result->Success(); + } else if (method_name == "removeJavascriptChannels") { + const auto* channels = std::get_if(arguments); + if (channels) { + for (flutter::EncodableValue channel : *channels) { + if (std::holds_alternative(channel)) { + webview_instance_->RemoveJavascriptInterface( + std::get(channel), "postMessage"); + } + } + } + result->Success(); + } else if (method_name == "clearCache") { + webview_instance_->ClearCache(); + result->Success(); + } else if (method_name == "getTitle") { + result->Success(flutter::EncodableValue(webview_instance_->GetTitle())); + } else if (method_name == "scrollTo") { + int x = 0, y = 0; + if (GetValueFromEncodableMap(arguments, "x", &x) && + GetValueFromEncodableMap(arguments, "y", &y)) { + webview_instance_->ScrollTo(x, y); + result->Success(); + } else { + result->Error("Invalid argument", "No x or y provided."); + } + } else if (method_name == "scrollBy") { + int x = 0, y = 0; + if (GetValueFromEncodableMap(arguments, "x", &x) && + GetValueFromEncodableMap(arguments, "y", &y)) { + webview_instance_->ScrollBy(x, y); + result->Success(); + } else { + result->Error("Invalid argument", "No x or y provided."); + } + } else if (method_name == "getScrollX") { + result->Success(flutter::EncodableValue(webview_instance_->GetScrollX())); + } else if (method_name == "getScrollY") { + result->Success(flutter::EncodableValue(webview_instance_->GetScrollY())); + } else if (method_name == "loadFlutterAsset") { + const auto* key = std::get_if(arguments); + if (key) { + char* res_path = app_get_resource_path(); + if (res_path) { + std::string url = + std::string("file://") + res_path + "flutter_assets/" + *key; + free(res_path); + webview_instance_->LoadURL(url); + result->Success(); + } else { + result->Error("Operation failed", + "Could not get the flutter_assets path."); + } + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "loadHtmlString") { + std::string html, base_url; + if (!GetValueFromEncodableMap(arguments, "html", &html)) { + result->Error("Invalid argument", "No html provided."); + return; + } + if (GetValueFromEncodableMap(arguments, "baseUrl", &base_url)) { + LOG_WARN("loadHtmlString: baseUrl is not supported and will be + ignored."); + } + webview_instance_->LoadData(html); + result->Success(); + } else if (method_name == "loadFile") { + const auto* file_path = std::get_if(arguments); + if (file_path) { + std::string url = std::string("file://") + *file_path; + webview_instance_->LoadURL(url); + result->Success(); + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "loadRequest") { + result->NotImplemented(); + } else if (method_name == "setCookie") { + result->NotImplemented(); + } else { + result->NotImplemented(); + }*/ + result->NotImplemented(); +} + +FlutterDesktopGpuSurfaceDescriptor* TizenView::ObtainGpuSurface(size_t width, + size_t height) { + std::lock_guard lock(mutex_); + if (!candidate_surface_) { + if (rendered_surface_) { + LOG_ERROR("CJS Return GpuSurface %d %d", width, height); + return rendered_surface_->GpuSurface(); + } + LOG_ERROR("CJS Return NULL %d %d", width, height); + return nullptr; + } + if (rendered_surface_ && rendered_surface_->IsUsed()) { + LOG_ERROR("CJS Release %d %d", width, height); + tbm_pool_->Release(rendered_surface_); + } + rendered_surface_ = candidate_surface_; + candidate_surface_ = nullptr; + LOG_ERROR("CJS Return rendered %d %d", width, height); + return rendered_surface_->GpuSurface(); +} diff --git a/packages/tizen_view/tizen/src/tizen_view.h b/packages/tizen_view/tizen/src/tizen_view.h new file mode 100644 index 000000000..7546e18c1 --- /dev/null +++ b/packages/tizen_view/tizen/src/tizen_view.h @@ -0,0 +1,73 @@ +// Copyright 2020 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_TIZEN_VIEW_H_ +#define FLUTTER_PLUGIN_TIZEN_VIEW_H_ + +#include +#include +#include +#include +#include +#include + +#include + +class BufferPool; +class BufferUnit; + +class TizenView : public PlatformView { + public: + TizenView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params); + ~TizenView(); + + virtual void Dispose() override; + + virtual void Resize(double width, double height) override; + virtual void Touch(int type, int button, double x, double y, double dx, + double dy) override; + virtual void SetDirection(int direction) override; + + virtual void ClearFocus() override {} + + virtual bool SendKey(const char* key, const char* string, const char* compose, + uint32_t modifiers, uint32_t scan_code, + bool is_down) override; + + // LWE::WebContainer* GetWebViewInstance() { return webview_instance_; } + // Evas_Object* GetNativeHandle() { return native_handle_; } + + FlutterDesktopGpuSurfaceDescriptor* ObtainGpuSurface(size_t width, + size_t height); + + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + std::string GetChannelName(); + + void InitTizenView(); + + // LWE::WebContainer* webview_instance_ = nullptr; + Evas_Object* native_handle_ = nullptr; + flutter::TextureRegistrar* texture_registrar_; + double width_; + double height_; + BufferUnit* working_surface_ = nullptr; + BufferUnit* candidate_surface_ = nullptr; + BufferUnit* rendered_surface_ = nullptr; + bool is_mouse_lbutton_down_ = false; + bool has_navigation_delegate_ = false; + bool has_progress_tracking_ = false; + std::unique_ptr> channel_; + std::unique_ptr texture_variant_; + std::mutex mutex_; + std::unique_ptr tbm_pool_; + bool use_sw_backend_; +}; + +#endif // FLUTTER_PLUGIN_TIZEN_VIEW_H_ diff --git a/packages/tizen_view/tizen/src/tizen_view_plugin.cc b/packages/tizen_view/tizen/src/tizen_view_plugin.cc new file mode 100644 index 000000000..3f0b43508 --- /dev/null +++ b/packages/tizen_view/tizen/src/tizen_view_plugin.cc @@ -0,0 +1,44 @@ +#include "tizen_view_plugin.h" + +// For getPlatformVersion; remove unless needed for your plugin implementation. +#include +#include +#include +#include +#include + +#include +#include + +#include "log.h" +#include "tizenview_factory.h" +// #include "message.h" +#include "tizen_view.h" + +namespace { + +constexpr char kViewType[] = "plugins.flutter.io/tizenview"; + +class TizenViewPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto plugin = std::make_unique(); + registrar->AddPlugin(std::move(plugin)); + } + + TizenViewPlugin() {} + + virtual ~TizenViewPlugin() {} +}; + +} // namespace + +void TizenViewPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef core_registrar) { + flutter::PluginRegistrar* registrar = + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(core_registrar); + FlutterDesktopRegisterViewFactory( + core_registrar, kViewType, std::make_unique(registrar)); + TizenViewPlugin::RegisterWithRegistrar(registrar); +} diff --git a/packages/tizen_view/tizen/src/tizenview_factory.cc b/packages/tizen_view/tizen/src/tizenview_factory.cc new file mode 100644 index 000000000..49f4bc078 --- /dev/null +++ b/packages/tizen_view/tizen/src/tizenview_factory.cc @@ -0,0 +1,48 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tizenview_factory.h" + +#include +#include +#include + +#include +#include + +#include "log.h" +// #include "lwe/LWEWebView.h" +#include "tizen_view.h" + +static std::string GetAppDataPath() { + char* path = app_get_data_path(); + if (!path) { + return "/tmp/"; + } + std::string result = std::string(path); + free(path); + return result; +} + +TizenViewFactory::TizenViewFactory(flutter::PluginRegistrar* registrar) + : PlatformViewFactory(registrar) { + texture_registrar_ = registrar->texture_registrar(); + + std::string data_path = GetAppDataPath(); + std::string local_storage_path = data_path + "StarFish_localStorage.db"; + std::string cookie_path = data_path + "StarFish_cookies.db"; + std::string cache_path = data_path + "Starfish_cache.db"; + + // LWE::LWE::Initialize(local_storage_path.c_str(), cookie_path.c_str(), + // cache_path.c_str()); +} + +PlatformView* TizenViewFactory::Create(int view_id, double width, double height, + const ByteMessage& params) { + return new TizenView(GetPluginRegistrar(), view_id, texture_registrar_, width, + height, *GetCodec().DecodeMessage(params)); +} + +void TizenViewFactory::Dispose() { // LWE::LWE::Finalize(); +} diff --git a/packages/tizen_view/tizen/src/tizenview_factory.h b/packages/tizen_view/tizen/src/tizenview_factory.h new file mode 100644 index 000000000..7139e3c53 --- /dev/null +++ b/packages/tizen_view/tizen/src/tizenview_factory.h @@ -0,0 +1,27 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_TIZENVIEW_FACTORY_H_ +#define FLUTTER_PLUGIN_TIZENVIEW_FACTORY_H_ + +#include +#include +#include + +#include + +class TizenViewFactory : public PlatformViewFactory { + public: + TizenViewFactory(flutter::PluginRegistrar* registrar); + + virtual PlatformView* Create(int view_id, double width, double height, + const ByteMessage& params) override; + + virtual void Dispose() override; + + private: + flutter::TextureRegistrar* texture_registrar_; +}; + +#endif // FLUTTER_PLUGIN_TIZENVIEW_FACTORY_H_ diff --git a/packages/url_launcher/README.md b/packages/url_launcher/README.md index 1c478477c..c0fe4dfa1 100644 --- a/packages/url_launcher/README.md +++ b/packages/url_launcher/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/url_launcher_tizen.svg)](https://pub.dev/packages/url_launcher_tizen) -The Tizen implementation of [`url_launcher`](https://github.com/flutter/plugins/tree/master/packages/url_launcher). +The Tizen implementation of [`url_launcher`](https://github.com/flutter/plugins/tree/main/packages/url_launcher). ## Usage diff --git a/packages/video_player/README.md b/packages/video_player/README.md index f23534909..4aa9516f1 100644 --- a/packages/video_player/README.md +++ b/packages/video_player/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/video_player_tizen.svg)](https://pub.dev/packages/video_player_tizen) -The Tizen implementation of [`video_player`](https://github.com/flutter/plugins/tree/master/packages/video_player). +The Tizen implementation of [`video_player`](https://github.com/flutter/plugins/tree/main/packages/video_player). ## Required privileges diff --git a/packages/wearable_rotary/CHANGELOG.md b/packages/wearable_rotary/CHANGELOG.md index bd47b9606..738bc51ef 100644 --- a/packages/wearable_rotary/CHANGELOG.md +++ b/packages/wearable_rotary/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 2.0.0 +* [BREAKING] `RotaryEvent` is now a class with `direction` and `magnitude` properties. +* Adds Wear OS support. +* Adds `RotaryScrollController` for easily making scrolling widgets respond to rotary input. * Refactor the C++ code. ## 1.0.2 diff --git a/packages/wearable_rotary/README.md b/packages/wearable_rotary/README.md index db3a34cea..929c5f809 100644 --- a/packages/wearable_rotary/README.md +++ b/packages/wearable_rotary/README.md @@ -2,7 +2,27 @@ [![pub package](https://img.shields.io/pub/v/wearable_rotary.svg)](https://pub.dev/packages/wearable_rotary) -Plugin that can listen to rotary events on Galaxy watch devices. +Flutter plugin that can listen to rotary events on Wear OS and Tizen Galaxy watch devices. + +## Setup + +### Android + +Add the following to `MainActivity.kt`: + +```kotlin +import android.view.MotionEvent +import com.samsung.wearable_rotary.WearableRotaryPlugin + +class MainActivity : FlutterActivity() { + override fun onGenericMotionEvent(event: MotionEvent?): Boolean { + return when { + WearableRotaryPlugin.onGenericMotionEvent(event) -> true + else -> super.onGenericMotionEvent(event) + } + } +} +``` ## Usage @@ -10,7 +30,7 @@ To use this plugin, add `wearable_rotary` as a dependency in your `pubspec.yaml` ```yaml dependencies: - wearable_rotary: ^1.0.2 + wearable_rotary: ^2.0.0 ``` Then, import `wearable_rotary` in your Dart code. @@ -22,17 +42,21 @@ import 'package:wearable_rotary/wearable_rotary.dart'; // Be informed when an event (RotaryEvent.clockwise or RotaryEvent.counterClockwise) occurs. StreamSubscription rotarySubscription = rotaryEvents.listen((RotaryEvent event) { - if (event == RotaryEvent.clockwise) { + if (event.direction == RotaryDirection.clockwise) { // Do something. - } else if (event == RotaryEvent.counterClockwise) { + } else if (event.direction == RotaryDirection.counterClockwise) { // Do something. } }); // Be sure to cancel on dispose. rotarySubscription.cancel(); + +// Use [RotaryScrollController] to easily make scrolling widgets respond to rotary input. +ListView(controller: RotaryScrollController()); ``` ## Supported devices +- Wear OS devices with rotary input (Galaxy Watch 4, Pixel Watch, etc.) - Galaxy Watch series (running Tizen 4.0 or later) diff --git a/packages/wearable_rotary/android/.gitignore b/packages/wearable_rotary/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/packages/wearable_rotary/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/wearable_rotary/android/build.gradle b/packages/wearable_rotary/android/build.gradle new file mode 100644 index 000000000..5fae32467 --- /dev/null +++ b/packages/wearable_rotary/android/build.gradle @@ -0,0 +1,46 @@ +group 'com.samsung.wearable_rotary' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.20' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 18 + } +} diff --git a/packages/wearable_rotary/android/settings.gradle b/packages/wearable_rotary/android/settings.gradle new file mode 100644 index 000000000..6479325e7 --- /dev/null +++ b/packages/wearable_rotary/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'wearable_rotary' diff --git a/packages/wearable_rotary/android/src/main/AndroidManifest.xml b/packages/wearable_rotary/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e62c739ba --- /dev/null +++ b/packages/wearable_rotary/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/wearable_rotary/android/src/main/kotlin/com/samsung/wearable_rotary/WearableRotaryPlugin.kt b/packages/wearable_rotary/android/src/main/kotlin/com/samsung/wearable_rotary/WearableRotaryPlugin.kt new file mode 100644 index 000000000..b43e654a2 --- /dev/null +++ b/packages/wearable_rotary/android/src/main/kotlin/com/samsung/wearable_rotary/WearableRotaryPlugin.kt @@ -0,0 +1,62 @@ +package com.samsung.wearable_rotary + +import android.view.MotionEvent +import android.view.ViewConfiguration +import androidx.core.view.InputDeviceCompat +import androidx.core.view.MotionEventCompat +import androidx.core.view.ViewConfigurationCompat +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import kotlin.math.abs +import kotlin.properties.Delegates + +/** WearableRotaryPlugin */ +class WearableRotaryPlugin : FlutterPlugin, EventChannel.StreamHandler { + companion object { + private var scaleFactor by Delegates.notNull() + private var events: EventChannel.EventSink? = null + + fun onGenericMotionEvent(event: MotionEvent?): Boolean { + if (event == null || event.action != MotionEvent.ACTION_SCROLL || !event.isFromSource( + InputDeviceCompat.SOURCE_ROTARY_ENCODER + ) + ) { + return false + } + + val delta = event.getAxisValue(MotionEventCompat.AXIS_SCROLL) * scaleFactor + events?.success( + mapOf( + "direction" to if (delta < 0) "clockwise" else "counterClockwise", + "magnitude" to abs(delta), + ) + ) + return true + } + } + + private lateinit var channel: EventChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = + EventChannel(flutterPluginBinding.binaryMessenger, "flutter.wearable_rotary.channel") + channel.setStreamHandler(this) + + val context = flutterPluginBinding.applicationContext + scaleFactor = ViewConfigurationCompat.getScaledVerticalScrollFactor( + ViewConfiguration.get(context), context + ) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setStreamHandler(null) + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + Companion.events = events + } + + override fun onCancel(arguments: Any?) { + events = null + } +} diff --git a/packages/wearable_rotary/example/android/.gitignore b/packages/wearable_rotary/example/android/.gitignore new file mode 100644 index 000000000..6f568019d --- /dev/null +++ b/packages/wearable_rotary/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/wearable_rotary/example/android/app/build.gradle b/packages/wearable_rotary/example/android/app/build.gradle new file mode 100644 index 000000000..1902a6e13 --- /dev/null +++ b/packages/wearable_rotary/example/android/app/build.gradle @@ -0,0 +1,66 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "com.samsung.wearable_rotary_example" + minSdkVersion 20 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/wearable_rotary/example/android/app/src/debug/AndroidManifest.xml b/packages/wearable_rotary/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..794f6448b --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/wearable_rotary/example/android/app/src/main/AndroidManifest.xml b/packages/wearable_rotary/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..310e39a1f --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/wearable_rotary/example/android/app/src/main/kotlin/com/samsung/wearable_rotary_example/MainActivity.kt b/packages/wearable_rotary/example/android/app/src/main/kotlin/com/samsung/wearable_rotary_example/MainActivity.kt new file mode 100644 index 000000000..7fa67bde1 --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/main/kotlin/com/samsung/wearable_rotary_example/MainActivity.kt @@ -0,0 +1,14 @@ +package com.samsung.wearable_rotary_example + +import android.view.MotionEvent +import com.samsung.wearable_rotary.WearableRotaryPlugin +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { + override fun onGenericMotionEvent(event: MotionEvent?): Boolean { + return when { + WearableRotaryPlugin.onGenericMotionEvent(event) -> true + else -> super.onGenericMotionEvent(event) + } + } +} diff --git a/packages/wearable_rotary/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/wearable_rotary/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/wearable_rotary/example/android/app/src/main/res/drawable/launch_background.xml b/packages/wearable_rotary/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..304732f88 --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/wearable_rotary/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/wearable_rotary/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/wearable_rotary/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/wearable_rotary/example/android/app/src/main/res/values-night/styles.xml b/packages/wearable_rotary/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..06952be74 --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/wearable_rotary/example/android/app/src/main/res/values/styles.xml b/packages/wearable_rotary/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..e349b1e9a --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/packages/wearable_rotary/example/android/app/src/profile/AndroidManifest.xml b/packages/wearable_rotary/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..794f6448b --- /dev/null +++ b/packages/wearable_rotary/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/wearable_rotary/example/android/build.gradle b/packages/wearable_rotary/example/android/build.gradle new file mode 100644 index 000000000..090ffc6a8 --- /dev/null +++ b/packages/wearable_rotary/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.20' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/wearable_rotary/example/android/gradle.properties b/packages/wearable_rotary/example/android/gradle.properties new file mode 100644 index 000000000..94adc3a3f --- /dev/null +++ b/packages/wearable_rotary/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/wearable_rotary/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/wearable_rotary/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..cb24abda1 --- /dev/null +++ b/packages/wearable_rotary/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/wearable_rotary/example/android/settings.gradle b/packages/wearable_rotary/example/android/settings.gradle new file mode 100644 index 000000000..44e62bcf0 --- /dev/null +++ b/packages/wearable_rotary/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/wearable_rotary/example/lib/custom_page_view.dart b/packages/wearable_rotary/example/lib/custom_page_view.dart deleted file mode 100644 index 4eec68cfd..000000000 --- a/packages/wearable_rotary/example/lib/custom_page_view.dart +++ /dev/null @@ -1,74 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:wearable_rotary/wearable_rotary.dart'; - -class CustomPageView extends StatefulWidget { - const CustomPageView(this.scrollDirection, {Key? key}) : super(key: key); - - final Axis scrollDirection; - - @override - State createState() => _CustomPageViewState(); -} - -class _CustomPageViewState extends State { - StreamSubscription? _rotarySubscription; - final PageController _pager = PageController(); - int _currentPageIdx = 0; - - @override - void initState() { - super.initState(); - _rotarySubscription = rotaryEvents.listen((RotaryEvent event) { - if (event == RotaryEvent.clockwise) { - if (_currentPageIdx != Colors.primaries.length - 1) { - _pager.animateToPage( - ++_currentPageIdx, - duration: const Duration(milliseconds: 300), - curve: Curves.ease, - ); - } - } else if (event == RotaryEvent.counterClockwise) { - if (_currentPageIdx != 0) { - _pager.animateToPage( - --_currentPageIdx, - duration: const Duration(milliseconds: 300), - curve: Curves.ease, - ); - } - } - }); - } - - @override - void dispose() { - super.dispose(); - _rotarySubscription?.cancel(); - _pager.dispose(); - } - - @override - Widget build(BuildContext context) { - final String title = widget.scrollDirection == Axis.vertical - ? 'VerticalPageView' - : 'HorizontalPageView'; - return Scaffold( - appBar: AppBar(title: Text(title)), - body: PageView.builder( - controller: _pager, - scrollDirection: widget.scrollDirection, - itemCount: Colors.primaries.length, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Container(color: Colors.primaries[index]), - ); - }, - onPageChanged: (int index) => _currentPageIdx = index, - ), - ); - } -} diff --git a/packages/wearable_rotary/example/lib/main.dart b/packages/wearable_rotary/example/lib/main.dart index c4ff0cfc8..b5f5bf8bc 100644 --- a/packages/wearable_rotary/example/lib/main.dart +++ b/packages/wearable_rotary/example/lib/main.dart @@ -5,18 +5,21 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; - -import 'custom_page_view.dart'; +import 'package:wearable_rotary/wearable_rotary.dart'; void main() { - runApp(const MaterialApp( - title: 'Rotary example app', - home: MyApp(), - )); + runApp( + MaterialApp( + title: 'Rotary example app', + home: const MyApp(), + // Set the target platform to iOS so that navigation behaves as expected for a wearable app + theme: ThemeData(platform: TargetPlatform.iOS), + ), + ); } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -27,26 +30,27 @@ class MyApp extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - child: const Text('HorizontalPageView'), + child: const Text('HorizontalScrollView'), onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => - const CustomPageView(Axis.horizontal), + builder: (BuildContext context) => const RotaryScrollPage( + scrollDirection: Axis.horizontal, + ), ), ); }, ), const SizedBox(height: 10), ElevatedButton( - child: const Text('VerticalPageView'), + child: const Text('VerticalScrollView'), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => - const CustomPageView(Axis.vertical), + const RotaryScrollPage(scrollDirection: Axis.vertical), ), ); }, @@ -57,3 +61,31 @@ class MyApp extends StatelessWidget { ); } } + +class RotaryScrollPage extends StatelessWidget { + const RotaryScrollPage({super.key, required this.scrollDirection}); + + final Axis scrollDirection; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + scrollDirection == Axis.vertical + ? 'VerticalScrollView' + : 'HorizontalScrollView', + ), + ), + body: Center( + child: ListView.builder( + padding: const EdgeInsets.all(16), + controller: RotaryScrollController(), + scrollDirection: scrollDirection, + itemCount: 1000, + itemBuilder: (BuildContext context, int index) => Text('Item $index'), + ), + ), + ); + } +} diff --git a/packages/wearable_rotary/example/pubspec.yaml b/packages/wearable_rotary/example/pubspec.yaml index 37d97f738..b1494ce60 100644 --- a/packages/wearable_rotary/example/pubspec.yaml +++ b/packages/wearable_rotary/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the wearable_rotary plugin. publish_to: "none" environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.0.0" dependencies: diff --git a/packages/wearable_rotary/lib/src/rotary_scroll_controller.dart b/packages/wearable_rotary/lib/src/rotary_scroll_controller.dart new file mode 100644 index 000000000..fddbfb83f --- /dev/null +++ b/packages/wearable_rotary/lib/src/rotary_scroll_controller.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:wearable_rotary/wearable_rotary.dart'; + +/// A [ScrollController] that responds to [RotaryEvent]s. +class RotaryScrollController extends ScrollController { + /// Constructor + RotaryScrollController({this.maxIncrement = 50}); + + StreamSubscription? _subscription; + + /// The maximum amount a [RotaryEvent] can increment the scroll position. + /// + /// Also the fallback increment if [RotaryEvent.magnitude] is null. + final double maxIncrement; + + void _onRotaryEvent(RotaryEvent event) { + final double increment = min(event.magnitude ?? maxIncrement, maxIncrement); + + final double newOffset; + if (event.direction == RotaryDirection.clockwise) { + newOffset = min(offset + increment, position.maxScrollExtent); + } else { + newOffset = max(offset - increment, position.minScrollExtent); + } + jumpTo(newOffset); + } + + @override + void attach(ScrollPosition position) { + _subscription ??= rotaryEvents.listen(_onRotaryEvent); + super.attach(position); + } + + @override + void detach(ScrollPosition position) { + _subscription?.cancel(); + super.detach(position); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} diff --git a/packages/wearable_rotary/lib/src/wearable_rotary_base.dart b/packages/wearable_rotary/lib/src/wearable_rotary_base.dart new file mode 100644 index 000000000..ae782a0b2 --- /dev/null +++ b/packages/wearable_rotary/lib/src/wearable_rotary_base.dart @@ -0,0 +1,62 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +const String _channelName = 'flutter.wearable_rotary.channel'; + +const EventChannel _channel = EventChannel(_channelName); + +/// A broadcast stream of events from the device rotary sensor. +Stream get rotaryEvents { + return _rotaryEvents ??= _channel + .receiveBroadcastStream() + .map((dynamic event) => _parseEvent(event)); +} + +Stream? _rotaryEvents; + +/// A rotary event. +class RotaryEvent { + /// Constructor + const RotaryEvent({ + required this.direction, + this.magnitude, + }); + + /// The direction of the rotary event. + final RotaryDirection direction; + + /// The magnitude of the rotation. + /// + /// Null on Tizen devices. Tizen devices only report the [direction] of the + /// event, and the rotation angle may differ by device. + final double? magnitude; +} + +/// The direction of the rotary event. +enum RotaryDirection { + /// A rotation event in the clockwise direction. + clockwise, + + /// A rotation event in the counter clockwise direction. + counterClockwise, +} + +RotaryEvent _parseEvent(dynamic event) { + if (event is Map) { + return RotaryEvent( + direction: RotaryDirection.values.byName(event['direction'] as String), + magnitude: event['magnitude'] as double, + ); + } else if (event is bool) { + return event + ? const RotaryEvent(direction: RotaryDirection.clockwise) + : const RotaryEvent(direction: RotaryDirection.counterClockwise); + } else { + throw PlatformException( + code: 'type_cast', + details: 'Platform plugin returns invalid type for rotary event'); + } +} diff --git a/packages/wearable_rotary/lib/wearable_rotary.dart b/packages/wearable_rotary/lib/wearable_rotary.dart index a4abf8bac..c0f9f688f 100644 --- a/packages/wearable_rotary/lib/wearable_rotary.dart +++ b/packages/wearable_rotary/lib/wearable_rotary.dart @@ -1,37 +1,4 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +library wearable_rotary; -import 'package:flutter/services.dart'; - -const String _channelName = 'flutter.wearable_rotary.channel'; - -const EventChannel _channel = EventChannel(_channelName); - -/// A broadcast stream of events from the device rotary sensor. -Stream get rotaryEvents { - return _rotaryEvents ??= _channel - .receiveBroadcastStream() - .map((dynamic event) => _parseEvent(event)); -} - -Stream? _rotaryEvents; - -/// A single "click" of rotation. The rotation angle may differ by device. -enum RotaryEvent { - /// A rotation event in the clockwise direction. - clockwise, - - /// A rotation event in the counter clockwise direction. - counterClockwise, -} - -RotaryEvent _parseEvent(dynamic event) { - if (event is bool) { - return event ? RotaryEvent.clockwise : RotaryEvent.counterClockwise; - } else { - throw PlatformException( - code: 'type_cast', - details: 'Platform plugin returns non-bool type for rotary event'); - } -} +export 'src/rotary_scroll_controller.dart'; +export 'src/wearable_rotary_base.dart'; diff --git a/packages/wearable_rotary/pubspec.yaml b/packages/wearable_rotary/pubspec.yaml index d49dfefb1..550fb86c5 100644 --- a/packages/wearable_rotary/pubspec.yaml +++ b/packages/wearable_rotary/pubspec.yaml @@ -1,8 +1,8 @@ name: wearable_rotary -description: Flutter plugin that can listen to rotary events on Galaxy watch devices. +description: Flutter plugin that can listen to rotary events on Wear OS and Tizen Galaxy watch devices. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/wearable_rotary -version: 1.0.2 +version: 2.0.0 environment: sdk: '>=2.12.0 <3.0.0' @@ -11,6 +11,9 @@ environment: flutter: plugin: platforms: + android: + package: com.samsung.wearable_rotary + pluginClass: WearableRotaryPlugin tizen: pluginClass: WearableRotaryPlugin fileName: wearable_rotary_plugin.h diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index ae58e29f3..c8e1d2318 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 + +* Change the backing web engine from LWE to EFL WebKit (EWK). + ## 0.5.6 * Update LWE binary (9af6ea4101d173935fe6e6cd3f2c91ca17ed451e). diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index 684b3187d..5e9a7c2c8 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -2,7 +2,8 @@ [![pub package](https://img.shields.io/pub/v/webview_flutter_tizen.svg)](https://pub.dev/packages/webview_flutter_tizen) -The Tizen implementation of [`webview_flutter`](https://github.com/flutter/plugins/tree/master/packages/webview_flutter). +The Tizen implementation of [`webview_flutter`](https://github.com/flutter/plugins/tree/main/packages/webview_flutter) only for Tizen TV devices. +The WebView widget is backed by the EFL WebKit (EWK) on Tizen. ## Required privileges @@ -21,7 +22,7 @@ This package is not an _endorsed_ implementation of `webview_flutter`. Therefore ```yaml dependencies: webview_flutter: ^3.0.4 - webview_flutter_tizen: ^0.5.6 + webview_flutter_tizen: ^0.6.0 ``` ## Example @@ -46,6 +47,4 @@ class WebViewExampleState extends State { ## Supported devices -This plugin is supported on devices running Tizen 5.5 or later. - -The WebView widget is backed by the Lightweight Web Engine (LWE) on Tizen. For a detailed list of features supported by the Lightweight Web Engine, refer to [this page](https://git.tizen.org/cgit/platform/upstream/lightweight-web-engine/tree/docs/Spec.md?h=tizen). +This plugin is supported on Tizen TV devices running Tizen 5.5 or later. diff --git a/packages/webview_flutter/lib/webview_flutter_tizen.dart b/packages/webview_flutter/lib/webview_flutter_tizen.dart index 1470cba7d..70ae2ebf5 100644 --- a/packages/webview_flutter/lib/webview_flutter_tizen.dart +++ b/packages/webview_flutter/lib/webview_flutter_tizen.dart @@ -7,16 +7,14 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_tizen/widgets.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -part 'src/platform_view_tizen.dart'; - -/// Builds an Tizen webview. +/// Builds a Tizen webview. /// /// This is used as the default implementation for [WebView.platform] on Tizen. It uses a method channel to /// communicate with the platform code. diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 942969c7c..777933d6d 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_tizen description: Tizen implementation of the webview plugin homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/webview_flutter -version: 0.5.6 +version: 0.6.0 environment: sdk: ">=2.17.0 <3.0.0" @@ -19,6 +19,7 @@ flutter: dependencies: flutter: sdk: flutter + flutter_tizen: ^0.2.0 webview_flutter: ^3.0.4 webview_flutter_platform_interface: ^1.8.0 diff --git a/packages/webview_flutter/tizen/project_def.prop b/packages/webview_flutter/tizen/project_def.prop index c497d3003..ecd814012 100644 --- a/packages/webview_flutter/tizen/project_def.prop +++ b/packages/webview_flutter/tizen/project_def.prop @@ -24,6 +24,6 @@ USER_INC_FILES = USER_CPP_INC_FILES = # Linker options -USER_LIBS = pthread lightweight-web-engine.flutter tuv -USER_LIB_DIRS = lib/${BUILD_ARCH} -USER_LFLAGS = -Wl,-rpath='$$ORIGIN' +USER_LIBS = +USER_LIB_DIRS = +USER_LFLAGS = diff --git a/packages/webview_flutter/tizen/src/buffer_pool.cc b/packages/webview_flutter/tizen/src/buffer_pool.cc index 7e3a2dd0e..ed8fde021 100644 --- a/packages/webview_flutter/tizen/src/buffer_pool.cc +++ b/packages/webview_flutter/tizen/src/buffer_pool.cc @@ -9,7 +9,7 @@ BufferUnit::BufferUnit(int32_t width, int32_t height) { Reset(width, height); } BufferUnit::~BufferUnit() { - if (tbm_surface_) { + if (tbm_surface_ && !use_external_buffer_) { tbm_surface_destroy(tbm_surface_); tbm_surface_ = nullptr; } @@ -19,6 +19,23 @@ BufferUnit::~BufferUnit() { } } +void BufferUnit::UseExternalBuffer() { + if (!use_external_buffer_) { + use_external_buffer_ = true; + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + } +} + +void BufferUnit::SetExternalBuffer(tbm_surface_h tbm_surface) { + if (use_external_buffer_) { + tbm_surface_ = tbm_surface; + gpu_surface_->handle = tbm_surface_; + } +} + bool BufferUnit::MarkInUse() { if (!is_used_) { is_used_ = true; diff --git a/packages/webview_flutter/tizen/src/buffer_pool.h b/packages/webview_flutter/tizen/src/buffer_pool.h index e648a7fe9..0128c63ce 100644 --- a/packages/webview_flutter/tizen/src/buffer_pool.h +++ b/packages/webview_flutter/tizen/src/buffer_pool.h @@ -24,6 +24,9 @@ class BufferUnit { bool IsUsed() { return is_used_ && tbm_surface_; } + void UseExternalBuffer(); + void SetExternalBuffer(tbm_surface_h tbm_surface); + tbm_surface_h Surface(); FlutterDesktopGpuSurfaceDescriptor* GpuSurface() { return gpu_surface_; } @@ -35,6 +38,7 @@ class BufferUnit { private: bool is_used_ = false; + bool use_external_buffer_ = false; int32_t width_ = 0; int32_t height_ = 0; tbm_surface_h tbm_surface_ = nullptr; diff --git a/packages/webview_flutter/tizen/src/ewk_internal_api_binding.cc b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.cc new file mode 100644 index 000000000..82d7757a6 --- /dev/null +++ b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.cc @@ -0,0 +1,64 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ewk_internal_api_binding.h" + +#include + +EwkInternalApiBinding::EwkInternalApiBinding() { + handle_ = dlopen("libchromium-ewk.so", RTLD_LAZY); +} + +EwkInternalApiBinding::~EwkInternalApiBinding() { + if (handle_) { + dlclose(handle_); + } +} + +bool EwkInternalApiBinding::Initialize() { + if (handle_ == nullptr) { + return false; + } + + // ewk_view + view.SetBackgroundColor = reinterpret_cast( + dlsym(handle_, "ewk_view_bg_color_set")); + view.FeedTouchEvent = reinterpret_cast( + dlsym(handle_, "ewk_view_feed_touch_event")); + view.SendKeyEvent = reinterpret_cast( + dlsym(handle_, "ewk_view_send_key_event")); + view.OffscreenRenderingEnabledSet = + reinterpret_cast( + dlsym(handle_, "ewk_view_offscreen_rendering_enabled_set")); + view.ImeWindowSet = reinterpret_cast( + dlsym(handle_, "ewk_view_ime_window_set")); + view.KeyEventsEnabledSet = reinterpret_cast( + dlsym(handle_, "ewk_view_key_events_enabled_set")); + + // ewk_main + main.SetArguments = reinterpret_cast( + dlsym(handle_, "ewk_set_arguments")); + + // ewk_settings + settings.ImePanelEnabledSet = + reinterpret_cast( + dlsym(handle_, "ewk_settings_ime_panel_enabled_set")); + + // ewk_console_message + console_message.LevelGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_level_get")); + console_message.TextGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_text_get")); + console_message.LineGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_line_get")); + console_message.SourceGet = reinterpret_cast( + dlsym(handle_, "ewk_console_message_source_get")); + + return view.SetBackgroundColor && view.FeedTouchEvent && view.SendKeyEvent && + view.OffscreenRenderingEnabledSet && view.ImeWindowSet && + view.KeyEventsEnabledSet && main.SetArguments && + settings.ImePanelEnabledSet && console_message.LevelGet && + console_message.TextGet && console_message.LineGet && + console_message.SourceGet; +} diff --git a/packages/webview_flutter/tizen/src/ewk_internal_api_binding.h b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.h new file mode 100644 index 000000000..f4a8971f4 --- /dev/null +++ b/packages/webview_flutter/tizen/src/ewk_internal_api_binding.h @@ -0,0 +1,117 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_EWK_INTERNAL_API_BINDING_H_ +#define FLUTTER_PLUGIN_EWK_INTERNAL_API_BINDING_H_ + +#include + +typedef enum { + EWK_TOUCH_START, + EWK_TOUCH_MOVE, + EWK_TOUCH_END, + EWK_TOUCH_CANCEL +} Ewk_Touch_Event_Type; + +typedef struct _Ewk_Touch_Point Ewk_Touch_Point; + +struct _Ewk_Touch_Point { + int id; + int x; + int y; + Evas_Touch_Point_State state; +}; + +typedef Eina_Bool (*EwkViewBgColorSetFnPtr)(Evas_Object* obj, int r, int g, + int b, int a); +typedef Eina_Bool (*EwkViewFeedTouchEventFnPtr)(Evas_Object* obj, + Ewk_Touch_Event_Type type, + const Eina_List* points, + const Evas_Modifier* modifiers); +typedef Eina_Bool (*EwkViewSendKeyEventFnPtr)(Evas_Object* obj, void* key_event, + Eina_Bool is_press); +typedef void (*EwkViewOffscreenRenderingEnabledSetFnPtr)(Evas_Object* obj, + Eina_Bool enabled); +typedef void (*EwkViewImeWindowSetFnPtr)(Evas_Object* obj, void* window); +typedef Eina_Bool (*EwkViewKeyEventsEnabledSetFnPtr)(Evas_Object* obj, + Eina_Bool enabled); + +typedef struct { + EwkViewBgColorSetFnPtr SetBackgroundColor = nullptr; + EwkViewFeedTouchEventFnPtr FeedTouchEvent = nullptr; + EwkViewSendKeyEventFnPtr SendKeyEvent = nullptr; + EwkViewOffscreenRenderingEnabledSetFnPtr OffscreenRenderingEnabledSet = + nullptr; + EwkViewImeWindowSetFnPtr ImeWindowSet = nullptr; + EwkViewKeyEventsEnabledSetFnPtr KeyEventsEnabledSet = nullptr; +} EwkViewProcTable; + +typedef void (*EwkSetArgumentsFnPtr)(int argc, char** argv); + +typedef struct { + EwkSetArgumentsFnPtr SetArguments = nullptr; +} EwkMainProcTable; + +typedef struct Ewk_Settings Ewk_Settings; +typedef void (*EwkSettingsImePanelEnabledSetFnPtr)(Ewk_Settings* settings, + Eina_Bool enabled); + +typedef struct { + EwkSettingsImePanelEnabledSetFnPtr ImePanelEnabledSet = nullptr; +} EWKSettingsProcTable; + +typedef struct _Ewk_Console_Message Ewk_Console_Message; + +typedef enum { + EWK_CONSOLE_MESSAGE_LEVEL_NULL, + EWK_CONSOLE_MESSAGE_LEVEL_LOG, + EWK_CONSOLE_MESSAGE_LEVEL_WARNING, + EWK_CONSOLE_MESSAGE_LEVEL_ERROR, + EWK_CONSOLE_MESSAGE_LEVEL_DEBUG, + EWK_CONSOLE_MESSAGE_LEVEL_INFO, +} Ewk_Console_Message_Level; + +typedef Ewk_Console_Message_Level (*EwkConsoleMessageLevelGetFnPtr)( + const Ewk_Console_Message* message); +typedef Eina_Stringshare* (*EwkConsoleMessageTextGetFnPtr)( + const Ewk_Console_Message* message); +typedef unsigned (*EwkConsoleMessageLineGetFnPtr)( + const Ewk_Console_Message* message); +typedef Eina_Stringshare* (*EwkConsoleMessageSourceGetFnPtr)( + const Ewk_Console_Message* message); + +typedef struct { + EwkConsoleMessageLevelGetFnPtr LevelGet = nullptr; + EwkConsoleMessageTextGetFnPtr TextGet = nullptr; + EwkConsoleMessageLineGetFnPtr LineGet = nullptr; + EwkConsoleMessageSourceGetFnPtr SourceGet = nullptr; +} EWKConsoleMessageProcTable; + +class EwkInternalApiBinding { + public: + static EwkInternalApiBinding& GetInstance() { + static EwkInternalApiBinding instance = EwkInternalApiBinding(); + return instance; + } + + ~EwkInternalApiBinding(); + + EwkInternalApiBinding(const EwkInternalApiBinding&) = delete; + + EwkInternalApiBinding& operator=(const EwkInternalApiBinding&) = delete; + + bool Initialize(); + + EwkViewProcTable view; + EwkMainProcTable main; + EWKSettingsProcTable settings; + EWKConsoleMessageProcTable console_message; + + private: + EwkInternalApiBinding(); + + void* handle_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_EWK_INTERNAL_API_BINDING_H_ diff --git a/packages/webview_flutter/tizen/src/webview.cc b/packages/webview_flutter/tizen/src/webview.cc index 934defcc4..7144bbade 100644 --- a/packages/webview_flutter/tizen/src/webview.cc +++ b/packages/webview_flutter/tizen/src/webview.cc @@ -4,117 +4,76 @@ #include "webview.h" +#include #include #include #include -#include #include -#include -#include - #include "buffer_pool.h" +#include "ewk_internal_api_binding.h" #include "log.h" -#include "lwe/LWEWebView.h" -#include "lwe/PlatformIntegrationData.h" #include "webview_factory.h" -static constexpr size_t kBufferPoolSize = 5; +namespace { + +typedef flutter::MethodCall FlMethodCall; +typedef flutter::MethodResult FlMethodResult; +typedef flutter::MethodChannel FlMethodChannel; -extern "C" size_t LWE_EXPORT createWebViewInstance( - unsigned x, unsigned y, unsigned width, unsigned height, - float devicePixelRatio, const char* defaultFontName, const char* locale, - const char* timezoneID, - const std::function<::LWE::WebContainer::ExternalImageInfo(void)>& - prepareImageCb, - const std::function& flushCb, - bool useSWBackend); +constexpr size_t kBufferPoolSize = 5; +constexpr char kEwkInstance[] = "ewk_instance"; class NavigationRequestResult : public flutter::MethodResult { public: - NavigationRequestResult(std::string url, WebView* webview) - : url_(url), webview_(webview) {} + NavigationRequestResult(WebView* webview) : webview_(webview) {} void SuccessInternal(const flutter::EncodableValue* should_load) override { if (std::holds_alternative(*should_load)) { if (std::get(*should_load)) { - LoadUrl(); + webview_->Resume(); + return; } } + webview_->Stop(); } void ErrorInternal(const std::string& error_code, const std::string& error_message, const flutter::EncodableValue* error_details) override { + webview_->Stop(); LOG_ERROR("The request unexpectedly completed with an error."); } void NotImplementedInternal() override { + webview_->Stop(); LOG_ERROR("The target method was unexpectedly unimplemented."); } private: - void LoadUrl() { - if (webview_ && webview_->GetWebViewInstance()) { - webview_->GetWebViewInstance()->LoadURL(url_); - } - } - - std::string url_; WebView* webview_; }; -enum class ResourceErrorType { - NoError, - UnknownError, - HostLookupError, - UnsupportedAuthSchemeError, - AuthenticationError, - ProxyAuthenticationError, - ConnectError, - IOError, - TimeoutError, - RedirectLoopError, - UnsupportedSchemeError, - FailedSSLHandshakeError, - BadURLError, - FileError, - FileNotFoundError, - TooManyRequestError, -}; - -static std::string ErrorCodeToString(int error_code) { - switch (ResourceErrorType(error_code)) { - case ResourceErrorType::AuthenticationError: +std::string ErrorCodeToString(int error_code) { + switch (error_code) { + case EWK_ERROR_CODE_AUTHENTICATION: return "authentication"; - case ResourceErrorType::BadURLError: + case EWK_ERROR_CODE_BAD_URL: return "badUrl"; - case ResourceErrorType::ConnectError: - return "connect"; - case ResourceErrorType::FailedSSLHandshakeError: + case EWK_ERROR_CODE_FAILED_TLS_HANDSHAKE: return "failedSslHandshake"; - case ResourceErrorType::FileError: + case EWK_ERROR_CODE_FAILED_FILE_IO: return "file"; - case ResourceErrorType::FileNotFoundError: - return "fileNotFound"; - case ResourceErrorType::HostLookupError: + case EWK_ERROR_CODE_CANT_LOOKUP_HOST: return "hostLookup"; - case ResourceErrorType::IOError: - return "io"; - case ResourceErrorType::ProxyAuthenticationError: - return "proxyAuthentication"; - case ResourceErrorType::RedirectLoopError: - return "redirectLoop"; - case ResourceErrorType::TimeoutError: + case EWK_ERROR_CODE_REQUEST_TIMEOUT: return "timeout"; - case ResourceErrorType::TooManyRequestError: + case EWK_ERROR_CODE_TOO_MANY_REQUESTS: return "tooManyRequests"; - case ResourceErrorType::UnknownError: + case EWK_ERROR_CODE_UNKNOWN: return "unknown"; - case ResourceErrorType::UnsupportedAuthSchemeError: - return "unsupportedAuthScheme"; - case ResourceErrorType::UnsupportedSchemeError: + case EWK_ERROR_CODE_UNSUPPORTED_SCHEME: return "unsupportedScheme"; default: LOG_ERROR("Unknown error type: %d", error_code); @@ -123,8 +82,8 @@ static std::string ErrorCodeToString(int error_code) { } template -static bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, - std::string key, T* out) { +bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, + std::string key, T* out) { if (auto* map = std::get_if(arguments)) { auto iter = map->find(flutter::EncodableValue(key)); if (iter != map->end() && !iter->second.IsNull()) { @@ -137,34 +96,24 @@ static bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, return false; } -static bool IsRunningOnEmulator() { - bool result = false; - char* value = nullptr; - int ret = system_info_get_platform_string( - "http://tizen.org/system/model_name", &value); - if (ret == SYSTEM_INFO_ERROR_NONE && strcmp(value, "Emulator") == 0) { - result = true; - } - if (value) { - free(value); - } - return result; -} +} // namespace WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, flutter::TextureRegistrar* texture_registrar, double width, - double height, const flutter::EncodableValue& params) + double height, const flutter::EncodableValue& params, + void* win) : PlatformView(registrar, view_id, nullptr), texture_registrar_(texture_registrar), width_(width), - height_(height) { - use_sw_backend_ = IsRunningOnEmulator(); - if (use_sw_backend_) { - tbm_pool_ = std::make_unique(width, height); - } else { - tbm_pool_ = std::make_unique(width, height, kBufferPoolSize); + height_(height), + win_(win) { + if (!EwkInternalApiBinding::GetInstance().Initialize()) { + LOG_ERROR("Failed to Initialize EWK internal APIs."); + return; } + tbm_pool_ = std::make_unique(width, height); + texture_variant_ = std::make_unique(flutter::GpuSurfaceTexture( kFlutterDesktopGpuSurfaceTypeNone, @@ -176,7 +125,7 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, InitWebView(); - channel_ = std::make_unique>( + channel_ = std::make_unique( GetPluginRegistrar()->messenger(), GetChannelName(), &flutter::StandardMethodCodec::GetInstance()); channel_->SetMethodCallHandler( @@ -184,11 +133,9 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, webview->HandleMethodCall(call, std::move(result)); }); - auto cookie_channel = - std::make_unique>( - GetPluginRegistrar()->messenger(), - "plugins.flutter.io/cookie_manager", - &flutter::StandardMethodCodec::GetInstance()); + auto cookie_channel = std::make_unique( + GetPluginRegistrar()->messenger(), "plugins.flutter.io/cookie_manager", + &flutter::StandardMethodCodec::GetInstance()); cookie_channel->SetMethodCallHandler( [webview = this](const auto& call, auto result) { webview->HandleCookieMethodCall(call, std::move(result)); @@ -201,10 +148,9 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, int color; if (GetValueFromEncodableMap(¶ms, "backgroundColor", &color)) { - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetBaseBackgroundColor(color >> 16 & 0xff, color >> 8 & 0xff, - color & 0xff, color >> 24 & 0xff); - webview_instance_->SetSettings(settings); + EwkInternalApiBinding::GetInstance().view.SetBackgroundColor( + webview_instance_, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, + color >> 24 & 0xff); } flutter::EncodableMap settings; @@ -225,68 +171,9 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, std::string user_agent; if (GetValueFromEncodableMap(¶ms, "userAgent", &user_agent)) { - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetUserAgentString(user_agent); - webview_instance_->SetSettings(settings); + ewk_view_user_agent_set(webview_instance_, user_agent.c_str()); } - - webview_instance_->RegisterOnPageStartedHandler( - [this](LWE::WebContainer* container, const std::string& url) { - flutter::EncodableMap args = { - {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; - channel_->InvokeMethod("onPageStarted", - std::make_unique(args)); - }); - webview_instance_->RegisterOnPageLoadedHandler( - [this](LWE::WebContainer* container, const std::string& url) { - flutter::EncodableMap args = { - {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; - channel_->InvokeMethod("onPageFinished", - std::make_unique(args)); - }); - webview_instance_->RegisterOnProgressChangedHandler( - [this](LWE::WebContainer* container, int progress) { - if (!has_progress_tracking_) { - return; - } - flutter::EncodableMap args = {{flutter::EncodableValue("progress"), - flutter::EncodableValue(progress)}}; - channel_->InvokeMethod("onProgress", - std::make_unique(args)); - }); - webview_instance_->RegisterOnReceivedErrorHandler( - [this](LWE::WebContainer* container, LWE::ResourceError error) { - flutter::EncodableMap args = { - {flutter::EncodableValue("errorCode"), - flutter::EncodableValue(error.GetErrorCode())}, - {flutter::EncodableValue("description"), - flutter::EncodableValue(error.GetDescription())}, - {flutter::EncodableValue("errorType"), - flutter::EncodableValue(ErrorCodeToString(error.GetErrorCode()))}, - {flutter::EncodableValue("failingUrl"), - flutter::EncodableValue(error.GetUrl())}, - }; - channel_->InvokeMethod("onWebResourceError", - std::make_unique(args)); - }); - webview_instance_->RegisterShouldOverrideUrlLoadingHandler( - [this](LWE::WebContainer* view, const std::string& url) -> bool { - if (!has_navigation_delegate_) { - return false; - } - flutter::EncodableMap args = { - {flutter::EncodableValue("url"), flutter::EncodableValue(url)}, - {flutter::EncodableValue("isForMainFrame"), - flutter::EncodableValue(true)}, - }; - auto result = std::make_unique(url, this); - channel_->InvokeMethod("navigationRequest", - std::make_unique(args), - std::move(result)); - return true; - }); - - webview_instance_->LoadURL(url); + ewk_view_url_set(webview_instance_, url.c_str()); } void WebView::ApplySettings(const flutter::EncodableMap& settings) { @@ -294,7 +181,6 @@ void WebView::ApplySettings(const flutter::EncodableMap& settings) { if (std::holds_alternative(key)) { std::string string_key = std::get(key); if (string_key == "jsMode") { - // NOTE: Not supported by LWE on Tizen. } else if (string_key == "hasNavigationDelegate") { if (std::holds_alternative(value)) { has_navigation_delegate_ = std::get(value); @@ -304,19 +190,14 @@ void WebView::ApplySettings(const flutter::EncodableMap& settings) { has_progress_tracking_ = std::get(value); } } else if (string_key == "debuggingEnabled") { - // NOTE: Not supported by LWE on Tizen. } else if (string_key == "gestureNavigationEnabled") { - // NOTE: Not supported by LWE on Tizen. } else if (string_key == "allowsInlineMediaPlayback") { - // no-op inline media playback is always allowed on Tizen. } else if (string_key == "userAgent") { if (std::holds_alternative(value)) { - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetUserAgentString(std::get(value)); - webview_instance_->SetSettings(settings); + ewk_view_user_agent_set(webview_instance_, + std::get(value).c_str()); } } else if (string_key == "zoomEnabled") { - // NOTE: Not supported by LWE on Tizen. } else { LOG_WARN("Unknown settings key: %s", string_key.c_str()); } @@ -333,18 +214,8 @@ void WebView::ApplySettings(const flutter::EncodableMap& settings) { */ void WebView::RegisterJavaScriptChannelName(const std::string& name) { LOG_DEBUG("Register a JavaScript channel: %s", name.c_str()); - - auto on_message = [this, name](const std::string& message) -> std::string { - LOG_DEBUG("JavaScript channel message: %s", message.c_str()); - flutter::EncodableMap args = { - {flutter::EncodableValue("channel"), flutter::EncodableValue(name)}, - {flutter::EncodableValue("message"), flutter::EncodableValue(message)}, - }; - channel_->InvokeMethod("javascriptChannelMessage", - std::make_unique(args)); - return "success"; - }; - webview_instance_->AddJavaScriptInterface(name, "postMessage", on_message); + ewk_view_javascript_message_handler_add( + webview_instance_, &WebView::OnJavaScriptMessage, name.c_str()); } WebView::~WebView() { Dispose(); } @@ -354,12 +225,19 @@ std::string WebView::GetChannelName() { } void WebView::Dispose() { + evas_object_smart_callback_del(webview_instance_, "offscreen,frame,rendered", + &WebView::OnFrameRendered); + evas_object_smart_callback_del(webview_instance_, "load,started", + &WebView::OnLoadStarted); + evas_object_smart_callback_del(webview_instance_, "load,finished", + &WebView::OnLoadFinished); + evas_object_smart_callback_del(webview_instance_, "load,error", + &WebView::OnLoadError); + evas_object_smart_callback_del(webview_instance_, "console,message", + &WebView::OnConsoleMessage); + evas_object_smart_callback_del(webview_instance_, "policy,navigation,decide", + &WebView::OnNavigationPolicy); texture_registrar_->UnregisterTexture(GetTextureId()); - - if (webview_instance_) { - webview_instance_->Destroy(); - webview_instance_ = nullptr; - } } void WebView::Resize(double width, double height) { @@ -371,225 +249,37 @@ void WebView::Resize(double width, double height) { } tbm_pool_->Prepare(width_, height_); - webview_instance_->ResizeTo(width_, height_); + evas_object_resize(webview_instance_, width_, height_); } void WebView::Touch(int type, int button, double x, double y, double dx, double dy) { + Ewk_Touch_Event_Type mouse_event_type = EWK_TOUCH_START; + Evas_Touch_Point_State state = EVAS_TOUCH_POINT_DOWN; if (type == 0) { // down event - webview_instance_->DispatchMouseDownEvent( - LWE::MouseButtonValue::LeftButton, - LWE::MouseButtonsValue::LeftButtonDown, x, y); - is_mouse_lbutton_down_ = true; + mouse_event_type = EWK_TOUCH_START; + state = EVAS_TOUCH_POINT_DOWN; } else if (type == 1) { // move event - webview_instance_->DispatchMouseMoveEvent( - is_mouse_lbutton_down_ ? LWE::MouseButtonValue::LeftButton - : LWE::MouseButtonValue::NoButton, - is_mouse_lbutton_down_ ? LWE::MouseButtonsValue::LeftButtonDown - : LWE::MouseButtonsValue::NoButtonDown, - x, y); + mouse_event_type = EWK_TOUCH_MOVE; + state = EVAS_TOUCH_POINT_MOVE; + } else if (type == 2) { // up event - webview_instance_->DispatchMouseUpEvent( - LWE::MouseButtonValue::NoButton, LWE::MouseButtonsValue::NoButtonDown, - x, y); - is_mouse_lbutton_down_ = false; + mouse_event_type = EWK_TOUCH_END; + state = EVAS_TOUCH_POINT_UP; } else { - LOG_WARN("Unknown touch event type: %d", type); - } -} - -static LWE::KeyValue KeyToKeyValue(const std::string& key, - bool is_shift_pressed) { - if (key == "Left") { - return LWE::KeyValue::ArrowLeftKey; - } else if (key == "Right") { - return LWE::KeyValue::ArrowRightKey; - } else if (key == "Up") { - return LWE::KeyValue::ArrowUpKey; - } else if (key == "Down") { - return LWE::KeyValue::ArrowDownKey; - } else if (key == "space") { - return LWE::KeyValue::SpaceKey; - } else if (key == "Select") { - return LWE::KeyValue::EnterKey; - } else if (key == "Return") { - return LWE::KeyValue::EnterKey; - } else if (key == "Tab") { - return LWE::KeyValue::TabKey; - } else if (key == "BackSpace") { - return LWE::KeyValue::BackspaceKey; - } else if (key == "Escape") { - return LWE::KeyValue::EscapeKey; - } else if (key == "Delete") { - return LWE::KeyValue::DeleteKey; - } else if (key == "at") { - return LWE::KeyValue::AtMarkKey; - } else if (key == "minus") { - if (is_shift_pressed) { - return LWE::KeyValue::UnderScoreMarkKey; - } else { - return LWE::KeyValue::MinusMarkKey; - } - } else if (key == "equal") { - if (is_shift_pressed) { - return LWE::KeyValue::PlusMarkKey; - } else { - return LWE::KeyValue::EqualitySignKey; - } - } else if (key == "bracketleft") { - if (is_shift_pressed) { - return LWE::KeyValue::LeftCurlyBracketMarkKey; - } else { - return LWE::KeyValue::LeftSquareBracketKey; - } - } else if (key == "bracketright") { - if (is_shift_pressed) { - return LWE::KeyValue::RightCurlyBracketMarkKey; - } else { - return LWE::KeyValue::RightSquareBracketKey; - } - } else if (key == "semicolon") { - if (is_shift_pressed) { - return LWE::KeyValue::ColonMarkKey; - } else { - return LWE::KeyValue::SemiColonMarkKey; - } - } else if (key == "apostrophe") { - if (is_shift_pressed) { - return LWE::KeyValue::DoubleQuoteMarkKey; - } else { - return LWE::KeyValue::SingleQuoteMarkKey; - } - } else if (key == "comma") { - if (is_shift_pressed) { - return LWE::KeyValue::LessThanMarkKey; - } else { - return LWE::KeyValue::CommaMarkKey; - } - } else if (key == "period") { - if (is_shift_pressed) { - return LWE::KeyValue::GreaterThanSignKey; - } else { - return LWE::KeyValue::PeriodKey; - } - } else if (key == "slash") { - if (is_shift_pressed) { - return LWE::KeyValue::QuestionMarkKey; - } else { - return LWE::KeyValue::SlashKey; - } - } else if (key.length() == 1) { - const char ch = key.at(0); - if (ch >= '0' && ch <= '9') { - if (is_shift_pressed) { - switch (ch) { - case '1': - return LWE::KeyValue::ExclamationMarkKey; - case '2': - return LWE::KeyValue::AtMarkKey; - case '3': - return LWE::KeyValue::SharpMarkKey; - case '4': - return LWE::KeyValue::DollarMarkKey; - case '5': - return LWE::KeyValue::PercentMarkKey; - case '6': - return LWE::KeyValue::CaretMarkKey; - case '7': - return LWE::KeyValue::AmpersandMarkKey; - case '8': - return LWE::KeyValue::AsteriskMarkKey; - case '9': - return LWE::KeyValue::LeftParenthesisMarkKey; - case '0': - return LWE::KeyValue::RightParenthesisMarkKey; - } - } - return LWE::KeyValue(LWE::KeyValue::Digit0Key + ch - '0'); - } else if (ch >= 'a' && ch <= 'z') { - if (is_shift_pressed) { - return LWE::KeyValue(LWE::KeyValue::LowerAKey + ch - 'a' - 32); - } else { - return LWE::KeyValue(LWE::KeyValue::LowerAKey + ch - 'a'); - } - } else if (ch >= 'A' && ch <= 'Z') { - if (is_shift_pressed) { - return LWE::KeyValue(LWE::KeyValue::AKey + ch - 'A' + 32); - } else { - return LWE::KeyValue(LWE::KeyValue::AKey + ch - 'A'); - } - } - } else if (key == "XF86AudioRaiseVolume") { - return LWE::KeyValue::TVVolumeUpKey; - } else if (key == "XF86AudioLowerVolume") { - return LWE::KeyValue::TVVolumeDownKey; - } else if (key == "XF86AudioMute") { - return LWE::KeyValue::TVMuteKey; - } else if (key == "XF86RaiseChannel") { - return LWE::KeyValue::TVChannelUpKey; - } else if (key == "XF86LowerChannel") { - return LWE::KeyValue::TVChannelDownKey; - } else if (key == "XF86AudioRewind") { - return LWE::KeyValue::MediaTrackPreviousKey; - } else if (key == "XF86AudioNext") { - return LWE::KeyValue::MediaTrackNextKey; - } else if (key == "XF86AudioPause") { - return LWE::KeyValue::MediaPauseKey; - } else if (key == "XF86AudioRecord") { - return LWE::KeyValue::MediaRecordKey; - } else if (key == "XF86AudioPlay") { - return LWE::KeyValue::MediaPlayKey; - } else if (key == "XF86AudioStop") { - return LWE::KeyValue::MediaStopKey; - } else if (key == "XF86Info") { - return LWE::KeyValue::TVInfoKey; - } else if (key == "XF86Back") { - return LWE::KeyValue::TVReturnKey; - } else if (key == "XF86Red") { - return LWE::KeyValue::TVRedKey; - } else if (key == "XF86Green") { - return LWE::KeyValue::TVGreenKey; - } else if (key == "XF86Yellow") { - return LWE::KeyValue::TVYellowKey; - } else if (key == "XF86Blue") { - return LWE::KeyValue::TVBlueKey; - } else if (key == "XF86SysMenu") { - return LWE::KeyValue::TVMenuKey; - } else if (key == "XF86Home") { - return LWE::KeyValue::TVHomeKey; - } else if (key == "XF86Exit") { - return LWE::KeyValue::TVExitKey; - } else if (key == "XF86PreviousChannel") { - return LWE::KeyValue::TVPreviousChannel; - } else if (key == "XF86ChannelList") { - return LWE::KeyValue::TVChannelList; - } else if (key == "XF86ChannelGuide") { - return LWE::KeyValue::TVChannelGuide; - } else if (key == "XF86SimpleMenu") { - return LWE::KeyValue::TVSimpleMenu; - } else if (key == "XF86EManual") { - return LWE::KeyValue::TVEManual; - } else if (key == "XF86ExtraApp") { - return LWE::KeyValue::TVExtraApp; - } else if (key == "XF86Search") { - return LWE::KeyValue::TVSearch; - } else if (key == "XF86PictureSize") { - return LWE::KeyValue::TVPictureSize; - } else if (key == "XF86Sleep") { - return LWE::KeyValue::TVSleep; - } else if (key == "XF86Caption") { - return LWE::KeyValue::TVCaption; - } else if (key == "XF86More") { - return LWE::KeyValue::TVMore; - } else if (key == "XF86BTVoice") { - return LWE::KeyValue::TVBTVoice; - } else if (key == "XF86Color") { - return LWE::KeyValue::TVColor; - } else if (key == "XF86PlayBack") { - return LWE::KeyValue::TVPlayBack; + // TODO: Not implemented } - LOG_WARN("Unknown key name: %s", key.c_str()); - return LWE::KeyValue::UnidentifiedKey; + Eina_List* pointList = 0; + Ewk_Touch_Point* point = new Ewk_Touch_Point; + point->id = 0; + point->x = x; + point->y = y; + point->state = state; + pointList = eina_list_append(pointList, point); + + EwkInternalApiBinding::GetInstance().view.FeedTouchEvent( + webview_instance_, mouse_event_type, pointList, 0); + eina_list_free(pointList); } bool WebView::SendKey(const char* key, const char* string, const char* compose, @@ -598,95 +288,96 @@ bool WebView::SendKey(const char* key, const char* string, const char* compose, return false; } - bool is_shift_pressed = modifiers & 1; - - struct Param { - LWE::WebContainer* webview_instance; - LWE::KeyValue key_value; - bool is_down; - }; - - Param* param = new Param(); - param->webview_instance = webview_instance_; - param->key_value = KeyToKeyValue(key, is_shift_pressed); - param->is_down = is_down; - - if (param->key_value == LWE::KeyValue::TVReturnKey && - webview_instance_->CanGoBack()) { - webview_instance_->GoBack(); + if (is_down) { + Evas_Event_Key_Down downEvent; + memset(&downEvent, 0, sizeof(Evas_Event_Key_Down)); + downEvent.key = key; + downEvent.string = string; + void* evasKeyEvent = static_cast(&downEvent); + EwkInternalApiBinding::GetInstance().view.SendKeyEvent( + webview_instance_, evasKeyEvent, is_down); + return true; + } else { + Evas_Event_Key_Up upEvent; + memset(&upEvent, 0, sizeof(Evas_Event_Key_Up)); + upEvent.key = key; + upEvent.string = string; + void* evasKeyEvent = static_cast(&upEvent); + EwkInternalApiBinding::GetInstance().view.SendKeyEvent( + webview_instance_, evasKeyEvent, is_down); return true; } - webview_instance_->AddIdleCallback( - [](void* data) { - Param* param = reinterpret_cast(data); - if (param->is_down) { - param->webview_instance->DispatchKeyDownEvent(param->key_value); - param->webview_instance->DispatchKeyPressEvent(param->key_value); - } else { - param->webview_instance->DispatchKeyUpEvent(param->key_value); - } - delete param; - }, - param); - return false; } +void WebView::Resume() { ewk_view_resume(webview_instance_); } + +void WebView::Stop() { ewk_view_stop(webview_instance_); } + void WebView::SetDirection(int direction) { // TODO: Implement if necessary. } void WebView::InitWebView() { - if (webview_instance_) { - webview_instance_->Destroy(); - webview_instance_ = nullptr; - } - - float pixel_ratio = 1.0; - - auto on_prepare_image = [this]() -> LWE::WebContainer::ExternalImageInfo { - std::lock_guard lock(mutex_); - LWE::WebContainer::ExternalImageInfo result; - if (!working_surface_) { - working_surface_ = tbm_pool_->GetAvailableBuffer(); - } - if (working_surface_) { - result.imageAddress = working_surface_->Surface(); - } else { - result.imageAddress = nullptr; - } - return result; - }; - auto on_flush = [this](LWE::WebContainer* container, bool is_rendered) { - if (is_rendered) { - std::lock_guard lock(mutex_); - if (candidate_surface_) { - tbm_pool_->Release(candidate_surface_); - candidate_surface_ = nullptr; - } - candidate_surface_ = working_surface_; - working_surface_ = nullptr; - texture_registrar_->MarkTextureFrameAvailable(GetTextureId()); - } + char* chromium_argv[] = { + const_cast("--disable-pinch"), + const_cast("--js-flags=--expose-gc"), + const_cast("--single-process"), + const_cast("--no-zygote"), }; - - webview_instance_ = - reinterpret_cast(createWebViewInstance( - 0, 0, width_, height_, pixel_ratio, "SamsungOneUI", "ko-KR", - "Asia/Seoul", on_prepare_image, on_flush, use_sw_backend_)); - -#ifndef TV_PROFILE - LWE::Settings settings = webview_instance_->GetSettings(); - settings.SetUserAgentString( - "Mozilla/5.0 (like Gecko/54.0 Firefox/54.0) Mobile"); - webview_instance_->SetSettings(settings); -#endif + int chromium_argc = sizeof(chromium_argv) / sizeof(chromium_argv[0]); + EwkInternalApiBinding::GetInstance().main.SetArguments(chromium_argc, + chromium_argv); + + ewk_init(); + Ecore_Evas* evas = ecore_evas_new("wayland_egl", 0, 0, 1, 1, 0); + + webview_instance_ = ewk_view_add(ecore_evas_get(evas)); + ecore_evas_focus_set(evas, true); + ewk_view_focus_set(webview_instance_, true); + EwkInternalApiBinding::GetInstance().view.OffscreenRenderingEnabledSet( + webview_instance_, true); + + Ewk_Settings* settings = ewk_view_settings_get(webview_instance_); + + Ewk_Context* context = ewk_view_context_get(webview_instance_); + Ewk_Cookie_Manager* manager = ewk_context_cookie_manager_get(context); + if (manager) { + ewk_cookie_manager_accept_policy_set( + manager, EWK_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY); + } + ewk_settings_viewport_meta_tag_set(settings, false); + EwkInternalApiBinding::GetInstance().settings.ImePanelEnabledSet(settings, + true); + ewk_settings_javascript_enabled_set(settings, true); + + EwkInternalApiBinding::GetInstance().view.ImeWindowSet(webview_instance_, + win_); + EwkInternalApiBinding::GetInstance().view.KeyEventsEnabledSet( + webview_instance_, true); + ewk_context_cache_model_set(context, EWK_CACHE_MODEL_PRIMARY_WEBBROWSER); + + evas_object_smart_callback_add(webview_instance_, "offscreen,frame,rendered", + &WebView::OnFrameRendered, this); + evas_object_smart_callback_add(webview_instance_, "load,started", + &WebView::OnLoadStarted, this); + evas_object_smart_callback_add(webview_instance_, "load,finished", + &WebView::OnLoadFinished, this); + evas_object_smart_callback_add(webview_instance_, "load,error", + &WebView::OnLoadError, this); + evas_object_smart_callback_add(webview_instance_, "console,message", + &WebView::OnConsoleMessage, this); + evas_object_smart_callback_add(webview_instance_, "policy,navigation,decide", + &WebView::OnNavigationPolicy, this); + Resize(width_, height_); + evas_object_show(webview_instance_); + + evas_object_data_set(webview_instance_, kEwkInstance, this); } -void WebView::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { +void WebView::HandleMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { if (!webview_instance_) { result->Error("Invalid operation", "The webview instance has not been initialized."); @@ -696,12 +387,10 @@ void WebView::HandleMethodCall( const std::string& method_name = method_call.method_name(); const flutter::EncodableValue* arguments = method_call.arguments(); - LOG_DEBUG("Handle a method call: %s", method_name.c_str()); - if (method_name == "loadUrl") { std::string url; if (GetValueFromEncodableMap(arguments, "url", &url)) { - webview_instance_->LoadURL(url); + ewk_view_url_set(webview_instance_, url.c_str()); result->Success(); } else { result->Error("Invalid argument", "No url provided."); @@ -713,39 +402,31 @@ void WebView::HandleMethodCall( } result->Success(); } else if (method_name == "canGoBack") { - result->Success(flutter::EncodableValue(webview_instance_->CanGoBack())); + result->Success( + flutter::EncodableValue(ewk_view_back_possible(webview_instance_))); } else if (method_name == "canGoForward") { - result->Success(flutter::EncodableValue(webview_instance_->CanGoForward())); + result->Success( + flutter::EncodableValue(ewk_view_forward_possible(webview_instance_))); } else if (method_name == "goBack") { - webview_instance_->GoBack(); + ewk_view_back(webview_instance_); result->Success(); } else if (method_name == "goForward") { - webview_instance_->GoForward(); + ewk_view_forward(webview_instance_); result->Success(); } else if (method_name == "reload") { - webview_instance_->Reload(); + ewk_view_reload(webview_instance_); result->Success(); } else if (method_name == "currentUrl") { - result->Success(flutter::EncodableValue(webview_instance_->GetURL())); + result->Success( + flutter::EncodableValue(ewk_view_url_get(webview_instance_))); } else if (method_name == "evaluateJavascript" || method_name == "runJavascriptReturningResult" || method_name == "runJavascript") { const auto* javascript = std::get_if(arguments); if (javascript) { - bool should_return = method_name != "runJavascript"; - auto on_result = [result = result.release(), - should_return](std::string value) { - LOG_DEBUG("JavaScript evaluation result: %s", value.c_str()); - if (result) { - if (should_return) { - result->Success(flutter::EncodableValue(value)); - } else { - result->Success(); - } - delete result; - } - }; - webview_instance_->EvaluateJavaScript(*javascript, on_result); + auto p_result = result.release(); + ewk_view_script_execute(webview_instance_, javascript->c_str(), + &WebView::OnEvaluateJavaScript, p_result); } else { result->Error("Invalid argument", "The argument must be a string."); } @@ -760,26 +441,17 @@ void WebView::HandleMethodCall( } result->Success(); } else if (method_name == "removeJavascriptChannels") { - const auto* channels = std::get_if(arguments); - if (channels) { - for (flutter::EncodableValue channel : *channels) { - if (std::holds_alternative(channel)) { - webview_instance_->RemoveJavascriptInterface( - std::get(channel), "postMessage"); - } - } - } - result->Success(); + result->NotImplemented(); } else if (method_name == "clearCache") { - webview_instance_->ClearCache(); - result->Success(); + result->NotImplemented(); } else if (method_name == "getTitle") { - result->Success(flutter::EncodableValue(webview_instance_->GetTitle())); + result->Success(flutter::EncodableValue( + std::string(ewk_view_title_get(webview_instance_)))); } else if (method_name == "scrollTo") { int x = 0, y = 0; if (GetValueFromEncodableMap(arguments, "x", &x) && GetValueFromEncodableMap(arguments, "y", &y)) { - webview_instance_->ScrollTo(x, y); + ewk_view_scroll_set(webview_instance_, x, y); result->Success(); } else { result->Error("Invalid argument", "No x or y provided."); @@ -788,15 +460,19 @@ void WebView::HandleMethodCall( int x = 0, y = 0; if (GetValueFromEncodableMap(arguments, "x", &x) && GetValueFromEncodableMap(arguments, "y", &y)) { - webview_instance_->ScrollBy(x, y); + ewk_view_scroll_by(webview_instance_, x, y); result->Success(); } else { result->Error("Invalid argument", "No x or y provided."); } } else if (method_name == "getScrollX") { - result->Success(flutter::EncodableValue(webview_instance_->GetScrollX())); + int x = 0; + ewk_view_scroll_pos_get(webview_instance_, &x, nullptr); + result->Success(flutter::EncodableValue(x)); } else if (method_name == "getScrollY") { - result->Success(flutter::EncodableValue(webview_instance_->GetScrollY())); + int y = 0; + ewk_view_scroll_pos_get(webview_instance_, nullptr, &y); + result->Success(flutter::EncodableValue(y)); } else if (method_name == "loadFlutterAsset") { const auto* key = std::get_if(arguments); if (key) { @@ -805,7 +481,7 @@ void WebView::HandleMethodCall( std::string url = std::string("file://") + res_path + "flutter_assets/" + *key; free(res_path); - webview_instance_->LoadURL(url); + ewk_view_url_set(webview_instance_, url.c_str()); result->Success(); } else { result->Error("Operation failed", @@ -823,13 +499,14 @@ void WebView::HandleMethodCall( if (GetValueFromEncodableMap(arguments, "baseUrl", &base_url)) { LOG_WARN("loadHtmlString: baseUrl is not supported and will be ignored."); } - webview_instance_->LoadData(html); + ewk_view_html_string_load(webview_instance_, html.c_str(), base_url.c_str(), + nullptr); result->Success(); } else if (method_name == "loadFile") { const auto* file_path = std::get_if(arguments); if (file_path) { std::string url = std::string("file://") + *file_path; - webview_instance_->LoadURL(url); + ewk_view_url_set(webview_instance_, url.c_str()); result->Success(); } else { result->Error("Invalid argument", "The argument must be a string."); @@ -843,9 +520,8 @@ void WebView::HandleMethodCall( } } -void WebView::HandleCookieMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { +void WebView::HandleCookieMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { if (!webview_instance_) { result->Error("Invalid operation", "The webview instance has not been initialized."); @@ -855,9 +531,14 @@ void WebView::HandleCookieMethodCall( const std::string& method_name = method_call.method_name(); if (method_name == "clearCookies") { - LWE::CookieManager* cookie = LWE::CookieManager::GetInstance(); - cookie->ClearCookies(); - result->Success(flutter::EncodableValue(true)); + Ewk_Cookie_Manager* manager = + ewk_context_cookie_manager_get(ewk_view_context_get(webview_instance_)); + if (manager) { + ewk_cookie_manager_cookies_clear(manager); + result->Success(flutter::EncodableValue(true)); + } else { + result->Error("Operation failed", "Failed to get cookie manager"); + } } else { result->NotImplemented(); } @@ -879,3 +560,125 @@ FlutterDesktopGpuSurfaceDescriptor* WebView::ObtainGpuSurface(size_t width, candidate_surface_ = nullptr; return rendered_surface_->GpuSurface(); } + +void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { + if (event_info) { + WebView* webview = static_cast(data); + + std::lock_guard lock(webview->mutex_); + if (!webview->working_surface_) { + webview->working_surface_ = webview->tbm_pool_->GetAvailableBuffer(); + webview->working_surface_->UseExternalBuffer(); + } + webview->working_surface_->SetExternalBuffer( + static_cast(event_info)); + + if (webview->candidate_surface_) { + webview->tbm_pool_->Release(webview->candidate_surface_); + webview->candidate_surface_ = nullptr; + } + webview->candidate_surface_ = webview->working_surface_; + webview->working_surface_ = nullptr; + webview->texture_registrar_->MarkTextureFrameAvailable( + webview->GetTextureId()); + } +} + +void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + std::string url = std::string(ewk_view_url_get(webview->webview_instance_)); + flutter::EncodableMap args; + args.insert(std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + webview->channel_->InvokeMethod( + "onPageStarted", std::make_unique(args)); +} + +void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + std::string url = std::string(ewk_view_url_get(webview->webview_instance_)); + flutter::EncodableMap args; + args.insert(std::make_pair( + flutter::EncodableValue("url"), flutter::EncodableValue(url))); + webview->channel_->InvokeMethod( + "onPageFinished", std::make_unique(args)); +} + +void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + Ewk_Error* error = static_cast(event_info); + flutter::EncodableMap args = { + {flutter::EncodableValue("errorCode"), + flutter::EncodableValue(ewk_error_code_get(error))}, + {flutter::EncodableValue("description"), + flutter::EncodableValue(ewk_error_description_get(error))}, + {flutter::EncodableValue("errorType"), + flutter::EncodableValue(ErrorCodeToString(ewk_error_code_get(error)))}, + {flutter::EncodableValue("failingUrl"), + flutter::EncodableValue(ewk_error_url_get(error))}, + }; + webview->channel_->InvokeMethod( + "onWebResourceError", std::make_unique(args)); +} + +void WebView::OnConsoleMessage(void* data, Evas_Object* obj, void* event_info) { + Ewk_Console_Message* message = static_cast(event_info); + LOG_INFO( + "console message:%s: %d: %d: %s", + EwkInternalApiBinding::GetInstance().console_message.SourceGet(message), + EwkInternalApiBinding::GetInstance().console_message.LineGet(message), + EwkInternalApiBinding::GetInstance().console_message.LevelGet(message), + EwkInternalApiBinding::GetInstance().console_message.TextGet(message)); +} + +void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, + void* event_info) { + WebView* webview = static_cast(data); + Ewk_Policy_Decision* policy_decision = + static_cast(event_info); + + const char* url = ewk_policy_decision_url_get(policy_decision); + if (!webview->has_navigation_delegate_) { + ewk_policy_decision_use(policy_decision); + return; + } + ewk_view_suspend(webview->webview_instance_); + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), flutter::EncodableValue(url)}, + {flutter::EncodableValue("isForMainFrame"), + flutter::EncodableValue(true)}, + }; + auto result = std::make_unique(webview); + webview->channel_->InvokeMethod( + "navigationRequest", std::make_unique(args), + std::move(result)); +} + +void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, + void* user_data) { + FlMethodResult* result = static_cast(user_data); + result->Success(flutter::EncodableValue(result_value)); + delete result; +} + +void WebView::OnJavaScriptMessage(Evas_Object* obj, + Ewk_Script_Message message) { + if (obj) { + WebView* webview = + static_cast(evas_object_data_get(obj, kEwkInstance)); + if (webview->channel_) { + std::string channel_name(message.name); + std::string channel_message(static_cast(message.body)); + + flutter::EncodableMap args = { + {flutter::EncodableValue("channel"), + flutter::EncodableValue(channel_name)}, + {flutter::EncodableValue("message"), + flutter::EncodableValue(channel_message)}, + }; + webview->channel_->InvokeMethod( + "javascriptChannelMessage", + std::make_unique(args)); + } + } +} diff --git a/packages/webview_flutter/tizen/src/webview.h b/packages/webview_flutter/tizen/src/webview.h index d78142d0a..a0a528a6a 100644 --- a/packages/webview_flutter/tizen/src/webview.h +++ b/packages/webview_flutter/tizen/src/webview.h @@ -2,9 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_PLUGIN_WEVIEW_H_ -#define FLUTTER_PLUGIN_WEVIEW_H_ +#ifndef FLUTTER_PLUGIN_WEBVIEW_H_ +#define FLUTTER_PLUGIN_WEBVIEW_H_ +#include +#include #include #include #include @@ -17,10 +19,6 @@ #include #include -namespace LWE { -class WebContainer; -} - class BufferPool; class BufferUnit; @@ -28,7 +26,7 @@ class WebView : public PlatformView { public: WebView(flutter::PluginRegistrar* registrar, int view_id, flutter::TextureRegistrar* texture_registrar, double width, - double height, const flutter::EncodableValue& params); + double height, const flutter::EncodableValue& params, void* win); ~WebView(); virtual void Dispose() override; @@ -44,7 +42,11 @@ class WebView : public PlatformView { uint32_t modifiers, uint32_t scan_code, bool is_down) override; - LWE::WebContainer* GetWebViewInstance() { return webview_instance_; } + void Resume(); + + void Stop(); + + Evas_Object* GetWebViewInstance() { return webview_instance_; } FlutterDesktopGpuSurfaceDescriptor* ObtainGpuSurface(size_t width, size_t height); @@ -63,21 +65,31 @@ class WebView : public PlatformView { void InitWebView(); - LWE::WebContainer* webview_instance_ = nullptr; + static void OnFrameRendered(void* data, Evas_Object* obj, void* event_info); + static void OnLoadStarted(void* data, Evas_Object* obj, void* event_info); + static void OnLoadFinished(void* data, Evas_Object* obj, void* event_info); + static void OnLoadError(void* data, Evas_Object* obj, void* event_info); + static void OnConsoleMessage(void* data, Evas_Object* obj, void* event_info); + static void OnNavigationPolicy(void* data, Evas_Object* obj, + void* event_info); + static void OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, + void* user_data); + static void OnJavaScriptMessage(Evas_Object* obj, Ewk_Script_Message message); + + Evas_Object* webview_instance_ = nullptr; + void* win_ = nullptr; flutter::TextureRegistrar* texture_registrar_; - double width_; - double height_; + double width_ = 0.0; + double height_ = 0.0; BufferUnit* working_surface_ = nullptr; BufferUnit* candidate_surface_ = nullptr; BufferUnit* rendered_surface_ = nullptr; - bool is_mouse_lbutton_down_ = false; bool has_navigation_delegate_ = false; bool has_progress_tracking_ = false; std::unique_ptr> channel_; std::unique_ptr texture_variant_; std::mutex mutex_; std::unique_ptr tbm_pool_; - bool use_sw_backend_; }; -#endif // FLUTTER_PLUGIN_WEVIEW_H_ +#endif // FLUTTER_PLUGIN_WEBVIEW_H_ diff --git a/packages/webview_flutter/tizen/src/webview_factory.cc b/packages/webview_flutter/tizen/src/webview_factory.cc index a7344d9aa..13fbec833 100644 --- a/packages/webview_flutter/tizen/src/webview_factory.cc +++ b/packages/webview_flutter/tizen/src/webview_factory.cc @@ -12,36 +12,17 @@ #include #include "log.h" -#include "lwe/LWEWebView.h" #include "webview.h" -static std::string GetAppDataPath() { - char* path = app_get_data_path(); - if (!path) { - return "/tmp/"; - } - std::string result = std::string(path); - free(path); - return result; -} - -WebViewFactory::WebViewFactory(flutter::PluginRegistrar* registrar) - : PlatformViewFactory(registrar) { +WebViewFactory::WebViewFactory(flutter::PluginRegistrar* registrar, void* win) + : PlatformViewFactory(registrar), win_(win) { texture_registrar_ = registrar->texture_registrar(); - - std::string data_path = GetAppDataPath(); - std::string local_storage_path = data_path + "StarFish_localStorage.db"; - std::string cookie_path = data_path + "StarFish_cookies.db"; - std::string cache_path = data_path + "Starfish_cache.db"; - - LWE::LWE::Initialize(local_storage_path.c_str(), cookie_path.c_str(), - cache_path.c_str()); } PlatformView* WebViewFactory::Create(int view_id, double width, double height, const ByteMessage& params) { return new WebView(GetPluginRegistrar(), view_id, texture_registrar_, width, - height, *GetCodec().DecodeMessage(params)); + height, *GetCodec().DecodeMessage(params), win_); } -void WebViewFactory::Dispose() { LWE::LWE::Finalize(); } +void WebViewFactory::Dispose() {} diff --git a/packages/webview_flutter/tizen/src/webview_factory.h b/packages/webview_flutter/tizen/src/webview_factory.h index fae47915f..ebb7c96d2 100644 --- a/packages/webview_flutter/tizen/src/webview_factory.h +++ b/packages/webview_flutter/tizen/src/webview_factory.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_PLUGIN_WEVIEW_FACTORY_H_ -#define FLUTTER_PLUGIN_WEVIEW_FACTORY_H_ +#ifndef FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ #include #include @@ -13,7 +13,7 @@ class WebViewFactory : public PlatformViewFactory { public: - WebViewFactory(flutter::PluginRegistrar* registrar); + WebViewFactory(flutter::PluginRegistrar* registrar, void* win); virtual PlatformView* Create(int view_id, double width, double height, const ByteMessage& params) override; @@ -22,6 +22,7 @@ class WebViewFactory : public PlatformViewFactory { private: flutter::TextureRegistrar* texture_registrar_; + void* win_ = nullptr; }; -#endif // FLUTTER_PLUGIN_WEVIEW_FACTORY_H_ +#endif // FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ diff --git a/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc b/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc index 66b6fd6e3..749f51a9a 100644 --- a/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc +++ b/packages/webview_flutter/tizen/src/webview_flutter_tizen_plugin.cc @@ -34,7 +34,11 @@ void WebviewFlutterTizenPluginRegisterWithRegistrar( flutter::PluginRegistrar* registrar = flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(core_registrar); + FlutterDesktopViewRef view = + FlutterDesktopPluginRegistrarGetView(core_registrar); FlutterDesktopRegisterViewFactory( - core_registrar, kViewType, std::make_unique(registrar)); + core_registrar, kViewType, + std::make_unique( + registrar, FlutterDesktopViewGetNativeHandle(view))); WebviewFlutterTizenPlugin::RegisterWithRegistrar(registrar); } diff --git a/packages/webview_flutter_lwe/.gitignore b/packages/webview_flutter_lwe/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/packages/webview_flutter_lwe/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/webview_flutter_lwe/CHANGELOG.md b/packages/webview_flutter_lwe/CHANGELOG.md new file mode 100644 index 000000000..1d30ecd7c --- /dev/null +++ b/packages/webview_flutter_lwe/CHANGELOG.md @@ -0,0 +1,4 @@ +## 0.1.0 + +* Initial release. +* Rename from webview_flutter_tizen to webview_flutter_lwe. \ No newline at end of file diff --git a/packages/webview_flutter_lwe/LICENSE b/packages/webview_flutter_lwe/LICENSE new file mode 100644 index 000000000..934dd180c --- /dev/null +++ b/packages/webview_flutter_lwe/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2021 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter_lwe/README.md b/packages/webview_flutter_lwe/README.md new file mode 100644 index 000000000..7b9a31cb6 --- /dev/null +++ b/packages/webview_flutter_lwe/README.md @@ -0,0 +1,51 @@ +# webview_flutter_lwe + +[![pub package](https://img.shields.io/pub/v/webview_flutter_lwe.svg)](https://pub.dev/packages/webview_flutter_lwe) + +The Tizen implementation of [`webview_flutter`](https://github.com/flutter/plugins/tree/main/packages/webview_flutter) backed by the Lightweight Web Engine (LWE). + +## Required privileges + +To use this plugin, add below lines under the `` section in your `tizen-manifest.xml` file. + +```xml + + http://tizen.org/privilege/internet + +``` + +## Usage + +This package is not an _endorsed_ implementation of `webview_flutter`. Therefore, you have to include `webview_flutter_lwe` alongside `webview_flutter` as dependencies in your `pubspec.yaml` file. + +```yaml +dependencies: + webview_flutter: ^3.0.4 + webview_flutter_lwe: ^0.1.0 +``` + +## Example + +```dart +import 'package:webview_flutter/webview_flutter.dart'; + +class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key}) : super(key: key); + + @override + WebViewExampleState createState() => WebViewExampleState(); +} + +class WebViewExampleState extends State { + @override + Widget build(BuildContext context) { + return WebView(initialUrl: 'https://flutter.dev'); + } +} +``` + +## Supported devices + +This plugin is supported on devices running Tizen 5.5 or later. + +For a detailed list of features supported by the Lightweight Web Engine, refer to [this page](https://git.tizen.org/cgit/platform/upstream/lightweight-web-engine/tree/docs/Spec.md?h=tizen). diff --git a/packages/webview_flutter_lwe/example/.gitignore b/packages/webview_flutter_lwe/example/.gitignore new file mode 100644 index 000000000..9d532b18a --- /dev/null +++ b/packages/webview_flutter_lwe/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_lwe/example/README.md b/packages/webview_flutter_lwe/example/README.md new file mode 100644 index 000000000..f86f4681b --- /dev/null +++ b/packages/webview_flutter_lwe/example/README.md @@ -0,0 +1,7 @@ +# webview_flutter_lwe_example + +Demonstrates how to use the webview_flutter_lwe plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). \ No newline at end of file diff --git a/packages/webview_flutter_lwe/example/assets/sample_audio.ogg b/packages/webview_flutter_lwe/example/assets/sample_audio.ogg new file mode 100644 index 000000000..27e171042 Binary files /dev/null and b/packages/webview_flutter_lwe/example/assets/sample_audio.ogg differ diff --git a/packages/webview_flutter_lwe/example/assets/sample_video.mp4 b/packages/webview_flutter_lwe/example/assets/sample_video.mp4 new file mode 100644 index 000000000..a203d0cdf Binary files /dev/null and b/packages/webview_flutter_lwe/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter_lwe/example/assets/www/index.html b/packages/webview_flutter_lwe/example/assets/www/index.html new file mode 100644 index 000000000..9895dd3ce --- /dev/null +++ b/packages/webview_flutter_lwe/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

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

+ + + \ No newline at end of file diff --git a/packages/webview_flutter_lwe/example/assets/www/styles/style.css b/packages/webview_flutter_lwe/example/assets/www/styles/style.css new file mode 100644 index 000000000..c2140b8b0 --- /dev/null +++ b/packages/webview_flutter_lwe/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000..73827ce66 --- /dev/null +++ b/packages/webview_flutter_lwe/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,640 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const bool _skipDueToIssue86757 = true; + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + await controller.runJavascript('Echo.postMessage("hello");'); + expect(messagesReceived, equals(['hello'])); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + + testWidgets('resize webview', (WidgetTester tester) async { + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } + }, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + const String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); + + group('NavigationDelegate', () { + const String blankPage = ''; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .runJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.runJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion(primaryUrl)); + }, + skip: _skipDueToIssue86757, + ); +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); +} + +Future _runJavascriptReturningResult( + WebViewController controller, String js) async { + return await controller.runJavascriptReturningResult(js); +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } +} diff --git a/packages/webview_flutter_lwe/example/lib/main.dart b/packages/webview_flutter_lwe/example/lib/main.dart new file mode 100644 index 000000000..a6298219c --- /dev/null +++ b/packages/webview_flutter_lwe/example/lib/main.dart @@ -0,0 +1,501 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +void main() => runApp(const MaterialApp(home: WebViewExample())); + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

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

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

Local demo page

+

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

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

Transparent background test

+
+
+ + +'''; + +class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key, this.cookieManager}) : super(key: key); + + final CookieManager? cookieManager; + + @override + State createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.green, + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + NavigationControls(_controller.future), + SampleMenu(_controller.future, widget.cookieManager), + ], + ), + body: WebView( + initialUrl: 'https://flutter.dev', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + onProgress: (int progress) { + print('WebView is loading (progress : $progress%)'); + }, + javascriptChannels: { + _toasterJavascriptChannel(context), + }, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, + onPageFinished: (String url) { + print('Page finished loading: $url'); + }, + gestureNavigationEnabled: true, + backgroundColor: const Color(0x00000000), + ), + floatingActionButton: favoriteButton(), + ); + } + + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + return FloatingActionButton( + onPressed: () async { + String? url; + if (controller.hasData) { + url = await controller.data!.currentUrl(); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + controller.hasData + ? 'Favorited $url' + : 'Unable to favorite', + ), + ), + ); + }, + child: const Icon(Icons.favorite), + ); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, + doPostRequest, + loadLocalFile, + loadFlutterAsset, + loadHtmlString, + transparentBackground, + setCookie, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller, CookieManager? cookieManager, {Key? key}) + : cookieManager = cookieManager ?? CookieManager(), + super(key: key); + + final Future controller; + late final CookieManager cookieManager; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + case MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; + case MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: MenuOptions.showUserAgent, + enabled: controller.hasData, + child: const Text('Show user agent'), + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), + ], + ); + }, + ); + } + + Future _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Toaster JavaScript channel we registered + // with the WebView. + await controller.runJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + } + + Future _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.runJavascriptReturningResult('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + Future _onAddToCache( + WebViewController controller, BuildContext context) async { + await controller.runJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + Future _onListCache( + WebViewController controller, BuildContext context) async { + await controller.runJavascript('caches.keys()' + // ignore: missing_whitespace_between_adjacent_strings + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Toaster.postMessage(caches))'); + } + + Future _onClearCache( + WebViewController controller, BuildContext context) async { + await controller.clearCache(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } + + Future _onClearCookies(BuildContext context) async { + final bool hadCookies = await cookieManager.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + Future _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await cookieManager.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + + Future _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + + Future _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalExamplePage); + } + + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File( + {tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); + + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); + + return indexFile.path; + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture, {Key? key}) + : assert(_webViewControllerFuture != null), + super(key: key); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoBack()) { + await controller.goBack(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoForward()) { + await controller.goForward(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No forward history item')), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller!.reload(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/packages/webview_flutter_lwe/example/pubspec.yaml b/packages/webview_flutter_lwe/example/pubspec.yaml new file mode 100644 index 000000000..779a88c58 --- /dev/null +++ b/packages/webview_flutter_lwe/example/pubspec.yaml @@ -0,0 +1,37 @@ +name: webview_flutter_lwe_example +description: Demonstrates how to use the webview_flutter_lwe plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + path_provider: ^2.0.7 + path_provider_tizen: + path: ../../path_provider/ + webview_flutter: ^3.0.4 + webview_flutter_lwe: + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css diff --git a/packages/webview_flutter_lwe/example/test_driver/integration_test.dart b/packages/webview_flutter_lwe/example/test_driver/integration_test.dart new file mode 100644 index 000000000..4f10f2a52 --- /dev/null +++ b/packages/webview_flutter_lwe/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter_lwe/example/tizen/.gitignore b/packages/webview_flutter_lwe/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/webview_flutter_lwe/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/webview_flutter_lwe/example/tizen/App.cs b/packages/webview_flutter_lwe/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/webview_flutter_lwe/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_lwe/example/tizen/Runner.csproj b/packages/webview_flutter_lwe/example/tizen/Runner.csproj new file mode 100644 index 000000000..f4e369d0c --- /dev/null +++ b/packages/webview_flutter_lwe/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/webview_flutter_lwe/example/tizen/shared/res/ic_launcher.png b/packages/webview_flutter_lwe/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/webview_flutter_lwe/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/webview_flutter_lwe/example/tizen/tizen-manifest.xml b/packages/webview_flutter_lwe/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..e14dc6c40 --- /dev/null +++ b/packages/webview_flutter_lwe/example/tizen/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + + http://tizen.org/privilege/internet + + + diff --git a/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart b/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart new file mode 100644 index 000000000..95858ec0b --- /dev/null +++ b/packages/webview_flutter_lwe/lib/webview_flutter_lwe.dart @@ -0,0 +1,94 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_tizen/widgets.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds a Tizen webview. +/// +/// This is used as the default implementation for [WebView.platform] on Tizen. It uses a method channel to +/// communicate with the platform code. +class LweWebView implements WebViewPlatform { + /// Sets a tizen [WebViewPlatform]. + static void register() { + WebView.platform = LweWebView(); + WebViewCookieManagerPlatform.instance = WebViewTizenCookieManager(); + } + + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + assert(webViewPlatformCallbacksHandler != null); + return GestureDetector( + onLongPress: () {}, + excludeFromSemantics: true, + child: TizenView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + )); + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + } + + @override + Future clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } +} + +/// Handles all cookie operations for the current platform. +class WebViewTizenCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + + @override + Future setCookie(WebViewCookie cookie) async { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + return MethodChannelWebViewPlatform.setCookie(cookie); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +} diff --git a/packages/webview_flutter_lwe/pubspec.yaml b/packages/webview_flutter_lwe/pubspec.yaml new file mode 100644 index 000000000..cb6c9f2b2 --- /dev/null +++ b/packages/webview_flutter_lwe/pubspec.yaml @@ -0,0 +1,28 @@ +name: webview_flutter_lwe +description: Tizen implementation of the webview plugin backed by Lightweight Web Engine. +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/webview_flutter_lwe +version: 0.1.0 + +environment: + sdk: ">=2.17.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: WebviewFlutterLwePlugin + fileName: webview_flutter_lwe_plugin.h + dartPluginClass: LweWebView + +dependencies: + flutter: + sdk: flutter + flutter_tizen: ^0.2.0 + webview_flutter: ^3.0.4 + webview_flutter_platform_interface: ^1.8.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/webview_flutter_lwe/tizen/.gitignore b/packages/webview_flutter_lwe/tizen/.gitignore new file mode 100644 index 000000000..a2a7d62b1 --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/webview_flutter/tizen/inc/lwe/LWEWebView.h b/packages/webview_flutter_lwe/tizen/inc/lwe/LWEWebView.h similarity index 100% rename from packages/webview_flutter/tizen/inc/lwe/LWEWebView.h rename to packages/webview_flutter_lwe/tizen/inc/lwe/LWEWebView.h diff --git a/packages/webview_flutter/tizen/inc/lwe/PlatformIntegrationData.h b/packages/webview_flutter_lwe/tizen/inc/lwe/PlatformIntegrationData.h similarity index 100% rename from packages/webview_flutter/tizen/inc/lwe/PlatformIntegrationData.h rename to packages/webview_flutter_lwe/tizen/inc/lwe/PlatformIntegrationData.h diff --git a/packages/google_sign_in/tizen/inc/google_sign_in_tizen_plugin.h b/packages/webview_flutter_lwe/tizen/inc/webview_flutter_lwe_plugin.h similarity index 59% rename from packages/google_sign_in/tizen/inc/google_sign_in_tizen_plugin.h rename to packages/webview_flutter_lwe/tizen/inc/webview_flutter_lwe_plugin.h index 36c1cca71..1a906cbb5 100644 --- a/packages/google_sign_in/tizen/inc/google_sign_in_tizen_plugin.h +++ b/packages/webview_flutter_lwe/tizen/inc/webview_flutter_lwe_plugin.h @@ -1,5 +1,5 @@ -#ifndef FLUTTER_PLUGIN_GOOGLE_SIGN_IN_TIZEN_PLUGIN_H_ -#define FLUTTER_PLUGIN_GOOGLE_SIGN_IN_TIZEN_PLUGIN_H_ +#ifndef FLUTTER_PLUGIN_WEBVIEW_FLUTTER_LWE_PLUGIN_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FLUTTER_LWE_PLUGIN_H_ #include @@ -13,11 +13,11 @@ extern "C" { #endif -FLUTTER_PLUGIN_EXPORT void GoogleSignInTizenPluginRegisterWithRegistrar( +FLUTTER_PLUGIN_EXPORT void WebviewFlutterLwePluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) } // extern "C" #endif -#endif // FLUTTER_PLUGIN_GOOGLE_SIGN_IN_TIZEN_PLUGIN_H_ +#endif // FLUTTER_PLUGIN_WEBVIEW_FLUTTER_LWE_PLUGIN_H_ diff --git a/packages/webview_flutter/tizen/lib/aarch64/liblightweight-web-engine.flutter.so b/packages/webview_flutter_lwe/tizen/lib/aarch64/liblightweight-web-engine.flutter.so similarity index 100% rename from packages/webview_flutter/tizen/lib/aarch64/liblightweight-web-engine.flutter.so rename to packages/webview_flutter_lwe/tizen/lib/aarch64/liblightweight-web-engine.flutter.so diff --git a/packages/webview_flutter/tizen/lib/aarch64/libtuv.so b/packages/webview_flutter_lwe/tizen/lib/aarch64/libtuv.so similarity index 100% rename from packages/webview_flutter/tizen/lib/aarch64/libtuv.so rename to packages/webview_flutter_lwe/tizen/lib/aarch64/libtuv.so diff --git a/packages/webview_flutter/tizen/lib/armel/liblightweight-web-engine.flutter.so b/packages/webview_flutter_lwe/tizen/lib/armel/liblightweight-web-engine.flutter.so similarity index 100% rename from packages/webview_flutter/tizen/lib/armel/liblightweight-web-engine.flutter.so rename to packages/webview_flutter_lwe/tizen/lib/armel/liblightweight-web-engine.flutter.so diff --git a/packages/webview_flutter/tizen/lib/armel/libtuv.so b/packages/webview_flutter_lwe/tizen/lib/armel/libtuv.so similarity index 100% rename from packages/webview_flutter/tizen/lib/armel/libtuv.so rename to packages/webview_flutter_lwe/tizen/lib/armel/libtuv.so diff --git a/packages/webview_flutter/tizen/lib/i586/liblightweight-web-engine.flutter.so b/packages/webview_flutter_lwe/tizen/lib/i586/liblightweight-web-engine.flutter.so similarity index 100% rename from packages/webview_flutter/tizen/lib/i586/liblightweight-web-engine.flutter.so rename to packages/webview_flutter_lwe/tizen/lib/i586/liblightweight-web-engine.flutter.so diff --git a/packages/webview_flutter/tizen/lib/i586/libtuv.so b/packages/webview_flutter_lwe/tizen/lib/i586/libtuv.so similarity index 100% rename from packages/webview_flutter/tizen/lib/i586/libtuv.so rename to packages/webview_flutter_lwe/tizen/lib/i586/libtuv.so diff --git a/packages/webview_flutter_lwe/tizen/project_def.prop b/packages/webview_flutter_lwe/tizen/project_def.prop new file mode 100644 index 000000000..0cbbd41ea --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/project_def.prop @@ -0,0 +1,29 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = webview_flutter_lwe_plugin +type = sharedLib +profile = common-5.5 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Compiler flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = + +# Linker options +USER_LIBS = pthread lightweight-web-engine.flutter tuv +USER_LIB_DIRS = lib/${BUILD_ARCH} +USER_LFLAGS = -Wl,-rpath='$$ORIGIN' diff --git a/packages/webview_flutter_lwe/tizen/src/buffer_pool.cc b/packages/webview_flutter_lwe/tizen/src/buffer_pool.cc new file mode 100644 index 000000000..7e3a2dd0e --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/buffer_pool.cc @@ -0,0 +1,134 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffer_pool.h" + +#include "log.h" + +BufferUnit::BufferUnit(int32_t width, int32_t height) { Reset(width, height); } + +BufferUnit::~BufferUnit() { + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + if (gpu_surface_) { + delete gpu_surface_; + gpu_surface_ = nullptr; + } +} + +bool BufferUnit::MarkInUse() { + if (!is_used_) { + is_used_ = true; + return true; + } + return false; +} + +void BufferUnit::UnmarkInUse() { is_used_ = false; } + +tbm_surface_h BufferUnit::Surface() { + if (IsUsed()) { + return tbm_surface_; + } + return nullptr; +} + +void BufferUnit::Reset(int32_t width, int32_t height) { + if (width_ == width && height_ == height) { + return; + } + width_ = width; + height_ = height; + + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + if (gpu_surface_) { + delete gpu_surface_; + gpu_surface_ = nullptr; + } + + tbm_surface_ = tbm_surface_create(width_, height_, TBM_FORMAT_ARGB8888); + gpu_surface_ = new FlutterDesktopGpuSurfaceDescriptor(); + gpu_surface_->width = width_; + gpu_surface_->height = height_; + gpu_surface_->handle = tbm_surface_; + gpu_surface_->release_callback = [](void* release_context) { + BufferUnit* buffer = reinterpret_cast(release_context); + buffer->UnmarkInUse(); + }; + gpu_surface_->release_context = this; +} + +BufferPool::BufferPool(int32_t width, int32_t height, size_t pool_size) { + for (size_t index = 0; index < pool_size; index++) { + pool_.emplace_back(std::make_unique(width, height)); + } + Prepare(width, height); +} + +BufferPool::~BufferPool() {} + +BufferUnit* BufferPool::GetAvailableBuffer() { + std::lock_guard lock(mutex_); + for (size_t index = 0; index < pool_.size(); index++) { + size_t current = (index + last_index_) % pool_.size(); + BufferUnit* buffer = pool_[current].get(); + if (buffer->MarkInUse()) { + last_index_ = current; + return buffer; + } + } + return nullptr; +} + +void BufferPool::Release(BufferUnit* buffer) { + std::lock_guard lock(mutex_); + buffer->UnmarkInUse(); +} + +void BufferPool::Prepare(int32_t width, int32_t height) { + std::lock_guard lock(mutex_); + for (size_t index = 0; index < pool_.size(); index++) { + BufferUnit* buffer = pool_[index].get(); + buffer->Reset(width, height); + } +} + +SingleBufferPool::SingleBufferPool(int32_t width, int32_t height) + : BufferPool(width, height, 1) {} + +SingleBufferPool::~SingleBufferPool() {} + +BufferUnit* SingleBufferPool::GetAvailableBuffer() { + BufferUnit* buffer = pool_[0].get(); + buffer->MarkInUse(); + return buffer; +} + +void SingleBufferPool::Release(BufferUnit* buffer) {} + +#ifndef NDEBUG +#include +void BufferUnit::DumpToPng(int file_name) { + char file_path[256]; + sprintf(file_path, "/tmp/dump%d.png", file_name); + + tbm_surface_info_s surface_info; + tbm_surface_map(tbm_surface_, TBM_SURF_OPTION_WRITE, &surface_info); + + unsigned char* buffer = surface_info.planes[0].ptr; + cairo_surface_t* png_buffer = cairo_image_surface_create_for_data( + buffer, CAIRO_FORMAT_ARGB32, width_, height_, + surface_info.planes[0].stride); + + cairo_surface_write_to_png(png_buffer, file_path); + + tbm_surface_unmap(tbm_surface_); + cairo_surface_destroy(png_buffer); +} +#endif diff --git a/packages/webview_flutter_lwe/tizen/src/buffer_pool.h b/packages/webview_flutter_lwe/tizen/src/buffer_pool.h new file mode 100644 index 000000000..e648a7fe9 --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/buffer_pool.h @@ -0,0 +1,71 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BUFFER_POOL_H_ +#define FLUTTER_PLUGIN_BUFFER_POOL_H_ + +#include +#include + +#include +#include +#include + +class BufferUnit { + public: + explicit BufferUnit(int32_t width, int32_t height); + ~BufferUnit(); + + void Reset(int32_t width, int32_t height); + + bool MarkInUse(); + void UnmarkInUse(); + + bool IsUsed() { return is_used_ && tbm_surface_; } + + tbm_surface_h Surface(); + + FlutterDesktopGpuSurfaceDescriptor* GpuSurface() { return gpu_surface_; } + +#ifndef NDEBUG + // TODO: Unused code. + void DumpToPng(int file_name); +#endif + + private: + bool is_used_ = false; + int32_t width_ = 0; + int32_t height_ = 0; + tbm_surface_h tbm_surface_ = nullptr; + FlutterDesktopGpuSurfaceDescriptor* gpu_surface_ = nullptr; +}; + +class BufferPool { + public: + explicit BufferPool(int32_t width, int32_t height, size_t pool_size); + virtual ~BufferPool(); + + virtual BufferUnit* GetAvailableBuffer(); + virtual void Release(BufferUnit* buffer); + + void Prepare(int32_t with, int32_t height); + + protected: + std::vector> pool_; + + private: + size_t last_index_ = 0; + std::mutex mutex_; +}; + +class SingleBufferPool : public BufferPool { + public: + explicit SingleBufferPool(int32_t width, int32_t height); + ~SingleBufferPool(); + + virtual BufferUnit* GetAvailableBuffer() override; + virtual void Release(BufferUnit* buffer) override; +}; + +#endif // FLUTTER_PLUGIN_BUFFER_POOL_H_ diff --git a/packages/webview_flutter_lwe/tizen/src/log.h b/packages/webview_flutter_lwe/tizen/src/log.h new file mode 100644 index 000000000..a0b770132 --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "WebviewFlutterLwePlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/webview_flutter_lwe/tizen/src/webview.cc b/packages/webview_flutter_lwe/tizen/src/webview.cc new file mode 100644 index 000000000..934defcc4 --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/webview.cc @@ -0,0 +1,881 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webview.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "buffer_pool.h" +#include "log.h" +#include "lwe/LWEWebView.h" +#include "lwe/PlatformIntegrationData.h" +#include "webview_factory.h" + +static constexpr size_t kBufferPoolSize = 5; + +extern "C" size_t LWE_EXPORT createWebViewInstance( + unsigned x, unsigned y, unsigned width, unsigned height, + float devicePixelRatio, const char* defaultFontName, const char* locale, + const char* timezoneID, + const std::function<::LWE::WebContainer::ExternalImageInfo(void)>& + prepareImageCb, + const std::function& flushCb, + bool useSWBackend); + +class NavigationRequestResult + : public flutter::MethodResult { + public: + NavigationRequestResult(std::string url, WebView* webview) + : url_(url), webview_(webview) {} + + void SuccessInternal(const flutter::EncodableValue* should_load) override { + if (std::holds_alternative(*should_load)) { + if (std::get(*should_load)) { + LoadUrl(); + } + } + } + + void ErrorInternal(const std::string& error_code, + const std::string& error_message, + const flutter::EncodableValue* error_details) override { + LOG_ERROR("The request unexpectedly completed with an error."); + } + + void NotImplementedInternal() override { + LOG_ERROR("The target method was unexpectedly unimplemented."); + } + + private: + void LoadUrl() { + if (webview_ && webview_->GetWebViewInstance()) { + webview_->GetWebViewInstance()->LoadURL(url_); + } + } + + std::string url_; + WebView* webview_; +}; + +enum class ResourceErrorType { + NoError, + UnknownError, + HostLookupError, + UnsupportedAuthSchemeError, + AuthenticationError, + ProxyAuthenticationError, + ConnectError, + IOError, + TimeoutError, + RedirectLoopError, + UnsupportedSchemeError, + FailedSSLHandshakeError, + BadURLError, + FileError, + FileNotFoundError, + TooManyRequestError, +}; + +static std::string ErrorCodeToString(int error_code) { + switch (ResourceErrorType(error_code)) { + case ResourceErrorType::AuthenticationError: + return "authentication"; + case ResourceErrorType::BadURLError: + return "badUrl"; + case ResourceErrorType::ConnectError: + return "connect"; + case ResourceErrorType::FailedSSLHandshakeError: + return "failedSslHandshake"; + case ResourceErrorType::FileError: + return "file"; + case ResourceErrorType::FileNotFoundError: + return "fileNotFound"; + case ResourceErrorType::HostLookupError: + return "hostLookup"; + case ResourceErrorType::IOError: + return "io"; + case ResourceErrorType::ProxyAuthenticationError: + return "proxyAuthentication"; + case ResourceErrorType::RedirectLoopError: + return "redirectLoop"; + case ResourceErrorType::TimeoutError: + return "timeout"; + case ResourceErrorType::TooManyRequestError: + return "tooManyRequests"; + case ResourceErrorType::UnknownError: + return "unknown"; + case ResourceErrorType::UnsupportedAuthSchemeError: + return "unsupportedAuthScheme"; + case ResourceErrorType::UnsupportedSchemeError: + return "unsupportedScheme"; + default: + LOG_ERROR("Unknown error type: %d", error_code); + return std::to_string(error_code); + } +} + +template +static bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, + std::string key, T* out) { + if (auto* map = std::get_if(arguments)) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto* value = std::get_if(&iter->second)) { + *out = *value; + return true; + } + } + } + return false; +} + +static bool IsRunningOnEmulator() { + bool result = false; + char* value = nullptr; + int ret = system_info_get_platform_string( + "http://tizen.org/system/model_name", &value); + if (ret == SYSTEM_INFO_ERROR_NONE && strcmp(value, "Emulator") == 0) { + result = true; + } + if (value) { + free(value); + } + return result; +} + +WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params) + : PlatformView(registrar, view_id, nullptr), + texture_registrar_(texture_registrar), + width_(width), + height_(height) { + use_sw_backend_ = IsRunningOnEmulator(); + if (use_sw_backend_) { + tbm_pool_ = std::make_unique(width, height); + } else { + tbm_pool_ = std::make_unique(width, height, kBufferPoolSize); + } + + texture_variant_ = + std::make_unique(flutter::GpuSurfaceTexture( + kFlutterDesktopGpuSurfaceTypeNone, + [this](size_t width, + size_t height) -> const FlutterDesktopGpuSurfaceDescriptor* { + return ObtainGpuSurface(width, height); + })); + SetTextureId(texture_registrar_->RegisterTexture(texture_variant_.get())); + + InitWebView(); + + channel_ = std::make_unique>( + GetPluginRegistrar()->messenger(), GetChannelName(), + &flutter::StandardMethodCodec::GetInstance()); + channel_->SetMethodCallHandler( + [webview = this](const auto& call, auto result) { + webview->HandleMethodCall(call, std::move(result)); + }); + + auto cookie_channel = + std::make_unique>( + GetPluginRegistrar()->messenger(), + "plugins.flutter.io/cookie_manager", + &flutter::StandardMethodCodec::GetInstance()); + cookie_channel->SetMethodCallHandler( + [webview = this](const auto& call, auto result) { + webview->HandleCookieMethodCall(call, std::move(result)); + }); + + std::string url; + if (!GetValueFromEncodableMap(¶ms, "initialUrl", &url)) { + url = "about:blank"; + } + + int color; + if (GetValueFromEncodableMap(¶ms, "backgroundColor", &color)) { + LWE::Settings settings = webview_instance_->GetSettings(); + settings.SetBaseBackgroundColor(color >> 16 & 0xff, color >> 8 & 0xff, + color & 0xff, color >> 24 & 0xff); + webview_instance_->SetSettings(settings); + } + + flutter::EncodableMap settings; + if (GetValueFromEncodableMap(¶ms, "settings", &settings)) { + ApplySettings(settings); + } + + flutter::EncodableList names; + if (GetValueFromEncodableMap(¶ms, "javascriptChannelNames", &names)) { + for (flutter::EncodableValue name : names) { + if (std::holds_alternative(name)) { + RegisterJavaScriptChannelName(std::get(name)); + } + } + } + + // TODO: Implement autoMediaPlaybackPolicy. + + std::string user_agent; + if (GetValueFromEncodableMap(¶ms, "userAgent", &user_agent)) { + LWE::Settings settings = webview_instance_->GetSettings(); + settings.SetUserAgentString(user_agent); + webview_instance_->SetSettings(settings); + } + + webview_instance_->RegisterOnPageStartedHandler( + [this](LWE::WebContainer* container, const std::string& url) { + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; + channel_->InvokeMethod("onPageStarted", + std::make_unique(args)); + }); + webview_instance_->RegisterOnPageLoadedHandler( + [this](LWE::WebContainer* container, const std::string& url) { + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; + channel_->InvokeMethod("onPageFinished", + std::make_unique(args)); + }); + webview_instance_->RegisterOnProgressChangedHandler( + [this](LWE::WebContainer* container, int progress) { + if (!has_progress_tracking_) { + return; + } + flutter::EncodableMap args = {{flutter::EncodableValue("progress"), + flutter::EncodableValue(progress)}}; + channel_->InvokeMethod("onProgress", + std::make_unique(args)); + }); + webview_instance_->RegisterOnReceivedErrorHandler( + [this](LWE::WebContainer* container, LWE::ResourceError error) { + flutter::EncodableMap args = { + {flutter::EncodableValue("errorCode"), + flutter::EncodableValue(error.GetErrorCode())}, + {flutter::EncodableValue("description"), + flutter::EncodableValue(error.GetDescription())}, + {flutter::EncodableValue("errorType"), + flutter::EncodableValue(ErrorCodeToString(error.GetErrorCode()))}, + {flutter::EncodableValue("failingUrl"), + flutter::EncodableValue(error.GetUrl())}, + }; + channel_->InvokeMethod("onWebResourceError", + std::make_unique(args)); + }); + webview_instance_->RegisterShouldOverrideUrlLoadingHandler( + [this](LWE::WebContainer* view, const std::string& url) -> bool { + if (!has_navigation_delegate_) { + return false; + } + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), flutter::EncodableValue(url)}, + {flutter::EncodableValue("isForMainFrame"), + flutter::EncodableValue(true)}, + }; + auto result = std::make_unique(url, this); + channel_->InvokeMethod("navigationRequest", + std::make_unique(args), + std::move(result)); + return true; + }); + + webview_instance_->LoadURL(url); +} + +void WebView::ApplySettings(const flutter::EncodableMap& settings) { + for (const auto& [key, value] : settings) { + if (std::holds_alternative(key)) { + std::string string_key = std::get(key); + if (string_key == "jsMode") { + // NOTE: Not supported by LWE on Tizen. + } else if (string_key == "hasNavigationDelegate") { + if (std::holds_alternative(value)) { + has_navigation_delegate_ = std::get(value); + } + } else if (string_key == "hasProgressTracking") { + if (std::holds_alternative(value)) { + has_progress_tracking_ = std::get(value); + } + } else if (string_key == "debuggingEnabled") { + // NOTE: Not supported by LWE on Tizen. + } else if (string_key == "gestureNavigationEnabled") { + // NOTE: Not supported by LWE on Tizen. + } else if (string_key == "allowsInlineMediaPlayback") { + // no-op inline media playback is always allowed on Tizen. + } else if (string_key == "userAgent") { + if (std::holds_alternative(value)) { + LWE::Settings settings = webview_instance_->GetSettings(); + settings.SetUserAgentString(std::get(value)); + webview_instance_->SetSettings(settings); + } + } else if (string_key == "zoomEnabled") { + // NOTE: Not supported by LWE on Tizen. + } else { + LOG_WARN("Unknown settings key: %s", string_key.c_str()); + } + } + } +} + +/** + * Added as a JavaScript interface to the WebView for any JavaScript channel + * that the Dart code sets up. + * + * Exposes a single method named `postMessage` to JavaScript, which sends a + * message over a method channel to the Dart code. + */ +void WebView::RegisterJavaScriptChannelName(const std::string& name) { + LOG_DEBUG("Register a JavaScript channel: %s", name.c_str()); + + auto on_message = [this, name](const std::string& message) -> std::string { + LOG_DEBUG("JavaScript channel message: %s", message.c_str()); + flutter::EncodableMap args = { + {flutter::EncodableValue("channel"), flutter::EncodableValue(name)}, + {flutter::EncodableValue("message"), flutter::EncodableValue(message)}, + }; + channel_->InvokeMethod("javascriptChannelMessage", + std::make_unique(args)); + return "success"; + }; + webview_instance_->AddJavaScriptInterface(name, "postMessage", on_message); +} + +WebView::~WebView() { Dispose(); } + +std::string WebView::GetChannelName() { + return "plugins.flutter.io/webview_" + std::to_string(GetViewId()); +} + +void WebView::Dispose() { + texture_registrar_->UnregisterTexture(GetTextureId()); + + if (webview_instance_) { + webview_instance_->Destroy(); + webview_instance_ = nullptr; + } +} + +void WebView::Resize(double width, double height) { + width_ = width; + height_ = height; + + if (candidate_surface_) { + candidate_surface_ = nullptr; + } + + tbm_pool_->Prepare(width_, height_); + webview_instance_->ResizeTo(width_, height_); +} + +void WebView::Touch(int type, int button, double x, double y, double dx, + double dy) { + if (type == 0) { // down event + webview_instance_->DispatchMouseDownEvent( + LWE::MouseButtonValue::LeftButton, + LWE::MouseButtonsValue::LeftButtonDown, x, y); + is_mouse_lbutton_down_ = true; + } else if (type == 1) { // move event + webview_instance_->DispatchMouseMoveEvent( + is_mouse_lbutton_down_ ? LWE::MouseButtonValue::LeftButton + : LWE::MouseButtonValue::NoButton, + is_mouse_lbutton_down_ ? LWE::MouseButtonsValue::LeftButtonDown + : LWE::MouseButtonsValue::NoButtonDown, + x, y); + } else if (type == 2) { // up event + webview_instance_->DispatchMouseUpEvent( + LWE::MouseButtonValue::NoButton, LWE::MouseButtonsValue::NoButtonDown, + x, y); + is_mouse_lbutton_down_ = false; + } else { + LOG_WARN("Unknown touch event type: %d", type); + } +} + +static LWE::KeyValue KeyToKeyValue(const std::string& key, + bool is_shift_pressed) { + if (key == "Left") { + return LWE::KeyValue::ArrowLeftKey; + } else if (key == "Right") { + return LWE::KeyValue::ArrowRightKey; + } else if (key == "Up") { + return LWE::KeyValue::ArrowUpKey; + } else if (key == "Down") { + return LWE::KeyValue::ArrowDownKey; + } else if (key == "space") { + return LWE::KeyValue::SpaceKey; + } else if (key == "Select") { + return LWE::KeyValue::EnterKey; + } else if (key == "Return") { + return LWE::KeyValue::EnterKey; + } else if (key == "Tab") { + return LWE::KeyValue::TabKey; + } else if (key == "BackSpace") { + return LWE::KeyValue::BackspaceKey; + } else if (key == "Escape") { + return LWE::KeyValue::EscapeKey; + } else if (key == "Delete") { + return LWE::KeyValue::DeleteKey; + } else if (key == "at") { + return LWE::KeyValue::AtMarkKey; + } else if (key == "minus") { + if (is_shift_pressed) { + return LWE::KeyValue::UnderScoreMarkKey; + } else { + return LWE::KeyValue::MinusMarkKey; + } + } else if (key == "equal") { + if (is_shift_pressed) { + return LWE::KeyValue::PlusMarkKey; + } else { + return LWE::KeyValue::EqualitySignKey; + } + } else if (key == "bracketleft") { + if (is_shift_pressed) { + return LWE::KeyValue::LeftCurlyBracketMarkKey; + } else { + return LWE::KeyValue::LeftSquareBracketKey; + } + } else if (key == "bracketright") { + if (is_shift_pressed) { + return LWE::KeyValue::RightCurlyBracketMarkKey; + } else { + return LWE::KeyValue::RightSquareBracketKey; + } + } else if (key == "semicolon") { + if (is_shift_pressed) { + return LWE::KeyValue::ColonMarkKey; + } else { + return LWE::KeyValue::SemiColonMarkKey; + } + } else if (key == "apostrophe") { + if (is_shift_pressed) { + return LWE::KeyValue::DoubleQuoteMarkKey; + } else { + return LWE::KeyValue::SingleQuoteMarkKey; + } + } else if (key == "comma") { + if (is_shift_pressed) { + return LWE::KeyValue::LessThanMarkKey; + } else { + return LWE::KeyValue::CommaMarkKey; + } + } else if (key == "period") { + if (is_shift_pressed) { + return LWE::KeyValue::GreaterThanSignKey; + } else { + return LWE::KeyValue::PeriodKey; + } + } else if (key == "slash") { + if (is_shift_pressed) { + return LWE::KeyValue::QuestionMarkKey; + } else { + return LWE::KeyValue::SlashKey; + } + } else if (key.length() == 1) { + const char ch = key.at(0); + if (ch >= '0' && ch <= '9') { + if (is_shift_pressed) { + switch (ch) { + case '1': + return LWE::KeyValue::ExclamationMarkKey; + case '2': + return LWE::KeyValue::AtMarkKey; + case '3': + return LWE::KeyValue::SharpMarkKey; + case '4': + return LWE::KeyValue::DollarMarkKey; + case '5': + return LWE::KeyValue::PercentMarkKey; + case '6': + return LWE::KeyValue::CaretMarkKey; + case '7': + return LWE::KeyValue::AmpersandMarkKey; + case '8': + return LWE::KeyValue::AsteriskMarkKey; + case '9': + return LWE::KeyValue::LeftParenthesisMarkKey; + case '0': + return LWE::KeyValue::RightParenthesisMarkKey; + } + } + return LWE::KeyValue(LWE::KeyValue::Digit0Key + ch - '0'); + } else if (ch >= 'a' && ch <= 'z') { + if (is_shift_pressed) { + return LWE::KeyValue(LWE::KeyValue::LowerAKey + ch - 'a' - 32); + } else { + return LWE::KeyValue(LWE::KeyValue::LowerAKey + ch - 'a'); + } + } else if (ch >= 'A' && ch <= 'Z') { + if (is_shift_pressed) { + return LWE::KeyValue(LWE::KeyValue::AKey + ch - 'A' + 32); + } else { + return LWE::KeyValue(LWE::KeyValue::AKey + ch - 'A'); + } + } + } else if (key == "XF86AudioRaiseVolume") { + return LWE::KeyValue::TVVolumeUpKey; + } else if (key == "XF86AudioLowerVolume") { + return LWE::KeyValue::TVVolumeDownKey; + } else if (key == "XF86AudioMute") { + return LWE::KeyValue::TVMuteKey; + } else if (key == "XF86RaiseChannel") { + return LWE::KeyValue::TVChannelUpKey; + } else if (key == "XF86LowerChannel") { + return LWE::KeyValue::TVChannelDownKey; + } else if (key == "XF86AudioRewind") { + return LWE::KeyValue::MediaTrackPreviousKey; + } else if (key == "XF86AudioNext") { + return LWE::KeyValue::MediaTrackNextKey; + } else if (key == "XF86AudioPause") { + return LWE::KeyValue::MediaPauseKey; + } else if (key == "XF86AudioRecord") { + return LWE::KeyValue::MediaRecordKey; + } else if (key == "XF86AudioPlay") { + return LWE::KeyValue::MediaPlayKey; + } else if (key == "XF86AudioStop") { + return LWE::KeyValue::MediaStopKey; + } else if (key == "XF86Info") { + return LWE::KeyValue::TVInfoKey; + } else if (key == "XF86Back") { + return LWE::KeyValue::TVReturnKey; + } else if (key == "XF86Red") { + return LWE::KeyValue::TVRedKey; + } else if (key == "XF86Green") { + return LWE::KeyValue::TVGreenKey; + } else if (key == "XF86Yellow") { + return LWE::KeyValue::TVYellowKey; + } else if (key == "XF86Blue") { + return LWE::KeyValue::TVBlueKey; + } else if (key == "XF86SysMenu") { + return LWE::KeyValue::TVMenuKey; + } else if (key == "XF86Home") { + return LWE::KeyValue::TVHomeKey; + } else if (key == "XF86Exit") { + return LWE::KeyValue::TVExitKey; + } else if (key == "XF86PreviousChannel") { + return LWE::KeyValue::TVPreviousChannel; + } else if (key == "XF86ChannelList") { + return LWE::KeyValue::TVChannelList; + } else if (key == "XF86ChannelGuide") { + return LWE::KeyValue::TVChannelGuide; + } else if (key == "XF86SimpleMenu") { + return LWE::KeyValue::TVSimpleMenu; + } else if (key == "XF86EManual") { + return LWE::KeyValue::TVEManual; + } else if (key == "XF86ExtraApp") { + return LWE::KeyValue::TVExtraApp; + } else if (key == "XF86Search") { + return LWE::KeyValue::TVSearch; + } else if (key == "XF86PictureSize") { + return LWE::KeyValue::TVPictureSize; + } else if (key == "XF86Sleep") { + return LWE::KeyValue::TVSleep; + } else if (key == "XF86Caption") { + return LWE::KeyValue::TVCaption; + } else if (key == "XF86More") { + return LWE::KeyValue::TVMore; + } else if (key == "XF86BTVoice") { + return LWE::KeyValue::TVBTVoice; + } else if (key == "XF86Color") { + return LWE::KeyValue::TVColor; + } else if (key == "XF86PlayBack") { + return LWE::KeyValue::TVPlayBack; + } + LOG_WARN("Unknown key name: %s", key.c_str()); + return LWE::KeyValue::UnidentifiedKey; +} + +bool WebView::SendKey(const char* key, const char* string, const char* compose, + uint32_t modifiers, uint32_t scan_code, bool is_down) { + if (!IsFocused()) { + return false; + } + + bool is_shift_pressed = modifiers & 1; + + struct Param { + LWE::WebContainer* webview_instance; + LWE::KeyValue key_value; + bool is_down; + }; + + Param* param = new Param(); + param->webview_instance = webview_instance_; + param->key_value = KeyToKeyValue(key, is_shift_pressed); + param->is_down = is_down; + + if (param->key_value == LWE::KeyValue::TVReturnKey && + webview_instance_->CanGoBack()) { + webview_instance_->GoBack(); + return true; + } + + webview_instance_->AddIdleCallback( + [](void* data) { + Param* param = reinterpret_cast(data); + if (param->is_down) { + param->webview_instance->DispatchKeyDownEvent(param->key_value); + param->webview_instance->DispatchKeyPressEvent(param->key_value); + } else { + param->webview_instance->DispatchKeyUpEvent(param->key_value); + } + delete param; + }, + param); + + return false; +} + +void WebView::SetDirection(int direction) { + // TODO: Implement if necessary. +} + +void WebView::InitWebView() { + if (webview_instance_) { + webview_instance_->Destroy(); + webview_instance_ = nullptr; + } + + float pixel_ratio = 1.0; + + auto on_prepare_image = [this]() -> LWE::WebContainer::ExternalImageInfo { + std::lock_guard lock(mutex_); + LWE::WebContainer::ExternalImageInfo result; + if (!working_surface_) { + working_surface_ = tbm_pool_->GetAvailableBuffer(); + } + if (working_surface_) { + result.imageAddress = working_surface_->Surface(); + } else { + result.imageAddress = nullptr; + } + return result; + }; + auto on_flush = [this](LWE::WebContainer* container, bool is_rendered) { + if (is_rendered) { + std::lock_guard lock(mutex_); + if (candidate_surface_) { + tbm_pool_->Release(candidate_surface_); + candidate_surface_ = nullptr; + } + candidate_surface_ = working_surface_; + working_surface_ = nullptr; + texture_registrar_->MarkTextureFrameAvailable(GetTextureId()); + } + }; + + webview_instance_ = + reinterpret_cast(createWebViewInstance( + 0, 0, width_, height_, pixel_ratio, "SamsungOneUI", "ko-KR", + "Asia/Seoul", on_prepare_image, on_flush, use_sw_backend_)); + +#ifndef TV_PROFILE + LWE::Settings settings = webview_instance_->GetSettings(); + settings.SetUserAgentString( + "Mozilla/5.0 (like Gecko/54.0 Firefox/54.0) Mobile"); + webview_instance_->SetSettings(settings); +#endif +} + +void WebView::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (!webview_instance_) { + result->Error("Invalid operation", + "The webview instance has not been initialized."); + return; + } + + const std::string& method_name = method_call.method_name(); + const flutter::EncodableValue* arguments = method_call.arguments(); + + LOG_DEBUG("Handle a method call: %s", method_name.c_str()); + + if (method_name == "loadUrl") { + std::string url; + if (GetValueFromEncodableMap(arguments, "url", &url)) { + webview_instance_->LoadURL(url); + result->Success(); + } else { + result->Error("Invalid argument", "No url provided."); + } + } else if (method_name == "updateSettings") { + const auto* settings = std::get_if(arguments); + if (settings) { + ApplySettings(*settings); + } + result->Success(); + } else if (method_name == "canGoBack") { + result->Success(flutter::EncodableValue(webview_instance_->CanGoBack())); + } else if (method_name == "canGoForward") { + result->Success(flutter::EncodableValue(webview_instance_->CanGoForward())); + } else if (method_name == "goBack") { + webview_instance_->GoBack(); + result->Success(); + } else if (method_name == "goForward") { + webview_instance_->GoForward(); + result->Success(); + } else if (method_name == "reload") { + webview_instance_->Reload(); + result->Success(); + } else if (method_name == "currentUrl") { + result->Success(flutter::EncodableValue(webview_instance_->GetURL())); + } else if (method_name == "evaluateJavascript" || + method_name == "runJavascriptReturningResult" || + method_name == "runJavascript") { + const auto* javascript = std::get_if(arguments); + if (javascript) { + bool should_return = method_name != "runJavascript"; + auto on_result = [result = result.release(), + should_return](std::string value) { + LOG_DEBUG("JavaScript evaluation result: %s", value.c_str()); + if (result) { + if (should_return) { + result->Success(flutter::EncodableValue(value)); + } else { + result->Success(); + } + delete result; + } + }; + webview_instance_->EvaluateJavaScript(*javascript, on_result); + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "addJavascriptChannels") { + const auto* channels = std::get_if(arguments); + if (channels) { + for (flutter::EncodableValue channel : *channels) { + if (std::holds_alternative(channel)) { + RegisterJavaScriptChannelName(std::get(channel)); + } + } + } + result->Success(); + } else if (method_name == "removeJavascriptChannels") { + const auto* channels = std::get_if(arguments); + if (channels) { + for (flutter::EncodableValue channel : *channels) { + if (std::holds_alternative(channel)) { + webview_instance_->RemoveJavascriptInterface( + std::get(channel), "postMessage"); + } + } + } + result->Success(); + } else if (method_name == "clearCache") { + webview_instance_->ClearCache(); + result->Success(); + } else if (method_name == "getTitle") { + result->Success(flutter::EncodableValue(webview_instance_->GetTitle())); + } else if (method_name == "scrollTo") { + int x = 0, y = 0; + if (GetValueFromEncodableMap(arguments, "x", &x) && + GetValueFromEncodableMap(arguments, "y", &y)) { + webview_instance_->ScrollTo(x, y); + result->Success(); + } else { + result->Error("Invalid argument", "No x or y provided."); + } + } else if (method_name == "scrollBy") { + int x = 0, y = 0; + if (GetValueFromEncodableMap(arguments, "x", &x) && + GetValueFromEncodableMap(arguments, "y", &y)) { + webview_instance_->ScrollBy(x, y); + result->Success(); + } else { + result->Error("Invalid argument", "No x or y provided."); + } + } else if (method_name == "getScrollX") { + result->Success(flutter::EncodableValue(webview_instance_->GetScrollX())); + } else if (method_name == "getScrollY") { + result->Success(flutter::EncodableValue(webview_instance_->GetScrollY())); + } else if (method_name == "loadFlutterAsset") { + const auto* key = std::get_if(arguments); + if (key) { + char* res_path = app_get_resource_path(); + if (res_path) { + std::string url = + std::string("file://") + res_path + "flutter_assets/" + *key; + free(res_path); + webview_instance_->LoadURL(url); + result->Success(); + } else { + result->Error("Operation failed", + "Could not get the flutter_assets path."); + } + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "loadHtmlString") { + std::string html, base_url; + if (!GetValueFromEncodableMap(arguments, "html", &html)) { + result->Error("Invalid argument", "No html provided."); + return; + } + if (GetValueFromEncodableMap(arguments, "baseUrl", &base_url)) { + LOG_WARN("loadHtmlString: baseUrl is not supported and will be ignored."); + } + webview_instance_->LoadData(html); + result->Success(); + } else if (method_name == "loadFile") { + const auto* file_path = std::get_if(arguments); + if (file_path) { + std::string url = std::string("file://") + *file_path; + webview_instance_->LoadURL(url); + result->Success(); + } else { + result->Error("Invalid argument", "The argument must be a string."); + } + } else if (method_name == "loadRequest") { + result->NotImplemented(); + } else if (method_name == "setCookie") { + result->NotImplemented(); + } else { + result->NotImplemented(); + } +} + +void WebView::HandleCookieMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (!webview_instance_) { + result->Error("Invalid operation", + "The webview instance has not been initialized."); + return; + } + + const std::string& method_name = method_call.method_name(); + + if (method_name == "clearCookies") { + LWE::CookieManager* cookie = LWE::CookieManager::GetInstance(); + cookie->ClearCookies(); + result->Success(flutter::EncodableValue(true)); + } else { + result->NotImplemented(); + } +} + +FlutterDesktopGpuSurfaceDescriptor* WebView::ObtainGpuSurface(size_t width, + size_t height) { + std::lock_guard lock(mutex_); + if (!candidate_surface_) { + if (rendered_surface_) { + return rendered_surface_->GpuSurface(); + } + return nullptr; + } + if (rendered_surface_ && rendered_surface_->IsUsed()) { + tbm_pool_->Release(rendered_surface_); + } + rendered_surface_ = candidate_surface_; + candidate_surface_ = nullptr; + return rendered_surface_->GpuSurface(); +} diff --git a/packages/webview_flutter_lwe/tizen/src/webview.h b/packages/webview_flutter_lwe/tizen/src/webview.h new file mode 100644 index 000000000..f8f8b21e9 --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/webview.h @@ -0,0 +1,83 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_WEBVIEW_H_ +#define FLUTTER_PLUGIN_WEBVIEW_H_ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace LWE { +class WebContainer; +} + +class BufferPool; +class BufferUnit; + +class WebView : public PlatformView { + public: + WebView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params); + ~WebView(); + + virtual void Dispose() override; + + virtual void Resize(double width, double height) override; + virtual void Touch(int type, int button, double x, double y, double dx, + double dy) override; + virtual void SetDirection(int direction) override; + + virtual void ClearFocus() override {} + + virtual bool SendKey(const char* key, const char* string, const char* compose, + uint32_t modifiers, uint32_t scan_code, + bool is_down) override; + + LWE::WebContainer* GetWebViewInstance() { return webview_instance_; } + + FlutterDesktopGpuSurfaceDescriptor* ObtainGpuSurface(size_t width, + size_t height); + + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + void HandleCookieMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + void ApplySettings(const flutter::EncodableMap& settings); + void RegisterJavaScriptChannelName(const std::string& name); + std::string GetChannelName(); + + void InitWebView(); + + LWE::WebContainer* webview_instance_ = nullptr; + flutter::TextureRegistrar* texture_registrar_; + double width_; + double height_; + BufferUnit* working_surface_ = nullptr; + BufferUnit* candidate_surface_ = nullptr; + BufferUnit* rendered_surface_ = nullptr; + bool is_mouse_lbutton_down_ = false; + bool has_navigation_delegate_ = false; + bool has_progress_tracking_ = false; + std::unique_ptr> channel_; + std::unique_ptr texture_variant_; + std::mutex mutex_; + std::unique_ptr tbm_pool_; + bool use_sw_backend_; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_H_ diff --git a/packages/webview_flutter_lwe/tizen/src/webview_factory.cc b/packages/webview_flutter_lwe/tizen/src/webview_factory.cc new file mode 100644 index 000000000..a7344d9aa --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/webview_factory.cc @@ -0,0 +1,47 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webview_factory.h" + +#include +#include +#include + +#include +#include + +#include "log.h" +#include "lwe/LWEWebView.h" +#include "webview.h" + +static std::string GetAppDataPath() { + char* path = app_get_data_path(); + if (!path) { + return "/tmp/"; + } + std::string result = std::string(path); + free(path); + return result; +} + +WebViewFactory::WebViewFactory(flutter::PluginRegistrar* registrar) + : PlatformViewFactory(registrar) { + texture_registrar_ = registrar->texture_registrar(); + + std::string data_path = GetAppDataPath(); + std::string local_storage_path = data_path + "StarFish_localStorage.db"; + std::string cookie_path = data_path + "StarFish_cookies.db"; + std::string cache_path = data_path + "Starfish_cache.db"; + + LWE::LWE::Initialize(local_storage_path.c_str(), cookie_path.c_str(), + cache_path.c_str()); +} + +PlatformView* WebViewFactory::Create(int view_id, double width, double height, + const ByteMessage& params) { + return new WebView(GetPluginRegistrar(), view_id, texture_registrar_, width, + height, *GetCodec().DecodeMessage(params)); +} + +void WebViewFactory::Dispose() { LWE::LWE::Finalize(); } diff --git a/packages/webview_flutter_lwe/tizen/src/webview_factory.h b/packages/webview_flutter_lwe/tizen/src/webview_factory.h new file mode 100644 index 000000000..0332b03dc --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/webview_factory.h @@ -0,0 +1,27 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ + +#include +#include +#include + +#include + +class WebViewFactory : public PlatformViewFactory { + public: + WebViewFactory(flutter::PluginRegistrar* registrar); + + virtual PlatformView* Create(int view_id, double width, double height, + const ByteMessage& params) override; + + virtual void Dispose() override; + + private: + flutter::TextureRegistrar* texture_registrar_; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ diff --git a/packages/webview_flutter_lwe/tizen/src/webview_flutter_lwe_plugin.cc b/packages/webview_flutter_lwe/tizen/src/webview_flutter_lwe_plugin.cc new file mode 100644 index 000000000..570b471c1 --- /dev/null +++ b/packages/webview_flutter_lwe/tizen/src/webview_flutter_lwe_plugin.cc @@ -0,0 +1,40 @@ +// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webview_flutter_lwe_plugin.h" + +#include +#include + +#include + +#include "webview_factory.h" + +namespace { + +constexpr char kViewType[] = "plugins.flutter.io/webview"; + +class WebviewFlutterLwePlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto plugin = std::make_unique(); + registrar->AddPlugin(std::move(plugin)); + } + + WebviewFlutterLwePlugin() {} + + virtual ~WebviewFlutterLwePlugin() {} +}; + +} // namespace + +void WebviewFlutterLwePluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef core_registrar) { + flutter::PluginRegistrar* registrar = + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(core_registrar); + FlutterDesktopRegisterViewFactory( + core_registrar, kViewType, std::make_unique(registrar)); + WebviewFlutterLwePlugin::RegisterWithRegistrar(registrar); +}