keyword_extensions_delegate_impl.cc revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
1// Copyright 2014 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/autocomplete/keyword_extensions_delegate_impl.h"
6
7#include "base/strings/utf_string_conversions.h"
8#include "chrome/browser/chrome_notification_types.h"
9#include "chrome/browser/extensions/api/omnibox/omnibox_api.h"
10#include "chrome/browser/extensions/extension_service.h"
11#include "chrome/browser/extensions/extension_util.h"
12#include "chrome/browser/profiles/profile.h"
13#include "content/public/browser/notification_details.h"
14#include "content/public/browser/notification_source.h"
15#include "extensions/browser/extension_system.h"
16
17namespace omnibox_api = extensions::api::omnibox;
18
19int KeywordExtensionsDelegateImpl::global_input_uid_ = 0;
20
21KeywordExtensionsDelegateImpl::KeywordExtensionsDelegateImpl(
22    KeywordProvider* provider)
23    : KeywordExtensionsDelegate(provider),
24      provider_(provider) {
25  DCHECK(provider_);
26
27  current_input_id_ = 0;
28  // Extension suggestions always come from the original profile, since that's
29  // where extensions run. We use the input ID to distinguish whether the
30  // suggestions are meant for us.
31  registrar_.Add(
32      this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY,
33      content::Source<Profile>(profile()->GetOriginalProfile()));
34  registrar_.Add(
35      this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
36      content::Source<Profile>(profile()->GetOriginalProfile()));
37  registrar_.Add(
38      this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED,
39      content::Source<Profile>(profile()));
40}
41
42KeywordExtensionsDelegateImpl::~KeywordExtensionsDelegateImpl() {
43}
44
45void  KeywordExtensionsDelegateImpl::IncrementInputId() {
46  current_input_id_ = ++global_input_uid_;
47}
48
49bool KeywordExtensionsDelegateImpl::IsEnabledExtension(
50    Profile* profile,
51    const std::string& extension_id) {
52  ExtensionService* extension_service =
53      extensions::ExtensionSystem::Get(profile)->extension_service();
54  const extensions::Extension* extension =
55      extension_service->GetExtensionById(extension_id, false);
56  return extension &&
57      (!profile->IsOffTheRecord() ||
58       !extensions::util::IsIncognitoEnabled(extension_id, profile));
59}
60
61bool KeywordExtensionsDelegateImpl::Start(
62    const AutocompleteInput& input,
63    bool minimal_changes,
64    const TemplateURL* template_url,
65    const base::string16& remaining_input) {
66  DCHECK(template_url);
67
68  if (input.want_asynchronous_matches()) {
69    std::string extension_id = template_url->GetExtensionId();
70    if (extension_id != current_keyword_extension_id_)
71      MaybeEndExtensionKeywordMode();
72    if (current_keyword_extension_id_.empty())
73      EnterExtensionKeywordMode(extension_id);
74  }
75
76  extensions::ApplyDefaultSuggestionForExtensionKeyword(
77      profile(), template_url, remaining_input, &matches()->front());
78
79  if (minimal_changes) {
80    // If the input hasn't significantly changed, we can just use the
81    // suggestions from last time. We need to readjust the relevance to
82    // ensure it is less than the main match's relevance.
83    for (size_t i = 0; i < extension_suggest_matches_.size(); ++i) {
84      matches()->push_back(extension_suggest_matches_[i]);
85      matches()->back().relevance = matches()->front().relevance - (i + 1);
86    }
87  } else if (input.want_asynchronous_matches()) {
88    extension_suggest_last_input_ = input;
89    extension_suggest_matches_.clear();
90
91    // We only have to wait for suggest results if there are actually
92    // extensions listening for input changes.
93    if (extensions::ExtensionOmniboxEventRouter::OnInputChanged(
94            profile(), template_url->GetExtensionId(),
95            base::UTF16ToUTF8(remaining_input), current_input_id_))
96      set_done(false);
97  }
98  return input.want_asynchronous_matches();
99}
100
101void KeywordExtensionsDelegateImpl::EnterExtensionKeywordMode(
102    const std::string& extension_id) {
103  DCHECK(current_keyword_extension_id_.empty());
104  current_keyword_extension_id_ = extension_id;
105
106  extensions::ExtensionOmniboxEventRouter::OnInputStarted(
107      profile(), current_keyword_extension_id_);
108}
109
110void KeywordExtensionsDelegateImpl::MaybeEndExtensionKeywordMode() {
111  if (!current_keyword_extension_id_.empty()) {
112    extensions::ExtensionOmniboxEventRouter::OnInputCancelled(
113        profile(), current_keyword_extension_id_);
114    current_keyword_extension_id_.clear();
115  }
116}
117
118void KeywordExtensionsDelegateImpl::Observe(
119    int type,
120    const content::NotificationSource& source,
121    const content::NotificationDetails& details) {
122  TemplateURLService* model = provider_->GetTemplateURLService();
123  const AutocompleteInput& input = extension_suggest_last_input_;
124
125  switch (type) {
126    case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED:
127      // Input has been accepted, so we're done with this input session. Ensure
128      // we don't send the OnInputCancelled event, or handle any more stray
129      // suggestions_ready events.
130      current_keyword_extension_id_.clear();
131      current_input_id_ = 0;
132      return;
133
134    case chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED: {
135      // It's possible to change the default suggestion while not in an editing
136      // session.
137      base::string16 keyword, remaining_input;
138      if (matches()->empty() || current_keyword_extension_id_.empty() ||
139          !KeywordProvider::ExtractKeywordFromInput(
140              input, &keyword, &remaining_input))
141        return;
142
143      const TemplateURL* template_url(
144          model->GetTemplateURLForKeyword(keyword));
145      extensions::ApplyDefaultSuggestionForExtensionKeyword(
146          profile(), template_url, remaining_input, &matches()->front());
147      OnProviderUpdate(true);
148      return;
149    }
150
151    case chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY: {
152      const omnibox_api::SendSuggestions::Params& suggestions =
153          *content::Details<
154              omnibox_api::SendSuggestions::Params>(details).ptr();
155      if (suggestions.request_id != current_input_id_)
156        return;  // This is an old result. Just ignore.
157
158      base::string16 keyword, remaining_input;
159      bool result = KeywordProvider::ExtractKeywordFromInput(
160          input, &keyword, &remaining_input);
161      DCHECK(result);
162      const TemplateURL* template_url =
163          model->GetTemplateURLForKeyword(keyword);
164
165      // TODO(mpcomplete): consider clamping the number of suggestions to
166      // AutocompleteProvider::kMaxMatches.
167      for (size_t i = 0; i < suggestions.suggest_results.size(); ++i) {
168        const omnibox_api::SuggestResult& suggestion =
169            *suggestions.suggest_results[i];
170        // We want to order these suggestions in descending order, so start with
171        // the relevance of the first result (added synchronously in Start()),
172        // and subtract 1 for each subsequent suggestion from the extension.
173        // We recompute the first match's relevance; we know that |complete|
174        // is true, because we wouldn't get results from the extension unless
175        // the full keyword had been typed.
176        int first_relevance = KeywordProvider::CalculateRelevance(
177            input.type(), true, true, input.prefer_keyword(),
178            input.allow_exact_keyword_match());
179        // Because these matches are async, we should never let them become the
180        // default match, lest we introduce race conditions in the omnibox user
181        // interaction.
182        extension_suggest_matches_.push_back(
183            provider_->CreateAutocompleteMatch(
184                template_url, input, keyword.length(),
185                base::UTF8ToUTF16(suggestion.content), false,
186                first_relevance - (i + 1)));
187
188        AutocompleteMatch* match = &extension_suggest_matches_.back();
189        match->contents.assign(base::UTF8ToUTF16(suggestion.description));
190        match->contents_class =
191            extensions::StyleTypesToACMatchClassifications(suggestion);
192      }
193
194      set_done(true);
195      matches()->insert(matches()->end(),
196                        extension_suggest_matches_.begin(),
197                        extension_suggest_matches_.end());
198      OnProviderUpdate(!extension_suggest_matches_.empty());
199      return;
200    }
201
202    default:
203      NOTREACHED();
204      return;
205  }
206}
207
208void KeywordExtensionsDelegateImpl::OnProviderUpdate(bool updated_matches) {
209  provider_->listener_->OnProviderUpdate(updated_matches);
210}
211