instant_controller.cc revision dc0f95d653279beabeb9817299e2902918ba123e
1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/instant/instant_controller.h" 6 7#include "build/build_config.h" 8#include "base/command_line.h" 9#include "base/message_loop.h" 10#include "base/metrics/histogram.h" 11#include "chrome/browser/autocomplete/autocomplete_match.h" 12#include "chrome/browser/instant/instant_delegate.h" 13#include "chrome/browser/instant/instant_loader.h" 14#include "chrome/browser/instant/instant_loader_manager.h" 15#include "chrome/browser/instant/promo_counter.h" 16#include "chrome/browser/platform_util.h" 17#include "chrome/browser/prefs/pref_service.h" 18#include "chrome/browser/profiles/profile.h" 19#include "chrome/browser/search_engines/template_url.h" 20#include "chrome/browser/search_engines/template_url_model.h" 21#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 22#include "chrome/common/chrome_switches.h" 23#include "chrome/common/notification_service.h" 24#include "chrome/common/pref_names.h" 25#include "chrome/common/url_constants.h" 26#include "content/browser/renderer_host/render_widget_host_view.h" 27#include "content/browser/tab_contents/tab_contents.h" 28 29// Number of ms to delay between loading urls. 30static const int kUpdateDelayMS = 200; 31 32// static 33InstantController::HostBlacklist* InstantController::host_blacklist_ = NULL; 34 35InstantController::InstantController(Profile* profile, 36 InstantDelegate* delegate) 37 : delegate_(delegate), 38 tab_contents_(NULL), 39 is_active_(false), 40 displayable_loader_(NULL), 41 commit_on_mouse_up_(false), 42 last_transition_type_(PageTransition::LINK), 43 ALLOW_THIS_IN_INITIALIZER_LIST(destroy_factory_(this)) { 44 PrefService* service = profile->GetPrefs(); 45 if (service) { 46 // kInstantWasEnabledOnce was added after instant, set it now to make sure 47 // it is correctly set. 48 service->SetBoolean(prefs::kInstantEnabledOnce, true); 49 } 50} 51 52InstantController::~InstantController() { 53} 54 55// static 56void InstantController::RegisterUserPrefs(PrefService* prefs) { 57 prefs->RegisterBooleanPref(prefs::kInstantConfirmDialogShown, false); 58 prefs->RegisterBooleanPref(prefs::kInstantEnabled, false); 59 prefs->RegisterBooleanPref(prefs::kInstantEnabledOnce, false); 60 prefs->RegisterInt64Pref(prefs::kInstantEnabledTime, false); 61 PromoCounter::RegisterUserPrefs(prefs, prefs::kInstantPromo); 62} 63 64// static 65void InstantController::RecordMetrics(Profile* profile) { 66 if (!IsEnabled(profile)) 67 return; 68 69 PrefService* service = profile->GetPrefs(); 70 if (service) { 71 int64 enable_time = service->GetInt64(prefs::kInstantEnabledTime); 72 if (!enable_time) { 73 service->SetInt64(prefs::kInstantEnabledTime, 74 base::Time::Now().ToInternalValue()); 75 } else { 76 base::TimeDelta delta = 77 base::Time::Now() - base::Time::FromInternalValue(enable_time); 78 // Histogram from 1 hour to 30 days. 79 UMA_HISTOGRAM_CUSTOM_COUNTS("Instant.EnabledTime.Predictive", 80 delta.InHours(), 1, 30 * 24, 50); 81 } 82 } 83} 84 85// static 86bool InstantController::IsEnabled(Profile* profile) { 87 PrefService* prefs = profile->GetPrefs(); 88 return prefs->GetBoolean(prefs::kInstantEnabled); 89} 90 91// static 92void InstantController::Enable(Profile* profile) { 93 PromoCounter* promo_counter = profile->GetInstantPromoCounter(); 94 if (promo_counter) 95 promo_counter->Hide(); 96 97 PrefService* service = profile->GetPrefs(); 98 if (!service) 99 return; 100 101 service->SetBoolean(prefs::kInstantEnabled, true); 102 service->SetBoolean(prefs::kInstantConfirmDialogShown, true); 103 service->SetInt64(prefs::kInstantEnabledTime, 104 base::Time::Now().ToInternalValue()); 105 service->SetBoolean(prefs::kInstantEnabledOnce, true); 106} 107 108// static 109void InstantController::Disable(Profile* profile) { 110 PrefService* service = profile->GetPrefs(); 111 if (!service || !IsEnabled(profile)) 112 return; 113 114 int64 enable_time = service->GetInt64(prefs::kInstantEnabledTime); 115 if (enable_time) { 116 base::TimeDelta delta = 117 base::Time::Now() - base::Time::FromInternalValue(enable_time); 118 // Histogram from 1 minute to 10 days. 119 UMA_HISTOGRAM_CUSTOM_COUNTS("Instant.TimeToDisable.Predictive", 120 delta.InMinutes(), 1, 60 * 24 * 10, 50); 121 } 122 123 service->SetBoolean(prefs::kInstantEnabled, false); 124} 125 126// static 127bool InstantController::CommitIfCurrent(InstantController* controller) { 128 if (controller && controller->IsCurrent()) { 129 controller->CommitCurrentPreview(INSTANT_COMMIT_PRESSED_ENTER); 130 return true; 131 } 132 return false; 133} 134 135void InstantController::Update(TabContentsWrapper* tab_contents, 136 const AutocompleteMatch& match, 137 const string16& user_text, 138 bool verbatim, 139 string16* suggested_text) { 140 suggested_text->clear(); 141 142 if (tab_contents != tab_contents_) 143 DestroyPreviewContents(); 144 145 const GURL& url = match.destination_url; 146 tab_contents_ = tab_contents; 147 commit_on_mouse_up_ = false; 148 last_transition_type_ = match.transition; 149 const TemplateURL* template_url = NULL; 150 151 if (url.is_empty() || !url.is_valid()) { 152 // Assume we were invoked with GURL() and should destroy all. 153 DestroyPreviewContents(); 154 return; 155 } 156 157 if (!ShouldShowPreviewFor(match, &template_url)) { 158 DestroyPreviewContentsAndLeaveActive(); 159 return; 160 } 161 162 if (!loader_manager_.get()) 163 loader_manager_.reset(new InstantLoaderManager(this)); 164 165 if (!is_active_) { 166 is_active_ = true; 167 delegate_->PrepareForInstant(); 168 } 169 170 TemplateURLID template_url_id = template_url ? template_url->id() : 0; 171 // Verbatim only makes sense if the search engines supports instant. 172 bool real_verbatim = template_url_id ? verbatim : false; 173 174 if (ShouldUpdateNow(template_url_id, match.destination_url)) { 175 UpdateLoader(template_url, match.destination_url, match.transition, 176 user_text, real_verbatim, suggested_text); 177 } else { 178 ScheduleUpdate(match.destination_url); 179 } 180 181 NotificationService::current()->Notify( 182 NotificationType::INSTANT_CONTROLLER_UPDATED, 183 Source<InstantController>(this), 184 NotificationService::NoDetails()); 185} 186 187void InstantController::SetOmniboxBounds(const gfx::Rect& bounds) { 188 if (omnibox_bounds_ == bounds) 189 return; 190 191 // Always track the omnibox bounds. That way if Update is later invoked the 192 // bounds are in sync. 193 omnibox_bounds_ = bounds; 194 if (loader_manager_.get()) { 195 if (loader_manager_->current_loader()) 196 loader_manager_->current_loader()->SetOmniboxBounds(bounds); 197 if (loader_manager_->pending_loader()) 198 loader_manager_->pending_loader()->SetOmniboxBounds(bounds); 199 } 200} 201 202void InstantController::DestroyPreviewContents() { 203 if (!loader_manager_.get()) { 204 // We're not showing anything, nothing to do. 205 return; 206 } 207 208 // ReleasePreviewContents sets is_active_ to false, but we need to set it 209 // before notifying the delegate, otherwise if the delegate asks for the state 210 // we'll still be active. 211 is_active_ = false; 212 delegate_->HideInstant(); 213 delete ReleasePreviewContents(INSTANT_COMMIT_DESTROY); 214} 215 216void InstantController::DestroyPreviewContentsAndLeaveActive() { 217 commit_on_mouse_up_ = false; 218 if (displayable_loader_) { 219 displayable_loader_ = NULL; 220 delegate_->HideInstant(); 221 } 222 223 // TODO(sky): this shouldn't nuke the loader. It should just nuke non-instant 224 // loaders and hide instant loaders. 225 loader_manager_.reset(new InstantLoaderManager(this)); 226 update_timer_.Stop(); 227} 228 229bool InstantController::IsCurrent() { 230 return loader_manager_.get() && loader_manager_->active_loader() && 231 loader_manager_->active_loader()->ready() && !update_timer_.IsRunning(); 232} 233 234void InstantController::CommitCurrentPreview(InstantCommitType type) { 235 DCHECK(loader_manager_.get()); 236 DCHECK(loader_manager_->current_loader()); 237 TabContentsWrapper* tab = ReleasePreviewContents(type); 238 delegate_->CommitInstant(tab); 239 CompleteRelease(tab->tab_contents()); 240} 241 242void InstantController::SetCommitOnMouseUp() { 243 commit_on_mouse_up_ = true; 244} 245 246bool InstantController::IsMouseDownFromActivate() { 247 DCHECK(loader_manager_.get()); 248 DCHECK(loader_manager_->current_loader()); 249 return loader_manager_->current_loader()->IsMouseDownFromActivate(); 250} 251 252#if defined(OS_MACOSX) 253void InstantController::OnAutocompleteLostFocus( 254 gfx::NativeView view_gaining_focus) { 255 // If |IsMouseDownFromActivate()| returns false, the RenderWidgetHostView did 256 // not receive a mouseDown event. Therefore, we should destroy the preview. 257 // Otherwise, the RWHV was clicked, so we commit the preview. 258 if (!is_displayable() || !GetPreviewContents() || 259 !IsMouseDownFromActivate()) { 260 DestroyPreviewContents(); 261 } else if (IsShowingInstant()) { 262 SetCommitOnMouseUp(); 263 } else { 264 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 265 } 266} 267#else 268void InstantController::OnAutocompleteLostFocus( 269 gfx::NativeView view_gaining_focus) { 270 if (!is_active() || !GetPreviewContents()) { 271 DestroyPreviewContents(); 272 return; 273 } 274 275 RenderWidgetHostView* rwhv = 276 GetPreviewContents()->tab_contents()->GetRenderWidgetHostView(); 277 if (!view_gaining_focus || !rwhv) { 278 DestroyPreviewContents(); 279 return; 280 } 281 282 gfx::NativeView tab_view = 283 GetPreviewContents()->tab_contents()->GetNativeView(); 284 // Focus is going to the renderer. 285 if (rwhv->GetNativeView() == view_gaining_focus || 286 tab_view == view_gaining_focus) { 287 if (!IsMouseDownFromActivate()) { 288 // If the mouse is not down, focus is not going to the renderer. Someone 289 // else moved focus and we shouldn't commit. 290 DestroyPreviewContents(); 291 return; 292 } 293 294 if (IsShowingInstant()) { 295 // We're showing instant results. As instant results may shift when 296 // committing we commit on the mouse up. This way a slow click still 297 // works fine. 298 SetCommitOnMouseUp(); 299 return; 300 } 301 302 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 303 return; 304 } 305 306 // Walk up the view hierarchy. If the view gaining focus is a subview of the 307 // TabContents view (such as a windowed plugin or http auth dialog), we want 308 // to keep the preview contents. Otherwise, focus has gone somewhere else, 309 // such as the JS inspector, and we want to cancel the preview. 310 gfx::NativeView view_gaining_focus_ancestor = view_gaining_focus; 311 while (view_gaining_focus_ancestor && 312 view_gaining_focus_ancestor != tab_view) { 313 view_gaining_focus_ancestor = 314 platform_util::GetParent(view_gaining_focus_ancestor); 315 } 316 317 if (view_gaining_focus_ancestor) { 318 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 319 return; 320 } 321 322 DestroyPreviewContents(); 323} 324#endif 325 326TabContentsWrapper* InstantController::ReleasePreviewContents( 327 InstantCommitType type) { 328 if (!loader_manager_.get()) 329 return NULL; 330 331 // Loader may be null if the url blacklisted instant. 332 scoped_ptr<InstantLoader> loader; 333 if (loader_manager_->current_loader()) 334 loader.reset(loader_manager_->ReleaseCurrentLoader()); 335 TabContentsWrapper* tab = loader.get() ? 336 loader->ReleasePreviewContents(type) : NULL; 337 338 ClearBlacklist(); 339 is_active_ = false; 340 displayable_loader_ = NULL; 341 commit_on_mouse_up_ = false; 342 omnibox_bounds_ = gfx::Rect(); 343 loader_manager_.reset(); 344 update_timer_.Stop(); 345 return tab; 346} 347 348void InstantController::CompleteRelease(TabContents* tab) { 349 tab->SetAllContentsBlocked(false); 350} 351 352TabContentsWrapper* InstantController::GetPreviewContents() { 353 return loader_manager_.get() && loader_manager_->current_loader() ? 354 loader_manager_->current_loader()->preview_contents() : NULL; 355} 356 357bool InstantController::IsShowingInstant() { 358 return loader_manager_.get() && loader_manager_->current_loader() && 359 loader_manager_->current_loader()->is_showing_instant(); 360} 361 362bool InstantController::MightSupportInstant() { 363 return loader_manager_.get() && loader_manager_->active_loader() && 364 loader_manager_->active_loader()->is_showing_instant(); 365} 366 367GURL InstantController::GetCurrentURL() { 368 return loader_manager_.get() && loader_manager_->active_loader() ? 369 loader_manager_->active_loader()->url() : GURL(); 370} 371 372void InstantController::ShowInstantLoader(InstantLoader* loader) { 373 DCHECK(loader_manager_.get()); 374 scoped_ptr<InstantLoader> old_loader; 375 if (loader == loader_manager_->pending_loader()) { 376 loader_manager_->MakePendingCurrent(&old_loader); 377 } else if (loader != loader_manager_->current_loader()) { 378 // Notification from a loader that is no longer the current (either we have 379 // a pending, or its an instant loader). Ignore it. 380 return; 381 } 382 383 UpdateDisplayableLoader(); 384 385 NotificationService::current()->Notify( 386 NotificationType::INSTANT_CONTROLLER_SHOWN, 387 Source<InstantController>(this), 388 NotificationService::NoDetails()); 389} 390 391void InstantController::SetSuggestedTextFor(InstantLoader* loader, 392 const string16& text) { 393 if (loader_manager_->current_loader() == loader) 394 delegate_->SetSuggestedText(text); 395} 396 397gfx::Rect InstantController::GetInstantBounds() { 398 return delegate_->GetInstantBounds(); 399} 400 401bool InstantController::ShouldCommitInstantOnMouseUp() { 402 return commit_on_mouse_up_; 403} 404 405void InstantController::CommitInstantLoader(InstantLoader* loader) { 406 if (loader_manager_.get() && loader_manager_->current_loader() == loader) { 407 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 408 } else { 409 // This can happen if the mouse was down, we swapped out the preview and 410 // the mouse was released. Generally this shouldn't happen, but if it does 411 // revert. 412 DestroyPreviewContents(); 413 } 414} 415 416void InstantController::InstantLoaderDoesntSupportInstant( 417 InstantLoader* loader) { 418 DCHECK(!loader->ready()); // We better not be showing this loader. 419 DCHECK(loader->template_url_id()); 420 421 VLOG(1) << "provider does not support instant"; 422 423 // Don't attempt to use instant for this search engine again. 424 BlacklistFromInstant(loader->template_url_id()); 425 426 if (loader_manager_->active_loader() == loader) { 427 // The loader is active, hide all. 428 DestroyPreviewContentsAndLeaveActive(); 429 } else { 430 scoped_ptr<InstantLoader> owned_loader( 431 loader_manager_->ReleaseLoader(loader)); 432 UpdateDisplayableLoader(); 433 } 434} 435 436void InstantController::AddToBlacklist(InstantLoader* loader, const GURL& url) { 437 std::string host = url.host(); 438 if (host.empty()) 439 return; 440 441 if (!host_blacklist_) 442 host_blacklist_ = new HostBlacklist; 443 host_blacklist_->insert(host); 444 445 if (!loader_manager_.get()) 446 return; 447 448 // Because of the state of the stack we can't destroy the loader now. 449 ScheduleDestroy(loader); 450 451 loader_manager_->ReleaseLoader(loader); 452 453 UpdateDisplayableLoader(); 454} 455 456void InstantController::UpdateDisplayableLoader() { 457 InstantLoader* loader = NULL; 458 // As soon as the pending loader is displayable it becomes the current loader, 459 // so we need only concern ourselves with the current loader here. 460 if (loader_manager_.get() && loader_manager_->current_loader() && 461 loader_manager_->current_loader()->ready()) { 462 loader = loader_manager_->current_loader(); 463 } 464 if (loader == displayable_loader_) 465 return; 466 467 displayable_loader_ = loader; 468 469 if (!displayable_loader_) { 470 delegate_->HideInstant(); 471 } else { 472 delegate_->ShowInstant(displayable_loader_->preview_contents()); 473 NotificationService::current()->Notify( 474 NotificationType::INSTANT_CONTROLLER_SHOWN, 475 Source<InstantController>(this), 476 NotificationService::NoDetails()); 477 } 478} 479 480TabContentsWrapper* InstantController::GetPendingPreviewContents() { 481 return loader_manager_.get() && loader_manager_->pending_loader() ? 482 loader_manager_->pending_loader()->preview_contents() : NULL; 483} 484 485bool InstantController::ShouldUpdateNow(TemplateURLID instant_id, 486 const GURL& url) { 487 DCHECK(loader_manager_.get()); 488 489 if (instant_id) { 490 // Update sites that support instant immediately, they can do their own 491 // throttling. 492 return true; 493 } 494 495 if (url.SchemeIsFile()) 496 return true; // File urls should load quickly, so don't delay loading them. 497 498 if (loader_manager_->WillUpateChangeActiveLoader(instant_id)) { 499 // If Update would change loaders, update now. This indicates transitioning 500 // from an instant to non-instant loader. 501 return true; 502 } 503 504 InstantLoader* active_loader = loader_manager_->active_loader(); 505 // WillUpateChangeActiveLoader should return true if no active loader, so 506 // we know there will be an active loader if we get here. 507 DCHECK(active_loader); 508 // Immediately update if the url is the same (which should result in nothing 509 // happening) or the hosts differ, otherwise we'll delay the update. 510 return (active_loader->url() == url) || 511 (active_loader->url().host() != url.host()); 512} 513 514void InstantController::ScheduleUpdate(const GURL& url) { 515 scheduled_url_ = url; 516 517 update_timer_.Stop(); 518 update_timer_.Start(base::TimeDelta::FromMilliseconds(kUpdateDelayMS), 519 this, &InstantController::ProcessScheduledUpdate); 520} 521 522void InstantController::ProcessScheduledUpdate() { 523 DCHECK(loader_manager_.get()); 524 525 // We only delay loading of sites that don't support instant, so we can ignore 526 // suggested_text here. 527 string16 suggested_text; 528 UpdateLoader(NULL, scheduled_url_, last_transition_type_, string16(), false, 529 &suggested_text); 530} 531 532void InstantController::UpdateLoader(const TemplateURL* template_url, 533 const GURL& url, 534 PageTransition::Type transition_type, 535 const string16& user_text, 536 bool verbatim, 537 string16* suggested_text) { 538 update_timer_.Stop(); 539 540 scoped_ptr<InstantLoader> owned_loader; 541 TemplateURLID template_url_id = template_url ? template_url->id() : 0; 542 InstantLoader* new_loader = 543 loader_manager_->UpdateLoader(template_url_id, &owned_loader); 544 545 new_loader->SetOmniboxBounds(omnibox_bounds_); 546 new_loader->Update(tab_contents_, template_url, url, transition_type, 547 user_text, verbatim, suggested_text); 548 UpdateDisplayableLoader(); 549} 550 551bool InstantController::ShouldShowPreviewFor(const AutocompleteMatch& match, 552 const TemplateURL** template_url) { 553 const TemplateURL* t_url = GetTemplateURL(match); 554 if (t_url) { 555 if (!t_url->id() || 556 !t_url->instant_url() || 557 IsBlacklistedFromInstant(t_url->id()) || 558 !t_url->instant_url()->SupportsReplacement()) { 559 // To avoid extra load on other search engines we only enable previews if 560 // they support the instant API. 561 return false; 562 } 563 } 564 *template_url = t_url; 565 566 if (match.destination_url.SchemeIs(chrome::kJavaScriptScheme)) 567 return false; 568 569 // Extension keywords don't have a real destionation URL. 570 if (match.template_url && match.template_url->IsExtensionKeyword()) 571 return false; 572 573 // Was the host blacklisted? 574 if (host_blacklist_ && host_blacklist_->count(match.destination_url.host())) 575 return false; 576 577 return true; 578} 579 580void InstantController::BlacklistFromInstant(TemplateURLID id) { 581 blacklisted_ids_.insert(id); 582} 583 584bool InstantController::IsBlacklistedFromInstant(TemplateURLID id) { 585 return blacklisted_ids_.count(id) > 0; 586} 587 588void InstantController::ClearBlacklist() { 589 blacklisted_ids_.clear(); 590} 591 592void InstantController::ScheduleDestroy(InstantLoader* loader) { 593 loaders_to_destroy_.push_back(loader); 594 if (destroy_factory_.empty()) { 595 MessageLoop::current()->PostTask( 596 FROM_HERE, destroy_factory_.NewRunnableMethod( 597 &InstantController::DestroyLoaders)); 598 } 599} 600 601void InstantController::DestroyLoaders() { 602 loaders_to_destroy_.reset(); 603} 604 605const TemplateURL* InstantController::GetTemplateURL( 606 const AutocompleteMatch& match) { 607 const TemplateURL* template_url = match.template_url; 608 if (match.type == AutocompleteMatch::SEARCH_WHAT_YOU_TYPED || 609 match.type == AutocompleteMatch::SEARCH_HISTORY || 610 match.type == AutocompleteMatch::SEARCH_SUGGEST) { 611 TemplateURLModel* model = tab_contents_->profile()->GetTemplateURLModel(); 612 template_url = model ? model->GetDefaultSearchProvider() : NULL; 613 } 614 return template_url; 615} 616