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