diff --git a/packages/flutter_inappwebview/CHANGELOG.md b/packages/flutter_inappwebview/CHANGELOG.md
index c6321e01e..142cdd800 100644
--- a/packages/flutter_inappwebview/CHANGELOG.md
+++ b/packages/flutter_inappwebview/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.2.0
+
+- Fix `onTitleChanged` to fire on title changes after the initial load, not just once at load finish.
+- Fix `getUrl()` returning a cancelled navigation's URL, and skip `shouldOverrideUrlLoading` for app-initiated navigations.
+- Fix `shouldOverrideUrlLoading` not being invoked for a hardware/remote Back-key navigation.
+- Fix `scrollBy`/`getScrollX`/`getScrollY` occasionally returning a stale scroll position.
+- Fix `onUpdateVisitedHistory`/`getUrl()` staying pinned to a cancelled navigation's URL after a later same-document URL change, and bound how long a pending `scrollTo`/`scrollBy` target is trusted.
+
## 0.1.4
- Fix a SIGTRAP crash on TV app teardown by calling `ewk_init()`/`ewk_shutdown()` exactly once per process.
diff --git a/packages/flutter_inappwebview/README.md b/packages/flutter_inappwebview/README.md
index 551631da5..6ecfa54d3 100644
--- a/packages/flutter_inappwebview/README.md
+++ b/packages/flutter_inappwebview/README.md
@@ -26,7 +26,7 @@ Add the internet privilege to the app manifest:
```yaml
dependencies:
flutter_inappwebview: ^6.1.5
- flutter_inappwebview_tizen: ^0.1.4
+ flutter_inappwebview_tizen: ^0.2.0
```
```dart
diff --git a/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html b/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html
new file mode 100644
index 000000000..bb80a8033
--- /dev/null
+++ b/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Load file test
+
+
+ Loaded from asset
+
+
diff --git a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart
index 58232c234..517cbbc71 100644
--- a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart
+++ b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart
@@ -3,7 +3,9 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
+import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
@@ -44,11 +46,13 @@ void main() {
late String firstUrl;
late String secondUrl;
late String blockedUrl;
+ late String echoPostUrl;
+ late String slowUrl;
setUpAll(() async {
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
unawaited(
- server.forEach((HttpRequest request) {
+ server.forEach((HttpRequest request) async {
request.response.headers.contentType = ContentType.html;
switch (request.uri.path) {
case '/first':
@@ -57,18 +61,26 @@ void main() {
request.response.write(_htmlPage('Second page'));
case '/blocked':
request.response.write(_htmlPage('Blocked page'));
+ case '/echo-post':
+ final String body = await utf8.decoder.bind(request).join();
+ request.response.write('$body
');
+ case '/slow':
+ await Future.delayed(const Duration(seconds: 5));
+ request.response.write(_htmlPage('Slow page'));
case '/favicon.ico':
request.response.statusCode = HttpStatus.notFound;
default:
fail('unexpected request: ${request.method} ${request.uri}');
}
- request.response.close();
+ await request.response.close();
}),
);
final String baseUrl = 'http://${server.address.address}:${server.port}';
firstUrl = '$baseUrl/first';
secondUrl = '$baseUrl/second';
blockedUrl = '$baseUrl/blocked';
+ echoPostUrl = '$baseUrl/echo-post';
+ slowUrl = '$baseUrl/slow';
});
tearDownAll(() => server.close(force: true));
@@ -286,16 +298,378 @@ document.cookie;
);
expect(cookieAfter.toString(), isNot(contains('tizen_inappwebview=1')));
});
+
+ testWidgets('getProgress reports 100 once the page finishes loading', (
+ 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);
+
+ expect(await controller.getProgress(), 100);
+ });
+
+ testWidgets('reload reloads the currently displayed page', (
+ 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 Future reloaded = loadStops.stream.first.timeout(
+ const Duration(seconds: 10),
+ );
+ await controller.reload();
+ expect(await reloaded, firstUrl);
+ });
+
+ testWidgets('loadUrl navigates to a new URL', (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 Future secondLoad = _waitForValue(
+ loadStops.stream,
+ secondUrl,
+ );
+ await controller.loadUrl(urlRequest: URLRequest(url: WebUri(secondUrl)));
+ expect(await secondLoad, secondUrl);
+ expect((await controller.getUrl()).toString(), secondUrl);
+ });
+
+ testWidgets('postUrl and loadUrl submit an HTTP POST request body', (
+ WidgetTester tester,
+ ) async {
+ final StreamController loadStops =
+ StreamController.broadcast();
+ addTearDown(loadStops.close);
+
+ final InAppWebViewController controller = await _pumpWebView(
+ tester,
+ onLoadStop: (_, WebUri? url) {
+ if (url != null) {
+ loadStops.add(url.toString());
+ }
+ },
+ );
+
+ final Future firstPost = _waitForValue(
+ loadStops.stream,
+ echoPostUrl,
+ );
+ await controller.postUrl(
+ url: WebUri(echoPostUrl),
+ postData: Uint8List.fromList(utf8.encode('name=postUrl')),
+ );
+ await firstPost;
+ expect(
+ await _waitForCondition(
+ () => controller.evaluateJavascript(
+ source: "document.querySelector('p')?.textContent",
+ ),
+ (Object? value) => value == 'name=postUrl',
+ ),
+ 'name=postUrl',
+ );
+
+ final Future secondPost = loadStops.stream.first.timeout(
+ const Duration(seconds: 10),
+ );
+ await controller.loadUrl(
+ urlRequest: URLRequest(
+ url: WebUri(echoPostUrl),
+ method: 'POST',
+ body: Uint8List.fromList(utf8.encode('name=loadUrl')),
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ ),
+ );
+ expect(await secondPost, echoPostUrl);
+ expect(
+ await _waitForCondition(
+ () => controller.evaluateJavascript(
+ source: "document.querySelector('p')?.textContent",
+ ),
+ (Object? value) => value == 'name=loadUrl',
+ ),
+ 'name=loadUrl',
+ );
+ });
+
+ testWidgets('loadFile loads a bundled asset file', (
+ WidgetTester tester,
+ ) async {
+ final StreamController loadStops =
+ StreamController.broadcast();
+ addTearDown(loadStops.close);
+
+ final InAppWebViewController controller = await _pumpWebView(
+ tester,
+ onLoadStop: (_, WebUri? url) {
+ if (url != null) {
+ loadStops.add(url.toString());
+ }
+ },
+ );
+
+ final Future fileLoaded = loadStops.stream.firstWhere(
+ (String url) => url.endsWith('load_file_test.html'),
+ );
+ await controller.loadFile(
+ assetFilePath: 'assets/test_assets/load_file_test.html',
+ );
+ await fileLoaded.timeout(const Duration(seconds: 10));
+
+ expect(
+ await _waitForCondition(
+ () => controller.evaluateJavascript(source: "document.title"),
+ (Object? value) => value == 'Load file test',
+ ),
+ 'Load file test',
+ );
+ expect(
+ await controller.evaluateJavascript(
+ source: "document.querySelector('h1').textContent",
+ ),
+ 'Loaded from asset',
+ );
+ });
+
+ testWidgets('programmatic scroll updates and reports the scroll position', (
+ WidgetTester tester,
+ ) async {
+ final InAppWebViewController controller = await _pumpWebView(tester);
+ await _loadFixture(controller);
+
+ await controller.scrollTo(x: 0, y: 0);
+
+ const int scrollX = 30;
+ const int scrollY = 40;
+ await controller.scrollTo(x: scrollX, y: scrollY);
+ expect(await controller.getScrollX(), scrollX);
+ expect(await controller.getScrollY(), scrollY);
+
+ await controller.scrollBy(x: scrollX, y: scrollY);
+ expect(await controller.getScrollX(), scrollX * 2);
+ expect(await controller.getScrollY(), scrollY * 2);
+ });
+
+ testWidgets('onScrollChanged fires when the scroll position changes', (
+ WidgetTester tester,
+ ) async {
+ final Completer scrollChanged = Completer();
+ final InAppWebViewController controller = await _pumpWebView(
+ tester,
+ onScrollChanged: (_, int x, int y) {
+ if (x == 50 && y == 60 && !scrollChanged.isCompleted) {
+ scrollChanged.complete();
+ }
+ },
+ );
+ await _loadFixture(controller);
+
+ await controller.scrollTo(x: 50, y: 60);
+ await scrollChanged.future.timeout(const Duration(seconds: 10));
+ });
+
+ testWidgets('onTitleChanged fires when document.title changes', (
+ WidgetTester tester,
+ ) async {
+ final Completer titleChanged = Completer();
+ final InAppWebViewController controller = await _pumpWebView(
+ tester,
+ onTitleChanged: (_, String? title) {
+ if (title == 'updated title' && !titleChanged.isCompleted) {
+ titleChanged.complete();
+ }
+ },
+ );
+ await _loadFixture(controller);
+
+ await controller.evaluateJavascript(
+ source: "document.title = 'updated title';",
+ );
+ await titleChanged.future.timeout(const Duration(seconds: 10));
+ });
+
+ testWidgets('stopLoading interrupts an in-flight page load', (
+ WidgetTester tester,
+ ) async {
+ final StreamController loadStops =
+ StreamController.broadcast();
+ addTearDown(loadStops.close);
+
+ await _pumpWebView(
+ tester,
+ initialUrl: slowUrl,
+ onLoadStart: (InAppWebViewController controller, WebUri? url) {
+ controller.stopLoading();
+ },
+ onLoadStop: (_, WebUri? url) {
+ if (url != null) {
+ loadStops.add(url.toString());
+ }
+ },
+ );
+
+ final Future slowLoad = _waitForValue(
+ loadStops.stream,
+ slowUrl,
+ timeout: const Duration(seconds: 2),
+ );
+ await expectLater(slowLoad, throwsA(isA()));
+ });
+
+ testWidgets('clearAllCache completes without throwing', (
+ WidgetTester tester,
+ ) async {
+ await expectLater(
+ InAppWebViewController.clearAllCache(includeDiskFiles: true),
+ completes,
+ );
+ });
+
+ testWidgets('zoomBy triggers onZoomScaleChanged', (
+ WidgetTester tester,
+ ) async {
+ final Completer zoomRatio = Completer();
+ final InAppWebViewController controller = await _pumpWebView(
+ tester,
+ onZoomScaleChanged: (_, double oldScale, double newScale) {
+ if (!zoomRatio.isCompleted) {
+ zoomRatio.complete(newScale / oldScale);
+ }
+ },
+ );
+ await _loadFixture(controller);
+
+ await controller.zoomBy(zoomFactor: 2);
+ expect(await zoomRatio.future.timeout(const Duration(seconds: 10)), 2);
+ });
+
+ testWidgets(
+ 'onReceivedError reports a host lookup failure for an unresolvable URL',
+ (WidgetTester tester) async {
+ final Completer receivedError =
+ Completer();
+
+ await _pumpWebView(
+ tester,
+ initialUrl: 'http://this-domain-does-not-exist.invalid/',
+ onReceivedError: (_, WebResourceRequest __, WebResourceError error) {
+ if (!receivedError.isCompleted) {
+ receivedError.complete(error);
+ }
+ },
+ );
+
+ final WebResourceError error = await receivedError.future.timeout(
+ const Duration(seconds: 10),
+ );
+ expect(error.type, WebResourceErrorType.HOST_LOOKUP);
+ },
+ );
+
+ testWidgets('onReceivedError is not raised for a successful page load', (
+ WidgetTester tester,
+ ) async {
+ final StreamController loadStops =
+ StreamController.broadcast();
+ final Completer receivedError = Completer();
+ addTearDown(loadStops.close);
+
+ await _pumpWebView(
+ tester,
+ initialUrl: firstUrl,
+ onLoadStop: (_, WebUri? url) {
+ if (url != null) {
+ loadStops.add(url.toString());
+ }
+ },
+ onReceivedError: (_, WebResourceRequest __, WebResourceError ___) {
+ receivedError.complete();
+ },
+ );
+ await _waitForValue(loadStops.stream, firstUrl);
+
+ await expectLater(
+ receivedError.future.timeout(const Duration(seconds: 1)),
+ throwsA(isA()),
+ );
+ });
+
+ testWidgets('setSettings applies updated webview settings', (
+ WidgetTester tester,
+ ) async {
+ final InAppWebViewController controller = await _pumpWebView(tester);
+ await _loadFixture(controller);
+
+ await expectLater(
+ controller.setSettings(
+ settings: InAppWebViewSettings(
+ javaScriptEnabled: true,
+ supportZoom: true,
+ ),
+ ),
+ completes,
+ );
+ expect(
+ await controller.evaluateJavascript(
+ source: "document.querySelector('h1').textContent",
+ ),
+ 'Fixture Page',
+ );
+ });
}
Future _pumpWebView(
WidgetTester tester, {
String initialUrl = 'about:blank',
InAppWebViewSettings? initialSettings,
+ void Function(InAppWebViewController, WebUri?)? onLoadStart,
void Function(InAppWebViewController, WebUri?)? onLoadStop,
void Function(InAppWebViewController, int)? onProgressChanged,
void Function(InAppWebViewController, ConsoleMessage)? onConsoleMessage,
void Function(InAppWebViewController, WebUri?, bool?)? onUpdateVisitedHistory,
+ void Function(InAppWebViewController, int, int)? onScrollChanged,
+ void Function(InAppWebViewController, String?)? onTitleChanged,
+ void Function(InAppWebViewController, double, double)? onZoomScaleChanged,
+ void Function(InAppWebViewController, WebResourceRequest, WebResourceError)?
+ onReceivedError,
Future Function(InAppWebViewController, JsAlertRequest)?
onJsAlert,
Future Function(InAppWebViewController, JsConfirmRequest)?
@@ -319,10 +693,15 @@ Future _pumpWebView(
initialSettings: initialSettings,
initialUrlRequest: URLRequest(url: WebUri(initialUrl)),
onWebViewCreated: controllerCompleter.complete,
+ onLoadStart: onLoadStart,
onLoadStop: onLoadStop,
onProgressChanged: onProgressChanged,
onConsoleMessage: onConsoleMessage,
onUpdateVisitedHistory: onUpdateVisitedHistory,
+ onScrollChanged: onScrollChanged,
+ onTitleChanged: onTitleChanged,
+ onZoomScaleChanged: onZoomScaleChanged,
+ onReceivedError: onReceivedError,
onJsAlert: onJsAlert,
onJsConfirm: onJsConfirm,
onJsPrompt: onJsPrompt,
@@ -383,6 +762,25 @@ Future _waitForValue(
return stream.firstWhere((T event) => event == value).timeout(timeout);
}
+Future _waitForCondition(
+ Future Function() poll,
+ bool Function(Object? value) isReady, {
+ Duration timeout = const Duration(seconds: 10),
+}) async {
+ Object? lastResult;
+ final DateTime end = DateTime.now().add(timeout);
+
+ while (DateTime.now().isBefore(end)) {
+ lastResult = await poll();
+ if (isReady(lastResult)) {
+ return lastResult;
+ }
+ await Future.delayed(const Duration(milliseconds: 200));
+ }
+
+ throw TimeoutException('Condition not met. Last result: $lastResult');
+}
+
String _htmlPage(String title) {
return '''
diff --git a/packages/flutter_inappwebview/example/pubspec.yaml b/packages/flutter_inappwebview/example/pubspec.yaml
index ccb0a802b..0bf1815d4 100644
--- a/packages/flutter_inappwebview/example/pubspec.yaml
+++ b/packages/flutter_inappwebview/example/pubspec.yaml
@@ -31,3 +31,5 @@ dev_dependencies:
flutter:
uses-material-design: true
+ assets:
+ - assets/test_assets/
diff --git a/packages/flutter_inappwebview/pubspec.yaml b/packages/flutter_inappwebview/pubspec.yaml
index 2373e7234..4f4a16f33 100644
--- a/packages/flutter_inappwebview/pubspec.yaml
+++ b/packages/flutter_inappwebview/pubspec.yaml
@@ -2,7 +2,7 @@ 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/main/packages/flutter_inappwebview
-version: 0.1.4
+version: 0.2.0
environment:
sdk: ">=3.8.0 <4.0.0"
diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc
index 9523b37da..655e28bff 100644
--- a/packages/flutter_inappwebview/tizen/src/webview.cc
+++ b/packages/flutter_inappwebview/tizen/src/webview.cc
@@ -342,9 +342,24 @@ void WebView::StopNavigation() {
if (disposed_ || !webview_instance_) {
return;
}
+ is_navigation_cancelled_ = true;
+ if (!url_before_navigation_.empty()) {
+ committed_url_ = url_before_navigation_;
+ }
+ // ewk_view_stop() has no effect while the view is suspended.
+ ewk_view_resume(webview_instance_);
ewk_view_stop(webview_instance_);
}
+bool WebView::NavigateProgrammatically(const std::function& ewk_call) {
+ is_programmatic_navigation_ = true;
+ const bool started = ewk_call();
+ if (!started) {
+ is_programmatic_navigation_ = false;
+ }
+ return started;
+}
+
void WebView::Dispose() {
if (disposed_) {
return;
@@ -389,6 +404,8 @@ void WebView::Dispose() {
&WebView::OnNavigationPolicy);
evas_object_smart_callback_del(webview_instance_, "url,changed",
&WebView::OnUrlChange);
+ evas_object_smart_callback_del(webview_instance_, "title,changed",
+ &WebView::OnTitleChange);
auto& ewk_view = EwkInternalApiBinding::GetInstance().view;
if (ewk_view.OnJavaScriptAlert) {
ewk_view.OnJavaScriptAlert(webview_instance_, nullptr, nullptr);
@@ -522,6 +539,7 @@ bool WebView::SendKey(const char* key, const char* string, const char* compose,
if (strcmp(key, "XF86Back") == 0 && !is_down) {
if (ewk_view_back_possible(webview_instance_)) {
+ // Not programmatic: must still reach shouldOverrideUrlLoading.
ewk_view_back(webview_instance_);
return true;
}
@@ -647,6 +665,8 @@ bool WebView::InitWebView() {
&WebView::OnNavigationPolicy, this);
evas_object_smart_callback_add(webview_instance_, "url,changed",
&WebView::OnUrlChange, this);
+ evas_object_smart_callback_add(webview_instance_, "title,changed",
+ &WebView::OnTitleChange, this);
Resize(width_, height_);
evas_object_show(webview_instance_);
@@ -712,7 +732,10 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) {
std::string url =
std::string("file://") + res_path + "flutter_assets/" + initial_file;
free(res_path);
- ewk_view_url_set(webview_instance_, url.c_str());
+ NavigateProgrammatically([this, &url] {
+ ewk_view_url_set(webview_instance_, url.c_str());
+ return true;
+ });
return;
}
}
@@ -724,8 +747,11 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) {
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);
+ NavigateProgrammatically([this, &data, &base_url] {
+ ewk_view_html_string_load(webview_instance_, data.c_str(),
+ base_url.c_str(), nullptr);
+ return true;
+ });
return;
}
}
@@ -735,7 +761,10 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) {
&url_request)) {
std::string url;
if (GetValueFromEncodableMap(url_request, "url", &url) && !url.empty()) {
- ewk_view_url_set(webview_instance_, url.c_str());
+ NavigateProgrammatically([this, &url] {
+ ewk_view_url_set(webview_instance_, url.c_str());
+ return true;
+ });
}
}
}
@@ -795,16 +824,22 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
}
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()));
+ const bool ret = NavigateProgrammatically([&] {
+ return 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());
+ NavigateProgrammatically([this, &url] {
+ ewk_view_url_set(webview_instance_, url.c_str());
+ return true;
+ });
}
result->Success();
} else if (method_name == "postUrl") {
@@ -818,9 +853,11 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
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()));
+ const bool ret = NavigateProgrammatically([&] {
+ return 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 {
@@ -833,8 +870,13 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
return;
}
GetValueFromEncodableMap(arguments, "baseUrl", &base_url);
- ewk_view_html_string_load(webview_instance_, data.c_str(), base_url.c_str(),
- nullptr);
+ // Bypasses OnNavigationPolicy, so clear any stale cancellation here.
+ is_navigation_cancelled_ = false;
+ NavigateProgrammatically([this, &data, &base_url] {
+ ewk_view_html_string_load(webview_instance_, data.c_str(),
+ base_url.c_str(), nullptr);
+ return true;
+ });
result->Success();
} else if (method_name == "loadFile") {
std::string file_path;
@@ -854,7 +896,10 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
url = std::string("file://") + res_path + "flutter_assets/" + file_path;
free(res_path);
}
- ewk_view_url_set(webview_instance_, url.c_str());
+ NavigateProgrammatically([this, &url] {
+ ewk_view_url_set(webview_instance_, url.c_str());
+ return true;
+ });
result->Success();
} else if (method_name == "canGoBack") {
result->Success(flutter::EncodableValue(
@@ -863,18 +908,28 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
result->Success(flutter::EncodableValue(
static_cast(ewk_view_forward_possible(webview_instance_))));
} else if (method_name == "goBack") {
- ewk_view_back(webview_instance_);
+ NavigateProgrammatically(
+ [this] { return static_cast(ewk_view_back(webview_instance_)); });
result->Success();
} else if (method_name == "goForward") {
- ewk_view_forward(webview_instance_);
+ NavigateProgrammatically([this] {
+ return static_cast(ewk_view_forward(webview_instance_));
+ });
result->Success();
} else if (method_name == "reload") {
- ewk_view_reload(webview_instance_);
+ NavigateProgrammatically([this] {
+ ewk_view_reload(webview_instance_);
+ return true;
+ });
result->Success();
} else if (method_name == "getUrl") {
- const char* url = ewk_view_url_get(webview_instance_);
- result->Success(url ? flutter::EncodableValue(url)
- : flutter::EncodableValue());
+ if (is_navigation_cancelled_ && !committed_url_.empty()) {
+ result->Success(flutter::EncodableValue(committed_url_));
+ } else {
+ 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))
@@ -913,11 +968,32 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
}
if (method_name == "scrollTo") {
ewk_view_scroll_set(webview_instance_, x, y);
+ target_scroll_x_ = x;
+ target_scroll_y_ = y;
+ target_scroll_set_time_ = std::chrono::steady_clock::now();
} else {
- ewk_view_scroll_by(webview_instance_, x, y);
+ int32_t current_x = 0, current_y = 0;
+ ewk_view_scroll_pos_get(webview_instance_, ¤t_x, ¤t_y);
+ // Only trust a pending target briefly; it can go stale (manual
+ // scroll, EWK clamping) since scroll_set() applies asynchronously.
+ constexpr auto kTargetTtl = std::chrono::milliseconds(100);
+ const bool target_fresh =
+ std::chrono::steady_clock::now() - target_scroll_set_time_ <
+ kTargetTtl;
+ int32_t base_x = (target_fresh && target_scroll_x_ >= 0)
+ ? target_scroll_x_
+ : current_x;
+ int32_t base_y = (target_fresh && target_scroll_y_ >= 0)
+ ? target_scroll_y_
+ : current_y;
+ target_scroll_x_ = base_x + x;
+ target_scroll_y_ = base_y + y;
+ target_scroll_set_time_ = std::chrono::steady_clock::now();
+ ewk_view_scroll_set(webview_instance_, target_scroll_x_,
+ target_scroll_y_);
}
- int32_t new_x = 0, new_y = 0;
- ewk_view_scroll_pos_get(webview_instance_, &new_x, &new_y);
+ int32_t new_x = target_scroll_x_;
+ int32_t new_y = target_scroll_y_;
flutter::EncodableMap args = {
{flutter::EncodableValue("x"), flutter::EncodableValue(new_x)},
{flutter::EncodableValue("y"), flutter::EncodableValue(new_y)},
@@ -928,6 +1004,17 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call,
} else if (method_name == "getScrollX" || method_name == "getScrollY") {
int32_t x = 0, y = 0;
ewk_view_scroll_pos_get(webview_instance_, &x, &y);
+ if (method_name == "getScrollX") {
+ if (target_scroll_x_ >= 0) {
+ x = target_scroll_x_;
+ target_scroll_x_ = -1;
+ }
+ } else {
+ if (target_scroll_y_ >= 0) {
+ y = target_scroll_y_;
+ target_scroll_y_ = -1;
+ }
+ }
result->Success(
flutter::EncodableValue(method_name == "getScrollX" ? x : y));
} else if (method_name == "zoomBy") {
@@ -1030,6 +1117,9 @@ void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) {
void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) {
WebView* webview = static_cast(data);
+ webview->is_programmatic_navigation_ = false;
+ webview->target_scroll_x_ = -1;
+ webview->target_scroll_y_ = -1;
flutter::EncodableMap args = {
{flutter::EncodableValue("url"),
flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}};
@@ -1039,20 +1129,12 @@ void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) {
void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) {
WebView* webview = static_cast(data);
+ webview->is_programmatic_navigation_ = false;
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) {
@@ -1067,6 +1149,9 @@ void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) {
void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) {
WebView* webview = static_cast(data);
+ webview->is_programmatic_navigation_ = false;
+ webview->target_scroll_x_ = -1;
+ webview->target_scroll_y_ = -1;
Ewk_Error* error = static_cast(event_info);
std::string url =
ewk_error_url_get(error) ? std::string(ewk_error_url_get(error)) : "";
@@ -1107,15 +1192,30 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj,
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);
+
+ // A new decision means any prior cancellation is now stale.
+ webview->is_navigation_cancelled_ = false;
+
+ if (webview->is_programmatic_navigation_) {
+ // App-initiated navigations skip shouldOverrideUrlLoading.
+ webview->is_programmatic_navigation_ = false;
+ ewk_policy_decision_use(policy_decision);
+ return;
+ }
+
if (!webview->has_navigation_delegate_) {
+ ewk_policy_decision_use(policy_decision);
return;
}
+ // Snapshot before accepting: ewk_policy_decision_use() can trigger
+ // "url,changed" for the new URL immediately.
+ const std::string url_before_navigation =
+ GetViewUrl(webview->webview_instance_);
+ ewk_policy_decision_use(policy_decision);
+ webview->url_before_navigation_ = url_before_navigation;
+
+ // Suspended until NavigationRequestResult resumes or stops it.
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_);
@@ -1130,15 +1230,34 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj,
void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) {
WebView* webview = static_cast(data);
+ if (webview->is_navigation_cancelled_) {
+ // Drop only this one stale event, then clear the flag, or a later
+ // same-document change (pushState/replaceState) would be ignored too.
+ webview->is_navigation_cancelled_ = false;
+ return;
+ }
+ webview->committed_url_ = GetViewUrl(webview->webview_instance_);
flutter::EncodableMap args = {
{flutter::EncodableValue("url"),
- flutter::EncodableValue(GetViewUrl(webview->webview_instance_))},
+ flutter::EncodableValue(webview->committed_url_)},
{flutter::EncodableValue("isReload"), flutter::EncodableValue(false)}};
webview->webview_channel_->InvokeMethod(
"onUpdateVisitedHistory",
std::make_unique(args));
}
+void WebView::OnTitleChange(void* data, Evas_Object* obj, void* event_info) {
+ WebView* webview = static_cast(data);
+ const char* title = static_cast(event_info);
+ if (!title) {
+ return;
+ }
+ flutter::EncodableMap args = {
+ {flutter::EncodableValue("title"), flutter::EncodableValue(title)}};
+ webview->webview_channel_->InvokeMethod(
+ "onTitleChanged", std::make_unique(args));
+}
+
void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value,
void* user_data) {
FlMethodResult* result = static_cast(user_data);
diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h
index 7823d0fd7..864a6824d 100644
--- a/packages/flutter_inappwebview/tizen/src/webview.h
+++ b/packages/flutter_inappwebview/tizen/src/webview.h
@@ -14,7 +14,9 @@
#include
#include
+#include
#include
+#include
#include
#include
#include
@@ -89,6 +91,10 @@ class WebView : public PlatformView {
bool InitWebView();
+ // Marks an app-initiated navigation so OnNavigationPolicy skips
+ // shouldOverrideUrlLoading; cleared immediately if it never started.
+ bool NavigateProgrammatically(const std::function& ewk_call);
+
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);
@@ -98,6 +104,7 @@ class WebView : public PlatformView {
static void OnNavigationPolicy(void* data, Evas_Object* obj,
void* event_info);
static void OnUrlChange(void* data, Evas_Object* obj, void* event_info);
+ static void OnTitleChange(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,
@@ -133,6 +140,13 @@ class WebView : public PlatformView {
bool texture_registered_ = false;
bool disposed_ = false;
Ewk_Mouse_Button_Type mouse_button_type_ = (Ewk_Mouse_Button_Type)0;
+ bool is_programmatic_navigation_ = false;
+ bool is_navigation_cancelled_ = false;
+ std::string committed_url_;
+ std::string url_before_navigation_;
+ int32_t target_scroll_x_ = -1;
+ int32_t target_scroll_y_ = -1;
+ std::chrono::steady_clock::time_point target_scroll_set_time_;
static std::set instances_;
static std::mutex instances_mutex_;