diff --git a/DEPS b/DEPS index 3d3ec3297b6b..f83659eaf031 100644 --- a/DEPS +++ b/DEPS @@ -31,7 +31,7 @@ vars = { # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. - 'canvaskit_cipd_instance': 'NcwvqeeKK7urddCbEdDvHytdaCiCA_8-4oS_T_ouGfgC', + 'canvaskit_cipd_instance': '8MSYGWVWzrTJIoVL00ZquruZs-weuwLBy1kt1AawJiIC', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the @@ -39,7 +39,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': '547d54e13cc8c1ce9279792fd16c189663e18f96', + 'dart_revision': '0180af250ff518cc0fa494a4eb484ce11ec1e62c', # 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/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index ab5895d8245b..8ef627e52a5e 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 51c303e95a17cde2eb9ee7806c16a1c5 +Signature: cc2abaf0233d38199643282d27336c28 UNUSED LICENSES: diff --git a/lib/web_ui/dev/canvaskit_lock.yaml b/lib/web_ui/dev/canvaskit_lock.yaml index f66b919c09cf..d83e801d1694 100644 --- a/lib/web_ui/dev/canvaskit_lock.yaml +++ b/lib/web_ui/dev/canvaskit_lock.yaml @@ -1,4 +1,4 @@ # Specifies the version of CanvasKit to use for Flutter Web apps. # # See `lib/web_ui/README.md` for how to update this file. -canvaskit_version: "0.31.0" +canvaskit_version: "0.33.0" diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 3eb49efe23e0..d5fcf7c2b660 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -758,9 +758,14 @@ class SkColorType { class SkAnimatedImage { external int getFrameCount(); - /// Returns duration in milliseconds. external int getRepetitionCount(); + + /// Returns duration in milliseconds. + external int currentFrameDuration(); + + /// Advances to the next frame and returns its duration in milliseconds. external int decodeNextFrame(); + external SkImage makeImageAtCurrentFrame(); external int width(); external int height(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart b/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart index 2467e2a32a89..15a6a93834ee 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart @@ -31,8 +31,8 @@ class CkAnimatedImage extends ManagedSkiaObject int _frameCount = 0; int _repetitionCount = -1; - /// The index to the next frame to be decoded. - int _nextFrameIndex = 0; + /// Current frame index. + int _currentFrameIndex = 0; @override SkAnimatedImage createDefault() { @@ -48,11 +48,16 @@ class CkAnimatedImage extends ManagedSkiaObject _frameCount = animatedImage.getFrameCount(); _repetitionCount = animatedImage.getRepetitionCount(); - // If the object has been deleted then resurrected, it may already have - // iterated over some frames. We need to skip over them. - for (int i = 0; i < _nextFrameIndex; i++) { + // Normally CanvasKit initializes `SkAnimatedImage` to point to the first + // frame in the animation. However, if the Skia object has been deleted then + // resurrected, the framework/app may already have advanced to one of the + // subsequent frames. When that happens the value of _currentFrameIndex will + // be something other than zero, and we need to tell the decoder to skip + // over the previous frames to point to the current one. + for (int i = 0; i < _currentFrameIndex; i++) { animatedImage.decodeNextFrame(); } + return animatedImage; } @@ -100,10 +105,23 @@ class CkAnimatedImage extends ManagedSkiaObject @override Future getNextFrame() { assert(_debugCheckIsNotDisposed()); - final int durationMillis = skiaObject.decodeNextFrame(); - final Duration duration = Duration(milliseconds: durationMillis); - final CkImage image = CkImage(skiaObject.makeImageAtCurrentFrame()); - _nextFrameIndex = (_nextFrameIndex + 1) % _frameCount; - return Future.value(AnimatedImageFrameInfo(duration, image)); + final SkAnimatedImage animatedImage = skiaObject; + + // SkAnimatedImage comes pre-initialized to point to the current frame (by + // default the first frame, and, with some special resurrection logic in + // `createDefault`, to a subsequent frame if resurrection happens in the + // middle of animation). Flutter's `Codec` semantics is to initialize to + // point to "just before the first frame", i.e. the first invocation of + // `getNextFrame` returns the first frame. Therefore, we have to read the + // current Skia frame, then advance SkAnimatedImage to the next frame, and + // return the current frame. + final ui.FrameInfo currentFrame = AnimatedImageFrameInfo( + Duration(milliseconds: animatedImage.currentFrameDuration()), + CkImage(animatedImage.makeImageAtCurrentFrame()), + ); + + animatedImage.decodeNextFrame(); + _currentFrameIndex = (_currentFrameIndex + 1) % _frameCount; + return Future.value(currentFrame); } } diff --git a/lib/web_ui/lib/src/engine/configuration.dart b/lib/web_ui/lib/src/engine/configuration.dart index c5f373557f18..6d808ad541c7 100644 --- a/lib/web_ui/lib/src/engine/configuration.dart +++ b/lib/web_ui/lib/src/engine/configuration.dart @@ -32,7 +32,7 @@ import 'package:js/js.dart'; /// The version of CanvasKit used by the web engine by default. // DO NOT EDIT THE NEXT LINE OF CODE MANUALLY // See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. -const String _canvaskitVersion = '0.31.0'; +const String _canvaskitVersion = '0.33.0'; /// The Web Engine configuration for the current application. FlutterConfiguration get configuration => _configuration ??= FlutterConfiguration(_jsConfiguration); diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 5b9c0437ffb8..b18a03778e75 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -782,9 +782,10 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy { // On Safari Desktop, when a form is focused, it opens an autofill menu // immediately. // Flutter framework sends `setEditableSizeAndTransform` for informing - // the engine about the location of the text field. This call will - // arrive after `show` call. Therefore form is placed, when - // `setEditableSizeAndTransform` method is called and focus called on the + // the engine about the location of the text field. This call may arrive + // after the first `show` call, depending on the text input widget's + // implementation. Therefore form is placed, when + // `setEditableSizeAndTransform` method is called and focus called on the // form only after placing it to the correct position and only once after // that. Calling focus multiple times causes flickering. focusedFormElement!.focus(); @@ -800,6 +801,9 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy { @override void initializeElementPlacement() { + if (geometry != null) { + placeElement(); + } activeDomElement.focus(); } } diff --git a/lib/web_ui/test/canvaskit/image_golden_test.dart b/lib/web_ui/test/canvaskit/image_golden_test.dart index abfacd4bb591..fb140b3f79f3 100644 --- a/lib/web_ui/test/canvaskit/image_golden_test.dart +++ b/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -94,9 +94,9 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { expect(image.repetitionCount, -1); final ui.FrameInfo frame1 = await image.getNextFrame(); - await expectFrameData(frame1, [0, 255, 0, 255]); + await expectFrameData(frame1, [255, 0, 0, 255]); final ui.FrameInfo frame2 = await image.getNextFrame(); - await expectFrameData(frame2, [0, 0, 255, 255]); + await expectFrameData(frame2, [0, 255, 0, 255]); // Pretend that the image is temporarily deleted. image.delete(); @@ -104,7 +104,7 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { // Check that we got the 3rd frame after resurrection. final ui.FrameInfo frame3 = await image.getNextFrame(); - await expectFrameData(frame3, [255, 0, 0, 255]); + await expectFrameData(frame3, [0, 0, 255, 255]); testCollector.collectNow(); }); @@ -548,11 +548,10 @@ void _testCkAnimatedImage() { test('CkAnimatedImage toByteData(RGBA)', () async { final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test'); - // TODO(yjbanov): frame sequence is wrong (https://github.com/flutter/flutter/issues/95281) const List> expectedColors = >[ + [255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], - [255, 0, 0, 255], ]; for (int i = 0; i < image.frameCount; i++) { final ui.FrameInfo frame = await image.getNextFrame(); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 3a37ab76743b..a5dc20790c98 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -551,6 +551,40 @@ void testMain() { expect(spy.messages, isEmpty); }); + test('setClient, setEditingState, setSizeAndTransform, show - input element is put into the DOM', () { + editingStrategy = SafariDesktopTextEditingStrategy(textEditing!); + textEditing!.debugTextEditingStrategyOverride = editingStrategy; + final MethodCall setClient = MethodCall( + 'TextInput.setClient', [123, flutterSinglelineConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); + + // Editing shouldn't have started yet. + expect(document.activeElement, document.body); + + // The "setSizeAndTransform" message has to be here before we call + // checkInputEditingState, since on some platforms (e.g. Desktop Safari) + // we don't put the input element into the DOM until we get its correct + // dimensions from the framework. + final MethodCall setSizeAndTransform = + configureSetSizeAndTransformMethodCall(150, 50, + Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); + sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + expect(defaultTextEditingRoot.activeElement, + textEditing!.strategy.domElement); + }); + test('setClient, setEditingState, show, updateConfig, clearClient', () { final MethodCall setClient = MethodCall('TextInput.setClient', [ 123, diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index ad5644c45f98..6246bcff6e26 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -81,6 +81,12 @@ void Rasterizer::Setup(std::unique_ptr surface) { } } +void Rasterizer::TeardownExternalViewEmbedder() { + if (external_view_embedder_) { + external_view_embedder_->Teardown(); + } +} + void Rasterizer::Teardown() { auto context_switch = surface_ ? surface_->MakeRenderContextCurrent() : nullptr; @@ -97,10 +103,6 @@ void Rasterizer::Teardown() { raster_thread_merger_->UnMergeNowIfLastOne(); raster_thread_merger_->SetMergeUnmergeCallback(nullptr); } - - if (external_view_embedder_) { - external_view_embedder_->Teardown(); - } } void Rasterizer::EnableThreadMergerIfNeeded() { diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h index fb381cc0f9af..8f0959d8390a 100644 --- a/shell/common/rasterizer.h +++ b/shell/common/rasterizer.h @@ -140,6 +140,13 @@ class Rasterizer final : public SnapshotDelegate { /// void Teardown(); + //---------------------------------------------------------------------------- + /// @brief Releases any resource used by the external view embedder. + /// For example, overlay surfaces or Android views. + /// On Android, this method post a task to the platform thread, + /// and waits until it completes. + void TeardownExternalViewEmbedder(); + //---------------------------------------------------------------------------- /// @brief Notifies the rasterizer that there is a low memory situation /// and it must purge as many unnecessary resources as possible. diff --git a/shell/common/shell.cc b/shell/common/shell.cc index ff891ffbcac1..b0a796632760 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -878,6 +878,11 @@ void Shell::OnPlatformViewDestroyed() { fml::TaskRunner::RunNowOrPostTask(task_runners_.GetRasterTaskRunner(), raster_task); latch.Wait(); + // On Android, the external view embedder posts a task to the platform thread, + // and waits until it completes. + // As a result, the platform thread must not be blocked prior to calling + // this method. + rasterizer_->TeardownExternalViewEmbedder(); } // |PlatformView::Delegate| diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc index 0c326eb950b0..84f2ab937812 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -4,6 +4,8 @@ #include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" +#include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/fml/task_runner.h" #include "flutter/fml/trace_event.h" #include "flutter/shell/platform/android/surface/android_surface.h" @@ -12,12 +14,14 @@ namespace flutter { AndroidExternalViewEmbedder::AndroidExternalViewEmbedder( const AndroidContext& android_context, std::shared_ptr jni_facade, - std::shared_ptr surface_factory) + std::shared_ptr surface_factory, + TaskRunners task_runners) : ExternalViewEmbedder(), android_context_(android_context), jni_facade_(jni_facade), surface_factory_(surface_factory), - surface_pool_(std::make_unique()) {} + surface_pool_(std::make_unique()), + task_runners_(task_runners) {} // |ExternalViewEmbedder| void AndroidExternalViewEmbedder::PrerollCompositeEmbeddedView( @@ -264,8 +268,8 @@ void AndroidExternalViewEmbedder::BeginFrame( // The surface size changed. Therefore, destroy existing surfaces as // the existing surfaces in the pool can't be recycled. - if (frame_size_ != frame_size && raster_thread_merger->IsOnPlatformThread()) { - surface_pool_->DestroyLayers(jni_facade_); + if (frame_size_ != frame_size) { + DestroySurfaces(); } surface_pool_->SetFrameSize(frame_size); // JNI method must be called on the platform thread. @@ -300,7 +304,18 @@ bool AndroidExternalViewEmbedder::SupportsDynamicThreadMerging() { // |ExternalViewEmbedder| void AndroidExternalViewEmbedder::Teardown() { - surface_pool_->DestroyLayers(jni_facade_); + DestroySurfaces(); +} + +// |ExternalViewEmbedder| +void AndroidExternalViewEmbedder::DestroySurfaces() { + fml::AutoResetWaitableEvent latch; + fml::TaskRunner::RunNowOrPostTask(task_runners_.GetPlatformTaskRunner(), + [&]() { + surface_pool_->DestroyLayers(jni_facade_); + latch.Signal(); + }); + latch.Wait(); } } // namespace flutter diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.h b/shell/platform/android/external_view_embedder/external_view_embedder.h index 278d7693e766..2ec13297bb9a 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.h +++ b/shell/platform/android/external_view_embedder/external_view_embedder.h @@ -7,6 +7,7 @@ #include +#include "flutter/common/task_runners.h" #include "flutter/flow/embedded_views.h" #include "flutter/flow/rtree.h" #include "flutter/shell/platform/android/context/android_context.h" @@ -32,7 +33,8 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { AndroidExternalViewEmbedder( const AndroidContext& android_context, std::shared_ptr jni_facade, - std::shared_ptr surface_factory); + std::shared_ptr surface_factory, + TaskRunners task_runners); // |ExternalViewEmbedder| void PrerollCompositeEmbeddedView( @@ -99,6 +101,9 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { // Holds surfaces. Allows to recycle surfaces or allocate new ones. const std::unique_ptr surface_pool_; + // The task runners. + const TaskRunners task_runners_; + // The size of the root canvas. SkISize frame_size_; @@ -126,6 +131,11 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { // The number of platform views in the previous frame. int64_t previous_frame_view_count_; + // Destroys the surfaces created from the surface factory. + // This method schedules a task on the platform thread, and waits for + // the task until it completes. + void DestroySurfaces(); + // Resets the state. void Reset(); diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index c5a3c19701a3..57966440d0d0 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#define FML_USED_ON_EMBEDDER + #include #include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" @@ -87,12 +89,24 @@ fml::RefPtr GetThreadMergerFromRasterThread( rasterizer_queue_id); } +TaskRunners GetTaskRunnersForFixture() { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + auto& loop = fml::MessageLoop::GetCurrent(); + return { + "test", + loop.GetTaskRunner(), // platform + loop.GetTaskRunner(), // raster + loop.GetTaskRunner(), // ui + loop.GetTaskRunner() // io + }; +} + TEST(AndroidExternalViewEmbedder, GetCurrentCanvases) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = GetThreadMergerFromPlatformThread(&rasterizer_thread); @@ -117,7 +131,7 @@ TEST(AndroidExternalViewEmbedder, GetCurrentCanvasesCompositeOrder) { auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = GetThreadMergerFromPlatformThread(&rasterizer_thread); @@ -140,7 +154,7 @@ TEST(AndroidExternalViewEmbedder, GetCurrentCanvasesCompositeOrder) { TEST(AndroidExternalViewEmbedder, CompositeEmbeddedView) { auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, nullptr, nullptr); + android_context, nullptr, nullptr, GetTaskRunnersForFixture()); ASSERT_EQ(nullptr, embedder->CompositeEmbeddedView(0)); embedder->PrerollCompositeEmbeddedView( @@ -156,7 +170,7 @@ TEST(AndroidExternalViewEmbedder, CompositeEmbeddedView) { TEST(AndroidExternalViewEmbedder, CancelFrame) { auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, nullptr, nullptr); + android_context, nullptr, nullptr, GetTaskRunnersForFixture()); embedder->PrerollCompositeEmbeddedView( 0, std::make_unique()); @@ -170,7 +184,7 @@ TEST(AndroidExternalViewEmbedder, RasterizerRunsOnPlatformThread) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = @@ -204,7 +218,7 @@ TEST(AndroidExternalViewEmbedder, RasterizerRunsOnRasterizerThread) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = @@ -225,7 +239,7 @@ TEST(AndroidExternalViewEmbedder, PlatformViewRect) { auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = GetThreadMergerFromPlatformThread(&rasterizer_thread); @@ -253,7 +267,7 @@ TEST(AndroidExternalViewEmbedder, PlatformViewRectChangedParams) { auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = GetThreadMergerFromPlatformThread(&rasterizer_thread); @@ -328,7 +342,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { return android_surface_mock; }); auto embedder = std::make_unique( - *android_context, jni_mock, surface_factory); + *android_context, jni_mock, surface_factory, GetTaskRunnersForFixture()); auto raster_thread_merger = GetThreadMergerFromPlatformThread(); @@ -521,7 +535,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrameOverlayComposition) { return android_surface_mock; }); auto embedder = std::make_unique( - *android_context, jni_mock, surface_factory); + *android_context, jni_mock, surface_factory, GetTaskRunnersForFixture()); auto raster_thread_merger = GetThreadMergerFromPlatformThread(); @@ -623,7 +637,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFramePlatformViewWithoutAnyOverlay) { return android_surface_mock; }); auto embedder = std::make_unique( - *android_context, jni_mock, surface_factory); + *android_context, jni_mock, surface_factory, GetTaskRunnersForFixture()); auto raster_thread_merger = GetThreadMergerFromPlatformThread(); @@ -662,7 +676,7 @@ TEST(AndroidExternalViewEmbedder, DoesNotCallJNIPlatformThreadOnlyMethods) { auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); // While on the raster thread, don't make JNI calls as these methods can only // run on the platform thread. @@ -711,7 +725,7 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { }); auto embedder = std::make_unique( - *android_context, jni_mock, surface_factory); + *android_context, jni_mock, surface_factory, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = GetThreadMergerFromPlatformThread(&rasterizer_thread); @@ -798,7 +812,7 @@ TEST(AndroidExternalViewEmbedder, DoesNotDestroyOverlayLayersOnSizeChange) { }); auto embedder = std::make_unique( - *android_context, jni_mock, surface_factory); + *android_context, jni_mock, surface_factory, GetTaskRunnersForFixture()); // ------------------ First frame ------------------ // { @@ -843,9 +857,7 @@ TEST(AndroidExternalViewEmbedder, DoesNotDestroyOverlayLayersOnSizeChange) { embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); } - // Changing the frame size from the raster thread does not make JNI calls. - - EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()).Times(0); + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()).Times(1); EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()).Times(0); fml::Thread platform_thread("platform"); @@ -857,7 +869,7 @@ TEST(AndroidExternalViewEmbedder, SupportsDynamicThreadMerging) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); ASSERT_TRUE(embedder->SupportsDynamicThreadMerging()); } @@ -865,7 +877,7 @@ TEST(AndroidExternalViewEmbedder, DisableThreadMerger) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); auto embedder = std::make_unique( - android_context, jni_mock, nullptr); + android_context, jni_mock, nullptr, GetTaskRunnersForFixture()); fml::Thread platform_thread("platform"); auto raster_thread_merger = GetThreadMergerFromRasterThread(&platform_thread); @@ -921,7 +933,7 @@ TEST(AndroidExternalViewEmbedder, Teardown) { }); auto embedder = std::make_unique( - *android_context, jni_mock, surface_factory); + *android_context, jni_mock, surface_factory, GetTaskRunnersForFixture()); fml::Thread rasterizer_thread("rasterizer"); auto raster_thread_merger = GetThreadMergerFromPlatformThread(&rasterizer_thread); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index aa78142ebb3d..6e858204d9dd 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1193,7 +1193,7 @@ public void detachFromFlutterEngine() { flutterEngine.getPlatformViewsController().detachFromView(); // Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge. - flutterEngine.getPlatformViewsController().detachAccessibiltyBridge(); + flutterEngine.getPlatformViewsController().detachAccessibilityBridge(); // Disconnect and clean up the AccessibilityBridge. accessibilityBridge.release(); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java index 0de5c2651b98..92f034d840d1 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java @@ -68,7 +68,7 @@ default void onFlutterViewDetached() {} */ // Default interface methods are supported on all min SDK versions of Android. @SuppressLint("NewApi") - default void onInputConnectionLocked() {}; + default void onInputConnectionLocked() {} /** * Callback fired when the platform input connection has been unlocked. See also {@link @@ -79,5 +79,5 @@ default void onFlutterViewDetached() {} */ // Default interface methods are supported on all min SDK versions of Android. @SuppressLint("NewApi") - default void onInputConnectionUnlocked() {}; + default void onInputConnectionUnlocked() {} } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java index 503e9c8d661e..ad693a872488 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java @@ -32,5 +32,5 @@ public interface PlatformViewsAccessibilityDelegate { *

Any accessibility events sent by platform views belonging to this delegate will be ignored * until a new accessibility bridge is attached. */ - void detachAccessibiltyBridge(); + void detachAccessibilityBridge(); } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 2e9a9c2e54c2..29ace9e502c2 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -41,8 +41,9 @@ /** * Manages platform views. * - *

Each {@link io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A - * platform views controller can be attached to at most one Flutter view. + *

Each {@link io.flutter.embedding.engine.FlutterEngine} or {@link + * io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A platform views + * controller can be attached to at most one Flutter view. */ public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { private static final String TAG = "PlatformViewsController"; @@ -55,8 +56,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega private Context context; // The View currently rendering the Flutter UI associated with these platform views. - // TODO(egarciad): Investigate if this can be downcasted to `FlutterView`. - private View flutterView; + private FlutterView flutterView; // The texture registry maintaining the textures into which the embedded views will be rendered. @Nullable private TextureRegistry textureRegistry; @@ -111,10 +111,10 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega private boolean synchronizeToNativeViewHierarchy = true; // Overlay layer IDs that were displayed since the start of the current frame. - private HashSet currentFrameUsedOverlayLayerIds; + private final HashSet currentFrameUsedOverlayLayerIds; // Platform view IDs that were displayed since the start of the current frame. - private HashSet currentFrameUsedPlatformViewIds; + private final HashSet currentFrameUsedPlatformViewIds; // Used to acquire the original motion events using the motionEventIds. private final MotionEventTracker motionEventTracker; @@ -122,6 +122,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() { + @TargetApi(Build.VERSION_CODES.KITKAT) @Override public void createAndroidViewForPlatformView( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { @@ -149,6 +150,7 @@ public void createAndroidViewForPlatformView( } final PlatformView platformView = factory.create(context, request.viewId, createParams); + platformView.getView().setLayoutDirection(request.direction); platformViews.put(request.viewId, platformView); } @@ -290,12 +292,9 @@ public void resizePlatformView( vdController.resize( physicalWidth, physicalHeight, - new Runnable() { - @Override - public void run() { - unlockInputConnection(vdController); - onComplete.run(); - } + () -> { + unlockInputConnection(vdController); + onComplete.run(); }); } @@ -331,13 +330,20 @@ public void setDirection(int viewId, int direction) { } ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); - View view = vdControllers.get(viewId).getView(); - if (view == null) { + final PlatformView platformView = platformViews.get(viewId); + if (platformView != null) { + platformView.getView().setLayoutDirection(direction); + return; + } + VirtualDisplayController controller = vdControllers.get(viewId); + if (controller == null) { throw new IllegalStateException( - "Sending touch to an unknown view with id: " + direction); + "Trying to set direction: " + + direction + + " to an unknown platform view with id: " + + viewId); } - - view.setLayoutDirection(direction); + controller.getView().setLayoutDirection(direction); } @Override @@ -483,7 +489,7 @@ public void detach() { * This {@code PlatformViewsController} and its {@code FlutterEngine} is now attached to an * Android {@code View} that renders a Flutter UI. */ - public void attachToView(@NonNull View flutterView) { + public void attachToView(@NonNull FlutterView flutterView) { this.flutterView = flutterView; // Inform all existing platform views that they are now associated with @@ -502,6 +508,7 @@ public void attachToView(@NonNull View flutterView) { */ public void detachFromView() { destroyOverlaySurfaces(); + removeOverlaySurfaces(); this.flutterView = null; flutterViewConvertedToImageView = false; @@ -518,7 +525,7 @@ public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) { } @Override - public void detachAccessibiltyBridge() { + public void detachAccessibilityBridge() { accessibilityEventsDelegate.setAccessibilityBridge(null); } @@ -720,7 +727,7 @@ private void flushAllViews() { private void initializeRootImageViewIfNeeded() { if (synchronizeToNativeViewHierarchy && !flutterViewConvertedToImageView) { - ((FlutterView) flutterView).convertToImageView(); + flutterView.convertToImageView(); flutterViewConvertedToImageView = true; } } @@ -764,7 +771,7 @@ void initializePlatformViewIfNeeded(int viewId) { platformViewParent.put(viewId, parentView); parentView.addView(platformView.getView()); - ((FlutterView) flutterView).addView(parentView); + flutterView.addView(parentView); } public void attachToFlutterRenderer(FlutterRenderer flutterRenderer) { @@ -829,7 +836,7 @@ public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) final FlutterImageView overlayView = overlayLayerViews.get(id); if (overlayView.getParent() == null) { - ((FlutterView) flutterView).addView(overlayView); + flutterView.addView(overlayView); } FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams((int) width, (int) height); @@ -852,14 +859,13 @@ public void onBeginFrame() { *

This member is not intended for public use, and is only visible for testing. */ public void onEndFrame() { - final FlutterView view = (FlutterView) flutterView; // If there are no platform views in the current frame, // then revert the image view surface and use the previous surface. // // Otherwise, acquire the latest image. if (flutterViewConvertedToImageView && currentFrameUsedPlatformViewIds.isEmpty()) { flutterViewConvertedToImageView = false; - view.revertImageView( + flutterView.revertImageView( () -> { // Destroy overlay surfaces once the surface reversion is completed. finishFrame(false); @@ -876,7 +882,7 @@ public void onEndFrame() { // dropped. // For example, a toolbar widget painted by Flutter may not be rendered. final boolean isFrameRenderedUsingImageReaders = - flutterViewConvertedToImageView && view.acquireLatestImageViewFrame(); + flutterViewConvertedToImageView && flutterView.acquireLatestImageViewFrame(); finishFrame(isFrameRenderedUsingImageReaders); } @@ -886,7 +892,7 @@ private void finishFrame(boolean isFrameRenderedUsingImageReaders) { final FlutterImageView overlayView = overlayLayerViews.valueAt(i); if (currentFrameUsedOverlayLayerIds.contains(overlayId)) { - ((FlutterView) flutterView).attachOverlaySurfaceToRender(overlayView); + flutterView.attachOverlaySurfaceToRender(overlayView); final boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage(); isFrameRenderedUsingImageReaders &= didAcquireOverlaySurfaceImage; } else { @@ -965,18 +971,26 @@ public FlutterOverlaySurface createOverlaySurface() { * Destroys the overlay surfaces and removes them from the view hierarchy. * *

This method is used only internally by {@code FlutterJNI}. - * - *

This member is not intended for public use, and is only visible for testing. */ public void destroyOverlaySurfaces() { for (int i = 0; i < overlayLayerViews.size(); i++) { - int overlayId = overlayLayerViews.keyAt(i); - FlutterImageView overlayView = overlayLayerViews.valueAt(i); + final FlutterImageView overlayView = overlayLayerViews.valueAt(i); overlayView.detachFromRenderer(); overlayView.closeImageReader(); - if (flutterView != null) { - ((FlutterView) flutterView).removeView(overlayView); - } + // Don't remove overlayView from the view hierarchy since this method can + // be called while the Android framework is iterating over the array of views. + // See ViewGroup#dispatchDetachedFromWindow(), and + // https://github.com/flutter/flutter/issues/97679. + } + } + + private void removeOverlaySurfaces() { + if (flutterView == null) { + Log.e(TAG, "removeOverlaySurfaces called while flutter view is null"); + return; + } + for (int i = 0; i < overlayLayerViews.size(); i++) { + flutterView.removeView(overlayLayerViews.valueAt(i)); } overlayLayerViews.clear(); } diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index b40ef87e5ccc..a561f73868d8 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -4,7 +4,6 @@ package io.flutter.plugin.platform; -import static android.content.Context.INPUT_METHOD_SERVICE; import static android.content.Context.WINDOW_SERVICE; import static android.view.View.OnFocusChangeListener; @@ -99,7 +98,7 @@ static class PresentationState { // presentation. private FrameLayout container; - private PresentationState state; + private final PresentationState state; private boolean startFocused = false; diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index ec037ee68017..fec53e89a6d9 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -64,7 +64,7 @@ public static VirtualDisplayController create( private final OnFocusChangeListener focusChangeListener; private VirtualDisplay virtualDisplay; @VisibleForTesting SingleViewPresentation presentation; - private Surface surface; + private final Surface surface; private VirtualDisplayController( Context context, diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 42a2857f18f4..62faf47ef6bc 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -137,8 +137,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { @NonNull private final AccessibilityViewEmbedder accessibilityViewEmbedder; // The delegate for interacting with embedded platform views. Used to embed accessibility data for - // an embedded - // view in the accessibility tree. + // an embedded view in the accessibility tree. @NonNull private final PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate; // Android's {@link ContentResolver}, which is used to observe the global @@ -386,11 +385,7 @@ public AccessibilityBridge( @NonNull AccessibilityChannel accessibilityChannel, @NonNull AccessibilityManager accessibilityManager, @NonNull ContentResolver contentResolver, - // This should be @NonNull once the plumbing for - // io.flutter.embedding.engine.android.FlutterView is done. - // TODO(mattcarrol): Add the annotation once the plumbing is done. - // https://github.com/flutter/flutter/issues/29618 - PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { + @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { this( rootAccessibilityView, accessibilityChannel, @@ -407,11 +402,7 @@ public AccessibilityBridge( @NonNull AccessibilityManager accessibilityManager, @NonNull ContentResolver contentResolver, @NonNull AccessibilityViewEmbedder accessibilityViewEmbedder, - // This should be @NonNull once the plumbing for - // io.flutter.embedding.engine.android.FlutterView is done. - // TODO(mattcarrol): Add the annotation once the plumbing is done. - // https://github.com/flutter/flutter/issues/29618 - PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { + @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { this.rootAccessibilityView = rootAccessibilityView; this.accessibilityChannel = accessibilityChannel; this.accessibilityManager = accessibilityManager; @@ -464,13 +455,7 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { this.contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver); } - // platformViewsAccessibilityDelegate should be @NonNull once the plumbing - // for io.flutter.embedding.engine.android.FlutterView is done. - // TODO(mattcarrol): Remove the null check once the plumbing is done. - // https://github.com/flutter/flutter/issues/29618 - if (platformViewsAccessibilityDelegate != null) { - platformViewsAccessibilityDelegate.attachAccessibilityBridge(this); - } + platformViewsAccessibilityDelegate.attachAccessibilityBridge(this); } /** @@ -482,13 +467,7 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { */ public void release() { isReleased = true; - // platformViewsAccessibilityDelegate should be @NonNull once the plumbing - // for io.flutter.embedding.engine.android.FlutterView is done. - // TODO(mattcarrol): Remove the null check once the plumbing is done. - // https://github.com/flutter/flutter/issues/29618 - if (platformViewsAccessibilityDelegate != null) { - platformViewsAccessibilityDelegate.detachAccessibiltyBridge(); - } + platformViewsAccessibilityDelegate.detachAccessibilityBridge(); setOnAccessibilityChangeListener(null); accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index c3d4c696628f..f259c73b4952 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -14,13 +14,12 @@ #include "flutter/shell/platform/android/android_external_texture_gl.h" #include "flutter/shell/platform/android/android_surface_gl.h" #include "flutter/shell/platform/android/android_surface_software.h" -#include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" -#include "flutter/shell/platform/android/surface/android_surface.h" -#include "flutter/shell/platform/android/surface/snapshot_surface_producer.h" - #include "flutter/shell/platform/android/context/android_context.h" +#include "flutter/shell/platform/android/external_view_embedder/external_view_embedder.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" #include "flutter/shell/platform/android/platform_message_response_android.h" +#include "flutter/shell/platform/android/surface/android_surface.h" +#include "flutter/shell/platform/android/surface/snapshot_surface_producer.h" #include "flutter/shell/platform/android/vsync_waiter_android.h" namespace flutter { @@ -255,7 +254,7 @@ std::unique_ptr PlatformViewAndroid::CreateRenderingSurface() { std::shared_ptr PlatformViewAndroid::CreateExternalViewEmbedder() { return std::make_shared( - *android_context_, jni_facade_, surface_factory_); + *android_context_, jni_facade_, surface_factory_, task_runners_); } // |PlatformView| diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 4f6da7c82692..5f1233c48841 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -61,7 +61,7 @@ public class PlatformViewsControllerTest { public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() { // Setup test structure. // Create a fake View that represents the View that renders a Flutter UI. - View fakeFlutterView = new View(RuntimeEnvironment.systemContext); + FlutterView fakeFlutterView = new FlutterView(RuntimeEnvironment.systemContext); // Create fake VirtualDisplayControllers. This requires internal knowledge of // PlatformViewsController. We know that all PlatformViewsController does is @@ -623,17 +623,13 @@ public void detach__destroysOverlaySurfaces() { platformViewsController.detach(); - assertThrows( - IllegalStateException.class, - () -> { - platformViewsController.onDisplayOverlaySurface( - overlaySurface.getId(), /* x=*/ 0, /* y=*/ 0, /* width=*/ 10, /* height=*/ 10); - }); + verify(overlayImageView, times(1)).closeImageReader(); + verify(overlayImageView, times(1)).detachFromRenderer(); } @Test @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) - public void detachFromView__removesOverlaySurfaces() { + public void detachFromView__removesAndDestroysOverlayViews() { final PlatformViewsController platformViewsController = new PlatformViewsController(); final int platformViewId = 0; @@ -650,6 +646,9 @@ public void detachFromView__removesOverlaySurfaces() { jni.attachToNative(); attach(jni, platformViewsController); + final FlutterView flutterView = mock(FlutterView.class); + platformViewsController.attachToView(flutterView); + final FlutterImageView overlayImageView = mock(FlutterImageView.class); when(overlayImageView.acquireLatestImage()).thenReturn(true); @@ -661,17 +660,14 @@ public void detachFromView__removesOverlaySurfaces() { platformViewsController.detachFromView(); - assertThrows( - IllegalStateException.class, - () -> { - platformViewsController.onDisplayOverlaySurface( - overlaySurface.getId(), /* x=*/ 0, /* y=*/ 0, /* width=*/ 10, /* height=*/ 10); - }); + verify(overlayImageView, times(1)).closeImageReader(); + verify(overlayImageView, times(1)).detachFromRenderer(); + verify(flutterView, times(1)).removeView(overlayImageView); } @Test @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) - public void destroyOverlaySurfaces__doesNotThrowIfControllerIsDetached() { + public void destroyOverlaySurfaces__doesNotThrowIfFlutterViewIsDetached() { final PlatformViewsController platformViewsController = new PlatformViewsController(); final int platformViewId = 0; @@ -688,6 +684,9 @@ public void destroyOverlaySurfaces__doesNotThrowIfControllerIsDetached() { jni.attachToNative(); attach(jni, platformViewsController); + final FlutterView flutterView = mock(FlutterView.class); + platformViewsController.attachToView(flutterView); + final FlutterImageView overlayImageView = mock(FlutterImageView.class); when(overlayImageView.acquireLatestImage()).thenReturn(true); @@ -701,6 +700,42 @@ public void destroyOverlaySurfaces__doesNotThrowIfControllerIsDetached() { platformViewsController.destroyOverlaySurfaces(); verify(overlayImageView, times(1)).closeImageReader(); + verify(overlayImageView, times(1)).detachFromRenderer(); + } + + @Test + @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + public void destroyOverlaySurfaces__doesNotRemoveOverlayView() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + + final int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + final PlatformView platformView = mock(PlatformView.class); + when(platformView.getView()).thenReturn(mock(View.class)); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + final FlutterJNI jni = new FlutterJNI(); + jni.attachToNative(); + attach(jni, platformViewsController); + + final FlutterView flutterView = mock(FlutterView.class); + platformViewsController.attachToView(flutterView); + + final FlutterImageView overlayImageView = mock(FlutterImageView.class); + when(overlayImageView.acquireLatestImage()).thenReturn(true); + + final FlutterOverlaySurface overlaySurface = + platformViewsController.createOverlaySurface(overlayImageView); + + platformViewsController.onDisplayOverlaySurface( + overlaySurface.getId(), /* x=*/ 0, /* y=*/ 0, /* width=*/ 10, /* height=*/ 10); + + platformViewsController.destroyOverlaySurfaces(); + verify(flutterView, never()).removeView(overlayImageView); } @Test