// Copyright (c) 2011 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/instant/instant_controller.h"
#include "base/command_line.h"
#include "base/message_loop.h"
#include "base/metrics/histogram.h"
#include "build/build_config.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/instant/instant_delegate.h"
#include "chrome/browser/instant/instant_loader.h"
#include "chrome/browser/instant/instant_loader_manager.h"
#include "chrome/browser/instant/promo_counter.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_model.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "content/browser/renderer_host/render_widget_host_view.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_service.h"
// Number of ms to delay between loading urls.
static const int kUpdateDelayMS = 200;
// Amount of time we delay before showing pages that have a non-200 status.
static const int kShowDelayMS = 800;
// static
InstantController::HostBlacklist* InstantController::host_blacklist_ = NULL;
InstantController::InstantController(Profile* profile,
InstantDelegate* delegate)
: delegate_(delegate),
tab_contents_(NULL),
is_active_(false),
displayable_loader_(NULL),
commit_on_mouse_up_(false),
last_transition_type_(PageTransition::LINK),
ALLOW_THIS_IN_INITIALIZER_LIST(destroy_factory_(this)) {
PrefService* service = profile->GetPrefs();
if (service) {
// kInstantWasEnabledOnce was added after instant, set it now to make sure
// it is correctly set.
service->SetBoolean(prefs::kInstantEnabledOnce, true);
}
}
InstantController::~InstantController() {
}
// static
void InstantController::RegisterUserPrefs(PrefService* prefs) {
prefs->RegisterBooleanPref(prefs::kInstantConfirmDialogShown, false);
prefs->RegisterBooleanPref(prefs::kInstantEnabled, false);
prefs->RegisterBooleanPref(prefs::kInstantEnabledOnce, false);
prefs->RegisterInt64Pref(prefs::kInstantEnabledTime, false);
PromoCounter::RegisterUserPrefs(prefs, prefs::kInstantPromo);
}
// static
void InstantController::RecordMetrics(Profile* profile) {
if (!IsEnabled(profile))
return;
PrefService* service = profile->GetPrefs();
if (service) {
int64 enable_time = service->GetInt64(prefs::kInstantEnabledTime);
if (!enable_time) {
service->SetInt64(prefs::kInstantEnabledTime,
base::Time::Now().ToInternalValue());
} else {
base::TimeDelta delta =
base::Time::Now() - base::Time::FromInternalValue(enable_time);
// Histogram from 1 hour to 30 days.
UMA_HISTOGRAM_CUSTOM_COUNTS("Instant.EnabledTime.Predictive",
delta.InHours(), 1, 30 * 24, 50);
}
}
}
// static
bool InstantController::IsEnabled(Profile* profile) {
PrefService* prefs = profile->GetPrefs();
return prefs->GetBoolean(prefs::kInstantEnabled);
}
// static
void InstantController::Enable(Profile* profile) {
PromoCounter* promo_counter = profile->GetInstantPromoCounter();
if (promo_counter)
promo_counter->Hide();
PrefService* service = profile->GetPrefs();
if (!service)
return;
service->SetBoolean(prefs::kInstantEnabled, true);
service->SetBoolean(prefs::kInstantConfirmDialogShown, true);
service->SetInt64(prefs::kInstantEnabledTime,
base::Time::Now().ToInternalValue());
service->SetBoolean(prefs::kInstantEnabledOnce, true);
}
// static
void InstantController::Disable(Profile* profile) {
PrefService* service = profile->GetPrefs();
if (!service || !IsEnabled(profile))
return;
int64 enable_time = service->GetInt64(prefs::kInstantEnabledTime);
if (enable_time) {
base::TimeDelta delta =
base::Time::Now() - base::Time::FromInternalValue(enable_time);
// Histogram from 1 minute to 10 days.
UMA_HISTOGRAM_CUSTOM_COUNTS("Instant.TimeToDisable.Predictive",
delta.InMinutes(), 1, 60 * 24 * 10, 50);
}
service->SetBoolean(prefs::kInstantEnabled, false);
}
// static
bool InstantController::CommitIfCurrent(InstantController* controller) {
if (controller && controller->IsCurrent()) {
controller->CommitCurrentPreview(INSTANT_COMMIT_PRESSED_ENTER);
return true;
}
return false;
}
void InstantController::Update(TabContentsWrapper* tab_contents,
const AutocompleteMatch& match,
const string16& user_text,
bool verbatim,
string16* suggested_text) {
suggested_text->clear();
if (tab_contents != tab_contents_)
DestroyPreviewContents();
const GURL& url = match.destination_url;
tab_contents_ = tab_contents;
commit_on_mouse_up_ = false;
last_transition_type_ = match.transition;
const TemplateURL* template_url = NULL;
if (url.is_empty() || !url.is_valid()) {
// Assume we were invoked with GURL() and should destroy all.
DestroyPreviewContents();
return;
}
if (!ShouldShowPreviewFor(match, &template_url)) {
DestroyPreviewContentsAndLeaveActive();
return;
}
if (!loader_manager_.get())
loader_manager_.reset(new InstantLoaderManager(this));
if (!is_active_) {
is_active_ = true;
delegate_->PrepareForInstant();
}
TemplateURLID template_url_id = template_url ? template_url->id() : 0;
// Verbatim only makes sense if the search engines supports instant.
bool real_verbatim = template_url_id ? verbatim : false;
if (ShouldUpdateNow(template_url_id, match.destination_url)) {
UpdateLoader(template_url, match.destination_url, match.transition,
user_text, real_verbatim, suggested_text);
} else {
ScheduleUpdate(match.destination_url);
}
NotificationService::current()->Notify(
NotificationType::INSTANT_CONTROLLER_UPDATED,
Source<InstantController>(this),
NotificationService::NoDetails());
}
void InstantController::SetOmniboxBounds(const gfx::Rect& bounds) {
if (omnibox_bounds_ == bounds)
return;
// Always track the omnibox bounds. That way if Update is later invoked the
// bounds are in sync.
omnibox_bounds_ = bounds;
if (loader_manager_.get()) {
if (loader_manager_->current_loader())
loader_manager_->current_loader()->SetOmniboxBounds(bounds);
if (loader_manager_->pending_loader())
loader_manager_->pending_loader()->SetOmniboxBounds(bounds);
}
}
void InstantController::DestroyPreviewContents() {
if (!loader_manager_.get()) {
// We're not showing anything, nothing to do.
return;
}
// ReleasePreviewContents sets is_active_ to false, but we need to set it
// before notifying the delegate, otherwise if the delegate asks for the state
// we'll still be active.
is_active_ = false;
delegate_->HideInstant();
delete ReleasePreviewContents(INSTANT_COMMIT_DESTROY);
}
void InstantController::DestroyPreviewContentsAndLeaveActive() {
commit_on_mouse_up_ = false;
if (displayable_loader_) {
displayable_loader_ = NULL;
delegate_->HideInstant();
}
// TODO(sky): this shouldn't nuke the loader. It should just nuke non-instant
// loaders and hide instant loaders.
loader_manager_.reset(new InstantLoaderManager(this));
show_timer_.Stop();
update_timer_.Stop();
}
bool InstantController::IsCurrent() {
return loader_manager_.get() && loader_manager_->active_loader() &&
loader_manager_->active_loader()->ready() &&
!loader_manager_->active_loader()->needs_reload() &&
!update_timer_.IsRunning();
}
void InstantController::CommitCurrentPreview(InstantCommitType type) {
if (type == INSTANT_COMMIT_PRESSED_ENTER && show_timer_.IsRunning()) {
// The user pressed enter and the show timer is running. This means the
// pending_loader returned an error code and we're not showing it. Force it
// to be shown.
show_timer_.Stop();
ShowTimerFired();
}
DCHECK(loader_manager_.get());
DCHECK(loader_manager_->current_loader());
bool showing_instant =
loader_manager_->current_loader()->is_showing_instant();
TabContentsWrapper* tab = ReleasePreviewContents(type);
// If the loader was showing an instant page then it's navigation stack is
// something like: search-engine-home-page (eg google.com) search-term1
// search-term2 .... Each search-term navigation corresponds to the page
// deciding enough time has passed to commit a navigation. We don't want the
// searche-engine-home-page navigation in this case so we pass true to
// CopyStateFromAndPrune to have the search-engine-home-page navigation
// removed.
tab->controller().CopyStateFromAndPrune(
&tab_contents_->controller(), showing_instant);
delegate_->CommitInstant(tab);
CompleteRelease(tab->tab_contents());
}
void InstantController::SetCommitOnMouseUp() {
commit_on_mouse_up_ = true;
}
bool InstantController::IsMouseDownFromActivate() {
DCHECK(loader_manager_.get());
DCHECK(loader_manager_->current_loader());
return loader_manager_->current_loader()->IsMouseDownFromActivate();
}
#if defined(OS_MACOSX)
void InstantController::OnAutocompleteLostFocus(
gfx::NativeView view_gaining_focus) {
// If |IsMouseDownFromActivate()| returns false, the RenderWidgetHostView did
// not receive a mouseDown event. Therefore, we should destroy the preview.
// Otherwise, the RWHV was clicked, so we commit the preview.
if (!is_displayable() || !GetPreviewContents() ||
!IsMouseDownFromActivate()) {
DestroyPreviewContents();
} else if (IsShowingInstant()) {
SetCommitOnMouseUp();
} else {
CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST);
}
}
#else
void InstantController::OnAutocompleteLostFocus(
gfx::NativeView view_gaining_focus) {
if (!is_active() || !GetPreviewContents()) {
DestroyPreviewContents();
return;
}
RenderWidgetHostView* rwhv =
GetPreviewContents()->tab_contents()->GetRenderWidgetHostView();
if (!view_gaining_focus || !rwhv) {
DestroyPreviewContents();
return;
}
gfx::NativeView tab_view =
GetPreviewContents()->tab_contents()->GetNativeView();
// Focus is going to the renderer.
if (rwhv->GetNativeView() == view_gaining_focus ||
tab_view == view_gaining_focus) {
if (!IsMouseDownFromActivate()) {
// If the mouse is not down, focus is not going to the renderer. Someone
// else moved focus and we shouldn't commit.
DestroyPreviewContents();
return;
}
if (IsShowingInstant()) {
// We're showing instant results. As instant results may shift when
// committing we commit on the mouse up. This way a slow click still
// works fine.
SetCommitOnMouseUp();
return;
}
CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST);
return;
}
// Walk up the view hierarchy. If the view gaining focus is a subview of the
// TabContents view (such as a windowed plugin or http auth dialog), we want
// to keep the preview contents. Otherwise, focus has gone somewhere else,
// such as the JS inspector, and we want to cancel the preview.
gfx::NativeView view_gaining_focus_ancestor = view_gaining_focus;
while (view_gaining_focus_ancestor &&
view_gaining_focus_ancestor != tab_view) {
view_gaining_focus_ancestor =
platform_util::GetParent(view_gaining_focus_ancestor);
}
if (view_gaining_focus_ancestor) {
CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST);
return;
}
DestroyPreviewContents();
}
#endif
TabContentsWrapper* InstantController::ReleasePreviewContents(
InstantCommitType type) {
if (!loader_manager_.get())
return NULL;
// Make sure the pending loader is active. Ideally we would call
// ShowTimerFired, but if Release is invoked from the browser we don't want to
// attempt to show the tab contents (since its being added to a new tab).
if (type == INSTANT_COMMIT_PRESSED_ENTER && show_timer_.IsRunning()) {
InstantLoader* loader = loader_manager_->active_loader();
if (loader && loader->ready() &&
loader == loader_manager_->pending_loader()) {
scoped_ptr<InstantLoader> old_loader;
loader_manager_->MakePendingCurrent(&old_loader);
}
}
// Loader may be null if the url blacklisted instant.
scoped_ptr<InstantLoader> loader;
if (loader_manager_->current_loader())
loader.reset(loader_manager_->ReleaseCurrentLoader());
TabContentsWrapper* tab = loader.get() ?
loader->ReleasePreviewContents(type) : NULL;
ClearBlacklist();
is_active_ = false;
displayable_loader_ = NULL;
commit_on_mouse_up_ = false;
omnibox_bounds_ = gfx::Rect();
loader_manager_.reset();
update_timer_.Stop();
show_timer_.Stop();
return tab;
}
void InstantController::CompleteRelease(TabContents* tab) {
tab->SetAllContentsBlocked(false);
}
TabContentsWrapper* InstantController::GetPreviewContents() {
return loader_manager_.get() && loader_manager_->current_loader() ?
loader_manager_->current_loader()->preview_contents() : NULL;
}
bool InstantController::IsShowingInstant() {
return loader_manager_.get() && loader_manager_->current_loader() &&
loader_manager_->current_loader()->is_showing_instant();
}
bool InstantController::MightSupportInstant() {
return loader_manager_.get() && loader_manager_->active_loader() &&
loader_manager_->active_loader()->is_showing_instant();
}
GURL InstantController::GetCurrentURL() {
return loader_manager_.get() && loader_manager_->active_loader() ?
loader_manager_->active_loader()->url() : GURL();
}
void InstantController::InstantStatusChanged(InstantLoader* loader) {
if (!loader->http_status_ok()) {
// Status isn't ok, start a timer that when fires shows the result. This
// delays showing 403 pages and the like.
show_timer_.Stop();
show_timer_.Start(
base::TimeDelta::FromMilliseconds(kShowDelayMS),
this, &InstantController::ShowTimerFired);
UpdateDisplayableLoader();
return;
}
ProcessInstantStatusChanged(loader);
}
void InstantController::SetSuggestedTextFor(
InstantLoader* loader,
const string16& text,
InstantCompleteBehavior behavior) {
if (loader_manager_->current_loader() == loader)
delegate_->SetSuggestedText(text, behavior);
}
gfx::Rect InstantController::GetInstantBounds() {
return delegate_->GetInstantBounds();
}
bool InstantController::ShouldCommitInstantOnMouseUp() {
return commit_on_mouse_up_;
}
void InstantController::CommitInstantLoader(InstantLoader* loader) {
if (loader_manager_.get() && loader_manager_->current_loader() == loader) {
CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST);
} else {
// This can happen if the mouse was down, we swapped out the preview and
// the mouse was released. Generally this shouldn't happen, but if it does
// revert.
DestroyPreviewContents();
}
}
void InstantController::InstantLoaderDoesntSupportInstant(
InstantLoader* loader) {
DCHECK(!loader->ready()); // We better not be showing this loader.
DCHECK(loader->template_url_id());
VLOG(1) << "provider does not support instant";
// Don't attempt to use instant for this search engine again.
BlacklistFromInstant(loader->template_url_id());
// Because of the state of the stack we can't destroy the loader now.
bool was_pending = loader_manager_->pending_loader() == loader;
ScheduleDestroy(loader_manager_->ReleaseLoader(loader));
if (was_pending) {
// |loader| was the pending loader. We may be showing another TabContents to
// the user (what was current). Destroy it.
DestroyPreviewContentsAndLeaveActive();
} else {
// |loader| wasn't pending, yet it may still be the displayed loader.
UpdateDisplayableLoader();
}
}
void InstantController::AddToBlacklist(InstantLoader* loader, const GURL& url) {
std::string host = url.host();
if (host.empty())
return;
if (!host_blacklist_)
host_blacklist_ = new HostBlacklist;
host_blacklist_->insert(host);
if (!loader_manager_.get())
return;
// Because of the state of the stack we can't destroy the loader now.
ScheduleDestroy(loader);
loader_manager_->ReleaseLoader(loader);
UpdateDisplayableLoader();
}
void InstantController::UpdateDisplayableLoader() {
InstantLoader* loader = NULL;
// As soon as the pending loader is displayable it becomes the current loader,
// so we need only concern ourselves with the current loader here.
if (loader_manager_.get() && loader_manager_->current_loader() &&
loader_manager_->current_loader()->ready() &&
(!show_timer_.IsRunning() ||
loader_manager_->current_loader()->http_status_ok())) {
loader = loader_manager_->current_loader();
}
if (loader == displayable_loader_)
return;
displayable_loader_ = loader;
if (!displayable_loader_) {
delegate_->HideInstant();
} else {
delegate_->ShowInstant(displayable_loader_->preview_contents());
NotificationService::current()->Notify(
NotificationType::INSTANT_CONTROLLER_SHOWN,
Source<InstantController>(this),
NotificationService::NoDetails());
}
}
TabContentsWrapper* InstantController::GetPendingPreviewContents() {
return loader_manager_.get() && loader_manager_->pending_loader() ?
loader_manager_->pending_loader()->preview_contents() : NULL;
}
bool InstantController::ShouldUpdateNow(TemplateURLID instant_id,
const GURL& url) {
DCHECK(loader_manager_.get());
if (instant_id) {
// Update sites that support instant immediately, they can do their own
// throttling.
return true;
}
if (url.SchemeIsFile())
return true; // File urls should load quickly, so don't delay loading them.
if (loader_manager_->WillUpateChangeActiveLoader(instant_id)) {
// If Update would change loaders, update now. This indicates transitioning
// from an instant to non-instant loader.
return true;
}
InstantLoader* active_loader = loader_manager_->active_loader();
// WillUpateChangeActiveLoader should return true if no active loader, so
// we know there will be an active loader if we get here.
DCHECK(active_loader);
// Immediately update if the url is the same (which should result in nothing
// happening) or the hosts differ, otherwise we'll delay the update.
return (active_loader->url() == url) ||
(active_loader->url().host() != url.host());
}
void InstantController::ScheduleUpdate(const GURL& url) {
scheduled_url_ = url;
update_timer_.Stop();
update_timer_.Start(base::TimeDelta::FromMilliseconds(kUpdateDelayMS),
this, &InstantController::ProcessScheduledUpdate);
}
void InstantController::ProcessScheduledUpdate() {
DCHECK(loader_manager_.get());
// We only delay loading of sites that don't support instant, so we can ignore
// suggested_text here.
string16 suggested_text;
UpdateLoader(NULL, scheduled_url_, last_transition_type_, string16(), false,
&suggested_text);
}
void InstantController::ProcessInstantStatusChanged(InstantLoader* loader) {
DCHECK(loader_manager_.get());
scoped_ptr<InstantLoader> old_loader;
if (loader == loader_manager_->pending_loader()) {
loader_manager_->MakePendingCurrent(&old_loader);
} else if (loader != loader_manager_->current_loader()) {
// Notification from a loader that is no longer the current (either we have
// a pending, or its an instant loader). Ignore it.
return;
}
UpdateDisplayableLoader();
}
void InstantController::ShowTimerFired() {
if (!loader_manager_.get())
return;
InstantLoader* loader = loader_manager_->active_loader();
if (loader && loader->ready())
ProcessInstantStatusChanged(loader);
}
void InstantController::UpdateLoader(const TemplateURL* template_url,
const GURL& url,
PageTransition::Type transition_type,
const string16& user_text,
bool verbatim,
string16* suggested_text) {
update_timer_.Stop();
scoped_ptr<InstantLoader> owned_loader;
TemplateURLID template_url_id = template_url ? template_url->id() : 0;
InstantLoader* new_loader =
loader_manager_->UpdateLoader(template_url_id, &owned_loader);
new_loader->SetOmniboxBounds(omnibox_bounds_);
if (new_loader->Update(tab_contents_, template_url, url, transition_type,
user_text, verbatim, suggested_text)) {
show_timer_.Stop();
if (!new_loader->http_status_ok()) {
show_timer_.Start(
base::TimeDelta::FromMilliseconds(kShowDelayMS),
this, &InstantController::ShowTimerFired);
}
}
UpdateDisplayableLoader();
}
bool InstantController::ShouldShowPreviewFor(const AutocompleteMatch& match,
const TemplateURL** template_url) {
const TemplateURL* t_url = GetTemplateURL(match);
if (t_url) {
if (!t_url->id() ||
!t_url->instant_url() ||
IsBlacklistedFromInstant(t_url->id()) ||
!t_url->instant_url()->SupportsReplacement()) {
// To avoid extra load on other search engines we only enable previews if
// they support the instant API.
return false;
}
}
*template_url = t_url;
if (match.destination_url.SchemeIs(chrome::kJavaScriptScheme))
return false;
// Extension keywords don't have a real destionation URL.
if (match.template_url && match.template_url->IsExtensionKeyword())
return false;
// Was the host blacklisted?
if (host_blacklist_ && host_blacklist_->count(match.destination_url.host()))
return false;
return true;
}
void InstantController::BlacklistFromInstant(TemplateURLID id) {
blacklisted_ids_.insert(id);
}
bool InstantController::IsBlacklistedFromInstant(TemplateURLID id) {
return blacklisted_ids_.count(id) > 0;
}
void InstantController::ClearBlacklist() {
blacklisted_ids_.clear();
}
void InstantController::ScheduleDestroy(InstantLoader* loader) {
loaders_to_destroy_.push_back(loader);
if (destroy_factory_.empty()) {
MessageLoop::current()->PostTask(
FROM_HERE, destroy_factory_.NewRunnableMethod(
&InstantController::DestroyLoaders));
}
}
void InstantController::DestroyLoaders() {
loaders_to_destroy_.reset();
}
const TemplateURL* InstantController::GetTemplateURL(
const AutocompleteMatch& match) {
const TemplateURL* template_url = match.template_url;
if (match.type == AutocompleteMatch::SEARCH_WHAT_YOU_TYPED ||
match.type == AutocompleteMatch::SEARCH_HISTORY ||
match.type == AutocompleteMatch::SEARCH_SUGGEST) {
TemplateURLModel* model = tab_contents_->profile()->GetTemplateURLModel();
template_url = model ? model->GetDefaultSearchProvider() : NULL;
}
return template_url;
}