blob: fa0f43241946475f12ca1a4620a52ea40b58fd79 [file] [log] [blame]
// Copyright 2021 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.
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <fuchsia/web/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/zx/clock.h>
#include <lib/zx/time.h>
#include <zircon/errors.h>
#include <zircon/status.h>
#include <string>
#include <test/virtualkeyboard/cpp/fidl.h>
#include "src/lib/fsl/vmo/vector.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/json_parser/json_parser.h"
namespace {
fuchsia::mem::Buffer BufferFromString(const std::string& script) {
fuchsia::mem::Buffer buffer;
uint64_t num_bytes = script.size();
zx::vmo vmo;
zx_status_t status = zx::vmo::create(num_bytes, 0u, &vmo);
FX_CHECK(status >= 0);
status = vmo.write(script.data(), 0, num_bytes);
FX_CHECK(status >= 0);
buffer.vmo = std::move(vmo);
buffer.size = num_bytes;
return buffer;
}
std::string StringFromBuffer(const fuchsia::mem::Buffer& buffer) {
size_t num_bytes = buffer.size;
std::string str(num_bytes, 'x');
buffer.vmo.read(str.data(), 0, num_bytes);
return str;
}
class NavListener : public fuchsia::web::NavigationEventListener {
public:
// |fuchsia::web::NavigationEventListener|
void OnNavigationStateChanged(fuchsia::web::NavigationState nav_state,
OnNavigationStateChangedCallback send_ack) override {
if (nav_state.has_url()) {
FX_LOGS(DEBUG) << "nav_state.url = " << nav_state.url();
}
if (nav_state.has_page_type()) {
FX_LOGS(DEBUG) << "nav_state.page_type = " << static_cast<size_t>(nav_state.page_type());
}
if (nav_state.has_is_main_document_loaded()) {
FX_LOGS(INFO) << "nav_state.is_main_document_loaded = "
<< nav_state.is_main_document_loaded();
}
send_ack();
}
};
// Implements a simple web app, which responds to touch events.
class WebApp : public fuchsia::ui::app::ViewProvider {
public:
WebApp()
: loop_(&kAsyncLoopConfigAttachToCurrentThread),
context_(sys::ComponentContext::CreateAndServeOutgoingDirectory()),
view_provider_binding_(this) {
SetupWebEngine();
SetupViewProvider();
}
void Run() {
FX_LOGS(INFO) << "Loading web app";
fuchsia::web::NavigationControllerPtr navigation_controller;
NavListener navigation_event_listener;
fidl::Binding<fuchsia::web::NavigationEventListener> navigation_event_listener_binding(
&navigation_event_listener);
bool is_app_loaded = false;
web_frame_->SetNavigationEventListener(navigation_event_listener_binding.NewBinding());
web_frame_->GetNavigationController(navigation_controller.NewRequest());
web_frame_->SetJavaScriptLogLevel(fuchsia::web::ConsoleLogLevel::DEBUG);
navigation_controller->LoadUrl("about:blank", fuchsia::web::LoadUrlParams(), [](auto result) {
if (result.is_err()) {
FX_LOGS(FATAL) << "Error while loading URL: " << static_cast<uint32_t>(result.err());
}
});
web_frame_->ExecuteJavaScript({"*"}, BufferFromString(kAppCode), [&is_app_loaded](auto result) {
if (result.is_err()) {
FX_LOGS(FATAL) << "Error while executing JavaScript: "
<< static_cast<uint32_t>(result.err());
} else {
FX_LOGS(INFO) << "App body loaded";
is_app_loaded = true;
}
});
FX_LOGS(INFO) << "Wait for app to load";
RunLoopUntil([&] { return is_app_loaded; });
// Plumb view to child.
FX_LOGS(INFO) << "Waiting for view creation args from parent";
// The client view could be attached by
// `CreateViewWithViewRef` or `CreateView2`, so we need to account for both
// possibilities here.
RunLoopUntil([&] { return view_token_.has_value() || create_view2_args_.has_value(); });
if (view_token_.has_value()) {
if (!view_ref_.has_value()) {
auto view_ref_pair = scenic::ViewRefPair::New();
view_ref_.emplace(std::move(view_ref_pair.view_ref));
view_ref_control_.emplace(std::move(view_ref_pair.control_ref));
}
web_frame_->CreateViewWithViewRef(std::move(*view_token_), std::move(*view_ref_control_),
std::move(*view_ref_));
} else {
// If we received `CreateView2Args`, use `CreateView2`.
web_frame_->CreateView2(std::move(*create_view2_args_));
}
FX_LOGS(INFO) << "Requesting input position";
fuchsia::web::MessagePortPtr input_position_port;
SendMessageToWebPage(input_position_port.NewRequest(), "GET_INPUT_POSITION");
FX_LOGS(INFO) << "Waiting for input position";
std::optional<rapidjson::Document> input_position;
input_position_port->ReceiveMessage([&input_position](auto web_message) {
input_position = json::JSONParser().ParseFromString(StringFromBuffer(web_message.data()),
"web-app-response");
});
RunLoopUntil([&] { return input_position.has_value(); });
// Validate structure of input position.
FX_LOGS(INFO) << "Return input position to test fixture";
const auto& input_pos = input_position.value();
for (const auto& element : {"left", "right", "top", "bottom"}) {
FX_CHECK(input_pos.HasMember(element)) << "HasMember failed for " << element;
// Apparently sometimes these values can be floating points too.
FX_CHECK(input_pos[element].IsNumber()) << "IsNumber failed for " << element;
}
// Relay position to parent.
test::virtualkeyboard::InputPositionListenerSyncPtr position_listener_proxy;
context_->svc()->Connect(position_listener_proxy.NewRequest());
position_listener_proxy->Notify(test::virtualkeyboard::BoundingBox{
.x0 = static_cast<uint32_t>(input_pos["left"].GetFloat()),
.y0 = static_cast<uint32_t>(input_pos["top"].GetFloat()),
.x1 = static_cast<uint32_t>(input_pos["right"].GetFloat()),
.y1 = static_cast<uint32_t>(input_pos["bottom"].GetFloat())});
loop_.Run();
}
private:
static constexpr auto kAppCode = R"JS(
console.info('injecting body');
// Create a page with a single input box.
// * When the user taps inside the input box (and the keyboard is currently hidden),
// web-engine should request the virtual keyboard be made visible.
// * When the user taps outside the input box (and the keyboard is currently visible),
// web-engine should request the virtual keyboard me made hidden.
document.write('<html><body><input id="textbox" /></body></html>');
document.body.style.backgroundColor='#ff00ff';
document.body.onclick = function(event) {
document.body.style.backgroundColor='#40e0d0';
let touch_event = JSON.stringify({
x: event.screenX,
y: event.screenY,
});
console.info('Got touch event ', touch_event);
};
function receiveMessage(event) {
if (event.data == "GET_INPUT_POSITION") {
let message = JSON.stringify(document.getElementById('textbox').getBoundingClientRect());
console.info('sending input position ', message);
event.ports[0].postMessage(message);
} else {
console.error('ignoring unexpected message: ' + event.data);
}
};
window.addEventListener('message', receiveMessage, false);
)JS";
void SetupWebEngine() {
auto web_context_provider = context_->svc()->Connect<fuchsia::web::ContextProvider>();
auto incoming_service_clone = context_->svc()->CloneChannel();
web_context_provider.set_error_handler([](zx_status_t status) {
FX_LOGS(WARNING) << "web_context_provider: " << zx_status_get_string(status);
});
FX_CHECK(incoming_service_clone.is_valid());
fuchsia::web::CreateContextParams params;
params.set_service_directory(std::move(incoming_service_clone));
params.set_features(fuchsia::web::ContextFeatureFlags::KEYBOARD |
fuchsia::web::ContextFeatureFlags::VIRTUAL_KEYBOARD |
fuchsia::web::ContextFeatureFlags::NETWORK);
web_context_provider->Create(std::move(params), web_context_.NewRequest());
web_context_.set_error_handler([](zx_status_t status) {
FX_LOGS(WARNING) << "web_context_: " << zx_status_get_string(status);
});
web_context_->CreateFrame(web_frame_.NewRequest());
web_frame_.set_error_handler([](zx_status_t status) {
FX_LOGS(WARNING) << "web_frame_: " << zx_status_get_string(status);
});
}
void SetupViewProvider() {
fidl::InterfaceRequestHandler<fuchsia::ui::app::ViewProvider> handler =
[&](fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) {
if (view_provider_binding_.is_bound()) {
request.Close(ZX_ERR_ALREADY_BOUND);
return;
}
view_provider_binding_.Bind(std::move(request));
};
context_->outgoing()->AddPublicService(std::move(handler));
}
// |fuchsia::ui::app::ViewProvider|
void CreateViewWithViewRef(zx::eventpair token,
fuchsia::ui::views::ViewRefControl view_ref_control,
fuchsia::ui::views::ViewRef view_ref) override {
view_token_ = scenic::ToViewToken(std::move(token));
view_ref_control_ = std::move(view_ref_control);
view_ref_ = std::move(view_ref);
}
// |fuchsia::ui::app::ViewProvider|
void CreateView2(fuchsia::ui::app::CreateView2Args args) override {
fuchsia::ui::views::ViewCreationToken token;
create_view2_args_.emplace();
create_view2_args_->set_view_creation_token(std::move(*args.mutable_view_creation_token()));
}
void SendMessageToWebPage(fidl::InterfaceRequest<fuchsia::web::MessagePort> message_port,
const std::string& message) {
fuchsia::web::WebMessage web_message;
web_message.set_data(BufferFromString(message));
std::vector<fuchsia::web::OutgoingTransferable> outgoing;
outgoing.emplace_back(
fuchsia::web::OutgoingTransferable::WithMessagePort(std::move(message_port)));
web_message.set_outgoing_transfer(std::move(outgoing));
web_frame_->PostMessage(/*target_origin=*/"*", std::move(web_message),
[](auto result) { FX_CHECK(!result.is_err()); });
}
template <typename PredicateT>
void RunLoopUntil(PredicateT predicate) {
while (!predicate()) {
loop_.Run(zx::time::infinite(), true);
}
}
async::Loop loop_;
std::unique_ptr<sys::ComponentContext> context_;
fidl::Binding<fuchsia::ui::app::ViewProvider> view_provider_binding_;
fuchsia::web::ContextPtr web_context_;
fuchsia::web::FramePtr web_frame_;
// CreateView / CreateViewWithViewRef args.
std::optional<fuchsia::ui::views::ViewToken> view_token_;
std::optional<fuchsia::ui::views::ViewRef> view_ref_;
std::optional<fuchsia::ui::views::ViewRefControl> view_ref_control_;
// CreateView2 args.
std::optional<fuchsia::web::CreateView2Args> create_view2_args_;
};
} // namespace
int main(int argc, const char** argv) { WebApp().Run(); }