diff --git a/.github/labeler.yml b/.github/labeler.yml index 16f2d5dde..a57a165c3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -7,6 +7,8 @@ - packages/device_info_plus/**/* "p: firebase_core": - packages/firebase_core/**/* +"p: flutter_inappwebview": + - packages/flutter_inappwebview/**/* "p: flutter_secure_storage": - packages/flutter_secure_storage/**/* "p: flutter_tts": diff --git a/.github/recipe.yaml b/.github/recipe.yaml index 681deab2b..ef8bb5aa7 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -3,6 +3,7 @@ plugins: connectivity_plus: ["tv-9.0"] device_info_plus: ["tv-9.0"] flutter_secure_storage: ["tv-9.0"] + flutter_inappwebview: ["tv-9.0"] flutter_tts: ["tv-9.0"] integration_test: ["tv-9.0"] messageport: ["tv-9.0"] diff --git a/README.md b/README.md index 7462e3a35..1a3b4bb92 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**connectivity_plus_tizen**](packages/connectivity_plus) | [connectivity_plus](https://pub.dev/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://pub.dev/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 | | [**firebase_core_tizen**](packages/firebase_core) | [firebase_core](https://pub.dev/packages/firebase_core) | [![pub package](https://img.shields.io/pub/v/firebase_core_tizen.svg)](https://pub.dev/packages/firebase_core_tizen) | No | +| [**flutter_inappwebview_tizen**](packages/flutter_inappwebview_tizen) | [flutter_inappwebview](https://pub.dev/packages/flutter_inappwebview) | [![pub package](https://img.shields.io/pub/v/flutter_inappwebview_tizen.svg)](https://pub.dev/packages/flutter_inappwebview_tizen) | No | | [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | [flutter_secure_storage](https://pub.dev/packages/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://pub.dev/packages/flutter_tts) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_tts_tizen.svg)](https://pub.dev/packages/flutter_tts_tizen) | No | | [**flutter_webrtc_tizen**](packages/flutter_webrtc) | [flutter_webrtc](https://pub.dev/packages/flutter_webrtc) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_webrtc_tizen.svg)](https://pub.dev/packages/flutter_webrtc_tizen) | No | @@ -58,6 +59,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**connectivity_plus_tizen**](packages/connectivity_plus) | ✔️ | ✔️ | ✔️ | Returns incorrect connection status | | [**device_info_plus_tizen**](packages/device_info_plus) | ✔️ | ✔️ | ✔️ | | [**firebase_core**](packages/firebase_core) | ✔️ | ✔️ | ✔️ | +| [**flutter_inappwebview_tizen**](packages/flutter_inappwebview_tizen) | ✔️ | ✔️ | ✔️ | | [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | ✔️ | ✔️ | ✔️ | | [**flutter_tts_tizen**](packages/flutter_tts) | ✔️ | ✔️ | ✔️ | | [**flutter_webrtc_tizen**](packages/flutter_webrtc) | ✔️ | ❌ | ❌ | No camera | diff --git a/packages/flutter_inappwebview/.gitignore b/packages/flutter_inappwebview/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/packages/flutter_inappwebview/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/flutter_inappwebview/CHANGELOG.md b/packages/flutter_inappwebview/CHANGELOG.md new file mode 100644 index 000000000..d62edec68 --- /dev/null +++ b/packages/flutter_inappwebview/CHANGELOG.md @@ -0,0 +1,4 @@ +## 0.1.0 + +- Initial release of `flutter_inappwebview_tizen`. +- \ No newline at end of file diff --git a/packages/flutter_inappwebview/LICENSE b/packages/flutter_inappwebview/LICENSE new file mode 100644 index 000000000..f0c96aa2c --- /dev/null +++ b/packages/flutter_inappwebview/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2026 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 name of the copyright holder 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_inappwebview/README.md b/packages/flutter_inappwebview/README.md new file mode 100644 index 000000000..192a1284c --- /dev/null +++ b/packages/flutter_inappwebview/README.md @@ -0,0 +1,108 @@ +# flutter_inappwebview_tizen + +[![pub package](https://img.shields.io/pub/v/flutter_inappwebview_tizen.svg)](https://pub.dev/packages/flutter_inappwebview_tizen) + +The Tizen implementation of [`flutter_inappwebview`](https://pub.dev/packages/flutter_inappwebview). + +This package follows the same EWK-backed offscreen rendering approach as +[`webview_flutter_tizen`](https://pub.dev/packages/webview_flutter_tizen) and +maps it onto the `flutter_inappwebview` platform interface. Only the surface +that maps cleanly onto the Tizen WebView (chromium-efl) is implemented; every +other API raises `UnsupportedError` (or the `UnimplementedError` produced by +the `flutter_inappwebview_platform_interface` defaults). + +## Required privileges + +Add the internet privilege to the app manifest: + +```xml + + http://tizen.org/privilege/internet + +``` + +## Usage + +```yaml +dependencies: + flutter_inappwebview: ^6.1.5 + flutter_inappwebview_tizen: + path: ../flutter_inappwebview_tizen +``` + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class WebViewExample extends StatelessWidget { + const WebViewExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: InAppWebView( + initialUrlRequest: URLRequest(url: WebUri('https://flutter.dev')), + initialSettings: InAppWebViewSettings( + javaScriptEnabled: true, + useShouldOverrideUrlLoading: true, + ), + ), + ); + } +} +``` + +## Note + +- To play Youtube(video player), make app's background color to transparent. + +```diff +--- a/packages/flutter_inappwebview/example/lib/in_app_webiew_example.screen.dart ++++ b/packages/flutter_inappwebview/example/lib/in_app_webiew_example.screen.dart +@@ -105,14 +105,13 @@ class _InAppWebViewExampleScreenState extends State { + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("InAppWebView")), ++ backgroundColor: Colors.transparent, + drawer: myDrawer(context: context), +``` + +## Supported + +- `InAppWebView` platform view +- URL/file/data loading, back/forward/reload/stop, post requests +- JavaScript evaluation +- Navigation interception via `shouldOverrideUrlLoading` +- `onLoadStart`, `onLoadStop`, `onReceivedError`, `onProgressChanged`, + `onTitleChanged`, `onConsoleMessage`, `onScrollChanged`, + `onZoomScaleChanged`, `onUpdateVisitedHistory`, and JavaScript dialog + callbacks (`onJsAlert`, `onJsConfirm`, `onJsPrompt`) +- A subset of `InAppWebViewSettings`: `javaScriptEnabled`, `supportZoom`, + `userAgent`, `transparentBackground`, and `useShouldOverrideUrlLoading` +- `clearCache`, `clearAllCache`, `getDefaultUserAgent`, `handlesURLScheme` +- `CookieManager.deleteAllCookies()` + +`InAppWebViewController.getDefaultUserAgent()` returns the EWK user agent that +was captured the first time an `InAppWebView` was created in the process; the +value is cached so it remains available after every webview is disposed. +Calling it before any `InAppWebView` has been created returns an empty string. + +## Not supported + +The following capabilities are intentionally out of scope and raise +`UnsupportedError`/`UnimplementedError` when invoked: + +- `InAppBrowser`, `HeadlessInAppWebView`, `ChromeSafariBrowser` +- `InAppWebViewKeepAlive`, popup `windowId`, context menus, pull-to-refresh +- `FindInteractionController`, `PrintJobController`, `WebMessage*`, + `WebStorage*`, `WebViewEnvironment`, `HttpAuthCredentialDatabase` +- JavaScript handlers/channels and user script injection +- Per-cookie mutation/query APIs (`setCookie`, `getCookie(s)`, + `deleteCookie(s)`) +- Resource interception, custom-scheme loading, downloads, permission and + geolocation prompts, render-process events, fullscreen, capture state, + content-size, and other callbacks not listed above +- Multi-step `goBackOrForward`/`canGoBackOrForward`/`goTo`, `isLoading`, + `getOriginalUrl`, `getSettings`, `reloadFromOrigin`, `pause`/`resume` +- DevTools, simulated requests, screenshot/PDF/web-archive capture, JS/CSS + injection, service worker / proxy / tracing / path-handler controllers diff --git a/packages/flutter_inappwebview/analysis_options.yaml b/packages/flutter_inappwebview/analysis_options.yaml new file mode 100644 index 000000000..516160a40 --- /dev/null +++ b/packages/flutter_inappwebview/analysis_options.yaml @@ -0,0 +1,7 @@ +include: ../../analysis_options.yaml + +analyzer: + language: + strict-casts: false + strict-inference: false + strict-raw-types: false diff --git a/packages/flutter_inappwebview/example/.gitignore b/packages/flutter_inappwebview/example/.gitignore new file mode 100644 index 000000000..9d532b18a --- /dev/null +++ b/packages/flutter_inappwebview/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/flutter_inappwebview/example/README.md b/packages/flutter_inappwebview/example/README.md new file mode 100644 index 000000000..efb03c0fb --- /dev/null +++ b/packages/flutter_inappwebview/example/README.md @@ -0,0 +1,7 @@ +# flutter_inappwebview_tizen_example + +Demonstrates how to use the flutter_inappwebview_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_inappwebview/example/analysis_options.yaml b/packages/flutter_inappwebview/example/analysis_options.yaml new file mode 100644 index 000000000..b7464d806 --- /dev/null +++ b/packages/flutter_inappwebview/example/analysis_options.yaml @@ -0,0 +1,19 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# Mirrors the upstream `flutter_inappwebview/example` configuration so the +# Tizen example can stay close to its upstream counterpart. + +include: package:flutter_lints/flutter.yaml + +linter: + rules: + constant_identifier_names: ignore + deprecated_member_use_from_same_package: ignore + +analyzer: + errors: + deprecated_member_use: ignore + deprecated_member_use_from_same_package: ignore + unnecessary_cast: ignore + unnecessary_import: ignore diff --git a/packages/flutter_inappwebview/example/assets/www/index.html b/packages/flutter_inappwebview/example/assets/www/index.html new file mode 100644 index 000000000..9895dd3ce --- /dev/null +++ b/packages/flutter_inappwebview/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/flutter_inappwebview/example/assets/www/styles/style.css b/packages/flutter_inappwebview/example/assets/www/styles/style.css new file mode 100644 index 000000000..c2140b8b0 --- /dev/null +++ b/packages/flutter_inappwebview/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart new file mode 100644 index 000000000..58232c234 --- /dev/null +++ b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart @@ -0,0 +1,402 @@ +// Copyright 2026 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:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +const String _fixtureHtml = ''' + + + + + Fixture title + + + +
+

Fixture Page

+
+ + +'''; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late HttpServer server; + late String firstUrl; + late String secondUrl; + late String blockedUrl; + + setUpAll(() async { + server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + unawaited( + server.forEach((HttpRequest request) { + request.response.headers.contentType = ContentType.html; + switch (request.uri.path) { + case '/first': + request.response.write(_htmlPage('First page')); + case '/second': + request.response.write(_htmlPage('Second page')); + case '/blocked': + request.response.write(_htmlPage('Blocked page')); + case '/favicon.ico': + request.response.statusCode = HttpStatus.notFound; + default: + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }), + ); + final String baseUrl = 'http://${server.address.address}:${server.port}'; + firstUrl = '$baseUrl/first'; + secondUrl = '$baseUrl/second'; + blockedUrl = '$baseUrl/blocked'; + }); + + tearDownAll(() => server.close(force: true)); + + testWidgets('load callbacks and navigation controls', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + final StreamController progressValues = + StreamController.broadcast(); + final StreamController consoleMessages = + StreamController.broadcast(); + final StreamController navigationActions = + StreamController.broadcast(); + final StreamController visitedUrls = + StreamController.broadcast(); + addTearDown(loadStops.close); + addTearDown(progressValues.close); + addTearDown(consoleMessages.close); + addTearDown(navigationActions.close); + addTearDown(visitedUrls.close); + + final Future firstLoad = _waitForValue(loadStops.stream, firstUrl); + final Future initialProgress = progressValues.stream + .firstWhere((int progress) => progress == 100) + .timeout(const Duration(seconds: 10)); + final Future consoleMessage = _waitForValue( + consoleMessages.stream, + 'First page ready', + ); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + initialSettings: InAppWebViewSettings(useShouldOverrideUrlLoading: true), + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + onProgressChanged: (_, int progress) { + progressValues.add(progress); + }, + onConsoleMessage: (_, ConsoleMessage message) { + consoleMessages.add(message.message); + }, + onUpdateVisitedHistory: (_, WebUri? url, __) { + if (url != null) { + visitedUrls.add(url.toString()); + } + }, + shouldOverrideUrlLoading: (_, NavigationAction action) async { + final String? url = action.request.url?.toString(); + if (url != null) { + navigationActions.add(url); + } + return url == blockedUrl + ? NavigationActionPolicy.CANCEL + : NavigationActionPolicy.ALLOW; + }, + ); + + expect(await firstLoad, firstUrl); + expect(await initialProgress, 100); + expect(await consoleMessage, 'First page ready'); + expect((await controller.getUrl()).toString(), firstUrl); + expect(await controller.getTitle(), 'First page'); + + final Future secondNavigation = _waitForValue( + navigationActions.stream, + secondUrl, + ); + final Future secondLoad = _waitForValue( + loadStops.stream, + secondUrl, + ); + final Future secondVisited = _waitForValue( + visitedUrls.stream, + secondUrl, + ); + + await controller.evaluateJavascript( + source: "document.getElementById('second-link').click();", + ); + + expect(await secondNavigation, secondUrl); + expect(await secondLoad, secondUrl); + expect(await secondVisited, secondUrl); + expect(await controller.getTitle(), 'Second page'); + expect(await controller.canGoBack(), isTrue); + + final Future firstReloaded = _waitForValue( + loadStops.stream, + firstUrl, + ); + await controller.goBack(); + expect(await firstReloaded, firstUrl); + expect((await controller.getUrl()).toString(), firstUrl); + + final Future blockedNavigation = _waitForValue( + navigationActions.stream, + blockedUrl, + ); + final Future blockedLoad = _waitForValue( + loadStops.stream, + blockedUrl, + timeout: const Duration(seconds: 1), + ); + await controller.evaluateJavascript( + source: "document.getElementById('blocked-link').click();", + ); + expect(await blockedNavigation, blockedUrl); + await expectLater(blockedLoad, throwsA(isA())); + expect((await controller.getUrl()).toString(), firstUrl); + }); + + testWidgets('loadData and evaluate JavaScript', (WidgetTester tester) async { + final InAppWebViewController controller = await _pumpWebView(tester); + + await _loadFixture(controller); + + expect(await InAppWebViewController.getDefaultUserAgent(), isNotEmpty); + expect(await InAppWebViewController.handlesURLScheme('https'), isTrue); + expect( + await InAppWebViewController.handlesURLScheme('custom-scheme'), + isFalse, + ); + expect(await controller.getTitle(), 'Fixture title'); + expect( + await controller.evaluateJavascript( + source: "document.querySelector('h1').textContent", + ), + 'Fixture Page', + ); + }); + + testWidgets('JavaScript dialog callbacks control responses', ( + WidgetTester tester, + ) async { + final Completer alertMessage = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onJsAlert: (_, JsAlertRequest request) async { + alertMessage.complete(request.message); + return JsAlertResponse(handledByClient: true); + }, + onJsConfirm: (_, __) async { + return JsConfirmResponse(handledByClient: true); + }, + onJsPrompt: (_, __) async { + return JsPromptResponse( + action: JsPromptResponseAction.CONFIRM, + handledByClient: true, + value: 'from callback', + ); + }, + ); + + await _loadFixture(controller); + + expect( + await controller.evaluateJavascript( + source: "alert('alert message'); 'alert complete';", + ), + 'alert complete', + ); + expect( + await alertMessage.future.timeout(const Duration(seconds: 10)), + 'alert message', + ); + expect( + await controller.evaluateJavascript(source: "confirm('confirm?')"), + isFalse, + ); + expect( + await controller.evaluateJavascript(source: "prompt('prompt?', 'base')"), + 'from callback', + ); + }); + + testWidgets('deleteAllCookies clears the cookie store', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + final Object? cookieBefore = await controller.evaluateJavascript( + source: ''' +document.cookie = 'tizen_inappwebview=1; path=/'; +document.cookie; +''', + ); + expect(cookieBefore.toString(), contains('tizen_inappwebview=1')); + + expect(await CookieManager.instance().deleteAllCookies(), isTrue); + + final Future reloaded = _waitForValue(loadStops.stream, firstUrl); + await controller.reload(); + expect(await reloaded, firstUrl); + final Object? cookieAfter = await controller.evaluateJavascript( + source: 'document.cookie', + ); + expect(cookieAfter.toString(), isNot(contains('tizen_inappwebview=1'))); + }); +} + +Future _pumpWebView( + WidgetTester tester, { + String initialUrl = 'about:blank', + InAppWebViewSettings? initialSettings, + void Function(InAppWebViewController, WebUri?)? onLoadStop, + void Function(InAppWebViewController, int)? onProgressChanged, + void Function(InAppWebViewController, ConsoleMessage)? onConsoleMessage, + void Function(InAppWebViewController, WebUri?, bool?)? onUpdateVisitedHistory, + Future Function(InAppWebViewController, JsAlertRequest)? + onJsAlert, + Future Function(InAppWebViewController, JsConfirmRequest)? + onJsConfirm, + Future Function(InAppWebViewController, JsPromptRequest)? + onJsPrompt, + Future Function( + InAppWebViewController, + NavigationAction, + )? + shouldOverrideUrlLoading, +}) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox.expand( + child: InAppWebView( + initialSettings: initialSettings, + initialUrlRequest: URLRequest(url: WebUri(initialUrl)), + onWebViewCreated: controllerCompleter.complete, + onLoadStop: onLoadStop, + onProgressChanged: onProgressChanged, + onConsoleMessage: onConsoleMessage, + onUpdateVisitedHistory: onUpdateVisitedHistory, + onJsAlert: onJsAlert, + onJsConfirm: onJsConfirm, + onJsPrompt: onJsPrompt, + shouldOverrideUrlLoading: shouldOverrideUrlLoading, + ), + ), + ), + ), + ); + + addTearDown(() async { + await tester.pumpWidget(const SizedBox.shrink()); + }); + + return controllerCompleter.future.timeout(const Duration(seconds: 10)); +} + +Future _loadFixture(InAppWebViewController controller) async { + await controller.loadData(data: _fixtureHtml); + await _waitForFixtureDocument( + controller, + ).timeout(const Duration(seconds: 10)); +} + +Future _waitForFixtureDocument(InAppWebViewController controller) async { + Object? lastResult; + final DateTime end = DateTime.now().add(const Duration(seconds: 10)); + + while (DateTime.now().isBefore(end)) { + try { + final Object? title = await controller.evaluateJavascript( + source: 'document.title', + ); + final Object? heading = await controller.evaluateJavascript( + source: "document.querySelector('h1')?.textContent", + ); + if (title?.toString() == 'Fixture title' && + heading?.toString() == 'Fixture Page') { + return; + } + lastResult = 'title=$title heading=$heading'; + } catch (error) { + lastResult = error; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + + throw TimeoutException( + 'Fixture page did not become ready. Last result: $lastResult', + ); +} + +Future _waitForValue( + Stream stream, + T value, { + Duration timeout = const Duration(seconds: 10), +}) { + return stream.firstWhere((T event) => event == value).timeout(timeout); +} + +String _htmlPage(String title) { + return ''' + + + + + $title + + +

$title

+ Second + Blocked + + + +'''; +} diff --git a/packages/flutter_inappwebview/example/lib/chrome_safari_browser_example.screen.dart b/packages/flutter_inappwebview/example/lib/chrome_safari_browser_example.screen.dart new file mode 100644 index 000000000..8fd6ae659 --- /dev/null +++ b/packages/flutter_inappwebview/example/lib/chrome_safari_browser_example.screen.dart @@ -0,0 +1,132 @@ +// Copyright 2023 Lorenzo Pichilli. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// Imported from https://pub.dev/packages/flutter_inappwebview. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'main.dart'; + +class MyChromeSafariBrowser extends ChromeSafariBrowser { + @override + void onOpened() async { + print("ChromeSafari browser opened"); + } + + @override + void onCompletedInitialLoad(didLoadSuccessfully) { + print("ChromeSafari browser initial load completed"); + } + + @override + void onClosed() { + print("ChromeSafari browser closed"); + } +} + +class ChromeSafariBrowserExampleScreen extends StatefulWidget { + final ChromeSafariBrowser browser = MyChromeSafariBrowser(); + + @override + _ChromeSafariBrowserExampleScreenState createState() => + _ChromeSafariBrowserExampleScreenState(); +} + +class _ChromeSafariBrowserExampleScreenState + extends State { + @override + void initState() { + rootBundle.load('assets/images/flutter-logo.png').then((actionButtonIcon) { + if (defaultTargetPlatform == TargetPlatform.android) { + widget.browser.setActionButton( + ChromeSafariBrowserActionButton( + id: 1, + description: 'Action Button description', + icon: actionButtonIcon.buffer.asUint8List(), + onClick: (url, title) { + print('Action Button 1 clicked!'); + print(url); + print(title); + }, + ), + ); + } + }); + + widget.browser.addMenuItem( + ChromeSafariBrowserMenuItem( + id: 2, + label: 'Custom item menu 1', + image: UIImage(systemName: "sun.max"), + onClick: (url, title) { + print('Custom item menu 1 clicked!'); + print(url); + print(title); + }, + ), + ); + widget.browser.addMenuItem( + ChromeSafariBrowserMenuItem( + id: 3, + label: 'Custom item menu 2', + image: UIImage(systemName: "pencil"), + onClick: (url, title) { + print('Custom item menu 2 clicked!'); + print(url); + print(title); + }, + ), + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("ChromeSafariBrowser")), + drawer: myDrawer(context: context), + body: Center( + child: ElevatedButton( + onPressed: () async { + await widget.browser.open( + url: WebUri("https://flutter.dev/"), + settings: ChromeSafariBrowserSettings( + shareState: CustomTabsShareState.SHARE_STATE_OFF, + isSingleInstance: false, + isTrustedWebActivity: false, + keepAliveEnabled: true, + startAnimations: [ + AndroidResource.anim( + name: "slide_in_left", + defPackage: "android", + ), + AndroidResource.anim( + name: "slide_out_right", + defPackage: "android", + ), + ], + exitAnimations: [ + AndroidResource.anim( + name: "abc_slide_in_top", + defPackage: + "com.pichillilorenzo.flutter_inappwebviewexample", + ), + AndroidResource.anim( + name: "abc_slide_out_top", + defPackage: + "com.pichillilorenzo.flutter_inappwebviewexample", + ), + ], + dismissButtonStyle: DismissButtonStyle.CLOSE, + presentationStyle: ModalPresentationStyle.OVER_FULL_SCREEN, + ), + ); + }, + child: Text("Open Chrome Safari Browser"), + ), + ), + ); + } +} diff --git a/packages/flutter_inappwebview/example/lib/headless_in_app_webview.screen.dart b/packages/flutter_inappwebview/example/lib/headless_in_app_webview.screen.dart new file mode 100644 index 000000000..177c73674 --- /dev/null +++ b/packages/flutter_inappwebview/example/lib/headless_in_app_webview.screen.dart @@ -0,0 +1,126 @@ +// Copyright 2023 Lorenzo Pichilli. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// Imported from https://pub.dev/packages/flutter_inappwebview. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'main.dart'; + +class HeadlessInAppWebViewExampleScreen extends StatefulWidget { + @override + _HeadlessInAppWebViewExampleScreenState createState() => + _HeadlessInAppWebViewExampleScreenState(); +} + +class _HeadlessInAppWebViewExampleScreenState + extends State { + HeadlessInAppWebView? headlessWebView; + String url = ""; + + @override + void initState() { + super.initState(); + + var url = !kIsWeb + ? WebUri("https://flutter.dev") + : WebUri("http://localhost:${Uri.base.port}/page.html"); + + headlessWebView = HeadlessInAppWebView( + webViewEnvironment: webViewEnvironment, + initialUrlRequest: URLRequest(url: url), + initialSettings: InAppWebViewSettings(isInspectable: kDebugMode), + onWebViewCreated: (controller) { + print('HeadlessInAppWebView created!'); + }, + onConsoleMessage: (controller, consoleMessage) { + print("CONSOLE MESSAGE: " + consoleMessage.message); + }, + onLoadStart: (controller, url) async { + setState(() { + this.url = url.toString(); + }); + }, + onLoadStop: (controller, url) async { + setState(() { + this.url = url.toString(); + }); + }, + onUpdateVisitedHistory: (controller, url, isReload) { + setState(() { + this.url = url.toString(); + }); + }, + ); + } + + @override + void dispose() { + super.dispose(); + headlessWebView?.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("HeadlessInAppWebView")), + drawer: myDrawer(context: context), + body: SafeArea( + child: Column( + children: [ + Container( + padding: EdgeInsets.all(20.0), + child: Text( + "CURRENT URL\n${(url.length > 50) ? url.substring(0, 50) + "..." : url}", + ), + ), + Center( + child: ElevatedButton( + onPressed: () async { + await headlessWebView?.dispose(); + await headlessWebView?.run(); + }, + child: Text("Run HeadlessInAppWebView"), + ), + ), + Container(height: 10), + Center( + child: ElevatedButton( + onPressed: () async { + if (headlessWebView?.isRunning() ?? false) { + await headlessWebView?.webViewController + ?.evaluateJavascript( + source: """console.log('Here is the message!');""", + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'HeadlessInAppWebView is not running. Click on "Run HeadlessInAppWebView"!', + ), + ), + ); + } + }, + child: Text("Send console.log message"), + ), + ), + Container(height: 10), + Center( + child: ElevatedButton( + onPressed: () { + headlessWebView?.dispose(); + setState(() { + this.url = ""; + }); + }, + child: Text("Dispose HeadlessInAppWebView"), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter_inappwebview/example/lib/in_app_browser_example.screen.dart b/packages/flutter_inappwebview/example/lib/in_app_browser_example.screen.dart new file mode 100644 index 000000000..1f9d4c54a --- /dev/null +++ b/packages/flutter_inappwebview/example/lib/in_app_browser_example.screen.dart @@ -0,0 +1,159 @@ +// Copyright 2023 Lorenzo Pichilli. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// Imported from https://pub.dev/packages/flutter_inappwebview. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'main.dart'; + +class MyInAppBrowser extends InAppBrowser { + MyInAppBrowser({ + int? windowId, + UnmodifiableListView? initialUserScripts, + PullToRefreshController? pullToRefreshController, + }) : super( + windowId: windowId, + initialUserScripts: initialUserScripts, + pullToRefreshController: pullToRefreshController, + webViewEnvironment: webViewEnvironment, + ); + + @override + Future onBrowserCreated() async { + print("\n\nBrowser Created!\n\n"); + } + + @override + Future onLoadStart(url) async {} + + @override + Future onLoadStop(url) async { + pullToRefreshController?.endRefreshing(); + } + + @override + Future onPermissionRequest(request) async { + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.GRANT, + ); + } + + @override + void onLoadError(url, code, message) { + pullToRefreshController?.endRefreshing(); + } + + @override + void onProgressChanged(progress) { + if (progress == 100) { + pullToRefreshController?.endRefreshing(); + } + } + + @override + void onExit() { + print("\n\nBrowser closed!\n\n"); + } + + @override + Future shouldOverrideUrlLoading( + navigationAction, + ) async { + print("\n\nOverride ${navigationAction.request.url}\n\n"); + return NavigationActionPolicy.ALLOW; + } + + void onMainWindowWillClose() { + close(); + } +} + +class InAppBrowserExampleScreen extends StatefulWidget { + @override + _InAppBrowserExampleScreenState createState() => + _InAppBrowserExampleScreenState(); +} + +class _InAppBrowserExampleScreenState extends State { + late final MyInAppBrowser browser; + + @override + void initState() { + super.initState(); + + PullToRefreshController? pullToRefreshController = + kIsWeb || + ![ + TargetPlatform.iOS, + TargetPlatform.android, + ].contains(defaultTargetPlatform) + ? null + : PullToRefreshController( + settings: PullToRefreshSettings(color: Colors.black), + onRefresh: () async { + if (Platform.isAndroid) { + browser.webViewController?.reload(); + } else if (Platform.isIOS) { + browser.webViewController?.loadUrl( + urlRequest: URLRequest( + url: await browser.webViewController?.getUrl(), + ), + ); + } + }, + ); + + browser = MyInAppBrowser(pullToRefreshController: pullToRefreshController); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("InAppBrowser")), + drawer: myDrawer(context: context), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + await browser.openUrlRequest( + urlRequest: URLRequest(url: WebUri("https://flutter.dev")), + settings: InAppBrowserClassSettings( + browserSettings: InAppBrowserSettings( + toolbarTopBackgroundColor: Colors.blue, + presentationStyle: ModalPresentationStyle.POPOVER, + ), + webViewSettings: InAppWebViewSettings( + isInspectable: kDebugMode, + useShouldOverrideUrlLoading: true, + useOnLoadResource: true, + ), + ), + ); + }, + child: Text("Open In-App Browser"), + ), + Container(height: 40), + ElevatedButton( + onPressed: () async { + await InAppBrowser.openWithSystemBrowser( + url: WebUri("https://flutter.dev/"), + ); + }, + child: Text("Open System Browser"), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter_inappwebview/example/lib/in_app_webiew_example.screen.dart b/packages/flutter_inappwebview/example/lib/in_app_webiew_example.screen.dart new file mode 100644 index 000000000..b34751d82 --- /dev/null +++ b/packages/flutter_inappwebview/example/lib/in_app_webiew_example.screen.dart @@ -0,0 +1,260 @@ +// Copyright 2023 Lorenzo Pichilli. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// Imported from https://pub.dev/packages/flutter_inappwebview. + +import 'dart:collection'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +// TIZENFIX: Detect Tizen so unsupported features can be skipped at build time. +import 'package:flutter_tizen/flutter_tizen.dart' as flutter_tizen; +import 'package:url_launcher/url_launcher.dart'; + +import 'main.dart'; + +class InAppWebViewExampleScreen extends StatefulWidget { + @override + _InAppWebViewExampleScreenState createState() => + _InAppWebViewExampleScreenState(); +} + +class _InAppWebViewExampleScreenState extends State { + final GlobalKey webViewKey = GlobalKey(); + + InAppWebViewController? webViewController; + InAppWebViewSettings settings = InAppWebViewSettings( + isInspectable: kDebugMode, + mediaPlaybackRequiresUserGesture: false, + allowsInlineMediaPlayback: true, + iframeAllow: "camera; microphone", + iframeAllowFullscreen: true, + ); + + PullToRefreshController? pullToRefreshController; + + late ContextMenu contextMenu; + String url = ""; + double progress = 0; + final urlController = TextEditingController(); + + @override + void initState() { + super.initState(); + + contextMenu = ContextMenu( + menuItems: [ + ContextMenuItem( + id: 1, + title: "Special", + action: () async { + print("Menu item Special clicked!"); + print(await webViewController?.getSelectedText()); + await webViewController?.clearFocus(); + }, + ), + ], + settings: ContextMenuSettings(hideDefaultSystemContextMenuItems: false), + onCreateContextMenu: (hitTestResult) async { + print("onCreateContextMenu"); + print(hitTestResult.extra); + print(await webViewController?.getSelectedText()); + }, + onHideContextMenu: () { + print("onHideContextMenu"); + }, + onContextMenuActionItemClicked: (contextMenuItemClicked) async { + var id = contextMenuItemClicked.id; + print( + "onContextMenuActionItemClicked: " + + id.toString() + + " " + + contextMenuItemClicked.title, + ); + }, + ); + + pullToRefreshController = + kIsWeb || + ![ + TargetPlatform.iOS, + TargetPlatform.android, + ].contains(defaultTargetPlatform) + ? null + : PullToRefreshController( + settings: PullToRefreshSettings(color: Colors.blue), + onRefresh: () async { + if (defaultTargetPlatform == TargetPlatform.android) { + webViewController?.reload(); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + webViewController?.loadUrl( + urlRequest: URLRequest( + url: await webViewController?.getUrl(), + ), + ); + } + }, + ); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("InAppWebView")), + backgroundColor: Colors + .transparent, // for tizen - entire app transparent for WebView video buffer + drawer: myDrawer(context: context), + body: SafeArea( + child: Column( + children: [ + ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, // for tizen + child: TextField( + decoration: InputDecoration(prefixIcon: Icon(Icons.search)), + controller: urlController, + keyboardType: TextInputType.text, + onSubmitted: (value) { + var url = WebUri(value); + if (url.scheme.isEmpty) { + url = WebUri( + (!kIsWeb + ? "https://www.google.com/search?q=" + : "https://www.bing.com/search?q=") + + value, + ); + } + webViewController?.loadUrl(urlRequest: URLRequest(url: url)); + }, + ), + ), + Expanded( + child: Stack( + children: [ + InAppWebView( + key: webViewKey, + webViewEnvironment: webViewEnvironment, + initialUrlRequest: URLRequest( + url: WebUri('https://flutter.dev'), + ), + // initialUrlRequest: + // URLRequest(url: WebUri(Uri.base.toString().replaceFirst("/#/", "/") + 'page.html')), + // initialFile: "assets/index.html", + initialUserScripts: UnmodifiableListView([]), + initialSettings: settings, + // TIZENFIX: ContextMenu is not implemented on Tizen and + // the widget would throw on construction. Pass null on Tizen. + contextMenu: flutter_tizen.isTizen ? null : contextMenu, + pullToRefreshController: pullToRefreshController, + onWebViewCreated: (controller) async { + webViewController = controller; + }, + onLoadStart: (controller, url) async { + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + // TIZENFIX: onPermissionRequest is not implemented on Tizen + // and is rejected by the widget. Skip the callback there. + onPermissionRequest: flutter_tizen.isTizen + ? null + : (controller, request) async { + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.GRANT, + ); + }, + shouldOverrideUrlLoading: + (controller, navigationAction) async { + var uri = navigationAction.request.url!; + + if (![ + "http", + "https", + "file", + "chrome", + "data", + "javascript", + "about", + ].contains(uri.scheme)) { + if (await canLaunchUrl(uri)) { + // Launch the App + await launchUrl(uri); + // and cancel the request + return NavigationActionPolicy.CANCEL; + } + } + + return NavigationActionPolicy.ALLOW; + }, + onLoadStop: (controller, url) async { + pullToRefreshController?.endRefreshing(); + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + onReceivedError: (controller, request, error) { + pullToRefreshController?.endRefreshing(); + }, + onProgressChanged: (controller, progress) { + if (progress == 100) { + pullToRefreshController?.endRefreshing(); + } + setState(() { + this.progress = progress / 100; + urlController.text = this.url; + }); + }, + onUpdateVisitedHistory: (controller, url, isReload) { + setState(() { + this.url = url.toString(); + urlController.text = this.url; + }); + }, + onConsoleMessage: (controller, consoleMessage) { + print(consoleMessage); + }, + ), + progress < 1.0 + ? LinearProgressIndicator(value: progress) + : Container(), + ], + ), + ), + ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, // for tizen + child: ButtonBar( + alignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + child: Icon(Icons.arrow_back), + onPressed: () { + webViewController?.goBack(); + }, + ), + ElevatedButton( + child: Icon(Icons.arrow_forward), + onPressed: () { + webViewController?.goForward(); + }, + ), + ElevatedButton( + child: Icon(Icons.refresh), + onPressed: () { + webViewController?.reload(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter_inappwebview/example/lib/main.dart b/packages/flutter_inappwebview/example/lib/main.dart new file mode 100644 index 000000000..40c2c833a --- /dev/null +++ b/packages/flutter_inappwebview/example/lib/main.dart @@ -0,0 +1,246 @@ +// Copyright 2023 Lorenzo Pichilli. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// Imported from https://pub.dev/packages/flutter_inappwebview. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +// TIZENFIX: Detect Tizen so the drawer/routes can hide unsupported screens. +import 'package:flutter_tizen/flutter_tizen.dart' as flutter_tizen; + +import 'package:flutter_inappwebview_tizen_example/chrome_safari_browser_example.screen.dart'; +import 'package:flutter_inappwebview_tizen_example/headless_in_app_webview.screen.dart'; +import 'package:flutter_inappwebview_tizen_example/in_app_webiew_example.screen.dart'; +import 'package:flutter_inappwebview_tizen_example/in_app_browser_example.screen.dart'; +import 'package:flutter_inappwebview_tizen_example/web_authentication_session_example.screen.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; + +// import 'package:path_provider/path_provider.dart'; +// import 'package:permission_handler/permission_handler.dart'; + +// TIZENFIX: InAppLocalhostServer is not implemented on Tizen. The reference is +// kept (matching upstream) but it is null so no native call is made. +final localhostServer = !flutter_tizen.isTizen + ? InAppLocalhostServer(documentRoot: 'assets') + : null; +WebViewEnvironment? webViewEnvironment; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + // await Permission.camera.request(); + // await Permission.microphone.request(); + // await Permission.storage.request(); + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) { + final availableVersion = await WebViewEnvironment.getAvailableVersion(); + assert( + availableVersion != null, + 'Failed to find an installed WebView2 runtime or non-stable Microsoft Edge installation.', + ); + + webViewEnvironment = await WebViewEnvironment.create( + settings: WebViewEnvironmentSettings(userDataFolder: 'custom_path'), + ); + } + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode); + } + + runApp(MyApp()); +} + +PointerInterceptor myDrawer({required BuildContext context}) { + var children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ListTile( + title: Text('InAppBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/InAppBrowser'); + }, + ), + ListTile( + title: Text('ChromeSafariBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/ChromeSafariBrowser'); + }, + ), + ListTile( + title: Text('WebAuthenticationSession'), + onTap: () { + Navigator.pushReplacementNamed(context, '/WebAuthenticationSession'); + }, + ), + ListTile( + title: Text('HeadlessInAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); + }, + ), + ]; + if (kIsWeb) { + children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ]; + } else if (defaultTargetPlatform == TargetPlatform.macOS) { + children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ListTile( + title: Text('InAppBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/InAppBrowser'); + }, + ), + ListTile( + title: Text('WebAuthenticationSession'), + onTap: () { + Navigator.pushReplacementNamed(context, '/WebAuthenticationSession'); + }, + ), + ListTile( + title: Text('HeadlessInAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); + }, + ), + ]; + } else if (defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.linux) { + children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ListTile( + title: Text('InAppBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/InAppBrowser'); + }, + ), + ListTile( + title: Text('HeadlessInAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); + }, + ), + ]; + } + // TIZENFIX: Tizen reports as TargetPlatform.linux but only InAppWebView is + // supported. Replace the drawer entries with a Tizen-only list so that + // navigating to one of the unsupported screens cannot crash the example. + if (flutter_tizen.isTizen) { + children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ]; + } + return PointerInterceptor( + child: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + child: Text('flutter_inappwebview example'), + decoration: BoxDecoration(color: Colors.blue), + ), + ...children, + ], + ), + ), + ); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (kIsWeb) { + return MaterialApp( + initialRoute: '/', + routes: {'/': (context) => InAppWebViewExampleScreen()}, + ); + } + // TIZENFIX: Only register the InAppWebView route on Tizen. The other + // routes from the upstream sample point at unsupported features that + // would crash on instantiation if the user navigated to them. + if (flutter_tizen.isTizen) { + return MaterialApp( + initialRoute: '/', + routes: {'/': (context) => InAppWebViewExampleScreen()}, + ); + } + if (defaultTargetPlatform == TargetPlatform.macOS) { + return MaterialApp( + initialRoute: '/', + routes: { + '/': (context) => InAppWebViewExampleScreen(), + '/InAppBrowser': (context) => InAppBrowserExampleScreen(), + '/HeadlessInAppWebView': (context) => + HeadlessInAppWebViewExampleScreen(), + '/WebAuthenticationSession': (context) => + WebAuthenticationSessionExampleScreen(), + }, + ); + } else if (defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.linux) { + return MaterialApp( + initialRoute: '/', + routes: { + '/': (context) => InAppWebViewExampleScreen(), + '/InAppBrowser': (context) => InAppBrowserExampleScreen(), + '/HeadlessInAppWebView': (context) => + HeadlessInAppWebViewExampleScreen(), + }, + ); + } + return MaterialApp( + initialRoute: '/', + routes: { + '/': (context) => InAppWebViewExampleScreen(), + '/InAppBrowser': (context) => InAppBrowserExampleScreen(), + '/ChromeSafariBrowser': (context) => ChromeSafariBrowserExampleScreen(), + '/HeadlessInAppWebView': (context) => + HeadlessInAppWebViewExampleScreen(), + '/WebAuthenticationSession': (context) => + WebAuthenticationSessionExampleScreen(), + }, + ); + } +} diff --git a/packages/flutter_inappwebview/example/lib/web_authentication_session_example.screen.dart b/packages/flutter_inappwebview/example/lib/web_authentication_session_example.screen.dart new file mode 100644 index 000000000..0fd30af63 --- /dev/null +++ b/packages/flutter_inappwebview/example/lib/web_authentication_session_example.screen.dart @@ -0,0 +1,125 @@ +// Copyright 2023 Lorenzo Pichilli. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// Imported from https://pub.dev/packages/flutter_inappwebview. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'main.dart'; + +class WebAuthenticationSessionExampleScreen extends StatefulWidget { + @override + _WebAuthenticationSessionExampleScreenState createState() => + _WebAuthenticationSessionExampleScreenState(); +} + +class _WebAuthenticationSessionExampleScreenState + extends State { + WebAuthenticationSession? session; + String? token; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + session?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("WebAuthenticationSession")), + drawer: myDrawer(context: context), + body: SafeArea( + child: Column( + children: [ + Center( + child: Container( + padding: EdgeInsets.all(20.0), + child: Text("Token: $token"), + ), + ), + session != null + ? Container() + : Center( + child: ElevatedButton( + onPressed: () async { + if (session == null && + !kIsWeb && + [ + TargetPlatform.iOS, + TargetPlatform.macOS, + ].contains(defaultTargetPlatform) && + await WebAuthenticationSession.isAvailable()) { + session = await WebAuthenticationSession.create( + url: WebUri("http://localhost:8080/web-auth.html"), + callbackURLScheme: "test", + onComplete: (url, error) async { + if (url != null) { + setState(() { + token = url.queryParameters["token"]; + }); + } + }, + ); + setState(() {}); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Cannot create Web Authentication Session!', + ), + ), + ); + } + }, + child: Text("Create Web Auth Session"), + ), + ), + session == null + ? Container() + : Center( + child: ElevatedButton( + onPressed: () async { + var started = false; + if (await session?.canStart() ?? false) { + started = await session?.start() ?? false; + } + if (!started) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Cannot start Web Authentication Session!', + ), + ), + ); + } + }, + child: Text("Start Web Auth Session"), + ), + ), + session == null + ? Container() + : Center( + child: ElevatedButton( + onPressed: () async { + await session?.dispose(); + setState(() { + token = null; + session = null; + }); + }, + child: Text("Dispose Web Auth Session"), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter_inappwebview/example/pubspec.yaml b/packages/flutter_inappwebview/example/pubspec.yaml new file mode 100644 index 000000000..ccb0a802b --- /dev/null +++ b/packages/flutter_inappwebview/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: flutter_inappwebview_tizen_example +description: Demonstrates how to use the flutter_inappwebview_tizen plugin. +publish_to: "none" + +environment: + sdk: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" + +dependencies: + flutter: + sdk: flutter + flutter_inappwebview: ^6.1.5 + flutter_inappwebview_tizen: + path: ../ + flutter_tizen: ^0.2.1 + pointer_interceptor: ^0.10.1+2 + url_launcher: ^6.3.0 + url_launcher_tizen: + path: ../../url_launcher + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_lints: ^4.0.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + +flutter: + uses-material-design: true diff --git a/packages/flutter_inappwebview/example/test_driver/integration_test.dart b/packages/flutter_inappwebview/example/test_driver/integration_test.dart new file mode 100644 index 000000000..4f10f2a52 --- /dev/null +++ b/packages/flutter_inappwebview/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/flutter_inappwebview/example/tizen/.gitignore b/packages/flutter_inappwebview/example/tizen/.gitignore new file mode 100644 index 000000000..ac0738be2 --- /dev/null +++ b/packages/flutter_inappwebview/example/tizen/.gitignore @@ -0,0 +1,12 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ + +# Tizen Core CLI (tz) related files +tizen_dotnet_project.yaml +*.csproj.backup + +# Flutter-tizen dependency information file +.app.deps.json diff --git a/packages/flutter_inappwebview/example/tizen/App.cs b/packages/flutter_inappwebview/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/flutter_inappwebview/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_inappwebview/example/tizen/Runner.csproj b/packages/flutter_inappwebview/example/tizen/Runner.csproj new file mode 100644 index 000000000..da93051cc --- /dev/null +++ b/packages/flutter_inappwebview/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen80 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/flutter_inappwebview/example/tizen/shared/res/ic_launcher.png b/packages/flutter_inappwebview/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/flutter_inappwebview/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/flutter_inappwebview/example/tizen/tizen-manifest.xml b/packages/flutter_inappwebview/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..4ca042d28 --- /dev/null +++ b/packages/flutter_inappwebview/example/tizen/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + + http://tizen.org/privilege/internet + + + diff --git a/packages/flutter_inappwebview/lib/flutter_inappwebview_tizen.dart b/packages/flutter_inappwebview/lib/flutter_inappwebview_tizen.dart new file mode 100644 index 000000000..690d01e66 --- /dev/null +++ b/packages/flutter_inappwebview/lib/flutter_inappwebview_tizen.dart @@ -0,0 +1,5 @@ +// Copyright 2026 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. + +export 'src/main.dart'; diff --git a/packages/flutter_inappwebview/lib/src/cookie_manager.dart b/packages/flutter_inappwebview/lib/src/cookie_manager.dart new file mode 100644 index 000000000..2d8aad6cc --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/cookie_manager.dart @@ -0,0 +1,147 @@ +// Copyright 2026 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/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [TizenCookieManager]. +@immutable +class TizenCookieManagerCreationParams + extends PlatformCookieManagerCreationParams { + /// Creates a new [TizenCookieManagerCreationParams] instance. + const TizenCookieManagerCreationParams(); + + /// Creates a [TizenCookieManagerCreationParams] from a generic + /// [PlatformCookieManagerCreationParams]. + factory TizenCookieManagerCreationParams.fromPlatformCookieManagerCreationParams( + // ignore: avoid_unused_constructor_parameters + PlatformCookieManagerCreationParams params, + ) { + return const TizenCookieManagerCreationParams(); + } +} + +/// Tizen implementation of [PlatformCookieManager]. +/// +/// Only [deleteAllCookies] is supported on Tizen. Per-cookie mutation and +/// query APIs throw [UnsupportedError]. +class TizenCookieManager extends PlatformCookieManager with ChannelController { + /// Creates a new [TizenCookieManager]. + TizenCookieManager(PlatformCookieManagerCreationParams params) + : super.implementation( + params is TizenCookieManagerCreationParams + ? params + : TizenCookieManagerCreationParams.fromPlatformCookieManagerCreationParams( + params, + ), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_cookiemanager', + ); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async {} + + static TizenCookieManager? _instance; + + /// Returns the lazily-constructed singleton. + static TizenCookieManager instance() { + return _instance ??= TizenCookieManager( + const TizenCookieManagerCreationParams(), + ); + } + + Never _unsupported(String method) { + throw UnsupportedError( + '$method is not implemented on flutter_inappwebview_tizen.', + ); + } + + @override + Future setCookie({ + required WebUri url, + required String name, + required String value, + String path = '/', + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + bool? isHttpOnly, + HTTPCookieSameSitePolicy? sameSite, + @Deprecated('Use webViewController instead') + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController, + }) async { + _unsupported('setCookie'); + } + + @override + Future> getCookies({ + required WebUri url, + @Deprecated('Use webViewController instead') + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController, + }) async { + _unsupported('getCookies'); + } + + @override + Future getCookie({ + required WebUri url, + required String name, + @Deprecated('Use webViewController instead') + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController, + }) async { + _unsupported('getCookie'); + } + + @override + Future deleteCookie({ + required WebUri url, + required String name, + String path = '/', + String? domain, + @Deprecated('Use webViewController instead') + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController, + }) async { + _unsupported('deleteCookie'); + } + + @override + Future deleteCookies({ + required WebUri url, + String path = '/', + String? domain, + @Deprecated('Use webViewController instead') + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController, + }) async { + _unsupported('deleteCookies'); + } + + @override + Future deleteAllCookies() async { + final bool result = + await channel?.invokeMethod('deleteAllCookies') ?? false; + return result; + } + + @override + void dispose() { + disposeChannel(); + } +} + +/// Internal hook that exposes [_handleMethod] without making it part of the +/// public surface. +extension InternalCookieManager on TizenCookieManager { + /// Returns the channel handler bound to [_handleMethod]. + Future Function(MethodCall call) get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview/lib/src/in_app_webview/_static_channel.dart b/packages/flutter_inappwebview/lib/src/in_app_webview/_static_channel.dart new file mode 100644 index 000000000..64d0d7bc3 --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/in_app_webview/_static_channel.dart @@ -0,0 +1,10 @@ +// Copyright 2026 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'; + +/// Process-wide method channel used by static InAppWebView APIs. +const MethodChannel IN_APP_WEBVIEW_STATIC_CHANNEL = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_manager', +); diff --git a/packages/flutter_inappwebview/lib/src/in_app_webview/in_app_webview.dart b/packages/flutter_inappwebview/lib/src/in_app_webview/in_app_webview.dart new file mode 100644 index 000000000..b437ec318 --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/in_app_webview/in_app_webview.dart @@ -0,0 +1,563 @@ +// Copyright 2026 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/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'package:flutter_tizen/widgets.dart'; + +import 'in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [PlatformInAppWebViewWidget]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +class TizenInAppWebViewWidgetCreationParams + extends PlatformInAppWebViewWidgetCreationParams { + /// Creates a new [TizenInAppWebViewWidgetCreationParams] instance. + TizenInAppWebViewWidgetCreationParams({ + super.controllerFromPlatform, + super.key, + super.layoutDirection, + super.gestureRecognizers, + super.headlessWebView, + super.keepAlive, + super.preventGestureDelay, + super.windowId, + super.webViewEnvironment, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated('Use onReceivedHttpError instead') super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated('Use onPrintRequest instead') super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead', + ) + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + super.pullToRefreshController, + super.findInteractionController, + }); + + /// Constructs a [TizenInAppWebViewWidgetCreationParams] using a + /// [PlatformInAppWebViewWidgetCreationParams]. + TizenInAppWebViewWidgetCreationParams.fromPlatformInAppWebViewWidgetCreationParams( + PlatformInAppWebViewWidgetCreationParams params, + ) : this( + controllerFromPlatform: params.controllerFromPlatform, + key: params.key, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + headlessWebView: params.headlessWebView, + keepAlive: params.keepAlive, + preventGestureDelay: params.preventGestureDelay, + windowId: params.windowId, + webViewEnvironment: params.webViewEnvironment, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController, + findInteractionController: params.findInteractionController, + ); +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} +class TizenInAppWebViewWidget extends PlatformInAppWebViewWidget { + /// Constructs a [TizenInAppWebViewWidget]. + /// + ///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} + TizenInAppWebViewWidget(PlatformInAppWebViewWidgetCreationParams params) + : super.implementation( + params is TizenInAppWebViewWidgetCreationParams + ? params + : TizenInAppWebViewWidgetCreationParams.fromPlatformInAppWebViewWidgetCreationParams( + params, + ), + ); + + TizenInAppWebViewController? _controller; + + @override + Widget build(BuildContext context) { + _ensureSupportedCreationParams(); + + final InAppWebViewSettings initialSettings = + params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + final Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + // ignore: deprecated_member_use_from_same_package + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + return TizenView( + key: params.key, + viewType: 'com.pichillilorenzo/flutter_inappwebview', + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + onPlatformViewCreated: _onPlatformViewCreated, + creationParamsCodec: const StandardMessageCodec(), + creationParams: { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + }, + ); + } + + void _onPlatformViewCreated(int id) { + _controller = TizenInAppWebViewController( + PlatformInAppWebViewControllerCreationParams( + id: id, + webviewParams: params, + ), + ); + debugLog( + className: 'TizenInAppWebViewWidget', + id: id.toString(), + debugLoggingSettings: PlatformInAppWebViewController.debugLoggingSettings, + method: 'onWebViewCreated', + args: [], + ); + if (params.onWebViewCreated != null) { + params.onWebViewCreated!( + params.controllerFromPlatform?.call(_controller!) ?? _controller!, + ); + } + } + + void _ensureSupportedCreationParams() { + _rejectIf(params.headlessWebView != null, 'HeadlessInAppWebView'); + _rejectIf(params.keepAlive != null, 'InAppWebViewKeepAlive'); + _rejectIf(params.windowId != null, 'windowId'); + _rejectIf(params.webViewEnvironment != null, 'WebViewEnvironment'); + _rejectIf( + params.findInteractionController != null, + 'FindInteractionController', + ); + _rejectIf(params.pullToRefreshController != null, 'PullToRefresh'); + _rejectIf(params.contextMenu != null, 'contextMenu'); + _rejectIf( + params.initialUserScripts?.isNotEmpty ?? false, + 'initialUserScripts', + ); + + _rejectIf(params.onReceivedHttpError != null, 'onReceivedHttpError'); + // ignore: deprecated_member_use_from_same_package + _rejectIf(params.onLoadHttpError != null, 'onLoadHttpError'); + _rejectIf(params.onLoadResource != null, 'onLoadResource'); + _rejectIf( + params.onLoadResourceWithCustomScheme != null, + 'onLoadResourceWithCustomScheme', + ); + _rejectIf( + // ignore: deprecated_member_use_from_same_package + params.onLoadResourceCustomScheme != null, + 'onLoadResourceCustomScheme', + ); + _rejectIf(params.onDownloadStartRequest != null, 'onDownloadStartRequest'); + // ignore: deprecated_member_use_from_same_package + _rejectIf(params.onDownloadStart != null, 'onDownloadStart'); + _rejectIf(params.onCreateWindow != null, 'onCreateWindow'); + _rejectIf(params.onCloseWindow != null, 'onCloseWindow'); + _rejectIf( + params.onReceivedHttpAuthRequest != null, + 'onReceivedHttpAuthRequest', + ); + _rejectIf( + params.onReceivedServerTrustAuthRequest != null, + 'onReceivedServerTrustAuthRequest', + ); + _rejectIf( + params.onReceivedClientCertRequest != null, + 'onReceivedClientCertRequest', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf(params.onFindResultReceived != null, 'onFindResultReceived'); + _rejectIf( + params.shouldInterceptAjaxRequest != null, + 'shouldInterceptAjaxRequest', + ); + _rejectIf(params.onAjaxReadyStateChange != null, 'onAjaxReadyStateChange'); + _rejectIf(params.onAjaxProgress != null, 'onAjaxProgress'); + _rejectIf( + params.shouldInterceptFetchRequest != null, + 'shouldInterceptFetchRequest', + ); + _rejectIf(params.onPrintRequest != null, 'onPrintRequest'); + // ignore: deprecated_member_use_from_same_package + _rejectIf(params.onPrint != null, 'onPrint'); + _rejectIf( + params.onLongPressHitTestResult != null, + 'onLongPressHitTestResult', + ); + _rejectIf(params.onEnterFullscreen != null, 'onEnterFullscreen'); + _rejectIf(params.onExitFullscreen != null, 'onExitFullscreen'); + _rejectIf(params.onPageCommitVisible != null, 'onPageCommitVisible'); + _rejectIf(params.onWindowFocus != null, 'onWindowFocus'); + _rejectIf(params.onWindowBlur != null, 'onWindowBlur'); + _rejectIf(params.onOverScrolled != null, 'onOverScrolled'); + _rejectIf(params.onSafeBrowsingHit != null, 'onSafeBrowsingHit'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnSafeBrowsingHit != null, + 'androidOnSafeBrowsingHit', + ); + _rejectIf(params.onPermissionRequest != null, 'onPermissionRequest'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnPermissionRequest != null, + 'androidOnPermissionRequest', + ); + _rejectIf( + params.onPermissionRequestCanceled != null, + 'onPermissionRequestCanceled', + ); + _rejectIf( + params.onGeolocationPermissionsShowPrompt != null, + 'onGeolocationPermissionsShowPrompt', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnGeolocationPermissionsShowPrompt != null, + 'androidOnGeolocationPermissionsShowPrompt', + ); + _rejectIf( + params.onGeolocationPermissionsHidePrompt != null, + 'onGeolocationPermissionsHidePrompt', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnGeolocationPermissionsHidePrompt != null, + 'androidOnGeolocationPermissionsHidePrompt', + ); + _rejectIf(params.shouldInterceptRequest != null, 'shouldInterceptRequest'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidShouldInterceptRequest != null, + 'androidShouldInterceptRequest', + ); + _rejectIf(params.onRenderProcessGone != null, 'onRenderProcessGone'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnRenderProcessGone != null, + 'androidOnRenderProcessGone', + ); + _rejectIf( + params.onRenderProcessResponsive != null, + 'onRenderProcessResponsive', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnRenderProcessResponsive != null, + 'androidOnRenderProcessResponsive', + ); + _rejectIf( + params.onRenderProcessUnresponsive != null, + 'onRenderProcessUnresponsive', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnRenderProcessUnresponsive != null, + 'androidOnRenderProcessUnresponsive', + ); + _rejectIf(params.onFormResubmission != null, 'onFormResubmission'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnFormResubmission != null, + 'androidOnFormResubmission', + ); + _rejectIf(params.onReceivedIcon != null, 'onReceivedIcon'); + // ignore: deprecated_member_use_from_same_package + _rejectIf(params.androidOnReceivedIcon != null, 'androidOnReceivedIcon'); + _rejectIf(params.onReceivedTouchIconUrl != null, 'onReceivedTouchIconUrl'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnReceivedTouchIconUrl != null, + 'androidOnReceivedTouchIconUrl', + ); + _rejectIf(params.onJsBeforeUnload != null, 'onJsBeforeUnload'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnJsBeforeUnload != null, + 'androidOnJsBeforeUnload', + ); + _rejectIf(params.onReceivedLoginRequest != null, 'onReceivedLoginRequest'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.androidOnReceivedLoginRequest != null, + 'androidOnReceivedLoginRequest', + ); + _rejectIf(params.onRequestFocus != null, 'onRequestFocus'); + _rejectIf( + params.onWebContentProcessDidTerminate != null, + 'onWebContentProcessDidTerminate', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.iosOnWebContentProcessDidTerminate != null, + 'iosOnWebContentProcessDidTerminate', + ); + _rejectIf( + params.onDidReceiveServerRedirectForProvisionalNavigation != null, + 'onDidReceiveServerRedirectForProvisionalNavigation', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.iosOnDidReceiveServerRedirectForProvisionalNavigation != null, + 'iosOnDidReceiveServerRedirectForProvisionalNavigation', + ); + _rejectIf(params.onNavigationResponse != null, 'onNavigationResponse'); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.iosOnNavigationResponse != null, + 'iosOnNavigationResponse', + ); + _rejectIf( + params.shouldAllowDeprecatedTLS != null, + 'shouldAllowDeprecatedTLS', + ); + // ignore: deprecated_member_use_from_same_package + _rejectIf( + params.iosShouldAllowDeprecatedTLS != null, + 'iosShouldAllowDeprecatedTLS', + ); + _rejectIf( + params.onCameraCaptureStateChanged != null, + 'onCameraCaptureStateChanged', + ); + _rejectIf( + params.onMicrophoneCaptureStateChanged != null, + 'onMicrophoneCaptureStateChanged', + ); + _rejectIf(params.onContentSizeChanged != null, 'onContentSizeChanged'); + } + + void _rejectIf(bool condition, String feature) { + if (condition) { + throw UnsupportedError( + '$feature is not implemented on flutter_inappwebview_tizen.', + ); + } + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + } + + @override + void dispose() { + debugLog( + className: 'TizenInAppWebViewWidget', + id: _controller?.getViewId()?.toString(), + debugLoggingSettings: PlatformInAppWebViewController.debugLoggingSettings, + method: 'dispose', + args: [], + ); + _controller?.dispose(); + _controller = null; + } + + @override + T controllerFromPlatform(PlatformInAppWebViewController controller) { + return (params.controllerFromPlatform?.call(controller) ?? controller) as T; + } +} diff --git a/packages/flutter_inappwebview/lib/src/in_app_webview/in_app_webview_controller.dart b/packages/flutter_inappwebview/lib/src/in_app_webview/in_app_webview_controller.dart new file mode 100644 index 000000000..941a56632 --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/in_app_webview/in_app_webview_controller.dart @@ -0,0 +1,1078 @@ +// Copyright 2026 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: avoid_dynamic_calls + +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '_static_channel.dart'; + +/// Object specifying creation parameters for creating a [TizenInAppWebViewController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppWebViewControllerCreationParams] for +/// more information. +@immutable +class TizenInAppWebViewControllerCreationParams + extends PlatformInAppWebViewControllerCreationParams { + /// Creates a new [TizenInAppWebViewControllerCreationParams] instance. + const TizenInAppWebViewControllerCreationParams({ + required super.id, + super.webviewParams, + }); + + /// Creates a [TizenInAppWebViewControllerCreationParams] instance based on [PlatformInAppWebViewControllerCreationParams]. + factory TizenInAppWebViewControllerCreationParams.fromPlatformInAppWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppWebViewControllerCreationParams params, + ) { + return TizenInAppWebViewControllerCreationParams( + id: params.id, + webviewParams: params.webviewParams, + ); + } +} + +/// Error returned in `InAppWebView.onReceivedError` when a Tizen web resource +/// loading error occurs. +class TizenWebResourceError extends WebResourceError { + /// Creates a new [TizenWebResourceError]. + TizenWebResourceError._({required this.errorCode, required super.description}) + : super(type: _errorCodeToErrorType(errorCode)); + + /// Unknown error. + static const int unknown = 0; + + /// Failed to file I/O. + static const int failedFileIO = 3; + + /// Cannot connect to Network. + static const int cantConnect = 4; + + /// Fail to look up host from DNS. + static const int cantHostLookup = 5; + + /// Fail to SSL/TLS handshake. + static const int failedSslHandshake = 6; + + /// Connection timeout. + static const int requestTimeout = 8; + + /// Too many redirects. + static const int tooManyRedirect = 9; + + /// Too many requests during this load. + static const int tooManyRequests = 10; + + /// Malformed url. + static const int badUrl = 11; + + /// Unsupported scheme. + static const int unsupportedScheme = 12; + + /// User authentication failed on server. + static const int authenticationFailed = 13; + + /// Raw EWK error code. + final int errorCode; + + static WebResourceErrorType _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case unknown: + return WebResourceErrorType.UNKNOWN; + case failedFileIO: + return WebResourceErrorType.GENERIC_FILE_ERROR; + case cantConnect: + return WebResourceErrorType.CANNOT_CONNECT_TO_HOST; + case cantHostLookup: + return WebResourceErrorType.HOST_LOOKUP; + case failedSslHandshake: + return WebResourceErrorType.FAILED_SSL_HANDSHAKE; + case requestTimeout: + return WebResourceErrorType.TIMEOUT; + case tooManyRedirect: + return WebResourceErrorType.TOO_MANY_REDIRECTS; + case tooManyRequests: + return WebResourceErrorType.TOO_MANY_REQUESTS; + case badUrl: + return WebResourceErrorType.BAD_URL; + case unsupportedScheme: + return WebResourceErrorType.UNSUPPORTED_SCHEME; + case authenticationFailed: + return WebResourceErrorType.USER_AUTHENTICATION_FAILED; + } + + return WebResourceErrorType.UNKNOWN; + } +} + +///Controls an [InAppWebView] widget instance. +/// +///If you are using the [InAppWebView] widget, an [InAppWebViewController] instance can be obtained by setting the [InAppWebView.onWebViewCreated] +///callback. +class TizenInAppWebViewController extends PlatformInAppWebViewController + with ChannelController { + /// Creates a new [TizenInAppWebViewController]. + TizenInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params, + ) : super.implementation( + params is TizenInAppWebViewControllerCreationParams + ? params + : TizenInAppWebViewControllerCreationParams.fromPlatformInAppWebViewControllerCreationParams( + params, + ), + ) { + channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_$id'); + handler = handleMethod; + initMethodCallHandler(); + + _init(params); + } + + /// Returns the lazily-constructed singleton used for static channel calls. + factory TizenInAppWebViewController.static() { + return _staticValue; + } + static const MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + dynamic _controllerFromPlatform; + + static final TizenInAppWebViewController _staticValue = + TizenInAppWebViewController( + const TizenInAppWebViewControllerCreationParams(id: null), + ); + + void _init(PlatformInAppWebViewControllerCreationParams params) { + _controllerFromPlatform = + params.webviewParams?.controllerFromPlatform?.call(this) ?? this; + } + + void _debugLog(String method, dynamic args) { + debugLog( + className: 'TizenInAppWebViewController', + name: 'WebView', + id: getViewId().toString(), + debugLoggingSettings: PlatformInAppWebViewController.debugLoggingSettings, + method: method, + args: args, + ); + } + + Never _unsupported(String method) { + throw UnsupportedError( + '$method is not implemented on flutter_inappwebview_tizen.', + ); + } + + WebResourceError? _parseWebResourceError(Map? errorMap) { + if (errorMap == null) { + return null; + } + + final String description = + errorMap['description']?.toString() ?? 'Unknown error'; + final dynamic nativeType = errorMap['type']; + final dynamic nativeErrorCode = errorMap['errorCode'] ?? nativeType; + final int? errorCode = nativeErrorCode is int ? nativeErrorCode : null; + if (errorCode != null) { + return TizenWebResourceError._( + errorCode: errorCode, + description: description, + ); + } + + final WebResourceErrorType type = + WebResourceErrorType.fromNativeValue( + nativeType is int ? nativeType : null, + ) ?? + WebResourceErrorType.UNKNOWN; + return WebResourceError(description: description, type: type); + } + + Future _handleMethod(MethodCall call) async { + if (PlatformInAppWebViewController.debugLoggingSettings.enabled) { + _debugLog(call.method, call.arguments); + } + + switch (call.method) { + case 'onLoadStart': + if (webviewParams != null && webviewParams!.onLoadStart != null) { + final String? url = call.arguments['url']; + webviewParams!.onLoadStart!( + _controllerFromPlatform, + url != null ? WebUri(url) : null, + ); + } + case 'onLoadStop': + if (webviewParams != null && webviewParams!.onLoadStop != null) { + final String? url = call.arguments['url']; + webviewParams!.onLoadStop!( + _controllerFromPlatform, + url != null ? WebUri(url) : null, + ); + } + case 'onReceivedError': + if (webviewParams != null && + (webviewParams!.onReceivedError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError != null)) { + final Map? requestMap = call.arguments['request'] + ?.cast(); + final Map? errorMap = call.arguments['error'] + ?.cast(); + if (requestMap == null || errorMap == null) { + break; + } + final WebResourceRequest? request = WebResourceRequest.fromMap( + requestMap, + ); + final WebResourceError? error = _parseWebResourceError(errorMap); + if (request == null || error == null) { + break; + } + final bool isForMainFrame = request.isForMainFrame ?? false; + if (webviewParams!.onReceivedError != null) { + webviewParams!.onReceivedError!( + _controllerFromPlatform, + request, + error, + ); + } else if (isForMainFrame) { + final int errorCode = error is TizenWebResourceError + ? error.errorCode + : error.type.toNativeValue() ?? -1; + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError!( + _controllerFromPlatform, + request.url, + errorCode, + error.description, + ); + } + } + case 'onProgressChanged': + if (webviewParams != null && webviewParams!.onProgressChanged != null) { + final int progress = call.arguments['progress']; + webviewParams!.onProgressChanged!(_controllerFromPlatform, progress); + } + case 'shouldOverrideUrlLoading': + if (webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) { + final NavigationAction navigationAction = NavigationAction.fromMap( + call.arguments.cast(), + )!; + return (await webviewParams!.shouldOverrideUrlLoading!( + _controllerFromPlatform, + navigationAction, + ))?.toNativeValue(); + } + case 'onConsoleMessage': + if (webviewParams != null && webviewParams!.onConsoleMessage != null) { + final ConsoleMessage consoleMessage = ConsoleMessage.fromMap( + call.arguments.cast(), + )!; + webviewParams!.onConsoleMessage!( + _controllerFromPlatform, + consoleMessage, + ); + } + case 'onScrollChanged': + if (webviewParams != null && webviewParams!.onScrollChanged != null) { + final int x = call.arguments['x']; + final int y = call.arguments['y']; + webviewParams!.onScrollChanged!(_controllerFromPlatform, x, y); + } + case 'onTitleChanged': + if (webviewParams != null && webviewParams!.onTitleChanged != null) { + final String? title = call.arguments['title']; + webviewParams!.onTitleChanged!(_controllerFromPlatform, title); + } + case 'onZoomScaleChanged': + if (webviewParams != null && + webviewParams!.onZoomScaleChanged != null) { + final double oldScale = call.arguments['oldScale']; + final double newScale = call.arguments['newScale']; + webviewParams!.onZoomScaleChanged!( + _controllerFromPlatform, + oldScale, + newScale, + ); + } + case 'onJsAlert': + final JsAlertRequest alertRequest = JsAlertRequest.fromMap( + call.arguments.cast(), + )!; + if (webviewParams != null && webviewParams!.onJsAlert != null) { + await webviewParams!.onJsAlert!( + _controllerFromPlatform, + alertRequest, + ); + } + // Always reply: EWK suspends JS execution until the alert is acknowledged. + await channel?.invokeMethod('javaScriptAlertReply'); + case 'onJsConfirm': + final JsConfirmRequest confirmRequest = JsConfirmRequest.fromMap( + call.arguments.cast(), + )!; + bool confirmed = true; + if (webviewParams != null && webviewParams!.onJsConfirm != null) { + final JsConfirmResponse? response = await webviewParams!.onJsConfirm!( + _controllerFromPlatform, + confirmRequest, + ); + if (response?.action == JsConfirmResponseAction.CANCEL) { + confirmed = false; + } + } + await channel?.invokeMethod('javaScriptConfirmReply', confirmed); + case 'onJsPrompt': + final JsPromptRequest promptRequest = JsPromptRequest.fromMap( + call.arguments.cast(), + )!; + String? reply; + if (webviewParams != null && webviewParams!.onJsPrompt != null) { + final JsPromptResponse? response = await webviewParams!.onJsPrompt!( + _controllerFromPlatform, + promptRequest, + ); + if (response != null && + response.action != JsPromptResponseAction.CANCEL) { + reply = response.value ?? promptRequest.defaultValue ?? ''; + } + } else { + reply = promptRequest.defaultValue ?? ''; + } + // A null reply signals that the prompt was cancelled. + await channel?.invokeMethod('javaScriptPromptReply', reply); + case 'onUpdateVisitedHistory': + if (webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) { + final String? url = call.arguments['url']; + final bool? isReload = call.arguments['isReload']; + webviewParams!.onUpdateVisitedHistory!( + _controllerFromPlatform, + url != null ? WebUri(url) : null, + isReload, + ); + } + default: + throw UnimplementedError('Unimplemented ${call.method} method'); + } + return null; + } + + @override + Future getUrl() async { + final Map args = {}; + final String? url = await channel?.invokeMethod('getUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getTitle() async { + final Map args = {}; + return await channel?.invokeMethod('getTitle', args); + } + + @override + Future getProgress() async { + final Map args = {}; + return await channel?.invokeMethod('getProgress', args); + } + + @override + Future getHtml() async { + _unsupported('getHtml'); + } + + @override + Future> getFavicons() async { + _unsupported('getFavicons'); + } + + @override + Future loadUrl({ + required URLRequest urlRequest, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo, + }) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme('file'), + ); + assert( + iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme('file'), + ); + + final Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString(), + ); + await channel?.invokeMethod('loadUrl', args); + } + + @override + Future postUrl({ + required WebUri url, + required Uint8List postData, + }) async { + assert(url.toString().isNotEmpty); + final Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('postData', () => postData); + await channel?.invokeMethod('postUrl', args); + } + + @override + Future loadData({ + required String data, + String mimeType = 'text/html', + String encoding = 'utf8', + WebUri? baseUrl, + @Deprecated('Use historyUrl instead') Uri? androidHistoryUrl, + WebUri? historyUrl, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo, + }) async { + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme('file'), + ); + assert( + iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme('file'), + ); + + final Map args = {}; + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? 'about:blank'); + args.putIfAbsent( + 'historyUrl', + () => + historyUrl?.toString() ?? + androidHistoryUrl?.toString() ?? + 'about:blank', + ); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString(), + ); + await channel?.invokeMethod('loadData', args); + } + + @override + Future loadFile({required String assetFilePath}) async { + assert(assetFilePath.isNotEmpty); + final Map args = {}; + args.putIfAbsent('assetFilePath', () => assetFilePath); + await channel?.invokeMethod('loadFile', args); + } + + @override + Future reload() async { + final Map args = {}; + await channel?.invokeMethod('reload', args); + } + + @override + Future goBack() async { + final Map args = {}; + await channel?.invokeMethod('goBack', args); + } + + @override + Future canGoBack() async { + final Map args = {}; + return await channel?.invokeMethod('canGoBack', args) ?? false; + } + + @override + Future goForward() async { + final Map args = {}; + await channel?.invokeMethod('goForward', args); + } + + @override + Future canGoForward() async { + final Map args = {}; + return await channel?.invokeMethod('canGoForward', args) ?? false; + } + + @override + Future goBackOrForward({required int steps}) async { + _unsupported('goBackOrForward'); + } + + @override + Future canGoBackOrForward({required int steps}) async { + _unsupported('canGoBackOrForward'); + } + + @override + Future goTo({required WebHistoryItem historyItem}) async { + _unsupported('goTo'); + } + + @override + Future isLoading() async { + _unsupported('isLoading'); + } + + @override + Future stopLoading() async { + final Map args = {}; + await channel?.invokeMethod('stopLoading', args); + } + + @override + Future evaluateJavascript({ + required String source, + ContentWorld? contentWorld, + }) async { + final Map args = {}; + args.putIfAbsent('source', () => source); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + dynamic data = await channel?.invokeMethod('evaluateJavascript', args); + if (data is String) { + try { + // Try to JSON-decode the value coming from JavaScript; if it isn't + // valid JSON, return the raw string. + data = json.decode(data); + } on FormatException { + // Not valid JSON; keep the original string. + } + } + return data; + } + + @override + Future injectJavascriptFileFromUrl({ + required WebUri urlFile, + ScriptHtmlTagAttributes? scriptHtmlTagAttributes, + }) async { + _unsupported('injectJavascriptFileFromUrl'); + } + + @override + Future injectJavascriptFileFromAsset({ + required String assetFilePath, + }) async { + _unsupported('injectJavascriptFileFromAsset'); + } + + @override + Future injectCSSCode({required String source}) async { + _unsupported('injectCSSCode'); + } + + @override + Future injectCSSFileFromUrl({ + required WebUri urlFile, + CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes, + }) async { + _unsupported('injectCSSFileFromUrl'); + } + + @override + Future injectCSSFileFromAsset({required String assetFilePath}) async { + _unsupported('injectCSSFileFromAsset'); + } + + @override + void addJavaScriptHandler({ + required String handlerName, + required JavaScriptHandlerCallback callback, + }) { + _unsupported('addJavaScriptHandler'); + } + + @override + JavaScriptHandlerCallback? removeJavaScriptHandler({ + required String handlerName, + }) { + return null; + } + + @override + bool hasJavaScriptHandler({required String handlerName}) { + return false; + } + + @override + Future takeScreenshot({ + ScreenshotConfiguration? screenshotConfiguration, + }) async { + _unsupported('takeScreenshot'); + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppWebViewGroupOptions options}) async { + final InAppWebViewSettings settings = + InAppWebViewSettings.fromMap(options.toMap()) ?? InAppWebViewSettings(); + await setSettings(settings: settings); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + final InAppWebViewSettings? settings = await getSettings(); + + Map? options = settings?.toMap(); + if (options != null) { + options = options.cast(); + return InAppWebViewGroupOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings({required InAppWebViewSettings settings}) async { + final Map args = {}; + + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + _unsupported('getSettings'); + } + + @override + Future getCopyBackForwardList() async { + _unsupported('getCopyBackForwardList'); + } + + @override + @Deprecated('Use InAppWebViewController.clearAllCache instead') + Future clearCache() async { + final Map args = {}; + await channel?.invokeMethod('clearCache', args); + } + + @override + @Deprecated('Use FindInteractionController.findAll instead') + Future findAllAsync({required String find}) async { + _unsupported('findAllAsync'); + } + + @override + @Deprecated('Use FindInteractionController.findNext instead') + Future findNext({required bool forward}) async { + _unsupported('findNext'); + } + + @override + @Deprecated('Use FindInteractionController.clearMatches instead') + Future clearMatches() async { + _unsupported('clearMatches'); + } + + @override + @Deprecated('Use tRexRunnerHtml instead') + Future getTRexRunnerHtml() async { + return tRexRunnerHtml; + } + + @override + @Deprecated('Use tRexRunnerCss instead') + Future getTRexRunnerCss() async { + return tRexRunnerCss; + } + + @override + Future scrollTo({ + required int x, + required int y, + bool animated = false, + }) async { + final Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollTo', args); + } + + @override + Future scrollBy({ + required int x, + required int y, + bool animated = false, + }) async { + final Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollBy', args); + } + + @override + Future pauseTimers() async { + _unsupported('pauseTimers'); + } + + @override + Future resumeTimers() async { + _unsupported('resumeTimers'); + } + + @override + Future printCurrentPage({ + PrintJobSettings? settings, + }) async { + _unsupported('printCurrentPage'); + } + + @override + Future getContentHeight() async { + _unsupported('getContentHeight'); + } + + @override + Future getContentWidth() async { + _unsupported('getContentWidth'); + } + + @override + Future zoomBy({ + required double zoomFactor, + @Deprecated('Use animated instead') bool? iosAnimated, + bool animated = false, + }) async { + final Map args = {}; + args.putIfAbsent('zoomFactor', () => zoomFactor); + args.putIfAbsent('animated', () => iosAnimated ?? animated); + return await channel?.invokeMethod('zoomBy', args); + } + + @override + Future getOriginalUrl() async { + _unsupported('getOriginalUrl'); + } + + @override + @Deprecated('Use getZoomScale instead') + Future getScale() async { + return getZoomScale(); + } + + @override + Future getSelectedText() async { + _unsupported('getSelectedText'); + } + + @override + Future> getMetaTags() async { + _unsupported('getMetaTags'); + } + + @override + Future getMetaThemeColor() async { + _unsupported('getMetaThemeColor'); + } + + @override + Future getScrollX() async { + final Map args = {}; + return await channel?.invokeMethod('getScrollX', args); + } + + @override + Future getScrollY() async { + final Map args = {}; + return await channel?.invokeMethod('getScrollY', args); + } + + @override + Future getCertificate() async { + _unsupported('getCertificate'); + } + + @override + Future addUserScript({required UserScript userScript}) async { + _unsupported('addUserScript'); + } + + @override + Future addUserScripts({required List userScripts}) async { + _unsupported('addUserScripts'); + } + + @override + Future removeUserScript({required UserScript userScript}) async { + _unsupported('removeUserScript'); + } + + @override + Future removeUserScriptsByGroupName({required String groupName}) async { + _unsupported('removeUserScriptsByGroupName'); + } + + @override + Future removeUserScripts({ + required List userScripts, + }) async { + _unsupported('removeUserScripts'); + } + + @override + Future removeAllUserScripts() async { + _unsupported('removeAllUserScripts'); + } + + @override + bool hasUserScript({required UserScript userScript}) { + return false; + } + + @override + Future callAsyncJavaScript({ + required String functionBody, + Map arguments = const {}, + ContentWorld? contentWorld, + }) async { + _unsupported('callAsyncJavaScript'); + } + + @override + Future saveWebArchive({ + required String filePath, + bool autoname = false, + }) async { + _unsupported('saveWebArchive'); + } + + @override + Future isSecureContext() async { + _unsupported('isSecureContext'); + } + + @override + Future createWebMessageChannel() async { + _unsupported('createWebMessageChannel'); + } + + @override + Future postWebMessage({ + required WebMessage message, + WebUri? targetOrigin, + }) async { + _unsupported('postWebMessage'); + } + + @override + Future addWebMessageListener( + PlatformWebMessageListener webMessageListener, + ) async { + _unsupported('addWebMessageListener'); + } + + @override + bool hasWebMessageListener(PlatformWebMessageListener webMessageListener) { + return false; + } + + @override + Future canScrollVertically() async { + _unsupported('canScrollVertically'); + } + + @override + Future canScrollHorizontally() async { + _unsupported('canScrollHorizontally'); + } + + @override + Future reloadFromOrigin() async { + _unsupported('reloadFromOrigin'); + } + + @override + Future createPdf({ + @Deprecated('Use pdfConfiguration instead') + // ignore: deprecated_member_use_from_same_package + IOSWKPDFConfiguration? iosWKPdfConfiguration, + PDFConfiguration? pdfConfiguration, + }) async { + _unsupported('createPdf'); + } + + @override + Future createWebArchiveData() async { + _unsupported('createWebArchiveData'); + } + + @override + Future hasOnlySecureContent() async { + _unsupported('hasOnlySecureContent'); + } + + @override + Future pauseAllMediaPlayback() async { + _unsupported('pauseAllMediaPlayback'); + } + + @override + Future setAllMediaPlaybackSuspended({required bool suspended}) async { + _unsupported('setAllMediaPlaybackSuspended'); + } + + @override + Future closeAllMediaPresentations() async { + _unsupported('closeAllMediaPresentations'); + } + + @override + Future requestMediaPlaybackState() async { + _unsupported('requestMediaPlaybackState'); + } + + @override + Future isInFullscreen() async { + _unsupported('isInFullscreen'); + } + + @override + Future getCameraCaptureState() async { + _unsupported('getCameraCaptureState'); + } + + @override + Future setCameraCaptureState({required MediaCaptureState state}) async { + _unsupported('setCameraCaptureState'); + } + + @override + Future getMicrophoneCaptureState() async { + _unsupported('getMicrophoneCaptureState'); + } + + @override + Future setMicrophoneCaptureState({ + required MediaCaptureState state, + }) async { + _unsupported('setMicrophoneCaptureState'); + } + + @override + Future loadSimulatedRequest({ + required URLRequest urlRequest, + required Uint8List data, + URLResponse? urlResponse, + }) async { + _unsupported('loadSimulatedRequest'); + } + + @override + Future openDevTools() async { + _unsupported('openDevTools'); + } + + @override + Future callDevToolsProtocolMethod({ + required String methodName, + Map? parameters, + }) async { + _unsupported('callDevToolsProtocolMethod'); + } + + @override + Future addDevToolsProtocolEventListener({ + required String eventName, + required Function(dynamic data) callback, + }) async { + _unsupported('addDevToolsProtocolEventListener'); + } + + @override + Future removeDevToolsProtocolEventListener({ + required String eventName, + }) async { + _unsupported('removeDevToolsProtocolEventListener'); + } + + @override + Future pause() async { + _unsupported('pause'); + } + + @override + Future resume() async { + _unsupported('resume'); + } + + @override + Future getDefaultUserAgent() async { + final Map args = {}; + return await _staticChannel.invokeMethod( + 'getDefaultUserAgent', + args, + ) ?? + ''; + } + + @override + Future handlesURLScheme(String urlScheme) async { + final Map args = {}; + args.putIfAbsent('urlScheme', () => urlScheme); + return await _staticChannel.invokeMethod('handlesURLScheme', args); + } + + @override + Future disposeKeepAlive(InAppWebViewKeepAlive keepAlive) async { + _unsupported('disposeKeepAlive'); + } + + @override + Future clearAllCache({bool includeDiskFiles = true}) async { + final Map args = {}; + args.putIfAbsent('includeDiskFiles', () => includeDiskFiles); + await _staticChannel.invokeMethod('clearAllCache', args); + } + + @override + Future get tRexRunnerHtml async => rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html', + ); + + @override + Future get tRexRunnerCss async => rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css', + ); + + @override + dynamic getViewId() { + return id; + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(); + _controllerFromPlatform = null; + } +} + +/// Internal hook that exposes [_handleMethod] to the constructor without +/// making it part of the public controller surface. +extension InternalInAppWebViewController on TizenInAppWebViewController { + /// Returns the channel handler bound to [_handleMethod]. + Future Function(MethodCall call) get handleMethod => _handleMethod; +} diff --git a/packages/flutter_inappwebview/lib/src/in_app_webview/main.dart b/packages/flutter_inappwebview/lib/src/in_app_webview/main.dart new file mode 100644 index 000000000..086bcf6d7 --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/in_app_webview/main.dart @@ -0,0 +1,6 @@ +// Copyright 2026 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. + +export 'in_app_webview.dart'; +export 'in_app_webview_controller.dart' hide InternalInAppWebViewController; diff --git a/packages/flutter_inappwebview/lib/src/inappwebview_platform.dart b/packages/flutter_inappwebview/lib/src/inappwebview_platform.dart new file mode 100644 index 000000000..33d11e812 --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/inappwebview_platform.dart @@ -0,0 +1,52 @@ +// Copyright 2026 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_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'cookie_manager.dart'; +import 'in_app_webview/in_app_webview.dart'; +import 'in_app_webview/in_app_webview_controller.dart'; + +/// Implementation of [InAppWebViewPlatform] using the Tizen WebView EWK API. +/// +/// Only the implemented surface is overridden; calls to unsupported +/// features fall through to the platform interface defaults, which raise +/// [UnimplementedError]. +class TizenInAppWebViewPlatform extends InAppWebViewPlatform { + /// Registers this class as the default instance of [InAppWebViewPlatform]. + static void register() { + registerWith(); + } + + /// Registers this class as the default instance of [InAppWebViewPlatform]. + static void registerWith() { + InAppWebViewPlatform.instance = TizenInAppWebViewPlatform(); + } + + @override + TizenCookieManager createPlatformCookieManager( + PlatformCookieManagerCreationParams params, + ) { + return TizenCookieManager(params); + } + + @override + TizenInAppWebViewController createPlatformInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params, + ) { + return TizenInAppWebViewController(params); + } + + @override + TizenInAppWebViewController createPlatformInAppWebViewControllerStatic() { + return TizenInAppWebViewController.static(); + } + + @override + TizenInAppWebViewWidget createPlatformInAppWebViewWidget( + PlatformInAppWebViewWidgetCreationParams params, + ) { + return TizenInAppWebViewWidget(params); + } +} diff --git a/packages/flutter_inappwebview/lib/src/main.dart b/packages/flutter_inappwebview/lib/src/main.dart new file mode 100644 index 000000000..f78f61536 --- /dev/null +++ b/packages/flutter_inappwebview/lib/src/main.dart @@ -0,0 +1,7 @@ +// Copyright 2026 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. + +export 'cookie_manager.dart' hide InternalCookieManager; +export 'in_app_webview/main.dart'; +export 'inappwebview_platform.dart'; diff --git a/packages/flutter_inappwebview/pubspec.yaml b/packages/flutter_inappwebview/pubspec.yaml new file mode 100644 index 000000000..2a25917ed --- /dev/null +++ b/packages/flutter_inappwebview/pubspec.yaml @@ -0,0 +1,34 @@ +name: flutter_inappwebview_tizen +description: Tizen implementation of the flutter_inappwebview plugin. +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/flutter_inappwebview +version: 0.1.0 + +environment: + sdk: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: FlutterInappwebviewTizenPlugin + fileName: flutter_inappwebview_tizen_plugin.h + dartPluginClass: TizenInAppWebViewPlatform + +dependencies: + flutter: + sdk: flutter + flutter_inappwebview_platform_interface: ^1.3.0+1 + flutter_tizen: ^0.2.1 + +dev_dependencies: + flutter_test: + sdk: flutter + +topics: + - html + - webview + - webview-flutter + - inappwebview + - browser diff --git a/packages/flutter_inappwebview/tizen/.gitignore b/packages/flutter_inappwebview/tizen/.gitignore new file mode 100644 index 000000000..a2a7d62b1 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/flutter_inappwebview/tizen/inc/flutter_inappwebview_tizen_plugin.h b/packages/flutter_inappwebview/tizen/inc/flutter_inappwebview_tizen_plugin.h new file mode 100644 index 000000000..bdf565356 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/inc/flutter_inappwebview_tizen_plugin.h @@ -0,0 +1,27 @@ +// Copyright 2026 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_FLUTTER_INAPPWEBVIEW_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_INAPPWEBVIEW_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 FlutterInappwebviewTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FLUTTER_INAPPWEBVIEW_TIZEN_PLUGIN_H_ diff --git a/packages/flutter_inappwebview/tizen/project_def.prop b/packages/flutter_inappwebview/tizen/project_def.prop new file mode 100644 index 000000000..c09310f67 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/project_def.prop @@ -0,0 +1,20 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = flutter_inappwebview_tizen_plugin +type = staticLib +profile = common-5.5 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL WEBVIEW_TIZEN_TOUCH_EVENTS_ENABLED +USER_CPP_UNDEFS = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/flutter_inappwebview/tizen/src/buffer_pool.cc b/packages/flutter_inappwebview/tizen/src/buffer_pool.cc new file mode 100644 index 000000000..a6491829e --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/buffer_pool.cc @@ -0,0 +1,180 @@ +// Copyright 2026 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 + +struct BufferReleaseState { + std::atomic_bool is_used = false; +}; + +namespace { + +struct GpuSurfaceDescriptorContext { + std::shared_ptr release_state; + FlutterDesktopGpuSurfaceDescriptor descriptor = {}; +}; + +void ReleaseGpuSurfaceDescriptor(void* release_context) { + auto context = + reinterpret_cast(release_context); + context->release_state->is_used.store(false); + delete context; +} + +} // namespace + +BufferUnit::BufferUnit(int32_t width, int32_t height) + : release_state_(std::make_shared()) { + Reset(width, height); +} + +BufferUnit::~BufferUnit() { + if (tbm_surface_ && !use_external_buffer_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } +} + +void BufferUnit::UseExternalBuffer() { + if (!use_external_buffer_) { + use_external_buffer_ = true; + if (tbm_surface_) { + tbm_surface_destroy(tbm_surface_); + tbm_surface_ = nullptr; + } + } +} + +void BufferUnit::SetExternalBuffer(tbm_surface_h tbm_surface) { + if (use_external_buffer_) { + tbm_surface_ = tbm_surface; + } +} + +bool BufferUnit::MarkInUse() { + bool expected = false; + return release_state_->is_used.compare_exchange_strong(expected, true); +} + +void BufferUnit::UnmarkInUse() { release_state_->is_used.store(false); } + +bool BufferUnit::IsUsed() { + return release_state_->is_used.load() && tbm_surface_; +} + +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_ && !use_external_buffer_) { + tbm_surface_destroy(tbm_surface_); + } + tbm_surface_ = nullptr; + + if (!use_external_buffer_) { + tbm_surface_ = tbm_surface_create(width_, height_, TBM_FORMAT_ARGB8888); + } +} + +FlutterDesktopGpuSurfaceDescriptor* BufferUnit::GpuSurface() { + if (!tbm_surface_) { + UnmarkInUse(); + return nullptr; + } + + auto context = new GpuSurfaceDescriptorContext(); + context->release_state = release_state_; + context->descriptor.width = width_; + context->descriptor.height = height_; + context->descriptor.handle = tbm_surface_; + context->descriptor.release_callback = ReleaseGpuSurfaceDescriptor; + context->descriptor.release_context = context; + return &context->descriptor; +} + +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() { + std::lock_guard lock(mutex_); + BufferUnit* buffer = pool_[0].get(); + if (buffer->MarkInUse()) { + return buffer; + } + return nullptr; +} + +void SingleBufferPool::Release(BufferUnit* buffer) { + BufferPool::Release(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/flutter_inappwebview/tizen/src/buffer_pool.h b/packages/flutter_inappwebview/tizen/src/buffer_pool.h new file mode 100644 index 000000000..83f01537c --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/buffer_pool.h @@ -0,0 +1,76 @@ +// Copyright 2026 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 + +struct BufferReleaseState; + +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(); + + void UseExternalBuffer(); + void SetExternalBuffer(tbm_surface_h tbm_surface); + + tbm_surface_h Surface(); + + FlutterDesktopGpuSurfaceDescriptor* GpuSurface(); + +#ifndef NDEBUG + // TODO: Unused code. + void DumpToPng(int file_name); +#endif + + private: + std::shared_ptr release_state_; + bool use_external_buffer_ = false; + int32_t width_ = 0; + int32_t height_ = 0; + tbm_surface_h tbm_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_; + std::mutex mutex_; + + private: + size_t last_index_ = 0; +}; + +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/flutter_inappwebview/tizen/src/ewk_internal_api_binding.cc b/packages/flutter_inappwebview/tizen/src/ewk_internal_api_binding.cc new file mode 100644 index 000000000..8f3fb1a62 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/ewk_internal_api_binding.cc @@ -0,0 +1,127 @@ +// Copyright 2026 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 + +#include "log.h" + +namespace { + +template +T ResolveEwkSymbol(void* handle, const char* symbol) { + dlerror(); + void* value = dlsym(handle, symbol); + const char* error = dlerror(); + if (error || !value) { + LOG_ERROR("Failed to resolve EWK symbol %s: %s", symbol, + error ? error : "not found"); + return nullptr; + } + return reinterpret_cast(value); +} + +} // namespace + +EwkInternalApiBinding::EwkInternalApiBinding() { + handle_ = dlopen("libchromium-ewk.so", RTLD_LAZY); + if (!handle_) { + const char* error = dlerror(); + LOG_ERROR("Failed to open libchromium-ewk.so: %s", + error ? error : "unknown error"); + } +} + +EwkInternalApiBinding::~EwkInternalApiBinding() { + if (handle_) { + dlclose(handle_); + } +} + +bool EwkInternalApiBinding::Initialize() { + if (!handle_) { + LOG_ERROR( + "EWK internal API binding was initialized without a library handle."); + return false; + } + + // ewk_view + view.SetBackgroundColor = ResolveEwkSymbol( + handle_, "ewk_view_bg_color_set"); + view.TouchEventsEnabledSet = + ResolveEwkSymbol( + handle_, "ewk_view_touch_events_enabled_set"); + view.FeedTouchEvent = ResolveEwkSymbol( + handle_, "ewk_view_feed_touch_event"); + view.MouseEventsEnabledSet = + ResolveEwkSymbol( + handle_, "ewk_view_mouse_events_enabled_set"); + view.FeedMouseDown = ResolveEwkSymbol( + handle_, "ewk_view_feed_mouse_down"); + view.FeedMouseUp = ResolveEwkSymbol( + handle_, "ewk_view_feed_mouse_up"); + view.FeedMouseWheel = ResolveEwkSymbol( + handle_, "ewk_view_feed_mouse_wheel"); + view.SendKeyEvent = ResolveEwkSymbol( + handle_, "ewk_view_send_key_event"); + view.OffscreenRenderingEnabledSet = + ResolveEwkSymbol( + handle_, "ewk_view_offscreen_rendering_enabled_set"); + view.ImeWindowSet = ResolveEwkSymbol( + handle_, "ewk_view_ime_window_set"); + view.KeyEventsEnabledSet = ResolveEwkSymbol( + handle_, "ewk_view_key_events_enabled_set"); + view.SupportVideoHoleSet = ResolveEwkSymbol( + handle_, "ewk_view_set_support_video_hole"); + + view.OnJavaScriptAlert = + ResolveEwkSymbol( + handle_, "ewk_view_javascript_alert_callback_set"); + view.OnJavaScriptConfirm = + ResolveEwkSymbol( + handle_, "ewk_view_javascript_confirm_callback_set"); + view.OnJavaScriptPrompt = + ResolveEwkSymbol( + handle_, "ewk_view_javascript_prompt_callback_set"); + + view.JavaScriptAlertReply = + ResolveEwkSymbol( + handle_, "ewk_view_javascript_alert_reply"); + view.JavaScriptConfirmReply = + ResolveEwkSymbol( + handle_, "ewk_view_javascript_confirm_reply"); + view.JavaScriptPromptReply = + ResolveEwkSymbol( + handle_, "ewk_view_javascript_prompt_reply"); + + // ewk_main + main.SetArguments = + ResolveEwkSymbol(handle_, "ewk_set_arguments"); + + // ewk_settings + settings.ImePanelEnabledSet = + ResolveEwkSymbol( + handle_, "ewk_settings_ime_panel_enabled_set"); + settings.ForceZoomSet = ResolveEwkSymbol( + handle_, "ewk_settings_force_zoom_set"); + + // ewk_console_message + console_message.LevelGet = ResolveEwkSymbol( + handle_, "ewk_console_message_level_get"); + console_message.TextGet = ResolveEwkSymbol( + handle_, "ewk_console_message_text_get"); + + return view.SetBackgroundColor && view.TouchEventsEnabledSet && + view.FeedTouchEvent && view.MouseEventsEnabledSet && + view.FeedMouseDown && view.FeedMouseUp && view.FeedMouseWheel && + view.SendKeyEvent && view.OffscreenRenderingEnabledSet && + view.ImeWindowSet && view.KeyEventsEnabledSet && + view.SupportVideoHoleSet && view.OnJavaScriptAlert && + view.OnJavaScriptConfirm && view.OnJavaScriptPrompt && + view.JavaScriptAlertReply && view.JavaScriptConfirmReply && + view.JavaScriptPromptReply && main.SetArguments && + settings.ImePanelEnabledSet && settings.ForceZoomSet && + console_message.LevelGet && console_message.TextGet; +} diff --git a/packages/flutter_inappwebview/tizen/src/ewk_internal_api_binding.h b/packages/flutter_inappwebview/tizen/src/ewk_internal_api_binding.h new file mode 100644 index 000000000..8d92fcdd4 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/ewk_internal_api_binding.h @@ -0,0 +1,173 @@ +// Copyright 2026 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; + +typedef enum { + EWK_MOUSE_BUTTON_LEFT = 1, + EWK_MOUSE_BUTTON_MIDDLE = 2, + EWK_MOUSE_BUTTON_RIGHT = 3 +} Ewk_Mouse_Button_Type; + +struct _Ewk_Touch_Point { + int id; + int x; + int y; + Evas_Touch_Point_State state; +}; + +typedef Eina_Bool (*Ewk_View_JavaScript_Alert_Callback)(Evas_Object* o, + const char* alert_text, + void* user_data); +typedef Eina_Bool (*Ewk_View_JavaScript_Confirm_Callback)(Evas_Object* o, + const char* message, + void* user_data); +typedef Eina_Bool (*Ewk_View_JavaScript_Prompt_Callback)( + Evas_Object* o, const char* message, const char* default_value, + void* user_data); + +typedef Eina_Bool (*EwkViewBgColorSetFnPtr)(Evas_Object* obj, int r, int g, + int b, int a); +typedef Eina_Bool (*EwkViewTouchEventsEnabledSetFnPtr)(Evas_Object* view, + Eina_Bool enabled); +typedef Eina_Bool (*EwkViewFeedTouchEventFnPtr)(Evas_Object* obj, + Ewk_Touch_Event_Type type, + const Eina_List* points, + const Evas_Modifier* modifiers); +typedef Eina_Bool (*EwkViewMouseEventsEnabledSetFnPtr)(Evas_Object* view, + Eina_Bool enabled); +typedef Eina_Bool (*EwkViewFeedMouseDownFnPtr)(Evas_Object* obj, + Ewk_Mouse_Button_Type button, + int x, int y); +typedef Eina_Bool (*EwkViewFeedMouseUpFnPtr)(Evas_Object* obj, + Ewk_Mouse_Button_Type button, + int x, int y); +typedef Eina_Bool (*EwkViewFeedMouseWheelFnPtr)(Evas_Object* obj, + Eina_Bool y_direction, int step, + int x, int y); +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 Eina_Bool (*EwkViewSupportVideoHoleSetFnPtr)(Evas_Object* obj, + void* window, + Eina_Bool enabled, + Eina_Bool is_supported); +typedef void (*EwkViewJavaScriptAlertCallbackSetFnPtr)( + Evas_Object* o, Ewk_View_JavaScript_Alert_Callback callback, + void* user_data); +typedef void (*EwkViewJavaScriptConfirmCallbackSetFnPtr)( + Evas_Object* o, Ewk_View_JavaScript_Confirm_Callback callback, + void* user_data); +typedef void (*EwkViewJavaScriptPromptCallbackSetFnPtr)( + Evas_Object* o, Ewk_View_JavaScript_Prompt_Callback callback, + void* user_data); +typedef void (*EwkViewJavaScriptAlertReplyFnPtr)(Evas_Object* o); +typedef void (*EwkViewJavaScriptConfirmReplyFnPtr)(Evas_Object* o, + Eina_Bool result); +typedef void (*EwkViewJavaScriptPromptReplyFnPtr)(Evas_Object* o, + const char* result); + +typedef struct { + EwkViewBgColorSetFnPtr SetBackgroundColor = nullptr; + EwkViewTouchEventsEnabledSetFnPtr TouchEventsEnabledSet = nullptr; + EwkViewFeedTouchEventFnPtr FeedTouchEvent = nullptr; + EwkViewMouseEventsEnabledSetFnPtr MouseEventsEnabledSet = nullptr; + EwkViewFeedMouseDownFnPtr FeedMouseDown = nullptr; + EwkViewFeedMouseUpFnPtr FeedMouseUp = nullptr; + EwkViewFeedMouseWheelFnPtr FeedMouseWheel = nullptr; + EwkViewSendKeyEventFnPtr SendKeyEvent = nullptr; + EwkViewOffscreenRenderingEnabledSetFnPtr OffscreenRenderingEnabledSet = + nullptr; + EwkViewImeWindowSetFnPtr ImeWindowSet = nullptr; + EwkViewKeyEventsEnabledSetFnPtr KeyEventsEnabledSet = nullptr; + EwkViewSupportVideoHoleSetFnPtr SupportVideoHoleSet = nullptr; + EwkViewJavaScriptAlertCallbackSetFnPtr OnJavaScriptAlert = nullptr; + EwkViewJavaScriptConfirmCallbackSetFnPtr OnJavaScriptConfirm = nullptr; + EwkViewJavaScriptPromptCallbackSetFnPtr OnJavaScriptPrompt = nullptr; + EwkViewJavaScriptAlertReplyFnPtr JavaScriptAlertReply = nullptr; + EwkViewJavaScriptConfirmReplyFnPtr JavaScriptConfirmReply = nullptr; + EwkViewJavaScriptPromptReplyFnPtr JavaScriptPromptReply = 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 void (*EwkSettingsForceZoomSetFnPtr)(Ewk_Settings* settings, + Eina_Bool enable); + +typedef struct { + EwkSettingsImePanelEnabledSetFnPtr ImePanelEnabledSet = nullptr; + EwkSettingsForceZoomSetFnPtr ForceZoomSet = 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 struct { + EwkConsoleMessageLevelGetFnPtr LevelGet = nullptr; + EwkConsoleMessageTextGetFnPtr TextGet = 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/flutter_inappwebview/tizen/src/flutter_inappwebview_tizen_plugin.cc b/packages/flutter_inappwebview/tizen/src/flutter_inappwebview_tizen_plugin.cc new file mode 100644 index 000000000..fd77a5be7 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/flutter_inappwebview_tizen_plugin.cc @@ -0,0 +1,126 @@ +// Copyright 2026 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_inappwebview_tizen_plugin.h" + +#include +#include +#include +#include + +#include + +#include "webview.h" +#include "webview_factory.h" + +namespace { + +constexpr char kViewType[] = "com.pichillilorenzo/flutter_inappwebview"; +constexpr char kManagerChannelName[] = + "com.pichillilorenzo/flutter_inappwebview_manager"; +constexpr char kCookieChannelName[] = + "com.pichillilorenzo/flutter_inappwebview_cookiemanager"; + +class FlutterInappwebviewTizenPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto manager_channel = + std::make_unique>( + registrar->messenger(), kManagerChannelName, + &flutter::StandardMethodCodec::GetInstance()); + auto cookie_channel = + std::make_unique>( + registrar->messenger(), kCookieChannelName, + &flutter::StandardMethodCodec::GetInstance()); + auto plugin = std::make_unique( + std::move(manager_channel), std::move(cookie_channel)); + registrar->AddPlugin(std::move(plugin)); + } + + FlutterInappwebviewTizenPlugin( + std::unique_ptr> + manager_channel, + std::unique_ptr> + cookie_channel) + : manager_channel_(std::move(manager_channel)), + cookie_channel_(std::move(cookie_channel)) { + manager_channel_->SetMethodCallHandler( + [this](const auto& call, auto result) { + HandleManagerMethodCall(call, std::move(result)); + }); + cookie_channel_->SetMethodCallHandler( + [this](const auto& call, auto result) { + HandleCookieMethodCall(call, std::move(result)); + }); + } + + virtual ~FlutterInappwebviewTizenPlugin() {} + + private: + void HandleManagerMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + const std::string& method_name = method_call.method_name(); + const auto* arguments = method_call.arguments(); + + if (method_name == "clearAllCache") { + WebView::ClearAllCache(); + result->Success(); + } else if (method_name == "handlesURLScheme") { + std::string url_scheme; + if (auto* map = std::get_if(arguments)) { + auto it = map->find(flutter::EncodableValue("urlScheme")); + if (it != map->end()) { + if (auto* value = std::get_if(&it->second)) { + url_scheme = *value; + } + } + } + const bool is_supported = url_scheme == "http" || url_scheme == "https" || + url_scheme == "file" || url_scheme == "data" || + url_scheme == "about" || + url_scheme == "javascript"; + result->Success(flutter::EncodableValue(is_supported)); + } else if (method_name == "getDefaultUserAgent") { + // Returns the cached EWK user agent. The value is captured the first + // time an InAppWebView is created in this process; before that the + // returned string is empty. + result->Success(flutter::EncodableValue(WebView::GetDefaultUserAgent())); + } else { + result->NotImplemented(); + } + } + + void HandleCookieMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + const std::string& method_name = method_call.method_name(); + if (method_name == "deleteAllCookies") { + result->Success(flutter::EncodableValue(WebView::ClearAllCookies())); + } else { + result->NotImplemented(); + } + } + + std::unique_ptr> + manager_channel_; + std::unique_ptr> + cookie_channel_; +}; + +} // namespace + +void FlutterInappwebviewTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef core_registrar) { + flutter::PluginRegistrar* registrar = + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(core_registrar); + FlutterDesktopViewRef view = + FlutterDesktopPluginRegistrarGetView(core_registrar); + FlutterDesktopRegisterViewFactory( + core_registrar, kViewType, + std::make_unique( + registrar, FlutterDesktopViewGetNativeHandle(view))); + FlutterInappwebviewTizenPlugin::RegisterWithRegistrar(registrar); +} diff --git a/packages/flutter_inappwebview/tizen/src/log.h b/packages/flutter_inappwebview/tizen/src/log.h new file mode 100644 index 000000000..4ddedaa55 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/log.h @@ -0,0 +1,28 @@ +// Copyright 2026 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 __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "WebviewFlutterTizenPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc new file mode 100644 index 000000000..a9511da5b --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -0,0 +1,1168 @@ +// Copyright 2026 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 "webview_factory.h" + +struct WebViewLifetimeState { + std::atomic_bool disposed = false; +}; + +namespace { + +constexpr char kInAppWebViewChannelName[] = + "com.pichillilorenzo/flutter_inappwebview_"; +constexpr int kConsoleMessageLog = 1; +constexpr int kConsoleMessageWarning = 2; +constexpr int kConsoleMessageError = 3; +constexpr int kConsoleMessageDebug = 4; +constexpr int kConsoleMessageInfo = 0; +constexpr int kWebResourceErrorUnknown = -1; + +int ConvertLogLevel(Ewk_Console_Message_Level level) { + switch (level) { + case EWK_CONSOLE_MESSAGE_LEVEL_NULL: + case EWK_CONSOLE_MESSAGE_LEVEL_LOG: + return kConsoleMessageLog; + case EWK_CONSOLE_MESSAGE_LEVEL_WARNING: + return kConsoleMessageWarning; + case EWK_CONSOLE_MESSAGE_LEVEL_ERROR: + return kConsoleMessageError; + case EWK_CONSOLE_MESSAGE_LEVEL_DEBUG: + return kConsoleMessageDebug; + case EWK_CONSOLE_MESSAGE_LEVEL_INFO: + return kConsoleMessageInfo; + default: + return kConsoleMessageLog; + } +} + +class NavigationRequestResult : public FlMethodResult { + public: + NavigationRequestResult(WebView* webview, + std::weak_ptr lifetime) + : webview_(webview), lifetime_(std::move(lifetime)) {} + + void SuccessInternal(const flutter::EncodableValue* should_load) override { + if (!IsWebViewAlive()) { + return; + } + // The Dart side returns NavigationActionPolicy.toNativeValue(): + // 0 = CANCEL, 1 = ALLOW. Treat anything else as a cancel. + if (should_load && std::holds_alternative(*should_load) && + std::get(*should_load) == 1) { + webview_->ResumeNavigation(); + return; + } + webview_->StopNavigation(); + } + + void ErrorInternal(const std::string& error_code, + const std::string& error_message, + const flutter::EncodableValue* error_details) override { + LOG_ERROR("The shouldOverrideUrlLoading reply errored: %s", + error_message.c_str()); + if (!IsWebViewAlive()) { + return; + } + webview_->StopNavigation(); + } + + void NotImplementedInternal() override { + LOG_ERROR("The shouldOverrideUrlLoading reply was unimplemented."); + if (!IsWebViewAlive()) { + return; + } + webview_->StopNavigation(); + } + + private: + bool IsWebViewAlive() { + auto lifetime = lifetime_.lock(); + return lifetime && !lifetime->disposed.load(); + } + + WebView* webview_; + std::weak_ptr lifetime_; +}; + +template +bool GetValueFromEncodableMap(const flutter::EncodableValue* arguments, + std::string key, T* out) { + if (auto* map = std::get_if(arguments)) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto* value = std::get_if(&iter->second)) { + *out = *value; + return true; + } + } + } + return false; +} + +template +bool GetValueFromEncodableMap(const flutter::EncodableMap& arguments, + std::string key, T* out) { + auto iter = arguments.find(flutter::EncodableValue(key)); + if (iter != arguments.end() && !iter->second.IsNull()) { + if (auto* value = std::get_if(&iter->second)) { + *out = *value; + return true; + } + } + return false; +} + +flutter::EncodableMap CreateRequestMap(const std::string& url, + const std::string& method = "GET") { + flutter::EncodableMap map; + map[flutter::EncodableValue("url")] = flutter::EncodableValue(url); + map[flutter::EncodableValue("headers")] = + flutter::EncodableValue(flutter::EncodableMap()); + map[flutter::EncodableValue("method")] = flutter::EncodableValue(method); + map[flutter::EncodableValue("hasGesture")] = flutter::EncodableValue(false); + map[flutter::EncodableValue("isForMainFrame")] = + flutter::EncodableValue(true); + map[flutter::EncodableValue("isRedirect")] = flutter::EncodableValue(false); + return map; +} + +flutter::EncodableMap CreateNavigationActionMap(const std::string& url) { + flutter::EncodableMap map; + map[flutter::EncodableValue("hasGesture")] = flutter::EncodableValue(false); + map[flutter::EncodableValue("isForMainFrame")] = + flutter::EncodableValue(true); + map[flutter::EncodableValue("isRedirect")] = flutter::EncodableValue(false); + map[flutter::EncodableValue("navigationType")] = flutter::EncodableValue(); + map[flutter::EncodableValue("request")] = + flutter::EncodableValue(CreateRequestMap(url)); + map[flutter::EncodableValue("shouldPerformDownload")] = + flutter::EncodableValue(false); + map[flutter::EncodableValue("sourceFrame")] = flutter::EncodableValue(); + map[flutter::EncodableValue("targetFrame")] = flutter::EncodableValue(); + return map; +} + +std::string GetViewUrl(Evas_Object* webview_instance) { + const char* url = ewk_view_url_get(webview_instance); + return url ? std::string(url) : std::string(); +} + +flutter::EncodableMap CreateErrorMap( + const std::string& description, int error_code = kWebResourceErrorUnknown) { + flutter::EncodableMap map; + map[flutter::EncodableValue("description")] = + flutter::EncodableValue(description); + map[flutter::EncodableValue("errorCode")] = + flutter::EncodableValue(error_code); + map[flutter::EncodableValue("type")] = flutter::EncodableValue(error_code); + return map; +} + +} // namespace + +std::set WebView::instances_; +std::mutex WebView::instances_mutex_; +std::string WebView::default_user_agent_; + +void WebView::ClearAllCache() { + std::lock_guard lock(instances_mutex_); + for (auto* instance : instances_) { + if (!instance || !instance->webview_instance_) { + continue; + } + Ewk_Context* context = ewk_view_context_get(instance->webview_instance_); + if (context) { + ewk_context_resource_cache_clear(context); + } + } +} + +bool WebView::ClearAllCookies() { + std::lock_guard lock(instances_mutex_); + for (auto* instance : instances_) { + if (!instance || !instance->webview_instance_) { + continue; + } + // EWK views in this plugin share the default context, so any live view can + // provide the process-wide cookie manager. + Ewk_Context* context = ewk_view_context_get(instance->webview_instance_); + Ewk_Cookie_Manager* cookie_manager = + context ? ewk_context_cookie_manager_get(context) : nullptr; + if (!cookie_manager) { + continue; + } + ewk_cookie_manager_cookies_clear(cookie_manager); + return true; + } + return false; +} + +std::string WebView::GetDefaultUserAgent() { + std::lock_guard lock(instances_mutex_); + for (auto* instance : instances_) { + if (!instance || !instance->webview_instance_) { + continue; + } + const char* user_agent = + ewk_view_user_agent_get(instance->webview_instance_); + if (user_agent) { + return std::string(user_agent); + } + } + // Fall back to the value cached during the first WebView creation. Empty if + // no InAppWebView has ever been created in this process. + return default_user_agent_; +} + +WebView::WebView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params, + void* window) + : PlatformView(registrar, view_id, nullptr), + texture_registrar_(texture_registrar), + width_(width), + height_(height), + window_(window), + lifetime_(std::make_shared()) { + if (!EwkInternalApiBinding::GetInstance().Initialize()) { + LOG_ERROR("Failed to initialize EWK internal APIs."); + return; + } + + tbm_pool_ = std::make_unique(width, height); + + texture_variant_ = + std::make_unique(flutter::GpuSurfaceTexture( + kFlutterDesktopGpuSurfaceTypeNone, + [this](size_t width, + size_t height) -> const FlutterDesktopGpuSurfaceDescriptor* { + return ObtainGpuSurface(width, height); + })); + int64_t texture_id = + texture_registrar_->RegisterTexture(texture_variant_.get()); + if (texture_id < 0) { + LOG_ERROR("Failed to register the WebView texture."); + return; + } + SetTextureId(static_cast(texture_id)); + texture_registered_ = true; + + webview_channel_ = std::make_unique( + GetPluginRegistrar()->messenger(), GetWebViewChannelName(), + &flutter::StandardMethodCodec::GetInstance()); + webview_channel_->SetMethodCallHandler( + [webview = this](const auto& call, auto result) { + webview->HandleWebViewMethodCall(call, std::move(result)); + }); + + if (!InitWebView()) { + LOG_ERROR("Failed to initialize EWK webview instance."); + return; + } + + { + std::lock_guard lock(instances_mutex_); + if (default_user_agent_.empty()) { + const char* user_agent = ewk_view_user_agent_get(webview_instance_); + if (user_agent) { + default_user_agent_ = std::string(user_agent); + } + } + instances_.insert(this); + } + + initialized_ = true; + ApplyInitialParams(params); +} + +WebView::~WebView() { Dispose(); } + +std::string WebView::GetWebViewChannelName() { + return std::string(kInAppWebViewChannelName) + std::to_string(GetViewId()); +} + +void WebView::ResumeNavigation() { + if (disposed_ || !webview_instance_) { + return; + } + ewk_view_resume(webview_instance_); +} + +void WebView::StopNavigation() { + if (disposed_ || !webview_instance_) { + return; + } + ewk_view_stop(webview_instance_); +} + +void WebView::Dispose() { + if (disposed_) { + return; + } + disposed_ = true; + lifetime_->disposed.store(true); + + { + std::lock_guard lock(instances_mutex_); + instances_.erase(this); + } + + if (webview_channel_) { + webview_channel_->SetMethodCallHandler(nullptr); + } + + if (texture_registered_) { + texture_registrar_->UnregisterTexture(GetTextureId(), nullptr); + texture_registered_ = false; + } + + if (webview_instance_) { + // The view may still be suspended while waiting on a Dart navigation + // reply that will never arrive. Resume so destruction does not stall. + ewk_view_resume(webview_instance_); + + 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,progress", + &WebView::OnProgress); + 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); + evas_object_smart_callback_del(webview_instance_, "url,changed", + &WebView::OnUrlChange); + auto& ewk_view = EwkInternalApiBinding::GetInstance().view; + if (ewk_view.OnJavaScriptAlert) { + ewk_view.OnJavaScriptAlert(webview_instance_, nullptr, nullptr); + } + if (ewk_view.OnJavaScriptConfirm) { + ewk_view.OnJavaScriptConfirm(webview_instance_, nullptr, nullptr); + } + if (ewk_view.OnJavaScriptPrompt) { + ewk_view.OnJavaScriptPrompt(webview_instance_, nullptr, nullptr); + } + evas_object_del(webview_instance_); + webview_instance_ = nullptr; + } + if (ecore_evas_) { + ecore_evas_free(ecore_evas_); + ecore_evas_ = nullptr; + } + + // ewk_shutdown(); +} + +void WebView::Offset(double left, double top) { + left_ = left; + top_ = top; + + evas_object_move(webview_instance_, static_cast(left_), + static_cast(top_)); +} + +void WebView::Resize(double width, double height) { + { + std::lock_guard lock(mutex_); + width_ = width; + height_ = height; + + if (working_surface_) { + tbm_pool_->Release(working_surface_); + working_surface_ = nullptr; + } + if (candidate_surface_) { + tbm_pool_->Release(candidate_surface_); + candidate_surface_ = nullptr; + } + rendered_surface_ = nullptr; + tbm_pool_->Prepare(width_, height_); + } + + evas_object_resize(webview_instance_, width_, height_); +} + +void WebView::Touch(int event_type, int button_type, double x, double y, + double dx, double dy) { +#ifdef WEBVIEW_TIZEN_TOUCH_EVENTS_ENABLED + SendTouchEvent(event_type, x, y); +#else + SendMouseEvent(event_type, button_type, x, y, dx, dy); +#endif +} + +void WebView::SendTouchEvent(int event_type, double x, double y) { + Ewk_Touch_Event_Type mouse_event_type = EWK_TOUCH_START; + Evas_Touch_Point_State state = EVAS_TOUCH_POINT_DOWN; + if (event_type == 0) { // down event + mouse_event_type = EWK_TOUCH_START; + state = EVAS_TOUCH_POINT_DOWN; + } else if (event_type == 1) { // move event + mouse_event_type = EWK_TOUCH_MOVE; + state = EVAS_TOUCH_POINT_MOVE; + } else if (event_type == 2) { // up event + mouse_event_type = EWK_TOUCH_END; + state = EVAS_TOUCH_POINT_UP; + } else { + LOG_WARN("Unknown touch event type: %d", event_type); + } + + Eina_List* points = 0; + Ewk_Touch_Point* point = new Ewk_Touch_Point; + point->id = 0; + point->x = x + left_; + point->y = y + top_; + point->state = state; + points = eina_list_append(points, point); + + EwkInternalApiBinding::GetInstance().view.FeedTouchEvent( + webview_instance_, mouse_event_type, points, 0); + eina_list_free(points); + delete point; +} + +void WebView::SendMouseEvent(int event_type, int button_type, double x, + double y, double dx, double dy) { + Ewk_Mouse_Button_Type mouse_button_type = (Ewk_Mouse_Button_Type)0; + switch (button_type) { + case 1: + mouse_button_type = EWK_MOUSE_BUTTON_LEFT; + break; + case 2: + mouse_button_type = EWK_MOUSE_BUTTON_RIGHT; + break; + case 4: + mouse_button_type = EWK_MOUSE_BUTTON_MIDDLE; + break; + } + + int px = x + left_; + int py = y + top_; + + if (event_type == 0) { // down event + mouse_button_type_ = mouse_button_type; + EwkInternalApiBinding::GetInstance().view.FeedMouseDown( + webview_instance_, mouse_button_type_, px, py); + } else if (event_type == 1) { + if (dy != 0) { + EwkInternalApiBinding::GetInstance().view.FeedMouseWheel( + webview_instance_, true, dy > 0 ? 1 : -1, px, py); + } + } else if (event_type == 2) { // up event + EwkInternalApiBinding::GetInstance().view.FeedMouseUp( + webview_instance_, mouse_button_type_, px, py); + mouse_button_type_ = mouse_button_type; + } else { + LOG_WARN("Unknown mouse event type: %d", event_type); + } +} + +bool WebView::SendKey(const char* key, const char* string, const char* compose, + uint32_t modifiers, uint32_t scan_code, bool is_down) { + if (!IsFocused()) { + return false; + } + + if (strcmp(key, "XF86Exit") == 0 && !is_down) { + return false; + } + + if (strcmp(key, "XF86Back") == 0 && !is_down) { + if (ewk_view_back_possible(webview_instance_)) { + ewk_view_back(webview_instance_); + return true; + } + return false; + } + + if (is_down) { + // TODO(swift-kim): Deal with other members of the structure. + Evas_Event_Key_Down down_event = {}; + down_event.key = key; + down_event.string = string; + EwkInternalApiBinding::GetInstance().view.SendKeyEvent( + webview_instance_, &down_event, is_down); + } else { + Evas_Event_Key_Up up_event = {}; + up_event.key = key; + up_event.string = string; + EwkInternalApiBinding::GetInstance().view.SendKeyEvent(webview_instance_, + &up_event, is_down); + } + return true; +} + +void WebView::SetDirection(int direction) { + // TODO: Implement if necessary. +} + +bool WebView::InitWebView() { + static std::once_flag ewk_args_once; + std::call_once(ewk_args_once, []() { + char* chromium_argv[] = { + const_cast("--disable-pinch"), + const_cast("--js-flags=--expose-gc"), + const_cast("--single-process"), + const_cast("--no-zygote"), + }; + int chromium_argc = sizeof(chromium_argv) / sizeof(chromium_argv[0]); + EwkInternalApiBinding::GetInstance().main.SetArguments(chromium_argc, + chromium_argv); + }); + + // TODO(jsuya): ewk_init() and ewk_shutdown() are designed to be called only + // once in a process.(If ewk_init() is called after ewk_shutdown() is + // called, SIGTRAP is called internally.) ewk_init() initializes the efl + // modules and web engine's arguments data. The efl modules are initialized + // by default in OS, and arguments data is also initialized through + // SetArguments() API, so calling ewk_init() is not necessary. Therefore, + // temporarily comment out ewk_init() and ewk_shutdown(). It can be reverted + // depending on updates to chromium-efl. + // ewk_init(); + ecore_evas_ = ecore_evas_new("wayland_egl", 0, 0, 1, 1, 0); + if (!ecore_evas_) { + LOG_ERROR("Failed to create Ecore_Evas for the WebView."); + return false; + } + + webview_instance_ = ewk_view_add(ecore_evas_get(ecore_evas_)); + if (!webview_instance_) { + ecore_evas_free(ecore_evas_); + ecore_evas_ = nullptr; + return false; + } + ecore_evas_focus_set(ecore_evas_, true); + ewk_view_focus_set(webview_instance_, true); + EwkInternalApiBinding::GetInstance().view.OffscreenRenderingEnabledSet( + webview_instance_, true); + + Ewk_Context* context = ewk_view_context_get(webview_instance_); + if (context) { + Ewk_Cookie_Manager* cookie_manager = + ewk_context_cookie_manager_get(context); + if (cookie_manager) { + ewk_cookie_manager_accept_policy_set( + cookie_manager, EWK_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY); + } + ewk_context_cache_model_set(context, EWK_CACHE_MODEL_PRIMARY_WEBBROWSER); + } else { + LOG_WARN("Unable to access the EWK context; skipping cookie/cache setup."); + } + + EwkInternalApiBinding::GetInstance().settings.ImePanelEnabledSet( + ewk_view_settings_get(webview_instance_), true); + EwkInternalApiBinding::GetInstance().settings.ForceZoomSet( + ewk_view_settings_get(webview_instance_), true); + EwkInternalApiBinding::GetInstance().view.ImeWindowSet(webview_instance_, + window_); + EwkInternalApiBinding::GetInstance().view.KeyEventsEnabledSet( + webview_instance_, true); +#ifdef WEBVIEW_TIZEN_TOUCH_EVENTS_ENABLED + EwkInternalApiBinding::GetInstance().view.TouchEventsEnabledSet( + webview_instance_, true); + EwkInternalApiBinding::GetInstance().view.MouseEventsEnabledSet( + webview_instance_, false); +#else + EwkInternalApiBinding::GetInstance().view.TouchEventsEnabledSet( + webview_instance_, false); + EwkInternalApiBinding::GetInstance().view.MouseEventsEnabledSet( + webview_instance_, true); +#endif + + EwkInternalApiBinding::GetInstance().view.OnJavaScriptAlert( + webview_instance_, &WebView::OnJavaScriptAlertDialog, this); + EwkInternalApiBinding::GetInstance().view.OnJavaScriptConfirm( + webview_instance_, &WebView::OnJavaScriptConfirmDialog, this); + EwkInternalApiBinding::GetInstance().view.OnJavaScriptPrompt( + webview_instance_, &WebView::OnJavaScriptPromptDialog, this); + +#ifdef TV_PROFILE + EwkInternalApiBinding::GetInstance().view.SupportVideoHoleSet( + webview_instance_, window_, true, false); +#endif + + 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,progress", + &WebView::OnProgress, 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); + evas_object_smart_callback_add(webview_instance_, "url,changed", + &WebView::OnUrlChange, this); + + Resize(width_, height_); + evas_object_show(webview_instance_); + + return true; +} + +template +void WebView::SetBackgroundColor(const T& color) { + EwkInternalApiBinding::GetInstance().view.SetBackgroundColor( + webview_instance_, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, + color >> 24 & 0xff); +} + +void WebView::ApplySettings(const flutter::EncodableMap& settings) { + bool bool_value = false; + if (GetValueFromEncodableMap(settings, "javaScriptEnabled", &bool_value)) { + ewk_settings_javascript_enabled_set( + ewk_view_settings_get(webview_instance_), bool_value); + } + + if (GetValueFromEncodableMap(settings, "supportZoom", &bool_value)) { + EwkInternalApiBinding::GetInstance().settings.ForceZoomSet( + ewk_view_settings_get(webview_instance_), bool_value); + } + + if (GetValueFromEncodableMap(settings, "useShouldOverrideUrlLoading", + &bool_value)) { + has_navigation_delegate_ = bool_value; + } + + std::string user_agent; + if (GetValueFromEncodableMap(settings, "userAgent", &user_agent) && + !user_agent.empty()) { + ewk_view_user_agent_set(webview_instance_, user_agent.c_str()); + } + + if (GetValueFromEncodableMap(settings, "transparentBackground", + &bool_value) && + bool_value) { + SetBackgroundColor(static_cast(0x00000000)); + } +} + +void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { + const auto* creation_params = std::get_if(¶ms); + if (!creation_params) { + return; + } + + flutter::EncodableMap initial_settings; + if (GetValueFromEncodableMap(*creation_params, "initialSettings", + &initial_settings)) { + ApplySettings(initial_settings); + } + + std::string initial_file; + if (GetValueFromEncodableMap(*creation_params, "initialFile", + &initial_file) && + !initial_file.empty()) { + char* res_path = app_get_resource_path(); + if (res_path) { + std::string url = + std::string("file://") + res_path + "flutter_assets/" + initial_file; + free(res_path); + ewk_view_url_set(webview_instance_, url.c_str()); + return; + } + } + + flutter::EncodableMap initial_data; + if (GetValueFromEncodableMap(*creation_params, "initialData", + &initial_data)) { + std::string data; + std::string base_url = "about:blank"; + if (GetValueFromEncodableMap(initial_data, "data", &data)) { + GetValueFromEncodableMap(initial_data, "baseUrl", &base_url); + ewk_view_html_string_load(webview_instance_, data.c_str(), + base_url.c_str(), nullptr); + return; + } + } + + flutter::EncodableMap url_request; + if (GetValueFromEncodableMap(*creation_params, "initialUrlRequest", + &url_request)) { + std::string url; + if (GetValueFromEncodableMap(url_request, "url", &url) && !url.empty()) { + ewk_view_url_set(webview_instance_, url.c_str()); + } + } +} + +void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, + std::unique_ptr result) { + const std::string& method_name = method_call.method_name(); + const flutter::EncodableValue* arguments = method_call.arguments(); + + if (!webview_instance_) { + result->Error("Invalid operation", + "The webview instance has not been initialized."); + return; + } + + if (method_name == "loadUrl") { + flutter::EncodableMap url_request; + if (!GetValueFromEncodableMap(arguments, "urlRequest", &url_request)) { + result->Error("Invalid argument", "No urlRequest provided."); + return; + } + + std::string url; + if (!GetValueFromEncodableMap(url_request, "url", &url)) { + result->Error("Invalid argument", "No url provided."); + return; + } + + std::string method = "GET"; + GetValueFromEncodableMap(url_request, "method", &method); + flutter::EncodableMap headers; + GetValueFromEncodableMap(url_request, "headers", &headers); + std::vector body; + GetValueFromEncodableMap(url_request, "body", &body); + + if (method == "POST" || !headers.empty() || !body.empty()) { + Eina_Hash* ewk_headers = eina_hash_new( + [](const void* key) -> unsigned int { + return key ? strlen(static_cast(key)) + 1 : 0; + }, + [](const void* key1, int key1_length, const void* key2, + int key2_length) -> int { + return strcmp(static_cast(key1), + static_cast(key2)); + }, + EINA_KEY_HASH(eina_hash_superfast), [](void* data) { free(data); }, + 10); + for (const auto& header : headers) { + auto key = std::get_if(&header.first); + auto value = std::get_if(&header.second); + if (key && value) { + eina_hash_add(ewk_headers, key->c_str(), strdup(value->c_str())); + } + } + if (!body.empty()) { + body.push_back('\0'); + } + const auto ewk_method = + method == "POST" ? EWK_HTTP_METHOD_POST : EWK_HTTP_METHOD_GET; + bool ret = ewk_view_url_request_set( + webview_instance_, url.c_str(), ewk_method, ewk_headers, + body.empty() ? nullptr : reinterpret_cast(body.data())); + eina_hash_free(ewk_headers); + if (!ret) { + result->Error("Operation failed", "Failed to load URL request."); + return; + } + } else { + ewk_view_url_set(webview_instance_, url.c_str()); + } + result->Success(); + } else if (method_name == "postUrl") { + std::string url; + std::vector body; + if (!GetValueFromEncodableMap(arguments, "url", &url)) { + result->Error("Invalid argument", "No url provided."); + return; + } + GetValueFromEncodableMap(arguments, "postData", &body); + if (!body.empty()) { + body.push_back('\0'); + } + const bool ret = ewk_view_url_request_set( + webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, + body.empty() ? nullptr : reinterpret_cast(body.data())); + if (ret) { + result->Success(); + } else { + result->Error("Operation failed", "Failed to submit POST request."); + } + } else if (method_name == "loadData") { + std::string data, base_url; + if (!GetValueFromEncodableMap(arguments, "data", &data)) { + result->Error("Invalid argument", "No data provided."); + return; + } + GetValueFromEncodableMap(arguments, "baseUrl", &base_url); + ewk_view_html_string_load(webview_instance_, data.c_str(), base_url.c_str(), + nullptr); + result->Success(); + } else if (method_name == "loadFile") { + std::string file_path; + if (!GetValueFromEncodableMap(arguments, "assetFilePath", &file_path)) { + result->Error("Invalid argument", "No assetFilePath provided."); + return; + } + std::string url; + if (!file_path.empty() && file_path[0] == '/') { + url = std::string("file://") + file_path; + } else { + char* res_path = app_get_resource_path(); + if (!res_path) { + result->Error("Operation failed", "Could not get app resource path."); + return; + } + url = std::string("file://") + res_path + "flutter_assets/" + file_path; + free(res_path); + } + ewk_view_url_set(webview_instance_, url.c_str()); + result->Success(); + } else if (method_name == "canGoBack") { + result->Success(flutter::EncodableValue( + static_cast(ewk_view_back_possible(webview_instance_)))); + } else if (method_name == "canGoForward") { + result->Success(flutter::EncodableValue( + static_cast(ewk_view_forward_possible(webview_instance_)))); + } else if (method_name == "goBack") { + ewk_view_back(webview_instance_); + result->Success(); + } else if (method_name == "goForward") { + ewk_view_forward(webview_instance_); + result->Success(); + } else if (method_name == "reload") { + ewk_view_reload(webview_instance_); + result->Success(); + } else if (method_name == "getUrl") { + const char* url = ewk_view_url_get(webview_instance_); + result->Success(url ? flutter::EncodableValue(url) + : flutter::EncodableValue()); + } else if (method_name == "getTitle") { + const char* title = ewk_view_title_get(webview_instance_); + result->Success(title ? flutter::EncodableValue(std::string(title)) + : flutter::EncodableValue()); + } else if (method_name == "getProgress") { + const int progress = + static_cast(ewk_view_load_progress_get(webview_instance_) * 100); + result->Success(flutter::EncodableValue(progress)); + } else if (method_name == "stopLoading") { + ewk_view_stop(webview_instance_); + result->Success(); + } else if (method_name == "evaluateJavascript") { + std::string javascript; + if (!GetValueFromEncodableMap(arguments, "source", &javascript)) { + result->Error("Invalid argument", "No source provided."); + return; + } + // Only release ownership when ewk accepts the request; otherwise reply + // synchronously so the caller is not left waiting and the result is freed. + if (ewk_view_script_execute(webview_instance_, javascript.c_str(), + &WebView::OnEvaluateJavaScript, result.get())) { + result.release(); + } else { + result->Error("Operation failed", "Failed to execute JavaScript."); + } + } else if (method_name == "clearCache") { + Ewk_Context* context = ewk_view_context_get(webview_instance_); + ewk_context_resource_cache_clear(context); + result->Success(); + } else if (method_name == "scrollTo" || method_name == "scrollBy") { + int32_t x = 0, y = 0; + if (!GetValueFromEncodableMap(arguments, "x", &x) || + !GetValueFromEncodableMap(arguments, "y", &y)) { + result->Error("Invalid argument", "No x or y provided."); + return; + } + if (method_name == "scrollTo") { + ewk_view_scroll_set(webview_instance_, x, y); + } else { + ewk_view_scroll_by(webview_instance_, x, y); + } + int32_t new_x = 0, new_y = 0; + ewk_view_scroll_pos_get(webview_instance_, &new_x, &new_y); + flutter::EncodableMap args = { + {flutter::EncodableValue("x"), flutter::EncodableValue(new_x)}, + {flutter::EncodableValue("y"), flutter::EncodableValue(new_y)}, + }; + webview_channel_->InvokeMethod( + "onScrollChanged", std::make_unique(args)); + result->Success(); + } else if (method_name == "getScrollX" || method_name == "getScrollY") { + int32_t x = 0, y = 0; + ewk_view_scroll_pos_get(webview_instance_, &x, &y); + result->Success( + flutter::EncodableValue(method_name == "getScrollX" ? x : y)); + } else if (method_name == "zoomBy") { + double zoom_factor = 1.0; + if (!GetValueFromEncodableMap(arguments, "zoomFactor", &zoom_factor)) { + result->Error("Invalid argument", "No zoomFactor provided."); + return; + } + const double old_scale = ewk_view_scale_get(webview_instance_); + const double new_scale = old_scale * zoom_factor; + ewk_view_scale_set(webview_instance_, new_scale, 0, 0); + flutter::EncodableMap args = { + {flutter::EncodableValue("oldScale"), + flutter::EncodableValue(old_scale)}, + {flutter::EncodableValue("newScale"), + flutter::EncodableValue(new_scale)}, + }; + webview_channel_->InvokeMethod( + "onZoomScaleChanged", std::make_unique(args)); + result->Success(); + } else if (method_name == "setSettings") { + flutter::EncodableMap settings; + if (GetValueFromEncodableMap(arguments, "settings", &settings)) { + ApplySettings(settings); + result->Success(); + } else { + result->Error("Invalid argument", "No settings provided."); + } + } else if (method_name == "javaScriptAlertReply") { + EwkInternalApiBinding::GetInstance().view.JavaScriptAlertReply( + webview_instance_); + result->Success(); + } else if (method_name == "javaScriptConfirmReply") { + const auto* value = std::get_if(arguments); + if (value) { + EwkInternalApiBinding::GetInstance().view.JavaScriptConfirmReply( + webview_instance_, *value); + result->Success(); + } else { + result->Error("Invalid argument", "The argument must be a bool."); + } + } else if (method_name == "javaScriptPromptReply") { + // A null argument signals that the prompt was cancelled; pass nullptr to + // EWK so the JavaScript prompt() call resolves to null. + const auto* value = std::get_if(arguments); + EwkInternalApiBinding::GetInstance().view.JavaScriptPromptReply( + webview_instance_, value ? value->c_str() : nullptr); + result->Success(); + } else { + result->NotImplemented(); + } +} + +FlutterDesktopGpuSurfaceDescriptor* WebView::ObtainGpuSurface(size_t width, + size_t height) { + std::lock_guard lock(mutex_); + if (!candidate_surface_) { + if (rendered_surface_) { + if (!rendered_surface_->MarkInUse()) { + return nullptr; + } + return rendered_surface_->GpuSurface(); + } + return nullptr; + } + rendered_surface_ = candidate_surface_; + candidate_surface_ = nullptr; + return rendered_surface_->GpuSurface(); +} + +void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { + if (event_info) { + WebView* webview = static_cast(data); + + std::lock_guard lock(webview->mutex_); + if (!webview->working_surface_) { + if (webview->candidate_surface_) { + webview->tbm_pool_->Release(webview->candidate_surface_); + webview->candidate_surface_ = nullptr; + } + webview->working_surface_ = webview->tbm_pool_->GetAvailableBuffer(); + if (!webview->working_surface_) { + return; + } + 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); + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), + flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; + webview->webview_channel_->InvokeMethod( + "onLoadStart", std::make_unique(args)); +} + +void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), + flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; + webview->webview_channel_->InvokeMethod( + "onLoadStop", std::make_unique(args)); + + const char* title = ewk_view_title_get(webview->webview_instance_); + if (title) { + flutter::EncodableMap title_args = { + {flutter::EncodableValue("title"), flutter::EncodableValue(title)}}; + webview->webview_channel_->InvokeMethod( + "onTitleChanged", + std::make_unique(title_args)); + } +} + +void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + int32_t progress = + static_cast((*static_cast(event_info)) * 100); + flutter::EncodableMap args = { + {flutter::EncodableValue("progress"), flutter::EncodableValue(progress)}}; + webview->webview_channel_->InvokeMethod( + "onProgressChanged", 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); + std::string url = + ewk_error_url_get(error) ? std::string(ewk_error_url_get(error)) : ""; + std::string description = + ewk_error_description_get(error) ? ewk_error_description_get(error) : ""; + + flutter::EncodableMap args = { + {flutter::EncodableValue("request"), + flutter::EncodableValue(CreateRequestMap(url))}, + {flutter::EncodableValue("error"), + flutter::EncodableValue( + CreateErrorMap(description, ewk_error_code_get(error)))}, + }; + webview->webview_channel_->InvokeMethod( + "onReceivedError", std::make_unique(args)); +} + +void WebView::OnConsoleMessage(void* data, Evas_Object* obj, void* event_info) { + Ewk_Console_Message* message = static_cast(event_info); + Ewk_Console_Message_Level log_level = + EwkInternalApiBinding::GetInstance().console_message.LevelGet(message); + std::string text = + EwkInternalApiBinding::GetInstance().console_message.TextGet(message); + WebView* webview = static_cast(data); + if (webview->webview_channel_) { + flutter::EncodableMap args = { + {flutter::EncodableValue("messageLevel"), + flutter::EncodableValue(ConvertLogLevel(log_level))}, + {flutter::EncodableValue("message"), flutter::EncodableValue(text)}, + }; + webview->webview_channel_->InvokeMethod( + "onConsoleMessage", std::make_unique(args)); + } +} + +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); + // Always accept the navigation on its original frame so iframe loads stay + // in their iframe. The view is then suspended while we wait for the Dart + // shouldOverrideUrlLoading response and either resumed (allow) or stopped + // (cancel) by NavigationRequestResult. + ewk_policy_decision_use(policy_decision); + if (!webview->has_navigation_delegate_) { + return; + } + + const char* url_cstr = ewk_policy_decision_url_get(policy_decision); + const std::string url = url_cstr ? std::string(url_cstr) : std::string(); + ewk_view_suspend(webview->webview_instance_); + + flutter::EncodableMap args = CreateNavigationActionMap(url); + auto result = + std::make_unique(webview, webview->lifetime_); + webview->webview_channel_->InvokeMethod( + "shouldOverrideUrlLoading", + std::make_unique(args), std::move(result)); +} + +void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), + flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}, + {flutter::EncodableValue("isReload"), flutter::EncodableValue(false)}}; + webview->webview_channel_->InvokeMethod( + "onUpdateVisitedHistory", + std::make_unique(args)); +} + +void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, + void* user_data) { + FlMethodResult* result = static_cast(user_data); + if (result_value) { + result->Success(flutter::EncodableValue(result_value)); + } else { + result->Success(); + } + delete result; +} + +Eina_Bool WebView::OnJavaScriptAlertDialog(Evas_Object* o, const char* message, + void* data) { + WebView* webview = static_cast(data); + flutter::EncodableMap args = { + {flutter::EncodableValue("message"), + flutter::EncodableValue(message ? std::string(message) : "")}, + {flutter::EncodableValue("url"), + flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}, + {flutter::EncodableValue("isMainFrame"), flutter::EncodableValue(true)}}; + webview->webview_channel_->InvokeMethod( + "onJsAlert", std::make_unique(args)); + return true; +} + +Eina_Bool WebView::OnJavaScriptConfirmDialog(Evas_Object* o, + const char* message, void* data) { + WebView* webview = static_cast(data); + flutter::EncodableMap args = { + {flutter::EncodableValue("message"), + flutter::EncodableValue(message ? std::string(message) : "")}, + {flutter::EncodableValue("url"), + flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}, + {flutter::EncodableValue("isMainFrame"), flutter::EncodableValue(true)}}; + webview->webview_channel_->InvokeMethod( + "onJsConfirm", std::make_unique(args)); + return true; +} + +Eina_Bool WebView::OnJavaScriptPromptDialog(Evas_Object* o, const char* message, + const char* default_text, + void* data) { + WebView* webview = static_cast(data); + flutter::EncodableMap args = { + {flutter::EncodableValue("message"), + flutter::EncodableValue(message ? std::string(message) : "")}, + {flutter::EncodableValue("url"), + flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}, + {flutter::EncodableValue("defaultValue"), + flutter::EncodableValue(default_text ? std::string(default_text) : "")}, + {flutter::EncodableValue("isMainFrame"), flutter::EncodableValue(true)}}; + webview->webview_channel_->InvokeMethod( + "onJsPrompt", std::make_unique(args)); + return true; +} diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h new file mode 100644 index 000000000..cf42b38f0 --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -0,0 +1,138 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_WEBVIEW_H_ +#define FLUTTER_PLUGIN_WEBVIEW_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "ewk_internal_api_binding.h" + +typedef flutter::MethodCall FlMethodCall; +typedef flutter::MethodResult FlMethodResult; +typedef flutter::MethodChannel FlMethodChannel; +typedef struct _Ecore_Evas Ecore_Evas; + +class BufferPool; +class BufferUnit; +struct WebViewLifetimeState; + +class WebView : public PlatformView { + public: + WebView(flutter::PluginRegistrar* registrar, int view_id, + flutter::TextureRegistrar* texture_registrar, double width, + double height, const flutter::EncodableValue& params, void* window); + ~WebView(); + + virtual void Dispose() override; + bool IsInitialized() const { return initialized_; } + + virtual void Offset(double left, double top) 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; + + // Used by NavigationRequestResult after a suspended navigation has been + // accepted (Resume) or cancelled (Stop) by the Dart shouldOverrideUrlLoading + // callback. + void ResumeNavigation(); + void StopNavigation(); + + Evas_Object* GetWebViewInstance() { return webview_instance_; } + + FlutterDesktopGpuSurfaceDescriptor* ObtainGpuSurface(size_t width, + size_t height); + + // Process-wide helpers used by static channels. They iterate live WebView + // instances and rely on the shared EWK context. + static void ClearAllCache(); + static bool ClearAllCookies(); + static std::string GetDefaultUserAgent(); + + private: + void HandleWebViewMethodCall(const FlMethodCall& method_call, + std::unique_ptr result); + void ApplyInitialParams(const flutter::EncodableValue& params); + void ApplySettings(const flutter::EncodableMap& settings); + + template + void SetBackgroundColor(const T& color); + + std::string GetWebViewChannelName(); + + bool InitWebView(); + + static void OnFrameRendered(void* data, Evas_Object* obj, void* event_info); + static void OnLoadStarted(void* data, Evas_Object* obj, void* event_info); + static void OnLoadFinished(void* data, Evas_Object* obj, void* event_info); + static void OnProgress(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 OnUrlChange(void* data, Evas_Object* obj, void* event_info); + static void OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, + void* user_data); + static Eina_Bool OnJavaScriptAlertDialog(Evas_Object* o, const char* message, + void* data); + static Eina_Bool OnJavaScriptConfirmDialog(Evas_Object* o, + const char* message, void* data); + static Eina_Bool OnJavaScriptPromptDialog(Evas_Object* o, const char* message, + const char* default_text, + void* data); + + void SendTouchEvent(int type, double x, double y); + void SendMouseEvent(int type, int button, double x, double y, double dx, + double dy); + + Evas_Object* webview_instance_ = nullptr; + Ecore_Evas* ecore_evas_ = nullptr; + flutter::TextureRegistrar* texture_registrar_; + double width_ = 0.0; + double height_ = 0.0; + double left_ = 0.0; + double top_ = 0.0; + void* window_ = nullptr; + BufferUnit* working_surface_ = nullptr; + BufferUnit* candidate_surface_ = nullptr; + BufferUnit* rendered_surface_ = nullptr; + bool has_navigation_delegate_ = false; + std::unique_ptr webview_channel_; + std::unique_ptr texture_variant_; + std::mutex mutex_; + std::unique_ptr tbm_pool_; + std::shared_ptr lifetime_; + bool initialized_ = false; + bool texture_registered_ = false; + bool disposed_ = false; + Ewk_Mouse_Button_Type mouse_button_type_ = (Ewk_Mouse_Button_Type)0; + + static std::set instances_; + static std::mutex instances_mutex_; + // Cached on the first WebView creation so static channel callers can read it + // even after every InAppWebView has been disposed. + static std::string default_user_agent_; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_H_ diff --git a/packages/flutter_inappwebview/tizen/src/webview_factory.cc b/packages/flutter_inappwebview/tizen/src/webview_factory.cc new file mode 100644 index 000000000..b23079a6c --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/webview_factory.cc @@ -0,0 +1,40 @@ +// Copyright 2026 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 + +#include "log.h" +#include "webview.h" + +WebViewFactory::WebViewFactory(flutter::PluginRegistrar* registrar, + void* window) + : PlatformViewFactory(registrar), window_(window) { + texture_registrar_ = registrar->texture_registrar(); +} + +PlatformView* WebViewFactory::Create(int view_id, double width, double height, + const ByteMessage& params) { + auto decoded_params = GetCodec().DecodeMessage(params); + if (!decoded_params) { + LOG_ERROR("Failed to decode WebView creation params."); + return nullptr; + } + auto webview = std::make_unique(GetPluginRegistrar(), view_id, + texture_registrar_, width, height, + *decoded_params, window_); + if (!webview->IsInitialized()) { + return nullptr; + } + return webview.release(); +} + +void WebViewFactory::Dispose() {} diff --git a/packages/flutter_inappwebview/tizen/src/webview_factory.h b/packages/flutter_inappwebview/tizen/src/webview_factory.h new file mode 100644 index 000000000..f67ffe61d --- /dev/null +++ b/packages/flutter_inappwebview/tizen/src/webview_factory.h @@ -0,0 +1,28 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ +#define FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_ + +#include +#include +#include + +#include + +class WebViewFactory : public PlatformViewFactory { + public: + WebViewFactory(flutter::PluginRegistrar* registrar, void* window); + + virtual PlatformView* Create(int view_id, double width, double height, + const ByteMessage& params) override; + + virtual void Dispose() override; + + private: + flutter::TextureRegistrar* texture_registrar_; + void* window_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_WEBVIEW_FACTORY_H_