diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index d5cfbda751886..386588cae5cd7 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -708,6 +708,7 @@ FILE: ../../../flutter/shell/common/persistent_cache_unittests.cc FILE: ../../../flutter/shell/common/pipeline.cc FILE: ../../../flutter/shell/common/pipeline.h FILE: ../../../flutter/shell/common/pipeline_unittests.cc +FILE: ../../../flutter/shell/common/platform_message_handler.h FILE: ../../../flutter/shell/common/platform_view.cc FILE: ../../../flutter/shell/common/platform_view.h FILE: ../../../flutter/shell/common/pointer_data_dispatcher.cc @@ -842,6 +843,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -939,6 +941,8 @@ FILE: ../../../flutter/shell/platform/android/jni/jni_mock_unittest.cc FILE: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.cc FILE: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.h FILE: ../../../flutter/shell/platform/android/library_loader.cc +FILE: ../../../flutter/shell/platform/android/platform_message_handler_android.cc +FILE: ../../../flutter/shell/platform/android/platform_message_handler_android.h FILE: ../../../flutter/shell/platform/android/platform_message_response_android.cc FILE: ../../../flutter/shell/platform/android/platform_message_response_android.h FILE: ../../../flutter/shell/platform/android/platform_view_android.cc diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index b8d00101df091..0a4ba228c98b5 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -73,6 +73,7 @@ source_set("common") { "engine.h", "pipeline.cc", "pipeline.h", + "platform_message_handler.h", "platform_view.cc", "platform_view.h", "pointer_data_dispatcher.cc", diff --git a/shell/common/platform_message_handler.h b/shell/common/platform_message_handler.h new file mode 100644 index 0000000000000..8e3f77e448236 --- /dev/null +++ b/shell/common/platform_message_handler.h @@ -0,0 +1,39 @@ +// 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. + +#ifndef SHELL_COMMON_PLATFORM_MESSAGE_HANDLER_H_ +#define SHELL_COMMON_PLATFORM_MESSAGE_HANDLER_H_ + +#include + +#include "flutter/lib/ui/window/platform_message.h" + +namespace flutter { + +/// An interface over the ability to handle PlatformMessages that are being sent +/// from Flutter to the host platform. +class PlatformMessageHandler { + public: + virtual ~PlatformMessageHandler() = default; + + /// Ultimately sends the PlatformMessage to the host platform. + /// This method is invoked on the ui thread. + virtual void HandlePlatformMessage( + std::unique_ptr message) = 0; + + /// Performs the return procedure for an associated call to + /// HandlePlatformMessage. + /// This method should be thread-safe and able to be invoked on any thread. + virtual void InvokePlatformMessageResponseCallback( + int response_id, + std::unique_ptr mapping) = 0; + + /// Performs the return procedure for an associated call to + /// HandlePlatformMessage where there is no return value. + /// This method should be thread-safe and able to be invoked on any thread. + virtual void InvokePlatformMessageEmptyResponseCallback(int response_id) = 0; +}; +} // namespace flutter + +#endif // SHELL_COMMON_PLATFORM_MESSAGE_HANDLER_H_ diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index cd3fada30afb4..2752a6e5dab28 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -184,4 +184,9 @@ PlatformView::CreateSnapshotSurfaceProducer() { return nullptr; } +std::shared_ptr +PlatformView::GetPlatformMessageHandler() const { + return nullptr; +} + } // namespace flutter diff --git a/shell/common/platform_view.h b/shell/common/platform_view.h index 85e0a4fe8e6b9..b6555f6c11968 100644 --- a/shell/common/platform_view.h +++ b/shell/common/platform_view.h @@ -22,6 +22,7 @@ #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/pointer_data_packet_converter.h" #include "flutter/lib/ui/window/viewport_metrics.h" +#include "flutter/shell/common/platform_message_handler.h" #include "flutter/shell/common/pointer_data_dispatcher.h" #include "flutter/shell/common/vsync_waiter.h" #include "third_party/skia/include/core/SkSize.h" @@ -810,6 +811,17 @@ class PlatformView { virtual std::unique_ptr CreateSnapshotSurfaceProducer(); + //-------------------------------------------------------------------------- + /// @brief Specifies a delegate that will receive PlatformMessages from + /// Flutter to the host platform. + /// + /// @details If this returns `null` that means PlatformMessages should be sent + /// to the PlatformView. That is to protect legacy behavior, any embedder + /// that wants to support executing Platform Channel handlers on background + /// threads should be returing a thread-safe PlatformMessageHandler instead. + virtual std::shared_ptr GetPlatformMessageHandler() + const; + protected: PlatformView::Delegate& delegate_; const TaskRunners task_runners_; diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 24061c24210d8..df301e717fea9 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -625,6 +625,7 @@ bool Shell::Setup(std::unique_ptr platform_view, } platform_view_ = std::move(platform_view); + platform_message_handler_ = platform_view_->GetPlatformMessageHandler(); engine_ = std::move(engine); rasterizer_ = std::move(rasterizer); io_manager_ = std::move(io_manager); @@ -1192,13 +1193,17 @@ void Shell::OnEngineHandlePlatformMessage( return; } - task_runners_.GetPlatformTaskRunner()->PostTask( - fml::MakeCopyable([view = platform_view_->GetWeakPtr(), - message = std::move(message)]() mutable { - if (view) { - view->HandlePlatformMessage(std::move(message)); - } - })); + if (platform_message_handler_) { + platform_message_handler_->HandlePlatformMessage(std::move(message)); + } else { + task_runners_.GetPlatformTaskRunner()->PostTask( + fml::MakeCopyable([view = platform_view_->GetWeakPtr(), + message = std::move(message)]() mutable { + if (view) { + view->HandlePlatformMessage(std::move(message)); + } + })); + } } void Shell::HandleEngineSkiaMessage(std::unique_ptr message) { @@ -1867,4 +1872,9 @@ fml::TimePoint Shell::GetCurrentTimePoint() { return fml::TimePoint::Now(); } +const std::shared_ptr& +Shell::GetPlatformMessageHandler() const { + return platform_message_handler_; +} + } // namespace flutter diff --git a/shell/common/shell.h b/shell/common/shell.h index 9fbdd5b1a423f..3884299d19294 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -395,6 +395,12 @@ class Shell final : public PlatformView::Delegate, /// @see `CreateCompatibleGenerator` void RegisterImageDecoder(ImageGeneratorFactory factory, int32_t priority); + //---------------------------------------------------------------------------- + /// @brief Returns the delegate object that handles PlatformMessage's from + /// Flutter to the host platform (and its responses). + const std::shared_ptr& GetPlatformMessageHandler() + const; + private: using ServiceProtocolHandler = std::function io_manager_; // on IO task runner std::shared_ptr is_gpu_disabled_sync_switch_; std::shared_ptr volatile_path_tracker_; + std::shared_ptr platform_message_handler_; fml::WeakPtr weak_engine_; // to be shared across threads fml::TaskRunnerAffineWeakPtr diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index b5cc9c8e3ae52..b986d580c080f 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -325,7 +325,8 @@ std::unique_ptr ShellTest::CreateShell( std::shared_ptr shell_test_external_view_embedder, bool is_gpu_disabled, - ShellTestPlatformView::BackendType rendering_backend) { + ShellTestPlatformView::BackendType rendering_backend, + Shell::CreateCallback platform_view_create_callback) { const auto vsync_clock = std::make_shared(); CreateVsyncWaiter create_vsync_waiter = [&]() { @@ -338,21 +339,21 @@ std::unique_ptr ShellTest::CreateShell( } }; - Shell::CreateCallback platfrom_view_create_callback = - [vsync_clock, // - &create_vsync_waiter, // - shell_test_external_view_embedder, // - rendering_backend // - ](Shell& shell) { - return ShellTestPlatformView::Create( - shell, // - shell.GetTaskRunners(), // - vsync_clock, // - std::move(create_vsync_waiter), // - rendering_backend, // - shell_test_external_view_embedder // - ); - }; + if (!platform_view_create_callback) { + platform_view_create_callback = [vsync_clock, // + &create_vsync_waiter, // + shell_test_external_view_embedder, // + rendering_backend // + ](Shell& shell) { + return ShellTestPlatformView::Create(shell, // + shell.GetTaskRunners(), // + vsync_clock, // + std::move(create_vsync_waiter), // + rendering_backend, // + shell_test_external_view_embedder // + ); + }; + } Shell::CreateCallback rasterizer_create_callback = [](Shell& shell) { return std::make_unique(shell); }; @@ -360,7 +361,7 @@ std::unique_ptr ShellTest::CreateShell( return Shell::Create(flutter::PlatformData(), // task_runners, // settings, // - platfrom_view_create_callback, // + platform_view_create_callback, // rasterizer_create_callback, // is_gpu_disabled // ); diff --git a/shell/common/shell_test.h b/shell/common/shell_test.h index 1524c150e377a..4a41060c75ee9 100644 --- a/shell/common/shell_test.h +++ b/shell/common/shell_test.h @@ -43,7 +43,9 @@ class ShellTest : public FixtureTest { shell_test_external_view_embedder = nullptr, bool is_gpu_disabled = false, ShellTestPlatformView::BackendType rendering_backend = - ShellTestPlatformView::BackendType::kDefaultBackend); + ShellTestPlatformView::BackendType::kDefaultBackend, + Shell::CreateCallback platform_view_create_callback = + nullptr); void DestroyShell(std::unique_ptr shell); void DestroyShell(std::unique_ptr shell, TaskRunners task_runners); TaskRunners GetTaskRunnersForFixture(); diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 0288b6088ec7b..b76cc6560e564 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -45,6 +45,10 @@ namespace flutter { namespace testing { + +using ::testing::_; +using ::testing::Return; + namespace { class MockPlatformViewDelegate : public PlatformView::Delegate { MOCK_METHOD1(OnPlatformViewCreated, void(std::unique_ptr surface)); @@ -119,6 +123,27 @@ class MockPlatformView : public PlatformView { MockPlatformView(MockPlatformViewDelegate& delegate, TaskRunners task_runners) : PlatformView(delegate, task_runners) {} MOCK_METHOD0(CreateRenderingSurface, std::unique_ptr()); + MOCK_CONST_METHOD0(GetPlatformMessageHandler, + std::shared_ptr()); +}; + +class MockPlatformMessageHandler : public PlatformMessageHandler { + public: + MOCK_METHOD1(HandlePlatformMessage, + void(std::unique_ptr message)); + MOCK_METHOD2(InvokePlatformMessageResponseCallback, + void(int response_id, std::unique_ptr mapping)); + MOCK_METHOD1(InvokePlatformMessageEmptyResponseCallback, + void(int response_id)); +}; + +class MockPlatformMessageResponse : public PlatformMessageResponse { + public: + static fml::RefPtr Create() { + return fml::AdoptRef(new MockPlatformMessageResponse()); + } + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); }; } // namespace @@ -3162,7 +3187,52 @@ TEST_F(ShellTest, UIWorkAfterOnPlatformViewDestroyed) { shell->GetTaskRunners().GetUITaskRunner(), [&ui_flush_latch]() { ui_flush_latch.Signal(); }); ui_flush_latch.Wait(); + DestroyShell(std::move(shell)); +} +TEST_F(ShellTest, UsesPlatformMessageHandler) { + TaskRunners task_runners = GetTaskRunnersForFixture(); + auto settings = CreateSettingsForFixture(); + MockPlatformViewDelegate platform_view_delegate; + auto platform_message_handler = + std::make_shared(); + int message_id = 1; + EXPECT_CALL(*platform_message_handler, HandlePlatformMessage(_)); + EXPECT_CALL(*platform_message_handler, + InvokePlatformMessageEmptyResponseCallback(message_id)); + Shell::CreateCallback platform_view_create_callback = + [&platform_view_delegate, task_runners, + platform_message_handler](flutter::Shell& shell) { + auto result = std::make_unique(platform_view_delegate, + task_runners); + EXPECT_CALL(*result, GetPlatformMessageHandler()) + .WillOnce(Return(platform_message_handler)); + return result; + }; + auto shell = CreateShell( + /*settings=*/std::move(settings), + /*task_runners=*/task_runners, + /*simulate_vsync=*/false, + /*shell_test_external_view_embedder=*/nullptr, + /*is_gpu_disabled=*/false, + /*rendering_backend=*/ + ShellTestPlatformView::BackendType::kDefaultBackend, + /*platform_view_create_callback=*/platform_view_create_callback); + + EXPECT_EQ(platform_message_handler, shell->GetPlatformMessageHandler()); + PostSync(task_runners.GetUITaskRunner(), [&shell]() { + size_t data_size = 4; + fml::MallocMapping bytes = + fml::MallocMapping(static_cast(malloc(data_size)), data_size); + fml::RefPtr response = + MockPlatformMessageResponse::Create(); + auto message = std::make_unique( + /*channel=*/"foo", /*data=*/std::move(bytes), /*response=*/response); + (static_cast(shell.get())) + ->OnEngineHandlePlatformMessage(std::move(message)); + }); + shell->GetPlatformMessageHandler() + ->InvokePlatformMessageEmptyResponseCallback(message_id); DestroyShell(std::move(shell)); } diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 104b2ab4a2992..5b1c649694f7a 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -79,6 +79,8 @@ source_set("flutter_shell_native_src") { "flutter_main.cc", "flutter_main.h", "library_loader.cc", + "platform_message_handler_android.cc", + "platform_message_handler_android.h", "platform_message_response_android.cc", "platform_message_response_android.h", "platform_view_android.cc", @@ -187,6 +189,7 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/dart/PlatformTaskQueue.java", "io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java", "io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java", "io/flutter/embedding/engine/loader/ApplicationInfoLoader.java", diff --git a/shell/platform/android/android_shell_holder.h b/shell/platform/android/android_shell_holder.h index 9ea264f366722..062e7e45c4cff 100644 --- a/shell/platform/android/android_shell_holder.h +++ b/shell/platform/android/android_shell_holder.h @@ -16,6 +16,7 @@ #include "flutter/shell/common/shell.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" +#include "flutter/shell/platform/android/platform_message_handler_android.h" #include "flutter/shell/platform/android/platform_view_android.h" namespace flutter { @@ -96,6 +97,11 @@ class AndroidShellHolder { void NotifyLowMemoryWarning(); + const std::shared_ptr& GetPlatformMessageHandler() + const { + return shell_->GetPlatformMessageHandler(); + } + private: const flutter::Settings settings_; const std::shared_ptr jni_facade_; diff --git a/shell/platform/android/android_shell_holder_unittests.cc b/shell/platform/android/android_shell_holder_unittests.cc index 2d3bc3ef311e1..85cb352e005f9 100644 --- a/shell/platform/android/android_shell_holder_unittests.cc +++ b/shell/platform/android/android_shell_holder_unittests.cc @@ -7,6 +7,7 @@ namespace flutter { namespace testing { namespace { class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI { + public: MOCK_METHOD2(FlutterViewHandlePlatformMessage, void(std::unique_ptr message, int responseId)); @@ -51,6 +52,15 @@ class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI { MOCK_METHOD0(GetDisplayRefreshRate, double()); MOCK_METHOD1(RequestDartDeferredLibrary, bool(int loading_unit_id)); }; + +class MockPlatformMessageResponse : public PlatformMessageResponse { + public: + static fml::RefPtr Create() { + return fml::AdoptRef(new MockPlatformMessageResponse()); + } + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); +}; } // namespace TEST(AndroidShellHolder, Create) { @@ -65,5 +75,34 @@ TEST(AndroidShellHolder, Create) { nullptr, /*is_fake_window=*/true); holder->GetPlatformView()->NotifyCreated(window); } + +TEST(AndroidShellHolder, HandlePlatformMessage) { + Settings settings; + settings.enable_software_rendering = false; + auto jni = std::make_shared(); + auto holder = std::make_unique(settings, jni); + EXPECT_NE(holder.get(), nullptr); + EXPECT_TRUE(holder->IsValid()); + EXPECT_NE(holder->GetPlatformView().get(), nullptr); + auto window = fml::MakeRefCounted( + nullptr, /*is_fake_window=*/true); + holder->GetPlatformView()->NotifyCreated(window); + EXPECT_TRUE(holder->GetPlatformMessageHandler()); + size_t data_size = 4; + fml::MallocMapping bytes = + fml::MallocMapping(static_cast(malloc(data_size)), data_size); + fml::RefPtr response = + MockPlatformMessageResponse::Create(); + auto message = std::make_unique( + /*channel=*/"foo", /*data=*/std::move(bytes), /*response=*/response); + int response_id = 1; + EXPECT_CALL(*jni, + FlutterViewHandlePlatformMessage(::testing::_, response_id)); + EXPECT_CALL(*response, CompleteEmpty()); + holder->GetPlatformMessageHandler()->HandlePlatformMessage( + std::move(message)); + holder->GetPlatformMessageHandler() + ->InvokePlatformMessageEmptyResponseCallback(response_id); +} } // namespace testing } // namespace flutter diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 9f0648e49741f..ac484f21f4601 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -41,6 +41,7 @@ import java.util.Locale; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Interface between Flutter embedding's Java code and Flutter engine's C/C++ code. @@ -98,6 +99,12 @@ @Keep public class FlutterJNI { private static final String TAG = "FlutterJNI"; + // This serializes the invocation of platform message responses and the + // attachment and detachment of the shell holder. This ensures that we don't + // detach FlutterJNI on the platform thread while a background thread invokes + // a message response. Typically accessing the shell holder happens on the + // platform thread and doesn't require locking. + private ReentrantReadWriteLock shellHolderLock = new ReentrantReadWriteLock(); // BEGIN Methods related to loading for FlutterLoader. /** @@ -304,7 +311,12 @@ public boolean isAttached() { public void attachToNative() { ensureRunningOnMainThread(); ensureNotAttachedToNative(); - nativeShellHolderId = performNativeAttach(this); + shellHolderLock.writeLock().lock(); + try { + nativeShellHolderId = performNativeAttach(this); + } finally { + shellHolderLock.writeLock().unlock(); + } } @VisibleForTesting @@ -368,8 +380,13 @@ private native FlutterJNI nativeSpawn( public void detachFromNativeAndReleaseResources() { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeDestroy(nativeShellHolderId); - nativeShellHolderId = null; + shellHolderLock.writeLock().lock(); + try { + nativeDestroy(nativeShellHolderId); + nativeShellHolderId = null; + } finally { + shellHolderLock.writeLock().unlock(); + } } private native void nativeDestroy(long nativeShellHolderId); @@ -858,14 +875,33 @@ public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformM this.platformMessageHandler = platformMessageHandler; } - // Called by native. + private native void nativeCleanupMessageData(long messageData); + + /** + * Destroys the resources provided sent to `handlePlatformMessage`. + * + *

This can be called on any thread. + * + * @param messageData the argument sent to handlePlatformMessage. + */ + public void cleanupMessageData(long messageData) { + // This doesn't rely on being attached like other methods. + nativeCleanupMessageData(messageData); + } + + // Called by native on the ui thread. // TODO(mattcarroll): determine if message is nonull or nullable @SuppressWarnings("unused") @VisibleForTesting public void handlePlatformMessage( - @NonNull final String channel, ByteBuffer message, final int replyId) { + @NonNull final String channel, + ByteBuffer message, + final int replyId, + final long messageData) { if (platformMessageHandler != null) { - platformMessageHandler.handleMessageFromDart(channel, message, replyId); + platformMessageHandler.handleMessageFromDart(channel, message, replyId, messageData); + } else { + nativeCleanupMessageData(messageData); } // TODO(mattcarroll): log dropped messages when in debug mode // (https://github.com/flutter/flutter/issues/25391) @@ -931,16 +967,20 @@ private native void nativeDispatchPlatformMessage( int responseId); // TODO(mattcarroll): differentiate between channel responses and platform responses. - @UiThread public void invokePlatformMessageEmptyResponseCallback(int responseId) { - ensureRunningOnMainThread(); - if (isAttached()) { - nativeInvokePlatformMessageEmptyResponseCallback(nativeShellHolderId, responseId); - } else { - Log.w( - TAG, - "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " - + responseId); + // Called on any thread. + shellHolderLock.readLock().lock(); + try { + if (isAttached()) { + nativeInvokePlatformMessageEmptyResponseCallback(nativeShellHolderId, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " + + responseId); + } + } finally { + shellHolderLock.readLock().unlock(); } } @@ -949,21 +989,25 @@ private native void nativeInvokePlatformMessageEmptyResponseCallback( long nativeShellHolderId, int responseId); // TODO(mattcarroll): differentiate between channel responses and platform responses. - @UiThread public void invokePlatformMessageResponseCallback( int responseId, @NonNull ByteBuffer message, int position) { - ensureRunningOnMainThread(); + // Called on any thread. if (!message.isDirect()) { throw new IllegalArgumentException("Expected a direct ByteBuffer."); } - if (isAttached()) { - nativeInvokePlatformMessageResponseCallback( - nativeShellHolderId, responseId, message, position); - } else { - Log.w( - TAG, - "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " - + responseId); + shellHolderLock.readLock().lock(); + try { + if (isAttached()) { + nativeInvokePlatformMessageResponseCallback( + nativeShellHolderId, responseId, message, position); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " + + responseId); + } + } finally { + shellHolderLock.readLock().unlock(); } } diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java index 511888112ccf5..5580d7a5cd258 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java @@ -168,6 +168,14 @@ public BinaryMessenger getBinaryMessenger() { } // ------ START BinaryMessenger (Deprecated: use getBinaryMessenger() instead) ----- + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ + @Deprecated + @UiThread + @Override + public TaskQueue makeBackgroundTaskQueue() { + return binaryMessenger.makeBackgroundTaskQueue(); + } + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ @Deprecated @Override @@ -195,6 +203,17 @@ public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { binaryMessenger.setMessageHandler(channel, handler); } + + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ + @Deprecated + @Override + @UiThread + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessenger.BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { + binaryMessenger.setMessageHandler(channel, handler, taskQueue); + } // ------ END BinaryMessenger ----- /** @@ -371,6 +390,10 @@ private DefaultBinaryMessenger(@NonNull DartMessenger messenger) { this.messenger = messenger; } + public TaskQueue makeBackgroundTaskQueue() { + return messenger.makeBackgroundTaskQueue(); + } + /** * Sends the given {@code message} from Android to Dart over the given {@code channel}. * @@ -416,5 +439,14 @@ public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { messenger.setMessageHandler(channel, handler); } + + @Override + @UiThread + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessenger.BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { + messenger.setMessageHandler(channel, handler, taskQueue); + } } } diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java index 2822b4060fbcd..c5faf03b5dd40 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java @@ -13,6 +13,11 @@ import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -26,25 +31,110 @@ class DartMessenger implements BinaryMessenger, PlatformMessageHandler { private static final String TAG = "DartMessenger"; @NonNull private final FlutterJNI flutterJNI; - @NonNull private final Map messageHandlers; + + @NonNull private final ConcurrentHashMap messageHandlers; + @NonNull private final Map pendingReplies; private int nextReplyId = 1; - DartMessenger(@NonNull FlutterJNI flutterJNI) { + @NonNull private final DartMessengerTaskQueue platformTaskQueue = new PlatformTaskQueue(); + + @NonNull private WeakHashMap createdTaskQueues; + + @NonNull private TaskQueueFactory taskQueueFactory; + + DartMessenger(@NonNull FlutterJNI flutterJNI, @NonNull TaskQueueFactory taskQueueFactory) { this.flutterJNI = flutterJNI; - this.messageHandlers = new HashMap<>(); + this.messageHandlers = new ConcurrentHashMap<>(); this.pendingReplies = new HashMap<>(); + this.createdTaskQueues = new WeakHashMap(); + this.taskQueueFactory = taskQueueFactory; + } + + DartMessenger(@NonNull FlutterJNI flutterJNI) { + this(flutterJNI, new DefaultTaskQueueFactory()); + } + + private static class TaskQueueToken implements TaskQueue {} + + interface DartMessengerTaskQueue { + void dispatch(@NonNull Runnable runnable); + } + + interface TaskQueueFactory { + DartMessengerTaskQueue makeBackgroundTaskQueue(); + } + + private static class DefaultTaskQueueFactory implements TaskQueueFactory { + public DartMessengerTaskQueue makeBackgroundTaskQueue() { + return new DefaultTaskQueue(); + } + } + + private static class HandlerInfo { + @NonNull public final BinaryMessenger.BinaryMessageHandler handler; + @Nullable public final DartMessengerTaskQueue taskQueue; + + HandlerInfo( + @NonNull BinaryMessenger.BinaryMessageHandler handler, + @Nullable DartMessengerTaskQueue taskQueue) { + this.handler = handler; + this.taskQueue = taskQueue; + } + } + + private static class DefaultTaskQueue implements DartMessengerTaskQueue { + @NonNull private final ExecutorService executor; + + DefaultTaskQueue() { + // TODO(gaaclarke): Use a shared thread pool with serial queues instead of + // making a thread for each TaskQueue. + ThreadFactory threadFactory = + (Runnable runnable) -> { + return new Thread(runnable, "DartMessenger.DefaultTaskQueue"); + }; + this.executor = Executors.newSingleThreadExecutor(threadFactory); + } + + @Override + public void dispatch(@NonNull Runnable runnable) { + executor.execute(runnable); + } + } + + @Override + public TaskQueue makeBackgroundTaskQueue() { + DartMessengerTaskQueue taskQueue = taskQueueFactory.makeBackgroundTaskQueue(); + TaskQueueToken token = new TaskQueueToken(); + createdTaskQueues.put(token, taskQueue); + return token; } @Override public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { + setMessageHandler(channel, handler, null); + } + + @Override + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessenger.BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { if (handler == null) { Log.v(TAG, "Removing handler for channel '" + channel + "'"); messageHandlers.remove(channel); } else { + DartMessengerTaskQueue dartMessengerTaskQueue = null; + if (taskQueue != null) { + dartMessengerTaskQueue = createdTaskQueues.get(taskQueue); + if (dartMessengerTaskQueue == null) { + throw new IllegalArgumentException( + "Unrecognized TaskQueue, use BinaryMessenger to create your TaskQueue (ex makeBackgroundTaskQueue)."); + } + } Log.v(TAG, "Setting handler for channel '" + channel + "'"); - messageHandlers.put(channel, handler); + messageHandlers.put(channel, new HandlerInfo(handler, dartMessengerTaskQueue)); } } @@ -72,20 +162,13 @@ public void send( } } - @Override - public void handleMessageFromDart( - @NonNull final String channel, @Nullable ByteBuffer message, final int replyId) { - Log.v(TAG, "Received message from Dart over channel '" + channel + "'"); - BinaryMessenger.BinaryMessageHandler handler = messageHandlers.get(channel); - if (handler != null) { + private void invokeHandler( + @Nullable HandlerInfo handlerInfo, @Nullable ByteBuffer message, final int replyId) { + // Called from any thread. + if (handlerInfo != null) { try { Log.v(TAG, "Deferring to registered handler to process message."); - handler.onMessage(message, new Reply(flutterJNI, replyId)); - if (message != null && message.isDirect()) { - // This ensures that if a user retains an instance to the ByteBuffer and it happens to - // be direct they will get a deterministic error. - message.limit(0); - } + handlerInfo.handler.onMessage(message, new Reply(flutterJNI, replyId)); } catch (Exception ex) { Log.e(TAG, "Uncaught exception in binary message listener", ex); flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId); @@ -98,6 +181,37 @@ public void handleMessageFromDart( } } + @Override + public void handleMessageFromDart( + @NonNull final String channel, + @Nullable ByteBuffer message, + final int replyId, + long messageData) { + // Called from the ui thread. + Log.v(TAG, "Received message from Dart over channel '" + channel + "'"); + @Nullable final HandlerInfo handlerInfo = messageHandlers.get(channel); + @Nullable + final DartMessengerTaskQueue taskQueue = (handlerInfo != null) ? handlerInfo.taskQueue : null; + Runnable myRunnable = + () -> { + try { + invokeHandler(handlerInfo, message, replyId); + if (message != null && message.isDirect()) { + // This ensures that if a user retains an instance to the ByteBuffer and it happens to + // be direct they will get a deterministic error. + message.limit(0); + } + } finally { + // This is deleting the data underneath the message object. + flutterJNI.cleanupMessageData(messageData); + } + }; + @NonNull + final DartMessengerTaskQueue nonnullTaskQueue = + taskQueue == null ? platformTaskQueue : taskQueue; + nonnullTaskQueue.dispatch(myRunnable); + } + @Override public void handlePlatformMessageResponse(int replyId, @Nullable ByteBuffer reply) { Log.v(TAG, "Received message reply from Dart."); diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java index 22bb6f195666f..36a7a9d7eb52c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java @@ -11,7 +11,10 @@ /** Handler that receives messages from Dart code. */ public interface PlatformMessageHandler { void handleMessageFromDart( - @NonNull final String channel, @Nullable ByteBuffer message, final int replyId); + @NonNull final String channel, + @Nullable ByteBuffer message, + final int replyId, + long messageData); void handlePlatformMessageResponse(int replyId, @Nullable ByteBuffer reply); } diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java new file mode 100644 index 0000000000000..e90ab89e62223 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java @@ -0,0 +1,19 @@ +// 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. + +package io.flutter.embedding.engine.dart; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; + +/** A BinaryMessenger.TaskQueue that posts to the platform thread (aka main thread). */ +public class PlatformTaskQueue implements DartMessenger.DartMessengerTaskQueue { + @NonNull private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void dispatch(@NonNull Runnable runnable) { + handler.post(runnable); + } +} diff --git a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java index 55eb3109af5bb..b1f06cb80c92f 100644 --- a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java @@ -36,6 +36,7 @@ public final class BasicMessageChannel { @NonNull private final BinaryMessenger messenger; @NonNull private final String name; @NonNull private final MessageCodec codec; + @Nullable private final BinaryMessenger.TaskQueue taskQueue; /** * Creates a new channel associated with the specified {@link BinaryMessenger} and with the @@ -47,6 +48,25 @@ public final class BasicMessageChannel { */ public BasicMessageChannel( @NonNull BinaryMessenger messenger, @NonNull String name, @NonNull MessageCodec codec) { + this(messenger, name, codec, null); + } + + /** + * Creates a new channel associated with the specified {@link BinaryMessenger} and with the + * specified name and {@link MessageCodec}. + * + * @param messenger a {@link BinaryMessenger}. + * @param name a channel name String. + * @param codec a {@link MessageCodec}. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. See also {@link + * BinaryMessenger#makeBackgroundTaskQueue()}. + */ + public BasicMessageChannel( + @NonNull BinaryMessenger messenger, + @NonNull String name, + @NonNull MessageCodec codec, + BinaryMessenger.TaskQueue taskQueue) { if (BuildConfig.DEBUG) { if (messenger == null) { Log.e(TAG, "Parameter messenger must not be null."); @@ -61,6 +81,7 @@ public BasicMessageChannel( this.messenger = messenger; this.name = name; this.codec = codec; + this.taskQueue = taskQueue; } /** @@ -101,7 +122,16 @@ public void send(@Nullable T message, @Nullable final Reply callback) { */ @UiThread public void setMessageHandler(@Nullable final MessageHandler handler) { - messenger.setMessageHandler(name, handler == null ? null : new IncomingMessageHandler(handler)); + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (taskQueue != null) { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMessageHandler(handler), taskQueue); + } else { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMessageHandler(handler)); + } } /** diff --git a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java index fb1a22e6f4d3c..968075f880b31 100644 --- a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java +++ b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java @@ -26,6 +26,27 @@ * @see EventChannel , which supports communication using event streams. */ public interface BinaryMessenger { + /** + * An abstraction over the threading policy used to invoke message handlers. + * + *

These are generated by calling methods like {@link + * BinaryMessenger#makeBackgroundTaskQueue()} and can be passed into platform channels' + * constructors to control the threading policy for handling platform channels' messages. + */ + public interface TaskQueue {} + + /** + * Creates a TaskQueue that executes the tasks serially on a background thread. + * + *

There is no guarantee that the tasks will execute on the same thread, just that execution is + * serial. + */ + @UiThread + default TaskQueue makeBackgroundTaskQueue() { + // TODO(92582): Remove default implementation when it is safe for Google Flutter users. + throw new UnsupportedOperationException("makeBackgroundTaskQueue not implemented."); + } + /** * Sends a binary message to the Flutter application. * @@ -66,6 +87,34 @@ public interface BinaryMessenger { @UiThread void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler); + /** + * Registers a handler to be invoked when the Flutter application sends a message to its host + * platform. + * + *

Registration overwrites any previous registration for the same channel name. Use a null + * handler to deregister. + * + *

If no handler has been registered for a particular channel, any incoming message on that + * channel will be handled silently by sending a null reply. + * + * @param channel the name {@link String} of the channel. + * @param handler a {@link BinaryMessageHandler} to be invoked on incoming messages, or null. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. + */ + @UiThread + default void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { + // TODO(92582): Remove default implementation when it is safe for Google Flutter users. + if (taskQueue != null) { + throw new UnsupportedOperationException( + "setMessageHandler called with nonnull taskQueue is not supported."); + } + setMessageHandler(channel, handler); + } + /** Handler for incoming binary messages from Flutter. */ interface BinaryMessageHandler { /** @@ -97,7 +146,6 @@ interface BinaryReply { * outgoing replies must place the reply bytes between position zero and current position. * Reply receivers can read from the buffer directly. */ - @UiThread void reply(@Nullable ByteBuffer reply); } } diff --git a/shell/platform/android/io/flutter/plugin/common/EventChannel.java b/shell/platform/android/io/flutter/plugin/common/EventChannel.java index 984b251b29d65..f8835f90904c4 100644 --- a/shell/platform/android/io/flutter/plugin/common/EventChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/EventChannel.java @@ -4,6 +4,7 @@ package io.flutter.plugin.common; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import io.flutter.BuildConfig; import io.flutter.Log; @@ -34,6 +35,7 @@ public final class EventChannel { private final BinaryMessenger messenger; private final String name; private final MethodCodec codec; + @Nullable private final BinaryMessenger.TaskQueue taskQueue; /** * Creates a new channel associated with the specified {@link BinaryMessenger} and with the @@ -55,6 +57,25 @@ public EventChannel(BinaryMessenger messenger, String name) { * @param codec a {@link MessageCodec}. */ public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec) { + this(messenger, name, codec, null); + } + + /** + * Creates a new channel associated with the specified {@link BinaryMessenger} and with the + * specified name and {@link MethodCodec}. + * + * @param messenger a {@link BinaryMessenger}. + * @param name a channel name String. + * @param codec a {@link MessageCodec}. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. See also {@link + * BinaryMessenger#makeBackgroundTaskQueue()}. + */ + public EventChannel( + BinaryMessenger messenger, + String name, + MethodCodec codec, + BinaryMessenger.TaskQueue taskQueue) { if (BuildConfig.DEBUG) { if (messenger == null) { Log.e(TAG, "Parameter messenger must not be null."); @@ -69,6 +90,7 @@ public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec) { this.messenger = messenger; this.name = name; this.codec = codec; + this.taskQueue = taskQueue; } /** @@ -83,8 +105,16 @@ public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec) { */ @UiThread public void setStreamHandler(final StreamHandler handler) { - messenger.setMessageHandler( - name, handler == null ? null : new IncomingStreamRequestHandler(handler)); + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (taskQueue != null) { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingStreamRequestHandler(handler), taskQueue); + } else { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingStreamRequestHandler(handler)); + } } /** diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index 901299b943019..3c0149754fccb 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -35,6 +35,7 @@ public class MethodChannel { private final BinaryMessenger messenger; private final String name; private final MethodCodec codec; + private final BinaryMessenger.TaskQueue taskQueue; /** * Creates a new channel associated with the specified {@link BinaryMessenger} and with the @@ -56,6 +57,25 @@ public MethodChannel(BinaryMessenger messenger, String name) { * @param codec a {@link MessageCodec}. */ public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec) { + this(messenger, name, codec, null); + } + + /** + * Creates a new channel associated with the specified {@link BinaryMessenger} and with the + * specified name and {@link MethodCodec}. + * + * @param messenger a {@link BinaryMessenger}. + * @param name a channel name String. + * @param codec a {@link MessageCodec}. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. See also {@link + * BinaryMessenger#makeBackgroundTaskQueue()}. + */ + public MethodChannel( + BinaryMessenger messenger, + String name, + MethodCodec codec, + @Nullable BinaryMessenger.TaskQueue taskQueue) { if (BuildConfig.DEBUG) { if (messenger == null) { Log.e(TAG, "Parameter messenger must not be null."); @@ -70,6 +90,7 @@ public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec) this.messenger = messenger; this.name = name; this.codec = codec; + this.taskQueue = taskQueue; } /** @@ -116,8 +137,16 @@ public void invokeMethod(String method, @Nullable Object arguments, @Nullable Re */ @UiThread public void setMethodCallHandler(final @Nullable MethodCallHandler handler) { - messenger.setMessageHandler( - name, handler == null ? null : new IncomingMethodCallHandler(handler)); + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (taskQueue != null) { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMethodCallHandler(handler), taskQueue); + } else { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMethodCallHandler(handler)); + } } /** diff --git a/shell/platform/android/io/flutter/view/FlutterNativeView.java b/shell/platform/android/io/flutter/view/FlutterNativeView.java index 63005dc8bb534..c44a23c9982bf 100644 --- a/shell/platform/android/io/flutter/view/FlutterNativeView.java +++ b/shell/platform/android/io/flutter/view/FlutterNativeView.java @@ -124,6 +124,12 @@ public static String getObservatoryUri() { return FlutterJNI.getObservatoryUri(); } + @Override + @UiThread + public TaskQueue makeBackgroundTaskQueue() { + return dartExecutor.getBinaryMessenger().makeBackgroundTaskQueue(); + } + @Override @UiThread public void send(String channel, ByteBuffer message) { @@ -147,6 +153,12 @@ public void setMessageHandler(String channel, BinaryMessageHandler handler) { dartExecutor.getBinaryMessenger().setMessageHandler(channel, handler); } + @Override + @UiThread + public void setMessageHandler(String channel, BinaryMessageHandler handler, TaskQueue taskQueue) { + dartExecutor.getBinaryMessenger().setMessageHandler(channel, handler, taskQueue); + } + /*package*/ FlutterJNI getFlutterJNI() { return mFlutterJNI; } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 4876f1b67c04c..317ec6562a1ca 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -822,6 +822,12 @@ public PointerIcon getSystemPointerIcon(int type) { return PointerIcon.getSystemIcon(getContext(), type); } + @Override + @UiThread + public TaskQueue makeBackgroundTaskQueue() { + return null; + } + @Override @UiThread public void send(String channel, ByteBuffer message) { @@ -844,6 +850,12 @@ public void setMessageHandler(String channel, BinaryMessageHandler handler) { mNativeView.setMessageHandler(channel, handler); } + @Override + @UiThread + public void setMessageHandler(String channel, BinaryMessageHandler handler, TaskQueue taskQueue) { + mNativeView.setMessageHandler(channel, handler, taskQueue); + } + /** Listener will be called on the Android UI thread once when Flutter renders the first frame. */ public interface FirstFrameListener { void onFirstFrame(); diff --git a/shell/platform/android/platform_message_handler_android.cc b/shell/platform/android/platform_message_handler_android.cc new file mode 100644 index 0000000000000..2179611a2f90e --- /dev/null +++ b/shell/platform/android/platform_message_handler_android.cc @@ -0,0 +1,64 @@ + +#include "flutter/shell/platform/android/platform_message_handler_android.h" + +namespace flutter { + +PlatformMessageHandlerAndroid::PlatformMessageHandlerAndroid( + const std::shared_ptr& jni_facade) + : jni_facade_(jni_facade) {} + +void PlatformMessageHandlerAndroid::InvokePlatformMessageResponseCallback( + int response_id, + std::unique_ptr mapping) { + // Called from any thread. + if (!response_id) { + return; + } + // TODO(gaaclarke): Move the jump to the ui thread here from + // PlatformMessageResponseDart so we won't need to use a mutex anymore. + fml::RefPtr message_response; + { + std::lock_guard lock(pending_responses_mutex_); + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) + return; + message_response = std::move(it->second); + pending_responses_.erase(it); + } + + message_response->Complete(std::move(mapping)); +} + +void PlatformMessageHandlerAndroid::InvokePlatformMessageEmptyResponseCallback( + int response_id) { + // Called from any thread. + if (!response_id) { + return; + } + fml::RefPtr message_response; + { + std::lock_guard lock(pending_responses_mutex_); + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) + return; + message_response = std::move(it->second); + pending_responses_.erase(it); + } + message_response->CompleteEmpty(); +} + +// |PlatformView| +void PlatformMessageHandlerAndroid::HandlePlatformMessage( + std::unique_ptr message) { + // Called from the ui thread. + int response_id = next_response_id_++; + if (auto response = message->response()) { + std::lock_guard lock(pending_responses_mutex_); + pending_responses_[response_id] = response; + } + // This call can re-enter in InvokePlatformMessageXxxResponseCallback. + jni_facade_->FlutterViewHandlePlatformMessage(std::move(message), + response_id); +} + +} // namespace flutter diff --git a/shell/platform/android/platform_message_handler_android.h b/shell/platform/android/platform_message_handler_android.h new file mode 100644 index 0000000000000..dc36013db69c1 --- /dev/null +++ b/shell/platform/android/platform_message_handler_android.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef SHELL_PLATFORM_ANDROID_PLATFORM_MESSAGE_HANDLER_H_ +#define SHELL_PLATFORM_ANDROID_PLATFORM_MESSAGE_HANDLER_H_ + +#include +#include +#include +#include + +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/shell/common/platform_message_handler.h" +#include "flutter/shell/platform/android/jni/platform_view_android_jni.h" + +namespace flutter { +class PlatformMessageHandlerAndroid : public PlatformMessageHandler { + public: + PlatformMessageHandlerAndroid( + const std::shared_ptr& jni_facade); + void HandlePlatformMessage(std::unique_ptr message) override; + void InvokePlatformMessageResponseCallback( + int response_id, + std::unique_ptr mapping) override; + + void InvokePlatformMessageEmptyResponseCallback(int response_id) override; + + private: + const std::shared_ptr jni_facade_; + int next_response_id_ = 1; + std::unordered_map> + pending_responses_; + std::mutex pending_responses_mutex_; +}; +} // namespace flutter + +#endif diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 50b9d55c6ca4a..064bbf92ae668 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -73,7 +73,8 @@ PlatformViewAndroid::PlatformViewAndroid( : PlatformView(delegate, std::move(task_runners)), jni_facade_(jni_facade), android_context_(std::move(android_context)), - platform_view_android_delegate_(jni_facade) { + platform_view_android_delegate_(jni_facade), + platform_message_handler_(new PlatformMessageHandlerAndroid(jni_facade)) { // TODO(dnfield): always create a pbuffer surface for background use to // resolve https://github.com/flutter/flutter/issues/73675 if (android_context_) { @@ -190,51 +191,11 @@ void PlatformViewAndroid::DispatchEmptyPlatformMessage(JNIEnv* env, std::move(response))); } -void PlatformViewAndroid::InvokePlatformMessageResponseCallback( - JNIEnv* env, - jint response_id, - jobject java_response_data, - jint java_response_position) { - if (!response_id) - return; - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) - return; - uint8_t* response_data = - static_cast(env->GetDirectBufferAddress(java_response_data)); - FML_DCHECK(response_data != nullptr); - std::vector response = std::vector( - response_data, response_data + java_response_position); - auto message_response = std::move(it->second); - pending_responses_.erase(it); - message_response->Complete( - std::make_unique(std::move(response))); -} - -void PlatformViewAndroid::InvokePlatformMessageEmptyResponseCallback( - JNIEnv* env, - jint response_id) { - if (!response_id) - return; - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) - return; - auto message_response = std::move(it->second); - pending_responses_.erase(it); - message_response->CompleteEmpty(); -} - // |PlatformView| void PlatformViewAndroid::HandlePlatformMessage( std::unique_ptr message) { - int response_id = next_response_id_++; - if (auto response = message->response()) { - pending_responses_[response_id] = response; - } - // This call can re-enter in InvokePlatformMessageXxxResponseCallback. - jni_facade_->FlutterViewHandlePlatformMessage(std::move(message), - response_id); - message = nullptr; + // Called from the ui thread. + platform_message_handler_->HandlePlatformMessage(std::move(message)); } // |PlatformView| diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index 1bd959ef855dd..0ff654f0fba72 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -17,6 +17,7 @@ #include "flutter/shell/common/snapshot_surface_producer.h" #include "flutter/shell/platform/android/context/android_context.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" +#include "flutter/shell/platform/android/platform_message_handler_android.h" #include "flutter/shell/platform/android/platform_view_android_delegate/platform_view_android_delegate.h" #include "flutter/shell/platform/android/surface/android_native_window.h" #include "flutter/shell/platform/android/surface/android_surface.h" @@ -79,14 +80,6 @@ class PlatformViewAndroid final : public PlatformView { std::string name, jint response_id); - void InvokePlatformMessageResponseCallback(JNIEnv* env, - jint response_id, - jobject java_response_data, - jint java_response_position); - - void InvokePlatformMessageEmptyResponseCallback(JNIEnv* env, - jint response_id); - void DispatchSemanticsAction(JNIEnv* env, jint id, jint action, @@ -116,6 +109,11 @@ class PlatformViewAndroid final : public PlatformView { return android_context_; } + std::shared_ptr GetPlatformMessageHandler() + const override { + return platform_message_handler_; + } + private: const std::shared_ptr jni_facade_; std::shared_ptr android_context_; @@ -124,11 +122,7 @@ class PlatformViewAndroid final : public PlatformView { PlatformViewAndroidDelegate platform_view_android_delegate_; std::unique_ptr android_surface_; - - // We use id 0 to mean that no response is expected. - int next_response_id_ = 1; - std::unordered_map> - pending_responses_; + std::shared_ptr platform_message_handler_; // |PlatformView| void UpdateSemantics( diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 376ac155516d0..8b8494b3d3a6d 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -405,6 +405,13 @@ static void DispatchEmptyPlatformMessage(JNIEnv* env, ); } +static void CleanupMessageData(JNIEnv* env, + jobject jcaller, + jlong message_data) { + // Called from any thread. + free(reinterpret_cast(message_data)); +} + static void DispatchPointerDataPacket(JNIEnv* env, jobject jcaller, jlong shell_holder, @@ -483,22 +490,21 @@ static void InvokePlatformMessageResponseCallback(JNIEnv* env, jint responseId, jobject message, jint position) { - ANDROID_SHELL_HOLDER->GetPlatformView() - ->InvokePlatformMessageResponseCallback(env, // - responseId, // - message, // - position // - ); + uint8_t* response_data = + static_cast(env->GetDirectBufferAddress(message)); + FML_DCHECK(response_data != nullptr); + auto mapping = std::make_unique( + fml::MallocMapping::Copy(response_data, response_data + position)); + ANDROID_SHELL_HOLDER->GetPlatformMessageHandler() + ->InvokePlatformMessageResponseCallback(responseId, std::move(mapping)); } static void InvokePlatformMessageEmptyResponseCallback(JNIEnv* env, jobject jcaller, jlong shell_holder, jint responseId) { - ANDROID_SHELL_HOLDER->GetPlatformView() - ->InvokePlatformMessageEmptyResponseCallback(env, // - responseId // - ); + ANDROID_SHELL_HOLDER->GetPlatformMessageHandler() + ->InvokePlatformMessageEmptyResponseCallback(responseId); } static void NotifyLowMemoryWarning(JNIEnv* env, @@ -638,6 +644,11 @@ bool RegisterApi(JNIEnv* env) { .signature = "(JLjava/lang/String;I)V", .fnPtr = reinterpret_cast(&DispatchEmptyPlatformMessage), }, + { + .name = "nativeCleanupMessageData", + .signature = "(J)V", + .fnPtr = reinterpret_cast(&CleanupMessageData), + }, { .name = "nativeDispatchPlatformMessage", .signature = "(JLjava/lang/String;Ljava/nio/ByteBuffer;II)V", @@ -819,7 +830,7 @@ bool RegisterApi(JNIEnv* env) { g_handle_platform_message_method = env->GetMethodID(g_flutter_jni_class->obj(), "handlePlatformMessage", - "(Ljava/lang/String;Ljava/nio/ByteBuffer;I)V"); + "(Ljava/lang/String;Ljava/nio/ByteBuffer;IJ)V"); if (g_handle_platform_message_method == nullptr) { FML_LOG(ERROR) << "Could not locate handlePlatformMessage method"; @@ -1102,6 +1113,7 @@ PlatformViewAndroidJNIImpl::~PlatformViewAndroidJNIImpl() = default; void PlatformViewAndroidJNIImpl::FlutterViewHandlePlatformMessage( std::unique_ptr message, int responseId) { + // Called from the ui thread. JNIEnv* env = fml::jni::AttachCurrentThread(); auto java_object = java_object_.get(env); @@ -1117,11 +1129,14 @@ void PlatformViewAndroidJNIImpl::FlutterViewHandlePlatformMessage( env, env->NewDirectByteBuffer( const_cast(message->data().GetMapping()), message->data().GetSize())); + // Message data is deleted in CleanupMessageData. + fml::MallocMapping mapping = message->releaseData(); env->CallVoidMethod(java_object.obj(), g_handle_platform_message_method, - java_channel.obj(), message_array.obj(), responseId); + java_channel.obj(), message_array.obj(), responseId, + mapping.Release()); } else { env->CallVoidMethod(java_object.obj(), g_handle_platform_message_method, - java_channel.obj(), nullptr, responseId); + java_channel.obj(), nullptr, responseId, nullptr); } FML_CHECK(fml::jni::CheckException(env)); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java index 5e1922103afbf..4e7a6fdb7c2e1 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.verify; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartMessenger.DartMessengerTaskQueue; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import java.nio.ByteBuffer; @@ -22,6 +23,8 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class DartMessengerTest { + SynchronousTaskQueue synchronousTaskQueue = new SynchronousTaskQueue(); + private static class ReportingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { public Throwable latestException; @@ -32,6 +35,12 @@ public void uncaughtException(Thread t, Throwable e) { } } + private static class SynchronousTaskQueue implements DartMessengerTaskQueue { + public void dispatch(Runnable runnable) { + runnable.run(); + } + } + @Test public void itHandlesErrors() { // Setup test. @@ -44,14 +53,14 @@ public void itHandlesErrors() { currentThread.setUncaughtExceptionHandler(reportingHandler); // Create object under test. - final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final DartMessenger messenger = new DartMessenger(fakeFlutterJni, () -> synchronousTaskQueue); final BinaryMessageHandler throwingHandler = mock(BinaryMessageHandler.class); Mockito.doThrow(AssertionError.class) .when(throwingHandler) .onMessage(any(ByteBuffer.class), any(DartMessenger.Reply.class)); - - messenger.setMessageHandler("test", throwingHandler); - messenger.handleMessageFromDart("test", ByteBuffer.allocate(0), 0); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + messenger.setMessageHandler("test", throwingHandler, taskQueue); + messenger.handleMessageFromDart("test", ByteBuffer.allocate(0), 0, 0); assertNotNull(reportingHandler.latestException); assertTrue(reportingHandler.latestException instanceof AssertionError); currentThread.setUncaughtExceptionHandler(savedHandler); @@ -61,21 +70,22 @@ public void itHandlesErrors() { public void givesDirectByteBuffer() { // Setup test. final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); - final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final DartMessenger messenger = new DartMessenger(fakeFlutterJni, () -> synchronousTaskQueue); final String channel = "foobar"; final boolean[] wasDirect = {false}; final BinaryMessenger.BinaryMessageHandler handler = (message, reply) -> { wasDirect[0] = message.isDirect(); }; - messenger.setMessageHandler(channel, handler); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + messenger.setMessageHandler(channel, handler, taskQueue); final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); message.rewind(); message.putChar('a'); message.putChar('b'); message.putChar('c'); message.putChar('d'); - messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123); + messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123, 0); assertTrue(wasDirect[0]); } @@ -83,7 +93,7 @@ public void givesDirectByteBuffer() { public void directByteBufferLimitZeroAfterUsage() { // Setup test. final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); - final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final DartMessenger messenger = new DartMessenger(fakeFlutterJni, () -> synchronousTaskQueue); final String channel = "foobar"; final ByteBuffer[] byteBuffers = {null}; final int bufferSize = 4 * 2; @@ -92,14 +102,15 @@ public void directByteBufferLimitZeroAfterUsage() { byteBuffers[0] = message; assertEquals(bufferSize, byteBuffers[0].limit()); }; - messenger.setMessageHandler(channel, handler); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + messenger.setMessageHandler(channel, handler, taskQueue); final ByteBuffer message = ByteBuffer.allocateDirect(bufferSize); message.rewind(); message.putChar('a'); message.putChar('b'); message.putChar('c'); message.putChar('d'); - messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123); + messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123, 0); assertNotNull(byteBuffers[0]); assertTrue(byteBuffers[0].isDirect()); assertEquals(0, byteBuffers[0].limit()); @@ -139,4 +150,40 @@ public void replyIdIncrementsOnNullReply() { messenger.send(channel, null, null); verify(fakeFlutterJni, times(1)).dispatchEmptyPlatformMessage(eq("foobar"), eq(2)); } + + @Test + public void cleansUpMessageData() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = new DartMessenger(fakeFlutterJni, () -> synchronousTaskQueue); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + String channel = "foobar"; + BinaryMessenger.BinaryMessageHandler handler = + (ByteBuffer message, BinaryMessenger.BinaryReply reply) -> { + reply.reply(null); + }; + messenger.setMessageHandler(channel, handler, taskQueue); + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + int replyId = 1; + long messageData = 1234; + messenger.handleMessageFromDart(channel, message, replyId, messageData); + verify(fakeFlutterJni).cleanupMessageData(eq(messageData)); + } + + @Test + public void cleansUpMessageDataOnError() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = new DartMessenger(fakeFlutterJni, () -> synchronousTaskQueue); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + String channel = "foobar"; + BinaryMessenger.BinaryMessageHandler handler = + (ByteBuffer message, BinaryMessenger.BinaryReply reply) -> { + throw new RuntimeException("hello"); + }; + messenger.setMessageHandler(channel, handler, taskQueue); + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + int replyId = 1; + long messageData = 1234; + messenger.handleMessageFromDart(channel, message, replyId, messageData); + verify(fakeFlutterJni).cleanupMessageData(eq(messageData)); + } } 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 21c392879d688..38c919cae33dc 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -218,7 +218,7 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void getPlatformViewById__hybridComposition() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -246,7 +246,7 @@ public void getPlatformViewById__hybridComposition() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__initializesAndroidView() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -268,7 +268,7 @@ public void createPlatformViewMessage__initializesAndroidView() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__throwsIfViewIsNull() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -296,7 +296,7 @@ public void createPlatformViewMessage__throwsIfViewIsNull() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void onDetachedFromJNI_clearsPlatformViewContext() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -326,7 +326,7 @@ public void onDetachedFromJNI_clearsPlatformViewContext() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void onPreEngineRestart_clearsPlatformViewContext() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -356,7 +356,7 @@ public void onPreEngineRestart_clearsPlatformViewContext() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__throwsIfViewHasParent() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -386,7 +386,7 @@ public void createPlatformViewMessage__throwsIfViewHasParent() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void disposeAndroidView__hybridComposition() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -427,7 +427,12 @@ public void disposeAndroidView__hybridComposition() { } @Test - @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + @Config( + shadows = { + ShadowFlutterSurfaceView.class, + ShadowFlutterJNI.class, + ShadowPlatformTaskQueue.class + }) public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -525,7 +530,12 @@ public void onEndFrame__removesPlatformView() { } @Test - @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + @Config( + shadows = { + ShadowFlutterSurfaceView.class, + ShadowFlutterJNI.class, + ShadowPlatformTaskQueue.class + }) public void onEndFrame__removesPlatformViewParent() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -563,7 +573,12 @@ public void onEndFrame__removesPlatformViewParent() { } @Test - @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + @Config( + shadows = { + ShadowFlutterSurfaceView.class, + ShadowFlutterJNI.class, + ShadowPlatformTaskQueue.class + }) public void detach__destroysOverlaySurfaces() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -695,7 +710,7 @@ public void checkInputConnectionProxy__falseIfViewIsNull() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void convertPlatformViewRenderSurfaceAsDefault() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -741,7 +756,7 @@ public void convertPlatformViewRenderSurfaceAsDefault() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void dontConverRenderSurfaceWhenFlagIsTrue() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -812,7 +827,10 @@ private static void createPlatformView( new MethodCall("create", platformViewCreateArguments); jni.handlePlatformMessage( - "flutter/platform_views", encodeMethodCall(platformCreateMethodCall), /*replyId=*/ 0); + "flutter/platform_views", + encodeMethodCall(platformCreateMethodCall), + /*replyId=*/ 0, + /*messageData=*/ 0); } private static void disposePlatformView( @@ -826,7 +844,10 @@ private static void disposePlatformView( new MethodCall("dispose", platformViewDisposeArguments); jni.handlePlatformMessage( - "flutter/platform_views", encodeMethodCall(platformDisposeMethodCall), /*replyId=*/ 0); + "flutter/platform_views", + encodeMethodCall(platformDisposeMethodCall), + /*replyId=*/ 0, + /*messageData=*/ 0); } private static void synchronizeToNativeViewHierarchy( @@ -835,7 +856,10 @@ private static void synchronizeToNativeViewHierarchy( final MethodCall convertMethodCall = new MethodCall("synchronizeToNativeViewHierarchy", yes); jni.handlePlatformMessage( - "flutter/platform_views", encodeMethodCall(convertMethodCall), /*replyId=*/ 0); + "flutter/platform_views", + encodeMethodCall(convertMethodCall), + /*replyId=*/ 0, + /*messageData=*/ 0); } private static FlutterView attach( @@ -901,6 +925,20 @@ public FlutterImageView createImageView() { return view; } + /** + * For convenience when writing tests, this allows us to make fake messages from Flutter via + * Platform Channels. Typically those calls happen on the ui thread which dispatches to the + * platform thread. Since tests run on the platform thread it makes it difficult to test without + * this, but isn't technically required. + */ + @Implements(io.flutter.embedding.engine.dart.PlatformTaskQueue.class) + public static class ShadowPlatformTaskQueue { + @Implementation + public void dispatch(Runnable runnable) { + runnable.run(); + } + } + @Implements(FlutterJNI.class) public static class ShadowFlutterJNI { private static SparseArray replies = new SparseArray<>();