diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 7185fc4718bdf..5bcc7614d181b 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -3094,6 +3094,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE @@ -3136,6 +3137,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platf ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java + ../../../flutter/LICENSE @@ -5868,6 +5870,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java @@ -5915,6 +5918,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java diff --git a/lib/ui/fixtures/ui_test.dart b/lib/ui/fixtures/ui_test.dart index a4b8ada735b2c..38866fb476ced 100644 --- a/lib/ui/fixtures/ui_test.dart +++ b/lib/ui/fixtures/ui_test.dart @@ -1100,45 +1100,3 @@ external void _callHook( Object? arg20, Object? arg21, ]); - -Scene _createRedBoxScene(Size size) { - final SceneBuilder builder = SceneBuilder(); - builder.pushOffset(0.0, 0.0); - final Paint paint = Paint() - ..color = Color.fromARGB(255, 255, 0, 0) - ..style = PaintingStyle.fill; - final PictureRecorder baseRecorder = PictureRecorder(); - final Canvas canvas = Canvas(baseRecorder); - canvas.drawRect(Rect.fromLTRB(0.0, 0.0, size.width, size.height), paint); - final Picture picture = baseRecorder.endRecording(); - builder.addPicture(Offset(0.0, 0.0), picture); - builder.pop(); - return builder.build(); -} - -@pragma('vm:entry-point') -void incorrectImmediateRender() { - PlatformDispatcher.instance.views.first.render(_createRedBoxScene(Size(2, 2))); - _finish(); - // Don't schedule a frame here. This test only checks if the - // [FlutterView.render] call is propagated to PlatformConfiguration.render - // and thus doesn't need anything from `Animator` or `Engine`, which, - // besides, are not even created in the native side at all. -} - -@pragma('vm:entry-point') -void incorrectDoubleRender() { - PlatformDispatcher.instance.onBeginFrame = (Duration value) { - PlatformDispatcher.instance.views.first.render(_createRedBoxScene(Size(2, 2))); - PlatformDispatcher.instance.views.first.render(_createRedBoxScene(Size(3, 3))); - }; - PlatformDispatcher.instance.onDrawFrame = () { - PlatformDispatcher.instance.views.first.render(_createRedBoxScene(Size(4, 4))); - PlatformDispatcher.instance.views.first.render(_createRedBoxScene(Size(5, 5))); - }; - _finish(); - // Don't schedule a frame here. This test only checks if the - // [FlutterView.render] call is propagated to PlatformConfiguration.render - // and thus doesn't need anything from `Animator` or `Engine`, which, - // besides, are not even created in the native side at all. -} diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index 5f42aa4af9f26..0bd387270ba13 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -308,21 +308,6 @@ class PlatformDispatcher { _invoke(onMetricsChanged, _onMetricsChangedZone); } - // [FlutterView]s for which [FlutterView.render] has already been called - // during the current [onBeginFrame]/[onDrawFrame] callback sequence. - // - // The field is null outside the scope of those callbacks indicating that - // calls to [FlutterView.render] must be ignored. Furthermore, if a given - // [FlutterView] is already present in this set when its [FlutterView.render] - // is called again, that call must be ignored as a duplicate. - // - // Between [onBeginFrame] and [onDrawFrame] the properties value is - // temporarily stored in `_renderedViewsBetweenCallbacks` so that it survives - // the gap between the two callbacks. - Set? _renderedViews; - // Temporary storage of the `_renderedViews` value between `_beginFrame` and - // `_drawFrame`. - Set? _renderedViewsBetweenCallbacks; /// A callback invoked when any view begins a frame. /// @@ -344,20 +329,11 @@ class PlatformDispatcher { // Called from the engine, via hooks.dart void _beginFrame(int microseconds) { - assert(_renderedViews == null); - assert(_renderedViewsBetweenCallbacks == null); - - _renderedViews = {}; _invoke1( onBeginFrame, _onBeginFrameZone, Duration(microseconds: microseconds), ); - _renderedViewsBetweenCallbacks = _renderedViews; - _renderedViews = null; - - assert(_renderedViews == null); - assert(_renderedViewsBetweenCallbacks != null); } /// A callback that is invoked for each frame after [onBeginFrame] has @@ -375,16 +351,7 @@ class PlatformDispatcher { // Called from the engine, via hooks.dart void _drawFrame() { - assert(_renderedViews == null); - assert(_renderedViewsBetweenCallbacks != null); - - _renderedViews = _renderedViewsBetweenCallbacks; - _renderedViewsBetweenCallbacks = null; _invoke(onDrawFrame, _onDrawFrameZone); - _renderedViews = null; - - assert(_renderedViews == null); - assert(_renderedViewsBetweenCallbacks == null); } /// A callback that is invoked when pointer data is available. diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 286e1485b3a9f..26a258cfa96c1 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -327,21 +327,14 @@ class FlutterView { /// Updates the view's rendering on the GPU with the newly provided [Scene]. /// - /// ## Requirement for calling this method - /// - /// This method must be called within the synchronous scope of the + /// This function must be called within the scope of the /// [PlatformDispatcher.onBeginFrame] or [PlatformDispatcher.onDrawFrame] - /// callbacks. Calls out of this scope will be ignored. To use this method, - /// create a callback that calls this method instead, and assign it to either - /// of the fields above; then schedule a frame, which is done typically with - /// [PlatformDispatcher.scheduleFrame]. Also, make sure the callback does not - /// have `await` before the `FlutterWindow.render` call. - /// - /// Additionally, this method can only be called once for each view during a - /// single [PlatformDispatcher.onBeginFrame]/[PlatformDispatcher.onDrawFrame] - /// callback sequence. Duplicate calls will be ignored in production. + /// callbacks being invoked. /// - /// ## How to record a scene + /// If this function is called a second time during a single + /// [PlatformDispatcher.onBeginFrame]/[PlatformDispatcher.onDrawFrame] + /// callback sequence or called outside the scope of those callbacks, the call + /// will be ignored. /// /// To record graphical operations, first create a [PictureRecorder], then /// construct a [Canvas], passing that [PictureRecorder] to its constructor. @@ -360,14 +353,7 @@ class FlutterView { /// scheduling of frames. /// * [RendererBinding], the Flutter framework class which manages layout and /// painting. - void render(Scene scene) { - if (platformDispatcher._renderedViews?.add(this) != true) { - // Duplicated calls or calls outside of onBeginFrame/onDrawFrame - // (indicated by _renderedViews being null) are ignored, as documented. - return; - } - _render(scene as _NativeScene); - } + void render(Scene scene) => _render(scene as _NativeScene); @Native)>(symbol: 'PlatformConfigurationNativeApi::Render') external static void _render(_NativeScene scene); diff --git a/lib/ui/window/platform_configuration_unittests.cc b/lib/ui/window/platform_configuration_unittests.cc index 1fa8377d69a74..7410caeb66d6c 100644 --- a/lib/ui/window/platform_configuration_unittests.cc +++ b/lib/ui/window/platform_configuration_unittests.cc @@ -15,171 +15,10 @@ #include "flutter/shell/common/shell_test.h" #include "flutter/shell/common/thread_host.h" #include "flutter/testing/testing.h" -#include "gmock/gmock.h" namespace flutter { - -namespace { - -static constexpr int64_t kImplicitViewId = 0; - -static void PostSync(const fml::RefPtr& task_runner, - const fml::closure& task) { - fml::AutoResetWaitableEvent latch; - fml::TaskRunner::RunNowOrPostTask(task_runner, [&latch, &task] { - task(); - latch.Signal(); - }); - latch.Wait(); -} - -class MockRuntimeDelegate : public RuntimeDelegate { - public: - MOCK_METHOD(std::string, DefaultRouteName, (), (override)); - MOCK_METHOD(void, ScheduleFrame, (bool), (override)); - MOCK_METHOD(void, - Render, - (std::unique_ptr, float), - (override)); - MOCK_METHOD(void, - UpdateSemantics, - (SemanticsNodeUpdates, CustomAccessibilityActionUpdates), - (override)); - MOCK_METHOD(void, - HandlePlatformMessage, - (std::unique_ptr), - (override)); - MOCK_METHOD(FontCollection&, GetFontCollection, (), (override)); - MOCK_METHOD(std::shared_ptr, GetAssetManager, (), (override)); - MOCK_METHOD(void, OnRootIsolateCreated, (), (override)); - MOCK_METHOD(void, - UpdateIsolateDescription, - (const std::string, int64_t), - (override)); - MOCK_METHOD(void, SetNeedsReportTimings, (bool), (override)); - MOCK_METHOD(std::unique_ptr>, - ComputePlatformResolvedLocale, - (const std::vector&), - (override)); - MOCK_METHOD(void, RequestDartDeferredLibrary, (intptr_t), (override)); - MOCK_METHOD(std::weak_ptr, - GetPlatformMessageHandler, - (), - (const, override)); - MOCK_METHOD(void, SendChannelUpdate, (std::string, bool), (override)); - MOCK_METHOD(double, - GetScaledFontSize, - (double font_size, int configuration_id), - (const, override)); -}; - -class MockPlatformMessageHandler : public PlatformMessageHandler { - public: - MOCK_METHOD(void, - HandlePlatformMessage, - (std::unique_ptr message), - (override)); - MOCK_METHOD(bool, - DoesHandlePlatformMessageOnPlatformThread, - (), - (const, override)); - MOCK_METHOD(void, - InvokePlatformMessageResponseCallback, - (int response_id, std::unique_ptr mapping), - (override)); - MOCK_METHOD(void, - InvokePlatformMessageEmptyResponseCallback, - (int response_id), - (override)); -}; - -// A class that can launch a RuntimeController with the specified -// RuntimeDelegate. -// -// To use this class, contruct this class with Create, call LaunchRootIsolate, -// and use the controller with ControllerTaskSync(). -class RuntimeControllerContext { - public: - using ControllerCallback = std::function; - - [[nodiscard]] static std::unique_ptr Create( - Settings settings, // - const TaskRunners& task_runners, // - RuntimeDelegate& client) { - auto [vm, isolate_snapshot] = Shell::InferVmInitDataFromSettings(settings); - FML_CHECK(vm) << "Must be able to initialize the VM."; - // Construct the class with `new` because `make_unique` has no access to the - // private constructor. - RuntimeControllerContext* raw_pointer = new RuntimeControllerContext( - settings, task_runners, client, std::move(vm), isolate_snapshot); - return std::unique_ptr(raw_pointer); - } - - ~RuntimeControllerContext() { - PostSync(task_runners_.GetUITaskRunner(), - [&]() { runtime_controller_.reset(); }); - } - - // Launch the root isolate. The post_launch callback will be executed in the - // same UI task, which can be used to create initial views. - void LaunchRootIsolate(RunConfiguration& configuration, - ControllerCallback post_launch) { - PostSync(task_runners_.GetUITaskRunner(), [&]() { - bool launch_success = runtime_controller_->LaunchRootIsolate( - settings_, // - []() {}, // - configuration.GetEntrypoint(), // - configuration.GetEntrypointLibrary(), // - configuration.GetEntrypointArgs(), // - configuration.TakeIsolateConfiguration()); // - ASSERT_TRUE(launch_success); - post_launch(*runtime_controller_); - }); - } - - // Run a task that operates the RuntimeController on the UI thread, and wait - // for the task to end. - void ControllerTaskSync(ControllerCallback task) { - ASSERT_TRUE(runtime_controller_); - ASSERT_TRUE(task); - PostSync(task_runners_.GetUITaskRunner(), - [&]() { task(*runtime_controller_); }); - } - - private: - RuntimeControllerContext(const Settings& settings, - const TaskRunners& task_runners, - RuntimeDelegate& client, - DartVMRef vm, - fml::RefPtr isolate_snapshot) - : settings_(settings), - task_runners_(task_runners), - isolate_snapshot_(std::move(isolate_snapshot)), - vm_(std::move(vm)), - runtime_controller_(std::make_unique( - client, - &vm_, - std::move(isolate_snapshot_), - settings.idle_notification_callback, // idle notification callback - flutter::PlatformData(), // platform data - settings.isolate_create_callback, // isolate create callback - settings.isolate_shutdown_callback, // isolate shutdown callback - settings.persistent_isolate_data, // persistent isolate data - UIDartState::Context{task_runners})) {} - - Settings settings_; - TaskRunners task_runners_; - fml::RefPtr isolate_snapshot_; - DartVMRef vm_; - std::unique_ptr runtime_controller_; -}; -} // namespace - namespace testing { -using ::testing::_; -using ::testing::Return; - class PlatformConfigurationTest : public ShellTest {}; TEST_F(PlatformConfigurationTest, Initialization) { @@ -493,84 +332,5 @@ TEST_F(PlatformConfigurationTest, SetDartPerformanceMode) { DestroyShell(std::move(shell), task_runners); } -TEST_F(PlatformConfigurationTest, OutOfScopeRenderCallsAreIgnored) { - Settings settings = CreateSettingsForFixture(); - TaskRunners task_runners = GetTaskRunnersForFixture(); - - MockRuntimeDelegate client; - auto platform_message_handler = - std::make_shared(); - EXPECT_CALL(client, GetPlatformMessageHandler) - .WillOnce(Return(platform_message_handler)); - // Render should not be called. - EXPECT_CALL(client, Render).Times(0); - - auto finish_latch = std::make_shared(); - auto finish = [finish_latch](Dart_NativeArguments args) { - finish_latch->Signal(); - }; - AddNativeCallback("Finish", CREATE_NATIVE_ENTRY(finish)); - - auto runtime_controller_context = - RuntimeControllerContext::Create(settings, task_runners, client); - - auto configuration = RunConfiguration::InferFromSettings(settings); - configuration.SetEntrypoint("incorrectImmediateRender"); - runtime_controller_context->LaunchRootIsolate( - configuration, [](RuntimeController& runtime_controller) { - runtime_controller.AddView( - kImplicitViewId, - ViewportMetrics( - /*pixel_ratio=*/1.0, /*width=*/20, /*height=*/20, - /*touch_slop=*/2, /*display_id=*/0)); - }); - - // Wait for the Dart main function to end. - finish_latch->Wait(); -} - -TEST_F(PlatformConfigurationTest, DuplicateRenderCallsAreIgnored) { - Settings settings = CreateSettingsForFixture(); - TaskRunners task_runners = GetTaskRunnersForFixture(); - - MockRuntimeDelegate client; - auto platform_message_handler = - std::make_shared(); - EXPECT_CALL(client, GetPlatformMessageHandler) - .WillOnce(Return(platform_message_handler)); - // Render should only be called once, because the second call is ignored. - EXPECT_CALL(client, Render).Times(1); - - auto finish_latch = std::make_shared(); - auto finish = [finish_latch](Dart_NativeArguments args) { - finish_latch->Signal(); - }; - AddNativeCallback("Finish", CREATE_NATIVE_ENTRY(finish)); - - auto runtime_controller_context = - RuntimeControllerContext::Create(settings, task_runners, client); - - auto configuration = RunConfiguration::InferFromSettings(settings); - configuration.SetEntrypoint("incorrectDoubleRender"); - runtime_controller_context->LaunchRootIsolate( - configuration, [](RuntimeController& runtime_controller) { - runtime_controller.AddView( - kImplicitViewId, - ViewportMetrics( - /*pixel_ratio=*/1.0, /*width=*/20, /*height=*/20, - /*touch_slop=*/2, /*display_id=*/0)); - }); - - // Wait for the Dart main function to end. - finish_latch->Wait(); - - // This call synchronously calls PlatformDispatcher's handleBeginFrame and - // handleDrawFrame. Therefore it doesn't have to wait for latches. - runtime_controller_context->ControllerTaskSync( - [](RuntimeController& runtime_controller) { - runtime_controller.BeginFrame(fml::TimePoint::Now(), 0); - }); -} - } // namespace testing } // namespace flutter diff --git a/shell/common/shell.cc b/shell/common/shell.cc index a6ba487b581f7..a7f7c9ae4c782 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -144,23 +144,6 @@ void PerformInitializationTasks(Settings& settings) { } // namespace -std::pair> -Shell::InferVmInitDataFromSettings(Settings& settings) { - // Always use the `vm_snapshot` and `isolate_snapshot` provided by the - // settings to launch the VM. If the VM is already running, the snapshot - // arguments are ignored. - auto vm_snapshot = DartSnapshot::VMSnapshotFromSettings(settings); - auto isolate_snapshot = DartSnapshot::IsolateSnapshotFromSettings(settings); - auto vm = DartVMRef::Create(settings, vm_snapshot, isolate_snapshot); - - // If the settings did not specify an `isolate_snapshot`, fall back to the - // one the VM was launched with. - if (!isolate_snapshot) { - isolate_snapshot = vm->GetVMData()->GetIsolateSnapshot(); - } - return {std::move(vm), isolate_snapshot}; -} - std::unique_ptr Shell::Create( const PlatformData& platform_data, const TaskRunners& task_runners, @@ -173,7 +156,19 @@ std::unique_ptr Shell::Create( TRACE_EVENT0("flutter", "Shell::Create"); - auto [vm, isolate_snapshot] = InferVmInitDataFromSettings(settings); + // Always use the `vm_snapshot` and `isolate_snapshot` provided by the + // settings to launch the VM. If the VM is already running, the snapshot + // arguments are ignored. + auto vm_snapshot = DartSnapshot::VMSnapshotFromSettings(settings); + auto isolate_snapshot = DartSnapshot::IsolateSnapshotFromSettings(settings); + auto vm = DartVMRef::Create(settings, vm_snapshot, isolate_snapshot); + FML_CHECK(vm) << "Must be able to initialize the VM."; + + // If the settings did not specify an `isolate_snapshot`, fall back to the + // one the VM was launched with. + if (!isolate_snapshot) { + isolate_snapshot = vm->GetVMData()->GetIsolateSnapshot(); + } auto resource_cache_limit_calculator = std::make_shared( settings.resource_cache_max_bytes_threshold); diff --git a/shell/common/shell.h b/shell/common/shell.h index 133fecc4d0f43..039eb653a05e7 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -438,16 +438,6 @@ class Shell final : public PlatformView::Delegate, const std::shared_ptr GetConcurrentWorkerTaskRunner() const; - // Infer the VM ref and the isolate snapshot based on the settings. - // - // If the VM is already running, the settings are ignored, but the returned - // isolate snapshot always prioritize what is specified by the settings, and - // falls back to the one VM was launched with. - // - // This function is what Shell::Create uses to infer snapshot settings. - static std::pair> - InferVmInitDataFromSettings(Settings& settings); - private: using ServiceProtocolHandler = std::function + + + + + + + + diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index bf09252581289..35a1f31e4c4cc 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -275,6 +275,7 @@ android_java_sources = [ "io/flutter/embedding/engine/systemchannels/NavigationChannel.java", "io/flutter/embedding/engine/systemchannels/PlatformChannel.java", "io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java", + "io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java", "io/flutter/embedding/engine/systemchannels/RestorationChannel.java", "io/flutter/embedding/engine/systemchannels/SettingsChannel.java", "io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java", @@ -322,6 +323,7 @@ android_java_sources = [ "io/flutter/plugin/platform/SingleViewPresentation.java", "io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java", "io/flutter/plugin/platform/VirtualDisplayController.java", + "io/flutter/plugin/text/ProcessTextPlugin.java", "io/flutter/util/HandlerCompat.java", "io/flutter/util/PathUtils.java", "io/flutter/util/Preconditions.java", diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 2bfa379f39c1e..cb00ec42b363c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -31,6 +31,7 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.ProcessTextChannel; import io.flutter.embedding.engine.systemchannels.RestorationChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SpellCheckChannel; @@ -38,6 +39,7 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.plugin.text.ProcessTextPlugin; import io.flutter.util.ViewUtils; import java.util.HashSet; import java.util.List; @@ -95,6 +97,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater { @NonNull private final NavigationChannel navigationChannel; @NonNull private final RestorationChannel restorationChannel; @NonNull private final PlatformChannel platformChannel; + @NonNull private final ProcessTextChannel processTextChannel; @NonNull private final SettingsChannel settingsChannel; @NonNull private final SpellCheckChannel spellCheckChannel; @NonNull private final SystemChannel systemChannel; @@ -329,6 +332,7 @@ public FlutterEngine( mouseCursorChannel = new MouseCursorChannel(dartExecutor); navigationChannel = new NavigationChannel(dartExecutor); platformChannel = new PlatformChannel(dartExecutor); + processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager()); restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData); settingsChannel = new SettingsChannel(dartExecutor); spellCheckChannel = new SpellCheckChannel(dartExecutor); @@ -384,6 +388,9 @@ public FlutterEngine( } ViewUtils.calculateMaximumDisplayMetrics(context, this); + + ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(this.getProcessTextChannel()); + this.pluginRegistry.add(processTextPlugin); } private void attachToJni() { @@ -545,6 +552,12 @@ public PlatformChannel getPlatformChannel() { return platformChannel; } + /** System channel that sends text processing requests from Flutter to Android. */ + @NonNull + public ProcessTextChannel getProcessTextChannel() { + return processTextChannel; + } + /** * System channel to exchange restoration data between framework and engine. * diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java new file mode 100644 index 0000000000000..f1d12bc681111 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java @@ -0,0 +1,122 @@ +// 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.systemchannels; + +import android.content.pm.PackageManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; +import java.util.ArrayList; +import java.util.Map; + +/** + * {@link ProcessTextChannel} is a platform channel that is used by the framework to initiate text + * processing feature in the embedding and for the embedding to send back the results. + * + *

When the framework needs to query the list of text processing actions (for instance to expose + * them in the selected text context menu), it will send to the embedding the message {@code + * ProcessText.queryTextActions}. In response, the {@link io.flutter.plugin.text.ProcessTextPlugin} + * will return a map of all activities that can process text. The map keys are generated IDs and the + * values are the activities labels. On the first request, the {@link + * io.flutter.plugin.text.ProcessTextPlugin} will make a call to Android's package manager to query + * all activities that can be performed for the {@code Intent.ACTION_PROCESS_TEXT} intent. + * + *

When a text processing action has to be executed, the framework will send to the embedding the + * message {@code ProcessText.processTextAction} with the {@code int id} of the choosen text action + * and the {@code String} of text to process as arguments. In response, the {@link + * io.flutter.plugin.text.ProcessTextPlugin} will make a call to the Android application activity to + * start the activity exposing the text action. The {@link io.flutter.plugin.text.ProcessTextPlugin} + * will return the processed text if there is one, or null if the activity did not return a + * transformed text. + * + *

{@link io.flutter.plugin.text.ProcessTextPlugin} implements {@link ProcessTextMethodHandler} + * that parses incoming messages from Flutter. + */ +public class ProcessTextChannel { + private static final String TAG = "ProcessTextChannel"; + private static final String CHANNEL_NAME = "flutter/processtext"; + private static final String METHOD_QUERY_TEXT_ACTIONS = "ProcessText.queryTextActions"; + private static final String METHOD_PROCESS_TEXT_ACTION = "ProcessText.processTextAction"; + + public final MethodChannel channel; + public final PackageManager packageManager; + private ProcessTextMethodHandler processTextMethodHandler; + + @NonNull + public final MethodChannel.MethodCallHandler parsingMethodHandler = + new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (processTextMethodHandler == null) { + return; + } + String method = call.method; + Object args = call.arguments; + switch (method) { + case METHOD_QUERY_TEXT_ACTIONS: + try { + Map actions = processTextMethodHandler.queryTextActions(); + result.success(actions); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + break; + case METHOD_PROCESS_TEXT_ACTION: + try { + final ArrayList argumentList = (ArrayList) args; + String id = (String) (argumentList.get(0)); + String text = (String) (argumentList.get(1)); + boolean readOnly = (boolean) (argumentList.get(2)); + processTextMethodHandler.processTextAction(id, text, readOnly, result); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + break; + default: + result.notImplemented(); + break; + } + } + }; + + public ProcessTextChannel( + @NonNull DartExecutor dartExecutor, @NonNull PackageManager packageManager) { + this.packageManager = packageManager; + channel = new MethodChannel(dartExecutor, CHANNEL_NAME, StandardMethodCodec.INSTANCE); + channel.setMethodCallHandler(parsingMethodHandler); + } + + /** + * Sets the {@link ProcessTextMethodHandler} which receives all requests to the text processing + * feature sent through this channel. + */ + public void setMethodHandler(@Nullable ProcessTextMethodHandler processTextMethodHandler) { + this.processTextMethodHandler = processTextMethodHandler; + } + + public interface ProcessTextMethodHandler { + /** Requests the map of text actions. Each text action has a unique id and a localized label. */ + Map queryTextActions(); + + /** + * Requests to run a text action on a given input text. + * + * @param id The ID of the text action returned by {@code ProcessText.queryTextActions}. + * @param input The text to be processed. + * @param readOnly Indicates to the activity if the processed text will be used as read-only. + * see + * https://developer.android.com/reference/android/content/Intent#EXTRA_PROCESS_TEXT_READONLY + * @param result The method channel result instance used to reply. + */ + void processTextAction( + @NonNull String id, + @NonNull String input, + @NonNull boolean readOnly, + @NonNull MethodChannel.Result result); + } +} diff --git a/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java b/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java new file mode 100644 index 0000000000000..3338ed2cd9bcf --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java @@ -0,0 +1,197 @@ +// 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.plugin.text; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.systemchannels.ProcessTextChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ProcessTextPlugin + implements FlutterPlugin, + ActivityAware, + ActivityResultListener, + ProcessTextChannel.ProcessTextMethodHandler { + private static final String TAG = "ProcessTextPlugin"; + + @NonNull private final ProcessTextChannel processTextChannel; + @NonNull private final PackageManager packageManager; + @Nullable private ActivityPluginBinding activityBinding; + private Map resolveInfosById; + + @NonNull + private Map requestsByCode = + new HashMap(); + + public ProcessTextPlugin(@NonNull ProcessTextChannel processTextChannel) { + this.processTextChannel = processTextChannel; + this.packageManager = processTextChannel.packageManager; + + processTextChannel.setMethodHandler(this); + } + + @Override + public Map queryTextActions() { + if (resolveInfosById == null) { + cacheResolveInfos(); + } + Map result = new HashMap(); + for (String id : resolveInfosById.keySet()) { + final ResolveInfo info = resolveInfosById.get(id); + result.put(id, info.loadLabel(packageManager).toString()); + } + return result; + } + + @Override + public void processTextAction( + @NonNull String id, + @NonNull String text, + @NonNull boolean readOnly, + @NonNull MethodChannel.Result result) { + if (activityBinding == null) { + result.error("error", "Plugin not bound to an Activity", null); + return; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + result.error("error", "Android version not supported", null); + return; + } + + if (resolveInfosById == null) { + result.error("error", "Can not process text actions before calling queryTextActions", null); + return; + } + + final ResolveInfo info = resolveInfosById.get(id); + if (info == null) { + result.error("error", "Text processing activity not found", null); + return; + } + + Integer requestCode = result.hashCode(); + requestsByCode.put(requestCode, result); + + Intent intent = new Intent(); + intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); + intent.setAction(Intent.ACTION_PROCESS_TEXT); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_PROCESS_TEXT, text); + intent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly); + + // Start the text processing activity. When the activity completes, the onActivityResult + // callback + // is called. + activityBinding.getActivity().startActivityForResult(intent, requestCode); + } + + private void cacheResolveInfos() { + resolveInfosById = new HashMap(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return; + } + + Intent intent = new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain"); + + List infos; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + infos = packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0)); + } else { + infos = packageManager.queryIntentActivities(intent, 0); + } + + for (ResolveInfo info : infos) { + final String id = info.activityInfo.name; + final String label = info.loadLabel(packageManager).toString(); + resolveInfosById.put(id, info); + } + } + + /** + * Executed when a text processing activity terminates. + * + *

When an activity returns a value, the request is completed successfully and returns the + * processed text. + * + *

When an activity does not return a value. the request is completed successfully and returns + * null. + */ + @TargetApi(Build.VERSION_CODES.M) + @RequiresApi(Build.VERSION_CODES.M) + public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent intent) { + // Return early if the result is not related to a request sent by this plugin. + if (!requestsByCode.containsKey(requestCode)) { + return false; + } + + String result = null; + if (resultCode == Activity.RESULT_OK) { + result = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT); + } + requestsByCode.remove(requestCode).success(result); + return true; + } + + /** + * Unregisters this {@code ProcessTextPlugin} as the {@code + * ProcessTextChannel.ProcessTextMethodHandler}, for the {@link + * io.flutter.embedding.engine.systemchannels.ProcessTextChannel}. + * + *

Do not invoke any methods on a {@code ProcessTextPlugin} after invoking this method. + */ + public void destroy() { + processTextChannel.setMethodHandler(null); + } + + // FlutterPlugin interface implementation. + + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + // Nothing to do because this plugin is instantiated by the engine. + } + + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + // Nothing to do because this plugin is instantiated by the engine. + } + + // ActivityAware interface implementation. + // + // Store the binding and manage the activity result listener. + + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + this.activityBinding = binding; + this.activityBinding.addActivityResultListener(this); + }; + + public void onDetachedFromActivityForConfigChanges() { + this.activityBinding.removeActivityResultListener(this); + this.activityBinding = null; + } + + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + this.activityBinding = binding; + this.activityBinding.addActivityResultListener(this); + } + + public void onDetachedFromActivity() { + this.activityBinding.removeActivityResultListener(this); + this.activityBinding = null; + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java b/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java new file mode 100644 index 0000000000000..e582f9b8c8fb8 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/text/ProcessTextPluginTest.java @@ -0,0 +1,280 @@ +package io.flutter.plugin.text; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import androidx.annotation.RequiresApi; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.systemchannels.ProcessTextChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +@RunWith(AndroidJUnit4.class) +@TargetApi(Build.VERSION_CODES.N) +@RequiresApi(Build.VERSION_CODES.N) +public class ProcessTextPluginTest { + + private static void sendToBinaryMessageHandler( + BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method, Object args) { + MethodCall methodCall = new MethodCall(method, args); + ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); + binaryMessageHandler.onMessage( + (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void respondsToProcessTextChannelMessage() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + ProcessTextChannel.ProcessTextMethodHandler mockHandler = + mock(ProcessTextChannel.ProcessTextMethodHandler.class); + PackageManager mockPackageManager = mock(PackageManager.class); + ProcessTextChannel processTextChannel = + new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + + processTextChannel.setMethodHandler(mockHandler); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + + sendToBinaryMessageHandler(binaryMessageHandler, "ProcessText.queryTextActions", null); + + verify(mockHandler).queryTextActions(); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void performQueryTextActions() { + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + PackageManager mockPackageManager = mock(PackageManager.class); + ProcessTextChannel processTextChannel = + new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + + // Set up mocked result for PackageManager.queryIntentActivities. + ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); + ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager); + List infos = new ArrayList(Arrays.asList(action1, action2)); + Intent intent = new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain"); + when(mockPackageManager.queryIntentActivities( + any(Intent.class), any(PackageManager.ResolveInfoFlags.class))) + .thenReturn(infos); + + // ProcessTextPlugin should retrieve the mocked text actions. + ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel); + Map textActions = processTextPlugin.queryTextActions(); + final String action1Id = "mockActivityName.Action1"; + final String action2Id = "mockActivityName.Action2"; + assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2")); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void performProcessTextActionWithNoReturnedValue() { + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + PackageManager mockPackageManager = mock(PackageManager.class); + ProcessTextChannel processTextChannel = + new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + + // Set up mocked result for PackageManager.queryIntentActivities. + ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); + ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager); + List infos = new ArrayList(Arrays.asList(action1, action2)); + when(mockPackageManager.queryIntentActivities( + any(Intent.class), any(PackageManager.ResolveInfoFlags.class))) + .thenReturn(infos); + + // ProcessTextPlugin should retrieve the mocked text actions. + ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel); + Map textActions = processTextPlugin.queryTextActions(); + final String action1Id = "mockActivityName.Action1"; + final String action2Id = "mockActivityName.Action2"; + assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2")); + + // Set up the activity binding. + ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + Activity mockActivity = mock(Activity.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity); + processTextPlugin.onAttachedToActivity(mockActivityPluginBinding); + + // Execute th first action. + String textToBeProcessed = "Flutter!"; + MethodChannel.Result result = mock(MethodChannel.Result.class); + processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result); + + // Activity.startActivityForResult should have been called. + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt()); + Intent intent = intentCaptor.getValue(); + assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed); + + // Simulate an Android activity answer which does not return a value. + Intent resultIntent = new Intent(); + processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent); + + // Success with no returned value is expected. + verify(result).success(null); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void performProcessTextActionWithReturnedValue() { + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + PackageManager mockPackageManager = mock(PackageManager.class); + ProcessTextChannel processTextChannel = + new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + + // Set up mocked result for PackageManager.queryIntentActivities. + ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); + ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager); + List infos = new ArrayList(Arrays.asList(action1, action2)); + when(mockPackageManager.queryIntentActivities( + any(Intent.class), any(PackageManager.ResolveInfoFlags.class))) + .thenReturn(infos); + + // ProcessTextPlugin should retrieve the mocked text actions. + ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel); + Map textActions = processTextPlugin.queryTextActions(); + final String action1Id = "mockActivityName.Action1"; + final String action2Id = "mockActivityName.Action2"; + assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2")); + + // Set up the activity binding. + ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + Activity mockActivity = mock(Activity.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity); + processTextPlugin.onAttachedToActivity(mockActivityPluginBinding); + + // Execute the first action. + String textToBeProcessed = "Flutter!"; + MethodChannel.Result result = mock(MethodChannel.Result.class); + processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result); + + // Activity.startActivityForResult should have been called. + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt()); + Intent intent = intentCaptor.getValue(); + assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed); + + // Simulate an Android activity answer which returns a transformed text. + String processedText = "Flutter!!!"; + Intent resultIntent = new Intent(); + resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, processedText); + processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent); + + // Success with the transformed text is expected. + verify(result).success(processedText); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void doNotCrashOnNonRelatedActivityResult() { + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + PackageManager mockPackageManager = mock(PackageManager.class); + ProcessTextChannel processTextChannel = + new ProcessTextChannel(mockBinaryMessenger, mockPackageManager); + + // Set up mocked result for PackageManager.queryIntentActivities. + ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager); + ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager); + List infos = new ArrayList(Arrays.asList(action1, action2)); + when(mockPackageManager.queryIntentActivities( + any(Intent.class), any(PackageManager.ResolveInfoFlags.class))) + .thenReturn(infos); + + // ProcessTextPlugin should retrieve the mocked text actions. + ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel); + Map textActions = processTextPlugin.queryTextActions(); + final String action1Id = "mockActivityName.Action1"; + final String action2Id = "mockActivityName.Action2"; + assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2")); + + // Set up the activity binding. + ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + Activity mockActivity = mock(Activity.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity); + processTextPlugin.onAttachedToActivity(mockActivityPluginBinding); + + // Execute the first action. + String textToBeProcessed = "Flutter!"; + MethodChannel.Result result = mock(MethodChannel.Result.class); + processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result); + + // Activity.startActivityForResult should have been called. + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt()); + Intent intent = intentCaptor.getValue(); + assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed); + + // Result to a request not sent by this plugin should be ignored. + final int externalRequestCode = 42; + processTextPlugin.onActivityResult(externalRequestCode, Activity.RESULT_OK, new Intent()); + + // Simulate an Android activity answer which returns a transformed text. + String processedText = "Flutter!!!"; + Intent resultIntent = new Intent(); + resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, processedText); + processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent); + + // Success with the transformed text is expected. + verify(result).success(processedText); + } + + private ResolveInfo createFakeResolveInfo(String label, PackageManager mockPackageManager) { + ResolveInfo resolveInfo = mock(ResolveInfo.class); + ActivityInfo activityInfo = new ActivityInfo(); + when(resolveInfo.loadLabel(mockPackageManager)).thenReturn(label); + + // Use Java reflection to set required member variables. + try { + Field activityField = ResolveInfo.class.getDeclaredField("activityInfo"); + activityField.setAccessible(true); + activityField.set(resolveInfo, activityInfo); + Field packageNameField = PackageItemInfo.class.getDeclaredField("packageName"); + packageNameField.setAccessible(true); + packageNameField.set(activityInfo, "mockActivityPackageName"); + Field nameField = PackageItemInfo.class.getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(activityInfo, "mockActivityName." + label); + } catch (Exception ex) { + // Test will failed if reflection APIs throw. + } + + return resolveInfo; + } +} diff --git a/testing/dart/platform_view_test.dart b/testing/dart/platform_view_test.dart index 7d467f872ab17..146c865899952 100644 --- a/testing/dart/platform_view_test.dart +++ b/testing/dart/platform_view_test.dart @@ -10,13 +10,10 @@ void main() { test('PlatformView layers do not emit errors from tester', () async { final SceneBuilder builder = SceneBuilder(); builder.addPlatformView(1); + final Scene scene = builder.build(); - PlatformDispatcher.instance.onBeginFrame = (Duration duration) { - final Scene scene = builder.build(); - PlatformDispatcher.instance.implicitView!.render(scene); - scene.dispose(); - }; - PlatformDispatcher.instance.scheduleFrame(); + PlatformDispatcher.instance.implicitView!.render(scene); + scene.dispose(); // Test harness asserts that this does not emit an error from the shell logs. }); }