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