From d63244175cfeba9ac15dd0af1d1ba3369218f046 Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Fri, 11 Oct 2024 13:23:44 -0700 Subject: [PATCH 01/26] Update Dart to 3.6.0-334.3.beta (#55826) The Dart beta was Re-authored to fix a broken Windows x64 build. The broken build didn't affect flutter, but updating here to be consistent and avoid confusion. --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 92ebc03cfcff8..eb59d89071d1b 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'd916a5f69a486de98316900f19ef0ff46834b03d', + 'dart_revision': 'e7a42fbc90c14c2d2eb1c00d132de202018d3777', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From af0f0d559c8a87d912a20971bbd84afc80a54b0f Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Fri, 18 Oct 2024 10:51:48 -0700 Subject: [PATCH 02/26] Beta fix macos xcprivacy manifest copy location (#55938) We were copying the macOS privacy manifest to the wrong path, which led to codesigning failing. See https://github.com/flutter/flutter/issues/157016#issuecomment-2420786225 for more context. --- shell/platform/darwin/macos/BUILD.gn | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 1224e15e02e5c..3ce4bfc1f0a77 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -253,7 +253,8 @@ copy("copy_framework_module_map") { copy("copy_framework_privacy_manifest") { visibility = [ ":*" ] sources = [ "framework/PrivacyInfo.xcprivacy" ] - outputs = [ "$_flutter_framework_dir/PrivacyInfo.xcprivacy" ] + outputs = + [ "$_flutter_framework_dir/Versions/A/Resources/PrivacyInfo.xcprivacy" ] } action("copy_framework_headers") { From 2ad45d554d559ed043eff8fe18b91f6fc30f6bff Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:58:01 -0800 Subject: [PATCH 03/26] [CP] Starts looking for the bdf fast path in relation to the snapshot_entity's transform (#55890) (#55917) cherry-picks https://github.com/flutter/engine/pull/55890 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../filters/gaussian_blur_filter_contents.cc | 2 +- impeller/geometry/matrix.h | 21 +++++++++++++++++++ impeller/geometry/matrix_unittests.cc | 12 +++++++++++ impeller/geometry/scalar.h | 5 +++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc b/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc index d2e39ead1817b..b70c717394894 100644 --- a/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc +++ b/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc @@ -239,7 +239,7 @@ DownsamplePassArgs CalculateDownsamplePassArgs( // .Contains(coverage_hint.value())) std::optional snapshot_coverage = input_snapshot.GetCoverage(); - if (input_snapshot.transform.IsIdentity() && + if (input_snapshot.transform.Equals(snapshot_entity.GetTransform()) && source_expanded_coverage_hint.has_value() && snapshot_coverage.has_value() && snapshot_coverage->Contains(source_expanded_coverage_hint.value())) { diff --git a/impeller/geometry/matrix.h b/impeller/geometry/matrix.h index 1726c30151fdf..166fc11f9609f 100644 --- a/impeller/geometry/matrix.h +++ b/impeller/geometry/matrix.h @@ -407,6 +407,27 @@ struct Matrix { std::optional Decompose() const; + bool Equals(const Matrix& matrix, Scalar epsilon = 1e-5f) const { + const Scalar* a = m; + const Scalar* b = matrix.m; + return ScalarNearlyEqual(a[0], b[0], epsilon) && + ScalarNearlyEqual(a[1], b[1], epsilon) && + ScalarNearlyEqual(a[2], b[2], epsilon) && + ScalarNearlyEqual(a[3], b[3], epsilon) && + ScalarNearlyEqual(a[4], b[4], epsilon) && + ScalarNearlyEqual(a[5], b[5], epsilon) && + ScalarNearlyEqual(a[6], b[6], epsilon) && + ScalarNearlyEqual(a[7], b[7], epsilon) && + ScalarNearlyEqual(a[8], b[8], epsilon) && + ScalarNearlyEqual(a[9], b[9], epsilon) && + ScalarNearlyEqual(a[10], b[10], epsilon) && + ScalarNearlyEqual(a[11], b[11], epsilon) && + ScalarNearlyEqual(a[12], b[12], epsilon) && + ScalarNearlyEqual(a[13], b[13], epsilon) && + ScalarNearlyEqual(a[14], b[14], epsilon) && + ScalarNearlyEqual(a[15], b[15], epsilon); + } + constexpr bool operator==(const Matrix& m) const { // clang-format off return vec[0] == m.vec[0] diff --git a/impeller/geometry/matrix_unittests.cc b/impeller/geometry/matrix_unittests.cc index ac160086994e3..91a2a50f2a7d4 100644 --- a/impeller/geometry/matrix_unittests.cc +++ b/impeller/geometry/matrix_unittests.cc @@ -25,6 +25,18 @@ TEST(MatrixTest, Multiply) { 11.0, 21.0, 0.0, 1.0))); } +TEST(MatrixTest, Equals) { + Matrix x; + Matrix y = x; + EXPECT_TRUE(x.Equals(y)); +} + +TEST(MatrixTest, NotEquals) { + Matrix x; + Matrix y = x.Translate({1, 0, 0}); + EXPECT_FALSE(x.Equals(y)); +} + TEST(MatrixTest, HasPerspective2D) { EXPECT_FALSE(Matrix().HasPerspective2D()); diff --git a/impeller/geometry/scalar.h b/impeller/geometry/scalar.h index 2600a49c42dc5..dadc52850cb5f 100644 --- a/impeller/geometry/scalar.h +++ b/impeller/geometry/scalar.h @@ -22,6 +22,11 @@ constexpr T Absolute(const T& val) { return val >= T{} ? val : -val; } +template <> +constexpr Scalar Absolute(const float& val) { + return fabsf(val); +} + constexpr inline bool ScalarNearlyZero(Scalar x, Scalar tolerance = kEhCloseEnough) { return Absolute(x) <= tolerance; From 77db57690506c6adfbfb40b34e5bc975e93cc589 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 5 Nov 2024 10:01:34 -0800 Subject: [PATCH 04/26] [CP-beta] Fix memory leak during gif upload on Android/Impeller (#55928) Cherry pick of https://github.com/flutter/engine/pull/55920 Issues: https://github.com/flutter/engine/pull/55928 --- lib/ui/painting/image_decoder_impeller.cc | 3 +++ lib/ui/painting/image_decoder_unittests.cc | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/lib/ui/painting/image_decoder_impeller.cc b/lib/ui/painting/image_decoder_impeller.cc index adb06b435fcb7..6a1118954a9be 100644 --- a/lib/ui/painting/image_decoder_impeller.cc +++ b/lib/ui/painting/image_decoder_impeller.cc @@ -483,6 +483,9 @@ ImageDecoderImpeller::UploadTextureToStorage( } texture->SetLabel(impeller::SPrintF("ui.Image(%p)", texture.get()).c_str()); + + context->DisposeThreadLocalCachedResources(); + return std::make_pair(impeller::DlImageImpeller::Make(std::move(texture)), std::string()); } diff --git a/lib/ui/painting/image_decoder_unittests.cc b/lib/ui/painting/image_decoder_unittests.cc index d8f76671ae09b..69096eeca3866 100644 --- a/lib/ui/painting/image_decoder_unittests.cc +++ b/lib/ui/painting/image_decoder_unittests.cc @@ -92,8 +92,12 @@ class TestImpellerContext : public impeller::Context { tasks_.clear(); } + void DisposeThreadLocalCachedResources() override { did_dispose_ = true; } + void Shutdown() override {} + bool DidDisposeResources() const { return did_dispose_; } + mutable size_t command_buffer_count_ = 0; private: @@ -103,6 +107,7 @@ class TestImpellerContext : public impeller::Context { }; std::vector tasks_; std::shared_ptr capabilities_; + bool did_dispose_ = false; }; } // namespace impeller @@ -367,12 +372,14 @@ TEST_F(ImageDecoderFixtureTest, ImpellerUploadToSharedNoGpu) { EXPECT_EQ(no_gpu_access_context->command_buffer_count_, 0ul); EXPECT_FALSE(invoked); + EXPECT_EQ(no_gpu_access_context->DidDisposeResources(), false); auto result = ImageDecoderImpeller::UploadTextureToStorage( no_gpu_access_context, bitmap); ASSERT_EQ(no_gpu_access_context->command_buffer_count_, 0ul); ASSERT_EQ(result.second, ""); + EXPECT_EQ(no_gpu_access_context->DidDisposeResources(), true); no_gpu_access_context->FlushTasks(/*fail=*/true); } From 9080aa8961066a9dffac9f497ba407ec083a7933 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:11:03 -0800 Subject: [PATCH 05/26] [cp] Removed clamping from dithering (#56140) (#56174) tests: https://github.com/flutter/flutter/pull/157643 fixes revert from https://github.com/flutter/flutter/pull/153976 The pixel format will clamp if it needs to anyway so this isn't necessary. https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. You must list at least one issue.* *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../shader_lib/impeller/dithering.glsl | 3 --- impeller/tools/malioc.json | 24 +++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/impeller/compiler/shader_lib/impeller/dithering.glsl b/impeller/compiler/shader_lib/impeller/dithering.glsl index e8e4e8439ce67..5494c4ecee45c 100644 --- a/impeller/compiler/shader_lib/impeller/dithering.glsl +++ b/impeller/compiler/shader_lib/impeller/dithering.glsl @@ -45,9 +45,6 @@ vec4 IPOrderedDither8x8(vec4 color, vec2 dest) { // Apply the dither to the color. color.rgb += dither * kDitherRate; - // Clamp the color values to [0,1]. - color.rgb = clamp(color.rgb, 0.0, 1.0); - return color; } diff --git a/impeller/tools/malioc.json b/impeller/tools/malioc.json index aafabbc2af84b..787254a4fa0e5 100644 --- a/impeller/tools/malioc.json +++ b/impeller/tools/malioc.json @@ -586,7 +586,7 @@ "shortest_path_cycles": [ 0.5, 0.109375, - 0.296875, + 0.28125, 0.5, 0.0, 0.25, @@ -596,9 +596,9 @@ "load_store" ], "total_cycles": [ - 1.4249999523162842, + 1.40625, 0.862500011920929, - 1.4249999523162842, + 1.40625, 0.875, 4.0, 0.25, @@ -636,7 +636,7 @@ "longest_path_cycles": [ 0.5, 0.109375, - 0.125, + 0.109375, 0.5, 0.0, 0.25, @@ -658,7 +658,7 @@ "shortest_path_cycles": [ 0.5, 0.109375, - 0.125, + 0.109375, 0.5, 0.0, 0.25, @@ -671,7 +671,7 @@ "total_cycles": [ 0.5, 0.109375, - 0.125, + 0.109375, 0.5, 0.0, 0.25, @@ -6298,7 +6298,7 @@ "shortest_path_cycles": [ 0.5625, 0.203125, - 0.265625, + 0.25, 0.5625, 0.0, 0.25, @@ -6308,9 +6308,9 @@ "load_store" ], "total_cycles": [ - 0.71875, + 0.699999988079071, 0.40625, - 0.71875, + 0.699999988079071, 0.5625, 4.0, 0.25, @@ -6768,7 +6768,7 @@ "shortest_path_cycles": [ 0.5625, 0.21875, - 0.28125, + 0.265625, 0.5625, 0.0, 0.25, @@ -6778,9 +6778,9 @@ "load_store" ], "total_cycles": [ - 0.71875, + 0.699999988079071, 0.421875, - 0.71875, + 0.699999988079071, 0.625, 4.0, 0.25, From 8b78a34828af65e87e3e897619b956bcd2a21503 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 5 Nov 2024 11:54:54 -0800 Subject: [PATCH 06/26] [CP] Disable thread merging for 3.27 release branch. (#56378) Remove the thread merging behavior on iOS for the 3.27 release branch as its not yet fully baked. --- .../darwin/ios/framework/Source/FlutterEngine.mm | 13 +++---------- .../ios/framework/Source/FlutterEngineTest.mm | 8 ++++---- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 713d4fd0e288f..01ee1847ea97c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -792,10 +792,8 @@ + (NSString*)generateThreadLabel:(NSString*)labelPrefix { // initialized. fml::MessageLoop::EnsureInitializedForCurrentThread(); - uint32_t threadHostType = flutter::ThreadHost::Type::kRaster | flutter::ThreadHost::Type::kIo; - if (!settings.enable_impeller) { - threadHostType |= flutter::ThreadHost::Type::kUi; - } + uint32_t threadHostType = flutter::ThreadHost::Type::kRaster | flutter::ThreadHost::Type::kIo | + flutter::ThreadHost::Type::kUi; if ([FlutterEngine isProfilerEnabled]) { threadHostType = threadHostType | flutter::ThreadHost::Type::kProfiler; @@ -877,12 +875,7 @@ - (BOOL)createShell:(NSString*)entrypoint flutter::Shell::CreateCallback on_create_rasterizer = [](flutter::Shell& shell) { return std::make_unique(shell); }; - fml::RefPtr ui_runner; - if (settings.enable_impeller) { - ui_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); - } else { - ui_runner = _threadHost->ui_thread->GetTaskRunner(); - } + fml::RefPtr ui_runner = _threadHost->ui_thread->GetTaskRunner(); flutter::TaskRunners task_runners(threadLabel.UTF8String, // label fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform _threadHost->raster_thread->GetTaskRunner(), // raster diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 26ad1e1c52f55..aeab1dff7ae40 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -477,8 +477,8 @@ - (void)testCanMergePlatformAndUIThread { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; [engine run]; - XCTAssertEqual(engine.shell.GetTaskRunners().GetUITaskRunner(), - engine.shell.GetTaskRunners().GetPlatformTaskRunner()); + XCTAssertNotEqual(engine.shell.GetTaskRunners().GetUITaskRunner(), + engine.shell.GetTaskRunners().GetPlatformTaskRunner()); } - (void)testCanNotUnMergePlatformAndUIThread { @@ -488,8 +488,8 @@ - (void)testCanNotUnMergePlatformAndUIThread { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; [engine run]; - XCTAssertEqual(engine.shell.GetTaskRunners().GetUITaskRunner(), - engine.shell.GetTaskRunners().GetPlatformTaskRunner()); + XCTAssertNotEqual(engine.shell.GetTaskRunners().GetUITaskRunner(), + engine.shell.GetTaskRunners().GetPlatformTaskRunner()); } @end From ada71c7e27943c0c3071b44558c9d3aaa8a32953 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 5 Nov 2024 11:54:57 -0800 Subject: [PATCH 07/26] [CP][Impeller] disable Adreno 506. (#56210) (#56216) Fixes https://github.com/flutter/flutter/issues/155185 --- impeller/renderer/backend/vulkan/driver_info_vk.cc | 6 +++++- .../renderer/backend/vulkan/driver_info_vk_unittests.cc | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/impeller/renderer/backend/vulkan/driver_info_vk.cc b/impeller/renderer/backend/vulkan/driver_info_vk.cc index e0fdb5728d44e..d0ce457fa6a02 100644 --- a/impeller/renderer/backend/vulkan/driver_info_vk.cc +++ b/impeller/renderer/backend/vulkan/driver_info_vk.cc @@ -332,13 +332,17 @@ bool DriverInfoVK::IsKnownBadDriver() const { if (adreno_gpu_.has_value()) { auto adreno = adreno_gpu_.value(); switch (adreno) { - // see: + // See: // https://github.com/flutter/flutter/issues/154103 // // Reports "VK_INCOMPLETE" when compiling certain entity shader with // vkCreateGraphicsPipelines, which is not a valid return status. // See https://github.com/flutter/flutter/issues/155185 . case AdrenoGPU::kAdreno630: + // See: + // https://github.com/flutter/flutter/issues/155185 + // Unknown crashes but device is not easily acquirable. + case AdrenoGPU::kAdreno506: return true; default: return false; diff --git a/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc b/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc index 59f0733b8b4e4..e95f76c67acfd 100644 --- a/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc +++ b/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc @@ -87,9 +87,10 @@ TEST(DriverInfoVKTest, EnabledDevicesAdreno) { EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 512")); EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 509")); EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 508")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 506")); EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 505")); EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 504")); + + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 506")); } } // namespace impeller::testing From e929345856a0b80f3b4a10899a92699322de8626 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 5 Nov 2024 11:55:00 -0800 Subject: [PATCH 08/26] [CP][Impeller] disable AHBs on devices that were upgraded to 29. (#56202) (#56208) Uses `ro.product.first_api_level` to disable AHBs on devices that began life pre 29. Fixes https://github.com/flutter/flutter/issues/157113 --- impeller/toolkit/android/shadow_realm.cc | 12 +++++++++++- impeller/toolkit/android/shadow_realm.h | 1 + .../toolkit/android/toolkit_android_unittests.cc | 16 +++++++++++----- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/impeller/toolkit/android/shadow_realm.cc b/impeller/toolkit/android/shadow_realm.cc index 6d19e6fe459f4..312f45c116a74 100644 --- a/impeller/toolkit/android/shadow_realm.cc +++ b/impeller/toolkit/android/shadow_realm.cc @@ -15,13 +15,23 @@ bool ShadowRealm::ShouldDisableAHB() { __system_property_get("ro.com.google.clientidbase", clientidbase); auto api_level = android_get_device_api_level(); + char first_api_level[PROP_VALUE_MAX]; + __system_property_get("ro.product.first_api_level", first_api_level); - return ShouldDisableAHBInternal(clientidbase, api_level); + return ShouldDisableAHBInternal(clientidbase, first_api_level, api_level); } // static bool ShadowRealm::ShouldDisableAHBInternal(std::string_view clientidbase, + std::string_view first_api_level, uint32_t api_level) { + // Most devices that have updated to API 29 don't seem to correctly + // support AHBs: https://github.com/flutter/flutter/issues/157113 + if (first_api_level == "28" || first_api_level == "27" || + first_api_level == "26" || first_api_level == "25" || + first_api_level == "24") { + return true; + } // From local testing, neither the swapchain nor AHB import works, see also: // https://github.com/flutter/flutter/issues/154068 if (clientidbase == kAndroidHuawei && api_level <= 29) { diff --git a/impeller/toolkit/android/shadow_realm.h b/impeller/toolkit/android/shadow_realm.h index 90f913b5af3bc..90f265ec612b6 100644 --- a/impeller/toolkit/android/shadow_realm.h +++ b/impeller/toolkit/android/shadow_realm.h @@ -18,6 +18,7 @@ class ShadowRealm { // For testing. static bool ShouldDisableAHBInternal(std::string_view clientidbase, + std::string_view first_api_level, uint32_t api_level); }; diff --git a/impeller/toolkit/android/toolkit_android_unittests.cc b/impeller/toolkit/android/toolkit_android_unittests.cc index c96e04c2dfe9c..ca8837d732ee2 100644 --- a/impeller/toolkit/android/toolkit_android_unittests.cc +++ b/impeller/toolkit/android/toolkit_android_unittests.cc @@ -136,11 +136,17 @@ TEST(ToolkitAndroidTest, CanPostAndWaitForFrameCallbacks) { } TEST(ToolkitAndroidTest, ShouldDisableAHB) { - EXPECT_FALSE(ShadowRealm::ShouldDisableAHB()); - - EXPECT_TRUE(ShadowRealm::ShouldDisableAHBInternal("android-huawei", 29)); - EXPECT_FALSE(ShadowRealm::ShouldDisableAHBInternal("android-huawei", 30)); - EXPECT_FALSE(ShadowRealm::ShouldDisableAHBInternal("something made up", 29)); + EXPECT_FALSE( + ShadowRealm::ShouldDisableAHBInternal("android-huawei", "30", 30)); + EXPECT_FALSE( + ShadowRealm::ShouldDisableAHBInternal("something made up", "29", 29)); + + EXPECT_TRUE( + ShadowRealm::ShouldDisableAHBInternal("android-huawei", "29", 29)); + EXPECT_TRUE( + ShadowRealm::ShouldDisableAHBInternal("something made up", "27", 29)); + EXPECT_TRUE( + ShadowRealm::ShouldDisableAHBInternal("android-huawei", "garbage", 29)); } } // namespace impeller::android::testing From dcb260da0db36c3c4a8f607dfb31f25443573f46 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 5 Nov 2024 11:58:02 -0800 Subject: [PATCH 09/26] [CP][Impeller] disable Maleoon GPU from using Vulkan. (#56203) (#56207) Fixes https://github.com/flutter/flutter/issues/156623 using info from https://vulkan.gpuinfo.org/displayreport.php?id=23730 . I dont believe these vulkan drivers are working correctly. --- impeller/renderer/backend/vulkan/driver_info_vk.cc | 5 +++++ .../backend/vulkan/driver_info_vk_unittests.cc | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/impeller/renderer/backend/vulkan/driver_info_vk.cc b/impeller/renderer/backend/vulkan/driver_info_vk.cc index d0ce457fa6a02..ca7cef979984c 100644 --- a/impeller/renderer/backend/vulkan/driver_info_vk.cc +++ b/impeller/renderer/backend/vulkan/driver_info_vk.cc @@ -348,6 +348,11 @@ bool DriverInfoVK::IsKnownBadDriver() const { return false; } } + // Disable Maleoon series GPUs, see: + // https://github.com/flutter/flutter/issues/156623 + if (vendor_ == VendorVK::kHuawei) { + return true; + } return false; } diff --git a/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc b/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc index e95f76c67acfd..9f9dc1d192f29 100644 --- a/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc +++ b/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc @@ -36,6 +36,19 @@ TEST_P(DriverInfoVKTest, CanDumpToLog) { EXPECT_TRUE(log.str().find("Driver Information") != std::string::npos); } +TEST(DriverInfoVKTest, CanIdentifyBadMaleoonDriver) { + auto const context = + MockVulkanContextBuilder() + .SetPhysicalPropertiesCallback( + [](VkPhysicalDevice device, VkPhysicalDeviceProperties* prop) { + prop->vendorID = 0x19E5; // Huawei + prop->deviceType = VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU; + }) + .Build(); + + EXPECT_TRUE(context->GetDriverInfo()->IsKnownBadDriver()); +} + bool IsBadVersionTest(std::string_view driver_name, bool qc = true) { auto const context = MockVulkanContextBuilder() From ccfd94b3a250e40355d6f9cc503d1baae9cbd1fd Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 12 Nov 2024 16:45:05 -0800 Subject: [PATCH 10/26] [CP] Roll Skia to 6062afaa505bf7e6c727a20cafe4c7bee0f02df8 (#56546) This picks up https://skia-review.googlesource.com/c/skia/+/919137 which fixes an iOS App Store rejection issue. Issue: https://github.com/flutter/flutter/issues/158423 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- DEPS | 2 +- ci/licenses_golden/licenses_skia | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPS b/DEPS index eb59d89071d1b..69be7a3286ce5 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '1349ddc074ad616c6660eb29f75dafe18f3714c0', + 'skia_revision': '6062afaa505bf7e6c727a20cafe4c7bee0f02df8', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index f80744d86965d..75834b7b17053 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: e41e121402f1b734a9825aaa785d49a8 +Signature: 086870b9a7d2aa20c37a6b0ade0856e2 ==================================================================================================== LIBRARY: etc1 From 397deba30fcb592f17dfb31b4e9e31e17fbfae9a Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Thu, 14 Nov 2024 09:51:50 -0800 Subject: [PATCH 11/26] [flutter_releases] Flutter beta 3.27.0-0.2.pre Engine Cherrypicks (#56583) # Flutter beta 3.27.0-0.2.pre Engine ## Scheduled Cherrypicks - Roll dart revision: dart-lang/sdk@847db142d --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 69be7a3286ce5..23ab816d03db7 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'e7a42fbc90c14c2d2eb1c00d132de202018d3777', + 'dart_revision': '847db142d385db9e561e68f20f4eb849466b0007', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 2416efe43ad0d8b5b818991cf17dee68bc61908b Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 6 Dec 2024 15:53:56 -0800 Subject: [PATCH 12/26] [CP][iOS] fix tracking of prev platform view items. (#56589) Ensures that all platform views items are tracked in the previous_composition_order_ set. Without this, they can occasionally be left on screen. --- .../darwin/ios/framework/Source/platform_views_controller.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/platform_views_controller.mm b/shell/platform/darwin/ios/framework/Source/platform_views_controller.mm index 7f8a73294953c..1afc84a0ad291 100644 --- a/shell/platform/darwin/ios/framework/Source/platform_views_controller.mm +++ b/shell/platform/darwin/ios/framework/Source/platform_views_controller.mm @@ -796,6 +796,7 @@ bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, previous_composition_order_.clear(); NSMutableArray* desired_platform_subviews = [NSMutableArray array]; for (int64_t platform_view_id : composition_order) { + previous_composition_order_.push_back(platform_view_id); UIView* platform_view_root = platform_views_[platform_view_id].root_view.get(); if (platform_view_root != nil) { [desired_platform_subviews addObject:platform_view_root]; @@ -806,7 +807,6 @@ bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, auto view = maybe_layer_data->second.layer->overlay_view_wrapper; if (view != nil) { [desired_platform_subviews addObject:view]; - previous_composition_order_.push_back(platform_view_id); } } } From 85033b0a2be39e69e7396db6115d79d2a11bcfff Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Mon, 9 Dec 2024 11:10:25 -0800 Subject: [PATCH 13/26] [CP-beta] Drop APNG frames that don't fit entirely within the destination surface. (#57062) This cherry-pick PR includes: https://github.com/flutter/engine/pull/56928 followed by https://github.com/flutter/engine/pull/57025 It supersedes https://github.com/flutter/engine/pull/56978. ### Issue Link: What is the link to the issue this cherry-pick is addressing? Issue was reported over email. ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Fixes an out-of-bounds memory write in APNG decoding. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) Fixes an issue in which an untrusted malformed APNG image could cause out of bounds memory writes, crashing the app. ### Workaround: Is there a workaround for this issue? There is no workaround. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Attempt to load the APNG used in the tests in the PR. --- lib/ui/fixtures/out_of_bounds.apng | Bin 0 -> 126 bytes lib/ui/fixtures/out_of_bounds_wrapping.apng | Bin 0 -> 126 bytes .../painting/image_decoder_no_gl_unittests.cc | 2 +- lib/ui/painting/image_generator_apng.cc | 36 +++++++++++++++- testing/dart/codec_test.dart | 40 ++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 lib/ui/fixtures/out_of_bounds.apng create mode 100644 lib/ui/fixtures/out_of_bounds_wrapping.apng diff --git a/lib/ui/fixtures/out_of_bounds.apng b/lib/ui/fixtures/out_of_bounds.apng new file mode 100644 index 0000000000000000000000000000000000000000..33993c7960e96e3d39b3d0b123ad3558ac9e6e6c GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nRj>O~;A0W*L!iTkPngMC4G>90G z3DE#z2!IGikh;U`H*o^l+@3CuAr*6y6CBtX7=#!Y%Rl}CDPiz*^>bP0l+XkKIP(-M literal 0 HcmV?d00001 diff --git a/lib/ui/fixtures/out_of_bounds_wrapping.apng b/lib/ui/fixtures/out_of_bounds_wrapping.apng new file mode 100644 index 0000000000000000000000000000000000000000..e4cc5d50cfe5f69ba28d6b3676aae19df9e21b8e GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nRj>O~;A0W*L!iTkPngMC4G>90G z3DE#z{09P{DxkU_B}+1Z0^FW1jv*Cuk`o-*85o2Z7|TEY0;yy0boFyt=akR{0R7Au AVgLXD literal 0 HcmV?d00001 diff --git a/lib/ui/painting/image_decoder_no_gl_unittests.cc b/lib/ui/painting/image_decoder_no_gl_unittests.cc index d935fa2e3e9f9..e9b8c208d0b09 100644 --- a/lib/ui/painting/image_decoder_no_gl_unittests.cc +++ b/lib/ui/painting/image_decoder_no_gl_unittests.cc @@ -187,7 +187,7 @@ TEST(ImageDecoderNoGLTest, ImpellerWideGamutIndexedPng) { #endif // IMPELLER_SUPPORTS_RENDERING } -TEST(ImageDecoderNoGLTest, ImepllerUnmultipliedAlphaPng) { +TEST(ImageDecoderNoGLTest, ImpellerUnmultipliedAlphaPng) { #if defined(OS_FUCHSIA) GTEST_SKIP() << "Fuchsia can't load the test fixtures."; #endif diff --git a/lib/ui/painting/image_generator_apng.cc b/lib/ui/painting/image_generator_apng.cc index 159d87638aeb2..08182c9ff2bd4 100644 --- a/lib/ui/painting/image_generator_apng.cc +++ b/lib/ui/painting/image_generator_apng.cc @@ -110,6 +110,28 @@ bool APNGImageGenerator::GetPixels(const SkImageInfo& info, << ") of APNG due to the frame missing data (frame_info)."; return false; } + if ( + // Check for unsigned integer wrapping for + // frame.{x|y}_offset + frame_info.{width|height}(). + frame.x_offset > + std::numeric_limits::max() - frame_info.width() || + frame.y_offset > + std::numeric_limits::max() - frame_info.height() || + + frame.x_offset + frame_info.width() > + static_cast(info.width()) || + frame.y_offset + frame_info.height() > + static_cast(info.height())) { + FML_DLOG(ERROR) + << "Decoded image at index " << image_index + << " (frame index: " << frame_index + << ") rejected because the destination region (x: " << frame.x_offset + << ", y: " << frame.y_offset << ", width: " << frame_info.width() + << ", height: " << frame_info.height() + << ") is not entirely within the destination surface (width: " + << info.width() << ", height: " << info.height() << ")."; + return false; + } //---------------------------------------------------------------------------- /// 3. Composite the frame onto the canvas. @@ -630,7 +652,19 @@ uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() { bool APNGImageGenerator::RenderDefaultImage(const SkImageInfo& info, void* pixels, size_t row_bytes) { - SkCodec::Result result = images_[0].codec->getPixels(info, pixels, row_bytes); + APNGImage& frame = images_[0]; + SkImageInfo frame_info = frame.codec->getInfo(); + if (frame_info.width() > info.width() || + frame_info.height() > info.height()) { + FML_DLOG(ERROR) + << "Default image rejected because the destination region (width: " + << frame_info.width() << ", height: " << frame_info.height() + << ") is not entirely within the destination surface (width: " + << info.width() << ", height: " << info.height() << ")."; + return false; + } + + SkCodec::Result result = frame.codec->getPixels(info, pixels, row_bytes); if (result != SkCodec::kSuccess) { FML_DLOG(ERROR) << "Failed to decode the APNG's default/fallback image. " "SkCodec::Result: " diff --git a/testing/dart/codec_test.dart b/testing/dart/codec_test.dart index e21b099a8cee0..7d0a2a23d2758 100644 --- a/testing/dart/codec_test.dart +++ b/testing/dart/codec_test.dart @@ -252,6 +252,46 @@ void main() { imageData = (await image.toByteData())!; expect(imageData.getUint32(imageData.lengthInBytes - 4), 0x00000000); }); + + test( + 'Animated apng frame decode does not crash with invalid destination region', + () async { + final Uint8List data = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'out_of_bounds.apng'), + ).readAsBytesSync(); + + final ui.Codec codec = await ui.instantiateImageCodec(data); + try { + await codec.getNextFrame(); + fail('exception not thrown'); + } on Exception catch (e) { + if (impellerEnabled) { + expect(e.toString(), contains('Could not decompress image.')); + } else { + expect(e.toString(), contains('Codec failed')); + } + } + }); + + test( + 'Animated apng frame decode does not crash with invalid destination region and bounds wrapping', + () async { + final Uint8List data = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'out_of_bounds_wrapping.apng'), + ).readAsBytesSync(); + + final ui.Codec codec = await ui.instantiateImageCodec(data); + try { + await codec.getNextFrame(); + fail('exception not thrown'); + } on Exception catch (e) { + if (impellerEnabled) { + expect(e.toString(), contains('Could not decompress image.')); + } else { + expect(e.toString(), contains('Codec failed')); + } + } + }); } /// Returns a File handle to a file in the skia/resources directory. From 6cfcdbf55e1ec86806bd09e24a4da3c04e092aa2 Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:48:17 -0800 Subject: [PATCH 14/26] [CP-beta][MANUAL][ios][platform_view] workaround for non-tappable webview #57030 (#57034) Manual version of https://github.com/flutter/engine/pull/57030#issuecomment-2524607775 Since I can't push it to the original branch. This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/158961 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Fix an issue on iOS 18.2 where web view's link is not tappable. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) Web view's link is not tappable on iOS 18.2 ### Workaround: Is there a workaround for this issue? No workaround ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? 1. Launch web view plugin's example app 2. Tap on `...` menu on top left corner 3. Tap on any link on web view. The link is working with the fix, and not working without the fix. --- shell/platform/darwin/ios/BUILD.gn | 1 + .../Source/FlutterPlatformViewsTest.mm | 160 +++++++++++++++++- .../Source/FlutterPlatformViews_Internal.h | 39 +++++ .../Source/FlutterPlatformViews_Internal.mm | 57 ++----- 4 files changed, 217 insertions(+), 40 deletions(-) diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 6bd0397b26b9f..ab58295121949 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -222,6 +222,7 @@ source_set("flutter_framework_source") { "CoreMedia.framework", "CoreVideo.framework", "QuartzCore.framework", + "WebKit.framework", "UIKit.framework", ] if (flutter_runtime_mode == "profile" || flutter_runtime_mode == "debug") { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 904a3eace2564..3faf41321e9fb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -4,6 +4,7 @@ #import #import +#import #import #include "fml/synchronization/count_down_latch.h" #include "shell/platform/darwin/ios/framework/Source/platform_views_controller.h" @@ -20,7 +21,7 @@ FLUTTER_ASSERT_ARC @class FlutterPlatformViewsTestMockPlatformView; -__weak static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil; +__weak static UIView* gMockPlatformView = nil; const float kFloatCompareEpsilon = 0.001; @interface FlutterPlatformViewsTestMockPlatformView : UIView @@ -83,6 +84,45 @@ @implementation FlutterPlatformViewsTestMockFlutterPlatformFactory @end +@interface FlutterPlatformViewsTestMockWebView : NSObject +@property(nonatomic, strong) UIView* view; +@property(nonatomic, assign) BOOL viewCreated; +@end + +@implementation FlutterPlatformViewsTestMockWebView +- (instancetype)init { + if (self = [super init]) { + _view = [[WKWebView alloc] init]; + gMockPlatformView = _view; + _viewCreated = NO; + } + return self; +} + +- (UIView*)view { + [self checkViewCreatedOnce]; + return _view; +} + +- (void)checkViewCreatedOnce { + if (self.viewCreated) { + abort(); + } + self.viewCreated = YES; +} +@end + +@interface FlutterPlatformViewsTestMockWebViewFactory : NSObject +@end + +@implementation FlutterPlatformViewsTestMockWebViewFactory +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FlutterPlatformViewsTestMockWebView alloc] init]; +} +@end + @interface FlutterPlatformViewsTestNilFlutterPlatformFactory : NSObject @end @@ -2782,6 +2822,124 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { flutterPlatformViewsController->Reset(); } +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + auto flutterPlatformViewsController = std::make_shared(); + flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner()); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockWebViewFactory* factory = + [[FlutterPlatformViewsTestMockWebViewFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockWebView", FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockWebView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + // Find touch inteceptor view + UIView* touchInteceptorView = gMockPlatformView; + while (touchInteceptorView != nil && + ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) { + touchInteceptorView = touchInteceptorView.superview; + } + XCTAssertNotNil(touchInteceptorView); + + XCTAssert(touchInteceptorView.gestureRecognizers.count == 2); + UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0]; + UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1]; + + XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]); + XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]); + + [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture]; + + if (@available(iOS 18.2, *)) { + // Since we remove and add back delayingRecognizer, it would be reordered to the last. + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], forwardingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], delayingRecognizer); + } else { + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer); + } +} + +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNonWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + auto flutterPlatformViewsController = std::make_shared(); + flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner()); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + // Find touch inteceptor view + UIView* touchInteceptorView = gMockPlatformView; + while (touchInteceptorView != nil && + ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) { + touchInteceptorView = touchInteceptorView.superview; + } + XCTAssertNotNil(touchInteceptorView); + + XCTAssert(touchInteceptorView.gestureRecognizers.count == 2); + UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0]; + UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1]; + + XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]); + XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]); + + [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture]; + + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer); +} + - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index b42c267074d4f..d9d9097cbe8d3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -160,4 +160,43 @@ @property(nonatomic, readonly) BOOL flt_hasFirstResponderInViewHierarchySubtree; @end +// This recognizer delays touch events from being dispatched to the responder chain until it failed +// recognizing a gesture. +// +// We only fail this recognizer when asked to do so by the Flutter framework (which does so by +// invoking an acceptGesture method on the platform_views channel). And this is how we allow the +// Flutter framework to delay or prevent the embedded view from getting a touch sequence. +@interface FlutterDelayingGestureRecognizer : UIGestureRecognizer + +// Indicates that if the `FlutterDelayingGestureRecognizer`'s state should be set to +// `UIGestureRecognizerStateEnded` during next `touchesEnded` call. +@property(nonatomic) BOOL shouldEndInNextTouchesEnded; + +// Indicates that the `FlutterDelayingGestureRecognizer`'s `touchesEnded` has been invoked without +// setting the state to `UIGestureRecognizerStateEnded`. +@property(nonatomic) BOOL touchedEndedWithoutBlocking; + +@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer; + +- (instancetype)initWithTarget:(id)target + action:(SEL)action + forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer; +@end + +// While the FlutterDelayingGestureRecognizer is preventing touches from hitting the responder chain +// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter +// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView +// while during this phase. +// +// If the Flutter framework decides to dispatch events to the embedded view, we fail the +// FlutterDelayingGestureRecognizer which sends the events up the responder chain. But since the +// events are handled by the embedded view they are not delivered to the Flutter framework in this +// phase as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events +// directly to the FlutterView. +@interface ForwardingGestureRecognizer : UIGestureRecognizer +- (instancetype)initWithTarget:(id)target + platformViewsController: + (fml::WeakPtr)platformViewsController; +@end + #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 5e76654bed179..1439591c1157a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -4,6 +4,8 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import + #include "flutter/display_list/effects/dl_image_filter.h" #include "flutter/fml/platform/darwin/cf_utils.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" @@ -510,45 +512,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } @end -// This recognizer delays touch events from being dispatched to the responder chain until it failed -// recognizing a gesture. -// -// We only fail this recognizer when asked to do so by the Flutter framework (which does so by -// invoking an acceptGesture method on the platform_views channel). And this is how we allow the -// Flutter framework to delay or prevent the embedded view from getting a touch sequence. -@interface FlutterDelayingGestureRecognizer : UIGestureRecognizer - -// Indicates that if the `FlutterDelayingGestureRecognizer`'s state should be set to -// `UIGestureRecognizerStateEnded` during next `touchesEnded` call. -@property(nonatomic) BOOL shouldEndInNextTouchesEnded; - -// Indicates that the `FlutterDelayingGestureRecognizer`'s `touchesEnded` has been invoked without -// setting the state to `UIGestureRecognizerStateEnded`. -@property(nonatomic) BOOL touchedEndedWithoutBlocking; - -@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer; - -- (instancetype)initWithTarget:(id)target - action:(SEL)action - forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer; -@end - -// While the FlutterDelayingGestureRecognizer is preventing touches from hitting the responder chain -// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter -// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView -// while during this phase. -// -// If the Flutter framework decides to dispatch events to the embedded view, we fail the -// FlutterDelayingGestureRecognizer which sends the events up the responder chain. But since the -// events are handled by the embedded view they are not delivered to the Flutter framework in this -// phase as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events -// directly to the FlutterView. -@interface ForwardingGestureRecognizer : UIGestureRecognizer -- (instancetype)initWithTarget:(id)target - platformViewsController: - (fml::WeakPtr)platformViewsController; -@end - @interface FlutterTouchInterceptingView () @property(nonatomic, weak, readonly) UIView* embeddedView; @property(nonatomic, readonly) FlutterDelayingGestureRecognizer* delayingRecognizer; @@ -595,6 +558,22 @@ - (void)blockGesture { case FlutterPlatformViewGestureRecognizersBlockingPolicyEager: // We block all other gesture recognizers immediately in this policy. self.delayingRecognizer.state = UIGestureRecognizerStateEnded; + + // On iOS 18.2, WKWebView's internal recognizer likely caches the old state of its blocking + // recognizers (i.e. delaying recognizer), resulting in non-tappable links. See + // https://github.com/flutter/flutter/issues/158961. Removing and adding back the delaying + // recognizer solves the problem, possibly because UIKit notifies all the recognizers related + // to (blocking or blocked by) this recognizer. It is not possible to inject this workaround + // from the web view plugin level. Right now we only observe this issue for + // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar + // issue arises for the other policy. + if (@available(iOS 18.2, *)) { + if ([self.embeddedView isKindOfClass:[WKWebView class]]) { + [self removeGestureRecognizer:self.delayingRecognizer]; + [self addGestureRecognizer:self.delayingRecognizer]; + } + } + break; case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded: if (self.delayingRecognizer.touchedEndedWithoutBlocking) { From 45ac4d6c6214098cb8e17041a74d210fdf983b73 Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Mon, 9 Dec 2024 12:51:48 -0800 Subject: [PATCH 15/26] [flutter_releases] Flutter stable 3.27.0 Engine Cherrypicks (#57035) # Flutter stable 3.27.0 Engine ## Scheduled Cherrypicks - Roll dart revision: dart-lang/sdk@ae7ca5199 --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 23ab816d03db7..eb091429de1f3 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '847db142d385db9e561e68f20f4eb849466b0007', + 'dart_revision': 'ae7ca5199a0559db0ae60533e9cedd3ce0d6ab04', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py From 83bacfc52569459a4a654727cad2546820cb0d6a Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 10 Dec 2024 13:22:09 -0500 Subject: [PATCH 16/26] [cp:beta][web] Work around wrong pointerId in coalesced events in iOS Safari 18.2 (#56719) (#56905) Manual cherry pick for https://github.com/flutter/engine/pull/56719 Cherrypick request: https://github.com/flutter/flutter/issues/159692 --- .../lib/src/engine/pointer_binding.dart | 33 ++++- .../event_position_helper.dart | 18 ++- .../test/engine/pointer_binding_test.dart | 139 +++++++++++++++++- 3 files changed, 174 insertions(+), 16 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 11502593eb97e..228b30a7f5a19 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -1010,20 +1010,32 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }); // Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp - _addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent event) { - final int device = _getPointerId(event); + _addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent moveEvent) { + final int device = _getPointerId(moveEvent); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); final List pointerData = []; - final List expandedEvents = _expandEvents(event); + final List expandedEvents = _expandEvents(moveEvent); for (final DomPointerEvent event in expandedEvents) { final _SanitizedDetails? up = sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!.toInt()); if (up != null) { - _convertEventsToPointerData(data: pointerData, event: event, details: up); + _convertEventsToPointerData( + data: pointerData, + event: event, + details: up, + pointerId: device, + eventTarget: moveEvent.target, + ); } final _SanitizedDetails move = sanitizer.sanitizeMoveEvent(buttons: event.buttons!.toInt()); - _convertEventsToPointerData(data: pointerData, event: event, details: move); + _convertEventsToPointerData( + data: pointerData, + event: event, + details: move, + pointerId: device, + eventTarget: moveEvent.target, + ); } - _callback(event, pointerData); + _callback(moveEvent, pointerData); }); _addPointerEventListener(_viewTarget, 'pointerleave', (DomPointerEvent event) { @@ -1077,12 +1089,17 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { required List data, required DomPointerEvent event, required _SanitizedDetails details, + // `pointerId` and `eventTarget` are optional but useful when it's not + // desired to get those values from the event object. For example, when the + // event is a coalesced event. + int? pointerId, + DomEventTarget? eventTarget, }) { final ui.PointerDeviceKind kind = _pointerTypeToDeviceKind(event.pointerType!); final double tilt = _computeHighestTilt(event); final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!); final num? pressure = event.pressure; - final ui.Offset offset = computeEventOffsetToTarget(event, _view); + final ui.Offset offset = computeEventOffsetToTarget(event, _view, eventTarget: eventTarget); _pointerDataConverter.convert( data, viewId: _view.viewId, @@ -1090,7 +1107,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { timeStamp: timeStamp, kind: kind, signalKind: ui.PointerSignalKind.none, - device: _getPointerId(event), + device: pointerId ?? _getPointerId(event), physicalX: offset.dx * _view.devicePixelRatio, physicalY: offset.dy * _view.devicePixelRatio, buttons: details.buttons, diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 319a3620cd741..494f940d58eb4 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -12,18 +12,23 @@ import '../text_editing/text_editing.dart'; import '../vector_math.dart'; import '../window.dart'; -/// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget]. +/// Returns an [ui.Offset] of the position of [event], relative to the position +/// of the Flutter [view]. /// /// The offset is *not* multiplied by DPR or anything else, it's the closest /// to what the DOM would return if we had currentTarget readily available. /// -/// This needs an `actualTarget`, because the `event.currentTarget` (which is what -/// this would really need to use) gets lost when the `event` comes from a "coalesced" -/// event. +/// This needs an `eventTarget`, because the `event.target` (which is what +/// this would really need to use) gets lost when the `event` comes from a +/// "coalesced" event (see https://github.com/flutter/flutter/issues/155987). /// /// It also takes into account semantics being enabled to fix the case where /// offsetX, offsetY == 0 (TalkBack events). -ui.Offset computeEventOffsetToTarget(DomMouseEvent event, EngineFlutterView view) { +ui.Offset computeEventOffsetToTarget( + DomMouseEvent event, + EngineFlutterView view, { + DomEventTarget? eventTarget, +}) { final DomElement actualTarget = view.dom.rootElement; // On a TalkBack event if (EngineSemantics.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { @@ -31,7 +36,8 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, EngineFlutterView view } // On one of our text-editing nodes - final bool isInput = view.dom.textEditingHost.contains(event.target! as DomNode); + eventTarget ??= event.target!; + final bool isInput = view.dom.textEditingHost.contains(eventTarget as DomNode); if (isInput) { final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry; if (inputGeometry != null) { diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index f765c492f7226..374ed872f428e 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -2526,6 +2526,88 @@ void testMain() { }, ); + test('ignores pointerId on coalesced events', () { + final _MultiPointerEventMixin context = _PointerEventContext(); + final List packets = []; + List data; + ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + context.multiTouchDown(const <_TouchDetails>[ + _TouchDetails(pointer: 52, clientX: 100, clientY: 101), + ]).forEach(rootElement.dispatchEvent); + expect(packets.length, 1); + + data = packets.single.data; + expect(data, hasLength(2)); + expect(data[0].change, equals(ui.PointerChange.add)); + expect(data[0].synthesized, isTrue); + expect(data[0].device, equals(52)); + expect(data[0].physicalX, equals(100 * dpi)); + expect(data[0].physicalY, equals(101 * dpi)); + + expect(data[1].change, equals(ui.PointerChange.down)); + expect(data[1].device, equals(52)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(100 * dpi)); + expect(data[1].physicalY, equals(101 * dpi)); + expect(data[1].physicalDeltaX, equals(0)); + expect(data[1].physicalDeltaY, equals(0)); + packets.clear(); + + // Pointer move with coaleasced events + context.multiTouchMove(const <_TouchDetails>[ + _TouchDetails(pointer: 52, coalescedEvents: <_CoalescedTouchDetails>[ + _CoalescedTouchDetails(pointer: 0, clientX: 301, clientY: 302), + _CoalescedTouchDetails(pointer: 0, clientX: 401, clientY: 402), + ]), + ]).forEach(rootElement.dispatchEvent); + expect(packets.length, 1); + + data = packets.single.data; + expect(data, hasLength(2)); + expect(data[0].change, equals(ui.PointerChange.move)); + expect(data[0].device, equals(52)); + expect(data[0].buttons, equals(1)); + expect(data[0].physicalX, equals(301 * dpi)); + expect(data[0].physicalY, equals(302 * dpi)); + expect(data[0].physicalDeltaX, equals(201 * dpi)); + expect(data[0].physicalDeltaY, equals(201 * dpi)); + + expect(data[1].change, equals(ui.PointerChange.move)); + expect(data[1].device, equals(52)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(401 * dpi)); + expect(data[1].physicalY, equals(402 * dpi)); + expect(data[1].physicalDeltaX, equals(100 * dpi)); + expect(data[1].physicalDeltaY, equals(100 * dpi)); + packets.clear(); + + // Pointer up + context.multiTouchUp(const <_TouchDetails>[ + _TouchDetails(pointer: 52, clientX: 401, clientY: 402), + ]).forEach(rootElement.dispatchEvent); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(2)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].device, equals(52)); + expect(packets[0].data[0].buttons, equals(0)); + expect(packets[0].data[0].physicalX, equals(401 * dpi)); + expect(packets[0].data[0].physicalY, equals(402 * dpi)); + expect(packets[0].data[0].physicalDeltaX, equals(0)); + expect(packets[0].data[0].physicalDeltaY, equals(0)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.remove)); + expect(packets[0].data[1].device, equals(52)); + expect(packets[0].data[1].buttons, equals(0)); + expect(packets[0].data[1].physicalX, equals(401 * dpi)); + expect(packets[0].data[1].physicalY, equals(402 * dpi)); + expect(packets[0].data[1].physicalDeltaX, equals(0)); + expect(packets[0].data[1].physicalDeltaY, equals(0)); + packets.clear(); + }); + test( 'correctly parses cancel event', () { @@ -3336,7 +3418,26 @@ mixin _ButtonedEventMixin on _BasicEventContext { } class _TouchDetails { - const _TouchDetails({this.pointer, this.clientX, this.clientY}); + const _TouchDetails({ + this.pointer, + this.clientX, + this.clientY, + this.coalescedEvents, + }); + + final int? pointer; + final double? clientX; + final double? clientY; + + final List<_CoalescedTouchDetails>? coalescedEvents; +} + +class _CoalescedTouchDetails { + const _CoalescedTouchDetails({ + this.pointer, + this.clientX, + this.clientY, + }); final int? pointer; final double? clientX; @@ -3395,6 +3496,10 @@ class _PointerEventContext extends _BasicEventContext @override List multiTouchDown(List<_TouchDetails> touches) { + assert( + touches.every((_TouchDetails details) => details.coalescedEvents == null), + 'Coalesced events are not allowed for pointerdown events.', + ); return touches .map((_TouchDetails details) => _downWithFullDetails( pointer: details.pointer, @@ -3458,6 +3563,7 @@ class _PointerEventContext extends _BasicEventContext clientX: details.clientX, clientY: details.clientY, pointerType: 'touch', + coalescedEvents: details.coalescedEvents, )) .toList(); } @@ -3487,8 +3593,9 @@ class _PointerEventContext extends _BasicEventContext int? buttons, int? pointer, String? pointerType, + List<_CoalescedTouchDetails>? coalescedEvents, }) { - return createDomPointerEvent('pointermove', { + final event = createDomPointerEvent('pointermove', { 'bubbles': true, 'pointerId': pointer, 'button': button, @@ -3497,6 +3604,26 @@ class _PointerEventContext extends _BasicEventContext 'clientY': clientY, 'pointerType': pointerType, }); + + if (coalescedEvents != null) { + // There's no JS API for setting coalesced events, so we need to + // monkey-patch the `getCoalescedEvents` method to return what we want. + final coalescedEventJs = coalescedEvents + .map((_CoalescedTouchDetails details) => _moveWithFullDetails( + pointer: details.pointer, + button: button, + buttons: buttons, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )).toJSAnyDeep; + + js_util.setProperty(event, 'getCoalescedEvents', js_util.allowInterop(() { + return coalescedEventJs; + })); + } + + return event; } @override @@ -3537,6 +3664,10 @@ class _PointerEventContext extends _BasicEventContext @override List multiTouchUp(List<_TouchDetails> touches) { + assert( + touches.every((_TouchDetails details) => details.coalescedEvents == null), + 'Coalesced events are not allowed for pointerup events.', + ); return touches .map((_TouchDetails details) => _upWithFullDetails( pointer: details.pointer, @@ -3587,6 +3718,10 @@ class _PointerEventContext extends _BasicEventContext @override List multiTouchCancel(List<_TouchDetails> touches) { + assert( + touches.every((_TouchDetails details) => details.coalescedEvents == null), + 'Coalesced events are not allowed for pointercancel events.', + ); return touches .map((_TouchDetails details) => createDomPointerEvent('pointercancel', { From eed6905acd14de93f76d2dc34d336235f56d29eb Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:40:38 -0800 Subject: [PATCH 17/26] [CP][3.27][ios]enable the webview non tappable workaround by checking subviews recursively #57168 (#57172) CP for https://github.com/flutter/engine/pull/57168 and https://github.com/flutter/engine/pull/57193 *List which issues are fixed by this PR. You must list at least one issue.* https://github.com/flutter/flutter/issues/158961 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../Source/FlutterPlatformViewsTest.mm | 213 ++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.mm | 23 +- 2 files changed, 235 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 3faf41321e9fb..b91fde939a861 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -69,6 +69,9 @@ - (void)checkViewCreatedOnce { self.viewCreated = YES; } +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockFlutterPlatformFactory @@ -110,6 +113,10 @@ - (void)checkViewCreatedOnce { } self.viewCreated = YES; } + +- (void)dealloc { + gMockPlatformView = nil; +} @end @interface FlutterPlatformViewsTestMockWebViewFactory : NSObject @@ -135,6 +142,93 @@ @implementation FlutterPlatformViewsTestNilFlutterPlatformFactory @end +@interface FlutterPlatformViewsTestMockWrapperWebView : NSObject +@property(nonatomic, strong) UIView* view; +@property(nonatomic, assign) BOOL viewCreated; +@end + +@implementation FlutterPlatformViewsTestMockWrapperWebView +- (instancetype)init { + if (self = [super init]) { + _view = [[UIView alloc] init]; + [_view addSubview:[[WKWebView alloc] init]]; + gMockPlatformView = _view; + _viewCreated = NO; + } + return self; +} + +- (UIView*)view { + [self checkViewCreatedOnce]; + return _view; +} + +- (void)checkViewCreatedOnce { + if (self.viewCreated) { + abort(); + } + self.viewCreated = YES; +} + +- (void)dealloc { + gMockPlatformView = nil; +} +@end + +@interface FlutterPlatformViewsTestMockWrapperWebViewFactory : NSObject +@end + +@implementation FlutterPlatformViewsTestMockWrapperWebViewFactory +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FlutterPlatformViewsTestMockWrapperWebView alloc] init]; +} +@end + +@interface FlutterPlatformViewsTestMockNestedWrapperWebView : NSObject +@property(nonatomic, strong) UIView* view; +@property(nonatomic, assign) BOOL viewCreated; +@end + +@implementation FlutterPlatformViewsTestMockNestedWrapperWebView +- (instancetype)init { + if (self = [super init]) { + _view = [[UIView alloc] init]; + UIView* childView = [[UIView alloc] init]; + [_view addSubview:childView]; + [childView addSubview:[[WKWebView alloc] init]]; + gMockPlatformView = _view; + _viewCreated = NO; + } + return self; +} + +- (UIView*)view { + [self checkViewCreatedOnce]; + return _view; +} + +- (void)checkViewCreatedOnce { + if (self.viewCreated) { + abort(); + } + self.viewCreated = YES; +} +@end + +@interface FlutterPlatformViewsTestMockNestedWrapperWebViewFactory + : NSObject +@end + +@implementation FlutterPlatformViewsTestMockNestedWrapperWebViewFactory +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FlutterPlatformViewsTestMockNestedWrapperWebView alloc] init]; +} +@end + namespace flutter { namespace { class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::Delegate { @@ -2883,6 +2977,125 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled { } } +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWrapperWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + auto flutterPlatformViewsController = std::make_shared(); + flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner()); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockWrapperWebViewFactory* factory = + [[FlutterPlatformViewsTestMockWrapperWebViewFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockWrapperWebView", FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockWrapperWebView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + // Find touch inteceptor view + UIView* touchInteceptorView = gMockPlatformView; + while (touchInteceptorView != nil && + ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) { + touchInteceptorView = touchInteceptorView.superview; + } + XCTAssertNotNil(touchInteceptorView); + + XCTAssert(touchInteceptorView.gestureRecognizers.count == 2); + UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0]; + UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1]; + + XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]); + XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]); + + [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture]; + + if (@available(iOS 18.2, *)) { + // Since we remove and add back delayingRecognizer, it would be reordered to the last. + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], forwardingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], delayingRecognizer); + } else { + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer); + } +} + +- (void) + testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNestedWrapperWebView { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/GetDefaultTaskRunner(), + /*raster=*/GetDefaultTaskRunner(), + /*ui=*/GetDefaultTaskRunner(), + /*io=*/GetDefaultTaskRunner()); + auto flutterPlatformViewsController = std::make_shared(); + flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner()); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockNestedWrapperWebViewFactory* factory = + [[FlutterPlatformViewsTestMockNestedWrapperWebViewFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockNestedWrapperWebView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockNestedWrapperWebView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + + // Find touch inteceptor view + UIView* touchInteceptorView = gMockPlatformView; + while (touchInteceptorView != nil && + ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) { + touchInteceptorView = touchInteceptorView.superview; + } + XCTAssertNotNil(touchInteceptorView); + + XCTAssert(touchInteceptorView.gestureRecognizers.count == 2); + UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0]; + UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1]; + + XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]); + XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]); + + [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture]; + + XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer); + XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer); +} + - (void) testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNonWebView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 1439591c1157a..7071183caa3b7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -553,6 +553,21 @@ - (void)releaseGesture { self.delayingRecognizer.state = UIGestureRecognizerStateFailed; } +- (BOOL)containsWebView:(UIView*)view remainingSubviewDepth:(int)remainingSubviewDepth { + if (remainingSubviewDepth < 0) { + return NO; + } + if ([view isKindOfClass:[WKWebView class]]) { + return YES; + } + for (UIView* subview in view.subviews) { + if ([self containsWebView:subview remainingSubviewDepth:remainingSubviewDepth - 1]) { + return YES; + } + } + return NO; +} + - (void)blockGesture { switch (_blockingPolicy) { case FlutterPlatformViewGestureRecognizersBlockingPolicyEager: @@ -568,7 +583,13 @@ - (void)blockGesture { // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar // issue arises for the other policy. if (@available(iOS 18.2, *)) { - if ([self.embeddedView isKindOfClass:[WKWebView class]]) { + // This workaround is designed for WKWebView only. The 1P web view plugin provides a + // WKWebView itself as the platform view. However, some 3P plugins provide wrappers of + // WKWebView instead. So we perform DFS to search the view hierarchy (with a depth limit). + // Passing a limit of 0 means only searching for platform view itself; Pass 1 to include its + // children as well, and so on. We should be conservative and start with a small number. The + // AdMob banner has a WKWebView at depth 7. + if ([self containsWebView:self.embeddedView remainingSubviewDepth:1]) { [self removeGestureRecognizer:self.delayingRecognizer]; [self addGestureRecognizer:self.delayingRecognizer]; } From 57618faade3a13600350b1b4d6fc90264e904df3 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 13 Dec 2024 15:26:22 -0800 Subject: [PATCH 18/26] [CP][Android] Disable SurfaceControl based swapchains. (#57173) Cherry pick for https://github.com/flutter/flutter/issues/160206 FYI @zanderso / @chinmaygarde --- .../backend/vulkan/swapchain/swapchain_vk.cc | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc b/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc index 1b872778dbc33..195b06f663e43 100644 --- a/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc +++ b/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc @@ -56,32 +56,6 @@ std::shared_ptr SwapchainVK::Create( return nullptr; } - // TODO(147533): AHB swapchains on emulators are not functional. - auto& context_vk = ContextVK::Cast(*context); - const auto emulator = context_vk.GetDriverInfo()->IsEmulator(); - const auto should_disable_sc = - context_vk.GetShouldDisableSurfaceControlSwapchain(); - - // Try AHB swapchains first. - if (!emulator && AHBSwapchainVK::IsAvailableOnPlatform() && - !android::ShadowRealm::ShouldDisableAHB() && !should_disable_sc) { - auto ahb_swapchain = std::shared_ptr(new AHBSwapchainVK( - context, // - window.GetHandle(), // - surface, // - window.GetSize(), // - enable_msaa // - )); - - if (ahb_swapchain->IsValid()) { - return ahb_swapchain; - } else { - VALIDATION_LOG - << "Could not create AHB swapchain. Falling back to KHR variant."; - } - } - - // Fallback to KHR swapchains if AHB swapchains aren't available. return Create(context, std::move(surface), window.GetSize(), enable_msaa); } #endif // FML_OS_ANDROID From cb4b5fff73850b2e42bd4de7cb9a4310a78ac40d Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 13 Dec 2024 15:49:11 -0800 Subject: [PATCH 19/26] [CP][Impeller] disable all Adrenos older than 630 (#57069) (#57100) Cherry pick for https://github.com/flutter/flutter/issues/160041 --- .../renderer/backend/vulkan/driver_info_vk.cc | 40 ++++++++++++++----- .../vulkan/driver_info_vk_unittests.cc | 20 ++++++---- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/impeller/renderer/backend/vulkan/driver_info_vk.cc b/impeller/renderer/backend/vulkan/driver_info_vk.cc index ca7cef979984c..fc96c828b613b 100644 --- a/impeller/renderer/backend/vulkan/driver_info_vk.cc +++ b/impeller/renderer/backend/vulkan/driver_info_vk.cc @@ -330,19 +330,39 @@ bool DriverInfoVK::IsEmulator() const { bool DriverInfoVK::IsKnownBadDriver() const { if (adreno_gpu_.has_value()) { - auto adreno = adreno_gpu_.value(); + AdrenoGPU adreno = adreno_gpu_.value(); + // See: + // https://github.com/flutter/flutter/issues/154103 + // + // Reports "VK_INCOMPLETE" when compiling certain entity shader with + // vkCreateGraphicsPipelines, which is not a valid return status. + // See https://github.com/flutter/flutter/issues/155185 . + // + // https://github.com/flutter/flutter/issues/155185 + // Unknown crashes but device is not easily acquirable. switch (adreno) { - // See: - // https://github.com/flutter/flutter/issues/154103 - // - // Reports "VK_INCOMPLETE" when compiling certain entity shader with - // vkCreateGraphicsPipelines, which is not a valid return status. - // See https://github.com/flutter/flutter/issues/155185 . + case AdrenoGPU::kAdreno640: case AdrenoGPU::kAdreno630: - // See: - // https://github.com/flutter/flutter/issues/155185 - // Unknown crashes but device is not easily acquirable. + case AdrenoGPU::kAdreno620: + case AdrenoGPU::kAdreno619: + case AdrenoGPU::kAdreno619L: + case AdrenoGPU::kAdreno618: + case AdrenoGPU::kAdreno616: + case AdrenoGPU::kAdreno615: + case AdrenoGPU::kAdreno613: + case AdrenoGPU::kAdreno612: + case AdrenoGPU::kAdreno610: + case AdrenoGPU::kAdreno608: + case AdrenoGPU::kAdreno605: + case AdrenoGPU::kAdreno540: + case AdrenoGPU::kAdreno530: + case AdrenoGPU::kAdreno512: + case AdrenoGPU::kAdreno510: + case AdrenoGPU::kAdreno509: + case AdrenoGPU::kAdreno508: case AdrenoGPU::kAdreno506: + case AdrenoGPU::kAdreno505: + case AdrenoGPU::kAdreno504: return true; default: return false; diff --git a/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc b/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc index 9f9dc1d192f29..67f07815320af 100644 --- a/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc +++ b/impeller/renderer/backend/vulkan/driver_info_vk_unittests.cc @@ -80,6 +80,18 @@ TEST(DriverInfoVKTest, DriverParsingArm) { TEST(DriverInfoVKTest, DisabledDevices) { EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 630")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 620")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 610")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 530")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 512")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 509")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 508")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 506")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 505")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 504")); + EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 640")); + + EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 650")); } TEST(DriverInfoVKTest, EnabledDevicesMali) { @@ -96,14 +108,6 @@ TEST(DriverInfoVKTest, EnabledDevicesAdreno) { EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 720")); EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 710")); EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 702")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 530")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 512")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 509")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 508")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 505")); - EXPECT_FALSE(IsBadVersionTest("Adreno (TM) 504")); - - EXPECT_TRUE(IsBadVersionTest("Adreno (TM) 506")); } } // namespace impeller::testing From 8c485775137bb234f179d22d83847156dccf0bc0 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:03:15 -0800 Subject: [PATCH 20/26] [CP-stable]Move detection of cutouts in Android engine to `onApplyWindowInsets` (#56379) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/158192 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples https://github.com/flutter/engine/pull/55992. - Positions of display cutouts on Android may not update - as returned by `MediaQuery` and used by `SafeArea` - upon screen orientation change. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) The positions of display cutouts as reported by `MediaQuery` would not properly update on screen orientation. ### Workaround: Is there a workaround for this issue? N/A ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Run the code sample provided by https://github.com/flutter/flutter/issues/155658 on Android and change the screen orientation. --- .../embedding/android/FlutterView.java | 59 +++--- .../engine/renderer/FlutterRenderer.java | 66 +++++-- .../embedding/android/FlutterViewTest.java | 179 +++++++++++++++--- .../engine/renderer/FlutterRendererTest.java | 22 ++- 4 files changed, 251 insertions(+), 75 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 0d5ae3a8efc5f..69f54010e4d04 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -194,13 +194,7 @@ public void onFlutterUiNoLongerDisplayed() { } }; - private final Consumer windowInfoListener = - new Consumer() { - @Override - public void accept(WindowLayoutInfo layoutInfo) { - setWindowInfoListenerDisplayFeatures(layoutInfo); - } - }; + private Consumer windowInfoListener; /** * Constructs a {@code FlutterView} programmatically, without any XML attributes. @@ -512,6 +506,10 @@ protected void onAttachedToWindow() { this.windowInfoRepo = createWindowInfoRepo(); Activity activity = ViewUtils.getActivity(getContext()); if (windowInfoRepo != null && activity != null) { + // Creating windowInfoListener on-demand instead of at initialization is necessary in order to + // prevent it from capturing the wrong instance of `this` when spying for testing. + // See https://github.com/mockito/mockito/issues/3479 + windowInfoListener = this::setWindowInfoListenerDisplayFeatures; windowInfoRepo.addWindowLayoutInfoListener( activity, ContextCompat.getMainExecutor(getContext()), windowInfoListener); } @@ -524,9 +522,10 @@ protected void onAttachedToWindow() { */ @Override protected void onDetachedFromWindow() { - if (windowInfoRepo != null) { + if (windowInfoRepo != null && windowInfoListener != null) { windowInfoRepo.removeWindowLayoutInfoListener(windowInfoListener); } + windowInfoListener = null; this.windowInfoRepo = null; super.onDetachedFromWindow(); } @@ -537,12 +536,12 @@ protected void onDetachedFromWindow() { */ @TargetApi(API_LEVELS.API_28) protected void setWindowInfoListenerDisplayFeatures(WindowLayoutInfo layoutInfo) { - List displayFeatures = layoutInfo.getDisplayFeatures(); - List result = new ArrayList<>(); + List newDisplayFeatures = layoutInfo.getDisplayFeatures(); + List flutterDisplayFeatures = new ArrayList<>(); // Data from WindowInfoTracker display features. Fold and hinge areas are // populated here. - for (DisplayFeature displayFeature : displayFeatures) { + for (DisplayFeature displayFeature : newDisplayFeatures) { Log.v( TAG, "WindowInfoTracker Display Feature reported with bounds = " @@ -565,31 +564,17 @@ protected void setWindowInfoListenerDisplayFeatures(WindowLayoutInfo layoutInfo) } else { state = DisplayFeatureState.UNKNOWN; } - result.add(new FlutterRenderer.DisplayFeature(displayFeature.getBounds(), type, state)); + flutterDisplayFeatures.add( + new FlutterRenderer.DisplayFeature(displayFeature.getBounds(), type, state)); } else { - result.add( + flutterDisplayFeatures.add( new FlutterRenderer.DisplayFeature( displayFeature.getBounds(), DisplayFeatureType.UNKNOWN, DisplayFeatureState.UNKNOWN)); } } - - // Data from the DisplayCutout bounds. Cutouts for cameras and other sensors are - // populated here. DisplayCutout was introduced in API 28. - if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) { - WindowInsets insets = getRootWindowInsets(); - if (insets != null) { - DisplayCutout cutout = insets.getDisplayCutout(); - if (cutout != null) { - for (Rect bounds : cutout.getBoundingRects()) { - Log.v(TAG, "DisplayCutout area reported with bounds = " + bounds.toString()); - result.add(new FlutterRenderer.DisplayFeature(bounds, DisplayFeatureType.CUTOUT)); - } - } - } - } - viewportMetrics.displayFeatures = result; + viewportMetrics.setDisplayFeatures(flutterDisplayFeatures); sendViewportMetricsToFlutter(); } @@ -782,6 +767,22 @@ navigationBarVisible && guessBottomKeyboardInset(insets) == 0 viewportMetrics.viewInsetLeft = 0; } + // Data from the DisplayCutout bounds. Cutouts for cameras and other sensors are + // populated here. DisplayCutout was introduced in API 28. + List displayCutouts = new ArrayList<>(); + if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) { + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + for (Rect bounds : cutout.getBoundingRects()) { + Log.v(TAG, "DisplayCutout area reported with bounds = " + bounds.toString()); + displayCutouts.add( + new FlutterRenderer.DisplayFeature( + bounds, DisplayFeatureType.CUTOUT, DisplayFeatureState.UNKNOWN)); + } + } + } + viewportMetrics.setDisplayCutouts(displayCutouts); + // The caption bar inset is a new addition, and the APIs called to query it utilize a list of // bounding Rects instead of an Insets object, which is a newer API method, as compared to the // existing Insets-based method calls above. diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java index f66ea69fc94e7..0009c062744ea 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -1137,6 +1137,13 @@ public void stopRenderingToSurface() { } } + private void translateFeatureBounds(int[] displayFeatureBounds, int offset, Rect bounds) { + displayFeatureBounds[offset] = bounds.left; + displayFeatureBounds[offset + 1] = bounds.top; + displayFeatureBounds[offset + 2] = bounds.right; + displayFeatureBounds[offset + 3] = bounds.bottom; + } + /** * Notifies Flutter that the viewport metrics, e.g. window height and width, have changed. * @@ -1187,20 +1194,31 @@ public void setViewportMetrics(@NonNull ViewportMetrics viewportMetrics) { + viewportMetrics.systemGestureInsetRight + "\n" + "Display Features: " - + viewportMetrics.displayFeatures.size()); - - int[] displayFeaturesBounds = new int[viewportMetrics.displayFeatures.size() * 4]; - int[] displayFeaturesType = new int[viewportMetrics.displayFeatures.size()]; - int[] displayFeaturesState = new int[viewportMetrics.displayFeatures.size()]; + + viewportMetrics.displayFeatures.size() + + "\n" + + "Display Cutouts: " + + viewportMetrics.displayCutouts.size()); + + int totalFeaturesAndCutouts = + viewportMetrics.displayFeatures.size() + viewportMetrics.displayCutouts.size(); + int[] displayFeaturesBounds = new int[totalFeaturesAndCutouts * 4]; + int[] displayFeaturesType = new int[totalFeaturesAndCutouts]; + int[] displayFeaturesState = new int[totalFeaturesAndCutouts]; for (int i = 0; i < viewportMetrics.displayFeatures.size(); i++) { DisplayFeature displayFeature = viewportMetrics.displayFeatures.get(i); - displayFeaturesBounds[4 * i] = displayFeature.bounds.left; - displayFeaturesBounds[4 * i + 1] = displayFeature.bounds.top; - displayFeaturesBounds[4 * i + 2] = displayFeature.bounds.right; - displayFeaturesBounds[4 * i + 3] = displayFeature.bounds.bottom; + translateFeatureBounds(displayFeaturesBounds, 4 * i, displayFeature.bounds); displayFeaturesType[i] = displayFeature.type.encodedValue; displayFeaturesState[i] = displayFeature.state.encodedValue; } + int cutoutOffset = viewportMetrics.displayFeatures.size() * 4; + for (int i = 0; i < viewportMetrics.displayCutouts.size(); i++) { + DisplayFeature displayCutout = viewportMetrics.displayCutouts.get(i); + translateFeatureBounds(displayFeaturesBounds, cutoutOffset + 4 * i, displayCutout.bounds); + displayFeaturesType[viewportMetrics.displayFeatures.size() + i] = + displayCutout.type.encodedValue; + displayFeaturesState[viewportMetrics.displayFeatures.size() + i] = + displayCutout.state.encodedValue; + } flutterJNI.setViewportMetrics( viewportMetrics.devicePixelRatio, @@ -1314,7 +1332,29 @@ boolean validate() { return width > 0 && height > 0 && devicePixelRatio > 0; } - public List displayFeatures = new ArrayList<>(); + // Features + private final List displayFeatures = new ArrayList<>(); + + // Specifically display cutouts. + private final List displayCutouts = new ArrayList<>(); + + public List getDisplayFeatures() { + return displayFeatures; + } + + public List getDisplayCutouts() { + return displayCutouts; + } + + public void setDisplayFeatures(List newFeatures) { + displayFeatures.clear(); + displayFeatures.addAll(newFeatures); + } + + public void setDisplayCutouts(List newCutouts) { + displayCutouts.clear(); + displayCutouts.addAll(newCutouts); + } } /** @@ -1337,12 +1377,6 @@ public DisplayFeature(Rect bounds, DisplayFeatureType type, DisplayFeatureState this.type = type; this.state = state; } - - public DisplayFeature(Rect bounds, DisplayFeatureType type) { - this.bounds = bounds; - this.type = type; - this.state = DisplayFeatureState.UNKNOWN; - } } /** diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index dc62638fb2e32..7247873ef1e0d 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -55,6 +55,8 @@ import io.flutter.plugin.platform.PlatformViewsController; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; @@ -634,21 +636,103 @@ public void systemInsetDisplayCutoutSimple() { when(windowInsets.getSystemGestureInsets()).thenReturn(systemGestureInsets); when(windowInsets.getDisplayCutout()).thenReturn(displayCutout); - Insets waterfallInsets = Insets.of(200, 0, 200, 0); + Insets waterfallInsets = Insets.of(200, 0, 250, 0); when(displayCutout.getWaterfallInsets()).thenReturn(waterfallInsets); - when(displayCutout.getSafeInsetTop()).thenReturn(150); - when(displayCutout.getSafeInsetBottom()).thenReturn(150); - when(displayCutout.getSafeInsetLeft()).thenReturn(150); - when(displayCutout.getSafeInsetRight()).thenReturn(150); + when(displayCutout.getSafeInsetLeft()).thenReturn(110); + when(displayCutout.getSafeInsetTop()).thenReturn(120); + when(displayCutout.getSafeInsetRight()).thenReturn(130); + when(displayCutout.getSafeInsetBottom()).thenReturn(140); flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); - validateViewportMetricPadding(viewportMetricsCaptor, 200, 150, 200, 150); + // Each dimension of the viewport metric paddings should be the maximum of the corresponding + // dimension from the display cutout's safe insets and waterfall insets. + validateViewportMetricPadding(viewportMetricsCaptor, 200, 120, 250, 140); assertEquals(100, viewportMetricsCaptor.getValue().viewInsetTop); } + @SuppressWarnings("deprecation") + @Test + @Config(minSdk = 28) + public void onApplyWindowInsetsSetsDisplayCutouts() { + // Use an Activity context so that FlutterView.onAttachedToWindow completes. + Context context = Robolectric.setupActivity(Activity.class); + FlutterView flutterView = spy(new FlutterView(context)); + assertEquals(0, flutterView.getSystemUiVisibility()); + when(flutterView.getWindowSystemUiVisibility()).thenReturn(0); + when(flutterView.getContext()).thenReturn(context); + + FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + + // When we attach a new FlutterView to the engine without any system insets, + // the viewport metrics default to 0. + flutterView.attachToFlutterEngine(flutterEngine); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); + + // Capture flutterView.setWindowInfoListenerDisplayFeatures. + WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = + mock(WindowInfoRepositoryCallbackAdapterWrapper.class); + doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); + ArgumentCaptor> consumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + flutterView.onAttachedToWindow(); + verify(windowInfoRepo).addWindowLayoutInfoListener(any(), any(), consumerCaptor.capture()); + Consumer consumer = consumerCaptor.getValue(); + + // Set display features in flutterView to ensure they are not overridden by display cutouts. + FoldingFeature displayFeature = mock(FoldingFeature.class); + Rect featureBounds = new Rect(10, 20, 30, 40); + when(displayFeature.getBounds()).thenReturn(featureBounds); + when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); + when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); + WindowLayoutInfo windowLayout = new WindowLayoutInfo(Collections.singletonList(displayFeature)); + clearInvocations(flutterRenderer); + consumer.accept(windowLayout); + + // Assert the display feature is set. + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + List features = + viewportMetricsCaptor.getValue().getDisplayFeatures(); + assertEquals(1, features.size()); + assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, features.get(0).type); + assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, features.get(0).state); + assertEquals(featureBounds, features.get(0).bounds); + + // Then we simulate the system applying a window inset. + List cutoutBoundingRects = + Arrays.asList(new Rect(0, 200, 300, 400), new Rect(150, 0, 300, 150)); + WindowInsets windowInsets = setupMockDisplayCutout(cutoutBoundingRects); + + clearInvocations(flutterRenderer); + flutterView.onApplyWindowInsets(windowInsets); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + + features = viewportMetricsCaptor.getValue().getDisplayFeatures(); + + // Assert the old display feature is still present. + assertEquals(1, features.size()); + assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, features.get(0).type); + assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, features.get(0).state); + assertEquals(featureBounds, features.get(0).bounds); + + List cutouts = + viewportMetricsCaptor.getValue().getDisplayCutouts(); + // Asserts for display cutouts. + assertEquals(2, cutouts.size()); + for (int i = 0; i < 2; i++) { + assertEquals(cutoutBoundingRects.get(i), cutouts.get(i).bounds); + assertEquals(FlutterRenderer.DisplayFeatureType.CUTOUT, cutouts.get(i).type); + assertEquals(FlutterRenderer.DisplayFeatureState.UNKNOWN, cutouts.get(i).state); + } + } + @SuppressWarnings("deprecation") // Robolectric.setupActivity // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @@ -694,36 +778,59 @@ public void itSendsHingeDisplayFeatureToFlutter() { FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + // Display features should be empty on attaching to engine. + flutterView.attachToFlutterEngine(flutterEngine); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(Collections.emptyList(), viewportMetricsCaptor.getValue().getDisplayFeatures()); + clearInvocations(flutterRenderer); + + // Test that display features do not override cutouts. + List cutoutBoundingRects = Collections.singletonList(new Rect(0, 200, 300, 400)); + WindowInsets windowInsets = setupMockDisplayCutout(cutoutBoundingRects); + flutterView.onApplyWindowInsets(windowInsets); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(1, viewportMetricsCaptor.getValue().getDisplayCutouts().size()); + assertEquals( + cutoutBoundingRects.get(0), + viewportMetricsCaptor.getValue().getDisplayCutouts().get(0).bounds); + clearInvocations(flutterRenderer); + FoldingFeature displayFeature = mock(FoldingFeature.class); - when(displayFeature.getBounds()).thenReturn(new Rect(0, 0, 100, 100)); + Rect featureRect = new Rect(0, 0, 100, 100); + when(displayFeature.getBounds()).thenReturn(featureRect); when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); - WindowLayoutInfo testWindowLayout = new WindowLayoutInfo(Arrays.asList(displayFeature)); + WindowLayoutInfo testWindowLayout = + new WindowLayoutInfo(Collections.singletonList(displayFeature)); // When FlutterView is attached to the engine and window, and a hinge display feature exists - flutterView.attachToFlutterEngine(flutterEngine); - ArgumentCaptor viewportMetricsCaptor = - ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(Arrays.asList(), viewportMetricsCaptor.getValue().displayFeatures); flutterView.onAttachedToWindow(); ArgumentCaptor> wmConsumerCaptor = - ArgumentCaptor.forClass((Class) Consumer.class); + ArgumentCaptor.forClass(Consumer.class); verify(windowInfoRepo).addWindowLayoutInfoListener(any(), any(), wmConsumerCaptor.capture()); Consumer wmConsumer = wmConsumerCaptor.getValue(); + clearInvocations(flutterRenderer); wmConsumer.accept(testWindowLayout); // Then the Renderer receives the display feature verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals( - FlutterRenderer.DisplayFeatureType.HINGE, - viewportMetricsCaptor.getValue().displayFeatures.get(0).type); - assertEquals( - FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, - viewportMetricsCaptor.getValue().displayFeatures.get(0).state); - assertEquals( - new Rect(0, 0, 100, 100), viewportMetricsCaptor.getValue().displayFeatures.get(0).bounds); + assertEquals(1, viewportMetricsCaptor.getValue().getDisplayFeatures().size()); + FlutterRenderer.DisplayFeature feature = + viewportMetricsCaptor.getValue().getDisplayFeatures().get(0); + assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, feature.type); + assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, feature.state); + assertEquals(featureRect, feature.bounds); + + // Assert the display cutout is unaffected. + assertEquals(1, viewportMetricsCaptor.getValue().getDisplayCutouts().size()); + FlutterRenderer.DisplayFeature cutout = + viewportMetricsCaptor.getValue().getDisplayCutouts().get(0); + assertEquals(cutoutBoundingRects.get(0), cutout.bounds); + assertEquals(FlutterRenderer.DisplayFeatureType.CUTOUT, cutout.type); + assertEquals(FlutterRenderer.DisplayFeatureState.UNKNOWN, cutout.state); } @Test @@ -1173,6 +1280,34 @@ private void mockSystemGestureInsetsIfNeed(WindowInsets windowInsets) { } } + @SuppressWarnings("deprecation") + private WindowInsets setupMockDisplayCutout(List boundingRects) { + WindowInsets windowInsets = mock(WindowInsets.class); + DisplayCutout displayCutout = mock(DisplayCutout.class); + when(windowInsets.getDisplayCutout()).thenReturn(displayCutout); + when(displayCutout.getBoundingRects()).thenReturn(boundingRects); + // The following mocked methods are necessary to avoid a NullPointerException when calling + // onApplyWindowInsets, but are irrelevant to the behavior this test concerns. + Insets unusedInsets = Insets.of(100, 100, 100, 100); + // WindowInsets::getSystemGestureInsets was added in API 29, deprecated in API 30. + if (Build.VERSION.SDK_INT == 29) { + when(windowInsets.getSystemGestureInsets()).thenReturn(unusedInsets); + } + // WindowInsets::getInsets was added in API 30. + if (Build.VERSION.SDK_INT >= 30) { + when(windowInsets.getInsets(anyInt())).thenReturn(unusedInsets); + } + // DisplayCutout::getWaterfallInsets was added in API 30. + if (Build.VERSION.SDK_INT >= 30) { + when(displayCutout.getWaterfallInsets()).thenReturn(unusedInsets); + } + when(displayCutout.getSafeInsetTop()).thenReturn(100); + when(displayCutout.getSafeInsetLeft()).thenReturn(100); + when(displayCutout.getSafeInsetBottom()).thenReturn(100); + when(displayCutout.getSafeInsetRight()).thenReturn(100); + return windowInsets; + } + /* * A custom shadow that reports fullscreen flag for system UI visibility */ diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index 953a3e490b680..ddfabd1b6bf56 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -288,14 +288,20 @@ public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { metrics.width = 1000; metrics.height = 1000; metrics.devicePixelRatio = 2; - metrics.displayFeatures.add( - new FlutterRenderer.DisplayFeature( - new Rect(10, 20, 30, 40), - FlutterRenderer.DisplayFeatureType.FOLD, - FlutterRenderer.DisplayFeatureState.POSTURE_HALF_OPENED)); - metrics.displayFeatures.add( - new FlutterRenderer.DisplayFeature( - new Rect(50, 60, 70, 80), FlutterRenderer.DisplayFeatureType.CUTOUT)); + metrics + .getDisplayFeatures() + .add( + new FlutterRenderer.DisplayFeature( + new Rect(10, 20, 30, 40), + FlutterRenderer.DisplayFeatureType.FOLD, + FlutterRenderer.DisplayFeatureState.POSTURE_HALF_OPENED)); + metrics + .getDisplayCutouts() + .add( + new FlutterRenderer.DisplayFeature( + new Rect(50, 60, 70, 80), + FlutterRenderer.DisplayFeatureType.CUTOUT, + FlutterRenderer.DisplayFeatureState.UNKNOWN)); // Execute the behavior under test. flutterRenderer.setViewportMetrics(metrics); From c3ea4bec7182bb78cf196060c82c3138120d4564 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 8 Jan 2025 15:41:31 -0800 Subject: [PATCH 21/26] [CP] Remove incorrect fml check. (#57249) This was removed at ToT but needs to also be cherry picked into 3.27 --- shell/platform/android/surface_texture_external_texture.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/surface_texture_external_texture.cc b/shell/platform/android/surface_texture_external_texture.cc index 9ace4b1c41d6d..7f0511138b479 100644 --- a/shell/platform/android/surface_texture_external_texture.cc +++ b/shell/platform/android/surface_texture_external_texture.cc @@ -52,7 +52,10 @@ void SurfaceTextureExternalTexture::Paint(PaintContext& context, if (should_process_frame) { ProcessFrame(context, bounds); } - FML_CHECK(state_ == AttachmentState::kAttached); + // If process frame failed, this may not be in attached state. + if (state_ != AttachmentState::kAttached) { + return; + } if (!dl_image_) { FML_LOG(WARNING) From d11906b936316de678dfbb12b84deba263ebee33 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 8 Jan 2025 15:51:25 -0800 Subject: [PATCH 22/26] [CP][Impeller] fix line/polygon depth and GLES scissor state. (#56494) (#57248) --- impeller/display_list/aiks_dl_unittests.cc | 60 +++++++++++++++++++ impeller/display_list/canvas.cc | 8 ++- impeller/display_list/canvas.h | 5 +- impeller/display_list/dl_dispatcher.cc | 4 +- .../renderer/backend/gles/render_pass_gles.cc | 2 - testing/impeller_golden_tests_output.txt | 6 ++ 6 files changed, 78 insertions(+), 7 deletions(-) diff --git a/impeller/display_list/aiks_dl_unittests.cc b/impeller/display_list/aiks_dl_unittests.cc index 133bb89169eab..e5dc1441f1707 100644 --- a/impeller/display_list/aiks_dl_unittests.cc +++ b/impeller/display_list/aiks_dl_unittests.cc @@ -3,12 +3,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include #include "display_list/dl_sampling_options.h" #include "display_list/dl_tile_mode.h" #include "display_list/effects/dl_color_filter.h" #include "display_list/effects/dl_color_source.h" #include "display_list/effects/dl_image_filter.h" #include "display_list/geometry/dl_geometry_types.h" +#include "display_list/geometry/dl_path.h" #include "display_list/image/dl_image.h" #include "flutter/impeller/display_list/aiks_unittests.h" @@ -21,7 +23,9 @@ #include "impeller/display_list/dl_dispatcher.h" #include "impeller/display_list/dl_image_impeller.h" #include "impeller/geometry/scalar.h" +#include "include/core/SkCanvas.h" #include "include/core/SkMatrix.h" +#include "include/core/SkPath.h" #include "include/core/SkRSXform.h" #include "include/core/SkRefCnt.h" @@ -974,5 +978,61 @@ TEST_P(AiksTest, CanEmptyPictureConvertToImage) { ASSERT_TRUE(OpenPlaygroundHere(recorder_builder.Build())); } +TEST_P(AiksTest, DepthValuesForLineMode) { + // Ensures that the additional draws created by line/polygon mode all + // have the same depth values. + DisplayListBuilder builder; + + SkPath path = SkPath::Circle(100, 100, 100); + + builder.DrawPath(path, DlPaint() + .setColor(DlColor::kRed()) + .setDrawStyle(DlDrawStyle::kStroke) + .setStrokeWidth(5)); + builder.Save(); + builder.ClipPath(path); + + std::vector points = { + SkPoint::Make(0, -200), SkPoint::Make(400, 200), SkPoint::Make(0, -100), + SkPoint::Make(400, 300), SkPoint::Make(0, 0), SkPoint::Make(400, 400), + SkPoint::Make(0, 100), SkPoint::Make(400, 500), SkPoint::Make(0, 150), + SkPoint::Make(400, 600)}; + + builder.DrawPoints(DisplayListBuilder::PointMode::kLines, points.size(), + points.data(), + DlPaint().setColor(DlColor::kBlue()).setStrokeWidth(10)); + builder.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, DepthValuesForPolygonMode) { + // Ensures that the additional draws created by line/polygon mode all + // have the same depth values. + DisplayListBuilder builder; + + SkPath path = SkPath::Circle(100, 100, 100); + + builder.DrawPath(path, DlPaint() + .setColor(DlColor::kRed()) + .setDrawStyle(DlDrawStyle::kStroke) + .setStrokeWidth(5)); + builder.Save(); + builder.ClipPath(path); + + std::vector points = { + SkPoint::Make(0, -200), SkPoint::Make(400, 200), SkPoint::Make(0, -100), + SkPoint::Make(400, 300), SkPoint::Make(0, 0), SkPoint::Make(400, 400), + SkPoint::Make(0, 100), SkPoint::Make(400, 500), SkPoint::Make(0, 150), + SkPoint::Make(400, 600)}; + + builder.DrawPoints(DisplayListBuilder::PointMode::kPolygon, points.size(), + points.data(), + DlPaint().setColor(DlColor::kBlue()).setStrokeWidth(10)); + builder.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + } // namespace testing } // namespace impeller diff --git a/impeller/display_list/canvas.cc b/impeller/display_list/canvas.cc index d14db69dc0d69..2634760368a06 100644 --- a/impeller/display_list/canvas.cc +++ b/impeller/display_list/canvas.cc @@ -543,13 +543,17 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect, return true; } -void Canvas::DrawLine(const Point& p0, const Point& p1, const Paint& paint) { +void Canvas::DrawLine(const Point& p0, + const Point& p1, + const Paint& paint, + bool reuse_depth) { Entity entity; entity.SetTransform(GetCurrentTransform()); entity.SetBlendMode(paint.blend_mode); LineGeometry geom(p0, p1, paint.stroke_width, paint.stroke_cap); - AddRenderEntityWithFiltersToCurrentPass(entity, &geom, paint); + AddRenderEntityWithFiltersToCurrentPass(entity, &geom, paint, + /*reuse_depth=*/reuse_depth); } void Canvas::DrawRect(const Rect& rect, const Paint& paint) { diff --git a/impeller/display_list/canvas.h b/impeller/display_list/canvas.h index 387cb5c81dc33..fd5e54169b18d 100644 --- a/impeller/display_list/canvas.h +++ b/impeller/display_list/canvas.h @@ -167,7 +167,10 @@ class Canvas { void DrawPaint(const Paint& paint); - void DrawLine(const Point& p0, const Point& p1, const Paint& paint); + void DrawLine(const Point& p0, + const Point& p1, + const Paint& paint, + bool reuse_depth = false); void DrawRect(const Rect& rect, const Paint& paint); diff --git a/impeller/display_list/dl_dispatcher.cc b/impeller/display_list/dl_dispatcher.cc index ffdd0c6eca9f8..c803f549752bf 100644 --- a/impeller/display_list/dl_dispatcher.cc +++ b/impeller/display_list/dl_dispatcher.cc @@ -675,7 +675,7 @@ void DlDispatcherBase::drawPoints(PointMode mode, for (uint32_t i = 1; i < count; i += 2) { Point p0 = points[i - 1]; Point p1 = points[i]; - GetCanvas().DrawLine(p0, p1, paint); + GetCanvas().DrawLine(p0, p1, paint, /*reuse_depth=*/i > 1); } break; case flutter::DlCanvas::PointMode::kPolygon: @@ -683,7 +683,7 @@ void DlDispatcherBase::drawPoints(PointMode mode, Point p0 = points[0]; for (uint32_t i = 1; i < count; i++) { Point p1 = points[i]; - GetCanvas().DrawLine(p0, p1, paint); + GetCanvas().DrawLine(p0, p1, paint, /*reuse_depth=*/i > 1); p0 = p1; } } diff --git a/impeller/renderer/backend/gles/render_pass_gles.cc b/impeller/renderer/backend/gles/render_pass_gles.cc index 1625dc03ac4a3..a9449a8643188 100644 --- a/impeller/renderer/backend/gles/render_pass_gles.cc +++ b/impeller/renderer/backend/gles/render_pass_gles.cc @@ -338,8 +338,6 @@ struct RenderPassData { scissor.GetWidth(), // width scissor.GetHeight() // height ); - } else { - gl.Disable(GL_SCISSOR_TEST); } //-------------------------------------------------------------------------- diff --git a/testing/impeller_golden_tests_output.txt b/testing/impeller_golden_tests_output.txt index 0979bbc406399..886c7a0f3527c 100644 --- a/testing/impeller_golden_tests_output.txt +++ b/testing/impeller_golden_tests_output.txt @@ -548,6 +548,12 @@ impeller_Play_AiksTest_CoordinateConversionsAreCorrect_Vulkan.png impeller_Play_AiksTest_CoverageOriginShouldBeAccountedForInSubpasses_Metal.png impeller_Play_AiksTest_CoverageOriginShouldBeAccountedForInSubpasses_OpenGLES.png impeller_Play_AiksTest_CoverageOriginShouldBeAccountedForInSubpasses_Vulkan.png +impeller_Play_AiksTest_DepthValuesForLineMode_Metal.png +impeller_Play_AiksTest_DepthValuesForLineMode_OpenGLES.png +impeller_Play_AiksTest_DepthValuesForLineMode_Vulkan.png +impeller_Play_AiksTest_DepthValuesForPolygonMode_Metal.png +impeller_Play_AiksTest_DepthValuesForPolygonMode_OpenGLES.png +impeller_Play_AiksTest_DepthValuesForPolygonMode_Vulkan.png impeller_Play_AiksTest_DispatcherDoesNotCullPerspectiveTransformedChildDisplayLists_Metal.png impeller_Play_AiksTest_DispatcherDoesNotCullPerspectiveTransformedChildDisplayLists_OpenGLES.png impeller_Play_AiksTest_DispatcherDoesNotCullPerspectiveTransformedChildDisplayLists_Vulkan.png From ce26d8b7e38d6c5f8167ff9f4368b18858a16c8a Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:23:21 -0800 Subject: [PATCH 23/26] [CP-stable][web] Don't close image source too early (#57221) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Links: https://github.com/flutter/flutter/issues/160199 https://github.com/flutter/flutter/issues/158093 ### Changelog Description: Fix a regression that causes some images on the web to render blank. ### Impact Description: Some images are showing as a blank rectangle in the Stable release of Flutter Web. ### Workaround: No workaround as far as I know. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: Make sure there's an image rendered in the following app: ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { var title = 'Web Images'; return MaterialApp( title: title, home: Scaffold( appBar: AppBar( title: Text(title), ), body: Image.network( 'https://picsum.photos/250?image=9', cacheHeight: 100, ), ), ); } } ``` --- .../lib/src/engine/canvaskit/image.dart | 32 ++++++++++++++++--- lib/web_ui/lib/src/engine/dom.dart | 6 ++++ lib/web_ui/test/canvaskit/image_test.dart | 30 +++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 812c98feb2099..5b4edb6a2939f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -7,6 +7,7 @@ import 'dart:js_interop'; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:meta/meta.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; @@ -403,11 +404,13 @@ class CkImage implements ui.Image, StackTraceDebugger { CkImage(SkImage skImage, {this.imageSource}) { box = CountedRef(skImage, this, 'SkImage'); _init(); + imageSource?.refCount++; } CkImage.cloneOf(this.box, {this.imageSource}) { _init(); box.ref(this); + imageSource?.refCount++; } void _init() { @@ -454,6 +457,8 @@ class CkImage implements ui.Image, StackTraceDebugger { ui.Image.onDispose?.call(this); _disposed = true; box.unref(this); + + imageSource?.refCount--; imageSource?.close(); } @@ -645,7 +650,26 @@ sealed class ImageSource { DomCanvasImageSource get canvasImageSource; int get width; int get height; - void close(); + + /// The number of references to this image source. + /// + /// Calling [close] is a no-op if [refCount] is greater than 0. + /// + /// Only when [refCount] is 0 will the [close] method actually close the + /// image source. + int refCount = 0; + + @visibleForTesting + bool debugIsClosed = false; + + void close() { + if (refCount == 0) { + _doClose(); + debugIsClosed = true; + } + } + + void _doClose(); } class VideoFrameImageSource extends ImageSource { @@ -654,7 +678,7 @@ class VideoFrameImageSource extends ImageSource { final VideoFrame videoFrame; @override - void close() { + void _doClose() { // Do nothing. Skia will close the VideoFrame when the SkImage is disposed. } @@ -674,7 +698,7 @@ class ImageElementImageSource extends ImageSource { final DomHTMLImageElement imageElement; @override - void close() { + void _doClose() { // There's no way to immediately close the element. Just let the // browser garbage collect it. } @@ -695,7 +719,7 @@ class ImageBitmapImageSource extends ImageSource { final DomImageBitmap imageBitmap; @override - void close() { + void _doClose() { imageBitmap.close(); } diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 5fb335f11b728..caf4888bbc05a 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -169,6 +169,12 @@ extension DomWindowExtension on DomWindow { /// The Trusted Types API (when available). /// See: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API external DomTrustedTypePolicyFactory? get trustedTypes; + + @JS('createImageBitmap') + external JSPromise _createImageBitmap(DomImageData source); + Future createImageBitmap(DomImageData source) { + return js_util.promiseToFuture(_createImageBitmap(source)); + } } typedef DomRequestAnimationFrameCallback = void Function(JSNumber highResTime); diff --git a/lib/web_ui/test/canvaskit/image_test.dart b/lib/web_ui/test/canvaskit/image_test.dart index c23ae19fd196a..9a30849365011 100644 --- a/lib/web_ui/test/canvaskit/image_test.dart +++ b/lib/web_ui/test/canvaskit/image_test.dart @@ -7,12 +7,11 @@ import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; -import 'package:ui/src/engine/canvaskit/image.dart'; -import 'package:ui/src/engine/image_decoder.dart'; -import 'package:ui/src/engine/util.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'common.dart'; +import 'test_data.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -131,6 +130,31 @@ void testMain() { expect(activeImages.length, 0); }); + + test('CkImage does not close image source too early', () async { + final ImageSource imageSource = ImageBitmapImageSource( + await domWindow.createImageBitmap(createBlankDomImageData(4, 4)), + ); + + final SkImage skImage1 = canvasKit.MakeAnimatedImageFromEncoded(k4x4PngImage)!.makeImageAtCurrentFrame(); + final CkImage image1 = CkImage(skImage1, imageSource: imageSource); + + final SkImage skImage2 = canvasKit.MakeAnimatedImageFromEncoded(k4x4PngImage)!.makeImageAtCurrentFrame(); + final CkImage image2 = CkImage(skImage2, imageSource: imageSource); + + final CkImage image3 = image1.clone(); + + expect(imageSource.debugIsClosed, isFalse); + + image1.dispose(); + expect(imageSource.debugIsClosed, isFalse); + + image2.dispose(); + expect(imageSource.debugIsClosed, isFalse); + + image3.dispose(); + expect(imageSource.debugIsClosed, isTrue); + }); } Future _createImage() => _createPicture().toImage(10, 10); From 9c0d3222c068ebdcff16c16c71787e281a33a6e0 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:57:26 -0800 Subject: [PATCH 24/26] [CP-stable][web] Use `eventTarget` when computing pointer offset (#57246) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Links: https://github.com/flutter/flutter/issues/160155 https://github.com/flutter/flutter/issues/159804 ### Changelog Description: Work around this Chromium bug: https://issues.chromium.org/issues/382473107 ### Impact Description: This bug is causing issues when mouse interacts with text fields in Flutter Web. ### Workaround: This CP *is* the workaround. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: - Open a Flutter Web app in Chrome. - Click on a text field. - Move the mouse over the text field. - There should be no errors in the console. --- .../event_position_helper.dart | 23 +++++++++---- .../event_position_helper_test.dart | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 494f940d58eb4..82954325ab85e 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -18,9 +18,12 @@ import '../window.dart'; /// The offset is *not* multiplied by DPR or anything else, it's the closest /// to what the DOM would return if we had currentTarget readily available. /// -/// This needs an `eventTarget`, because the `event.target` (which is what -/// this would really need to use) gets lost when the `event` comes from a -/// "coalesced" event (see https://github.com/flutter/flutter/issues/155987). +/// This takes an optional `eventTarget`, because the `event.target` may have +/// the wrong value for "coalesced" events. See: +/// +/// - https://github.com/flutter/flutter/issues/155987 +/// - https://github.com/flutter/flutter/issues/159804 +/// - https://g-issues.chromium.org/issues/382473107 /// /// It also takes into account semantics being enabled to fix the case where /// offsetX, offsetY == 0 (TalkBack events). @@ -41,12 +44,12 @@ ui.Offset computeEventOffsetToTarget( if (isInput) { final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry; if (inputGeometry != null) { - return _computeOffsetForInputs(event, inputGeometry); + return _computeOffsetForInputs(event, eventTarget, inputGeometry); } } // On another DOM Element (normally a platform view) - final bool isTargetOutsideOfShadowDOM = event.target != actualTarget; + final bool isTargetOutsideOfShadowDOM = eventTarget != actualTarget; if (isTargetOutsideOfShadowDOM) { final DomRect origin = actualTarget.getBoundingClientRect(); // event.clientX/Y and origin.x/y are relative **to the viewport**. @@ -70,8 +73,14 @@ ui.Offset computeEventOffsetToTarget( /// sent from the framework, which includes information on how to transform the /// underlying input element. We transform the `event.offset` points we receive /// using the values from the input's transform matrix. -ui.Offset _computeOffsetForInputs(DomMouseEvent event, EditableTextGeometry inputGeometry) { - final DomElement targetElement = event.target! as DomHTMLElement; +/// +/// See [computeEventOffsetToTarget] for more information about `eventTarget`. +ui.Offset _computeOffsetForInputs( + DomMouseEvent event, + DomEventTarget eventTarget, + EditableTextGeometry inputGeometry, +) { + final DomElement targetElement = eventTarget as DomElement; final DomHTMLElement domElement = textEditing.strategy.activeDomElement; assert(targetElement == domElement, 'The targeted input element must be the active input element'); final Float32List transformValues = inputGeometry.globalTransform; diff --git a/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart b/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart index eca204cd6e613..14f41dccbbf9a 100644 --- a/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart +++ b/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart @@ -32,6 +32,7 @@ void doTests() { group('computeEventOffsetToTarget', () { setUp(() { view = EngineFlutterView(EnginePlatformDispatcher.instance, domDocument.body!); + EnginePlatformDispatcher.instance.viewManager.registerView(view); rootElement = view.dom.rootElement; eventSource = createDomElement('div-event-source'); rootElement.append(eventSource); @@ -58,6 +59,7 @@ void doTests() { }); tearDown(() { + EnginePlatformDispatcher.instance.viewManager.unregisterView(view.viewId); view.dispose(); }); @@ -101,6 +103,36 @@ void doTests() { expect(offset.dy, 110); }); + test('eventTarget takes precedence', () async { + final input = view.dom.textEditingHost.appendChild(createDomElement('input')); + + textEditing.strategy.enable( + InputConfiguration(viewId: view.viewId), + onChange: (_, __) {}, + onAction: (_) {}, + ); + + addTearDown(() { + textEditing.strategy.disable(); + }); + + final moveEvent = createDomPointerEvent('pointermove', { + 'bubbles': true, + 'clientX': 10, + 'clientY': 20, + }); + + expect( + () => computeEventOffsetToTarget(moveEvent, view), + throwsA(anything), + ); + + expect( + () => computeEventOffsetToTarget(moveEvent, view, eventTarget: input), + returnsNormally, + ); + }); + test('Event dispatched by TalkBack gets a computed offset', () async { // Fill this in to test _computeOffsetForTalkbackEvent }, skip: 'To be implemented!'); From 41bb2f4fc940b78f2f1a233182cc306ea688ce52 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 9 Jan 2025 17:06:05 -0500 Subject: [PATCH 25/26] [CP-stable][web] Reland: Add `crossOrigin` property to `` tag used for decoding (#57272) Original PR: #57228 @christopherfujino Approved for hotfix to stable by @yjbanov in https://github.com/flutter/engine/pull/57228#issuecomment-2547011605 --- .../lib/src/engine/canvaskit/image.dart | 8 +- lib/web_ui/lib/src/engine/dom.dart | 16 ++ .../src/engine/html_image_element_codec.dart | 11 +- .../test/canvaskit/image_golden_test.dart | 13 ++ .../image/html_image_element_codec_test.dart | 38 ++++- .../image/html_image_element_codec_test.dart | 150 ++++++++++++++++++ lib/web_ui/test/ui/image/sample_image1.png | Bin 0 -> 14746 bytes 7 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 lib/web_ui/test/ui/image/html_image_element_codec_test.dart create mode 100644 lib/web_ui/test/ui/image/sample_image1.png diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 5b4edb6a2939f..306ff752c2563 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -161,7 +161,7 @@ ui.Image createCkImageFromImageElement( } class CkImageElementCodec extends HtmlImageElementCodec { - CkImageElementCodec(super.src); + CkImageElementCodec(super.src, {super.chunkCallback}); @override ui.Image createImageFromHTMLImageElement( @@ -170,7 +170,7 @@ class CkImageElementCodec extends HtmlImageElementCodec { } class CkImageBlobCodec extends HtmlBlobCodec { - CkImageBlobCodec(super.blob); + CkImageBlobCodec(super.blob, {super.chunkCallback}); @override ui.Image createImageFromHTMLImageElement( @@ -326,7 +326,7 @@ const String _kNetworkImageMessage = 'Failed to load network image.'; /// requesting from URI. Future skiaInstantiateWebImageCodec( String url, ui_web.ImageCodecChunkCallback? chunkCallback) async { - final CkImageElementCodec imageElementCodec = CkImageElementCodec(url); + final CkImageElementCodec imageElementCodec = CkImageElementCodec(url, chunkCallback: chunkCallback); try { await imageElementCodec.decode(); return imageElementCodec; @@ -339,7 +339,7 @@ Future skiaInstantiateWebImageCodec( data: list, contentType: imageType.mimeType, debugSource: url); } else { final DomBlob blob = createDomBlob([list.buffer]); - final CkImageBlobCodec codec = CkImageBlobCodec(blob); + final CkImageBlobCodec codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback); try { await codec.decode(); diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index caf4888bbc05a..336ddadbab602 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -990,6 +990,22 @@ extension DomHTMLImageElementExtension on DomHTMLImageElement { external set _height(JSNumber? value); set height(double? value) => _height = value?.toJS; + @JS('crossOrigin') + external JSString? get _crossOrigin; + String? get crossOrigin => _crossOrigin?.toDart; + + @JS('crossOrigin') + external set _crossOrigin(JSString? value); + set crossOrigin(String? value) => _crossOrigin = value?.toJS; + + @JS('decoding') + external JSString? get _decoding; + String? get decoding => _decoding?.toDart; + + @JS('decoding') + external set _decoding(JSString? value); + set decoding(String? value) => _decoding = value?.toJS; + @JS('decode') external JSPromise _decode(); Future decode() => js_util.promiseToFuture(_decode()); diff --git a/lib/web_ui/lib/src/engine/html_image_element_codec.dart b/lib/web_ui/lib/src/engine/html_image_element_codec.dart index 2bd6ccabc54f0..9784345e999f3 100644 --- a/lib/web_ui/lib/src/engine/html_image_element_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_element_codec.dart @@ -43,8 +43,13 @@ abstract class HtmlImageElementCodec implements ui.Codec { // builders to create UI. chunkCallback?.call(0, 100); imgElement = createDomHTMLImageElement(); - imgElement!.src = src; - setJsProperty(imgElement!, 'decoding', 'async'); + if (renderer is! HtmlRenderer) { + imgElement!.crossOrigin = 'anonymous'; + } + imgElement! + ..decoding = 'async' + ..src = src; + // Ignoring the returned future on purpose because we're communicating // through the `completer`. @@ -91,7 +96,7 @@ abstract class HtmlImageElementCodec implements ui.Codec { } abstract class HtmlBlobCodec extends HtmlImageElementCodec { - HtmlBlobCodec(this.blob) + HtmlBlobCodec(this.blob, {super.chunkCallback}) : super( domWindow.URL.createObjectURL(blob), debugSource: 'encoded image bytes', diff --git a/lib/web_ui/test/canvaskit/image_golden_test.dart b/lib/web_ui/test/canvaskit/image_golden_test.dart index b5c5c6085b113..e0681ce6e1f02 100644 --- a/lib/web_ui/test/canvaskit/image_golden_test.dart +++ b/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -253,6 +253,19 @@ Future testMain() async { } }); + test('crossOrigin requests cause an error', () async { + final String otherOrigin = + domWindow.location.origin.replaceAll('localhost', '127.0.0.1'); + bool gotError = false; + try { + final ui.Codec _ = await renderer.instantiateImageCodecFromUrl( + Uri.parse('$otherOrigin/test_images/1x1.png')); + } catch (e) { + gotError = true; + } + expect(gotError, isTrue, reason: 'Should have got CORS error'); + }); + _testCkAnimatedImage(); test('isAvif', () { diff --git a/lib/web_ui/test/engine/image/html_image_element_codec_test.dart b/lib/web_ui/test/engine/image/html_image_element_codec_test.dart index bbbf9ed2a58aa..772aeeb31cabb 100644 --- a/lib/web_ui/test/engine/image/html_image_element_codec_test.dart +++ b/lib/web_ui/test/engine/image/html_image_element_codec_test.dart @@ -7,12 +7,15 @@ import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine/canvaskit/image.dart'; +import 'package:ui/src/engine/dom.dart'; import 'package:ui/src/engine/html/image.dart'; import 'package:ui/src/engine/html_image_element_codec.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import '../../common/test_initialization.dart'; +import '../../ui/utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -60,16 +63,20 @@ Future testMain() async { expect(image.height, height); }); test('loads sample image', () async { - final HtmlImageElementCodec codec = - HtmlRendererImageCodec('sample_image1.png'); + final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png'); final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + expect(codec.imgElement, isNotNull); + expect(codec.imgElement!.src, contains('sample_image1.png')); + expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous'); + expect(codec.imgElement!.decoding, 'async'); + expect(frameInfo.image, isNotNull); expect(frameInfo.image.width, 100); expect(frameInfo.image.toString(), '[100×100]'); }); test('dispose image image', () async { - final HtmlImageElementCodec codec = - HtmlRendererImageCodec('sample_image1.png'); + final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png'); final ui.FrameInfo frameInfo = await codec.getNextFrame(); expect(frameInfo.image, isNotNull); expect(frameInfo.image.debugDisposed, isFalse); @@ -78,7 +85,7 @@ Future testMain() async { }); test('provides image loading progress', () async { final StringBuffer buffer = StringBuffer(); - final HtmlImageElementCodec codec = HtmlRendererImageCodec( + final HtmlImageElementCodec codec = createImageElementCodec( 'sample_image1.png', chunkCallback: (int loaded, int total) { buffer.write('$loaded/$total,'); }); @@ -89,7 +96,7 @@ Future testMain() async { /// Regression test for Firefox /// https://github.com/flutter/flutter/issues/66412 test('Returns nonzero natural width/height', () async { - final HtmlImageElementCodec codec = HtmlRendererImageCodec( + final HtmlImageElementCodec codec = createImageElementCodec( 'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I' 'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG' 'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx' @@ -103,7 +110,7 @@ Future testMain() async { final ui.FrameInfo frameInfo = await codec.getNextFrame(); expect(frameInfo.image.width, isNot(0)); }); - }); + }, skip: isSkwasm); group('ImageCodecUrl', () { test('loads sample image from web', () async { @@ -111,6 +118,12 @@ Future testMain() async { final HtmlImageElementCodec codec = await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec; final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + expect(codec.imgElement, isNotNull); + expect(codec.imgElement!.src, contains('sample_image1.png')); + expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous'); + expect(codec.imgElement!.decoding, 'async'); + expect(frameInfo.image, isNotNull); expect(frameInfo.image.width, 100); }); @@ -124,5 +137,14 @@ Future testMain() async { await codec.getNextFrame(); expect(buffer.toString(), '0/100,100/100,'); }); - }); + }, skip: isSkwasm); +} + +HtmlImageElementCodec createImageElementCodec( + String src, { + ui_web.ImageCodecChunkCallback? chunkCallback, +}) { + return isHtml + ? HtmlRendererImageCodec(src, chunkCallback: chunkCallback) + : CkImageElementCodec(src, chunkCallback: chunkCallback); } diff --git a/lib/web_ui/test/ui/image/html_image_element_codec_test.dart b/lib/web_ui/test/ui/image/html_image_element_codec_test.dart new file mode 100644 index 0000000000000..772aeeb31cabb --- /dev/null +++ b/lib/web_ui/test/ui/image/html_image_element_codec_test.dart @@ -0,0 +1,150 @@ +// 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. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine/canvaskit/image.dart'; +import 'package:ui/src/engine/dom.dart'; +import 'package:ui/src/engine/html/image.dart'; +import 'package:ui/src/engine/html_image_element_codec.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; + +import '../../common/test_initialization.dart'; +import '../../ui/utils.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests(); + group('$HtmlImageElementCodec', () { + test('supports raw images - RGBA8888', () async { + final Completer completer = Completer(); + const int width = 200; + const int height = 300; + final Uint32List list = Uint32List(width * height); + for (int index = 0; index < list.length; index += 1) { + list[index] = 0xFF0000FF; + } + ui.decodeImageFromPixels( + list.buffer.asUint8List(), + width, + height, + ui.PixelFormat.rgba8888, + (ui.Image image) => completer.complete(image), + ); + final ui.Image image = await completer.future; + expect(image.width, width); + expect(image.height, height); + }); + test('supports raw images - BGRA8888', () async { + final Completer completer = Completer(); + const int width = 200; + const int height = 300; + final Uint32List list = Uint32List(width * height); + for (int index = 0; index < list.length; index += 1) { + list[index] = 0xFF0000FF; + } + ui.decodeImageFromPixels( + list.buffer.asUint8List(), + width, + height, + ui.PixelFormat.bgra8888, + (ui.Image image) => completer.complete(image), + ); + final ui.Image image = await completer.future; + expect(image.width, width); + expect(image.height, height); + }); + test('loads sample image', () async { + final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png'); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + expect(codec.imgElement, isNotNull); + expect(codec.imgElement!.src, contains('sample_image1.png')); + expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous'); + expect(codec.imgElement!.decoding, 'async'); + + expect(frameInfo.image, isNotNull); + expect(frameInfo.image.width, 100); + expect(frameInfo.image.toString(), '[100×100]'); + }); + test('dispose image image', () async { + final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png'); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + expect(frameInfo.image, isNotNull); + expect(frameInfo.image.debugDisposed, isFalse); + frameInfo.image.dispose(); + expect(frameInfo.image.debugDisposed, isTrue); + }); + test('provides image loading progress', () async { + final StringBuffer buffer = StringBuffer(); + final HtmlImageElementCodec codec = createImageElementCodec( + 'sample_image1.png', chunkCallback: (int loaded, int total) { + buffer.write('$loaded/$total,'); + }); + await codec.getNextFrame(); + expect(buffer.toString(), '0/100,100/100,'); + }); + + /// Regression test for Firefox + /// https://github.com/flutter/flutter/issues/66412 + test('Returns nonzero natural width/height', () async { + final HtmlImageElementCodec codec = createImageElementCodec( + 'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I' + 'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG' + 'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx' + 'MiAyLjM5OSAxMiAxMiAwIDkuNjAxLTIuMzk5IDEyLTEyIDEyLTkuNjAxIDAtMTItMi' + '4zOTktMTItMTJDMCAyLjM5OSAyLjM5OSAwIDEyIDB6bS0xLjk2OSAxOC41NjRjMi41' + 'MjQuMDAzIDQuNjA0LTIuMDcgNC42MDktNC41OTUgMC0yLjUyMS0yLjA3NC00LjU5NS' + '00LjU5NS00LjU5NVM1LjQ1IDExLjQ0OSA1LjQ1IDEzLjk2OWMwIDIuNTE2IDIuMDY1' + 'IDQuNTg4IDQuNTgxIDQuNTk1em04LjM0NC0uMTg5VjUuNjI1SDUuNjI1djIuMjQ3aD' + 'EwLjQ5OHYxMC41MDNoMi4yNTJ6bS04LjM0NC02Ljc0OGEyLjM0MyAyLjM0MyAwIDEx' + 'LS4wMDIgNC42ODYgMi4zNDMgMi4zNDMgMCAwMS4wMDItNC42ODZ6Ii8+PC9zdmc+'); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + expect(frameInfo.image.width, isNot(0)); + }); + }, skip: isSkwasm); + + group('ImageCodecUrl', () { + test('loads sample image from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + final HtmlImageElementCodec codec = + await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec; + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + expect(codec.imgElement, isNotNull); + expect(codec.imgElement!.src, contains('sample_image1.png')); + expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous'); + expect(codec.imgElement!.decoding, 'async'); + + expect(frameInfo.image, isNotNull); + expect(frameInfo.image.width, 100); + }); + test('provides image loading progress from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + final StringBuffer buffer = StringBuffer(); + final HtmlImageElementCodec codec = await ui_web + .createImageCodecFromUrl(uri, chunkCallback: (int loaded, int total) { + buffer.write('$loaded/$total,'); + }) as HtmlImageElementCodec; + await codec.getNextFrame(); + expect(buffer.toString(), '0/100,100/100,'); + }); + }, skip: isSkwasm); +} + +HtmlImageElementCodec createImageElementCodec( + String src, { + ui_web.ImageCodecChunkCallback? chunkCallback, +}) { + return isHtml + ? HtmlRendererImageCodec(src, chunkCallback: chunkCallback) + : CkImageElementCodec(src, chunkCallback: chunkCallback); +} diff --git a/lib/web_ui/test/ui/image/sample_image1.png b/lib/web_ui/test/ui/image/sample_image1.png new file mode 100644 index 0000000000000000000000000000000000000000..0d1393b8052134902c82d85a4c478db6a716684c GIT binary patch literal 14746 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 e(}mphinJVDosk2bwfW} Date: Fri, 10 Jan 2025 09:02:03 -0800 Subject: [PATCH 26/26] [flutter_releases] Flutter stable 3.27.2 Engine Cherrypicks (#57271) # Flutter stable 3.27.2 Engine ## Scheduled Cherrypicks - Roll dart revision: dart-lang/sdk@1bf43bfd3 ## Infra Cherrypicks - https://github.com/flutter/engine/commit/1af22178a9c7d97b4c1d4bc34960d581085c3756 - https://github.com/flutter/engine/commit/6b022dfbc8ea9a966208831c5a4c595cbf9426e1 --- DEPS | 2 +- ci/builders/mac_unopt.json | 124 ++++++------------ .../Source/FlutterViewControllerTest.mm | 21 +-- 3 files changed, 52 insertions(+), 95 deletions(-) diff --git a/DEPS b/DEPS index eb091429de1f3..5cc1fadc6b9eb 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'ae7ca5199a0559db0ae60533e9cedd3ce0d6ab04', + 'dart_revision': '1bf43bfd314768e235f6e5618842469bab6494bd', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py diff --git a/ci/builders/mac_unopt.json b/ci/builders/mac_unopt.json index 3a95e217f5670..1f9a942362030 100644 --- a/ci/builders/mac_unopt.json +++ b/ci/builders/mac_unopt.json @@ -5,8 +5,7 @@ "drone_dimensions": [ "device_type=none", "os=Mac-13|Mac-14", - "cpu=x86", - "mac_model=Macmini8,1" + "cpu=arm64" ], "gclient_variables": { "download_android_deps": false, @@ -15,9 +14,11 @@ }, "gn": [ "--target-dir", - "ci/host_debug_tests", + "ci/host_debug_arm64_tests", "--runtime-mode", "debug", + "--mac-cpu", + "arm64", "--no-lto", "--prebuilt-dart-sdk", "--build-embedder-examples", @@ -25,12 +26,11 @@ "--rbe", "--no-goma", "--xcode-symlinks" - ], - "name": "ci/host_debug_tests", - "description": "Produces debug mode x64 macOS host-side tooling and builds host-side unit tests for x64 macOS.", + "name": "ci/host_debug_arm64_tests", + "description": "Produces debug mode arm64 macOS host-side tooling and builds host-side unit tests for arm64 macOS.", "ninja": { - "config": "ci/host_debug_tests", + "config": "ci/host_debug_arm64_tests", "targets": [] }, "properties": { @@ -44,8 +44,9 @@ "name": "Host Tests for host_debug", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", - "ci/host_debug_tests", + "ci/host_debug_arm64_tests", "--type", "dart,dart-host,engine", "--engine-capture-core-dump" @@ -58,8 +59,7 @@ "drone_dimensions": [ "device_type=none", "os=Mac-13|Mac-14", - "cpu=x86", - "mac_model=Macmini8,1" + "cpu=arm64" ], "gclient_variables": { "download_android_deps": false, @@ -67,9 +67,11 @@ }, "gn": [ "--target-dir", - "ci/host_profile_tests", + "ci/host_profile_arm64_tests", "--runtime-mode", "profile", + "--mac-cpu", + "arm64", "--no-lto", "--prebuilt-dart-sdk", "--build-embedder-examples", @@ -77,10 +79,10 @@ "--no-goma", "--xcode-symlinks" ], - "name": "ci/host_profile_tests", - "description": "Produces profile mode x64 macOS host-side tooling and builds host-side unit tests for x64 macOS.", + "name": "ci/host_profile_arm64_tests", + "description": "Produces profile mode arm64 macOS host-side tooling and builds host-side unit tests for arm64 macOS.", "ninja": { - "config": "ci/host_profile_tests", + "config": "ci/host_profile_arm64_tests", "targets": [] }, "properties": { @@ -94,8 +96,9 @@ "name": "Host Tests for host_profile", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", - "ci/host_profile_tests", + "ci/host_profile_arm64_tests", "--type", "dart,dart-host,engine", "--engine-capture-core-dump" @@ -108,14 +111,7 @@ "drone_dimensions": [ "device_type=none", "os=Mac-13|Mac-14", - "cpu=x86", - "mac_model=Macmini8,1" - ], - "dependencies": [ - { - "dependency": "goldctl", - "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd" - } + "cpu=arm64" ], "gclient_variables": { "download_android_deps": false, @@ -123,9 +119,11 @@ }, "gn": [ "--target-dir", - "ci/host_release_tests", + "ci/host_release_arm64_tests", "--runtime-mode", "release", + "--mac-cpu", + "arm64", "--no-lto", "--prebuilt-dart-sdk", "--build-embedder-examples", @@ -134,10 +132,10 @@ "--no-goma", "--xcode-symlinks" ], - "name": "ci/host_release_tests", - "description": "Produces release mode x64 macOS host-side tooling and builds host-side unit tests for x64 macOS.", + "name": "ci/host_release_arm64_tests", + "description": "Produces release mode arm64 macOS host-side tooling and builds host-side unit tests for arm64 macOS.", "ninja": { - "config": "ci/host_release_tests", + "config": "ci/host_release_arm64_tests", "targets": [] }, "properties": { @@ -148,11 +146,12 @@ "tests": [ { "language": "python3", - "name": "Impeller-golden, dart and engine tests for host_release", + "name": "Dart and engine tests for host_release", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", - "ci/host_release_tests", + "ci/host_release_arm64_tests", "--type", "dart,dart-host,engine" ] @@ -208,6 +207,7 @@ "name": "Impeller-golden for host_release", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", "ci/mac_release_arm64_tests", "--type", @@ -216,62 +216,6 @@ } ] }, - { - "cas_archive": false, - "drone_dimensions": [ - "device_type=none", - "os=Mac-13|Mac-14", - "cpu=x86", - "mac_model=Macmini8,1" - ], - "gclient_variables": { - "download_android_deps": false, - "use_rbe": true - }, - "gn": [ - "--target-dir", - "ci/host_debug_unopt", - "--runtime-mode", - "debug", - "--unoptimized", - "--no-lto", - "--prebuilt-dart-sdk", - "--enable-impeller-3d", - "--rbe", - "--no-goma", - "--xcode-symlinks" - ], - "name": "ci/host_debug_unopt", - "description": "Builds a debug mode unopt x64 macOS engine and runs host-side tests.", - "ninja": { - "config": "ci/host_debug_unopt", - "targets": [] - }, - "properties": { - "$flutter/osx_sdk": { - "sdk_version": "15a240d" - } - }, - "tests": [ - { - "language": "python3", - "name": "Host Tests for host_debug_unopt", - "script": "flutter/testing/run_tests.py", - "parameters": [ - "--variant", - "ci/host_debug_unopt", - "--type", - "dart,dart-host,engine", - "--engine-capture-core-dump" - ] - }, - { - "name": "Tests of tools/gn", - "language": "python3", - "script": "flutter/tools/gn_test.py" - } - ] - }, { "cas_archive": false, "properties": { @@ -318,6 +262,7 @@ "name": "Tests for ios_debug_unopt_sim", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", "ci/ios_debug_unopt_sim", "--type", @@ -358,6 +303,7 @@ "--prebuilt-dart-sdk", "--mac-cpu", "arm64", + "--enable-impeller-3d", "--rbe", "--no-goma", "--xcode-symlinks", @@ -381,6 +327,7 @@ "name": "Host Tests for host_debug_unopt_arm64", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", "ci/host_debug_unopt_arm64", "--type", @@ -388,6 +335,11 @@ "--engine-capture-core-dump", "--no-skia-gold" ] + }, + { + "name": "Tests of tools/gn", + "language": "python3", + "script": "flutter/tools/gn_test.py" } ] }, @@ -439,6 +391,7 @@ "name": "Tests for ios_debug_unopt_sim_arm64", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", "ci/ios_debug_unopt_sim_arm64", "--type", @@ -507,6 +460,7 @@ "name": "Tests for ios_debug_unopt_sim_arm64_extension_safe", "script": "flutter/testing/run_tests.py", "parameters": [ + "--quiet", "--variant", "ci/ios_debug_unopt_sim_arm64_extension_safe", "--type", diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index b63cf439c96a1..2eff3d715677b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -21,6 +21,9 @@ #pragma mark - Test Helper Classes +static const FlutterPointerEvent kDefaultFlutterPointerEvent = {}; +static const FlutterKeyEvent kDefaultFlutterKeyEvent = {}; + // A wrap to convert FlutterKeyEvent to a ObjC class. @interface KeyEventWrapper : NSObject @property(nonatomic) FlutterKeyEvent* data; @@ -339,7 +342,7 @@ - (bool)testKeyEventsAreSentToFramework:(id)engineMock { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andCall([FlutterViewControllerTestObjC class], @@ -375,7 +378,7 @@ - (bool)testKeyEventsAreSentToFramework:(id)engineMock { - (bool)testCtrlTabKeyEventIsPropagated:(id)engineMock { __block bool called = false; __block FlutterKeyEvent last_event; - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andDo((^(NSInvocation* invocation) { @@ -419,7 +422,7 @@ - (bool)testCtrlTabKeyEventIsPropagated:(id)engineMock { - (bool)testKeyEquivalentIsPassedToTextInputPlugin:(id)engineMock { __block bool called = false; __block FlutterKeyEvent last_event; - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andDo((^(NSInvocation* invocation) { @@ -471,7 +474,7 @@ - (bool)testKeyEventsArePropagatedIfNotHandled:(id)engineMock { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andCall([FlutterViewControllerTestObjC class], @@ -545,7 +548,7 @@ - (bool)testFlagsChangedEventsArePropagatedIfNotHandled:(id)engineMock { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andCall([FlutterViewControllerTestObjC class], @@ -598,7 +601,7 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled:(id)engineMock { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andCall([FlutterViewControllerTestObjC class], @@ -655,7 +658,7 @@ - (bool)testKeyboardIsRestartedOnEngineRestart:(id)engineMock { .andReturn(binaryMessengerMock); __block bool called = false; __block FlutterKeyEvent last_event; - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andDo((^(NSInvocation* invocation) { @@ -715,7 +718,7 @@ - (bool)testTrackpadGesturesAreSentToFramework:(id)engineMock { OCMStub([engineMock renderer]).andReturn(renderer_); __block bool called = false; __block FlutterPointerEvent last_event; - OCMStub([[engineMock ignoringNonObjectArgs] sendPointerEvent:FlutterPointerEvent{}]) + OCMStub([[engineMock ignoringNonObjectArgs] sendPointerEvent:kDefaultFlutterPointerEvent]) .andDo((^(NSInvocation* invocation) { FlutterPointerEvent* event; [invocation getArgument:&event atIndex:2]; @@ -1139,7 +1142,7 @@ - (bool)testModifierKeysAreSynthesizedOnMouseMove:(id)engineMock { // Capture calls to sendKeyEvent __block NSMutableArray* events = [NSMutableArray array]; - OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent callback:nil userData:nil]) .andDo((^(NSInvocation* invocation) {