instant_controller.cc revision 4a5e2dc747d50c653511c68ccb2cfbfb740bd5a7
1// Copyright (c) 2010 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 "base/command_line.h" 8#include "base/metrics/histogram.h" 9#include "base/rand_util.h" 10#include "chrome/browser/autocomplete/autocomplete_match.h" 11#include "chrome/browser/instant/instant_delegate.h" 12#include "chrome/browser/instant/instant_loader.h" 13#include "chrome/browser/instant/instant_loader_manager.h" 14#include "chrome/browser/instant/promo_counter.h" 15#include "chrome/browser/platform_util.h" 16#include "chrome/browser/prefs/pref_service.h" 17#include "chrome/browser/profile.h" 18#include "chrome/browser/renderer_host/render_widget_host_view.h" 19#include "chrome/browser/search_engines/template_url.h" 20#include "chrome/browser/search_engines/template_url_model.h" 21#include "chrome/browser/tab_contents/tab_contents.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 27// Number of ms to delay between loading urls. 28static const int kUpdateDelayMS = 200; 29 30static InstantController::Type GetType(Profile* profile) { 31 return InstantController::IsEnabled(profile, 32 InstantController::PREDICTIVE_TYPE) ? 33 InstantController::PREDICTIVE_TYPE : InstantController::VERBATIM_TYPE; 34} 35 36InstantController::InstantController(Profile* profile, 37 InstantDelegate* delegate) 38 : delegate_(delegate), 39 tab_contents_(NULL), 40 is_active_(false), 41 commit_on_mouse_up_(false), 42 last_transition_type_(PageTransition::LINK), 43 type_(GetType(profile)) { 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 prefs->RegisterIntegerPref(prefs::kInstantType, 0); 62 PromoCounter::RegisterUserPrefs(prefs, prefs::kInstantPromo); 63} 64 65// static 66void InstantController::RecordMetrics(Profile* profile) { 67 if (!IsEnabled(profile)) 68 return; 69 70 PrefService* service = profile->GetPrefs(); 71 if (service) { 72 int64 enable_time = service->GetInt64(prefs::kInstantEnabledTime); 73 if (!enable_time) { 74 service->SetInt64(prefs::kInstantEnabledTime, 75 base::Time::Now().ToInternalValue()); 76 } else { 77 base::TimeDelta delta = 78 base::Time::Now() - base::Time::FromInternalValue(enable_time); 79 std::string name = IsEnabled(profile, PREDICTIVE_TYPE) ? 80 "Instant.EnabledTime.Predictive" : "Instant.EnabledTime.Verbatim"; 81 // Histogram from 1 hour to 30 days. 82 UMA_HISTOGRAM_CUSTOM_COUNTS(name, delta.InHours(), 1, 30 * 24, 50); 83 } 84 } 85} 86 87// static 88bool InstantController::IsEnabled(Profile* profile) { 89 return IsEnabled(profile, PREDICTIVE_TYPE) || 90 IsEnabled(profile, VERBATIM_TYPE); 91} 92 93// static 94bool InstantController::IsEnabled(Profile* profile, Type type) { 95 // CommandLine takes precedence. 96 CommandLine* cl = CommandLine::ForCurrentProcess(); 97 if (type == PREDICTIVE_TYPE && 98 cl->HasSwitch(switches::kEnablePredictiveInstant)) { 99 return true; 100 } 101 if (type == VERBATIM_TYPE && 102 cl->HasSwitch(switches::kEnableVerbatimInstant)) { 103 return true; 104 } 105 106 // Then prefs. 107 PrefService* prefs = profile->GetPrefs(); 108 if (!prefs->GetBoolean(prefs::kInstantEnabled)) 109 return false; 110 111 Type pref_type = prefs->GetInteger(prefs::kInstantType) == 112 static_cast<int>(PREDICTIVE_TYPE) ? PREDICTIVE_TYPE : VERBATIM_TYPE; 113 return pref_type == type; 114} 115 116// static 117void InstantController::Enable(Profile* profile) { 118 PromoCounter* promo_counter = profile->GetInstantPromoCounter(); 119 if (promo_counter) 120 promo_counter->Hide(); 121 122 PrefService* service = profile->GetPrefs(); 123 if (!service) 124 return; 125 126 service->SetBoolean(prefs::kInstantEnabled, true); 127 service->SetBoolean(prefs::kInstantConfirmDialogShown, true); 128 service->SetInt64(prefs::kInstantEnabledTime, 129 base::Time::Now().ToInternalValue()); 130 service->SetBoolean(prefs::kInstantEnabledOnce, true); 131 // Randomly pick a type. We're doing this to get feedback as to which variant 132 // folks prefer. 133 service->SetInteger(prefs::kInstantType, 134 base::RandInt(static_cast<int>(PREDICTIVE_TYPE), 135 static_cast<int>(LAST_TYPE))); 136} 137 138// static 139void InstantController::Disable(Profile* profile) { 140 PrefService* service = profile->GetPrefs(); 141 if (!service) 142 return; 143 144 service->SetBoolean(prefs::kInstantEnabled, false); 145 146 int64 enable_time = service->GetInt64(prefs::kInstantEnabledTime); 147 if (!enable_time) 148 return; 149 150 base::TimeDelta delta = 151 base::Time::Now() - base::Time::FromInternalValue(enable_time); 152 std::string name = IsEnabled(profile, PREDICTIVE_TYPE) ? 153 "Instant.TimeToDisable.Predictive" : "Instant.TimeToDisable.Verbatim"; 154 // histogram from 1 minute to 10 days. 155 UMA_HISTOGRAM_CUSTOM_COUNTS(name, delta.InMinutes(), 1, 60 * 24 * 10, 50); 156} 157 158 159void InstantController::Update(TabContents* tab_contents, 160 const AutocompleteMatch& match, 161 const string16& user_text, 162 string16* suggested_text) { 163 if (tab_contents != tab_contents_) 164 DestroyPreviewContents(); 165 166 const GURL& url = match.destination_url; 167 168 tab_contents_ = tab_contents; 169 commit_on_mouse_up_ = false; 170 last_transition_type_ = match.transition; 171 172 if (loader_manager_.get() && loader_manager_->active_loader()->url() == url) 173 return; 174 175 if (url.is_empty() || !url.is_valid() || !ShouldShowPreviewFor(match)) { 176 DestroyPreviewContents(); 177 return; 178 } 179 180 const TemplateURL* template_url = GetTemplateURL(match); 181 TemplateURLID template_url_id = template_url ? template_url->id() : 0; 182 183 if (!loader_manager_.get()) 184 loader_manager_.reset(new InstantLoaderManager(this)); 185 186 if (!is_active_) 187 delegate_->PrepareForInstant(); 188 189 if (ShouldUpdateNow(template_url_id, match.destination_url)) { 190 UpdateLoader(template_url, match.destination_url, match.transition, 191 user_text, suggested_text); 192 } else { 193 ScheduleUpdate(match.destination_url); 194 } 195 196 NotificationService::current()->Notify( 197 NotificationType::INSTANT_CONTROLLER_UPDATED, 198 Source<InstantController>(this), 199 NotificationService::NoDetails()); 200} 201 202void InstantController::SetOmniboxBounds(const gfx::Rect& bounds) { 203 if (omnibox_bounds_ == bounds) 204 return; 205 206 if (loader_manager_.get()) { 207 omnibox_bounds_ = bounds; 208 if (loader_manager_->current_loader()) 209 loader_manager_->current_loader()->SetOmniboxBounds(bounds); 210 if (loader_manager_->pending_loader()) 211 loader_manager_->pending_loader()->SetOmniboxBounds(bounds); 212 } 213} 214 215void InstantController::DestroyPreviewContents() { 216 if (!loader_manager_.get()) { 217 // We're not showing anything, nothing to do. 218 return; 219 } 220 221 delegate_->HideInstant(); 222 delete ReleasePreviewContents(INSTANT_COMMIT_DESTROY); 223} 224 225bool InstantController::IsCurrent() { 226 return loader_manager_.get() && loader_manager_->active_loader()->ready() && 227 !update_timer_.IsRunning(); 228} 229 230void InstantController::CommitCurrentPreview(InstantCommitType type) { 231 DCHECK(loader_manager_.get()); 232 DCHECK(loader_manager_->current_loader()); 233 TabContents* tab = ReleasePreviewContents(type); 234 delegate_->CommitInstant(tab); 235 CompleteRelease(tab); 236} 237 238void InstantController::SetCommitOnMouseUp() { 239 commit_on_mouse_up_ = true; 240} 241 242bool InstantController::IsMouseDownFromActivate() { 243 DCHECK(loader_manager_.get()); 244 DCHECK(loader_manager_->current_loader()); 245 return loader_manager_->current_loader()->IsMouseDownFromActivate(); 246} 247 248void InstantController::OnAutocompleteLostFocus( 249 gfx::NativeView view_gaining_focus) { 250 if (!is_active() || !GetPreviewContents()) 251 return; 252 253 RenderWidgetHostView* rwhv = 254 GetPreviewContents()->GetRenderWidgetHostView(); 255 if (!view_gaining_focus || !rwhv) { 256 DestroyPreviewContents(); 257 return; 258 } 259 260 gfx::NativeView tab_view = GetPreviewContents()->GetNativeView(); 261 // Focus is going to the renderer. 262 if (rwhv->GetNativeView() == view_gaining_focus || 263 tab_view == view_gaining_focus) { 264 if (!IsMouseDownFromActivate()) { 265 // If the mouse is not down, focus is not going to the renderer. Someone 266 // else moved focus and we shouldn't commit. 267 DestroyPreviewContents(); 268 return; 269 } 270 271 if (IsShowingInstant()) { 272 // We're showing instant results. As instant results may shift when 273 // committing we commit on the mouse up. This way a slow click still 274 // works fine. 275 SetCommitOnMouseUp(); 276 return; 277 } 278 279 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 280 return; 281 } 282 283 // Walk up the view hierarchy. If the view gaining focus is a subview of the 284 // TabContents view (such as a windowed plugin or http auth dialog), we want 285 // to keep the preview contents. Otherwise, focus has gone somewhere else, 286 // such as the JS inspector, and we want to cancel the preview. 287 gfx::NativeView view_gaining_focus_ancestor = view_gaining_focus; 288 while (view_gaining_focus_ancestor && 289 view_gaining_focus_ancestor != tab_view) { 290 view_gaining_focus_ancestor = 291 platform_util::GetParent(view_gaining_focus_ancestor); 292 } 293 294 if (view_gaining_focus_ancestor) { 295 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 296 return; 297 } 298 299 DestroyPreviewContents(); 300} 301 302TabContents* InstantController::ReleasePreviewContents(InstantCommitType type) { 303 if (!loader_manager_.get()) 304 return NULL; 305 306 scoped_ptr<InstantLoader> loader(loader_manager_->ReleaseCurrentLoader()); 307 TabContents* tab = loader->ReleasePreviewContents(type); 308 309 ClearBlacklist(); 310 is_active_ = false; 311 omnibox_bounds_ = gfx::Rect(); 312 commit_on_mouse_up_ = false; 313 loader_manager_.reset(NULL); 314 update_timer_.Stop(); 315 return tab; 316} 317 318void InstantController::CompleteRelease(TabContents* tab) { 319 tab->SetAllContentsBlocked(false); 320} 321 322TabContents* InstantController::GetPreviewContents() { 323 return loader_manager_.get() ? 324 loader_manager_->current_loader()->preview_contents() : NULL; 325} 326 327bool InstantController::IsShowingInstant() { 328 return loader_manager_.get() && 329 loader_manager_->current_loader()->is_showing_instant(); 330} 331 332void InstantController::ShowInstantLoader(InstantLoader* loader) { 333 DCHECK(loader_manager_.get()); 334 if (loader_manager_->current_loader() == loader) { 335 is_active_ = true; 336 delegate_->ShowInstant(loader->preview_contents()); 337 } else if (loader_manager_->pending_loader() == loader) { 338 scoped_ptr<InstantLoader> old_loader; 339 loader_manager_->MakePendingCurrent(&old_loader); 340 delegate_->ShowInstant(loader->preview_contents()); 341 } else { 342 // The loader supports instant but isn't active yet. Nothing to do. 343 } 344 345 NotificationService::current()->Notify( 346 NotificationType::INSTANT_CONTROLLER_SHOWN, 347 Source<InstantController>(this), 348 NotificationService::NoDetails()); 349} 350 351void InstantController::SetSuggestedTextFor(InstantLoader* loader, 352 const string16& text) { 353 if (loader_manager_->current_loader() == loader) 354 delegate_->SetSuggestedText(text); 355} 356 357gfx::Rect InstantController::GetInstantBounds() { 358 return delegate_->GetInstantBounds(); 359} 360 361bool InstantController::ShouldCommitInstantOnMouseUp() { 362 return commit_on_mouse_up_; 363} 364 365void InstantController::CommitInstantLoader(InstantLoader* loader) { 366 if (loader_manager_.get() && loader_manager_->current_loader() == loader) { 367 CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST); 368 } else { 369 // This can happen if the mouse was down, we swapped out the preview and 370 // the mouse was released. Generally this shouldn't happen, but if it does 371 // revert. 372 DestroyPreviewContents(); 373 } 374} 375 376void InstantController::InstantLoaderDoesntSupportInstant( 377 InstantLoader* loader, 378 bool needs_reload, 379 const GURL& url_to_load) { 380 DCHECK(!loader->ready()); // We better not be showing this loader. 381 DCHECK(loader->template_url_id()); 382 383 BlacklistFromInstant(loader->template_url_id()); 384 385 if (loader_manager_->active_loader() == loader) { 386 // The loader is active. Continue to use it, but make sure it isn't tied to 387 // to the search engine anymore. ClearTemplateURLID ends up showing the 388 // loader. 389 loader_manager_->RemoveLoaderFromInstant(loader); 390 loader->ClearTemplateURLID(); 391 392 if (needs_reload) { 393 string16 suggested_text; 394 loader->Update(tab_contents_, 0, url_to_load, last_transition_type_, 395 loader->user_text(), &suggested_text); 396 } 397 } else { 398 loader_manager_->DestroyLoader(loader); 399 loader = NULL; 400 } 401} 402 403bool InstantController::ShouldUpdateNow(TemplateURLID instant_id, 404 const GURL& url) { 405 DCHECK(loader_manager_.get()); 406 407 if (instant_id) { 408 // Update sites that support instant immediately, they can do their own 409 // throttling. 410 return true; 411 } 412 413 if (url.SchemeIsFile()) 414 return true; // File urls should load quickly, so don't delay loading them. 415 416 if (loader_manager_->WillUpateChangeActiveLoader(instant_id)) { 417 // If Update would change loaders, update now. This indicates transitioning 418 // from an instant to non-instant loader. 419 return true; 420 } 421 422 InstantLoader* active_loader = loader_manager_->active_loader(); 423 // WillUpateChangeActiveLoader should return true if no active loader, so 424 // we know there will be an active loader if we get here. 425 DCHECK(active_loader); 426 // Immediately update if the hosts differ, otherwise we'll delay the update. 427 return active_loader->url().host() != url.host(); 428} 429 430void InstantController::ScheduleUpdate(const GURL& url) { 431 scheduled_url_ = url; 432 433 if (update_timer_.IsRunning()) 434 update_timer_.Stop(); 435 update_timer_.Start(base::TimeDelta::FromMilliseconds(kUpdateDelayMS), 436 this, &InstantController::ProcessScheduledUpdate); 437} 438 439void InstantController::ProcessScheduledUpdate() { 440 DCHECK(loader_manager_.get()); 441 442 // We only delay loading of sites that don't support instant, so we can ignore 443 // suggested_text here. 444 string16 suggested_text; 445 UpdateLoader(NULL, scheduled_url_, last_transition_type_, string16(), 446 &suggested_text); 447} 448 449void InstantController::UpdateLoader(const TemplateURL* template_url, 450 const GURL& url, 451 PageTransition::Type transition_type, 452 const string16& user_text, 453 string16* suggested_text) { 454 update_timer_.Stop(); 455 456 InstantLoader* old_loader = loader_manager_->current_loader(); 457 scoped_ptr<InstantLoader> owned_loader; 458 TemplateURLID template_url_id = template_url ? template_url->id() : 0; 459 InstantLoader* new_loader = 460 loader_manager_->UpdateLoader(template_url_id, &owned_loader); 461 462 new_loader->SetOmniboxBounds(omnibox_bounds_); 463 new_loader->Update(tab_contents_, template_url, url, transition_type, 464 user_text, suggested_text); 465 if (old_loader != new_loader && new_loader->ready()) 466 delegate_->ShowInstant(new_loader->preview_contents()); 467} 468 469bool InstantController::ShouldShowPreviewFor(const AutocompleteMatch& match) { 470 if (match.destination_url.SchemeIs(chrome::kJavaScriptScheme)) 471 return false; 472 473 // Extension keywords don't have a real destionation URL. 474 if (match.template_url && match.template_url->IsExtensionKeyword()) 475 return false; 476 477 return true; 478} 479 480void InstantController::BlacklistFromInstant(TemplateURLID id) { 481 blacklisted_ids_.insert(id); 482} 483 484bool InstantController::IsBlacklistedFromInstant(TemplateURLID id) { 485 return blacklisted_ids_.count(id) > 0; 486} 487 488void InstantController::ClearBlacklist() { 489 blacklisted_ids_.clear(); 490} 491 492const TemplateURL* InstantController::GetTemplateURL( 493 const AutocompleteMatch& match) { 494 if (type_ == VERBATIM_TYPE) { 495 // When using VERBATIM_TYPE we don't want to attempt to use the instant 496 // JavaScript API, otherwise the page would show predictive results. By 497 // returning NULL here we ensure we don't attempt to use the instant API. 498 // 499 // TODO: when the full search box API is in place we can lift this 500 // restriction and force the page to show verbatim results always. 501 return NULL; 502 } 503 504 const TemplateURL* template_url = match.template_url; 505 if (match.type == AutocompleteMatch::SEARCH_WHAT_YOU_TYPED || 506 match.type == AutocompleteMatch::SEARCH_HISTORY || 507 match.type == AutocompleteMatch::SEARCH_SUGGEST) { 508 TemplateURLModel* model = tab_contents_->profile()->GetTemplateURLModel(); 509 template_url = model ? model->GetDefaultSearchProvider() : NULL; 510 } 511 if (template_url && template_url->id() && 512 template_url->instant_url() && 513 !IsBlacklistedFromInstant(template_url->id()) && 514 template_url->instant_url()->SupportsReplacement()) { 515 return template_url; 516 } 517 return NULL; 518} 519