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/metrics/histogram.h" 8 #include "base/prefs/pref_service.h" 9 #include "base/strings/stringprintf.h" 10 #include "chrome/browser/chrome_notification_types.h" 11 #include "chrome/browser/content_settings/content_settings_provider.h" 12 #include "chrome/browser/content_settings/host_content_settings_map.h" 13 #include "chrome/browser/platform_util.h" 14 #include "chrome/browser/profiles/profile.h" 15 #include "chrome/browser/search/instant_service.h" 16 #include "chrome/browser/search/instant_service_factory.h" 17 #include "chrome/browser/search/search.h" 18 #include "chrome/browser/search_engines/search_terms_data.h" 19 #include "chrome/browser/search_engines/template_url_service.h" 20 #include "chrome/browser/search_engines/template_url_service_factory.h" 21 #include "chrome/browser/ui/browser_instant_controller.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/url_constants.h" 28 #include "components/sessions/serialized_navigation_entry.h" 29 #include "content/public/browser/navigation_entry.h" 30 #include "content/public/browser/notification_service.h" 31 #include "content/public/browser/render_process_host.h" 32 #include "content/public/browser/render_widget_host_view.h" 33 #include "content/public/browser/user_metrics.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 39 #if defined(TOOLKIT_VIEWS) 40 #include "ui/views/widget/widget.h" 41 #endif 42 43 namespace { 44 45 // For reporting Instant navigations. 46 enum InstantNavigation { 47 INSTANT_NAVIGATION_LOCAL_CLICK = 0, 48 INSTANT_NAVIGATION_LOCAL_SUBMIT = 1, 49 INSTANT_NAVIGATION_ONLINE_CLICK = 2, 50 INSTANT_NAVIGATION_ONLINE_SUBMIT = 3, 51 INSTANT_NAVIGATION_NONEXTENDED = 4, 52 INSTANT_NAVIGATION_MAX = 5 53 }; 54 55 void RecordNavigationHistogram(bool is_local, bool is_click, bool is_extended) { 56 InstantNavigation navigation; 57 if (!is_extended) { 58 navigation = INSTANT_NAVIGATION_NONEXTENDED; 59 } else if (is_local) { 60 navigation = is_click ? INSTANT_NAVIGATION_LOCAL_CLICK : 61 INSTANT_NAVIGATION_LOCAL_SUBMIT; 62 } else { 63 navigation = is_click ? INSTANT_NAVIGATION_ONLINE_CLICK : 64 INSTANT_NAVIGATION_ONLINE_SUBMIT; 65 } 66 UMA_HISTOGRAM_ENUMERATION("InstantExtended.InstantNavigation", 67 navigation, 68 INSTANT_NAVIGATION_MAX); 69 } 70 71 bool IsContentsFrom(const InstantPage* page, 72 const content::WebContents* contents) { 73 return page && (page->contents() == contents); 74 } 75 76 // Adds a transient NavigationEntry to the supplied |contents|'s 77 // NavigationController if the page's URL has not already been updated with the 78 // supplied |search_terms|. Sets the |search_terms| on the transient entry for 79 // search terms extraction to work correctly. 80 void EnsureSearchTermsAreSet(content::WebContents* contents, 81 const string16& search_terms) { 82 content::NavigationController* controller = &contents->GetController(); 83 84 // If search terms are already correct or there is already a transient entry 85 // (there shouldn't be), bail out early. 86 if (chrome::GetSearchTerms(contents) == search_terms || 87 controller->GetTransientEntry()) 88 return; 89 90 const content::NavigationEntry* active_entry = controller->GetActiveEntry(); 91 content::NavigationEntry* transient = controller->CreateNavigationEntry( 92 active_entry->GetURL(), 93 active_entry->GetReferrer(), 94 active_entry->GetTransitionType(), 95 false, 96 std::string(), 97 contents->GetBrowserContext()); 98 transient->SetExtraData(sessions::kSearchTermsKey, search_terms); 99 controller->SetTransientEntry(transient); 100 101 SearchTabHelper::FromWebContents(contents)->NavigationEntryUpdated(); 102 } 103 104 } // namespace 105 106 InstantController::InstantController(BrowserInstantController* browser) 107 : browser_(browser), 108 omnibox_focus_state_(OMNIBOX_FOCUS_NONE), 109 omnibox_focus_change_reason_(OMNIBOX_FOCUS_CHANGE_EXPLICIT), 110 omnibox_bounds_(-1, -1, 0, 0) { 111 } 112 113 InstantController::~InstantController() { 114 } 115 116 void InstantController::SetOmniboxBounds(const gfx::Rect& bounds) { 117 if (omnibox_bounds_ == bounds) 118 return; 119 120 omnibox_bounds_ = bounds; 121 if (instant_tab_) 122 instant_tab_->sender()->SetOmniboxBounds(omnibox_bounds_); 123 } 124 125 void InstantController::ToggleVoiceSearch() { 126 if (instant_tab_) 127 instant_tab_->sender()->ToggleVoiceSearch(); 128 } 129 130 void InstantController::InstantPageLoadFailed(content::WebContents* contents) { 131 if (!chrome::ShouldPreferRemoteNTPOnStartup()) { 132 // We only need to fall back on errors if we're showing the online page 133 // at startup, as otherwise we fall back correctly when trying to show 134 // a page that hasn't yet indicated that it supports the InstantExtended 135 // API. 136 return; 137 } 138 139 DCHECK(IsContentsFrom(instant_tab(), contents)); 140 141 // Verify we're not already on a local page and that the URL precisely 142 // equals the instant_url (minus the query params, as those will be filled 143 // in by template values). This check is necessary to make sure we don't 144 // inadvertently redirect to the local NTP if someone, say, reloads a SRP 145 // while offline, as a committed results page still counts as an instant 146 // url. We also check to make sure there's no forward history, as if 147 // someone hits the back button a lot when offline and returns to a NTP 148 // we don't want to redirect and nuke their forward history stack. 149 const GURL& current_url = contents->GetURL(); 150 GURL instant_url = chrome::GetInstantURL(profile(), 151 chrome::kDisableStartMargin); 152 if (instant_tab_->IsLocal() || 153 !chrome::MatchesOriginAndPath(instant_url, current_url) || 154 !current_url.ref().empty() || 155 contents->GetController().CanGoForward()) 156 return; 157 LOG_INSTANT_DEBUG_EVENT(this, "InstantPageLoadFailed: instant_tab"); 158 RedirectToLocalNTP(contents); 159 } 160 161 bool InstantController::SubmitQuery(const string16& search_terms) { 162 if (instant_tab_ && instant_tab_->supports_instant() && 163 search_mode_.is_origin_search()) { 164 // Use |instant_tab_| to run the query if we're already on a search results 165 // page. (NOTE: in particular, we do not send the query to NTPs.) 166 instant_tab_->sender()->Submit(search_terms); 167 instant_tab_->contents()->GetView()->Focus(); 168 EnsureSearchTermsAreSet(instant_tab_->contents(), search_terms); 169 return true; 170 } 171 return false; 172 } 173 174 void InstantController::OmniboxFocusChanged( 175 OmniboxFocusState state, 176 OmniboxFocusChangeReason reason, 177 gfx::NativeView view_gaining_focus) { 178 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf( 179 "OmniboxFocusChanged: %d to %d for reason %d", omnibox_focus_state_, 180 state, reason)); 181 182 omnibox_focus_state_ = state; 183 if (!instant_tab_) 184 return; 185 186 content::NotificationService::current()->Notify( 187 chrome::NOTIFICATION_OMNIBOX_FOCUS_CHANGED, 188 content::Source<InstantController>(this), 189 content::NotificationService::NoDetails()); 190 191 instant_tab_->sender()->FocusChanged(omnibox_focus_state_, reason); 192 // Don't send oninputstart/oninputend updates in response to focus changes 193 // if there's a navigation in progress. This prevents Chrome from sending 194 // a spurious oninputend when the user accepts a match in the omnibox. 195 if (instant_tab_->contents()->GetController().GetPendingEntry() == NULL) 196 instant_tab_->sender()->SetInputInProgress(IsInputInProgress()); 197 } 198 199 void InstantController::SearchModeChanged(const SearchMode& old_mode, 200 const SearchMode& new_mode) { 201 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf( 202 "SearchModeChanged: [origin:mode] %d:%d to %d:%d", old_mode.origin, 203 old_mode.mode, new_mode.origin, new_mode.mode)); 204 205 search_mode_ = new_mode; 206 ResetInstantTab(); 207 208 if (instant_tab_ && old_mode.is_ntp() != new_mode.is_ntp()) 209 instant_tab_->sender()->SetInputInProgress(IsInputInProgress()); 210 } 211 212 void InstantController::ActiveTabChanged() { 213 LOG_INSTANT_DEBUG_EVENT(this, "ActiveTabChanged"); 214 ResetInstantTab(); 215 } 216 217 void InstantController::TabDeactivated(content::WebContents* contents) { 218 // If user is deactivating an NTP tab, log the number of mouseovers for this 219 // NTP session. 220 if (chrome::IsInstantNTP(contents)) 221 InstantTab::EmitMouseoverCount(contents); 222 } 223 224 void InstantController::LogDebugEvent(const std::string& info) const { 225 DVLOG(1) << info; 226 227 debug_events_.push_front(std::make_pair( 228 base::Time::Now().ToInternalValue(), info)); 229 static const size_t kMaxDebugEventSize = 2000; 230 if (debug_events_.size() > kMaxDebugEventSize) 231 debug_events_.pop_back(); 232 } 233 234 void InstantController::ClearDebugEvents() { 235 debug_events_.clear(); 236 } 237 238 void InstantController::DeleteMostVisitedItem(const GURL& url) { 239 DCHECK(!url.is_empty()); 240 InstantService* instant_service = GetInstantService(); 241 if (!instant_service) 242 return; 243 244 instant_service->DeleteMostVisitedItem(url); 245 } 246 247 void InstantController::UndoMostVisitedDeletion(const GURL& url) { 248 DCHECK(!url.is_empty()); 249 InstantService* instant_service = GetInstantService(); 250 if (!instant_service) 251 return; 252 253 instant_service->UndoMostVisitedDeletion(url); 254 } 255 256 void InstantController::UndoAllMostVisitedDeletions() { 257 InstantService* instant_service = GetInstantService(); 258 if (!instant_service) 259 return; 260 261 instant_service->UndoAllMostVisitedDeletions(); 262 } 263 264 Profile* InstantController::profile() const { 265 return browser_->profile(); 266 } 267 268 InstantTab* InstantController::instant_tab() const { 269 return instant_tab_.get(); 270 } 271 272 void InstantController::InstantSupportChanged( 273 InstantSupportState instant_support) { 274 // Handle INSTANT_SUPPORT_YES here because InstantPage is not hooked up to the 275 // active tab. Search model changed listener in InstantPage will handle other 276 // cases. 277 if (instant_support != INSTANT_SUPPORT_YES) 278 return; 279 280 ResetInstantTab(); 281 } 282 283 void InstantController::InstantSupportDetermined( 284 const content::WebContents* contents, 285 bool supports_instant) { 286 DCHECK(IsContentsFrom(instant_tab(), contents)); 287 288 if (!supports_instant) 289 base::MessageLoop::current()->DeleteSoon(FROM_HERE, instant_tab_.release()); 290 291 content::NotificationService::current()->Notify( 292 chrome::NOTIFICATION_INSTANT_TAB_SUPPORT_DETERMINED, 293 content::Source<InstantController>(this), 294 content::NotificationService::NoDetails()); 295 } 296 297 void InstantController::InstantPageAboutToNavigateMainFrame( 298 const content::WebContents* contents, 299 const GURL& url) { 300 DCHECK(IsContentsFrom(instant_tab(), contents)); 301 302 // The Instant tab navigated. Send it the data it needs to display 303 // properly. 304 UpdateInfoForInstantTab(); 305 } 306 307 void InstantController::FocusOmnibox(const content::WebContents* contents, 308 OmniboxFocusState state) { 309 DCHECK(IsContentsFrom(instant_tab(), contents)); 310 browser_->FocusOmnibox(state); 311 } 312 313 void InstantController::NavigateToURL(const content::WebContents* contents, 314 const GURL& url, 315 content::PageTransition transition, 316 WindowOpenDisposition disposition, 317 bool is_search_type) { 318 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf( 319 "NavigateToURL: url='%s'", url.spec().c_str())); 320 321 // TODO(samarth): handle case where contents are no longer "active" (e.g. user 322 // has switched tabs). 323 if (transition == content::PAGE_TRANSITION_AUTO_BOOKMARK) { 324 content::RecordAction( 325 content::UserMetricsAction("InstantExtended.MostVisitedClicked")); 326 } else { 327 // Exclude navigation by Most Visited click and searches. 328 if (!is_search_type) 329 RecordNavigationHistogram(UsingLocalPage(), true, true); 330 } 331 browser_->OpenURL(url, transition, disposition); 332 } 333 334 void InstantController::PasteIntoOmnibox(const content::WebContents* contents, 335 const string16& text) { 336 if (search_mode_.is_origin_default()) 337 return; 338 339 DCHECK(IsContentsFrom(instant_tab(), contents)); 340 341 browser_->PasteIntoOmnibox(text); 342 } 343 344 void InstantController::ResetInstantTab() { 345 if (!search_mode_.is_origin_default()) { 346 content::WebContents* active_tab = browser_->GetActiveWebContents(); 347 if (!instant_tab_ || active_tab != instant_tab_->contents()) { 348 instant_tab_.reset(new InstantTab(this, browser_->profile())); 349 instant_tab_->Init(active_tab); 350 UpdateInfoForInstantTab(); 351 } 352 } else { 353 instant_tab_.reset(); 354 } 355 } 356 357 void InstantController::UpdateInfoForInstantTab() { 358 if (instant_tab_) { 359 instant_tab_->sender()->SetOmniboxBounds(omnibox_bounds_); 360 361 // Update theme details. 362 InstantService* instant_service = GetInstantService(); 363 if (instant_service) { 364 instant_service->UpdateThemeInfo(); 365 instant_service->UpdateMostVisitedItemsInfo(); 366 } 367 368 instant_tab_->InitializeFonts(); 369 instant_tab_->InitializePromos(); 370 instant_tab_->sender()->FocusChanged(omnibox_focus_state_, 371 omnibox_focus_change_reason_); 372 instant_tab_->sender()->SetInputInProgress(IsInputInProgress()); 373 } 374 } 375 376 bool InstantController::IsInputInProgress() const { 377 return !search_mode_.is_ntp() && 378 omnibox_focus_state_ == OMNIBOX_FOCUS_VISIBLE; 379 } 380 381 bool InstantController::UsingLocalPage() const { 382 return instant_tab_ && instant_tab_->IsLocal(); 383 } 384 385 void InstantController::RedirectToLocalNTP(content::WebContents* contents) { 386 contents->GetController().LoadURL( 387 GURL(chrome::kChromeSearchLocalNtpUrl), 388 content::Referrer(), 389 content::PAGE_TRANSITION_SERVER_REDIRECT, 390 std::string()); // No extra headers. 391 // TODO(dcblack): Remove extraneous history entry caused by 404s. 392 // Note that the base case of a 204 being returned doesn't push a history 393 // entry. 394 } 395 396 InstantService* InstantController::GetInstantService() const { 397 return InstantServiceFactory::GetForProfile(profile()); 398 } 399