Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ LeanCode <contribution@leancode.pl>
Piotr Denert <pdenert09@gmail.com>
Marcin Chudy <marcinchudy94@gmail.com>
Paweł Jakubowski <pawel.jakubowski@leancode.pl>
Mateusz Jabłoński <mateusz.jablonski@ramp.network>
4 changes: 4 additions & 0 deletions packages/webview_flutter/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.15.0

* Adds NavigationDelegate.onCreateWindow for target=_blank / window.open.

## 4.14.1

* Adds documentation for `NavigationDelegate` callback parameters.
Expand Down
6 changes: 6 additions & 0 deletions packages/webview_flutter/webview_flutter/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ flutter:
- assets/sample_video.mp4
- assets/www/index.html
- assets/www/styles/style.css
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
webview_flutter_android: {path: ../../../../packages/webview_flutter/webview_flutter_android}
webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface}
webview_flutter_wkwebview: {path: ../../../../packages/webview_flutter/webview_flutter_wkwebview}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class NavigationDelegate {
///
/// {@template webview_fluttter.NavigationDelegate.constructor}
/// **`onNavigationRequest`:** invoked when a navigation request is pending.
/// **`onCreateWindow`:** invoked when the page opens a new window
/// (`target=_blank` / `window.open`). The host should handle the URL; the
/// WebView will not load it when this callback is set.
/// **`onPageStarted`:** invoked when a page starts loading.
/// **`onPageFinished`:** invoked when a page finishes loading.
/// **`onProgress`:** invoked when page loading progress changes.
Expand All @@ -52,6 +55,7 @@ class NavigationDelegate {
/// {@endtemplate}
NavigationDelegate({
FutureOr<NavigationDecision> Function(NavigationRequest request)? onNavigationRequest,
void Function(String url)? onCreateWindow,
void Function(String url)? onPageStarted,
void Function(String url)? onPageFinished,
void Function(int progress)? onProgress,
Expand All @@ -63,6 +67,7 @@ class NavigationDelegate {
}) : this.fromPlatformCreationParams(
const PlatformNavigationDelegateCreationParams(),
onNavigationRequest: onNavigationRequest,
onCreateWindow: onCreateWindow,
onPageStarted: onPageStarted,
onPageFinished: onPageFinished,
onProgress: onProgress,
Expand Down Expand Up @@ -107,6 +112,7 @@ class NavigationDelegate {
NavigationDelegate.fromPlatformCreationParams(
PlatformNavigationDelegateCreationParams params, {
FutureOr<NavigationDecision> Function(NavigationRequest request)? onNavigationRequest,
void Function(String url)? onCreateWindow,
void Function(String url)? onPageStarted,
void Function(String url)? onPageFinished,
void Function(int progress)? onProgress,
Expand All @@ -118,6 +124,7 @@ class NavigationDelegate {
}) : this.fromPlatform(
PlatformNavigationDelegate(params),
onNavigationRequest: onNavigationRequest,
onCreateWindow: onCreateWindow,
onPageStarted: onPageStarted,
onPageFinished: onPageFinished,
onProgress: onProgress,
Expand All @@ -134,6 +141,7 @@ class NavigationDelegate {
NavigationDelegate.fromPlatform(
this.platform, {
this.onNavigationRequest,
this.onCreateWindow,
this.onPageStarted,
this.onPageFinished,
this.onProgress,
Expand All @@ -146,6 +154,9 @@ class NavigationDelegate {
if (onNavigationRequest != null) {
platform.setOnNavigationRequest(onNavigationRequest!);
}
if (onCreateWindow != null) {
platform.setOnCreateWindow(onCreateWindow!);
}
if (onPageStarted != null) {
platform.setOnPageStarted(onPageStarted!);
}
Expand Down Expand Up @@ -189,6 +200,12 @@ class NavigationDelegate {
/// See [NavigationDecision].
final NavigationRequestCallback? onNavigationRequest;

/// Invoked when the page requests a new window (`target=_blank` / `window.open`).
///
/// When set, the URL is not loaded in the WebView; the host should handle it
/// (e.g. open the system browser).
final CreateWindowCallback? onCreateWindow;

/// Invoked when a page has started loading.
final PageEventCallback? onPageStarted;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'
show
CreateWindowCallback,
HttpAuthRequest,
HttpResponseError,
HttpResponseErrorCallback,
Expand Down
14 changes: 10 additions & 4 deletions packages/webview_flutter/webview_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter
description: A Flutter plugin that provides a WebView widget backed by the system webview.
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 4.14.1
version: 4.15.0

environment:
sdk: ^3.10.0
Expand All @@ -21,9 +21,9 @@ flutter:
dependencies:
flutter:
sdk: flutter
webview_flutter_android: ^4.12.0
webview_flutter_platform_interface: ^2.15.1
webview_flutter_wkwebview: ^3.25.1
webview_flutter_android: ^4.14.0
webview_flutter_platform_interface: ^2.16.0
webview_flutter_wkwebview: ^3.27.0

dev_dependencies:
build_runner: ^2.1.5
Expand All @@ -36,3 +36,9 @@ topics:
- html
- webview
- webview-flutter
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
webview_flutter_android: {path: ../../../packages/webview_flutter/webview_flutter_android}
webview_flutter_platform_interface: {path: ../../../packages/webview_flutter/webview_flutter_platform_interface}
webview_flutter_wkwebview: {path: ../../../packages/webview_flutter/webview_flutter_wkwebview}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ void main() {
verify(delegate.platform.setOnNavigationRequest(onNavigationRequest));
});

test('onCreateWindow', () async {
WebViewPlatform.instance = TestWebViewPlatform();

void onCreateWindow(String url) {}

final delegate = NavigationDelegate(onCreateWindow: onCreateWindow);

verify(delegate.platform.setOnCreateWindow(onCreateWindow));
});

test('onPageStarted', () async {
WebViewPlatform.instance = TestWebViewPlatform();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ class MockPlatformNavigationDelegate extends _i1.Mock implements _i3.PlatformNav
)
as _i8.Future<void>);

@override
_i8.Future<void> setOnCreateWindow(_i3.CreateWindowCallback? onCreateWindow) =>
(super.noSuchMethod(
Invocation.method(#setOnCreateWindow, [onCreateWindow]),
returnValue: _i8.Future<void>.value(),
returnValueForMissingStub: _i8.Future<void>.value(),
)
as _i8.Future<void>);

@override
_i8.Future<void> setOnPageStarted(_i3.PageEventCallback? onPageStarted) =>
(super.noSuchMethod(
Expand Down
4 changes: 4 additions & 0 deletions packages/webview_flutter/webview_flutter_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.14.0

* Adds NavigationDelegate.onCreateWindow for target=_blank / window.open.

## 4.13.0

* Adds new method for accessing a native `WebView` from a `FlutterPluginBinding`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3566,6 +3566,45 @@ abstract class PigeonApiWebViewClient(
}
}

/** Notifies the host that the page requested a new window (`target=_blank` / `window.open`). */
fun onCreateWindow(
pigeon_instanceArg: android.webkit.WebViewClient,
urlArg: String,
callback: (Result<Unit>) -> Unit
) {
if (pigeonRegistrar.ignoreCallsToDart) {
callback(
Result.failure(
AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
return
} else if (!pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) {
callback(
Result.failure(
AndroidWebKitError(
"missing-instance-error",
"Callback to `WebViewClient.onCreateWindow` failed because native instance was not in the instance manager.",
"")))
return
}
val binaryMessenger = pigeonRegistrar.binaryMessenger
val codec = pigeonRegistrar.codec
val channelName = "dev.flutter.pigeon.webview_flutter_android.WebViewClient.onCreateWindow"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(pigeon_instanceArg, urlArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(
Result.failure(
AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName)))
}
}
}

/** Notify the host application to update its visited links database. */
fun doUpdateVisitedHistory(
pigeon_instanceArg: android.webkit.WebViewClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,18 @@ boolean onCreateWindow(
return false;
}

// Forward new-window navigations (`target=_blank` / `window.open`) to Dart
// via onCreateWindow — same role as WebChromeClient.onCreateWindow.
final WebViewClient windowWebViewClient =
new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(
@NonNull WebView windowWebView, @NonNull WebResourceRequest request) {
if (!webViewClient.shouldOverrideUrlLoading(view, request)) {
view.loadUrl(request.getUrl().toString());
final String url = request.getUrl().toString();
if (webViewClient instanceof WebViewClientProxyApi.WebViewClientImpl) {
((WebViewClientProxyApi.WebViewClientImpl) webViewClient).notifyCreateWindow(url);
} else if (!webViewClient.shouldOverrideUrlLoading(view, request)) {
view.loadUrl(url);
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public WebViewClientImpl(@NonNull WebViewClientProxyApi api) {
this.api = api;
}

/** Notifies Dart that the page requested a new window (`target=_blank` / `window.open`). */
public void notifyCreateWindow(@NonNull String url) {
api.getPigeonRegistrar().runOnMainThread(() -> api.onCreateWindow(this, url, reply -> null));
}

@Override
public void onPageStarted(@NonNull WebView view, @NonNull String url, @NonNull Bitmap favicon) {
api.getPigeonRegistrar()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ public void onCreateWindow() {
verify(mockWebView).loadUrl("https://www.google.com");
}

@Test
public void onCreateWindowWithWebViewClientImplNotifiesDart() {
final WebViewClientProxyApi mockWebViewClientApi = mock(WebViewClientProxyApi.class);
when(mockWebViewClientApi.getPigeonRegistrar()).thenReturn(new TestProxyApiRegistrar());
final WebViewClientProxyApi.WebViewClientImpl webViewClient =
new WebViewClientProxyApi.WebViewClientImpl(mockWebViewClientApi);

final WebView mockOnCreateWindowWebView = mock(WebView.class);
final Message message = new Message();
message.obj = mock(WebViewTransport.class);

final WebChromeClientProxyApi mockApi = mock(WebChromeClientProxyApi.class);
final WebChromeClientImpl instance = new WebChromeClientImpl(mockApi);

final WebView mockWebView = mock(WebView.class);
instance.setWebViewClient(webViewClient);
assertTrue(instance.onCreateWindow(mockWebView, message, mockOnCreateWindowWebView));

final ArgumentCaptor<WebViewClient> webViewClientCaptor =
ArgumentCaptor.forClass(WebViewClient.class);
verify(mockOnCreateWindowWebView).setWebViewClient(webViewClientCaptor.capture());
final WebViewClient onCreateWindowWebViewClient = webViewClientCaptor.getValue();
assertNotNull(onCreateWindowWebViewClient);

final WebResourceRequest mockRequest = mock(WebResourceRequest.class);
when(mockRequest.getUrl()).thenReturn(mock(Uri.class));
when(mockRequest.getUrl().toString()).thenReturn("https://www.google.com");

assertTrue(
onCreateWindowWebViewClient.shouldOverrideUrlLoading(
mockOnCreateWindowWebView, mockRequest));
verify(mockWebViewClientApi)
.onCreateWindow(eq(webViewClient), eq("https://www.google.com"), any());
verify(mockWebView, never()).loadUrl(any());
}

@Test
public void onPermissionRequest() {
final WebChromeClientProxyApi mockApi = mock(WebChromeClientProxyApi.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ public void onPageStarted() {
verify(mockApi).onPageStarted(eq(instance), eq(webView), eq(url), any());
}

@Test
public void onCreateWindow() {
final WebViewClientProxyApi mockApi = mock(WebViewClientProxyApi.class);
when(mockApi.getPigeonRegistrar()).thenReturn(new TestProxyApiRegistrar());

final WebViewClientImpl instance = new WebViewClientImpl(mockApi);
final String url = "https://www.google.com";
instance.notifyCreateWindow(url);

verify(mockApi).onCreateWindow(eq(instance), eq(url), any());
}

@Test
public void onReceivedError() {
final WebViewClientProxyApi mockApi = mock(WebViewClientProxyApi.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,26 +101,28 @@ Future<void> main() async {
);
});

testWidgets('withWeakRefenceTo allows encapsulating class to be garbage collected', (
WidgetTester tester,
) async {
final gcCompleter = Completer<int>();
final instanceManager = android.PigeonInstanceManager(
onWeakReferenceRemoved: gcCompleter.complete,
);
testWidgets(
'withWeakRefenceTo allows encapsulating class to be garbage collected',
(WidgetTester tester) async {
final gcCompleter = Completer<int>();
final instanceManager = android.PigeonInstanceManager(
onWeakReferenceRemoved: gcCompleter.complete,
);

ClassWithCallbackClass? instance = ClassWithCallbackClass();
instanceManager.addHostCreatedInstance(instance.callbackClass, 0);
instance = null;
ClassWithCallbackClass? instance = ClassWithCallbackClass();
instanceManager.addHostCreatedInstance(instance.callbackClass, 0);
instance = null;

// Force garbage collection.
await IntegrationTestWidgetsFlutterBinding.instance.watchPerformance(() async {
await tester.pumpAndSettle();
});
// Force garbage collection.
await IntegrationTestWidgetsFlutterBinding.instance.watchPerformance(() async {
await tester.pumpAndSettle();
});

final int gcIdentifier = await gcCompleter.future;
expect(gcIdentifier, 0);
}, timeout: const Timeout(Duration(seconds: 10)));
final int gcIdentifier = await gcCompleter.future;
expect(gcIdentifier, 0);
},
timeout: const Timeout(Duration(seconds: 10)),
);

// TODO(bparrishMines): This test is skipped because of
// https://github.com/flutter/flutter/issues/123327
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ flutter:
- assets/sample_video.mp4
- assets/www/index.html
- assets/www/styles/style.css
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface}
Loading