diff --git a/packages/flutter_inappwebview/CHANGELOG.md b/packages/flutter_inappwebview/CHANGELOG.md
index 39d9ed658..e2de3f121 100644
--- a/packages/flutter_inappwebview/CHANGELOG.md
+++ b/packages/flutter_inappwebview/CHANGELOG.md
@@ -1,3 +1,15 @@
+## 0.2.0
+
+- Fix `onTitleChanged` to also fire when the page's title changes after the
+ initial load (e.g. when JavaScript updates `document.title`), instead of
+ only once when loading finishes.
+- Fix a race where `getUrl()` could return the URL of a navigation that was
+ cancelled via `shouldOverrideUrlLoading`, and skip the
+ `shouldOverrideUrlLoading` round-trip for app-initiated navigations
+ (`loadUrl`, `goBack`, `reload`, etc.).
+- Fix `scrollBy`/`getScrollX`/`getScrollY` occasionally returning a stale
+ scroll position right after `scrollTo`/`scrollBy`.
+
## 0.1.1
- Fix a crash when a webview is disposed.
diff --git a/packages/flutter_inappwebview/README.md b/packages/flutter_inappwebview/README.md
index 456d45fb9..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.1
+ 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 b9e860a4c..88f9d2090 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/master/packages/flutter_inappwebview
-version: 0.1.1
+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 323947569..baca23f55 100644
--- a/packages/flutter_inappwebview/tizen/src/webview.cc
+++ b/packages/flutter_inappwebview/tizen/src/webview.cc
@@ -307,9 +307,23 @@ void WebView::StopNavigation() {
if (disposed_ || !webview_instance_) {
return;
}
+ is_navigation_cancelled_ = true;
+ if (!pending_navigation_revert_url_.empty()) {
+ committed_url_ = pending_navigation_revert_url_;
+ }
+ 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;
@@ -354,6 +368,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);
@@ -489,6 +505,9 @@ 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 wrapped in NavigateProgrammatically: this is a user-initiated
+ // navigation (remote Back key), so it must still reach
+ // shouldOverrideUrlLoading via OnNavigationPolicy.
ewk_view_back(webview_instance_);
return true;
}
@@ -620,6 +639,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_);
@@ -685,7 +706,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;
}
}
@@ -697,8 +721,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;
}
}
@@ -708,7 +735,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;
+ });
}
}
}
@@ -768,16 +798,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") {
@@ -791,9 +827,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 {
@@ -806,8 +844,15 @@ 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);
+ // ewk_view_html_string_load() doesn't go through OnNavigationPolicy, so
+ // a stale cancellation from an earlier navigation would otherwise never
+ // clear and getUrl() would keep returning the pre-cancellation URL.
+ 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;
@@ -827,7 +872,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(
@@ -836,18 +884,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))
@@ -886,11 +944,20 @@ 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;
} 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);
+ int32_t base_x = (target_scroll_x_ >= 0) ? target_scroll_x_ : current_x;
+ int32_t base_y = (target_scroll_y_ >= 0) ? target_scroll_y_ : current_y;
+ target_scroll_x_ = base_x + x;
+ target_scroll_y_ = base_y + y;
+ 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)},
@@ -901,6 +968,14 @@ 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 (target_scroll_x_ >= 0) {
+ x = target_scroll_x_;
+ target_scroll_x_ = -1;
+ }
+ 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") {
@@ -1003,6 +1078,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_))}};
@@ -1012,6 +1090,7 @@ 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_))}};
@@ -1040,6 +1119,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)) : "";
@@ -1080,6 +1162,26 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj,
WebView* webview = static_cast(data);
Ewk_Policy_Decision* policy_decision =
static_cast(event_info);
+
+ // A new navigation decision means any previous cancellation is stale:
+ // getUrl() should stop overriding with the old committed_url_ snapshot.
+ webview->is_navigation_cancelled_ = false;
+
+ if (webview->is_programmatic_navigation_) {
+ webview->is_programmatic_navigation_ = false;
+ ewk_policy_decision_use(policy_decision);
+ return;
+ }
+
+ // Snapshot the URL EWK is displaying before accepting the navigation
+ // below. EWK can fire "url,changed" for the new (possibly-to-be-cancelled)
+ // URL as soon as ewk_policy_decision_use() runs, racing with the async
+ // shouldOverrideUrlLoading round-trip. StopNavigation() reverts getUrl()
+ // using this snapshot rather than whatever "url,changed" reported last, so
+ // that race can't leave getUrl() stuck on a cancelled URL.
+ const std::string url_before_navigation =
+ GetViewUrl(webview->webview_instance_);
+
// 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
@@ -1089,6 +1191,8 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj,
return;
}
+ webview->pending_navigation_revert_url_ = url_before_navigation;
+
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_);
@@ -1103,15 +1207,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_) {
+ // Stale "url,changed" for the navigation we just cancelled (EWK can fire
+ // it before or after ewk_view_stop() takes effect); getUrl() is already
+ // pinned to committed_url_ and must not be overwritten with this URL.
+ 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 cf42b38f0..6bfe23c59 100644
--- a/packages/flutter_inappwebview/tizen/src/webview.h
+++ b/packages/flutter_inappwebview/tizen/src/webview.h
@@ -15,6 +15,7 @@
#include
#include
+#include
#include
#include
#include
@@ -83,6 +84,14 @@ class WebView : public PlatformView {
bool InitWebView();
+ // Runs an EWK call that starts a navigation the app itself requested
+ // (loadUrl, goBack, reload, ...), marking it as programmatic first so the
+ // next OnNavigationPolicy skips the shouldOverrideUrlLoading round-trip for
+ // it. If `ewk_call` reports the navigation never started, the flag is
+ // cleared immediately instead of leaking into some later, unrelated
+ // navigation. Returns whatever `ewk_call` returned.
+ 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);
@@ -92,6 +101,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,
@@ -127,6 +137,12 @@ 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 pending_navigation_revert_url_;
+ int32_t target_scroll_x_ = -1;
+ int32_t target_scroll_y_ = -1;
static std::set instances_;
static std::mutex instances_mutex_;