blob: 856352e0bcbb9db3ea978e8caac864d5984c9f37 [file] [log] [blame]
// Copyright 2014 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/page_handler.h"
#include <string>
#include "base/base64.h"
#include "base/bind.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/devtools/protocol/color_picker.h"
#include "content/browser/devtools/protocol/usage_and_quota_query.h"
#include "content/browser/geolocation/geolocation_dispatcher_host.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/view_messages.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/javascript_dialog_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/referrer.h"
#include "content/public/common/url_constants.h"
#include "storage/browser/quota/quota_manager.h"
#include "third_party/WebKit/public/platform/WebScreenInfo.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/page_transition_types.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/size_conversions.h"
#include "ui/snapshot/snapshot.h"
#include "url/gurl.h"
namespace content {
namespace devtools {
namespace page {
namespace {
static const char kPng[] = "png";
static const char kJpeg[] = "jpeg";
static int kDefaultScreenshotQuality = 80;
static int kFrameRateThresholdMs = 100;
static int kCaptureRetryLimit = 2;
void QueryUsageAndQuotaCompletedOnIOThread(
const UsageAndQuotaQuery::Callback& callback,
scoped_ptr<QueryUsageAndQuotaResponse> response) {
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
base::Bind(callback, base::Passed(&response)));
}
void QueryUsageAndQuotaOnIOThread(
scoped_refptr<storage::QuotaManager> quota_manager,
const GURL& security_origin,
const UsageAndQuotaQuery::Callback& callback) {
new UsageAndQuotaQuery(
quota_manager,
security_origin,
base::Bind(&QueryUsageAndQuotaCompletedOnIOThread,
callback));
}
} // namespace
typedef DevToolsProtocolClient::Response Response;
PageHandler::PageHandler()
: enabled_(false),
touch_emulation_enabled_(false),
screencast_enabled_(false),
screencast_quality_(kDefaultScreenshotQuality),
screencast_max_width_(-1),
screencast_max_height_(-1),
capture_retry_count_(0),
has_last_compositor_frame_metadata_(false),
color_picker_(new ColorPicker(base::Bind(
&PageHandler::OnColorPicked, base::Unretained(this)))),
host_(nullptr),
weak_factory_(this) {
}
PageHandler::~PageHandler() {
}
void PageHandler::SetRenderViewHost(RenderViewHostImpl* host) {
if (host_ == host)
return;
color_picker_->SetRenderViewHost(host);
host_ = host;
UpdateTouchEventEmulationState();
}
void PageHandler::SetClient(scoped_ptr<Client> client) {
client_.swap(client);
}
void PageHandler::Detached() {
Disable();
}
void PageHandler::OnSwapCompositorFrame(
const cc::CompositorFrameMetadata& frame_metadata) {
last_compositor_frame_metadata_ = frame_metadata;
has_last_compositor_frame_metadata_ = true;
if (screencast_enabled_)
InnerSwapCompositorFrame();
color_picker_->OnSwapCompositorFrame();
}
void PageHandler::OnVisibilityChanged(bool visible) {
if (!screencast_enabled_)
return;
NotifyScreencastVisibility(visible);
}
void PageHandler::DidAttachInterstitialPage() {
if (!enabled_)
return;
InterstitialShownParams params;
client_->InterstitialShown(params);
}
void PageHandler::DidDetachInterstitialPage() {
if (!enabled_)
return;
InterstitialHiddenParams params;
client_->InterstitialHidden(params);
}
Response PageHandler::Enable() {
enabled_ = true;
return Response::FallThrough();
}
Response PageHandler::Disable() {
enabled_ = false;
touch_emulation_enabled_ = false;
screencast_enabled_ = false;
UpdateTouchEventEmulationState();
color_picker_->SetEnabled(false);
return Response::FallThrough();
}
Response PageHandler::Reload(const bool* ignoreCache,
const std::string* script_to_evaluate_on_load,
const std::string* script_preprocessor) {
if (!host_)
return Response::InternalError("Could not connect to view");
WebContents* web_contents = WebContents::FromRenderViewHost(host_);
if (!web_contents)
return Response::InternalError("No WebContents to reload");
// Handle in browser only if it is crashed.
if (!web_contents->IsCrashed())
return Response::FallThrough();
web_contents->GetController().Reload(false);
return Response::OK();
}
Response PageHandler::Navigate(const std::string& url,
FrameId* frame_id) {
GURL gurl(url);
if (!gurl.is_valid())
return Response::InternalError("Cannot navigate to invalid URL");
if (!host_)
return Response::InternalError("Could not connect to view");
WebContents* web_contents = WebContents::FromRenderViewHost(host_);
if (!web_contents)
return Response::InternalError("No WebContents to navigate");
web_contents->GetController()
.LoadURL(gurl, Referrer(), ui::PAGE_TRANSITION_TYPED, std::string());
return Response::FallThrough();
}
Response PageHandler::GetNavigationHistory(
int* current_index,
std::vector<NavigationEntry>* entries) {
if (!host_)
return Response::InternalError("Could not connect to view");
WebContents* web_contents = WebContents::FromRenderViewHost(host_);
if (!web_contents)
return Response::InternalError("No WebContents to navigate");
NavigationController& controller = web_contents->GetController();
*current_index = controller.GetCurrentEntryIndex();
for (int i = 0; i != controller.GetEntryCount(); ++i) {
NavigationEntry entry;
entry.set_id(controller.GetEntryAtIndex(i)->GetUniqueID());
entry.set_url(controller.GetEntryAtIndex(i)->GetURL().spec());
entry.set_title(
base::UTF16ToUTF8(controller.GetEntryAtIndex(i)->GetTitle()));
entries->push_back(entry);
}
return Response::OK();
}
Response PageHandler::NavigateToHistoryEntry(int entry_id) {
if (!host_)
return Response::InternalError("Could not connect to view");
WebContents* web_contents = WebContents::FromRenderViewHost(host_);
if (!web_contents)
return Response::InternalError("No WebContents to navigate");
NavigationController& controller = web_contents->GetController();
for (int i = 0; i != controller.GetEntryCount(); ++i) {
if (controller.GetEntryAtIndex(i)->GetUniqueID() == entry_id) {
controller.GoToIndex(i);
return Response::OK();
}
}
return Response::InvalidParams("No entry with passed id");
}
Response PageHandler::SetGeolocationOverride(double* latitude,
double* longitude,
double* accuracy) {
if (!host_)
return Response::InternalError("Could not connect to view");
WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
WebContents::FromRenderViewHost(host_));
if (!web_contents)
return Response::InternalError("No WebContents to override");
GeolocationDispatcherHost* geolocation_host =
web_contents->geolocation_dispatcher_host();
scoped_ptr<Geoposition> geoposition(new Geoposition());
if (latitude && longitude && accuracy) {
geoposition->latitude = *latitude;
geoposition->longitude = *longitude;
geoposition->accuracy = *accuracy;
geoposition->timestamp = base::Time::Now();
if (!geoposition->Validate()) {
return Response::InternalError("Invalid geolocation");
}
} else {
geoposition->error_code = Geoposition::ERROR_CODE_POSITION_UNAVAILABLE;
}
geolocation_host->SetOverride(geoposition.Pass());
return Response::OK();
}
Response PageHandler::ClearGeolocationOverride() {
if (!host_)
return Response::InternalError("Could not connect to view");
WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
WebContents::FromRenderViewHost(host_));
if (!web_contents)
return Response::InternalError("No WebContents to override");
GeolocationDispatcherHost* geolocation_host =
web_contents->geolocation_dispatcher_host();
geolocation_host->ClearOverride();
return Response::OK();
}
Response PageHandler::SetTouchEmulationEnabled(bool enabled) {
touch_emulation_enabled_ = enabled;
UpdateTouchEventEmulationState();
return Response::FallThrough();
}
scoped_refptr<DevToolsProtocol::Response> PageHandler::CaptureScreenshot(
scoped_refptr<DevToolsProtocol::Command> command) {
if (!host_ || !host_->GetView())
return command->InternalErrorResponse("Could not connect to view");
host_->GetSnapshotFromBrowser(
base::Bind(&PageHandler::ScreenshotCaptured,
weak_factory_.GetWeakPtr(), command));
return command->AsyncResponsePromise();
}
Response PageHandler::CanScreencast(bool* result) {
#if defined(OS_ANDROID)
*result = true;
#else
*result = false;
#endif // defined(OS_ANDROID)
return Response::OK();
}
Response PageHandler::CanEmulate(bool* result) {
#if defined(OS_ANDROID)
*result = false;
#else
if (host_) {
if (WebContents* web_contents = WebContents::FromRenderViewHost(host_)) {
*result = !web_contents->GetVisibleURL().SchemeIs(kChromeDevToolsScheme);
} else {
*result = true;
}
} else {
*result = true;
}
#endif // defined(OS_ANDROID)
return Response::OK();
}
Response PageHandler::StartScreencast(const std::string* format,
const int* quality,
const int* max_width,
const int* max_height) {
if (!host_)
return Response::InternalError("Could not connect to view");
screencast_enabled_ = true;
screencast_format_ = format ? *format : kPng;
screencast_quality_ = quality ? *quality : kDefaultScreenshotQuality;
if (screencast_quality_ < 0 || screencast_quality_ > 100)
screencast_quality_ = kDefaultScreenshotQuality;
screencast_max_width_ = max_width ? *max_width : -1;
screencast_max_height_ = max_height ? *max_height : -1;
UpdateTouchEventEmulationState();
bool visible = !host_->is_hidden();
NotifyScreencastVisibility(visible);
if (visible) {
if (has_last_compositor_frame_metadata_)
InnerSwapCompositorFrame();
else
host_->Send(new ViewMsg_ForceRedraw(host_->GetRoutingID(), 0));
}
return Response::FallThrough();
}
Response PageHandler::StopScreencast() {
last_frame_time_ = base::TimeTicks();
screencast_enabled_ = false;
UpdateTouchEventEmulationState();
return Response::FallThrough();
}
Response PageHandler::HandleJavaScriptDialog(bool accept,
const std::string* prompt_text) {
base::string16 prompt_override;
if (prompt_text)
prompt_override = base::UTF8ToUTF16(*prompt_text);
if (!host_)
return Response::InternalError("Could not connect to view");
WebContents* web_contents = WebContents::FromRenderViewHost(host_);
if (!web_contents)
return Response::InternalError("No JavaScript dialog to handle");
JavaScriptDialogManager* manager =
web_contents->GetDelegate()->GetJavaScriptDialogManager();
if (manager && manager->HandleJavaScriptDialog(
web_contents, accept, prompt_text ? &prompt_override : nullptr)) {
return Response::OK();
}
return Response::InternalError("Could not handle JavaScript dialog");
}
scoped_refptr<DevToolsProtocol::Response> PageHandler::QueryUsageAndQuota(
const std::string& security_origin,
scoped_refptr<DevToolsProtocol::Command> command) {
if (!host_)
return command->InternalErrorResponse("Could not connect to view");
scoped_refptr<storage::QuotaManager> quota_manager =
host_->GetProcess()->GetStoragePartition()->GetQuotaManager();
BrowserThread::PostTask(
BrowserThread::IO,
FROM_HERE,
base::Bind(&QueryUsageAndQuotaOnIOThread,
quota_manager,
GURL(security_origin),
base::Bind(&PageHandler::QueryUsageAndQuotaCompleted,
weak_factory_.GetWeakPtr(),
command)));
return command->AsyncResponsePromise();
}
Response PageHandler::SetColorPickerEnabled(bool enabled) {
if (!host_)
return Response::InternalError("Could not connect to view");
color_picker_->SetEnabled(enabled);
return Response::OK();
}
void PageHandler::UpdateTouchEventEmulationState() {
if (!host_)
return;
bool enabled = touch_emulation_enabled_ || screencast_enabled_;
host_->SetTouchEventEmulationEnabled(enabled);
WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
WebContents::FromRenderViewHost(host_));
if (web_contents)
web_contents->SetForceDisableOverscrollContent(enabled);
}
void PageHandler::NotifyScreencastVisibility(bool visible) {
if (visible)
capture_retry_count_ = kCaptureRetryLimit;
ScreencastVisibilityChangedParams params;
params.set_visible(visible);
client_->ScreencastVisibilityChanged(params);
}
void PageHandler::InnerSwapCompositorFrame() {
if ((base::TimeTicks::Now() - last_frame_time_).InMilliseconds() <
kFrameRateThresholdMs) {
return;
}
if (!host_ || !host_->GetView())
return;
last_frame_time_ = base::TimeTicks::Now();
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
host_->GetView());
// TODO(vkuzkokov): do not use previous frame metadata.
cc::CompositorFrameMetadata& metadata = last_compositor_frame_metadata_;
gfx::SizeF viewport_size_dip = gfx::ScaleSize(
metadata.scrollable_viewport_size, metadata.page_scale_factor);
gfx::SizeF screen_size_dip = gfx::ScaleSize(view->GetPhysicalBackingSize(),
1 / metadata.device_scale_factor);
blink::WebScreenInfo screen_info;
view->GetScreenInfo(&screen_info);
double device_scale_factor = screen_info.deviceScaleFactor;
double scale = 1;
if (screencast_max_width_ > 0) {
double max_width_dip = screencast_max_width_ / device_scale_factor;
scale = std::min(scale, max_width_dip / screen_size_dip.width());
}
if (screencast_max_height_ > 0) {
double max_height_dip = screencast_max_height_ / device_scale_factor;
scale = std::min(scale, max_height_dip / screen_size_dip.height());
}
if (scale <= 0)
scale = 0.1;
gfx::Size snapshot_size_dip(gfx::ToRoundedSize(
gfx::ScaleSize(viewport_size_dip, scale)));
if (snapshot_size_dip.width() > 0 && snapshot_size_dip.height() > 0) {
gfx::Rect viewport_bounds_dip(gfx::ToRoundedSize(viewport_size_dip));
view->CopyFromCompositingSurface(
viewport_bounds_dip,
snapshot_size_dip,
base::Bind(&PageHandler::ScreencastFrameCaptured,
weak_factory_.GetWeakPtr(),
screencast_format_,
screencast_quality_,
last_compositor_frame_metadata_),
kN32_SkColorType);
}
}
void PageHandler::ScreencastFrameCaptured(
const std::string& format,
int quality,
const cc::CompositorFrameMetadata& metadata,
bool success,
const SkBitmap& bitmap) {
if (!success) {
if (capture_retry_count_) {
--capture_retry_count_;
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&PageHandler::InnerSwapCompositorFrame,
weak_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kFrameRateThresholdMs));
}
return;
}
std::vector<unsigned char> data;
SkAutoLockPixels lock_image(bitmap);
bool encoded;
if (format == kPng) {
encoded = gfx::PNGCodec::Encode(
reinterpret_cast<unsigned char*>(bitmap.getAddr32(0, 0)),
gfx::PNGCodec::FORMAT_SkBitmap,
gfx::Size(bitmap.width(), bitmap.height()),
bitmap.width() * bitmap.bytesPerPixel(),
false, std::vector<gfx::PNGCodec::Comment>(), &data);
} else if (format == kJpeg) {
encoded = gfx::JPEGCodec::Encode(
reinterpret_cast<unsigned char*>(bitmap.getAddr32(0, 0)),
gfx::JPEGCodec::FORMAT_SkBitmap,
bitmap.width(),
bitmap.height(),
bitmap.width() * bitmap.bytesPerPixel(),
quality, &data);
} else {
encoded = false;
}
if (!encoded)
return;
std::string base_64_data;
base::Base64Encode(
base::StringPiece(reinterpret_cast<char*>(&data[0]), data.size()),
&base_64_data);
ScreencastFrameMetadata param_metadata;
// Consider metadata empty in case it has no device scale factor.
if (metadata.device_scale_factor != 0 && host_) {
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
host_->GetView());
if (!view)
return;
gfx::SizeF viewport_size_dip = gfx::ScaleSize(
metadata.scrollable_viewport_size, metadata.page_scale_factor);
gfx::SizeF screen_size_dip = gfx::ScaleSize(
view->GetPhysicalBackingSize(), 1 / metadata.device_scale_factor);
param_metadata.set_device_scale_factor(metadata.device_scale_factor);
param_metadata.set_page_scale_factor(metadata.page_scale_factor);
param_metadata.set_page_scale_factor_min(metadata.min_page_scale_factor);
param_metadata.set_page_scale_factor_max(metadata.max_page_scale_factor);
param_metadata.set_offset_top(
metadata.location_bar_content_translation.y());
param_metadata.set_offset_bottom(screen_size_dip.height() -
metadata.location_bar_content_translation.y() -
viewport_size_dip.height());
param_metadata.set_device_width(screen_size_dip.width());
param_metadata.set_device_height(screen_size_dip.height());
param_metadata.set_scroll_offset_x(metadata.root_scroll_offset.x());
param_metadata.set_scroll_offset_y(metadata.root_scroll_offset.y());
devtools::dom::Rect viewport;
viewport.set_x(metadata.root_scroll_offset.x());
viewport.set_y(metadata.root_scroll_offset.y());
viewport.set_width(metadata.scrollable_viewport_size.width());
viewport.set_height(metadata.scrollable_viewport_size.height());
param_metadata.set_viewport(viewport);
}
ScreencastFrameParams params;
params.set_data(base_64_data);
params.set_metadata(param_metadata);
client_->ScreencastFrame(params);
}
void PageHandler::ScreenshotCaptured(
scoped_refptr<DevToolsProtocol::Command> command,
const unsigned char* png_data,
size_t png_size) {
if (!png_data || !png_size) {
client_->SendInternalErrorResponse(command,
"Unable to capture screenshot");
return;
}
std::string base_64_data;
base::Base64Encode(
base::StringPiece(reinterpret_cast<const char*>(png_data), png_size),
&base_64_data);
CaptureScreenshotResponse response;
response.set_data(base_64_data);
client_->SendCaptureScreenshotResponse(command, response);
}
void PageHandler::OnColorPicked(int r, int g, int b, int a) {
dom::RGBA color;
color.set_r(r);
color.set_g(g);
color.set_b(b);
color.set_a(a);
ColorPickedParams params;
params.set_color(color);
client_->ColorPicked(params);
}
void PageHandler::QueryUsageAndQuotaCompleted(
scoped_refptr<DevToolsProtocol::Command> command,
scoped_ptr<QueryUsageAndQuotaResponse> response_data) {
client_->SendQueryUsageAndQuotaResponse(command, *response_data);
}
} // namespace page
} // namespace devtools
} // namespace content