blob: bccbacbd7b69b2ee7aa51e8878b064e2ff1f3e6a [file] [log] [blame]
// Copyright 2015 The Chromium 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 "content/browser/devtools/protocol/emulation_handler.h"
#include <utility>
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "content/browser/devtools/devtools_session.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/input/touch_emulator.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/view_messages.h"
#include "content/public/common/url_constants.h"
#include "device/geolocation/public/cpp/geoposition.h"
#include "net/http/http_util.h"
#include "services/device/public/mojom/geolocation_context.mojom.h"
#include "services/device/public/mojom/geoposition.mojom.h"
#include "ui/events/gesture_detection/gesture_provider_config_helper.h"
namespace content {
namespace protocol {
namespace {
blink::WebScreenOrientationType WebScreenOrientationTypeFromString(
const std::string& type) {
if (type == Emulation::ScreenOrientation::TypeEnum::PortraitPrimary)
return blink::kWebScreenOrientationPortraitPrimary;
if (type == Emulation::ScreenOrientation::TypeEnum::PortraitSecondary)
return blink::kWebScreenOrientationPortraitSecondary;
if (type == Emulation::ScreenOrientation::TypeEnum::LandscapePrimary)
return blink::kWebScreenOrientationLandscapePrimary;
if (type == Emulation::ScreenOrientation::TypeEnum::LandscapeSecondary)
return blink::kWebScreenOrientationLandscapeSecondary;
return blink::kWebScreenOrientationUndefined;
}
ui::GestureProviderConfigType TouchEmulationConfigurationToType(
const std::string& protocol_value) {
ui::GestureProviderConfigType result =
ui::GestureProviderConfigType::CURRENT_PLATFORM;
if (protocol_value ==
Emulation::SetEmitTouchEventsForMouse::ConfigurationEnum::Mobile) {
result = ui::GestureProviderConfigType::GENERIC_MOBILE;
}
if (protocol_value ==
Emulation::SetEmitTouchEventsForMouse::ConfigurationEnum::Desktop) {
result = ui::GestureProviderConfigType::GENERIC_DESKTOP;
}
return result;
}
} // namespace
EmulationHandler::EmulationHandler()
: DevToolsDomainHandler(Emulation::Metainfo::domainName),
touch_emulation_enabled_(false),
device_emulation_enabled_(false),
host_(nullptr) {
}
EmulationHandler::~EmulationHandler() {
}
// static
std::vector<EmulationHandler*> EmulationHandler::ForAgentHost(
DevToolsAgentHostImpl* host) {
return DevToolsSession::HandlersForAgentHost<EmulationHandler>(
host, Emulation::Metainfo::domainName);
}
void EmulationHandler::SetRenderer(int process_host_id,
RenderFrameHostImpl* frame_host) {
if (host_ == frame_host)
return;
host_ = frame_host;
if (touch_emulation_enabled_)
UpdateTouchEventEmulationState();
UpdateDeviceEmulationState();
}
void EmulationHandler::Wire(UberDispatcher* dispatcher) {
Emulation::Dispatcher::wire(dispatcher, this);
}
Response EmulationHandler::Disable() {
if (touch_emulation_enabled_) {
touch_emulation_enabled_ = false;
UpdateTouchEventEmulationState();
}
user_agent_ = std::string();
device_emulation_enabled_ = false;
UpdateDeviceEmulationState();
return Response::OK();
}
Response EmulationHandler::SetGeolocationOverride(
Maybe<double> latitude, Maybe<double> longitude, Maybe<double> accuracy) {
if (!GetWebContents())
return Response::InternalError();
auto* geolocation_context = GetWebContents()->GetGeolocationContext();
auto geoposition = device::mojom::Geoposition::New();
if (latitude.isJust() && longitude.isJust() && accuracy.isJust()) {
geoposition->latitude = latitude.fromJust();
geoposition->longitude = longitude.fromJust();
geoposition->accuracy = accuracy.fromJust();
geoposition->timestamp = base::Time::Now();
if (!device::ValidateGeoposition(*geoposition))
return Response::Error("Invalid geolocation");
} else {
geoposition->error_code =
device::mojom::Geoposition::ErrorCode::POSITION_UNAVAILABLE;
}
geolocation_context->SetOverride(std::move(geoposition));
return Response::OK();
}
Response EmulationHandler::ClearGeolocationOverride() {
if (!GetWebContents())
return Response::InternalError();
auto* geolocation_context = GetWebContents()->GetGeolocationContext();
geolocation_context->ClearOverride();
return Response::OK();
}
Response EmulationHandler::SetEmitTouchEventsForMouse(
bool enabled,
Maybe<std::string> configuration) {
touch_emulation_enabled_ = enabled;
touch_emulation_configuration_ = configuration.fromMaybe("");
UpdateTouchEventEmulationState();
return Response::OK();
}
Response EmulationHandler::CanEmulate(bool* result) {
#if defined(OS_ANDROID)
*result = false;
#else
*result = true;
if (WebContentsImpl* web_contents = GetWebContents())
*result &= !web_contents->GetVisibleURL().SchemeIs(kChromeDevToolsScheme);
if (host_ && host_->GetRenderWidgetHost())
*result &= !host_->GetRenderWidgetHost()->auto_resize_enabled();
#endif // defined(OS_ANDROID)
return Response::OK();
}
Response EmulationHandler::SetDeviceMetricsOverride(
int width,
int height,
double device_scale_factor,
bool mobile,
Maybe<double> scale,
Maybe<int> screen_width,
Maybe<int> screen_height,
Maybe<int> position_x,
Maybe<int> position_y,
Maybe<bool> dont_set_visible_size,
Maybe<Emulation::ScreenOrientation> screen_orientation,
Maybe<protocol::Page::Viewport> viewport) {
const static int max_size = 10000000;
const static double max_scale = 10;
const static int max_orientation_angle = 360;
RenderWidgetHostImpl* widget_host =
host_ ? host_->GetRenderWidgetHost() : nullptr;
if (!widget_host)
return Response::Error("Target does not support metrics override");
if (screen_width.fromMaybe(0) < 0 || screen_height.fromMaybe(0) < 0 ||
screen_width.fromMaybe(0) > max_size ||
screen_height.fromMaybe(0) > max_size) {
return Response::InvalidParams(
"Screen width and height values must be positive, not greater than " +
base::IntToString(max_size));
}
if (position_x.fromMaybe(0) < 0 || position_y.fromMaybe(0) < 0 ||
position_x.fromMaybe(0) > screen_width.fromMaybe(0) ||
position_y.fromMaybe(0) > screen_height.fromMaybe(0)) {
return Response::InvalidParams("View position should be on the screen");
}
if (width < 0 || height < 0 || width > max_size || height > max_size) {
return Response::InvalidParams(
"Width and height values must be positive, not greater than " +
base::IntToString(max_size));
}
if (device_scale_factor < 0)
return Response::InvalidParams("deviceScaleFactor must be non-negative");
if (scale.fromMaybe(1) <= 0 || scale.fromMaybe(1) > max_scale) {
return Response::InvalidParams("scale must be positive, not greater than " +
base::NumberToString(max_scale));
}
blink::WebScreenOrientationType orientationType =
blink::kWebScreenOrientationUndefined;
int orientationAngle = 0;
if (screen_orientation.isJust()) {
Emulation::ScreenOrientation* orientation = screen_orientation.fromJust();
orientationType = WebScreenOrientationTypeFromString(
orientation->GetType());
if (orientationType == blink::kWebScreenOrientationUndefined)
return Response::InvalidParams("Invalid screen orientation type value");
orientationAngle = orientation->GetAngle();
if (orientationAngle < 0 || orientationAngle >= max_orientation_angle) {
return Response::InvalidParams(
"Screen orientation angle must be non-negative, less than " +
base::IntToString(max_orientation_angle));
}
}
blink::WebDeviceEmulationParams params;
params.screen_position = mobile ? blink::WebDeviceEmulationParams::kMobile
: blink::WebDeviceEmulationParams::kDesktop;
params.screen_size =
blink::WebSize(screen_width.fromMaybe(0), screen_height.fromMaybe(0));
if (position_x.isJust() && position_y.isJust()) {
params.view_position =
blink::WebPoint(position_x.fromMaybe(0), position_y.fromMaybe(0));
}
params.device_scale_factor = device_scale_factor;
params.view_size = blink::WebSize(width, height);
params.scale = scale.fromMaybe(1);
params.screen_orientation_type = orientationType;
params.screen_orientation_angle = orientationAngle;
if (viewport.isJust()) {
params.viewport_offset.x = viewport.fromJust()->GetX();
params.viewport_offset.y = viewport.fromJust()->GetY();
ScreenInfo screen_info;
widget_host->GetScreenInfo(&screen_info);
double dpfactor = device_scale_factor ? device_scale_factor /
screen_info.device_scale_factor
: 1;
params.viewport_scale = viewport.fromJust()->GetScale() * dpfactor;
// Resize the RenderWidgetHostView to the size of the overridden viewport.
width = gfx::ToRoundedInt(viewport.fromJust()->GetWidth() *
params.viewport_scale);
height = gfx::ToRoundedInt(viewport.fromJust()->GetHeight() *
params.viewport_scale);
}
bool size_changed = false;
if (!dont_set_visible_size.fromMaybe(false) && width > 0 && height > 0) {
if (GetWebContents()) {
size_changed =
GetWebContents()->SetDeviceEmulationSize(gfx::Size(width, height));
} else {
return Response::Error("Can't find the associated web contents");
}
}
if (device_emulation_enabled_ && params == device_emulation_params_) {
// Renderer should answer after size was changed, so that the response is
// only sent to the client once updates were applied.
if (size_changed)
return Response::FallThrough();
return Response::OK();
}
device_emulation_enabled_ = true;
device_emulation_params_ = params;
UpdateDeviceEmulationState();
// Renderer should answer after emulation params were updated, so that the
// response is only sent to the client once updates were applied.
// Unless the renderer has crashed.
if (GetWebContents() && GetWebContents()->IsCrashed())
return Response::OK();
return Response::FallThrough();
}
Response EmulationHandler::ClearDeviceMetricsOverride() {
if (!device_emulation_enabled_)
return Response::OK();
if (GetWebContents())
GetWebContents()->ClearDeviceEmulationSize();
else
return Response::Error("Can't find the associated web contents");
device_emulation_enabled_ = false;
device_emulation_params_ = blink::WebDeviceEmulationParams();
UpdateDeviceEmulationState();
// Renderer should answer after emulation was disabled, so that the response
// is only sent to the client once updates were applied.
// Unless the renderer has crashed.
if (GetWebContents() && GetWebContents()->IsCrashed())
return Response::OK();
return Response::FallThrough();
}
Response EmulationHandler::SetVisibleSize(int width, int height) {
if (width < 0 || height < 0)
return Response::InvalidParams("Width and height must be non-negative");
if (GetWebContents())
GetWebContents()->SetDeviceEmulationSize(gfx::Size(width, height));
else
return Response::Error("Can't find the associated web contents");
return Response::OK();
}
Response EmulationHandler::SetUserAgentOverride(
const std::string& user_agent,
Maybe<std::string> accept_language,
Maybe<std::string> platform) {
if (!user_agent.empty() && !net::HttpUtil::IsValidHeaderValue(user_agent))
return Response::InvalidParams("Invalid characters found in userAgent");
std::string accept_lang = accept_language.fromMaybe(std::string());
if (!accept_lang.empty() && !net::HttpUtil::IsValidHeaderValue(accept_lang)) {
return Response::InvalidParams(
"Invalid characters found in acceptLanguage");
}
user_agent_ = user_agent;
accept_language_ = accept_lang;
return Response::FallThrough();
}
blink::WebDeviceEmulationParams EmulationHandler::GetDeviceEmulationParams() {
return device_emulation_params_;
}
void EmulationHandler::SetDeviceEmulationParams(
const blink::WebDeviceEmulationParams& params) {
bool enabled = params != blink::WebDeviceEmulationParams();
device_emulation_enabled_ = enabled;
device_emulation_params_ = params;
UpdateDeviceEmulationState();
}
WebContentsImpl* EmulationHandler::GetWebContents() {
return host_ ?
static_cast<WebContentsImpl*>(WebContents::FromRenderFrameHost(host_)) :
nullptr;
}
void EmulationHandler::UpdateTouchEventEmulationState() {
if (!host_ || !host_->GetRenderWidgetHost())
return;
if (host_->GetParent() && !host_->IsCrossProcessSubframe())
return;
// We only have a single TouchEmulator for all frames, so let the main frame's
// EmulationHandler enable/disable it.
if (!host_->frame_tree_node()->IsMainFrame())
return;
if (touch_emulation_enabled_) {
if (auto* touch_emulator =
host_->GetRenderWidgetHost()->GetTouchEmulator()) {
touch_emulator->Enable(
TouchEmulator::Mode::kEmulatingTouchFromMouse,
TouchEmulationConfigurationToType(touch_emulation_configuration_));
}
} else {
if (auto* touch_emulator = host_->GetRenderWidgetHost()->GetTouchEmulator())
touch_emulator->Disable();
}
if (GetWebContents()) {
GetWebContents()->SetForceDisableOverscrollContent(
touch_emulation_enabled_);
}
}
void EmulationHandler::UpdateDeviceEmulationState() {
if (!host_ || !host_->GetRenderWidgetHost())
return;
if (host_->GetParent() && !host_->IsCrossProcessSubframe())
return;
// TODO(eseckler): Once we change this to mojo, we should wait for an ack to
// these messages from the renderer. The renderer should send the ack once the
// emulation params were applied. That way, we can avoid having to handle
// Set/ClearDeviceMetricsOverride in the renderer. With the old IPC system,
// this is tricky since we'd have to track the DevTools message id with the
// ViewMsg and acknowledgment, as well as plump the acknowledgment back to the
// EmulationHandler somehow. Mojo callbacks should make this much simpler.
if (device_emulation_enabled_) {
host_->GetRenderWidgetHost()->Send(new ViewMsg_EnableDeviceEmulation(
host_->GetRenderWidgetHost()->GetRoutingID(),
device_emulation_params_));
} else {
host_->GetRenderWidgetHost()->Send(new ViewMsg_DisableDeviceEmulation(
host_->GetRenderWidgetHost()->GetRoutingID()));
}
}
void EmulationHandler::ApplyOverrides(net::HttpRequestHeaders* headers) {
if (!user_agent_.empty())
headers->SetHeader(net::HttpRequestHeaders::kUserAgent, user_agent_);
if (!accept_language_.empty()) {
headers->SetHeader(
net::HttpRequestHeaders::kAcceptLanguage,
net::HttpUtil::GenerateAcceptLanguageHeader(accept_language_));
}
}
} // namespace protocol
} // namespace content