1// Copyright 2012 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/ui/search/instant_controller.h"
6
7#include "base/prefs/pref_service.h"
8#include "base/strings/stringprintf.h"
9#include "chrome/browser/chrome_notification_types.h"
10#include "chrome/browser/content_settings/content_settings_provider.h"
11#include "chrome/browser/content_settings/host_content_settings_map.h"
12#include "chrome/browser/platform_util.h"
13#include "chrome/browser/profiles/profile.h"
14#include "chrome/browser/search/instant_service.h"
15#include "chrome/browser/search/instant_service_factory.h"
16#include "chrome/browser/search/search.h"
17#include "chrome/browser/search_engines/search_terms_data.h"
18#include "chrome/browser/search_engines/template_url_service.h"
19#include "chrome/browser/search_engines/template_url_service_factory.h"
20#include "chrome/browser/ui/browser_instant_controller.h"
21#include "chrome/browser/ui/search/instant_search_prerenderer.h"
22#include "chrome/browser/ui/search/instant_tab.h"
23#include "chrome/browser/ui/search/search_tab_helper.h"
24#include "chrome/common/chrome_switches.h"
25#include "chrome/common/content_settings_types.h"
26#include "chrome/common/pref_names.h"
27#include "chrome/common/search_urls.h"
28#include "chrome/common/url_constants.h"
29#include "components/sessions/serialized_navigation_entry.h"
30#include "content/public/browser/navigation_entry.h"
31#include "content/public/browser/notification_service.h"
32#include "content/public/browser/render_process_host.h"
33#include "content/public/browser/render_widget_host_view.h"
34#include "content/public/browser/web_contents.h"
35#include "content/public/browser/web_contents_view.h"
36#include "net/base/escape.h"
37#include "net/base/network_change_notifier.h"
38#include "url/gurl.h"
39
40#if defined(TOOLKIT_VIEWS)
41#include "ui/views/widget/widget.h"
42#endif
43
44namespace {
45
46bool IsContentsFrom(const InstantPage* page,
47                    const content::WebContents* contents) {
48  return page && (page->contents() == contents);
49}
50
51// Adds a transient NavigationEntry to the supplied |contents|'s
52// NavigationController if the page's URL has not already been updated with the
53// supplied |search_terms|. Sets the |search_terms| on the transient entry for
54// search terms extraction to work correctly.
55void EnsureSearchTermsAreSet(content::WebContents* contents,
56                             const base::string16& search_terms) {
57  content::NavigationController* controller = &contents->GetController();
58
59  // If search terms are already correct or there is already a transient entry
60  // (there shouldn't be), bail out early.
61  if (chrome::GetSearchTerms(contents) == search_terms ||
62      controller->GetTransientEntry())
63    return;
64
65  const content::NavigationEntry* entry = controller->GetLastCommittedEntry();
66  content::NavigationEntry* transient = controller->CreateNavigationEntry(
67      entry->GetURL(),
68      entry->GetReferrer(),
69      entry->GetTransitionType(),
70      false,
71      std::string(),
72      contents->GetBrowserContext());
73  transient->SetExtraData(sessions::kSearchTermsKey, search_terms);
74  controller->SetTransientEntry(transient);
75
76  SearchTabHelper::FromWebContents(contents)->NavigationEntryUpdated();
77}
78
79}  // namespace
80
81InstantController::InstantController(BrowserInstantController* browser)
82    : browser_(browser),
83      omnibox_focus_state_(OMNIBOX_FOCUS_NONE),
84      omnibox_focus_change_reason_(OMNIBOX_FOCUS_CHANGE_EXPLICIT),
85      omnibox_bounds_(-1, -1, 0, 0) {
86}
87
88InstantController::~InstantController() {
89}
90
91void InstantController::SetOmniboxBounds(const gfx::Rect& bounds) {
92  if (omnibox_bounds_ == bounds)
93    return;
94
95  omnibox_bounds_ = bounds;
96  if (instant_tab_)
97    instant_tab_->sender()->SetOmniboxBounds(omnibox_bounds_);
98}
99
100void InstantController::SetSuggestionToPrefetch(
101    const InstantSuggestion& suggestion) {
102  if (instant_tab_ &&
103      SearchTabHelper::FromWebContents(instant_tab_->contents())->
104          IsSearchResultsPage()) {
105    if (chrome::ShouldPrefetchSearchResultsOnSRP() ||
106        chrome::ShouldPrefetchSearchResults()) {
107      SearchTabHelper::FromWebContents(instant_tab_->contents())->
108          SetSuggestionToPrefetch(suggestion);
109    }
110  } else {
111    if (chrome::ShouldPrefetchSearchResults()) {
112      InstantSearchPrerenderer* prerenderer =
113          InstantSearchPrerenderer::GetForProfile(profile());
114      if (prerenderer)
115        prerenderer->Prerender(suggestion);
116    }
117  }
118}
119
120void InstantController::InstantPageLoadFailed(content::WebContents* contents) {
121  DCHECK(IsContentsFrom(instant_tab(), contents));
122
123  // Verify we're not already on a local page and that the URL precisely
124  // equals the instant_url (minus the query params, as those will be filled
125  // in by template values).  This check is necessary to make sure we don't
126  // inadvertently redirect to the local NTP if someone, say, reloads a SRP
127  // while offline, as a committed results page still counts as an instant
128  // url.  We also check to make sure there's no forward history, as if
129  // someone hits the back button a lot when offline and returns to a NTP
130  // we don't want to redirect and nuke their forward history stack.
131  const GURL& current_url = contents->GetURL();
132  GURL instant_url = chrome::GetInstantURL(profile(),
133                                           chrome::kDisableStartMargin, false);
134  if (instant_tab_->IsLocal() ||
135      !search::MatchesOriginAndPath(instant_url, current_url) ||
136      !current_url.ref().empty() ||
137      contents->GetController().CanGoForward())
138    return;
139  LOG_INSTANT_DEBUG_EVENT(this, "InstantPageLoadFailed: instant_tab");
140  RedirectToLocalNTP(contents);
141}
142
143bool InstantController::SubmitQuery(const base::string16& search_terms) {
144  if (instant_tab_ && instant_tab_->supports_instant() &&
145      search_mode_.is_origin_search()) {
146    // Use |instant_tab_| to run the query if we're already on a search results
147    // page. (NOTE: in particular, we do not send the query to NTPs.)
148    SearchTabHelper::FromWebContents(instant_tab_->contents())->Submit(
149        search_terms);
150    instant_tab_->contents()->GetView()->Focus();
151    EnsureSearchTermsAreSet(instant_tab_->contents(), search_terms);
152    return true;
153  }
154  return false;
155}
156
157void InstantController::OmniboxFocusChanged(
158    OmniboxFocusState state,
159    OmniboxFocusChangeReason reason,
160    gfx::NativeView view_gaining_focus) {
161  LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
162      "OmniboxFocusChanged: %d to %d for reason %d", omnibox_focus_state_,
163      state, reason));
164
165  omnibox_focus_state_ = state;
166  if (!instant_tab_)
167    return;
168
169  content::NotificationService::current()->Notify(
170      chrome::NOTIFICATION_OMNIBOX_FOCUS_CHANGED,
171      content::Source<InstantController>(this),
172      content::NotificationService::NoDetails());
173
174  instant_tab_->sender()->FocusChanged(omnibox_focus_state_, reason);
175  // Don't send oninputstart/oninputend updates in response to focus changes
176  // if there's a navigation in progress. This prevents Chrome from sending
177  // a spurious oninputend when the user accepts a match in the omnibox.
178  if (instant_tab_->contents()->GetController().GetPendingEntry() == NULL)
179    instant_tab_->sender()->SetInputInProgress(IsInputInProgress());
180}
181
182void InstantController::SearchModeChanged(const SearchMode& old_mode,
183                                          const SearchMode& new_mode) {
184  LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
185      "SearchModeChanged: [origin:mode] %d:%d to %d:%d", old_mode.origin,
186      old_mode.mode, new_mode.origin, new_mode.mode));
187
188  search_mode_ = new_mode;
189  ResetInstantTab();
190
191  if (instant_tab_ && old_mode.is_ntp() != new_mode.is_ntp())
192    instant_tab_->sender()->SetInputInProgress(IsInputInProgress());
193}
194
195void InstantController::ActiveTabChanged() {
196  LOG_INSTANT_DEBUG_EVENT(this, "ActiveTabChanged");
197  ResetInstantTab();
198}
199
200void InstantController::TabDeactivated(content::WebContents* contents) {
201  // If user is deactivating an NTP tab, log the number of mouseovers for this
202  // NTP session.
203  if (chrome::IsInstantNTP(contents))
204    InstantTab::EmitNtpStatistics(contents);
205}
206
207void InstantController::LogDebugEvent(const std::string& info) const {
208  DVLOG(1) << info;
209
210  debug_events_.push_front(std::make_pair(
211      base::Time::Now().ToInternalValue(), info));
212  static const size_t kMaxDebugEventSize = 2000;
213  if (debug_events_.size() > kMaxDebugEventSize)
214    debug_events_.pop_back();
215}
216
217void InstantController::ClearDebugEvents() {
218  debug_events_.clear();
219}
220
221Profile* InstantController::profile() const {
222  return browser_->profile();
223}
224
225InstantTab* InstantController::instant_tab() const {
226  return instant_tab_.get();
227}
228
229void InstantController::InstantSupportChanged(
230    InstantSupportState instant_support) {
231  // Handle INSTANT_SUPPORT_YES here because InstantPage is not hooked up to the
232  // active tab. Search model changed listener in InstantPage will handle other
233  // cases.
234  if (instant_support != INSTANT_SUPPORT_YES)
235    return;
236
237  ResetInstantTab();
238}
239
240void InstantController::InstantSupportDetermined(
241    const content::WebContents* contents,
242    bool supports_instant) {
243  DCHECK(IsContentsFrom(instant_tab(), contents));
244
245  if (!supports_instant)
246    base::MessageLoop::current()->DeleteSoon(FROM_HERE, instant_tab_.release());
247
248  content::NotificationService::current()->Notify(
249      chrome::NOTIFICATION_INSTANT_TAB_SUPPORT_DETERMINED,
250      content::Source<InstantController>(this),
251      content::NotificationService::NoDetails());
252}
253
254void InstantController::InstantPageAboutToNavigateMainFrame(
255    const content::WebContents* contents,
256    const GURL& url) {
257  DCHECK(IsContentsFrom(instant_tab(), contents));
258
259  // The Instant tab navigated.  Send it the data it needs to display
260  // properly.
261  UpdateInfoForInstantTab();
262}
263
264void InstantController::ResetInstantTab() {
265  if (!search_mode_.is_origin_default()) {
266    content::WebContents* active_tab = browser_->GetActiveWebContents();
267    if (!instant_tab_ || active_tab != instant_tab_->contents()) {
268      instant_tab_.reset(new InstantTab(this, browser_->profile()));
269      instant_tab_->Init(active_tab);
270      UpdateInfoForInstantTab();
271    }
272  } else {
273    instant_tab_.reset();
274  }
275}
276
277void InstantController::UpdateInfoForInstantTab() {
278  if (instant_tab_) {
279    instant_tab_->sender()->SetOmniboxBounds(omnibox_bounds_);
280
281    // Update theme details.
282    InstantService* instant_service = GetInstantService();
283    if (instant_service) {
284      instant_service->UpdateThemeInfo();
285      instant_service->UpdateMostVisitedItemsInfo();
286    }
287
288    instant_tab_->sender()->FocusChanged(omnibox_focus_state_,
289                                         omnibox_focus_change_reason_);
290    instant_tab_->sender()->SetInputInProgress(IsInputInProgress());
291  }
292}
293
294bool InstantController::IsInputInProgress() const {
295  return !search_mode_.is_ntp() &&
296      omnibox_focus_state_ == OMNIBOX_FOCUS_VISIBLE;
297}
298
299void InstantController::RedirectToLocalNTP(content::WebContents* contents) {
300  contents->GetController().LoadURL(
301      GURL(chrome::kChromeSearchLocalNtpUrl),
302      content::Referrer(),
303      content::PAGE_TRANSITION_SERVER_REDIRECT,
304      std::string());  // No extra headers.
305  // TODO(dcblack): Remove extraneous history entry caused by 404s.
306  // Note that the base case of a 204 being returned doesn't push a history
307  // entry.
308}
309
310InstantService* InstantController::GetInstantService() const {
311  return InstantServiceFactory::GetForProfile(profile());
312}
313