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/search_tab_helper.h"
6
7#include "build/build_config.h"
8#include "chrome/browser/profiles/profile.h"
9#include "chrome/browser/search/instant_service.h"
10#include "chrome/browser/search/instant_service_factory.h"
11#include "chrome/browser/search/search.h"
12#include "chrome/browser/ui/browser.h"
13#include "chrome/browser/ui/browser_finder.h"
14#include "chrome/common/render_messages.h"
15#include "chrome/common/url_constants.h"
16#include "content/public/browser/navigation_controller.h"
17#include "content/public/browser/navigation_details.h"
18#include "content/public/browser/navigation_entry.h"
19#include "content/public/browser/navigation_type.h"
20#include "content/public/browser/notification_service.h"
21#include "content/public/browser/notification_types.h"
22#include "content/public/browser/render_process_host.h"
23#include "content/public/browser/web_contents.h"
24#include "grit/generated_resources.h"
25#include "ui/base/l10n/l10n_util.h"
26
27DEFINE_WEB_CONTENTS_USER_DATA_KEY(SearchTabHelper);
28
29namespace {
30
31bool IsNTP(const content::WebContents* contents) {
32  // We can't use WebContents::GetURL() because that uses the active entry,
33  // whereas we want the visible entry.
34  const content::NavigationEntry* entry =
35      contents->GetController().GetVisibleEntry();
36  if (entry && entry->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL))
37    return true;
38
39  return chrome::IsInstantNTP(contents);
40}
41
42bool IsSearchResults(const content::WebContents* contents) {
43  return !chrome::GetSearchTerms(contents).empty();
44}
45
46// TODO(kmadhusu): Move this helper from anonymous namespace to chrome
47// namespace and remove InstantPage::IsLocal().
48bool IsLocal(const content::WebContents* contents) {
49  return contents &&
50      contents->GetURL() == GURL(chrome::kChromeSearchLocalNtpUrl);
51}
52
53// Returns true if |contents| are rendered inside an Instant process.
54bool InInstantProcess(Profile* profile,
55                      const content::WebContents* contents) {
56  InstantService* instant_service =
57      InstantServiceFactory::GetForProfile(profile);
58  return instant_service &&
59      instant_service->IsInstantProcess(
60          contents->GetRenderProcessHost()->GetID());
61}
62
63// Updates the location bar to reflect |contents| Instant support state.
64void UpdateLocationBar(content::WebContents* contents) {
65// iOS and Android doesn't use the Instant framework.
66#if !defined(OS_IOS) && !defined(OS_ANDROID)
67  if (!contents)
68    return;
69
70  Browser* browser = chrome::FindBrowserWithWebContents(contents);
71  if (!browser)
72    return;
73  browser->OnWebContentsInstantSupportDisabled(contents);
74#endif
75}
76
77}  // namespace
78
79SearchTabHelper::SearchTabHelper(content::WebContents* web_contents)
80    : WebContentsObserver(web_contents),
81      is_search_enabled_(chrome::IsInstantExtendedAPIEnabled()),
82      user_input_in_progress_(false),
83      web_contents_(web_contents) {
84  if (!is_search_enabled_)
85    return;
86
87  // Observe NOTIFICATION_NAV_ENTRY_COMMITTED events so we can reset state
88  // associated with the WebContents  (such as mode, last known most visited
89  // items, instant support state etc).
90  registrar_.Add(
91      this,
92      content::NOTIFICATION_NAV_ENTRY_COMMITTED,
93      content::Source<content::NavigationController>(
94          &web_contents->GetController()));
95}
96
97SearchTabHelper::~SearchTabHelper() {
98}
99
100void SearchTabHelper::InitForPreloadedNTP() {
101  UpdateMode(true, true);
102}
103
104void SearchTabHelper::OmniboxEditModelChanged(bool user_input_in_progress,
105                                              bool cancelling) {
106  if (!is_search_enabled_)
107    return;
108
109  user_input_in_progress_ = user_input_in_progress;
110  if (!user_input_in_progress && !cancelling)
111    return;
112
113  UpdateMode(false, false);
114}
115
116void SearchTabHelper::NavigationEntryUpdated() {
117  if (!is_search_enabled_)
118    return;
119
120  UpdateMode(false, false);
121}
122
123void SearchTabHelper::InstantSupportChanged(bool instant_support) {
124  if (!is_search_enabled_)
125    return;
126
127  InstantSupportState new_state = instant_support ? INSTANT_SUPPORT_YES :
128      INSTANT_SUPPORT_NO;
129
130  model_.SetInstantSupportState(new_state);
131
132  content::NavigationEntry* entry =
133      web_contents_->GetController().GetVisibleEntry();
134  if (entry) {
135    chrome::SetInstantSupportStateInNavigationEntry(new_state, entry);
136    if (!instant_support)
137      UpdateLocationBar(web_contents_);
138  }
139}
140
141bool SearchTabHelper::SupportsInstant() const {
142  return model_.instant_support() == INSTANT_SUPPORT_YES;
143}
144
145void SearchTabHelper::Observe(
146    int type,
147    const content::NotificationSource& source,
148    const content::NotificationDetails& details) {
149  DCHECK_EQ(content::NOTIFICATION_NAV_ENTRY_COMMITTED, type);
150  content::LoadCommittedDetails* load_details =
151      content::Details<content::LoadCommittedDetails>(details).ptr();
152  if (!load_details->is_main_frame)
153    return;
154
155  UpdateMode(true, false);
156
157  content::NavigationEntry* entry =
158      web_contents_->GetController().GetVisibleEntry();
159  DCHECK(entry);
160
161  // Already determined the instant support state for this page, do not reset
162  // the instant support state.
163  //
164  // When we get a navigation entry committed event, there seem to be two ways
165  // to tell whether the navigation was "in-page". Ideally, when
166  // LoadCommittedDetails::is_in_page is true, we should have
167  // LoadCommittedDetails::type to be NAVIGATION_TYPE_IN_PAGE. Unfortunately,
168  // they are different in some cases. To workaround this bug, we are checking
169  // (is_in_page || type == NAVIGATION_TYPE_IN_PAGE). Please refer to
170  // crbug.com/251330 for more details.
171  if (load_details->is_in_page ||
172      load_details->type == content::NAVIGATION_TYPE_IN_PAGE) {
173    // When an "in-page" navigation happens, we will not receive a
174    // DidFinishLoad() event. Therefore, we will not determine the Instant
175    // support for the navigated page. So, copy over the Instant support from
176    // the previous entry. If the page does not support Instant, update the
177    // location bar from here to turn off search terms replacement.
178    chrome::SetInstantSupportStateInNavigationEntry(model_.instant_support(),
179                                                    entry);
180    if (model_.instant_support() == INSTANT_SUPPORT_NO)
181      UpdateLocationBar(web_contents_);
182    return;
183  }
184
185  model_.SetInstantSupportState(INSTANT_SUPPORT_UNKNOWN);
186  model_.SetVoiceSearchSupported(false);
187  chrome::SetInstantSupportStateInNavigationEntry(model_.instant_support(),
188                                                  entry);
189}
190
191void SearchTabHelper::DidNavigateMainFrame(
192    const content::LoadCommittedDetails& details,
193    const content::FrameNavigateParams& params) {
194  // Always set the title on the new tab page to be the one from our UI
195  // resources.  Normally, we set the title when we begin a NTP load, but it
196  // can get reset in several places (like when you press Reload). This check
197  // ensures that the title is properly set to the string defined by the Chrome
198  // UI language (rather than the server language) in all cases.
199  //
200  // We only override the title when it's nonempty to allow the page to set the
201  // title if it really wants. An empty title means to use the default. There's
202  // also a race condition between this code and the page's SetTitle call which
203  // this rule avoids.
204  content::NavigationEntry* entry =
205      web_contents_->GetController().GetActiveEntry();
206  if (entry && entry->GetTitle().empty() &&
207      (entry->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL) ||
208       chrome::NavEntryIsInstantNTP(web_contents_, entry))) {
209    entry->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE));
210  }
211}
212
213bool SearchTabHelper::OnMessageReceived(const IPC::Message& message) {
214  bool handled = true;
215  IPC_BEGIN_MESSAGE_MAP(SearchTabHelper, message)
216    IPC_MESSAGE_HANDLER(ChromeViewHostMsg_InstantSupportDetermined,
217                        OnInstantSupportDetermined)
218    IPC_MESSAGE_HANDLER(ChromeViewHostMsg_SetVoiceSearchSupported,
219                        OnSetVoiceSearchSupported)
220    IPC_MESSAGE_UNHANDLED(handled = false)
221  IPC_END_MESSAGE_MAP()
222  return handled;
223}
224
225void SearchTabHelper::DidFinishLoad(
226    int64 /* frame_id */,
227    const GURL&  /* validated_url */,
228    bool is_main_frame,
229    content::RenderViewHost* /* render_view_host */) {
230  if (is_main_frame)
231    DetermineIfPageSupportsInstant();
232}
233
234void SearchTabHelper::UpdateMode(bool update_origin, bool is_preloaded_ntp) {
235  SearchMode::Type type = SearchMode::MODE_DEFAULT;
236  SearchMode::Origin origin = SearchMode::ORIGIN_DEFAULT;
237  if (IsNTP(web_contents_) || is_preloaded_ntp) {
238    type = SearchMode::MODE_NTP;
239    origin = SearchMode::ORIGIN_NTP;
240  } else if (IsSearchResults(web_contents_)) {
241    type = SearchMode::MODE_SEARCH_RESULTS;
242    origin = SearchMode::ORIGIN_SEARCH;
243  }
244  if (!update_origin)
245    origin = model_.mode().origin;
246  if (user_input_in_progress_)
247    type = SearchMode::MODE_SEARCH_SUGGESTIONS;
248  model_.SetMode(SearchMode(type, origin));
249}
250
251void SearchTabHelper::DetermineIfPageSupportsInstant() {
252  Profile* profile =
253      Profile::FromBrowserContext(web_contents_->GetBrowserContext());
254  if (!InInstantProcess(profile, web_contents_)) {
255    // The page is not in the Instant process. This page does not support
256    // instant. If we send an IPC message to a page that is not in the Instant
257    // process, it will never receive it and will never respond. Therefore,
258    // return immediately.
259    InstantSupportChanged(false);
260  } else if (IsLocal(web_contents_)) {
261    // Local pages always support Instant.
262    InstantSupportChanged(true);
263  } else {
264    Send(new ChromeViewMsg_DetermineIfPageSupportsInstant(routing_id()));
265  }
266}
267
268void SearchTabHelper::OnInstantSupportDetermined(int page_id,
269                                                 bool instant_support) {
270  if (!web_contents()->IsActiveEntry(page_id))
271    return;
272
273  InstantSupportChanged(instant_support);
274}
275
276void SearchTabHelper::OnSetVoiceSearchSupported(int page_id, bool supported) {
277  if (web_contents()->IsActiveEntry(page_id))
278    model_.SetVoiceSearchSupported(supported);
279}
280