[url_launcher] Decode file URLs before passing it to ShellExecuteW (#7774)

- ShellExecuteW does not handle file: urls that contain %-encoded UTF-8 strings correctly.  %-encoded ASCII
   strings are handled correctly, as are file "urls" that contain Unicode strings in its path component.
- This change perform URL decode on file: urls before passing to ShellExecuteW.

Fixes https://github.com/flutter/flutter/issues/156790
diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
index 1340552..668ee6e 100644
--- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
@@ -1,6 +1,7 @@
-## NEXT
+## 3.1.3
 
 * Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
+* Fixes handling of `file:` URLs that contain UTF-8 encoded paths.
 
 ## 3.1.2
 
diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml
index addd536..629e9dc 100644
--- a/packages/url_launcher/url_launcher_windows/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml
@@ -2,7 +2,7 @@
 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.2
+version: 3.1.3
 
 environment:
   sdk: ^3.3.0
diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
index da39522..2b07f3a 100644
--- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
+++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
@@ -25,7 +25,7 @@
 target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
 target_include_directories(${PLUGIN_NAME} INTERFACE
   "${CMAKE_CURRENT_SOURCE_DIR}/include")
-target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
+target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin shlwapi.lib)
 
 # List of absolute paths to libraries that should be bundled with the plugin
 set(file_chooser_bundled_libraries
@@ -62,7 +62,7 @@
 )
 apply_standard_settings(${TEST_RUNNER})
 target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
-target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
+target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin shlwapi.lib)
 target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
 # flutter_wrapper_plugin has link dependencies on the Flutter DLL.
 add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
index 2672957..db46e1a 100644
--- a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
@@ -22,10 +22,12 @@
 
 using flutter::EncodableMap;
 using flutter::EncodableValue;
+using ::testing::_;
 using ::testing::DoAll;
 using ::testing::Pointee;
 using ::testing::Return;
 using ::testing::SetArgPointee;
+using ::testing::StrEq;
 
 class MockSystemApis : public SystemApis {
  public:
@@ -135,5 +137,28 @@
   EXPECT_TRUE(result.has_error());
 }
 
+TEST(UrlLauncherPlugin, LaunchUTF8EncodedFileURLSuccess) {
+  std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
+
+  // Return a success value (>32) from launching.
+  EXPECT_CALL(
+      *system,
+      ShellExecuteW(
+          _, StrEq(L"open"),
+          // 家の管理/スキャナ"),
+          StrEq(
+              L"file:///G:/\x5bb6\x306e\x7ba1\x7406/\x30b9\x30ad\x30e3\x30ca"),
+          _, _, _))
+      .WillOnce(Return(reinterpret_cast<HINSTANCE>(33)));
+
+  UrlLauncherPlugin plugin(std::move(system));
+  ErrorOr<bool> result = plugin.LaunchUrl(
+      "file:///G:/%E5%AE%B6%E3%81%AE%E7%AE%A1%E7%90%86/"
+      "%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8A");
+
+  ASSERT_FALSE(result.has_error());
+  EXPECT_TRUE(result.value());
+}
+
 }  // namespace test
 }  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
index c8e7b2f..b737b45 100644
--- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
@@ -6,6 +6,7 @@
 #include <flutter/method_channel.h>
 #include <flutter/plugin_registrar_windows.h>
 #include <flutter/standard_method_codec.h>
+#include <shlwapi.h>
 #include <windows.h>
 
 #include <memory>
@@ -98,7 +99,19 @@
 }
 
 ErrorOr<bool> UrlLauncherPlugin::LaunchUrl(const std::string& url) {
-  std::wstring url_wide = Utf16FromUtf8(url);
+  std::wstring url_wide;
+  if (url.find("file:") == 0) {
+    // ShellExecuteW does not process %-encoded UTF8 strings in file URLs.
+    DWORD unescaped_len = 0;
+    std::string unescaped_url = url;
+    if (FAILED(::UrlUnescapeA(unescaped_url.data(), /*pszUnescaped=*/nullptr,
+                              &unescaped_len, URL_UNESCAPE_INPLACE))) {
+      return FlutterError("open_error", "Failed to unescape file URL");
+    }
+    url_wide = Utf16FromUtf8(unescaped_url);
+  } else {
+    url_wide = Utf16FromUtf8(url);
+  }
 
   int status = static_cast<int>(reinterpret_cast<INT_PTR>(
       system_apis_->ShellExecuteW(nullptr, TEXT("open"), url_wide.c_str(),