Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(89)

Side by Side Diff: chrome/browser/instant/instant_controller.cc

Issue 12520005: Move desktop-specific Instant bits to c/b/ui/search. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase. Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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/instant/instant_controller.h"
6
7 #include "base/metrics/histogram.h"
8 #include "base/string_util.h"
9 #include "base/stringprintf.h"
10 #include "base/utf_string_conversions.h"
11 #include "chrome/browser/autocomplete/autocomplete_provider.h"
12 #include "chrome/browser/history/history_service.h"
13 #include "chrome/browser/history/history_service_factory.h"
14 #include "chrome/browser/history/history_tab_helper.h"
15 #include "chrome/browser/history/top_sites.h"
16 #include "chrome/browser/instant/instant_ntp.h"
17 #include "chrome/browser/instant/instant_overlay.h"
18 #include "chrome/browser/instant/instant_service.h"
19 #include "chrome/browser/instant/instant_service_factory.h"
20 #include "chrome/browser/instant/instant_tab.h"
21 #include "chrome/browser/instant/search.h"
22 #include "chrome/browser/platform_util.h"
23 #include "chrome/browser/search_engines/search_terms_data.h"
24 #include "chrome/browser/search_engines/template_url_service.h"
25 #include "chrome/browser/search_engines/template_url_service_factory.h"
26 #include "chrome/browser/ui/browser_instant_controller.h"
27 #include "chrome/browser/ui/search/search_tab_helper.h"
28 #include "chrome/common/chrome_notification_types.h"
29 #include "chrome/common/chrome_switches.h"
30 #include "chrome/common/url_constants.h"
31 #include "content/public/browser/navigation_entry.h"
32 #include "content/public/browser/notification_service.h"
33 #include "content/public/browser/render_process_host.h"
34 #include "content/public/browser/render_widget_host_view.h"
35 #include "content/public/browser/user_metrics.h"
36 #include "content/public/browser/web_contents.h"
37 #include "content/public/browser/web_contents_view.h"
38 #include "net/base/escape.h"
39 #include "third_party/icu/public/common/unicode/normalizer2.h"
40
41 #if defined(TOOLKIT_VIEWS)
42 #include "ui/views/widget/widget.h"
43 #endif
44
45 namespace {
46
47 // An artificial delay (in milliseconds) we introduce before telling the Instant
48 // page about the new omnibox bounds, in cases where the bounds shrink. This is
49 // to avoid the page jumping up/down very fast in response to bounds changes.
50 const int kUpdateBoundsDelayMS = 1000;
51
52 // The maximum number of times we'll load a non-Instant-supporting search engine
53 // before we give up and blacklist it for the rest of the browsing session.
54 const int kMaxInstantSupportFailures = 10;
55
56 // For reporting events of interest.
57 enum InstantControllerEvent {
58 INSTANT_CONTROLLER_EVENT_URL_ADDED_TO_BLACKLIST = 0,
59 INSTANT_CONTROLLER_EVENT_URL_REMOVED_FROM_BLACKLIST = 1,
60 INSTANT_CONTROLLER_EVENT_URL_BLOCKED_BY_BLACKLIST = 2,
61 INSTANT_CONTROLLER_EVENT_MAX = 3,
62 };
63
64 void RecordEventHistogram(InstantControllerEvent event) {
65 UMA_HISTOGRAM_ENUMERATION("Instant.InstantControllerEvent",
66 event,
67 INSTANT_CONTROLLER_EVENT_MAX);
68 }
69
70 void AddSessionStorageHistogram(bool extended_enabled,
71 const content::WebContents* tab1,
72 const content::WebContents* tab2) {
73 base::HistogramBase* histogram = base::BooleanHistogram::FactoryGet(
74 std::string("Instant.SessionStorageNamespace") +
75 (extended_enabled ? "_Extended" : "_Instant"),
76 base::HistogramBase::kUmaTargetedHistogramFlag);
77 const content::SessionStorageNamespaceMap& session_storage_map1 =
78 tab1->GetController().GetSessionStorageNamespaceMap();
79 const content::SessionStorageNamespaceMap& session_storage_map2 =
80 tab2->GetController().GetSessionStorageNamespaceMap();
81 bool is_session_storage_the_same =
82 session_storage_map1.size() == session_storage_map2.size();
83 if (is_session_storage_the_same) {
84 // The size is the same, so let's check that all entries match.
85 for (content::SessionStorageNamespaceMap::const_iterator
86 it1 = session_storage_map1.begin(),
87 it2 = session_storage_map2.begin();
88 it1 != session_storage_map1.end() && it2 != session_storage_map2.end();
89 ++it1, ++it2) {
90 if (it1->first != it2->first || it1->second != it2->second) {
91 is_session_storage_the_same = false;
92 break;
93 }
94 }
95 }
96 histogram->AddBoolean(is_session_storage_the_same);
97 }
98
99 string16 Normalize(const string16& str) {
100 UErrorCode status = U_ZERO_ERROR;
101 const icu::Normalizer2* normalizer =
102 icu::Normalizer2::getInstance(NULL, "nfkc_cf", UNORM2_COMPOSE, status);
103 if (normalizer == NULL || U_FAILURE(status))
104 return str;
105 icu::UnicodeString norm_str(normalizer->normalize(
106 icu::UnicodeString(FALSE, str.c_str(), str.size()), status));
107 if (U_FAILURE(status))
108 return str;
109 return string16(norm_str.getBuffer(), norm_str.length());
110 }
111
112 bool NormalizeAndStripPrefix(string16* text, const string16& prefix) {
113 string16 norm_prefix = Normalize(prefix);
114 string16 norm_text = Normalize(*text);
115 if (norm_prefix.size() <= norm_text.size() &&
116 norm_text.compare(0, norm_prefix.size(), norm_prefix) == 0) {
117 *text = norm_text.erase(0, norm_prefix.size());
118 return true;
119 }
120 return false;
121 }
122
123 // For TOOLKIT_VIEWS, the top level widget is always focused. If the focus
124 // change originated in views determine the child Widget from the view that is
125 // being focused.
126 gfx::NativeView GetViewGainingFocus(gfx::NativeView view_gaining_focus) {
127 #if defined(TOOLKIT_VIEWS)
128 views::Widget* widget = view_gaining_focus ?
129 views::Widget::GetWidgetForNativeView(view_gaining_focus) : NULL;
130 if (widget) {
131 views::FocusManager* focus_manager = widget->GetFocusManager();
132 if (focus_manager && focus_manager->is_changing_focus() &&
133 focus_manager->GetFocusedView() &&
134 focus_manager->GetFocusedView()->GetWidget())
135 return focus_manager->GetFocusedView()->GetWidget()->GetNativeView();
136 }
137 #endif
138 return view_gaining_focus;
139 }
140
141 // Returns true if |view| is the top-level contents view or a child view in the
142 // view hierarchy of |contents|.
143 bool IsViewInContents(gfx::NativeView view, content::WebContents* contents) {
144 content::RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView();
145 if (!view || !rwhv)
146 return false;
147
148 gfx::NativeView tab_view = contents->GetView()->GetNativeView();
149 if (view == rwhv->GetNativeView() || view == tab_view)
150 return true;
151
152 // Walk up the view hierarchy to determine if the view is a subview of the
153 // WebContents view (such as a windowed plugin or http auth dialog).
154 while (view) {
155 view = platform_util::GetParent(view);
156 if (view == tab_view)
157 return true;
158 }
159
160 return false;
161 }
162
163 bool IsFullHeight(const InstantOverlayModel& model) {
164 return model.height() == 100 && model.height_units() == INSTANT_SIZE_PERCENT;
165 }
166
167 bool IsContentsFrom(const InstantPage* page,
168 const content::WebContents* contents) {
169 return page && (page->contents() == contents);
170 }
171
172 // Adds a transient NavigationEntry to the supplied |contents|'s
173 // NavigationController if the page's URL has not already been updated with the
174 // supplied |search_terms|. Sets the |search_terms| on the transient entry for
175 // search terms extraction to work correctly.
176 void EnsureSearchTermsAreSet(content::WebContents* contents,
177 const string16& search_terms) {
178 content::NavigationController* controller = &contents->GetController();
179
180 // If search terms are already correct or there is already a transient entry
181 // (there shouldn't be), bail out early.
182 if (chrome::search::GetSearchTerms(contents) == search_terms ||
183 controller->GetTransientEntry())
184 return;
185
186 const content::NavigationEntry* active_entry = controller->GetActiveEntry();
187 content::NavigationEntry* transient = controller->CreateNavigationEntry(
188 active_entry->GetURL(),
189 active_entry->GetReferrer(),
190 active_entry->GetTransitionType(),
191 false,
192 std::string(),
193 contents->GetBrowserContext());
194 transient->SetExtraData(chrome::search::kInstantExtendedSearchTermsKey,
195 search_terms);
196 controller->SetTransientEntry(transient);
197
198 chrome::search::SearchTabHelper::FromWebContents(contents)->
199 NavigationEntryUpdated();
200 }
201
202 bool GetURLForMostVisitedItemId(Profile* profile,
203 uint64 most_visited_item_id,
204 GURL* url) {
205 InstantService* instant_service =
206 InstantServiceFactory::GetForProfile(profile);
207 if (!instant_service)
208 return false;
209 return instant_service->GetURLForMostVisitedItemId(most_visited_item_id, url);
210 }
211
212 // Creates a new restriced id if one is not found.
213 size_t GetMostVisitedItemIDForURL(Profile* profile, const GURL& url) {
214 InstantService* instant_service =
215 InstantServiceFactory::GetForProfile(profile);
216 if (!instant_service)
217 return 0;
218 return instant_service->AddURL(url);
219 }
220
221 } // namespace
222
223 InstantController::InstantController(chrome::BrowserInstantController* browser,
224 bool extended_enabled)
225 : browser_(browser),
226 extended_enabled_(extended_enabled),
227 instant_enabled_(false),
228 use_local_overlay_only_(true),
229 model_(ALLOW_THIS_IN_INITIALIZER_LIST(this)),
230 last_omnibox_text_has_inline_autocompletion_(false),
231 last_verbatim_(false),
232 last_transition_type_(content::PAGE_TRANSITION_LINK),
233 last_match_was_search_(false),
234 omnibox_focus_state_(OMNIBOX_FOCUS_NONE),
235 omnibox_bounds_(-1, -1, 0, 0),
236 allow_overlay_to_show_search_suggestions_(false),
237 ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)) {
238
239 // When the InstantController lives, the InstantService should live.
240 // InstantService sets up profile-level facilities such as the ThemeSource for
241 // the NTP.
242 InstantServiceFactory::GetForProfile(browser_->profile());
243 }
244
245 InstantController::~InstantController() {
246 }
247
248 bool InstantController::Update(const AutocompleteMatch& match,
249 const string16& user_text,
250 const string16& full_text,
251 size_t selection_start,
252 size_t selection_end,
253 bool verbatim,
254 bool user_input_in_progress,
255 bool omnibox_popup_is_open,
256 bool escape_pressed,
257 bool is_keyword_search) {
258 if (!extended_enabled_ && !instant_enabled_)
259 return false;
260
261 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
262 "Update: %s user_text='%s' full_text='%s' selection_start=%d "
263 "selection_end=%d verbatim=%d typing=%d popup=%d escape_pressed=%d "
264 "is_keyword_search=%d",
265 AutocompleteMatch::TypeToString(match.type).c_str(),
266 UTF16ToUTF8(user_text).c_str(), UTF16ToUTF8(full_text).c_str(),
267 static_cast<int>(selection_start), static_cast<int>(selection_end),
268 verbatim, user_input_in_progress, omnibox_popup_is_open, escape_pressed,
269 is_keyword_search));
270
271 // TODO(dhollowa): Complete keyword match UI. For now just hide suggestions.
272 // http://crbug.com/153932. Note, this early escape is happens prior to the
273 // DCHECKs below because |user_text| and |full_text| have different semantics
274 // when keyword search is in effect.
275 if (is_keyword_search) {
276 if (instant_tab_)
277 instant_tab_->Update(string16(), 0, 0, true);
278 else
279 HideOverlay();
280 last_match_was_search_ = false;
281 return false;
282 }
283
284 // Ignore spurious updates when the omnibox is blurred; otherwise click
285 // targets on the page may vanish before a click event arrives.
286 if (omnibox_focus_state_ == OMNIBOX_FOCUS_NONE)
287 return false;
288
289 // If the popup is open, the user has to be typing.
290 DCHECK(!omnibox_popup_is_open || user_input_in_progress);
291
292 // If the popup is closed, there should be no inline autocompletion.
293 DCHECK(omnibox_popup_is_open || user_text.empty() || user_text == full_text)
294 << user_text << "|" << full_text;
295
296 // If there's no text in the omnibox, the user can't have typed any.
297 DCHECK(!full_text.empty() || user_text.empty()) << user_text;
298
299 // If the user isn't typing, and the popup is closed, there can't be any
300 // user-typed text.
301 DCHECK(user_input_in_progress || omnibox_popup_is_open || user_text.empty())
302 << user_text;
303
304 // The overlay is being clicked and will commit soon. Don't change anything.
305 // TODO(sreeram): Add a browser test for this.
306 if (overlay_ && overlay_->is_pointer_down_from_activate())
307 return false;
308
309 // In non-extended mode, SearchModeChanged() is never called, so fake it. The
310 // mode is set to "disallow suggestions" here, so that if one of the early
311 // "return false" conditions is hit, suggestions will be disallowed. If the
312 // query is sent to the overlay, the mode is set to "allow" further below.
313 if (!extended_enabled_)
314 search_mode_.mode = chrome::search::Mode::MODE_DEFAULT;
315
316 last_match_was_search_ = AutocompleteMatch::IsSearchType(match.type) &&
317 !user_text.empty();
318
319 // In non extended mode, Instant is disabled for URLs and keyword mode.
320 if (!extended_enabled_ &&
321 (!last_match_was_search_ ||
322 match.type == AutocompleteMatch::SEARCH_OTHER_ENGINE)) {
323 HideOverlay();
324 return false;
325 }
326
327 // If we have an |instant_tab_| use it, else ensure we have an overlay that is
328 // current or is using the local overlay.
329 if (!instant_tab_ && !(overlay_ && overlay_->IsUsingLocalOverlay()) &&
330 !EnsureOverlayIsCurrent(false)) {
331 HideOverlay();
332 return false;
333 }
334
335 if (extended_enabled_) {
336 if (!omnibox_popup_is_open) {
337 if (!user_input_in_progress) {
338 // If the user isn't typing and the omnibox popup is closed, it means a
339 // regular navigation, tab-switch or the user hitting Escape.
340 if (instant_tab_) {
341 // The user is on a search results page. It may be showing results for
342 // a partial query the user typed before they hit Escape. Send the
343 // omnibox text to the page to restore the original results.
344 //
345 // In a tab switch, |instant_tab_| won't have updated yet, so it may
346 // be pointing to the previous tab (which was a search results page).
347 // Ensure we don't send the omnibox text to a random webpage (the new
348 // tab), by comparing the old and new WebContents.
349 if (escape_pressed &&
350 instant_tab_->contents() == browser_->GetActiveWebContents()) {
351 instant_tab_->Submit(full_text);
352 }
353 } else if (!full_text.empty()) {
354 // If |full_text| is empty, the user is on the NTP. The overlay may
355 // be showing custom NTP content; hide only if that's not the case.
356 HideOverlay();
357 }
358 } else if (full_text.empty()) {
359 // The user is typing, and backspaced away all omnibox text. Clear
360 // |last_omnibox_text_| so that we don't attempt to set suggestions.
361 last_omnibox_text_.clear();
362 last_user_text_.clear();
363 last_suggestion_ = InstantSuggestion();
364 if (instant_tab_) {
365 // On a search results page, tell it to clear old results.
366 instant_tab_->Update(string16(), 0, 0, true);
367 } else if (search_mode_.is_origin_ntp()) {
368 // On the NTP, tell the overlay to clear old results. Don't hide the
369 // overlay so it can show a blank page or logo if it wants.
370 overlay_->Update(string16(), 0, 0, true);
371 } else {
372 HideOverlay();
373 }
374 } else {
375 // The user switched to a tab with partial text already in the omnibox.
376 HideOverlay();
377
378 // The new tab may or may not be a search results page; we don't know
379 // since SearchModeChanged() hasn't been called yet. If it later turns
380 // out to be, we should store |full_text| now, so that if the user hits
381 // Enter, we'll send the correct query to instant_tab_->Submit(). If the
382 // partial text is not a query (|last_match_was_search_| is false), we
383 // won't Submit(), so no need to worry about that.
384 last_omnibox_text_ = full_text;
385 last_user_text_ = user_text;
386 last_suggestion_ = InstantSuggestion();
387 }
388 return false;
389 } else if (full_text.empty()) {
390 // The user typed a solitary "?". Same as the backspace case above.
391 last_omnibox_text_.clear();
392 last_user_text_.clear();
393 last_suggestion_ = InstantSuggestion();
394 if (instant_tab_)
395 instant_tab_->Update(string16(), 0, 0, true);
396 else if (search_mode_.is_origin_ntp())
397 overlay_->Update(string16(), 0, 0, true);
398 else
399 HideOverlay();
400 return false;
401 }
402 } else if (!omnibox_popup_is_open || full_text.empty()) {
403 // In the non-extended case, hide the overlay as long as the user isn't
404 // actively typing a non-empty query.
405 HideOverlay();
406 return false;
407 }
408
409 last_omnibox_text_has_inline_autocompletion_ = user_text != full_text;
410
411 // If the user continues typing the same query as the suggested text is
412 // showing, reuse the suggestion (but only for INSTANT_COMPLETE_NEVER).
413 bool reused_suggestion = false;
414 if (last_suggestion_.behavior == INSTANT_COMPLETE_NEVER &&
415 !last_omnibox_text_has_inline_autocompletion_) {
416 if (StartsWith(last_omnibox_text_, full_text, false)) {
417 // The user is backspacing away characters.
418 last_suggestion_.text.insert(0, last_omnibox_text_, full_text.size(),
419 last_omnibox_text_.size() - full_text.size());
420 reused_suggestion = true;
421 } else if (StartsWith(full_text, last_omnibox_text_, false)) {
422 // The user is typing forward. Normalize any added characters.
423 reused_suggestion = NormalizeAndStripPrefix(&last_suggestion_.text,
424 string16(full_text, last_omnibox_text_.size()));
425 }
426 }
427 if (!reused_suggestion)
428 last_suggestion_ = InstantSuggestion();
429
430 last_omnibox_text_ = full_text;
431 last_user_text_ = user_text;
432
433 if (!extended_enabled_) {
434 // In non-extended mode, the query is verbatim if there's any selection
435 // (including inline autocompletion) or if the cursor is not at the end.
436 verbatim = verbatim || selection_start != selection_end ||
437 selection_start != full_text.size();
438 }
439 last_verbatim_ = verbatim;
440
441 last_transition_type_ = match.transition;
442 url_for_history_ = match.destination_url;
443
444 // Allow search suggestions. In extended mode, SearchModeChanged() will set
445 // this, but it's not called in non-extended mode, so fake it.
446 if (!extended_enabled_)
447 search_mode_.mode = chrome::search::Mode::MODE_SEARCH_SUGGESTIONS;
448
449 if (instant_tab_) {
450 instant_tab_->Update(user_text, selection_start, selection_end, verbatim);
451 } else {
452 if (first_interaction_time_.is_null())
453 first_interaction_time_ = base::Time::Now();
454 allow_overlay_to_show_search_suggestions_ = true;
455
456 // For extended mode, if the loader is not ready at this point, switch over
457 // to a backup loader.
458 if (extended_enabled_ && !overlay_->supports_instant() &&
459 !overlay_->IsUsingLocalOverlay() && browser_->GetActiveWebContents()) {
460 CreateOverlay(chrome::kChromeSearchLocalOmniboxPopupURL,
461 browser_->GetActiveWebContents());
462 }
463
464 overlay_->Update(extended_enabled_ ? user_text : full_text,
465 selection_start, selection_end, verbatim);
466 }
467
468 content::NotificationService::current()->Notify(
469 chrome::NOTIFICATION_INSTANT_CONTROLLER_UPDATED,
470 content::Source<InstantController>(this),
471 content::NotificationService::NoDetails());
472
473 // We don't have new suggestions yet, but we can either reuse the existing
474 // suggestion or reset the existing "gray text".
475 browser_->SetInstantSuggestion(last_suggestion_);
476
477 return true;
478 }
479
480 scoped_ptr<content::WebContents> InstantController::ReleaseNTPContents() {
481 if (!extended_enabled_ || !ntp_)
482 return scoped_ptr<content::WebContents>(NULL);
483
484 LOG_INSTANT_DEBUG_EVENT(this, "ReleaseNTPContents");
485
486 scoped_ptr<content::WebContents> ntp_contents = ntp_->ReleaseContents();
487 ntp_.reset();
488 ResetNTP();
489 return ntp_contents.Pass();
490 }
491
492 // TODO(tonyg): This method only fires when the omnibox bounds change. It also
493 // needs to fire when the overlay bounds change (e.g.: open/close info bar).
494 void InstantController::SetPopupBounds(const gfx::Rect& bounds) {
495 if (!extended_enabled_ && !instant_enabled_)
496 return;
497
498 if (popup_bounds_ == bounds)
499 return;
500
501 popup_bounds_ = bounds;
502 if (popup_bounds_.height() > last_popup_bounds_.height()) {
503 update_bounds_timer_.Stop();
504 SendPopupBoundsToPage();
505 } else if (!update_bounds_timer_.IsRunning()) {
506 update_bounds_timer_.Start(FROM_HERE,
507 base::TimeDelta::FromMilliseconds(kUpdateBoundsDelayMS), this,
508 &InstantController::SendPopupBoundsToPage);
509 }
510 }
511
512 void InstantController::SetOmniboxBounds(const gfx::Rect& bounds) {
513 if (!extended_enabled_ || omnibox_bounds_ == bounds)
514 return;
515
516 omnibox_bounds_ = bounds;
517 if (overlay_)
518 overlay_->SetOmniboxBounds(omnibox_bounds_);
519 if (ntp_)
520 ntp_->SetOmniboxBounds(omnibox_bounds_);
521 if (instant_tab_)
522 instant_tab_->SetOmniboxBounds(omnibox_bounds_);
523 }
524
525 void InstantController::HandleAutocompleteResults(
526 const std::vector<AutocompleteProvider*>& providers) {
527 if (!extended_enabled_)
528 return;
529
530 if (!instant_tab_ && !overlay_)
531 return;
532
533 // The omnibox sends suggestions when its possibly imaginary popup closes
534 // as it stops autocomplete. Ignore these.
535 if (omnibox_focus_state_ == OMNIBOX_FOCUS_NONE)
536 return;
537
538 DVLOG(1) << "AutocompleteResults:";
539 std::vector<InstantAutocompleteResult> results;
540 for (ACProviders::const_iterator provider = providers.begin();
541 provider != providers.end(); ++provider) {
542 // Skip SearchProvider, since it only echoes suggestions.
543 if ((*provider)->type() == AutocompleteProvider::TYPE_SEARCH)
544 continue;
545 for (ACMatches::const_iterator match = (*provider)->matches().begin();
546 match != (*provider)->matches().end(); ++match) {
547 InstantAutocompleteResult result;
548 result.provider = UTF8ToUTF16((*provider)->GetName());
549 result.type = UTF8ToUTF16(AutocompleteMatch::TypeToString(match->type));
550 result.description = match->description;
551 result.destination_url = UTF8ToUTF16(match->destination_url.spec());
552 result.transition = match->transition;
553 result.relevance = match->relevance;
554 DVLOG(1) << " " << result.relevance << " " << result.type << " "
555 << result.provider << " " << result.destination_url << " '"
556 << result.description << "' " << result.transition;
557 results.push_back(result);
558 }
559 }
560 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
561 "HandleAutocompleteResults: total_results=%d",
562 static_cast<int>(results.size())));
563
564 if (instant_tab_)
565 instant_tab_->SendAutocompleteResults(results);
566 else
567 overlay_->SendAutocompleteResults(results);
568 }
569
570 bool InstantController::OnUpOrDownKeyPressed(int count) {
571 if (!extended_enabled_)
572 return false;
573
574 if (!instant_tab_ && !overlay_)
575 return false;
576
577 if (instant_tab_)
578 instant_tab_->UpOrDownKeyPressed(count);
579 else
580 overlay_->UpOrDownKeyPressed(count);
581
582 return true;
583 }
584
585 void InstantController::OnCancel(const AutocompleteMatch& match,
586 const string16& user_text,
587 const string16& full_text) {
588 if (!extended_enabled_)
589 return;
590
591 if (!instant_tab_ && !overlay_)
592 return;
593
594 // We manually reset the state here since the JS is not expected to do it.
595 // TODO(sreeram): Handle the case where user_text is now a URL
596 last_match_was_search_ = AutocompleteMatch::IsSearchType(match.type) &&
597 !full_text.empty();
598 last_omnibox_text_ = full_text;
599 last_user_text_ = user_text;
600 last_suggestion_ = InstantSuggestion();
601
602 if (instant_tab_)
603 instant_tab_->CancelSelection(full_text);
604 else
605 overlay_->CancelSelection(full_text);
606 }
607
608 content::WebContents* InstantController::GetOverlayContents() const {
609 return overlay_ ? overlay_->contents() : NULL;
610 }
611
612 bool InstantController::IsOverlayingSearchResults() const {
613 return model_.mode().is_search_suggestions() && IsFullHeight(model_) &&
614 (last_match_was_search_ ||
615 last_suggestion_.behavior == INSTANT_COMPLETE_NEVER);
616 }
617
618 bool InstantController::CommitIfPossible(InstantCommitType type) {
619 if (!extended_enabled_ && !instant_enabled_)
620 return false;
621
622 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
623 "CommitIfPossible: type=%d last_omnibox_text_='%s' "
624 "last_match_was_search_=%d instant_tab_=%d", type,
625 UTF16ToUTF8(last_omnibox_text_).c_str(), last_match_was_search_,
626 instant_tab_ != NULL));
627
628 // If we are on an already committed search results page, send a submit event
629 // to the page, but otherwise, nothing else to do.
630 if (instant_tab_) {
631 if (type == INSTANT_COMMIT_PRESSED_ENTER &&
632 (last_match_was_search_ ||
633 last_suggestion_.behavior == INSTANT_COMPLETE_NEVER)) {
634 last_suggestion_.text.clear();
635 instant_tab_->Submit(last_omnibox_text_);
636 instant_tab_->contents()->GetView()->Focus();
637 EnsureSearchTermsAreSet(instant_tab_->contents(), last_omnibox_text_);
638 return true;
639 }
640 return false;
641 }
642
643 // If the overlay is not showing at all, don't commit it.
644 if (!model_.mode().is_search_suggestions())
645 return false;
646
647 // If the overlay is showing at full height (with results), commit it.
648 // If it's showing at parial height, commit if it's navigating.
649 if (!IsOverlayingSearchResults() && type != INSTANT_COMMIT_NAVIGATED)
650 return false;
651
652 // There may re-entrance here, from the call to browser_->CommitInstant below,
653 // which can cause a TabDeactivated notification which gets back here.
654 // In this case, overlay_->ReleaseContents() was called already.
655 if (!GetOverlayContents())
656 return false;
657
658 // Never commit the local overlay.
659 if (overlay_->IsUsingLocalOverlay())
660 return false;
661
662 if (type == INSTANT_COMMIT_FOCUS_LOST) {
663 // Extended mode doesn't need or use the Cancel message.
664 if (!extended_enabled_)
665 overlay_->Cancel(last_omnibox_text_);
666 } else if (type != INSTANT_COMMIT_NAVIGATED) {
667 overlay_->Submit(last_omnibox_text_);
668 }
669
670 scoped_ptr<content::WebContents> overlay = overlay_->ReleaseContents();
671
672 // If the overlay page has navigated since the last Update(), we need to add
673 // the navigation to history ourselves. Else, the page will navigate after
674 // commit, and it will be added to history in the usual manner.
675 const history::HistoryAddPageArgs& last_navigation =
676 overlay_->last_navigation();
677 if (!last_navigation.url.is_empty()) {
678 content::NavigationEntry* entry = overlay->GetController().GetActiveEntry();
679
680 // The last navigation should be the same as the active entry if the overlay
681 // is in search mode. During navigation, the active entry could have
682 // changed since DidCommitProvisionalLoadForFrame is called after the entry
683 // is changed.
684 // TODO(shishir): Should we commit the last navigation for
685 // INSTANT_COMMIT_NAVIGATED.
686 DCHECK(type == INSTANT_COMMIT_NAVIGATED ||
687 last_navigation.url == entry->GetURL());
688
689 // Add the page to history.
690 HistoryTabHelper* history_tab_helper =
691 HistoryTabHelper::FromWebContents(overlay.get());
692 history_tab_helper->UpdateHistoryForNavigation(last_navigation);
693
694 // Update the page title.
695 history_tab_helper->UpdateHistoryPageTitle(*entry);
696 }
697
698 // Add a fake history entry with a non-Instant search URL, so that search
699 // terms extraction (for autocomplete history matches) works.
700 HistoryService* history = HistoryServiceFactory::GetForProfile(
701 Profile::FromBrowserContext(overlay->GetBrowserContext()),
702 Profile::EXPLICIT_ACCESS);
703 if (history) {
704 history->AddPage(url_for_history_, base::Time::Now(), NULL, 0, GURL(),
705 history::RedirectList(), last_transition_type_,
706 history::SOURCE_BROWSED, false);
707 }
708
709 if (type == INSTANT_COMMIT_PRESSED_ALT_ENTER) {
710 overlay->GetController().PruneAllButActive();
711 } else {
712 content::WebContents* active_tab = browser_->GetActiveWebContents();
713 AddSessionStorageHistogram(extended_enabled_, active_tab, overlay.get());
714 overlay->GetController().CopyStateFromAndPrune(
715 &active_tab->GetController());
716 }
717
718 if (extended_enabled_) {
719 // Adjust the search terms shown in the omnibox for this query. Hitting
720 // ENTER searches for what the user typed, so use last_omnibox_text_.
721 // Clicking on the overlay commits what is currently showing, so add in the
722 // gray text in that case.
723 if (type == INSTANT_COMMIT_FOCUS_LOST &&
724 last_suggestion_.behavior == INSTANT_COMPLETE_NEVER) {
725 // Update |last_omnibox_text_| so that the controller commits the proper
726 // query if the user focuses the omnibox and presses Enter.
727 last_omnibox_text_ += last_suggestion_.text;
728 }
729
730 EnsureSearchTermsAreSet(overlay.get(), last_omnibox_text_);
731 }
732
733 // Save notification source before we release the overlay.
734 content::Source<content::WebContents> notification_source(overlay.get());
735
736 browser_->CommitInstant(overlay.Pass(),
737 type == INSTANT_COMMIT_PRESSED_ALT_ENTER);
738
739 content::NotificationService::current()->Notify(
740 chrome::NOTIFICATION_INSTANT_COMMITTED,
741 notification_source,
742 content::NotificationService::NoDetails());
743
744 // Hide explicitly. See comments in HideOverlay() for why.
745 model_.SetOverlayState(chrome::search::Mode(), 0, INSTANT_SIZE_PERCENT);
746
747 // Delay deletion as we could've gotten here from an InstantOverlay method.
748 MessageLoop::current()->DeleteSoon(FROM_HERE, overlay_.release());
749
750 // Try to create another overlay immediately so that it is ready for the next
751 // user interaction.
752 EnsureOverlayIsCurrent(false);
753
754 LOG_INSTANT_DEBUG_EVENT(this, "Committed");
755 return true;
756 }
757
758 void InstantController::OmniboxFocusChanged(
759 OmniboxFocusState state,
760 OmniboxFocusChangeReason reason,
761 gfx::NativeView view_gaining_focus) {
762 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
763 "OmniboxFocusChanged: %d to %d for reason %d", omnibox_focus_state_,
764 state, reason));
765
766 OmniboxFocusState old_focus_state = omnibox_focus_state_;
767 omnibox_focus_state_ = state;
768 if (!extended_enabled_ && !instant_enabled_)
769 return;
770
771 // Tell the page if the key capture mode changed unless the focus state
772 // changed because of TYPING. This is because in that case, the browser hasn't
773 // really stopped capturing key strokes.
774 //
775 // (More practically, if we don't do this check, the page would receive
776 // onkeycapturechange before the corresponding onchange, and the page would
777 // have no way of telling whether the keycapturechange happened because of
778 // some actual user action or just because they started typing.)
779 if (extended_enabled_ && GetOverlayContents() &&
780 reason != OMNIBOX_FOCUS_CHANGE_TYPING) {
781 const bool is_key_capture_enabled =
782 omnibox_focus_state_ == OMNIBOX_FOCUS_INVISIBLE;
783 if (overlay_)
784 overlay_->KeyCaptureChanged(is_key_capture_enabled);
785 if (instant_tab_)
786 instant_tab_->KeyCaptureChanged(is_key_capture_enabled);
787 }
788
789 // If focus went from outside the omnibox to the omnibox, preload the default
790 // search engine, in anticipation of the user typing a query. If the reverse
791 // happened, commit or discard the overlay.
792 if (state != OMNIBOX_FOCUS_NONE && old_focus_state == OMNIBOX_FOCUS_NONE) {
793 // On explicit user actions, ignore the Instant blacklist.
794 EnsureOverlayIsCurrent(reason == OMNIBOX_FOCUS_CHANGE_EXPLICIT);
795 } else if (state == OMNIBOX_FOCUS_NONE &&
796 old_focus_state != OMNIBOX_FOCUS_NONE) {
797 OmniboxLostFocus(view_gaining_focus);
798 }
799 }
800
801 void InstantController::SearchModeChanged(
802 const chrome::search::Mode& old_mode,
803 const chrome::search::Mode& new_mode) {
804 if (!extended_enabled_)
805 return;
806
807 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
808 "SearchModeChanged: [origin:mode] %d:%d to %d:%d", old_mode.origin,
809 old_mode.mode, new_mode.origin, new_mode.mode));
810
811 search_mode_ = new_mode;
812 if (!new_mode.is_search_suggestions())
813 HideOverlay();
814
815 ResetInstantTab();
816 }
817
818 void InstantController::ActiveTabChanged() {
819 if (!extended_enabled_ && !instant_enabled_)
820 return;
821
822 LOG_INSTANT_DEBUG_EVENT(this, "ActiveTabChanged");
823
824 // When switching tabs, always hide the overlay.
825 HideOverlay();
826
827 if (extended_enabled_)
828 ResetInstantTab();
829 }
830
831 void InstantController::TabDeactivated(content::WebContents* contents) {
832 LOG_INSTANT_DEBUG_EVENT(this, "TabDeactivated");
833 if (extended_enabled_ && !contents->IsBeingDestroyed())
834 CommitIfPossible(INSTANT_COMMIT_FOCUS_LOST);
835 }
836
837 void InstantController::SetInstantEnabled(bool instant_enabled,
838 bool use_local_overlay_only) {
839 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
840 "SetInstantEnabled: instant_enabled=%d, use_local_overlay_only=%d",
841 instant_enabled, use_local_overlay_only));
842
843 // Non extended mode does not care about |use_local_overlay_only|.
844 if (instant_enabled == instant_enabled_ &&
845 (!extended_enabled_ ||
846 use_local_overlay_only == use_local_overlay_only_)) {
847 return;
848 }
849
850 instant_enabled_ = instant_enabled;
851 use_local_overlay_only_ = use_local_overlay_only;
852 HideInternal();
853 overlay_.reset();
854 if (extended_enabled_ || instant_enabled_)
855 EnsureOverlayIsCurrent(false);
856 if (extended_enabled_)
857 ResetNTP();
858 if (instant_tab_)
859 instant_tab_->SetDisplayInstantResults(instant_enabled_);
860 }
861
862 void InstantController::ThemeChanged(const ThemeBackgroundInfo& theme_info) {
863 if (!extended_enabled_)
864 return;
865
866 if (overlay_)
867 overlay_->SendThemeBackgroundInfo(theme_info);
868 if (ntp_)
869 ntp_->SendThemeBackgroundInfo(theme_info);
870 if (instant_tab_)
871 instant_tab_->SendThemeBackgroundInfo(theme_info);
872 }
873
874 void InstantController::SwappedOverlayContents() {
875 model_.SetOverlayContents(GetOverlayContents());
876 }
877
878 void InstantController::FocusedOverlayContents() {
879 #if defined(USE_AURA)
880 // On aura the omnibox only receives a focus lost if we initiate the focus
881 // change. This does that.
882 if (!model_.mode().is_default())
883 browser_->InstantOverlayFocused();
884 #endif
885 }
886
887 void InstantController::ReloadOverlayIfStale() {
888 // The local overlay is never stale.
889 if (overlay_ && overlay_->IsUsingLocalOverlay())
890 return;
891
892 // If the overlay is showing or the omnibox has focus, don't delete the
893 // overlay. It will get refreshed the next time the overlay is hidden or the
894 // omnibox loses focus.
895 if ((!overlay_ || overlay_->is_stale()) &&
896 omnibox_focus_state_ == OMNIBOX_FOCUS_NONE &&
897 model_.mode().is_default()) {
898 overlay_.reset();
899 EnsureOverlayIsCurrent(false);
900 }
901 }
902
903 void InstantController::OverlayLoadCompletedMainFrame() {
904 if (overlay_->supports_instant())
905 return;
906 InstantService* instant_service =
907 InstantServiceFactory::GetForProfile(browser_->profile());
908 content::WebContents* contents = overlay_->contents();
909 DCHECK(contents);
910 if (instant_service->IsInstantProcess(
911 contents->GetRenderProcessHost()->GetID())) {
912 return;
913 }
914 InstantSupportDetermined(contents, false);
915 }
916
917 void InstantController::LogDebugEvent(const std::string& info) const {
918 DVLOG(1) << info;
919
920 debug_events_.push_front(std::make_pair(
921 base::Time::Now().ToInternalValue(), info));
922 static const size_t kMaxDebugEventSize = 2000;
923 if (debug_events_.size() > kMaxDebugEventSize)
924 debug_events_.pop_back();
925 }
926
927 void InstantController::ClearDebugEvents() {
928 debug_events_.clear();
929 }
930
931 void InstantController::DeleteMostVisitedItem(uint64 most_visited_item_id) {
932 history::TopSites* top_sites = browser_->profile()->GetTopSites();
933 if (!top_sites)
934 return;
935
936 GURL url;
937 if (GetURLForMostVisitedItemId(browser_->profile(),
938 most_visited_item_id, &url))
939 top_sites->AddBlacklistedURL(url);
940 }
941
942 void InstantController::UndoMostVisitedDeletion(uint64 most_visited_item_id) {
943 history::TopSites* top_sites = browser_->profile()->GetTopSites();
944 if (!top_sites)
945 return;
946
947 GURL url;
948 if (GetURLForMostVisitedItemId(browser_->profile(),
949 most_visited_item_id, &url))
950 top_sites->RemoveBlacklistedURL(url);
951 }
952
953 void InstantController::UndoAllMostVisitedDeletions() {
954 history::TopSites* top_sites = browser_->profile()->GetTopSites();
955 if (!top_sites)
956 return;
957
958 top_sites->ClearBlacklistedURLs();
959 }
960
961 void InstantController::Observe(int type,
962 const content::NotificationSource& source,
963 const content::NotificationDetails& details) {
964 DCHECK_EQ(type, chrome::NOTIFICATION_TOP_SITES_CHANGED);
965 RequestMostVisitedItems();
966 }
967
968 // TODO(shishir): We assume that the WebContent's current RenderViewHost is the
969 // RenderViewHost being created which is not always true. Fix this.
970 void InstantController::InstantPageRenderViewCreated(
971 const content::WebContents* contents) {
972 if (!extended_enabled_)
973 return;
974
975 // Update theme info so that the page picks it up.
976 browser_->UpdateThemeInfo();
977
978 // Ensure the searchbox API has the correct initial state.
979 if (IsContentsFrom(overlay(), contents)) {
980 overlay_->SetDisplayInstantResults(instant_enabled_);
981 overlay_->KeyCaptureChanged(
982 omnibox_focus_state_ == OMNIBOX_FOCUS_INVISIBLE);
983 overlay_->SetOmniboxBounds(omnibox_bounds_);
984 overlay_->InitializeFonts();
985 overlay_->GrantChromeSearchAccessFromOrigin(GURL(overlay_->instant_url()));
986 } else if (IsContentsFrom(ntp(), contents)) {
987 ntp_->SetDisplayInstantResults(instant_enabled_);
988 ntp_->SetOmniboxBounds(omnibox_bounds_);
989 ntp_->InitializeFonts();
990 ntp_->GrantChromeSearchAccessFromOrigin(GURL(ntp_->instant_url()));
991 } else {
992 NOTREACHED();
993 }
994 StartListeningToMostVisitedChanges();
995 }
996
997 void InstantController::InstantSupportDetermined(
998 const content::WebContents* contents,
999 bool supports_instant) {
1000 if (IsContentsFrom(instant_tab(), contents)) {
1001 if (!supports_instant)
1002 MessageLoop::current()->DeleteSoon(FROM_HERE, instant_tab_.release());
1003 } else if (IsContentsFrom(ntp(), contents)) {
1004 if (supports_instant)
1005 RemoveFromBlacklist(ntp_->instant_url());
1006 else
1007 BlacklistAndResetNTP();
1008
1009 content::NotificationService::current()->Notify(
1010 chrome::NOTIFICATION_INSTANT_NTP_SUPPORT_DETERMINED,
1011 content::Source<InstantController>(this),
1012 content::NotificationService::NoDetails());
1013
1014 } else if (IsContentsFrom(overlay(), contents)) {
1015 if (supports_instant)
1016 RemoveFromBlacklist(overlay_->instant_url());
1017 else
1018 BlacklistAndResetOverlay();
1019
1020 content::NotificationService::current()->Notify(
1021 chrome::NOTIFICATION_INSTANT_OVERLAY_SUPPORT_DETERMINED,
1022 content::Source<InstantController>(this),
1023 content::NotificationService::NoDetails());
1024 }
1025 }
1026
1027 void InstantController::InstantPageRenderViewGone(
1028 const content::WebContents* contents) {
1029 if (IsContentsFrom(overlay(), contents))
1030 BlacklistAndResetOverlay();
1031 else if (IsContentsFrom(ntp(), contents))
1032 BlacklistAndResetNTP();
1033 else
1034 NOTREACHED();
1035 }
1036
1037 void InstantController::InstantPageAboutToNavigateMainFrame(
1038 const content::WebContents* contents,
1039 const GURL& url) {
1040 DCHECK(IsContentsFrom(overlay(), contents));
1041
1042 // If the page does not yet support Instant, we allow redirects and other
1043 // navigations to go through since the Instant URL can redirect - e.g. to
1044 // country specific pages.
1045 if (!overlay_->supports_instant())
1046 return;
1047
1048 GURL instant_url(overlay_->instant_url());
1049
1050 // If we are navigating to the Instant URL, do nothing.
1051 if (url == instant_url)
1052 return;
1053
1054 // Commit the navigation if either:
1055 // - The page is in NTP mode (so it could only navigate on a user click) or
1056 // - The page is not in NTP mode and we are navigating to a URL with a
1057 // different host or path than the Instant URL. This enables the instant
1058 // page when it is showing search results to change the query parameters
1059 // and fragments of the URL without it navigating.
1060 if (model_.mode().is_ntp() ||
1061 (url.host() != instant_url.host() || url.path() != instant_url.path())) {
1062 CommitIfPossible(INSTANT_COMMIT_NAVIGATED);
1063 }
1064 }
1065
1066 void InstantController::SetSuggestions(
1067 const content::WebContents* contents,
1068 const std::vector<InstantSuggestion>& suggestions) {
1069 LOG_INSTANT_DEBUG_EVENT(this, "SetSuggestions");
1070
1071 // Ignore if the message is from an unexpected source.
1072 if (IsContentsFrom(ntp(), contents))
1073 return;
1074 if (instant_tab_ && !IsContentsFrom(instant_tab(), contents))
1075 return;
1076 if (IsContentsFrom(overlay(), contents) &&
1077 !allow_overlay_to_show_search_suggestions_)
1078 return;
1079
1080 InstantSuggestion suggestion;
1081 if (!suggestions.empty())
1082 suggestion = suggestions[0];
1083
1084 if (instant_tab_ && search_mode_.is_search_results() &&
1085 suggestion.behavior == INSTANT_COMPLETE_REPLACE) {
1086 // Update |last_omnibox_text_| so that the controller commits the proper
1087 // query if the user focuses the omnibox and presses Enter.
1088 last_omnibox_text_ = suggestion.text;
1089 last_suggestion_ = InstantSuggestion();
1090 last_match_was_search_ = suggestion.type == INSTANT_SUGGESTION_SEARCH;
1091 // This means a committed page in state search called setValue(). We should
1092 // update the omnibox to reflect what the search page says.
1093 browser_->SetInstantSuggestion(suggestion);
1094 return;
1095 }
1096
1097 // Ignore if we are not currently accepting search suggestions.
1098 if (!search_mode_.is_search_suggestions() || last_omnibox_text_.empty())
1099 return;
1100
1101 if (suggestion.behavior == INSTANT_COMPLETE_REPLACE) {
1102 // We don't get an Update() when changing the omnibox due to a REPLACE
1103 // suggestion (so that we don't inadvertently cause the overlay to change
1104 // what it's showing, as the user arrows up/down through the page-provided
1105 // suggestions). So, update these state variables here.
1106 last_omnibox_text_ = suggestion.text;
1107 last_suggestion_ = InstantSuggestion();
1108 last_match_was_search_ = suggestion.type == INSTANT_SUGGESTION_SEARCH;
1109 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
1110 "ReplaceSuggestion text='%s' type=%d",
1111 UTF16ToUTF8(suggestion.text).c_str(), suggestion.type));
1112 browser_->SetInstantSuggestion(suggestion);
1113 } else {
1114 if (FixSuggestion(&suggestion)) {
1115 last_suggestion_ = suggestion;
1116 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
1117 "SetInstantSuggestion: text='%s' behavior=%d",
1118 UTF16ToUTF8(suggestion.text).c_str(),
1119 suggestion.behavior));
1120 browser_->SetInstantSuggestion(suggestion);
1121 content::NotificationService::current()->Notify(
1122 chrome::NOTIFICATION_INSTANT_SET_SUGGESTION,
1123 content::Source<InstantController>(this),
1124 content::NotificationService::NoDetails());
1125 } else {
1126 last_suggestion_ = InstantSuggestion();
1127 }
1128 }
1129
1130 // Extended mode pages will call ShowOverlay() when they are ready.
1131 if (!extended_enabled_)
1132 ShowOverlay(100, INSTANT_SIZE_PERCENT);
1133 }
1134
1135 void InstantController::ShowInstantOverlay(const content::WebContents* contents,
1136 int height,
1137 InstantSizeUnits units) {
1138 if (extended_enabled_ && IsContentsFrom(overlay(), contents))
1139 ShowOverlay(height, units);
1140 }
1141
1142 void InstantController::FocusOmnibox(const content::WebContents* contents) {
1143 if (!extended_enabled_)
1144 return;
1145
1146 DCHECK(IsContentsFrom(instant_tab(), contents));
1147 browser_->FocusOmnibox(true);
1148 }
1149
1150 void InstantController::StartCapturingKeyStrokes(
1151 const content::WebContents* contents) {
1152 if (!extended_enabled_)
1153 return;
1154
1155 DCHECK(IsContentsFrom(instant_tab(), contents));
1156 browser_->FocusOmnibox(false);
1157 }
1158
1159 void InstantController::StopCapturingKeyStrokes(
1160 content::WebContents* contents) {
1161 // Nothing to do if omnibox doesn't have invisible focus.
1162 if (!extended_enabled_ || omnibox_focus_state_ != OMNIBOX_FOCUS_INVISIBLE)
1163 return;
1164
1165 DCHECK(IsContentsFrom(instant_tab(), contents));
1166 contents->GetView()->Focus();
1167 }
1168
1169 void InstantController::NavigateToURL(const content::WebContents* contents,
1170 const GURL& url,
1171 content::PageTransition transition,
1172 WindowOpenDisposition disposition) {
1173 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
1174 "NavigateToURL: url='%s'", url.spec().c_str()));
1175
1176 // TODO(samarth): handle case where contents are no longer "active" (e.g. user
1177 // has switched tabs).
1178 if (!extended_enabled_)
1179 return;
1180 if (overlay_)
1181 HideOverlay();
1182
1183 if (transition == content::PAGE_TRANSITION_AUTO_BOOKMARK) {
1184 content::RecordAction(
1185 content::UserMetricsAction("InstantExtended.MostVisitedClicked"));
1186 }
1187 browser_->OpenURL(url, transition, disposition);
1188 }
1189
1190 void InstantController::OmniboxLostFocus(gfx::NativeView view_gaining_focus) {
1191 // If the overlay is showing custom NTP content, don't hide it, commit it
1192 // (no matter where the user clicked) or try to recreate it.
1193 if (model_.mode().is_ntp())
1194 return;
1195
1196 if (model_.mode().is_default()) {
1197 // Correct search terms if the user clicked on the committed results page
1198 // while showing an autocomplete suggestion
1199 if (instant_tab_ && !last_suggestion_.text.empty() &&
1200 last_suggestion_.behavior == INSTANT_COMPLETE_NEVER &&
1201 IsViewInContents(GetViewGainingFocus(view_gaining_focus),
1202 instant_tab_->contents())) {
1203 // Commit the omnibox's suggested grey text as if the user had typed it.
1204 browser_->CommitSuggestedText(true);
1205
1206 // Update the state so that next query from hitting Enter from the
1207 // omnibox is correct.
1208 last_omnibox_text_ += last_suggestion_.text;
1209 last_suggestion_ = InstantSuggestion();
1210 }
1211 // If the overlay is not showing at all, recreate it if it's stale.
1212 ReloadOverlayIfStale();
1213 MaybeSwitchToRemoteOverlay();
1214 return;
1215 }
1216
1217 // The overlay is showing search suggestions. If GetOverlayContents() is NULL,
1218 // we are in the commit path. Don't do anything.
1219 if (!GetOverlayContents())
1220 return;
1221
1222 #if defined(OS_MACOSX)
1223 // TODO(sreeram): See if Mac really needs this special treatment.
1224 if (!overlay_->is_pointer_down_from_activate())
1225 HideOverlay();
1226 #else
1227 if (IsFullHeight(model_))
1228 CommitIfPossible(INSTANT_COMMIT_FOCUS_LOST);
1229 else if (!IsViewInContents(GetViewGainingFocus(view_gaining_focus),
1230 overlay_->contents()))
1231 HideOverlay();
1232 #endif
1233 }
1234
1235 void InstantController::ResetNTP() {
1236 ntp_.reset();
1237 std::string instant_url;
1238 if (!GetInstantURL(browser_->profile(), false, &instant_url))
1239 return;
1240
1241 ntp_.reset(new InstantNTP(this, instant_url));
1242 ntp_->InitContents(browser_->profile(), browser_->GetActiveWebContents(),
1243 base::Bind(&InstantController::ResetNTP,
1244 base::Unretained(this)));
1245 }
1246
1247 bool InstantController::EnsureOverlayIsCurrent(bool ignore_blacklist) {
1248 // If there's no active tab, the browser is closing.
1249 const content::WebContents* active_tab = browser_->GetActiveWebContents();
1250 if (!active_tab)
1251 return false;
1252
1253 Profile* profile = Profile::FromBrowserContext(
1254 active_tab->GetBrowserContext());
1255 std::string instant_url;
1256 if (!GetInstantURL(profile, ignore_blacklist, &instant_url)) {
1257 // If we are in extended mode, fallback to the local overlay.
1258 if (extended_enabled_)
1259 instant_url = chrome::kChromeSearchLocalOmniboxPopupURL;
1260 else
1261 return false;
1262 }
1263
1264 if (!overlay_ || overlay_->instant_url() != instant_url)
1265 CreateOverlay(instant_url, active_tab);
1266
1267 return true;
1268 }
1269
1270 void InstantController::CreateOverlay(const std::string& instant_url,
1271 const content::WebContents* active_tab) {
1272 HideInternal();
1273 overlay_.reset(new InstantOverlay(this, instant_url));
1274 overlay_->InitContents(browser_->profile(), active_tab);
1275 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
1276 "CreateOverlay: instant_url='%s'", instant_url.c_str()));
1277 }
1278
1279 void InstantController::MaybeSwitchToRemoteOverlay() {
1280 if (!overlay_ || omnibox_focus_state_ != OMNIBOX_FOCUS_NONE ||
1281 !model_.mode().is_default()) {
1282 return;
1283 }
1284
1285 EnsureOverlayIsCurrent(false);
1286 }
1287
1288 void InstantController::ResetInstantTab() {
1289 // Do not wire up the InstantTab if Instant should only use local overlays, to
1290 // prevent it from sending data to the page.
1291 if (!search_mode_.is_origin_default() && !use_local_overlay_only_) {
1292 content::WebContents* active_tab = browser_->GetActiveWebContents();
1293 if (!instant_tab_ || active_tab != instant_tab_->contents()) {
1294 instant_tab_.reset(new InstantTab(this));
1295 instant_tab_->Init(active_tab);
1296 // Update theme info for this tab.
1297 browser_->UpdateThemeInfo();
1298 instant_tab_->SetDisplayInstantResults(instant_enabled_);
1299 instant_tab_->SetOmniboxBounds(omnibox_bounds_);
1300 instant_tab_->InitializeFonts();
1301 instant_tab_->GrantChromeSearchAccessFromOrigin(active_tab->GetURL());
1302 StartListeningToMostVisitedChanges();
1303 instant_tab_->KeyCaptureChanged(
1304 omnibox_focus_state_ == OMNIBOX_FOCUS_INVISIBLE);
1305 }
1306
1307 // Hide the |overlay_| since we are now using |instant_tab_| instead.
1308 HideOverlay();
1309 } else {
1310 instant_tab_.reset();
1311 }
1312 }
1313
1314 void InstantController::HideOverlay() {
1315 HideInternal();
1316 ReloadOverlayIfStale();
1317 MaybeSwitchToRemoteOverlay();
1318 }
1319
1320 void InstantController::HideInternal() {
1321 LOG_INSTANT_DEBUG_EVENT(this, "Hide");
1322
1323 // If GetOverlayContents() returns NULL, either we're already in the desired
1324 // MODE_DEFAULT state, or we're in the commit path. For the latter, don't
1325 // change the state just yet; else we may hide the overlay unnecessarily.
1326 // Instead, the state will be set correctly after the commit is done.
1327 if (GetOverlayContents()) {
1328 model_.SetOverlayState(chrome::search::Mode(), 0, INSTANT_SIZE_PERCENT);
1329 allow_overlay_to_show_search_suggestions_ = false;
1330
1331 // Send a message asking the overlay to clear out old results.
1332 overlay_->Update(string16(), 0, 0, true);
1333 }
1334
1335 // Clear the first interaction timestamp for later use.
1336 first_interaction_time_ = base::Time();
1337 }
1338
1339 void InstantController::ShowOverlay(int height, InstantSizeUnits units) {
1340 // If we are on a committed search results page, the |overlay_| is not in use.
1341 if (instant_tab_)
1342 return;
1343
1344 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
1345 "Show: height=%d units=%d", height, units));
1346
1347 // Must have updated omnibox after the last HideOverlay() to show suggestions.
1348 if (!allow_overlay_to_show_search_suggestions_)
1349 return;
1350
1351 // The page is trying to hide itself. Hide explicitly (i.e., don't use
1352 // HideOverlay()) so that it can change its mind.
1353 if (height == 0) {
1354 model_.SetOverlayState(chrome::search::Mode(), 0, INSTANT_SIZE_PERCENT);
1355 return;
1356 }
1357
1358 // If the overlay is being shown for the first time since the user started
1359 // typing, record a histogram value.
1360 if (!first_interaction_time_.is_null() && model_.mode().is_default()) {
1361 base::TimeDelta delta = base::Time::Now() - first_interaction_time_;
1362 UMA_HISTOGRAM_TIMES("Instant.TimeToFirstShow", delta);
1363 }
1364
1365 // Show at 100% height except in the following cases:
1366 // - The local overlay (omnibox popup) is being loaded.
1367 // - Instant is disabled. The page needs to be able to show only a dropdown.
1368 // - The page is over a website other than search or an NTP, and is not
1369 // already showing at 100% height.
1370 if (overlay_->IsUsingLocalOverlay() || !instant_enabled_ ||
1371 (search_mode_.is_origin_default() && !IsFullHeight(model_)))
1372 model_.SetOverlayState(search_mode_, height, units);
1373 else
1374 model_.SetOverlayState(search_mode_, 100, INSTANT_SIZE_PERCENT);
1375
1376 // If the overlay is being shown at full height and the omnibox is not
1377 // focused, commit right away.
1378 if (IsFullHeight(model_) && omnibox_focus_state_ == OMNIBOX_FOCUS_NONE)
1379 CommitIfPossible(INSTANT_COMMIT_FOCUS_LOST);
1380 }
1381
1382 void InstantController::SendPopupBoundsToPage() {
1383 if (last_popup_bounds_ == popup_bounds_ || !overlay_ ||
1384 overlay_->is_pointer_down_from_activate())
1385 return;
1386
1387 last_popup_bounds_ = popup_bounds_;
1388 gfx::Rect overlay_bounds = browser_->GetInstantBounds();
1389 gfx::Rect intersection = gfx::IntersectRects(popup_bounds_, overlay_bounds);
1390
1391 // Translate into window coordinates.
1392 if (!intersection.IsEmpty()) {
1393 intersection.Offset(-overlay_bounds.origin().x(),
1394 -overlay_bounds.origin().y());
1395 }
1396
1397 // In the current Chrome UI, these must always be true so they sanity check
1398 // the above operations. In a future UI, these may be removed or adjusted.
1399 // There is no point in sanity-checking |intersection.y()| because the omnibox
1400 // can be placed anywhere vertically relative to the overlay (for example, in
1401 // Mac fullscreen mode, the omnibox is fully enclosed by the overlay bounds).
1402 DCHECK_LE(0, intersection.x());
1403 DCHECK_LE(0, intersection.width());
1404 DCHECK_LE(0, intersection.height());
1405
1406 overlay_->SetPopupBounds(intersection);
1407 }
1408
1409 bool InstantController::GetInstantURL(Profile* profile,
1410 bool ignore_blacklist,
1411 std::string* instant_url) const {
1412 DCHECK(profile);
1413 instant_url->clear();
1414
1415 if (extended_enabled_ && use_local_overlay_only_) {
1416 *instant_url = chrome::kChromeSearchLocalOmniboxPopupURL;
1417 return true;
1418 }
1419
1420 const GURL instant_url_obj =
1421 chrome::search::GetInstantURL(profile, omnibox_bounds_.x());
1422 if (!instant_url_obj.is_valid())
1423 return false;
1424
1425 *instant_url = instant_url_obj.spec();
1426
1427 if (!ignore_blacklist) {
1428 std::map<std::string, int>::const_iterator iter =
1429 blacklisted_urls_.find(*instant_url);
1430 if (iter != blacklisted_urls_.end() &&
1431 iter->second > kMaxInstantSupportFailures) {
1432 RecordEventHistogram(INSTANT_CONTROLLER_EVENT_URL_BLOCKED_BY_BLACKLIST);
1433 LOG_INSTANT_DEBUG_EVENT(this, base::StringPrintf(
1434 "GetInstantURL: Instant URL blacklisted: url=%s",
1435 instant_url->c_str()));
1436 return false;
1437 }
1438 }
1439
1440 return true;
1441 }
1442
1443 void InstantController::BlacklistAndResetNTP() {
1444 ++blacklisted_urls_[ntp_->instant_url()];
1445 RecordEventHistogram(INSTANT_CONTROLLER_EVENT_URL_ADDED_TO_BLACKLIST);
1446 delete ntp_->ReleaseContents().release();
1447 MessageLoop::current()->DeleteSoon(FROM_HERE, ntp_.release());
1448 ResetNTP();
1449 }
1450
1451 void InstantController::BlacklistAndResetOverlay() {
1452 ++blacklisted_urls_[overlay_->instant_url()];
1453 RecordEventHistogram(INSTANT_CONTROLLER_EVENT_URL_ADDED_TO_BLACKLIST);
1454 HideInternal();
1455 delete overlay_->ReleaseContents().release();
1456 MessageLoop::current()->DeleteSoon(FROM_HERE, overlay_.release());
1457 EnsureOverlayIsCurrent(false);
1458 }
1459
1460 void InstantController::RemoveFromBlacklist(const std::string& url) {
1461 if (blacklisted_urls_.erase(url)) {
1462 RecordEventHistogram(INSTANT_CONTROLLER_EVENT_URL_REMOVED_FROM_BLACKLIST);
1463 }
1464 }
1465
1466 void InstantController::StartListeningToMostVisitedChanges() {
1467 history::TopSites* top_sites = browser_->profile()->GetTopSites();
1468 if (top_sites) {
1469 if (!registrar_.IsRegistered(
1470 this, chrome::NOTIFICATION_TOP_SITES_CHANGED,
1471 content::Source<history::TopSites>(top_sites))) {
1472 // TopSites updates itself after a delay. This is especially noticable
1473 // when your profile is empty. Ask TopSites to update itself when we're
1474 // about to show the new tab page.
1475 top_sites->SyncWithHistory();
1476
1477 RequestMostVisitedItems();
1478
1479 // Register for notification when TopSites changes.
1480 registrar_.Add(this, chrome::NOTIFICATION_TOP_SITES_CHANGED,
1481 content::Source<history::TopSites>(top_sites));
1482 } else {
1483 // We are already registered, so just get and send the most visited data.
1484 RequestMostVisitedItems();
1485 }
1486 }
1487 }
1488
1489 void InstantController::RequestMostVisitedItems() {
1490 history::TopSites* top_sites = browser_->profile()->GetTopSites();
1491 if (top_sites) {
1492 top_sites->GetMostVisitedURLs(
1493 base::Bind(&InstantController::OnMostVisitedItemsReceived,
1494 weak_ptr_factory_.GetWeakPtr()));
1495 }
1496 }
1497
1498 void InstantController::OnMostVisitedItemsReceived(
1499 const history::MostVisitedURLList& data) {
1500 std::vector<InstantMostVisitedItem> most_visited_items;
1501 for (size_t i = 0; i < data.size(); i++) {
1502 const history::MostVisitedURL& url = data[i];
1503
1504 InstantMostVisitedItem item;
1505 item.most_visited_item_id = GetMostVisitedItemIDForURL(browser_->profile(),
1506 url.url);
1507 item.url = url.url;
1508 item.title = url.title;
1509
1510 most_visited_items.push_back(item);
1511 }
1512 SendMostVisitedItems(most_visited_items);
1513 }
1514
1515 void InstantController::SendMostVisitedItems(
1516 const std::vector<InstantMostVisitedItem>& items) {
1517 if (overlay_)
1518 overlay_->SendMostVisitedItems(items);
1519 if (ntp_)
1520 ntp_->SendMostVisitedItems(items);
1521 if (instant_tab_)
1522 instant_tab_->SendMostVisitedItems(items);
1523 content::NotificationService::current()->Notify(
1524 chrome::NOTIFICATION_INSTANT_SENT_MOST_VISITED_ITEMS,
1525 content::Source<InstantController>(this),
1526 content::NotificationService::NoDetails());
1527 }
1528
1529 bool InstantController::FixSuggestion(InstantSuggestion* suggestion) const {
1530 // Don't suggest gray text if there already was inline autocompletion.
1531 // http://crbug.com/162303
1532 if (suggestion->behavior == INSTANT_COMPLETE_NEVER &&
1533 last_omnibox_text_has_inline_autocompletion_)
1534 return false;
1535
1536 // If the page is trying to set inline autocompletion in verbatim mode,
1537 // instead try suggesting the exact omnibox text. This makes the omnibox
1538 // interpret user text as an URL if possible while preventing unwanted
1539 // autocompletion during backspacing.
1540 if (suggestion->behavior == INSTANT_COMPLETE_NOW && last_verbatim_)
1541 suggestion->text = last_omnibox_text_;
1542
1543 // Suggestion text should be a full URL for URL suggestions, or the
1544 // completion of a query for query suggestions.
1545 if (suggestion->type == INSTANT_SUGGESTION_URL) {
1546 // If the suggestion is not a valid URL, perhaps it's something like
1547 // "foo.com". Try prefixing "http://". If it still isn't valid, drop it.
1548 if (!GURL(suggestion->text).is_valid()) {
1549 suggestion->text.insert(0, ASCIIToUTF16("http://"));
1550 if (!GURL(suggestion->text).is_valid())
1551 return false;
1552 }
1553
1554 // URL suggestions are only accepted if the query for which the suggestion
1555 // was generated is the same as |last_user_text_|.
1556 //
1557 // Any other URL suggestions--in particular suggestions for old user_text
1558 // lagging behind a slow IPC--are ignored. See crbug.com/181589.
1559 //
1560 // TODO(samarth): Accept stale suggestions if they would be accepted by
1561 // SearchProvider as an inlinable suggestion. http://crbug.com/191656.
1562 return suggestion->query == last_user_text_;
1563 }
1564
1565 if (suggestion->type == INSTANT_SUGGESTION_SEARCH) {
1566 if (StartsWith(suggestion->text, last_omnibox_text_, true)) {
1567 // The user typed an exact prefix of the suggestion.
1568 suggestion->text.erase(0, last_omnibox_text_.size());
1569 return true;
1570 } else if (NormalizeAndStripPrefix(&suggestion->text, last_omnibox_text_)) {
1571 // Unicode normalize and case-fold the user text and suggestion. If the
1572 // user text is a prefix, suggest the normalized, case-folded completion
1573 // for instance, if the user types 'i' and the suggestion is 'INSTANT',
1574 // suggest 'nstant'. Otherwise, the user text really isn't a prefix, so
1575 // suggest nothing.
1576 // TODO(samarth|jered): revisit this logic. http://crbug.com/196572.
1577 return true;
1578 }
1579 }
1580
1581 return false;
1582 }
OLDNEW
« no previous file with comments | « chrome/browser/instant/instant_controller.h ('k') | chrome/browser/instant/instant_extended_browsertest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698