// 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; }