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.
5 #include "chrome/browser/ui/search/search_tab_helper.h"
9 #include "base/memory/scoped_ptr.h"
10 #include "base/metrics/histogram.h"
11 #include "base/strings/string16.h"
12 #include "base/strings/string_util.h"
13 #include "build/build_config.h"
14 #include "chrome/browser/history/most_visited_tiles_experiment.h"
15 #include "chrome/browser/history/top_sites.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/search/instant_service.h"
18 #include "chrome/browser/search/instant_service_factory.h"
19 #include "chrome/browser/search/search.h"
20 #include "chrome/browser/signin/signin_manager.h"
21 #include "chrome/browser/signin/signin_manager_factory.h"
22 #include "chrome/browser/ui/app_list/app_list_util.h"
23 #include "chrome/browser/ui/browser.h"
24 #include "chrome/browser/ui/browser_finder.h"
25 #include "chrome/browser/ui/browser_navigator.h"
26 #include "chrome/browser/ui/browser_window.h"
27 #include "chrome/browser/ui/omnibox/location_bar.h"
28 #include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
29 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
30 #include "chrome/browser/ui/omnibox/omnibox_view.h"
31 #include "chrome/browser/ui/search/search_ipc_router_policy_impl.h"
32 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
33 #include "chrome/browser/ui/tabs/tab_strip_model.h"
34 #include "chrome/browser/ui/tabs/tab_strip_model_utils.h"
35 #include "chrome/browser/ui/webui/ntp/ntp_user_data_logger.h"
36 #include "chrome/common/url_constants.h"
37 #include "content/public/browser/navigation_controller.h"
38 #include "content/public/browser/navigation_details.h"
39 #include "content/public/browser/navigation_entry.h"
40 #include "content/public/browser/navigation_type.h"
41 #include "content/public/browser/render_process_host.h"
42 #include "content/public/browser/user_metrics.h"
43 #include "content/public/browser/web_contents.h"
44 #include "content/public/browser/web_contents_view.h"
45 #include "content/public/common/page_transition_types.h"
46 #include "content/public/common/referrer.h"
47 #include "grit/generated_resources.h"
48 #include "net/base/net_errors.h"
49 #include "ui/base/l10n/l10n_util.h"
52 DEFINE_WEB_CONTENTS_USER_DATA_KEY(SearchTabHelper
);
56 // For reporting Cacheable NTP navigations.
57 enum CacheableNTPLoad
{
58 CACHEABLE_NTP_LOAD_FAILED
= 0,
59 CACHEABLE_NTP_LOAD_SUCCEEDED
= 1,
60 CACHEABLE_NTP_LOAD_MAX
= 2
63 void RecordCacheableNTPLoadHistogram(bool succeeded
) {
64 UMA_HISTOGRAM_ENUMERATION("InstantExtended.CacheableNTPLoad",
65 succeeded
? CACHEABLE_NTP_LOAD_SUCCEEDED
:
66 CACHEABLE_NTP_LOAD_FAILED
,
67 CACHEABLE_NTP_LOAD_MAX
);
70 bool IsCacheableNTP(const content::WebContents
* contents
) {
71 const content::NavigationEntry
* entry
=
72 contents
->GetController().GetLastCommittedEntry();
73 return chrome::NavEntryIsInstantNTP(contents
, entry
) &&
74 entry
->GetURL() != GURL(chrome::kChromeSearchLocalNtpUrl
);
77 bool IsNTP(const content::WebContents
* contents
) {
78 // We can't use WebContents::GetURL() because that uses the active entry,
79 // whereas we want the visible entry.
80 const content::NavigationEntry
* entry
=
81 contents
->GetController().GetVisibleEntry();
82 if (entry
&& entry
->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL
))
85 return chrome::IsInstantNTP(contents
);
88 bool IsSearchResults(const content::WebContents
* contents
) {
89 return !chrome::GetSearchTerms(contents
).empty();
92 bool IsLocal(const content::WebContents
* contents
) {
95 const content::NavigationEntry
* entry
=
96 contents
->GetController().GetVisibleEntry();
97 return entry
&& entry
->GetURL() == GURL(chrome::kChromeSearchLocalNtpUrl
);
100 // Returns true if |contents| are rendered inside an Instant process.
101 bool InInstantProcess(Profile
* profile
,
102 const content::WebContents
* contents
) {
103 if (!profile
|| !contents
)
106 InstantService
* instant_service
=
107 InstantServiceFactory::GetForProfile(profile
);
108 return instant_service
&&
109 instant_service
->IsInstantProcess(
110 contents
->GetRenderProcessHost()->GetID());
113 // Updates the location bar to reflect |contents| Instant support state.
114 void UpdateLocationBar(content::WebContents
* contents
) {
115 // iOS and Android don't use the Instant framework.
116 #if !defined(OS_IOS) && !defined(OS_ANDROID)
120 Browser
* browser
= chrome::FindBrowserWithWebContents(contents
);
123 browser
->OnWebContentsInstantSupportDisabled(contents
);
127 // Called when an NTP finishes loading. If the load start time was noted,
128 // calculates and logs the total load time.
129 void RecordNewTabLoadTime(content::WebContents
* contents
) {
130 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
131 if (core_tab_helper
->new_tab_start_time().is_null())
134 base::TimeDelta duration
=
135 base::TimeTicks::Now() - core_tab_helper
->new_tab_start_time();
136 UMA_HISTOGRAM_TIMES("Tab.NewTabOnload", duration
);
137 core_tab_helper
->set_new_tab_start_time(base::TimeTicks());
142 SearchTabHelper::SearchTabHelper(content::WebContents
* web_contents
)
143 : WebContentsObserver(web_contents
),
144 is_search_enabled_(chrome::IsInstantExtendedAPIEnabled()),
145 user_input_in_progress_(false),
146 web_contents_(web_contents
),
147 ipc_router_(web_contents
, this,
148 make_scoped_ptr(new SearchIPCRouterPolicyImpl(web_contents
))
149 .PassAs
<SearchIPCRouter::Policy
>()),
150 instant_service_(NULL
) {
151 if (!is_search_enabled_
)
155 InstantServiceFactory::GetForProfile(
156 Profile::FromBrowserContext(web_contents_
->GetBrowserContext()));
157 if (instant_service_
)
158 instant_service_
->AddObserver(this);
161 SearchTabHelper::~SearchTabHelper() {
162 if (instant_service_
)
163 instant_service_
->RemoveObserver(this);
166 void SearchTabHelper::InitForPreloadedNTP() {
167 UpdateMode(true, true);
170 void SearchTabHelper::OmniboxEditModelChanged(bool user_input_in_progress
,
172 if (!is_search_enabled_
)
175 user_input_in_progress_
= user_input_in_progress
;
176 if (!user_input_in_progress
&& !cancelling
)
179 UpdateMode(false, false);
182 void SearchTabHelper::NavigationEntryUpdated() {
183 if (!is_search_enabled_
)
186 UpdateMode(false, false);
189 void SearchTabHelper::InstantSupportChanged(bool instant_support
) {
190 if (!is_search_enabled_
)
193 InstantSupportState new_state
= instant_support
? INSTANT_SUPPORT_YES
:
196 model_
.SetInstantSupportState(new_state
);
198 content::NavigationEntry
* entry
=
199 web_contents_
->GetController().GetVisibleEntry();
201 chrome::SetInstantSupportStateInNavigationEntry(new_state
, entry
);
202 if (!instant_support
)
203 UpdateLocationBar(web_contents_
);
207 bool SearchTabHelper::SupportsInstant() const {
208 return model_
.instant_support() == INSTANT_SUPPORT_YES
;
211 void SearchTabHelper::SetSuggestionToPrefetch(
212 const InstantSuggestion
& suggestion
) {
213 ipc_router_
.SetSuggestionToPrefetch(suggestion
);
216 void SearchTabHelper::Submit(const base::string16
& text
) {
217 ipc_router_
.Submit(text
);
220 void SearchTabHelper::OnTabActivated() {
221 ipc_router_
.OnTabActivated();
224 void SearchTabHelper::OnTabDeactivated() {
225 ipc_router_
.OnTabDeactivated();
228 void SearchTabHelper::ToggleVoiceSearch() {
229 ipc_router_
.ToggleVoiceSearch();
232 bool SearchTabHelper::IsSearchResultsPage() {
233 return model_
.mode().is_origin_search();
236 void SearchTabHelper::RenderViewCreated(
237 content::RenderViewHost
* render_view_host
) {
238 ipc_router_
.SetPromoInformation(IsAppLauncherEnabled());
241 void SearchTabHelper::DidStartNavigationToPendingEntry(
243 content::NavigationController::ReloadType
/* reload_type */) {
244 if (chrome::IsNTPURL(url
, profile())) {
245 // Set the title on any pending entry corresponding to the NTP. This
246 // prevents any flickering of the tab title.
247 content::NavigationEntry
* entry
=
248 web_contents_
->GetController().GetPendingEntry();
250 entry
->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE
));
254 void SearchTabHelper::DidNavigateMainFrame(
255 const content::LoadCommittedDetails
& details
,
256 const content::FrameNavigateParams
& params
) {
257 if (IsCacheableNTP(web_contents_
)) {
258 if (details
.http_status_code
== 204 || details
.http_status_code
>= 400) {
259 RedirectToLocalNTP();
260 RecordCacheableNTPLoadHistogram(false);
263 RecordCacheableNTPLoadHistogram(true);
266 // Always set the title on the new tab page to be the one from our UI
267 // resources. Normally, we set the title when we begin a NTP load, but it can
268 // get reset in several places (like when you press Reload). This check
269 // ensures that the title is properly set to the string defined by the Chrome
270 // UI language (rather than the server language) in all cases.
272 // We only override the title when it's nonempty to allow the page to set the
273 // title if it really wants. An empty title means to use the default. There's
274 // also a race condition between this code and the page's SetTitle call which
276 content::NavigationEntry
* entry
=
277 web_contents_
->GetController().GetLastCommittedEntry();
278 if (entry
&& entry
->GetTitle().empty() &&
279 (entry
->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL
) ||
280 chrome::NavEntryIsInstantNTP(web_contents_
, entry
))) {
281 entry
->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE
));
285 void SearchTabHelper::DidFailProvisionalLoad(
286 int64
/* frame_id */,
287 const base::string16
& /* frame_unique_name */,
289 const GURL
& validated_url
,
291 const base::string16
& /* error_description */,
292 content::RenderViewHost
* /* render_view_host */) {
293 // If error_code is ERR_ABORTED means that the user has canceled this
294 // navigation so it shouldn't be redirected.
296 error_code
!= net::ERR_ABORTED
&&
297 validated_url
!= GURL(chrome::kChromeSearchLocalNtpUrl
) &&
298 chrome::IsNTPURL(validated_url
, profile())) {
299 RedirectToLocalNTP();
300 RecordCacheableNTPLoadHistogram(false);
304 void SearchTabHelper::DidFinishLoad(
305 int64
/* frame_id */,
306 const GURL
& /* validated_url */,
308 content::RenderViewHost
* /* render_view_host */) {
310 if (chrome::IsInstantNTP(web_contents_
))
311 RecordNewTabLoadTime(web_contents_
);
313 DetermineIfPageSupportsInstant();
317 void SearchTabHelper::NavigationEntryCommitted(
318 const content::LoadCommittedDetails
& load_details
) {
319 if (!is_search_enabled_
)
322 if (!load_details
.is_main_frame
)
325 // TODO(kmadhusu): Set the page initial states (such as omnibox margin, etc)
326 // from here. Please refer to crbug.com/247517 for more details.
327 if (chrome::ShouldAssignURLToInstantRenderer(web_contents_
->GetURL(),
329 ipc_router_
.SetDisplayInstantResults();
332 UpdateMode(true, false);
334 content::NavigationEntry
* entry
=
335 web_contents_
->GetController().GetVisibleEntry();
338 // Already determined the instant support state for this page, do not reset
339 // the instant support state.
341 // When we get a navigation entry committed event, there seem to be two ways
342 // to tell whether the navigation was "in-page". Ideally, when
343 // LoadCommittedDetails::is_in_page is true, we should have
344 // LoadCommittedDetails::type to be NAVIGATION_TYPE_IN_PAGE. Unfortunately,
345 // they are different in some cases. To workaround this bug, we are checking
346 // (is_in_page || type == NAVIGATION_TYPE_IN_PAGE). Please refer to
347 // crbug.com/251330 for more details.
348 if (load_details
.is_in_page
||
349 load_details
.type
== content::NAVIGATION_TYPE_IN_PAGE
) {
350 // When an "in-page" navigation happens, we will not receive a
351 // DidFinishLoad() event. Therefore, we will not determine the Instant
352 // support for the navigated page. So, copy over the Instant support from
353 // the previous entry. If the page does not support Instant, update the
354 // location bar from here to turn off search terms replacement.
355 chrome::SetInstantSupportStateInNavigationEntry(model_
.instant_support(),
357 if (model_
.instant_support() == INSTANT_SUPPORT_NO
)
358 UpdateLocationBar(web_contents_
);
362 model_
.SetInstantSupportState(INSTANT_SUPPORT_UNKNOWN
);
363 model_
.SetVoiceSearchSupported(false);
364 chrome::SetInstantSupportStateInNavigationEntry(model_
.instant_support(),
368 void SearchTabHelper::OnInstantSupportDetermined(bool supports_instant
) {
369 InstantSupportChanged(supports_instant
);
372 void SearchTabHelper::OnSetVoiceSearchSupport(bool supports_voice_search
) {
373 model_
.SetVoiceSearchSupported(supports_voice_search
);
376 void SearchTabHelper::ThemeInfoChanged(const ThemeBackgroundInfo
& theme_info
) {
377 ipc_router_
.SendThemeBackgroundInfo(theme_info
);
380 void SearchTabHelper::MostVisitedItemsChanged(
381 const std::vector
<InstantMostVisitedItem
>& items
) {
382 std::vector
<InstantMostVisitedItem
> items_copy(items
);
383 MaybeRemoveMostVisitedItems(&items_copy
);
384 ipc_router_
.SendMostVisitedItems(items_copy
);
387 void SearchTabHelper::MaybeRemoveMostVisitedItems(
388 std::vector
<InstantMostVisitedItem
>* items
) {
389 // The code below uses APIs not available on Android and the experiment should
391 #if !defined(OS_ANDROID)
392 if (!history::MostVisitedTilesExperiment::IsDontShowOpenURLsEnabled())
396 Profile::FromBrowserContext(web_contents_
->GetBrowserContext());
400 Browser
* browser
= chrome::FindBrowserWithProfile(profile
,
401 chrome::GetActiveDesktop());
405 TabStripModel
* tab_strip_model
= browser
->tab_strip_model();
406 history::TopSites
* top_sites
= profile
->GetTopSites();
407 if (!tab_strip_model
|| !top_sites
) {
412 std::set
<std::string
> open_urls
;
413 chrome::GetOpenUrls(*tab_strip_model
, *top_sites
, &open_urls
);
414 history::MostVisitedTilesExperiment::RemoveItemsMatchingOpenTabs(
419 void SearchTabHelper::FocusOmnibox(OmniboxFocusState state
) {
420 // iOS and Android don't use the Instant framework.
421 #if !defined(OS_IOS) && !defined(OS_ANDROID)
422 Browser
* browser
= chrome::FindBrowserWithWebContents(web_contents());
426 OmniboxView
* omnibox
= browser
->window()->GetLocationBar()->GetOmniboxView();
427 // Do not add a default case in the switch block for the following reasons:
428 // (1) Explicitly handle the new states. If new states are added in the
429 // OmniboxFocusState, the compiler will warn the developer to handle the new
431 // (2) An attacker may control the renderer and sends the browser process a
432 // malformed IPC. This function responds to the invalid |state| values by
433 // doing nothing instead of crashing the browser process (intentional no-op).
435 case OMNIBOX_FOCUS_VISIBLE
:
437 omnibox
->model()->SetCaretVisibility(true);
439 case OMNIBOX_FOCUS_INVISIBLE
:
441 omnibox
->model()->SetCaretVisibility(false);
442 // If the user clicked on the fakebox, any text already in the omnibox
443 // should get cleared when they start typing. Selecting all the existing
444 // text is a convenient way to accomplish this. It also gives a slight
445 // visual cue to users who really understand selection state about what
446 // will happen if they start typing.
447 omnibox
->SelectAll(false);
449 case OMNIBOX_FOCUS_NONE
:
450 // Remove focus only if the popup is closed. This will prevent someone
451 // from changing the omnibox value and closing the popup without user
453 if (!omnibox
->model()->popup_model()->IsOpen())
454 web_contents()->GetView()->Focus();
460 void SearchTabHelper::NavigateToURL(const GURL
& url
,
461 WindowOpenDisposition disposition
,
462 bool is_most_visited_item_url
) {
463 // iOS and Android don't use the Instant framework.
464 #if !defined(OS_IOS) && !defined(OS_ANDROID)
465 // TODO(kmadhusu): Remove chrome::FindBrowser...() function call from here.
466 // Create a SearchTabHelperDelegate interface and have the Browser object
467 // implement that interface to provide the necessary functionality.
468 Browser
* browser
= chrome::FindBrowserWithWebContents(web_contents_
);
470 Profile::FromBrowserContext(web_contents_
->GetBrowserContext());
471 if (!browser
|| !profile
)
474 if (is_most_visited_item_url
) {
475 content::RecordAction(
476 base::UserMetricsAction("InstantExtended.MostVisitedClicked"));
479 chrome::NavigateParams
params(browser
, url
,
480 content::PAGE_TRANSITION_AUTO_BOOKMARK
);
481 params
.referrer
= content::Referrer();
482 params
.source_contents
= web_contents_
;
483 params
.disposition
= disposition
;
484 params
.is_renderer_initiated
= false;
485 params
.initiating_profile
= profile
;
486 chrome::Navigate(¶ms
);
490 void SearchTabHelper::OnDeleteMostVisitedItem(const GURL
& url
) {
491 DCHECK(!url
.is_empty());
492 if (instant_service_
)
493 instant_service_
->DeleteMostVisitedItem(url
);
496 void SearchTabHelper::OnUndoMostVisitedDeletion(const GURL
& url
) {
497 DCHECK(!url
.is_empty());
498 if (instant_service_
)
499 instant_service_
->UndoMostVisitedDeletion(url
);
502 void SearchTabHelper::OnUndoAllMostVisitedDeletions() {
503 if (instant_service_
)
504 instant_service_
->UndoAllMostVisitedDeletions();
507 void SearchTabHelper::OnLogEvent(NTPLoggingEventType event
) {
508 NTPUserDataLogger::GetOrCreateFromWebContents(
509 web_contents())->LogEvent(event
);
512 void SearchTabHelper::OnLogImpression(int position
,
513 const base::string16
& provider
) {
514 NTPUserDataLogger::GetOrCreateFromWebContents(
515 web_contents())->LogImpression(position
, provider
);
518 void SearchTabHelper::PasteIntoOmnibox(const base::string16
& text
) {
519 // iOS and Android don't use the Instant framework.
520 #if !defined(OS_IOS) && !defined(OS_ANDROID)
521 Browser
* browser
= chrome::FindBrowserWithWebContents(web_contents());
525 OmniboxView
* omnibox
= browser
->window()->GetLocationBar()->GetOmniboxView();
526 // The first case is for right click to paste, where the text is retrieved
527 // from the clipboard already sanitized. The second case is needed to handle
528 // drag-and-drop value and it has to be sanitazed before setting it into the
530 base::string16 text_to_paste
= text
.empty() ? omnibox
->GetClipboardText() :
531 omnibox
->SanitizeTextForPaste(text
);
533 if (text_to_paste
.empty())
536 if (!omnibox
->model()->has_focus())
539 omnibox
->OnBeforePossibleChange();
540 omnibox
->model()->OnPaste();
541 omnibox
->SetUserText(text_to_paste
);
542 omnibox
->OnAfterPossibleChange();
546 void SearchTabHelper::OnChromeIdentityCheck(const base::string16
& identity
) {
547 SigninManagerBase
* manager
= SigninManagerFactory::GetForProfile(profile());
549 const base::string16 username
=
550 base::UTF8ToUTF16(manager
->GetAuthenticatedUsername());
551 ipc_router_
.SendChromeIdentityCheckResult(identity
,
552 identity
== username
);
556 void SearchTabHelper::UpdateMode(bool update_origin
, bool is_preloaded_ntp
) {
557 SearchMode::Type type
= SearchMode::MODE_DEFAULT
;
558 SearchMode::Origin origin
= SearchMode::ORIGIN_DEFAULT
;
559 if (IsNTP(web_contents_
) || is_preloaded_ntp
) {
560 type
= SearchMode::MODE_NTP
;
561 origin
= SearchMode::ORIGIN_NTP
;
562 } else if (IsSearchResults(web_contents_
)) {
563 type
= SearchMode::MODE_SEARCH_RESULTS
;
564 origin
= SearchMode::ORIGIN_SEARCH
;
567 origin
= model_
.mode().origin
;
568 if (user_input_in_progress_
)
569 type
= SearchMode::MODE_SEARCH_SUGGESTIONS
;
570 model_
.SetMode(SearchMode(type
, origin
));
573 void SearchTabHelper::DetermineIfPageSupportsInstant() {
574 if (!InInstantProcess(profile(), web_contents_
)) {
575 // The page is not in the Instant process. This page does not support
576 // instant. If we send an IPC message to a page that is not in the Instant
577 // process, it will never receive it and will never respond. Therefore,
578 // return immediately.
579 InstantSupportChanged(false);
580 } else if (IsLocal(web_contents_
)) {
581 // Local pages always support Instant.
582 InstantSupportChanged(true);
584 ipc_router_
.DetermineIfPageSupportsInstant();
588 Profile
* SearchTabHelper::profile() const {
589 return Profile::FromBrowserContext(web_contents_
->GetBrowserContext());
592 void SearchTabHelper::RedirectToLocalNTP() {
593 // Extra parentheses to declare a variable.
594 content::NavigationController::LoadURLParams
load_params(
595 (GURL(chrome::kChromeSearchLocalNtpUrl
)));
596 load_params
.referrer
= content::Referrer();
597 load_params
.transition_type
= content::PAGE_TRANSITION_SERVER_REDIRECT
;
598 // Don't push a history entry.
599 load_params
.should_replace_current_entry
= true;
600 web_contents_
->GetController().LoadURLWithParams(load_params
);