diff --git a/BUILD.gn b/BUILD.gn index 85997c6568e09..7a759d70bd8f9 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -191,6 +191,7 @@ group("unittests") { if (is_mac) { public_deps += [ "//flutter/impeller/golden_tests:impeller_golden_tests", + "//flutter/shell/platform/darwin/common:availability_version_check_unittests", "//flutter/shell/platform/darwin/common:framework_common_unittests", "//flutter/third_party/spring_animation:spring_animation_unittests", ] diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index feed60b197fdf..412f6d957fbdb 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -280,6 +280,7 @@ ../../../flutter/shell/platform/common/text_input_model_unittests.cc ../../../flutter/shell/platform/common/text_range_unittests.cc ../../../flutter/shell/platform/darwin/Doxyfile +../../../flutter/shell/platform/darwin/common/availability_version_check_unittests.cc ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_codecs_unittest.mm ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm ../../../flutter/shell/platform/darwin/macos/README.md diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index f31b0f04c8c8b..f5fa01c758d78 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2597,6 +2597,7 @@ ORIGIN: ../../../flutter/shell/platform/common/text_input_model.cc + ../../../fl ORIGIN: ../../../flutter/shell/platform/common/text_input_model.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/text_range.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/command_line.h + ../../../flutter/LICENSE @@ -2731,6 +2732,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibilit ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection_test.mm + ../../../flutter/LICENSE @@ -5367,6 +5369,7 @@ FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_range.h FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc +FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.mm FILE: ../../../flutter/shell/platform/darwin/common/command_line.h @@ -5502,6 +5505,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection_test.mm diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index 115f3c1950007..42b9fd6ab0122 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -2416,19 +2416,18 @@ TEST_P(AiksTest, ClearColorOptimizationDoesNotApplyForBackdropFilters) { Picture picture = canvas.EndRecordingAsPicture(); std::optional actual_color; + bool found_subpass = false; picture.pass->IterateAllElements([&](EntityPass::Element& element) -> bool { if (auto subpass = std::get_if>(&element)) { actual_color = subpass->get()->GetClearColor(); + found_subpass = true; } // Fail if the first element isn't a subpass. return true; }); - ASSERT_TRUE(actual_color.has_value()); - if (!actual_color) { - return; - } - ASSERT_EQ(actual_color.value(), Color::BlackTransparent()); + EXPECT_TRUE(found_subpass); + EXPECT_FALSE(actual_color.has_value()); } TEST_P(AiksTest, CollapsedDrawPaintInSubpass) { @@ -3645,5 +3644,27 @@ TEST_P(AiksTest, MaskBlurWithZeroSigmaIsSkipped) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, SubpassWithClearColorOptimization) { + Canvas canvas; + + // Use a non-srcOver blend mode to ensure that we don't detect this as an + // opacity peephole optimization. + canvas.SaveLayer( + {.color = Color::Blue().WithAlpha(0.5), .blend_mode = BlendMode::kSource}, + Rect::MakeLTRB(0, 0, 200, 200)); + canvas.DrawPaint( + {.color = Color::BlackTransparent(), .blend_mode = BlendMode::kSource}); + canvas.Restore(); + + canvas.SaveLayer( + {.color = Color::Blue(), .blend_mode = BlendMode::kDestinationOver}); + canvas.Restore(); + + // This playground should appear blank on CI since we are only drawing + // transparent black. If the clear color optimization is broken, the texture + // will be filled with NaNs and may produce a magenta texture on macOS or iOS. + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + } // namespace testing } // namespace impeller diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index 32a49871ac506..4c4a0ab3f263c 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -540,6 +540,14 @@ void Canvas::SaveLayer(const Paint& paint, const std::shared_ptr& backdrop_filter) { Save(true, paint.blend_mode, backdrop_filter); + // The DisplayList bounds/rtree doesn't account for filters applied to parent + // layers, and so sub-DisplayLists are getting culled as if no filters are + // applied. + // See also: https://github.com/flutter/flutter/issues/139294 + if (paint.image_filter) { + xformation_stack_.back().cull_rect = std::nullopt; + } + auto& new_layer_pass = GetCurrentPass(); new_layer_pass.SetBoundsLimit(bounds); diff --git a/impeller/aiks/canvas_unittests.cc b/impeller/aiks/canvas_unittests.cc index ed83f0500a254..728316cb3c61d 100644 --- a/impeller/aiks/canvas_unittests.cc +++ b/impeller/aiks/canvas_unittests.cc @@ -4,6 +4,7 @@ #include "flutter/testing/testing.h" #include "impeller/aiks/canvas.h" +#include "impeller/aiks/image_filter.h" #include "impeller/geometry/path_builder.h" // TODO(zanderso): https://github.com/flutter/flutter/issues/127701 @@ -336,6 +337,23 @@ TEST(AiksCanvasTest, PathClipDiffAgainstFullyCoveredCullRect) { ASSERT_EQ(canvas.GetCurrentLocalCullingBounds().value(), result_cull); } +TEST(AiksCanvasTest, DisableLocalBoundsRectForFilteredSaveLayers) { + Rect initial_cull = Rect::MakeXYWH(0, 0, 10, 10); + + Canvas canvas(initial_cull); + ASSERT_TRUE(canvas.GetCurrentLocalCullingBounds().has_value()); + + canvas.Save(); + canvas.SaveLayer( + Paint{.image_filter = ImageFilter::MakeBlur( + Sigma(10), Sigma(10), FilterContents::BlurStyle::kNormal, + Entity::TileMode::kDecal)}); + ASSERT_FALSE(canvas.GetCurrentLocalCullingBounds().has_value()); + + canvas.Restore(); + ASSERT_TRUE(canvas.GetCurrentLocalCullingBounds().has_value()); +} + } // namespace testing } // namespace impeller diff --git a/impeller/aiks/paint_pass_delegate.cc b/impeller/aiks/paint_pass_delegate.cc index f9a081a0b55cf..604e4dc92831c 100644 --- a/impeller/aiks/paint_pass_delegate.cc +++ b/impeller/aiks/paint_pass_delegate.cc @@ -10,7 +10,6 @@ #include "impeller/entity/contents/texture_contents.h" #include "impeller/entity/entity_pass.h" #include "impeller/geometry/color.h" -#include "impeller/geometry/path_builder.h" namespace impeller { diff --git a/impeller/entity/contents/clip_contents.cc b/impeller/entity/contents/clip_contents.cc index 21c2eb1aa2469..77bbccc82f0c7 100644 --- a/impeller/entity/contents/clip_contents.cc +++ b/impeller/entity/contents/clip_contents.cc @@ -66,7 +66,7 @@ Contents::StencilCoverage ClipContents::GetStencilCoverage( bool ClipContents::ShouldRender( const Entity& entity, - const std::optional& stencil_coverage) const { + const std::optional stencil_coverage) const { return true; } @@ -163,7 +163,7 @@ Contents::StencilCoverage ClipRestoreContents::GetStencilCoverage( bool ClipRestoreContents::ShouldRender( const Entity& entity, - const std::optional& stencil_coverage) const { + const std::optional stencil_coverage) const { return true; } diff --git a/impeller/entity/contents/clip_contents.h b/impeller/entity/contents/clip_contents.h index 3b0faac98bc60..bc7b89ac055a5 100644 --- a/impeller/entity/contents/clip_contents.h +++ b/impeller/entity/contents/clip_contents.h @@ -35,7 +35,7 @@ class ClipContents final : public Contents { // |Contents| bool ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const override; + const std::optional stencil_coverage) const override; // |Contents| bool Render(const ContentContext& renderer, @@ -76,7 +76,7 @@ class ClipRestoreContents final : public Contents { // |Contents| bool ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const override; + const std::optional stencil_coverage) const override; // |Contents| bool Render(const ContentContext& renderer, diff --git a/impeller/entity/contents/contents.cc b/impeller/entity/contents/contents.cc index a181e22302b21..4671d23b4c0d5 100644 --- a/impeller/entity/contents/contents.cc +++ b/impeller/entity/contents/contents.cc @@ -133,11 +133,10 @@ bool Contents::ApplyColorFilter( } bool Contents::ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const { + const std::optional stencil_coverage) const { if (!stencil_coverage.has_value()) { return false; } - auto coverage = GetCoverage(entity); if (!coverage.has_value()) { return false; diff --git a/impeller/entity/contents/contents.h b/impeller/entity/contents/contents.h index b9dec5db2d12e..11b5ad00570de 100644 --- a/impeller/entity/contents/contents.h +++ b/impeller/entity/contents/contents.h @@ -113,7 +113,7 @@ class Contents { const std::string& label = "Snapshot") const; virtual bool ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const; + const std::optional stencil_coverage) const; //---------------------------------------------------------------------------- /// @brief Return the color source's intrinsic size, if available. diff --git a/impeller/entity/entity.cc b/impeller/entity/entity.cc index b7ebc2af966c9..ae18016932911 100644 --- a/impeller/entity/entity.cc +++ b/impeller/entity/entity.cc @@ -71,7 +71,11 @@ Contents::StencilCoverage Entity::GetStencilCoverage( } bool Entity::ShouldRender(const std::optional& stencil_coverage) const { +#ifdef IMPELLER_CONTENT_CULLING return contents_->ShouldRender(*this, stencil_coverage); +#else + return true; +#endif // IMPELLER_CONTENT_CULLING } void Entity::SetContents(std::shared_ptr contents) { diff --git a/impeller/entity/entity_pass.cc b/impeller/entity/entity_pass.cc index 64580febfcf24..495856a338bb5 100644 --- a/impeller/entity/entity_pass.cc +++ b/impeller/entity/entity_pass.cc @@ -368,7 +368,7 @@ bool EntityPass::Render(ContentContext& renderer, if (!supports_onscreen_backdrop_reads && reads_from_onscreen_backdrop) { auto offscreen_target = CreateRenderTarget( renderer, root_render_target.GetRenderTargetSize(), true, - GetClearColor(render_target.GetRenderTargetSize())); + GetClearColorOrDefault(render_target.GetRenderTargetSize())); if (!OnRender(renderer, // renderer capture, // capture @@ -475,7 +475,8 @@ bool EntityPass::Render(ContentContext& renderer, } // Set up the clear color of the root pass. - color0.clear_color = GetClearColor(render_target.GetRenderTargetSize()); + color0.clear_color = + GetClearColorOrDefault(render_target.GetRenderTargetSize()); root_render_target.SetColorAttachment(color0, 0); EntityPassTarget pass_target( @@ -628,10 +629,10 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( } auto subpass_target = CreateRenderTarget( - renderer, // renderer - subpass_size, // size - subpass->GetTotalPassReads(renderer) > 0, // readable - subpass->GetClearColor(subpass_size)); // clear_color + renderer, // renderer + subpass_size, // size + subpass->GetTotalPassReads(renderer) > 0, // readable + subpass->GetClearColorOrDefault(subpass_size)); // clear_color if (!subpass_target.IsValid()) { VALIDATION_LOG << "Subpass render target is invalid."; @@ -722,8 +723,7 @@ bool EntityPass::OnRender( } auto clear_color_size = pass_target.GetRenderTarget().GetRenderTargetSize(); - if (!collapsed_parent_pass && - !GetClearColor(clear_color_size).IsTransparent()) { + if (!collapsed_parent_pass && GetClearColor(clear_color_size).has_value()) { // Force the pass context to create at least one new pass if the clear color // is present. pass_context.GetRenderPass(pass_depth); @@ -1140,21 +1140,29 @@ void EntityPass::SetBlendMode(BlendMode blend_mode) { flood_clip_ = Entity::IsBlendModeDestructive(blend_mode); } -Color EntityPass::GetClearColor(ISize target_size) const { - Color result = Color::BlackTransparent(); +Color EntityPass::GetClearColorOrDefault(ISize size) const { + return GetClearColor(size).value_or(Color::BlackTransparent()); +} + +std::optional EntityPass::GetClearColor(ISize target_size) const { if (backdrop_filter_proc_) { - return result; + return std::nullopt; } + std::optional result = std::nullopt; for (const Element& element : elements_) { auto [entity_color, blend_mode] = ElementAsBackgroundColor(element, target_size); if (!entity_color.has_value()) { break; } - result = result.Blend(entity_color.value(), blend_mode); + result = result.value_or(Color::BlackTransparent()) + .Blend(entity_color.value(), blend_mode); } - return result.Premultiply(); + if (result.has_value()) { + return result->Premultiply(); + } + return result; } void EntityPass::SetBackdropFilter(BackdropFilterProc proc) { diff --git a/impeller/entity/entity_pass.h b/impeller/entity/entity_pass.h index d09649abfd9d0..c03a837047eeb 100644 --- a/impeller/entity/entity_pass.h +++ b/impeller/entity/entity_pass.h @@ -135,7 +135,13 @@ class EntityPass { void SetBlendMode(BlendMode blend_mode); - Color GetClearColor(ISize size = ISize::Infinite()) const; + /// @brief Return the premultiplied clear color of the pass entities, if any. + std::optional GetClearColor(ISize size = ISize::Infinite()) const; + + /// @brief Return the premultiplied clear color of the pass entities. + /// + /// If the entity pass has no clear color, this will return transparent black. + Color GetClearColorOrDefault(ISize size = ISize::Infinite()) const; void SetBackdropFilter(BackdropFilterProc proc); diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc index ca094aa0fe54f..2b28fa3ede2e7 100644 --- a/impeller/entity/entity_unittests.cc +++ b/impeller/entity/entity_unittests.cc @@ -1607,6 +1607,20 @@ TEST_P(EntityTest, SolidFillShouldRenderIsCorrect) { } } +TEST_P(EntityTest, DoesNotCullEntitiesByDefault) { + auto fill = std::make_shared(); + fill->SetColor(Color::CornflowerBlue()); + fill->SetGeometry( + Geometry::MakeRect(Rect::MakeLTRB(-1000, -1000, -900, -900))); + + Entity entity; + entity.SetContents(fill); + + // Even though the entity is offscreen, this should still render because we do + // not compute the coverage intersection by default. + EXPECT_TRUE(entity.ShouldRender(Rect::MakeLTRB(0, 0, 100, 100))); +} + TEST_P(EntityTest, ClipContentsShouldRenderIsCorrect) { // For clip ops, `ShouldRender` should always return true. diff --git a/shell/platform/darwin/common/BUILD.gn b/shell/platform/darwin/common/BUILD.gn index 0d29be6c703eb..786deba37e03f 100644 --- a/shell/platform/darwin/common/BUILD.gn +++ b/shell/platform/darwin/common/BUILD.gn @@ -50,6 +50,25 @@ source_set("availability_version_check") { public_configs = [ "//flutter:config" ] } +test_fixtures("availability_version_check_fixtures") { + fixtures = [] +} + +executable("availability_version_check_unittests") { + testonly = true + + sources = [ "availability_version_check_unittests.cc" ] + + deps = [ + ":availability_version_check", + ":availability_version_check_fixtures", + "//flutter/fml", + "//flutter/testing", + ] + + public_configs = [ "//flutter:config" ] +} + # Shared framework headers end up in the same folder as platform-specific # framework headers when consumed by clients, so the include paths assume they # are next to each other. diff --git a/shell/platform/darwin/common/availability_version_check.cc b/shell/platform/darwin/common/availability_version_check.cc index 67514cbf5561f..1564bec4335f0 100644 --- a/shell/platform/darwin/common/availability_version_check.cc +++ b/shell/platform/darwin/common/availability_version_check.cc @@ -2,20 +2,141 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "flutter/shell/platform/darwin/common/availability_version_check.h" + +#include +#include +#include + +#include #include #include -#include +#include "flutter/fml/build_config.h" +#include "flutter/fml/file.h" #include "flutter/fml/logging.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/platform/darwin/cf_utils.h" + +// The implementation of _availability_version_check defined in this file is +// based on the code in the clang-rt library at: +// +// https://github.com/llvm/llvm-project/blob/e315bf25a843582de39257e1345408a10dc08224/compiler-rt/lib/builtins/os_version_check.c +// +// Flutter provides its own implementation due to an issue introduced in recent +// versions of Clang following Clang 18 in which the clang-rt library declares +// weak linkage against the _availability_version_check symbol. This declaration +// causes apps to be rejected from the App Store. When Flutter statically links +// the implementation below, the weak linkage is satisfied at Engine build time, +// the symbol is no longer exposed from the Engine dylib, and apps will then +// not be rejected from the App Store. +// +// The implementation of _availability_version_check can delegate to the +// dynamically looked-up symbol on recent iOS versions, but the lookup will fail +// on iOS 11 and 12. When the lookup fails, the current OS version must be +// retrieved from a plist file at a well-known path. The logic for this below is +// copied from the clang-rt implementation and adapted for the Engine. -// See context in https://github.com/flutter/flutter/issues/132130 and +// See more context in https://github.com/flutter/flutter/issues/132130 and // https://github.com/flutter/engine/pull/44711. // TODO(zanderso): Remove this after Clang 18 rolls into Xcode. -// https://github.com/flutter/flutter/issues/133203 +// https://github.com/flutter/flutter/issues/133203. + +#define CF_PROPERTY_LIST_IMMUTABLE 0 + +namespace flutter { + +// This function parses the platform's version information out of a plist file +// at a well-known path. It parses the plist file using CoreFoundation functions +// to match the implementation in the clang-rt library. +std::optional ProductVersionFromSystemVersionPList() { + std::string plist_path = "/System/Library/CoreServices/SystemVersion.plist"; +#if FML_OS_IOS_SIMULATOR + char* plist_path_prefix = getenv("IPHONE_SIMULATOR_ROOT"); + if (!plist_path_prefix) { + FML_DLOG(ERROR) << "Failed to getenv IPHONE_SIMULATOR_ROOT"; + return std::nullopt; + } + plist_path = std::string(plist_path_prefix) + plist_path; +#endif // FML_OS_IOS_SIMULATOR + + auto plist_mapping = fml::FileMapping::CreateReadOnly(plist_path); + + // Get the file buffer into CF's format. We pass in a null allocator here * + // because we free PListBuf ourselves + auto file_contents = fml::CFRef(CFDataCreateWithBytesNoCopy( + nullptr, plist_mapping->GetMapping(), + static_cast(plist_mapping->GetSize()), kCFAllocatorNull)); + if (!file_contents) { + FML_DLOG(ERROR) << "Failed to CFDataCreateWithBytesNoCopyFunc"; + return std::nullopt; + } + + auto plist = fml::CFRef( + reinterpret_cast(CFPropertyListCreateWithData( + nullptr, file_contents, CF_PROPERTY_LIST_IMMUTABLE, nullptr, + nullptr))); + if (!plist) { + FML_DLOG(ERROR) << "Failed to CFPropertyListCreateWithDataFunc or " + "CFPropertyListCreateFromXMLDataFunc"; + return std::nullopt; + } + + auto product_version = + fml::CFRef(CFStringCreateWithCStringNoCopy( + nullptr, "ProductVersion", kCFStringEncodingASCII, kCFAllocatorNull)); + if (!product_version) { + FML_DLOG(ERROR) << "Failed to CFStringCreateWithCStringNoCopyFunc"; + return std::nullopt; + } + CFTypeRef opaque_value = CFDictionaryGetValue(plist, product_version); + if (!opaque_value || CFGetTypeID(opaque_value) != CFStringGetTypeID()) { + FML_DLOG(ERROR) << "Failed to CFDictionaryGetValueFunc"; + return std::nullopt; + } + + char version_str[32]; + if (!CFStringGetCString(reinterpret_cast(opaque_value), + version_str, sizeof(version_str), + kCFStringEncodingUTF8)) { + FML_DLOG(ERROR) << "Failed to CFStringGetCStringFunc"; + return std::nullopt; + } + + int32_t major = 0; + int32_t minor = 0; + int32_t subminor = 0; + int matches = sscanf(version_str, "%d.%d.%d", &major, &minor, &subminor); + // A major version number is sufficient. The minor and subminor numbers might + // not be present. + if (matches < 1) { + FML_DLOG(ERROR) << "Failed to match product version string: " + << version_str; + return std::nullopt; + } + + return ProductVersion{major, minor, subminor}; +} + +bool IsEncodedVersionLessThanOrSame(uint32_t encoded_lhs, ProductVersion rhs) { + // Parse the values out of encoded_lhs, then compare against rhs. + const int32_t major = (encoded_lhs >> 16) & 0xffff; + const int32_t minor = (encoded_lhs >> 8) & 0xff; + const int32_t subminor = encoded_lhs & 0xff; + auto lhs = ProductVersion{major, minor, subminor}; + + return lhs <= rhs; +} + +} // namespace flutter namespace { +// The host's OS version when the dynamic lookup of _availability_version_check +// has failed. +static flutter::ProductVersion g_version; + typedef uint32_t dyld_platform_t; typedef struct { @@ -36,13 +157,41 @@ void InitializeAvailabilityCheck(void* unused) { } AvailabilityVersionCheck = reinterpret_cast( dlsym(RTLD_DEFAULT, "_availability_version_check")); - FML_CHECK(AvailabilityVersionCheck); + if (AvailabilityVersionCheck) { + return; + } + + // If _availability_version_check can't be dynamically loaded, then version + // information must be parsed out of a system plist file. + auto product_version = flutter::ProductVersionFromSystemVersionPList(); + if (product_version.has_value()) { + g_version = product_version.value(); + } else { + // If reading version info out of the system plist file fails, then + // fall back to the minimum version that Flutter supports. +#if FML_OS_IOS || FML_OS_IOS_SIMULATOR + g_version = std::make_tuple(11, 0, 0); +#elif FML_OS_MACOSX + g_version = std::make_tuple(10, 14, 0); +#endif // FML_OS_MACOSX + } } extern "C" bool _availability_version_check(uint32_t count, dyld_build_version_t versions[]) { dispatch_once_f(&DispatchOnceCounter, NULL, InitializeAvailabilityCheck); - return AvailabilityVersionCheck(count, versions); + if (AvailabilityVersionCheck) { + return AvailabilityVersionCheck(count, versions); + } + + if (count == 0) { + return true; + } + + // This function is called in only one place in the clang-rt implementation + // where there is only one element in the array. + return flutter::IsEncodedVersionLessThanOrSame(versions[0].version, + g_version); } } // namespace diff --git a/shell/platform/darwin/common/availability_version_check.h b/shell/platform/darwin/common/availability_version_check.h new file mode 100644 index 0000000000000..8724a72d1990a --- /dev/null +++ b/shell/platform/darwin/common/availability_version_check.h @@ -0,0 +1,18 @@ +// 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. + +#include +#include +#include + +namespace flutter { + +using ProductVersion = + std::tuple; + +std::optional ProductVersionFromSystemVersionPList(); + +bool IsEncodedVersionLessThanOrSame(uint32_t encoded_lhs, ProductVersion rhs); + +} // namespace flutter diff --git a/shell/platform/darwin/common/availability_version_check_unittests.cc b/shell/platform/darwin/common/availability_version_check_unittests.cc new file mode 100644 index 0000000000000..7753d1cfa522d --- /dev/null +++ b/shell/platform/darwin/common/availability_version_check_unittests.cc @@ -0,0 +1,35 @@ +// 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. + +#include + +#include "flutter/shell/platform/darwin/common/availability_version_check.h" + +#include "gtest/gtest.h" + +TEST(AvailabilityVersionCheck, CanDecodeSystemPlist) { + auto maybe_product_version = flutter::ProductVersionFromSystemVersionPList(); + ASSERT_TRUE(maybe_product_version.has_value()); + if (maybe_product_version.has_value()) { + auto product_version = maybe_product_version.value(); + ASSERT_GT(product_version, std::make_tuple(0, 0, 0)); + } +} + +static inline uint32_t ConstructVersion(uint32_t major, + uint32_t minor, + uint32_t subminor) { + return ((major & 0xffff) << 16) | ((minor & 0xff) << 8) | (subminor & 0xff); +} + +TEST(AvailabilityVersionCheck, CanParseAndCompareVersions) { + auto rhs_version = std::make_tuple(17, 2, 0); + uint32_t encoded_lower_version = ConstructVersion(12, 3, 7); + ASSERT_TRUE(flutter::IsEncodedVersionLessThanOrSame(encoded_lower_version, + rhs_version)); + + uint32_t encoded_higher_version = ConstructVersion(42, 0, 1); + ASSERT_FALSE(flutter::IsEncodedVersionLessThanOrSame(encoded_higher_version, + rhs_version)); +} diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index ee7c546ac7b99..e3bbb98479ca2 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -328,6 +328,7 @@ shared_library("ios_test_flutter") { "framework/Source/FlutterViewControllerTest.mm", "framework/Source/SemanticsObjectTest.mm", "framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm", + "framework/Source/availability_version_check_test.mm", "framework/Source/connection_collection_test.mm", ] deps = [ diff --git a/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm b/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm new file mode 100644 index 0000000000000..c843893c216e8 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm @@ -0,0 +1,26 @@ +// 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 + +#import +#import + +#import "flutter/shell/platform/darwin/common/availability_version_check.h" + +@interface AvailabilityVersionCheckTest : XCTestCase +@end + +@implementation AvailabilityVersionCheckTest + +- (void)testSimple { + auto maybe_product_version = flutter::ProductVersionFromSystemVersionPList(); + XCTAssertTrue(maybe_product_version.has_value()); + if (maybe_product_version.has_value()) { + auto product_version = maybe_product_version.value(); + XCTAssertTrue(product_version > std::make_tuple(0, 0, 0)); + } +} + +@end diff --git a/testing/run_tests.py b/testing/run_tests.py index 9a0e24f4f1022..64a17f4efdc09 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -434,6 +434,7 @@ def make_test(name, flags=None, extra_env=None): unittests += [ # The accessibility library only supports Mac and Windows. make_test('accessibility_unittests'), + make_test('availability_version_check_unittests'), make_test('framework_common_unittests'), make_test('spring_animation_unittests'), ] diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 7c15465dc9ea8..92679d2a94ae1 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -510,6 +510,13 @@ class MultiPlatformViewBackgroundForegroundScenario extends Scenario PlatformMessageResponseCallback? callback, ) { final String message = utf8.decode(data!.buffer.asUint8List()); + + // The expected first event should be 'AppLifecycleState.resumed', but + // occasionally it will receive 'AppLifecycleState.inactive' first. Skip + // any messages until 'AppLifecycleState.resumed' is received. + if (_lastLifecycleState.isEmpty && message != 'AppLifecycleState.resumed') { + return; + } if (_lastLifecycleState == 'AppLifecycleState.inactive' && message == 'AppLifecycleState.resumed') { _nextFrame = _secondFrame;