| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // Entrypoint for running a Flutter app on Fuchsia. |
| // |
| // Usage: ./main <path_to_flutter_asset_bundle> |
| |
| #include <fuchsia/accessibility/semantics/cpp/fidl.h> |
| #include <fuchsia/fonts/cpp/fidl.h> |
| #include <fuchsia/scenic/scheduling/cpp/fidl.h> |
| #include <fuchsia/sysmem/cpp/fidl.h> |
| #include <fuchsia/ui/app/cpp/fidl.h> |
| #include <fuchsia/ui/composition/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/default.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/fidl/cpp/interface_request.h> |
| #include <lib/sys/cpp/component_context.h> |
| #include <lib/syslog/global.h> |
| #include <lib/ui/scenic/cpp/view_identity.h> |
| #include <lib/ui/scenic/cpp/view_ref_pair.h> |
| #include <zircon/status.h> |
| |
| #include <string> |
| |
| #include "platform_message_channels.h" |
| #include "src/embedder/accessibility_bridge.h" |
| #include "src/embedder/embedder_state.h" |
| #include "src/embedder/engine/embedder.h" |
| #include "src/embedder/flatland_ids.h" |
| #include "src/embedder/flatland_view_provider.h" |
| #include "src/embedder/fuchsia_logger.h" |
| #include "src/embedder/logging.h" |
| #include "src/embedder/mouse_delegate.h" |
| #include "src/embedder/platform_message_channels.h" |
| #include "src/embedder/root_inspect_node.h" |
| #include "src/embedder/software_surface.h" |
| #include "src/embedder/text_delegate.h" |
| #include "src/embedder/touch_delegate.h" |
| |
| namespace embedder { |
| namespace { |
| |
| // TODO(akbiggs): Don't hardcode this screen size, get it from |
| // an event instead. |
| constexpr int kScreenWidth = 1280; |
| constexpr int kScreenHeight = 800; |
| |
| /// Gets the current name of the Fuchsia process that |
| /// is running the embedder. |
| std::string GetCurrentProcessName() { |
| char name[ZX_MAX_NAME_LEN]; |
| zx_status_t status = zx::process::self()->get_property(ZX_PROP_NAME, name, sizeof(name)); |
| if (status != ZX_OK) { |
| FX_LOG(ERROR, kLogTag, "Failed to get process name for sysmem; using \"\"."); |
| return std::string(); |
| } |
| |
| return std::string(name); |
| } |
| |
| /// Gets the current process ID of the Fuchsia process that |
| /// is running the embedder. |
| zx_koid_t GetCurrentProcessId() { |
| zx_info_handle_basic_t info; |
| zx_status_t status = |
| zx::process::self()->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), |
| nullptr /* actual_count */, nullptr /* avail_count */); |
| if (status != ZX_OK) { |
| FX_LOG(ERROR, kLogTag, "Failed to get process ID for sysmem; using ZX_KOID_INVALID."); |
| return ZX_KOID_INVALID; |
| } |
| |
| return info.koid; |
| } |
| |
| /// Return kernel object id for fuchsia view ref |
| zx_koid_t GetKoid(const fuchsia::ui::views::ViewRef& view_ref) { |
| zx_handle_t handle = view_ref.reference.get(); |
| zx_info_handle_basic_t info; |
| zx_status_t status = |
| zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); |
| return status == ZX_OK ? info.koid : ZX_KOID_INVALID; |
| } |
| |
| /// Handle platform message from dart application, forwarded from flutter engine |
| void FuchsiaHandlePlatformMessage(const FlutterPlatformMessage* message, void* user_data) { |
| if (!strcmp(message->channel, kAccessibilityChannel)) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| embedder->accessibility_bridge_->HandlePlatformMessage(message); |
| } else if (!strcmp(message->channel, kTextInputChannel)) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| embedder->text_delegate_->HandleFlutterTextInputChannelPlatformMessage(message); |
| } else { |
| FX_LOGF(INFO, embedder::kLogTag, "FuchsiaHandlePlatformMessage invoked: %s", message->channel); |
| } |
| } |
| |
| /// Fuchsia implementation of the Flutter Embedder update_semantics_callback |
| void FuchsiaHandleSemanticsUpdate(const FlutterSemanticsUpdate* update, void* user_data) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| embedder->accessibility_bridge_->AddSemanticsUpdate(update); |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's |vsync_callback|. |
| void FuchsiaVsync(void* user_data, intptr_t baton) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| |
| // Encourage the Engine to vsync for a 60Hz display. |
| const uint64_t now_nanos = FlutterEngineGetCurrentTime(); |
| const uint64_t target_vsync_time = now_nanos + 16.6 * 1e6; |
| FlutterEngineOnVsync(embedder->engine, baton, now_nanos, target_vsync_time); |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's |log_message_callback|. |
| void FuchsiaLogMessage(const char* tag, const char* message, void* user_data) { |
| FX_LOG(INFO, tag, message); |
| } |
| |
| /// Callback for the embedder's FlutterEngineSendKeyEvent function. |
| void FuchsiaFlutterKeyEventCallback(bool handled, void* user_data) { |
| FX_LOG(INFO, embedder::kLogTag, "FlutterKeyEventCallback invoked!"); |
| } |
| |
| void FuchsiaFlutterDataCallback(const uint8_t* data, size_t size, void* user_data) { |
| auto message_data = static_cast<const unsigned char*>(data); |
| FX_LOGF(INFO, embedder::kLogTag, "FuchsiaFlutterDataCallback invoked, data: %s", message_data); |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's software rendering |
| /// |surface_acquire_callback|. |
| bool FuchsiaAcquireSoftwareSurface(void* user_data, size_t width, size_t height, |
| uint8_t** allocation, size_t* stride) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| |
| // TODO(akbiggs): Anything we need to do to clean up the old |
| // software surface explicitly before reassigning here? |
| embedder->software_surface = std::make_unique<SoftwareSurface>( |
| embedder->sysmem_allocator, embedder->flatland_allocator, |
| Size{.width = static_cast<uint32_t>(width), .height = static_cast<uint32_t>(height)}); |
| if (!embedder->software_surface->IsValid()) { |
| FX_LOGF(ERROR, kLogTag, "Failed to acquire software surface of size (%lu, %lu).", width, |
| height); |
| return false; |
| } |
| |
| // If we receive an unitialized surface, we need to create a |
| // Flatland image and associate it with the surface. |
| if (embedder->software_surface->GetImageId() == 0) { |
| auto image_id = embedder->flatland_connection->NextContentId().value; |
| const auto surface_size = embedder->software_surface->GetSize(); |
| |
| fuchsia::math::SizeU image_size; |
| image_size.width = surface_size.width; |
| image_size.height = surface_size.height; |
| |
| fuchsia::ui::composition::ImageProperties image_properties; |
| image_properties.set_size(image_size); |
| |
| embedder->flatland_connection->flatland()->CreateImage( |
| {image_id}, embedder->software_surface->GetBufferCollectionImportToken(), 0, |
| std::move(image_properties)); |
| |
| embedder->software_surface->SetImageId(image_id); |
| embedder->software_surface->SetReleaseImageCallback( |
| [flatland = embedder->flatland_connection.get(), image_id]() { |
| flatland->flatland()->ReleaseImage({image_id}); |
| }); |
| } |
| |
| // Enqueue fences for the next present. |
| embedder->flatland_connection->EnqueueAcquireFence(embedder->software_surface->GetAcquireFence()); |
| embedder->flatland_connection->EnqueueReleaseFence(embedder->software_surface->GetReleaseFence()); |
| |
| // Communicate the location of the allocated memory to Flutter so |
| // Flutter can render into it. |
| *allocation = embedder->software_surface->GetAllocation(); |
| *stride = embedder->software_surface->GetBytesPerRow(); |
| |
| return true; |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's software rendering |
| /// |surface_present_callback|. |
| bool FuchsiaPresentSoftwareSurface(void* user_data, const void* allocation, size_t row_bytes, |
| size_t height) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| |
| if (embedder->software_surface == nullptr) { |
| // We have no surface acquired to present. |
| return false; |
| } |
| |
| // TODO(akbiggs): Replace with composition logic matching |
| // https://github.com/flutter/engine/blob/main/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc#L108. |
| auto image_id = embedder->software_surface->GetImageId(); |
| embedder->flatland_connection->flatland()->SetContent({kRootTransformId}, {image_id}); |
| embedder->flatland_connection->Present(); |
| |
| // Signal to the rendered surface that we've finished writing. |
| embedder->software_surface->SignalWritesFinished( |
| /* on_read_finished = */ []() { |
| // TODO(akbiggs): Anything we need to do after the read |
| // finishes? |
| }); |
| |
| return true; |
| } |
| |
| /// Runs the Flutter app whose assets (including snapshots) are located |
| /// at |assets_path| in the component's package. |
| /// |
| /// The Engine instance will be stored in |embedder| and |embedder| will be |
| /// the user data for all callbacks from the Flutter Engine. |
| bool RunFlutterApp(const char* assets_path, EmbedderState* embedder) { |
| FlutterRendererConfig renderer_config = { |
| .type = kSoftware, |
| .software = |
| { |
| .struct_size = sizeof(FlutterSoftwareRendererConfig), |
| .surface_present_callback = FuchsiaPresentSoftwareSurface, |
| .surface_acquire_callback = FuchsiaAcquireSoftwareSurface, |
| }, |
| }; |
| // Connect and set up the system font provider. |
| fuchsia::fonts::ProviderSyncPtr sync_font_provider; |
| zx_status_t status = embedder->component_context->svc()->Connect<fuchsia::fonts::Provider>( |
| sync_font_provider.NewRequest()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to fonts provider service: %s", |
| zx_status_get_string(status)); |
| } |
| |
| FlutterProjectArgs project_args = { |
| .struct_size = sizeof(FlutterProjectArgs), |
| .assets_path = assets_path, |
| .platform_message_callback = FuchsiaHandlePlatformMessage, |
| .vsync_callback = FuchsiaVsync, |
| .log_message_callback = FuchsiaLogMessage, |
| .log_tag = "flutter_app", |
| .update_semantics_callback = FuchsiaHandleSemanticsUpdate, |
| .font_initialization_data = sync_font_provider.Unbind().TakeChannel().release()}; |
| |
| FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &renderer_config, |
| &project_args, embedder, &embedder->engine); |
| |
| if (result != kSuccess || embedder->engine == nullptr) { |
| FX_LOGF(ERROR, kLogTag, "Could not run the Flutter Engine. Error code: %d", |
| static_cast<int>(result)); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| } // namespace embedder |
| |
| int main(int argc, const char* argv[]) { |
| if (argc != 2) { |
| FX_LOG(ERROR, embedder::kLogTag, "usage: executable <path to flutter bundle>"); |
| return EXIT_FAILURE; |
| } |
| const char* assets_path = argv[1]; |
| |
| embedder::EmbedderState embedder = { |
| // This must be initialized before calling RunFlutterApp, as |
| // this hack https://github.com/flutter/engine/pull/33472 |
| // relies |
| // on it. |
| .component_context = sys::ComponentContext::Create(), |
| }; |
| |
| // Serving the component context must happen after RunFlutterApp |
| // because the embedder platform has a hack that adds inspect data |
| // into the Dart VM using the component context |
| // (https://github.com/flutter/engine/pull/33472). |
| if (!embedder::RunFlutterApp(assets_path, &embedder)) { |
| return EXIT_FAILURE; |
| } |
| |
| // Ensure default dispatcher is set after caling RunFlutterApp |
| FX_CHECK(async_get_default_dispatcher()); |
| |
| // Necessary to get Flutter to render frames. |
| FlutterWindowMetricsEvent window_metrics_event = { |
| .struct_size = sizeof(FlutterWindowMetricsEvent), |
| |
| // TODO(akbiggs): Don't hardcode these. |
| .width = embedder::kScreenWidth, |
| .height = embedder::kScreenHeight, |
| |
| .pixel_ratio = 1, |
| .left = 0, |
| .top = 0, |
| .physical_view_inset_top = 0, |
| .physical_view_inset_right = 0, |
| .physical_view_inset_bottom = 0, |
| .physical_view_inset_left = 0, |
| }; |
| FlutterEngineSendWindowMetricsEvent(embedder.engine, &window_metrics_event); |
| |
| // Connect to Keyboard service. |
| fuchsia::ui::input3::KeyboardHandle keyboard; |
| zx_status_t keyboard_status = |
| embedder.component_context->svc()->Connect<fuchsia::ui::input3::Keyboard>( |
| keyboard.NewRequest()); |
| if (keyboard_status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to fuchsia::ui::input3::Keyboard: %s", |
| zx_status_get_string(keyboard_status)); |
| return EXIT_FAILURE; |
| } |
| |
| // Connect to ImeService service. |
| fuchsia::ui::input::ImeServiceHandle ime_service; |
| zx_status_t ime_status = |
| embedder.component_context->svc()->Connect<fuchsia::ui::input::ImeService>( |
| ime_service.NewRequest()); |
| if (ime_status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to fuchsia::ui::input::ImeService: %s", |
| zx_status_get_string(ime_status)); |
| } |
| |
| // Connect to Flatland. |
| fuchsia::ui::composition::FlatlandHandle flatland_handle; |
| zx_status_t status = |
| embedder.component_context->svc()->Connect<fuchsia::ui::composition::Flatland>( |
| flatland_handle.NewRequest()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to Flatland: %s", |
| zx_status_get_string(status)); |
| return EXIT_FAILURE; |
| } |
| // TODO(akbiggs): Create this on the raster thread per |
| // FlatlandConnection's documentation. |
| embedder.flatland_connection = std::make_unique<embedder::FlatlandConnection>( |
| // TODO(akbiggs): Pick a more appropriate debug name based |
| // on the Flutter app name. |
| "FlutterEmbedder" /* debug_name */, std::move(flatland_handle), |
| /* on_error_callback = */ |
| []() { |
| // TODO(akbiggs): What should we do on errors? |
| }, |
| /* on_frame_presented_callback = */ |
| [](fuchsia::scenic::scheduling::FramePresentedInfo info) { |
| // TODO(akbiggs): What should we do after a frame |
| // finishes presenting? |
| }); |
| |
| // Focuser for programatically transfering View focus. |
| fuchsia::ui::views::FocuserHandle focuser; |
| fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused; |
| |
| // The protocol endpoints bound to a Flatland ViewCreationToken. |
| fuchsia::ui::composition::ViewBoundProtocols flatland_view_protocols; |
| flatland_view_protocols.set_view_focuser(focuser.NewRequest()); |
| flatland_view_protocols.set_view_ref_focused(view_ref_focused.NewRequest()); |
| |
| // Mouse & Touch source |
| fuchsia::ui::pointer::MouseSourceHandle mouse_source; |
| fuchsia::ui::pointer::TouchSourceHandle touch_source; |
| flatland_view_protocols.set_touch_source(touch_source.NewRequest()); |
| flatland_view_protocols.set_mouse_source(mouse_source.NewRequest()); |
| |
| auto view_ref_pair = scenic::ViewRefPair::New(); |
| |
| fuchsia::ui::views::ViewRef platform_view_ref; |
| view_ref_pair.view_ref.Clone(&platform_view_ref); |
| fuchsia::ui::views::ViewRef accessibility_view_ref; |
| view_ref_pair.view_ref.Clone(&accessibility_view_ref); |
| |
| // Response handle attached to a FlutterPlatformMessage to be called in the engine. |
| FlutterPlatformMessageResponseHandle* response_handle; |
| FlutterPlatformMessageCreateResponseHandle(embedder.engine, embedder::FuchsiaFlutterDataCallback, |
| &embedder, &response_handle); |
| |
| // Keyboard/Text input. |
| embedder.text_delegate_ = std::make_unique<embedder::TextDelegate>( |
| std::move(platform_view_ref), std::move(ime_service), std::move(keyboard), |
| /* key_event_dispatch_callback: [flutter/keydata] */ |
| [&embedder](const FlutterKeyEvent* event) { |
| // Send key event to the engine. |
| FlutterEngineResult result = FlutterEngineSendKeyEvent( |
| embedder.engine, event, embedder::FuchsiaFlutterKeyEventCallback, &embedder); |
| |
| if (result != kSuccess) { |
| FX_LOGF(INFO, embedder::kLogTag, "FlutterEngineSendKeyEvent failed for the %s channel.", |
| embedder::kKeyDataChannel); |
| } |
| }, |
| /*platform_dispatch_callback: [flutter/keyevent, flutter/textinput] */ |
| [&embedder](const FlutterPlatformMessage* message) { |
| // Send platform message to the engine. |
| FlutterEngineResult result = FlutterEngineSendPlatformMessage(embedder.engine, message); |
| |
| if (result != kSuccess) { |
| FX_LOGF(INFO, embedder::kLogTag, |
| "FlutterEngineSendPlatformMessage failed for the %s channel.", message->channel); |
| } |
| }, |
| response_handle); |
| |
| embedder::FlatlandViewProvider view_provider(embedder.flatland_connection.get(), |
| std::move(view_ref_pair), |
| std::move(flatland_view_protocols)); |
| |
| auto send_pointer_events_to_engine = [&embedder](std::vector<FlutterPointerEvent> events) { |
| if (events.empty()) |
| return; |
| FlutterEngineSendPointerEvent(embedder.engine, &events[0], events.size()); |
| }; |
| |
| auto mouse_delegate = std::make_unique<embedder::MouseDelegate>(std::move(mouse_source)); |
| auto touch_delegate = std::make_unique<embedder::TouchDelegate>(std::move(touch_source)); |
| mouse_delegate->WatchLoop(send_pointer_events_to_engine); |
| touch_delegate->WatchLoop(send_pointer_events_to_engine); |
| |
| // Return to ViewProvider Initialization |
| fidl::BindingSet<fuchsia::ui::app::ViewProvider> view_provider_bindings; |
| status = embedder.component_context->outgoing()->AddPublicService<fuchsia::ui::app::ViewProvider>( |
| [&view_provider, |
| &view_provider_bindings](fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) { |
| view_provider_bindings.AddBinding(&view_provider, std::move(request)); |
| }); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to add ViewProvider service: %s", |
| zx_status_get_string(status)); |
| return EXIT_FAILURE; |
| } |
| |
| // Our run loop's dispatcher must be used to serve the component |
| // context in order for requests' handlers to get called while |
| // we're looping. |
| status = embedder.component_context->outgoing()->ServeFromStartupInfo(); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to serve component context: %s", |
| zx_status_get_string(status)); |
| return EXIT_FAILURE; |
| } |
| |
| // Set up sysmem allocator for software rendering. |
| status = fdio_service_connect("/svc/fuchsia.sysmem.Allocator", |
| embedder.sysmem_allocator.NewRequest().TakeChannel().release()); |
| embedder.sysmem_allocator->SetDebugClientInfo(embedder::GetCurrentProcessName(), |
| embedder::GetCurrentProcessId()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to fuchsia.sysmem.Allocator: %s", |
| zx_status_get_string(status)); |
| return EXIT_FAILURE; |
| } |
| |
| // Set up Flatland allocator for software rendering. |
| status = fdio_service_connect("/svc/fuchsia.ui.composition.Allocator", |
| embedder.flatland_allocator.NewRequest().TakeChannel().release()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to fuchsia.ui.composition.Allocator: %s", |
| zx_status_get_string(status)); |
| return EXIT_FAILURE; |
| } |
| |
| embedder::AccessibilityBridge::SetSemanticsEnabledCallback set_semantics_enabled_callback = |
| [&embedder](bool enabled) { FlutterEngineUpdateSemanticsEnabled(embedder.engine, enabled); }; |
| |
| embedder::AccessibilityBridge::DispatchSemanticsActionCallback |
| dispatch_semantics_action_callback = |
| [&embedder](int32_t node_id, FlutterSemanticsAction action) { |
| FlutterEngineDispatchSemanticsAction(embedder.engine, node_id, action, nullptr, 0); |
| }; |
| |
| const std::string accessibility_inspect_name = |
| std::to_string(embedder::GetKoid(accessibility_view_ref)); |
| |
| // Connect to SemanticsManager service. |
| fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager; |
| |
| status = embedder.component_context->svc() |
| ->Connect<fuchsia::accessibility::semantics::SemanticsManager>( |
| semantics_manager.NewRequest()); |
| |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, |
| "fuchsia::accessibility::semantics::SemanticsManager connection failed: %s", |
| zx_status_get_string(status)); |
| } |
| |
| dart_utils::RootInspectNode::Initialize(embedder.component_context.get()); |
| |
| embedder.accessibility_bridge_ = std::make_unique<embedder::AccessibilityBridge>( |
| std::move(set_semantics_enabled_callback), std::move(dispatch_semantics_action_callback), |
| std::move(semantics_manager), std::move(accessibility_view_ref), |
| dart_utils::RootInspectNode::CreateRootChild(std::move(accessibility_inspect_name))); |
| |
| view_provider.SetPixelRatioCallback( |
| [&embedder](float ratio) { embedder.accessibility_bridge_->SetPixelRatio(ratio); }); |
| |
| // TODO(benbergkamp): temporary a workaround |
| FlutterEngineRunMessageLoop(); |
| |
| FX_LOG(INFO, embedder::kLogTag, "Done looping."); |
| return EXIT_SUCCESS; |
| } |