blob: 322c6ee54d0f1d3f5a166c8c1973601808cdf9be [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/omnibox/chrome_omnibox_client.h"
#include <stddef.h>
#include "base/bind.h"
#include "base/callback.h"
#include "base/macros.h"
#include "base/metrics/histogram.h"
#include "base/prefs/pref_service.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
#include "chrome/browser/autocomplete/chrome_autocomplete_provider_client.h"
#include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service_factory.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/bookmarks/bookmark_stats.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/extensions/api/omnibox/omnibox_api.h"
#include "chrome/browser/net/predictor.h"
#include "chrome/browser/predictors/autocomplete_action_predictor.h"
#include "chrome/browser/predictors/autocomplete_action_predictor_factory.h"
#include "chrome/browser/prerender/prerender_field_trial.h"
#include "chrome/browser/prerender/prerender_manager.h"
#include "chrome/browser/prerender/prerender_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/browser/ui/omnibox/chrome_omnibox_edit_controller.h"
#include "chrome/browser/ui/omnibox/chrome_omnibox_navigation_observer.h"
#include "chrome/browser/ui/search/instant_search_prerenderer.h"
#include "chrome/browser/ui/search/search_tab_helper.h"
#include "chrome/common/instant_types.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/favicon/content/content_favicon_driver.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "components/omnibox/browser/search_provider.h"
#include "components/search/search.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
using predictors::AutocompleteActionPredictor;
namespace {
// Returns the AutocompleteMatch that the InstantController should prefetch, if
// any.
//
// The SearchProvider may mark some suggestions to be prefetched based on
// instructions from the suggest server. If such a match ranks sufficiently
// highly or if kAllowPrefetchNonDefaultMatch field trial is enabled, we'll
// return it.
//
// If the kAllowPrefetchNonDefaultMatch field trial is enabled we return the
// prefetch suggestion even if it is not the default match. Otherwise we only
// care about matches that are the default or the second entry in the dropdown
// (which can happen for non-default matches when a top verbatim match is
// shown); for other matches, we think the likelihood of the user selecting
// them is low enough that prefetching isn't worth doing.
const AutocompleteMatch* GetMatchToPrefetch(const AutocompleteResult& result) {
if (search::ShouldAllowPrefetchNonDefaultMatch()) {
const AutocompleteResult::const_iterator prefetch_match = std::find_if(
result.begin(), result.end(), SearchProvider::ShouldPrefetch);
return prefetch_match != result.end() ? &(*prefetch_match) : NULL;
}
// If the default match should be prefetched, do that.
const auto default_match = result.default_match();
if ((default_match != result.end()) &&
SearchProvider::ShouldPrefetch(*default_match))
return &(*default_match);
// Otherwise, if the top match is a verbatim match and the very next match
// is prefetchable, fetch that.
if (result.TopMatchIsStandaloneVerbatimMatch() && (result.size() > 1) &&
SearchProvider::ShouldPrefetch(result.match_at(1)))
return &result.match_at(1);
return NULL;
}
// Calls the specified callback when the requested image is downloaded. This
// is a separate class instead of being implemented on ChromeOmniboxClient
// because BitmapFetcherService currently takes ownership of this object.
// TODO(dschuyler): Make BitmapFetcherService use the more typical non-owning
// ObserverList pattern and have ChromeOmniboxClient implement the Observer
// call directly.
class AnswerImageObserver : public BitmapFetcherService::Observer {
public:
explicit AnswerImageObserver(const BitmapFetchedCallback& callback)
: callback_(callback) {}
void OnImageChanged(BitmapFetcherService::RequestId request_id,
const SkBitmap& image) override;
private:
const BitmapFetchedCallback callback_;
DISALLOW_COPY_AND_ASSIGN(AnswerImageObserver);
};
void AnswerImageObserver::OnImageChanged(
BitmapFetcherService::RequestId request_id,
const SkBitmap& image) {
DCHECK(!image.empty());
callback_.Run(image);
}
} // namespace
ChromeOmniboxClient::ChromeOmniboxClient(OmniboxEditController* controller,
Profile* profile)
: controller_(static_cast<ChromeOmniboxEditController*>(controller)),
profile_(profile),
scheme_classifier_(profile),
request_id_(BitmapFetcherService::REQUEST_ID_INVALID) {}
ChromeOmniboxClient::~ChromeOmniboxClient() {
BitmapFetcherService* image_service =
BitmapFetcherServiceFactory::GetForBrowserContext(profile_);
if (image_service)
image_service->CancelRequest(request_id_);
}
scoped_ptr<AutocompleteProviderClient>
ChromeOmniboxClient::CreateAutocompleteProviderClient() {
return make_scoped_ptr(new ChromeAutocompleteProviderClient(profile_));
}
scoped_ptr<OmniboxNavigationObserver>
ChromeOmniboxClient::CreateOmniboxNavigationObserver(
const base::string16& text,
const AutocompleteMatch& match,
const AutocompleteMatch& alternate_nav_match) {
return make_scoped_ptr(new ChromeOmniboxNavigationObserver(
profile_, text, match, alternate_nav_match));
}
bool ChromeOmniboxClient::CurrentPageExists() const {
return (controller_->GetWebContents() != NULL);
}
const GURL& ChromeOmniboxClient::GetURL() const {
return controller_->GetWebContents()->GetVisibleURL();
}
const base::string16& ChromeOmniboxClient::GetTitle() const {
return controller_->GetWebContents()->GetTitle();
}
gfx::Image ChromeOmniboxClient::GetFavicon() const {
return favicon::ContentFaviconDriver::FromWebContents(
controller_->GetWebContents())
->GetFavicon();
}
bool ChromeOmniboxClient::IsInstantNTP() const {
return search::IsInstantNTP(controller_->GetWebContents());
}
bool ChromeOmniboxClient::IsSearchResultsPage() const {
Profile* profile = Profile::FromBrowserContext(
controller_->GetWebContents()->GetBrowserContext());
return TemplateURLServiceFactory::GetForProfile(profile)->
IsSearchResultsPageFromDefaultSearchProvider(GetURL());
}
bool ChromeOmniboxClient::IsLoading() const {
return controller_->GetWebContents()->IsLoading();
}
bool ChromeOmniboxClient::IsPasteAndGoEnabled() const {
return controller_->command_updater()->IsCommandEnabled(IDC_OPEN_CURRENT_URL);
}
bool ChromeOmniboxClient::IsNewTabPage(const std::string& url) const {
return url == chrome::kChromeUINewTabURL;
}
bool ChromeOmniboxClient::IsHomePage(const std::string& url) const {
return url == profile_->GetPrefs()->GetString(prefs::kHomePage);
}
const SessionID& ChromeOmniboxClient::GetSessionID() const {
return SessionTabHelper::FromWebContents(
controller_->GetWebContents())->session_id();
}
bookmarks::BookmarkModel* ChromeOmniboxClient::GetBookmarkModel() {
return BookmarkModelFactory::GetForProfile(profile_);
}
TemplateURLService* ChromeOmniboxClient::GetTemplateURLService() {
return TemplateURLServiceFactory::GetForProfile(profile_);
}
const AutocompleteSchemeClassifier&
ChromeOmniboxClient::GetSchemeClassifier() const {
return scheme_classifier_;
}
AutocompleteClassifier* ChromeOmniboxClient::GetAutocompleteClassifier() {
return AutocompleteClassifierFactory::GetForProfile(profile_);
}
gfx::Image ChromeOmniboxClient::GetIconIfExtensionMatch(
const AutocompleteMatch& match) const {
TemplateURLService* service =
TemplateURLServiceFactory::GetForProfile(profile_);
const TemplateURL* template_url = match.GetTemplateURL(service, false);
if (template_url &&
(template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION)) {
return extensions::OmniboxAPI::Get(profile_)
->GetOmniboxPopupIcon(template_url->GetExtensionId());
}
return gfx::Image();
}
bool ChromeOmniboxClient::ProcessExtensionKeyword(
TemplateURL* template_url,
const AutocompleteMatch& match,
WindowOpenDisposition disposition,
OmniboxNavigationObserver* observer) {
if (template_url->GetType() != TemplateURL::OMNIBOX_API_EXTENSION)
return false;
// Strip the keyword + leading space off the input, but don't exceed
// fill_into_edit. An obvious case is that the user may not have entered
// a leading space and is asking to launch this extension without any
// additional input.
size_t prefix_length =
std::min(match.keyword.length() + 1, match.fill_into_edit.length());
extensions::ExtensionOmniboxEventRouter::OnInputEntered(
controller_->GetWebContents(),
template_url->GetExtensionId(),
base::UTF16ToUTF8(match.fill_into_edit.substr(prefix_length)),
disposition);
static_cast<ChromeOmniboxNavigationObserver*>(observer)
->OnSuccessfulNavigation();
return true;
}
void ChromeOmniboxClient::OnInputStateChanged() {
if (!controller_->GetWebContents())
return;
SearchTabHelper::FromWebContents(
controller_->GetWebContents())->OmniboxInputStateChanged();
}
void ChromeOmniboxClient::OnFocusChanged(
OmniboxFocusState state,
OmniboxFocusChangeReason reason) {
if (!controller_->GetWebContents())
return;
SearchTabHelper::FromWebContents(
controller_->GetWebContents())->OmniboxFocusChanged(state, reason);
}
void ChromeOmniboxClient::OnResultChanged(
const AutocompleteResult& result,
bool default_match_changed,
const base::Callback<void(const SkBitmap& bitmap)>& on_bitmap_fetched) {
if (search::IsInstantExtendedAPIEnabled() &&
((default_match_changed && result.default_match() != result.end()) ||
(search::ShouldAllowPrefetchNonDefaultMatch() && !result.empty()))) {
InstantSuggestion prefetch_suggestion;
const AutocompleteMatch* match_to_prefetch = GetMatchToPrefetch(result);
if (match_to_prefetch) {
prefetch_suggestion.text = match_to_prefetch->contents;
prefetch_suggestion.metadata =
SearchProvider::GetSuggestMetadata(*match_to_prefetch);
}
// Send the prefetch suggestion unconditionally to the InstantPage. If
// there is no suggestion to prefetch, we need to send a blank query to
// clear the prefetched results.
SetSuggestionToPrefetch(prefetch_suggestion);
}
const auto match = std::find_if(
result.begin(), result.end(),
[](const AutocompleteMatch& current) { return !!current.answer; });
if (match != result.end()) {
BitmapFetcherService* image_service =
BitmapFetcherServiceFactory::GetForBrowserContext(profile_);
if (image_service) {
image_service->CancelRequest(request_id_);
request_id_ = image_service->RequestImage(
match->answer->second_line().image_url(),
new AnswerImageObserver(
base::Bind(&ChromeOmniboxClient::OnBitmapFetched,
base::Unretained(this), on_bitmap_fetched)));
}
}
}
void ChromeOmniboxClient::OnCurrentMatchChanged(
const AutocompleteMatch& match) {
if (!prerender::IsOmniboxEnabled(profile_))
DoPreconnect(match);
}
void ChromeOmniboxClient::OnTextChanged(const AutocompleteMatch& current_match,
bool user_input_in_progress,
base::string16& user_text,
const AutocompleteResult& result,
bool is_popup_open,
bool has_focus) {
AutocompleteActionPredictor::Action recommended_action =
AutocompleteActionPredictor::ACTION_NONE;
if (user_input_in_progress) {
InstantSearchPrerenderer* prerenderer =
InstantSearchPrerenderer::GetForProfile(profile_);
if (prerenderer &&
prerenderer->IsAllowed(current_match, controller_->GetWebContents()) &&
is_popup_open && has_focus) {
recommended_action = AutocompleteActionPredictor::ACTION_PRERENDER;
} else {
AutocompleteActionPredictor* action_predictor =
predictors::AutocompleteActionPredictorFactory::GetForProfile(
profile_);
action_predictor->RegisterTransitionalMatches(user_text, result);
// Confer with the AutocompleteActionPredictor to determine what action,
// if any, we should take. Get the recommended action here even if we
// don't need it so we can get stats for anyone who is opted in to UMA,
// but only get it if the user has actually typed something to avoid
// constructing it before it's needed. Note: This event is triggered as
// part of startup when the initial tab transitions to the start page.
recommended_action =
action_predictor->RecommendAction(user_text, current_match);
}
}
UMA_HISTOGRAM_ENUMERATION("AutocompleteActionPredictor.Action",
recommended_action,
AutocompleteActionPredictor::LAST_PREDICT_ACTION);
switch (recommended_action) {
case AutocompleteActionPredictor::ACTION_PRERENDER:
// It's possible that there is no current page, for instance if the tab
// has been closed or on return from a sleep state.
// (http://crbug.com/105689)
if (!CurrentPageExists())
break;
// Ask for prerendering if the destination URL is different than the
// current URL.
if (current_match.destination_url != GetURL())
DoPrerender(current_match);
break;
case AutocompleteActionPredictor::ACTION_PRECONNECT:
DoPreconnect(current_match);
break;
case AutocompleteActionPredictor::ACTION_NONE:
break;
}
}
void ChromeOmniboxClient::OnInputAccepted(const AutocompleteMatch& match) {
// While the user is typing, the instant search base page may be prerendered
// in the background. Even though certain inputs may not be eligible for
// prerendering, the prerender isn't automatically cancelled as the user
// continues typing, in hopes the final input will end up making use of the
// prerenderer. Intermediate inputs that are legal for prerendering will be
// sent to the prerendered page to keep it up to date; then once the user
// commits a navigation, it will trigger code in chrome::Navigate() to swap in
// the prerenderer.
//
// Unfortunately, that swap code only has the navigated URL, so it doesn't
// actually know whether the prerenderer has been sent the relevant input
// already, or whether instead the user manually navigated to something that
// looks like a search URL (which won't have been sent to the prerenderer).
// In this case, we need to ensure the prerenderer is cancelled here so that
// code can't attempt to wrongly swap-in, or it could swap in an empty page in
// place of the correct navigation.
//
// This would be clearer if we could swap in the prerenderer here instead of
// over in chrome::Navigate(), but we have to wait until then because the
// final decision about whether to use the prerendered page depends on other
// parts of the chrome::NavigateParams struct not available until then.
InstantSearchPrerenderer* prerenderer =
InstantSearchPrerenderer::GetForProfile(profile_);
if (prerenderer &&
!prerenderer->IsAllowed(match, controller_->GetWebContents()))
prerenderer->Cancel();
}
void ChromeOmniboxClient::OnRevert() {
AutocompleteActionPredictor* action_predictor =
predictors::AutocompleteActionPredictorFactory::GetForProfile(profile_);
action_predictor->ClearTransitionalMatches();
action_predictor->CancelPrerender();
}
void ChromeOmniboxClient::OnURLOpenedFromOmnibox(OmniboxLog* log) {
predictors::AutocompleteActionPredictorFactory::GetForProfile(profile_)
->OnOmniboxOpenedUrl(*log);
}
void ChromeOmniboxClient::OnBookmarkLaunched() {
RecordBookmarkLaunch(NULL, BOOKMARK_LAUNCH_LOCATION_OMNIBOX);
}
void ChromeOmniboxClient::DiscardNonCommittedNavigations() {
controller_->GetWebContents()->GetController().DiscardNonCommittedEntries();
}
void ChromeOmniboxClient::DoPrerender(
const AutocompleteMatch& match) {
content::WebContents* web_contents = controller_->GetWebContents();
gfx::Rect container_bounds = web_contents->GetContainerBounds();
InstantSearchPrerenderer* prerenderer =
InstantSearchPrerenderer::GetForProfile(profile_);
if (prerenderer && prerenderer->IsAllowed(match, web_contents)) {
prerenderer->Init(
web_contents->GetController().GetDefaultSessionStorageNamespace(),
container_bounds.size());
return;
}
predictors::AutocompleteActionPredictorFactory::GetForProfile(profile_)->
StartPrerendering(
match.destination_url,
web_contents->GetController().GetDefaultSessionStorageNamespace(),
container_bounds.size());
}
void ChromeOmniboxClient::DoPreconnect(const AutocompleteMatch& match) {
if (match.destination_url.SchemeIs(extensions::kExtensionScheme))
return;
// Warm up DNS Prefetch cache, or preconnect to a search service.
UMA_HISTOGRAM_ENUMERATION("Autocomplete.MatchType", match.type,
AutocompleteMatchType::NUM_TYPES);
if (profile_->GetNetworkPredictor()) {
profile_->GetNetworkPredictor()->AnticipateOmniboxUrl(
match.destination_url,
predictors::AutocompleteActionPredictor::IsPreconnectable(match));
}
// We could prefetch the alternate nav URL, if any, but because there
// can be many of these as a user types an initial series of characters,
// the OS DNS cache could suffer eviction problems for minimal gain.
}
void ChromeOmniboxClient::SetSuggestionToPrefetch(
const InstantSuggestion& suggestion) {
DCHECK(search::IsInstantExtendedAPIEnabled());
content::WebContents* web_contents = controller_->GetWebContents();
if (web_contents &&
SearchTabHelper::FromWebContents(web_contents)->IsSearchResultsPage()) {
if (search::ShouldPrefetchSearchResultsOnSRP()) {
SearchTabHelper::FromWebContents(web_contents)->
SetSuggestionToPrefetch(suggestion);
}
} else {
InstantSearchPrerenderer* prerenderer =
InstantSearchPrerenderer::GetForProfile(profile_);
if (prerenderer)
prerenderer->Prerender(suggestion);
}
}
void ChromeOmniboxClient::OnBitmapFetched(const BitmapFetchedCallback& callback,
const SkBitmap& bitmap) {
request_id_ = BitmapFetcherService::REQUEST_ID_INVALID;
callback.Run(bitmap);
}