Skip to content

Commit

Permalink
[url_launcher] Return false on Windows when there is no handler (#5359)
Browse files Browse the repository at this point in the history
Special-cases the handling of the error for "no registered handler" to return false, rather than throw, which better matches the behavior on other platforms.

Also updates Pigeon to 13 while I'm modifying the Pigeon definition.

Fixes flutter/flutter#138142
  • Loading branch information
stuartmorgan committed Dec 13, 2023
1 parent 73d2f3e commit ea318b9
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 37 deletions.
3 changes: 2 additions & 1 deletion packages/url_launcher/url_launcher_windows/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 3.1.1

* Updates `launchUrl` to return false instead of throwing when there is no handler.
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.

## 3.1.0
Expand Down
28 changes: 23 additions & 5 deletions packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart
Original file line number Diff line number Diff line change
@@ -1,7 +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.
// Autogenerated from Pigeon (v10.1.2), do not edit directly.
// Autogenerated from Pigeon (v13.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import

Expand All @@ -11,6 +11,17 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';

List<Object?> wrapResponse(
{Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
if (error == null) {
return <Object?>[result];
}
return <Object?>[error.code, error.message, error.details];
}

class UrlLauncherApi {
/// Constructor for [UrlLauncherApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
Expand All @@ -23,7 +34,8 @@ class UrlLauncherApi {

Future<bool> canLaunchUrl(String arg_url) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec,
'dev.flutter.pigeon.url_launcher_windows.UrlLauncherApi.canLaunchUrl',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_url]) as List<Object?>?;
Expand All @@ -48,9 +60,10 @@ class UrlLauncherApi {
}
}

Future<void> launchUrl(String arg_url) async {
Future<bool> launchUrl(String arg_url) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec,
'dev.flutter.pigeon.url_launcher_windows.UrlLauncherApi.launchUrl',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_url]) as List<Object?>?;
Expand All @@ -65,8 +78,13 @@ class UrlLauncherApi {
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return;
return (replyList[0] as bool?)!;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ class UrlLauncherWindows extends UrlLauncherPlatform {
required Map<String, String> headers,
String? webOnlyWindowName,
}) async {
await _hostApi.launchUrl(url);
// Failure is handled via a PlatformException from `launchUrl`.
return true;
return _hostApi.launchUrl(url);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ import 'package:pigeon/pigeon.dart';
@HostApi(dartHostTestHandler: 'TestUrlLauncherApi')
abstract class UrlLauncherApi {
bool canLaunchUrl(String url);
void launchUrl(String url);
bool launchUrl(String url);
}
4 changes: 2 additions & 2 deletions packages/url_launcher/url_launcher_windows/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: url_launcher_windows
description: Windows implementation of the url_launcher plugin.
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
version: 3.1.0
version: 3.1.1

environment:
sdk: ">=3.0.0 <4.0.0"
Expand All @@ -24,7 +24,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
pigeon: ^10.1.2
pigeon: ^13.0.0
test: ^1.16.3

topics:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ void main() {
api.canLaunch = true;

expect(
plugin.launch(
await plugin.launch(
'http://example.com/',
useSafariVC: true,
useWebView: false,
Expand All @@ -56,13 +56,30 @@ void main() {
universalLinksOnly: false,
headers: const <String, String>{},
),
completes);
true);
expect(api.argument, 'http://example.com/');
});

test('handles failure', () async {
api.canLaunch = false;

expect(
await plugin.launch(
'http://example.com/',
useSafariVC: true,
useWebView: false,
enableJavaScript: false,
enableDomStorage: false,
universalLinksOnly: false,
headers: const <String, String>{},
),
false);
expect(api.argument, 'http://example.com/');
});

test('handles errors', () async {
api.throwError = true;

await expectLater(
plugin.launch(
'http://example.com/',
Expand Down Expand Up @@ -122,23 +139,27 @@ class _FakeUrlLauncherApi implements UrlLauncherApi {
/// The argument that was passed to an API call.
String? argument;

/// Controls the behavior of the fake implementations.
/// Controls the behavior of the fake canLaunch implementations.
///
/// - [canLaunchUrl] returns this value.
/// - [launchUrl] throws if this is false.
/// - [launchUrl] returns this value if [throwError] is false.
bool canLaunch = false;

/// Whether to throw a platform exception.
bool throwError = false;

@override
Future<bool> canLaunchUrl(String url) async {
argument = url;
return canLaunch;
}

@override
Future<void> launchUrl(String url) async {
Future<bool> launchUrl(String url) async {
argument = url;
if (!canLaunch) {
if (throwError) {
throw PlatformException(code: 'Failed');
}
return canLaunch;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +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.
// Autogenerated from Pigeon (v10.1.2), do not edit directly.
// Autogenerated from Pigeon (v13.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

#undef _HAS_EXCEPTIONS
Expand Down Expand Up @@ -36,7 +36,8 @@ void UrlLauncherApi::SetUp(flutter::BinaryMessenger* binary_messenger,
UrlLauncherApi* api) {
{
auto channel = std::make_unique<BasicMessageChannel<>>(
binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl",
binary_messenger,
"dev.flutter.pigeon.url_launcher_windows.UrlLauncherApi.canLaunchUrl",
&GetCodec());
if (api != nullptr) {
channel->SetMessageHandler(
Expand Down Expand Up @@ -68,7 +69,8 @@ void UrlLauncherApi::SetUp(flutter::BinaryMessenger* binary_messenger,
}
{
auto channel = std::make_unique<BasicMessageChannel<>>(
binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl",
binary_messenger,
"dev.flutter.pigeon.url_launcher_windows.UrlLauncherApi.launchUrl",
&GetCodec());
if (api != nullptr) {
channel->SetMessageHandler(
Expand All @@ -82,13 +84,13 @@ void UrlLauncherApi::SetUp(flutter::BinaryMessenger* binary_messenger,
return;
}
const auto& url_arg = std::get<std::string>(encodable_url_arg);
std::optional<FlutterError> output = api->LaunchUrl(url_arg);
if (output.has_value()) {
reply(WrapError(output.value()));
ErrorOr<bool> output = api->LaunchUrl(url_arg);
if (output.has_error()) {
reply(WrapError(output.error()));
return;
}
EncodableList wrapped;
wrapped.push_back(EncodableValue());
wrapped.push_back(EncodableValue(std::move(output).TakeValue()));
reply(EncodableValue(std::move(wrapped)));
} catch (const std::exception& exception) {
reply(WrapError(exception.what()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +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.
// Autogenerated from Pigeon (v10.1.2), do not edit directly.
// Autogenerated from Pigeon (v13.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

#ifndef PIGEON_MESSAGES_G_H_
Expand Down Expand Up @@ -66,7 +66,7 @@ class UrlLauncherApi {
UrlLauncherApi& operator=(const UrlLauncherApi&) = delete;
virtual ~UrlLauncherApi() {}
virtual ErrorOr<bool> CanLaunchUrl(const std::string& url) = 0;
virtual std::optional<FlutterError> LaunchUrl(const std::string& url) = 0;
virtual ErrorOr<bool> LaunchUrl(const std::string& url) = 0;

// The codec used by UrlLauncherApi.
static const flutter::StandardMessageCodec& GetCodec();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,30 +94,45 @@ TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) {
EXPECT_FALSE(result.value());
}

TEST(UrlLauncherPlugin, LaunchSuccess) {
TEST(UrlLauncherPlugin, LaunchReportsSuccess) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();

// Return a success value (>32) from launching.
EXPECT_CALL(*system, ShellExecuteW)
.WillOnce(Return(reinterpret_cast<HINSTANCE>(33)));

UrlLauncherPlugin plugin(std::move(system));
std::optional<FlutterError> error = plugin.LaunchUrl("https://some.url.com");
ErrorOr<bool> result = plugin.LaunchUrl("https://some.url.com");

EXPECT_FALSE(error.has_value());
ASSERT_FALSE(result.has_error());
EXPECT_TRUE(result.value());
}

TEST(UrlLauncherPlugin, LaunchReportsFailure) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();

// Return a faile value (<=32) from launching.
// Return error 31 from launching, indicating no handler.
EXPECT_CALL(*system, ShellExecuteW)
.WillOnce(Return(reinterpret_cast<HINSTANCE>(SE_ERR_NOASSOC)));

UrlLauncherPlugin plugin(std::move(system));
ErrorOr<bool> result = plugin.LaunchUrl("https://some.url.com");

ASSERT_FALSE(result.has_error());
EXPECT_FALSE(result.value());
}

TEST(UrlLauncherPlugin, LaunchReportsError) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();

// Return a failure value (<=32) from launching.
EXPECT_CALL(*system, ShellExecuteW)
.WillOnce(Return(reinterpret_cast<HINSTANCE>(32)));

UrlLauncherPlugin plugin(std::move(system));
std::optional<FlutterError> error = plugin.LaunchUrl("https://some.url.com");
ErrorOr<bool> result = plugin.LaunchUrl("https://some.url.com");

EXPECT_TRUE(error.has_value());
EXPECT_TRUE(result.has_error());
}

} // namespace test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ ErrorOr<bool> UrlLauncherPlugin::CanLaunchUrl(const std::string& url) {
return has_handler;
}

std::optional<FlutterError> UrlLauncherPlugin::LaunchUrl(
const std::string& url) {
ErrorOr<bool> UrlLauncherPlugin::LaunchUrl(const std::string& url) {
std::wstring url_wide = Utf16FromUtf8(url);

int status = static_cast<int>(reinterpret_cast<INT_PTR>(
Expand All @@ -107,12 +106,18 @@ std::optional<FlutterError> UrlLauncherPlugin::LaunchUrl(

// Per ::ShellExecuteW documentation, anything >32 indicates success.
if (status <= 32) {
if (status == SE_ERR_NOASSOC) {
// NOASSOC just means there's nothing registered to handle launching;
// return false rather than an error for better consistency with other
// platforms.
return false;
}
std::ostringstream error_message;
error_message << "Failed to open " << url << ": ShellExecute error code "
<< status;
return FlutterError("open_error", error_message.str());
}
return std::nullopt;
return true;
}

} // namespace url_launcher_windows
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class UrlLauncherPlugin : public flutter::Plugin, public UrlLauncherApi {

// UrlLauncherApi:
ErrorOr<bool> CanLaunchUrl(const std::string& url) override;
std::optional<FlutterError> LaunchUrl(const std::string& url) override;
ErrorOr<bool> LaunchUrl(const std::string& url) override;

private:
std::unique_ptr<SystemApis> system_apis_;
Expand Down

0 comments on commit ea318b9

Please sign in to comment.