blob: c7cd95c215403b804cd5b0c9898aee01dc538f93 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/thumbnails/thumbnail_tab_helper.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "base/task_scheduler/post_task.h"
#include "base/task_scheduler/task_traits.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/thumbnails/thumbnail_service.h"
#include "chrome/browser/thumbnails/thumbnail_service_factory.h"
#include "chrome/browser/thumbnails/thumbnail_utils.h"
#include "chrome/common/chrome_features.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/scrollbar_size.h"
using thumbnails::ThumbnailingContext;
namespace {
// The desired thumbnail size in DIP. Note that on 1x devices, we actually take
// thumbnails of twice that size.
const int kThumbnailWidth = 154;
const int kThumbnailHeight = 96;
void ComputeThumbnailScore(const SkBitmap& thumbnail,
scoped_refptr<ThumbnailingContext> context) {
base::TimeTicks process_bitmap_start_time = base::TimeTicks::Now();
context->score.boring_score = color_utils::CalculateBoringScore(thumbnail);
context->score.good_clipping =
thumbnails::IsGoodClipping(context->clip_result);
base::TimeDelta process_bitmap_time =
base::TimeTicks::Now() - process_bitmap_start_time;
UMA_HISTOGRAM_TIMES("Thumbnails.ProcessBitmapTime", process_bitmap_time);
}
} // namespace
DEFINE_WEB_CONTENTS_USER_DATA_KEY(ThumbnailTabHelper);
// Overview
// --------
// This class provides a service for updating thumbnails to be used in the
// "Most visited" section of the New Tab page. The process is started by
// UpdateThumbnailIfNecessary(), which updates the thumbnail for the current
// tab if needed. The heuristics to judge whether to update the thumbnail are
// implemented in ThumbnailService::ShouldAcquirePageThumbnail().
// There are several triggers that can start the process:
// - When a renderer is about to be hidden (this usually occurs when the current
// tab is closed or another tab is clicked).
// - If features::kCaptureThumbnailOnLoadFinished is enabled: When a page load
// finishes.
// - If features::kCaptureThumbnailOnNavigatingAway is enabled: Just before
// navigating away from the current page.
ThumbnailTabHelper::ThumbnailTabHelper(content::WebContents* contents)
: content::WebContentsObserver(contents),
capture_on_load_finished_(base::FeatureList::IsEnabled(
features::kCaptureThumbnailOnLoadFinished)),
capture_on_navigating_away_(base::FeatureList::IsEnabled(
features::kCaptureThumbnailOnNavigatingAway)),
page_transition_(ui::PAGE_TRANSITION_LINK),
load_interrupted_(false),
weak_factory_(this) {
// Even though we deal in RenderWidgetHosts, we only care about its
// subclass, RenderViewHost when it is in a tab. We don't make thumbnails
// for RenderViewHosts that aren't in tabs, or RenderWidgetHosts that
// aren't views like select popups.
registrar_.Add(this,
content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED,
content::Source<content::WebContents>(contents));
}
ThumbnailTabHelper::~ThumbnailTabHelper() = default;
void ThumbnailTabHelper::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
switch (type) {
case content::NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED:
RenderViewHostCreated(
content::Details<content::RenderViewHost>(details).ptr());
break;
case content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED:
if (!*content::Details<bool>(details).ptr())
WidgetHidden(content::Source<content::RenderWidgetHost>(source).ptr());
break;
default:
NOTREACHED() << "Unexpected notification type: " << type;
}
}
void ThumbnailTabHelper::RenderViewDeleted(
content::RenderViewHost* render_view_host) {
bool registered = registrar_.IsRegistered(
this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED,
content::Source<content::RenderWidgetHost>(
render_view_host->GetWidget()));
if (registered) {
registrar_.Remove(this,
content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED,
content::Source<content::RenderWidgetHost>(
render_view_host->GetWidget()));
}
}
void ThumbnailTabHelper::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
if (capture_on_navigating_away_) {
// At this point, the new navigation has just been started, but the
// WebContents still shows the previous page. Grab a thumbnail before it
// goes away.
UpdateThumbnailIfNecessary();
}
// Reset the page transition to some uninteresting type, since the actual
// type isn't available at this point. We'll get it in DidFinishNavigation
// (if that happens, which isn't guaranteed).
page_transition_ = ui::PAGE_TRANSITION_LINK;
}
void ThumbnailTabHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->HasCommitted() ||
!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
page_transition_ = navigation_handle->GetPageTransition();
}
void ThumbnailTabHelper::DidStartLoading() {
// TODO(treib): We should probably track whether the load *finished* - as it
// is, an in-progress load will count as not interrupted.
load_interrupted_ = false;
}
void ThumbnailTabHelper::DidStopLoading() {
if (capture_on_load_finished_) {
UpdateThumbnailIfNecessary();
}
}
void ThumbnailTabHelper::NavigationStopped() {
// This function gets called when the page loading is interrupted by the
// stop button.
load_interrupted_ = true;
}
void ThumbnailTabHelper::UpdateThumbnailIfNecessary() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Ignore thumbnail update requests if one is already in progress.
if (thumbnailing_context_) {
return;
}
// Destroying a WebContents may trigger it to be hidden, prompting a snapshot
// which would be unwise to attempt <http://crbug.com/130097>. If the
// WebContents is in the middle of destruction, do not risk it.
if (!web_contents() || web_contents()->IsBeingDestroyed()) {
return;
}
// Note: Do *not* use GetLastVisibleURL - it might already have been updated
// for a new pending navigation. The committed URL is the one corresponding
// to the currently visible content.
const GURL& url = web_contents()->GetLastCommittedURL();
if (!url.is_valid()) {
return;
}
Profile* profile =
Profile::FromBrowserContext(web_contents()->GetBrowserContext());
scoped_refptr<thumbnails::ThumbnailService> thumbnail_service =
ThumbnailServiceFactory::GetForProfile(profile);
// Skip if we don't need to update the thumbnail.
if (!thumbnail_service ||
!thumbnail_service->ShouldAcquirePageThumbnail(url, page_transition_)) {
return;
}
AsyncProcessThumbnail(thumbnail_service);
}
void ThumbnailTabHelper::AsyncProcessThumbnail(
scoped_refptr<thumbnails::ThumbnailService> thumbnail_service) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::RenderWidgetHost* render_widget_host =
web_contents()->GetRenderViewHost()->GetWidget();
content::RenderWidgetHostView* view = render_widget_host->GetView();
if (!view || !view->IsSurfaceAvailableForCopy()) {
return;
}
// TODO(miu): This is the wrong size. It's the size of the view on-screen, and
// not the rendering size of the view. This will be replaced with the view's
// actual rendering size in a later change. http://crbug.com/73362
gfx::Rect copy_rect = gfx::Rect(view->GetViewBounds().size());
// Clip the pixels that will commonly hold a scrollbar, which looks bad in
// thumbnails.
int scrollbar_size = gfx::scrollbar_size();
copy_rect.Inset(0, 0, scrollbar_size, scrollbar_size);
if (copy_rect.IsEmpty()) {
return;
}
thumbnailing_context_ = new ThumbnailingContext(web_contents(),
thumbnail_service.get(),
load_interrupted_);
ui::ScaleFactor scale_factor =
ui::GetSupportedScaleFactor(
ui::GetScaleFactorForNativeView(view->GetNativeView()));
thumbnailing_context_->clip_result = thumbnails::GetCanvasCopyInfo(
copy_rect.size(), scale_factor,
gfx::Size(kThumbnailWidth, kThumbnailHeight), &copy_rect,
&thumbnailing_context_->requested_copy_size);
copy_from_surface_start_time_ = base::TimeTicks::Now();
view->CopyFromSurface(copy_rect, thumbnailing_context_->requested_copy_size,
base::Bind(&ThumbnailTabHelper::ProcessCapturedBitmap,
weak_factory_.GetWeakPtr()),
kN32_SkColorType);
}
void ThumbnailTabHelper::ProcessCapturedBitmap(
const SkBitmap& bitmap,
content::ReadbackResponse response) {
base::TimeDelta copy_from_surface_time =
base::TimeTicks::Now() - copy_from_surface_start_time_;
UMA_HISTOGRAM_TIMES("Thumbnails.CopyFromSurfaceTime", copy_from_surface_time);
if (response == content::READBACK_SUCCESS) {
// On success, we must be on the UI thread.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::PostTaskWithTraitsAndReply(
FROM_HERE,
{base::TaskPriority::BACKGROUND,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::Bind(&ComputeThumbnailScore, bitmap, thumbnailing_context_),
base::Bind(&ThumbnailTabHelper::UpdateThumbnail,
weak_factory_.GetWeakPtr(), bitmap));
} else {
// On failure because of shutdown we are not on the UI thread, so ensure
// that cleanup happens on that thread.
content::BrowserThread::PostTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&ThumbnailTabHelper::CleanUpFromThumbnailGeneration,
weak_factory_.GetWeakPtr()));
}
}
void ThumbnailTabHelper::UpdateThumbnail(const SkBitmap& thumbnail) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Feed the constructed thumbnail to the thumbnail service.
gfx::Image image = gfx::Image::CreateFrom1xBitmap(thumbnail);
thumbnailing_context_->service->SetPageThumbnail(*thumbnailing_context_,
image);
DVLOG(1) << "Thumbnail taken for " << thumbnailing_context_->url << ": "
<< thumbnailing_context_->score.ToString();
CleanUpFromThumbnailGeneration();
}
void ThumbnailTabHelper::CleanUpFromThumbnailGeneration() {
// Make a note that thumbnail generation is complete.
thumbnailing_context_ = nullptr;
}
void ThumbnailTabHelper::RenderViewHostCreated(
content::RenderViewHost* renderer) {
// NOTIFICATION_WEB_CONTENTS_RENDER_VIEW_HOST_CREATED is really a new
// RenderView, not RenderViewHost, and there is no good way to get
// notifications of RenderViewHosts. So just be tolerant of re-registrations.
bool registered = registrar_.IsRegistered(
this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED,
content::Source<content::RenderWidgetHost>(renderer->GetWidget()));
if (!registered) {
registrar_.Add(
this, content::NOTIFICATION_RENDER_WIDGET_VISIBILITY_CHANGED,
content::Source<content::RenderWidgetHost>(renderer->GetWidget()));
}
}
void ThumbnailTabHelper::WidgetHidden(content::RenderWidgetHost* widget) {
// Skip if a pending entry exists. WidgetHidden can be called while navigating
// pages and this is not a time when thumbnails should be generated.
if (!web_contents() || web_contents()->GetController().GetPendingEntry()) {
return;
}
UpdateThumbnailIfNecessary();
}