Home | History | Annotate | Download | only in omnibox
      1 // Copyright (c) 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/extensions/api/omnibox/omnibox_api.h"
      6 
      7 #include "base/json/json_writer.h"
      8 #include "base/lazy_instance.h"
      9 #include "base/metrics/histogram.h"
     10 #include "base/strings/string16.h"
     11 #include "base/strings/utf_string_conversions.h"
     12 #include "base/values.h"
     13 #include "chrome/browser/chrome_notification_types.h"
     14 #include "chrome/browser/extensions/event_router.h"
     15 #include "chrome/browser/extensions/extension_prefs.h"
     16 #include "chrome/browser/extensions/extension_service.h"
     17 #include "chrome/browser/extensions/extension_system.h"
     18 #include "chrome/browser/extensions/tab_helper.h"
     19 #include "chrome/browser/profiles/profile.h"
     20 #include "chrome/browser/search_engines/template_url.h"
     21 #include "chrome/browser/search_engines/template_url_service.h"
     22 #include "chrome/browser/search_engines/template_url_service_factory.h"
     23 #include "chrome/common/extensions/api/omnibox.h"
     24 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
     25 #include "chrome/common/extensions/extension.h"
     26 #include "content/public/browser/notification_details.h"
     27 #include "content/public/browser/notification_service.h"
     28 #include "ui/gfx/image/image.h"
     29 
     30 namespace events {
     31 const char kOnInputStarted[] = "omnibox.onInputStarted";
     32 const char kOnInputChanged[] = "omnibox.onInputChanged";
     33 const char kOnInputEntered[] = "omnibox.onInputEntered";
     34 const char kOnInputCancelled[] = "omnibox.onInputCancelled";
     35 }  // namespace events
     36 
     37 namespace extensions {
     38 
     39 namespace omnibox = api::omnibox;
     40 namespace SendSuggestions = omnibox::SendSuggestions;
     41 namespace SetDefaultSuggestion = omnibox::SetDefaultSuggestion;
     42 
     43 namespace {
     44 
     45 const char kSuggestionContent[] = "content";
     46 const char kSuggestionDescription[] = "description";
     47 const char kSuggestionDescriptionStyles[] = "descriptionStyles";
     48 const char kSuggestionDescriptionStylesRaw[] = "descriptionStylesRaw";
     49 const char kDescriptionStylesType[] = "type";
     50 const char kDescriptionStylesOffset[] = "offset";
     51 const char kDescriptionStylesLength[] = "length";
     52 const char kCurrentTabDisposition[] = "currentTab";
     53 const char kForegroundTabDisposition[] = "newForegroundTab";
     54 const char kBackgroundTabDisposition[] = "newBackgroundTab";
     55 
     56 // Pref key for omnibox.setDefaultSuggestion.
     57 const char kOmniboxDefaultSuggestion[] = "omnibox_default_suggestion";
     58 
     59 #if defined(OS_LINUX)
     60 static const int kOmniboxIconPaddingLeft = 2;
     61 static const int kOmniboxIconPaddingRight = 2;
     62 #elif defined(OS_MACOSX)
     63 static const int kOmniboxIconPaddingLeft = 0;
     64 static const int kOmniboxIconPaddingRight = 2;
     65 #else
     66 static const int kOmniboxIconPaddingLeft = 0;
     67 static const int kOmniboxIconPaddingRight = 0;
     68 #endif
     69 
     70 scoped_ptr<omnibox::SuggestResult> GetOmniboxDefaultSuggestion(
     71     Profile* profile,
     72     const std::string& extension_id) {
     73   ExtensionPrefs* prefs =
     74       ExtensionSystem::Get(profile)->extension_service()->extension_prefs();
     75 
     76   scoped_ptr<omnibox::SuggestResult> suggestion;
     77   const base::DictionaryValue* dict = NULL;
     78   if (prefs && prefs->ReadPrefAsDictionary(extension_id,
     79                                            kOmniboxDefaultSuggestion,
     80                                            &dict)) {
     81     suggestion.reset(new omnibox::SuggestResult);
     82     omnibox::SuggestResult::Populate(*dict, suggestion.get());
     83   }
     84   return suggestion.Pass();
     85 }
     86 
     87 // Tries to set the omnibox default suggestion; returns true on success or
     88 // false on failure.
     89 bool SetOmniboxDefaultSuggestion(
     90     Profile* profile,
     91     const std::string& extension_id,
     92     const omnibox::DefaultSuggestResult& suggestion) {
     93   ExtensionPrefs* prefs =
     94       ExtensionSystem::Get(profile)->extension_service()->extension_prefs();
     95   if (!prefs)
     96     return false;
     97 
     98   scoped_ptr<base::DictionaryValue> dict = suggestion.ToValue();
     99   // Add the content field so that the dictionary can be used to populate an
    100   // omnibox::SuggestResult.
    101   dict->SetWithoutPathExpansion(kSuggestionContent, new base::StringValue(""));
    102   prefs->UpdateExtensionPref(extension_id,
    103                              kOmniboxDefaultSuggestion,
    104                              dict.release());
    105 
    106   return true;
    107 }
    108 
    109 }  // namespace
    110 
    111 // static
    112 void ExtensionOmniboxEventRouter::OnInputStarted(
    113     Profile* profile, const std::string& extension_id) {
    114   scoped_ptr<Event> event(new Event(
    115       events::kOnInputStarted, make_scoped_ptr(new base::ListValue())));
    116   event->restrict_to_profile = profile;
    117   ExtensionSystem::Get(profile)->event_router()->
    118       DispatchEventToExtension(extension_id, event.Pass());
    119 }
    120 
    121 // static
    122 bool ExtensionOmniboxEventRouter::OnInputChanged(
    123     Profile* profile, const std::string& extension_id,
    124     const std::string& input, int suggest_id) {
    125   if (!extensions::ExtensionSystem::Get(profile)->event_router()->
    126           ExtensionHasEventListener(extension_id, events::kOnInputChanged))
    127     return false;
    128 
    129   scoped_ptr<base::ListValue> args(new base::ListValue());
    130   args->Set(0, Value::CreateStringValue(input));
    131   args->Set(1, Value::CreateIntegerValue(suggest_id));
    132 
    133   scoped_ptr<Event> event(new Event(events::kOnInputChanged, args.Pass()));
    134   event->restrict_to_profile = profile;
    135   ExtensionSystem::Get(profile)->event_router()->
    136       DispatchEventToExtension(extension_id, event.Pass());
    137   return true;
    138 }
    139 
    140 // static
    141 void ExtensionOmniboxEventRouter::OnInputEntered(
    142     content::WebContents* web_contents,
    143     const std::string& extension_id,
    144     const std::string& input,
    145     WindowOpenDisposition disposition) {
    146   Profile* profile =
    147       Profile::FromBrowserContext(web_contents->GetBrowserContext());
    148 
    149   const Extension* extension =
    150       ExtensionSystem::Get(profile)->extension_service()->extensions()->
    151           GetByID(extension_id);
    152   CHECK(extension);
    153   extensions::TabHelper::FromWebContents(web_contents)->
    154       active_tab_permission_granter()->GrantIfRequested(extension);
    155 
    156   scoped_ptr<base::ListValue> args(new base::ListValue());
    157   args->Set(0, Value::CreateStringValue(input));
    158   if (disposition == NEW_FOREGROUND_TAB)
    159     args->Set(1, Value::CreateStringValue(kForegroundTabDisposition));
    160   else if (disposition == NEW_BACKGROUND_TAB)
    161     args->Set(1, Value::CreateStringValue(kBackgroundTabDisposition));
    162   else
    163     args->Set(1, Value::CreateStringValue(kCurrentTabDisposition));
    164 
    165   scoped_ptr<Event> event(new Event(events::kOnInputEntered, args.Pass()));
    166   event->restrict_to_profile = profile;
    167   ExtensionSystem::Get(profile)->event_router()->
    168       DispatchEventToExtension(extension_id, event.Pass());
    169 
    170   content::NotificationService::current()->Notify(
    171       chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED,
    172       content::Source<Profile>(profile),
    173       content::NotificationService::NoDetails());
    174 }
    175 
    176 // static
    177 void ExtensionOmniboxEventRouter::OnInputCancelled(
    178     Profile* profile, const std::string& extension_id) {
    179   scoped_ptr<Event> event(new Event(
    180       events::kOnInputCancelled, make_scoped_ptr(new base::ListValue())));
    181   event->restrict_to_profile = profile;
    182   ExtensionSystem::Get(profile)->event_router()->
    183       DispatchEventToExtension(extension_id, event.Pass());
    184 }
    185 
    186 OmniboxAPI::OmniboxAPI(Profile* profile)
    187     : profile_(profile),
    188       url_service_(TemplateURLServiceFactory::GetForProfile(profile)) {
    189   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED,
    190                  content::Source<Profile>(profile));
    191   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED,
    192                  content::Source<Profile>(profile));
    193   if (url_service_) {
    194     registrar_.Add(this, chrome::NOTIFICATION_TEMPLATE_URL_SERVICE_LOADED,
    195                    content::Source<TemplateURLService>(url_service_));
    196   }
    197 
    198   // Use monochrome icons for Omnibox icons.
    199   omnibox_popup_icon_manager_.set_monochrome(true);
    200   omnibox_icon_manager_.set_monochrome(true);
    201   omnibox_icon_manager_.set_padding(gfx::Insets(0, kOmniboxIconPaddingLeft,
    202                                                 0, kOmniboxIconPaddingRight));
    203 }
    204 
    205 OmniboxAPI::~OmniboxAPI() {
    206 }
    207 
    208 static base::LazyInstance<ProfileKeyedAPIFactory<OmniboxAPI> >
    209     g_factory = LAZY_INSTANCE_INITIALIZER;
    210 
    211 // static
    212 ProfileKeyedAPIFactory<OmniboxAPI>* OmniboxAPI::GetFactoryInstance() {
    213   return &g_factory.Get();
    214 }
    215 
    216 // static
    217 OmniboxAPI* OmniboxAPI::Get(Profile* profile) {
    218   return ProfileKeyedAPIFactory<OmniboxAPI>::GetForProfile(profile);
    219 }
    220 
    221 void OmniboxAPI::Observe(int type,
    222                          const content::NotificationSource& source,
    223                          const content::NotificationDetails& details) {
    224   if (type == chrome::NOTIFICATION_EXTENSION_LOADED) {
    225     const Extension* extension =
    226         content::Details<const Extension>(details).ptr();
    227     const std::string& keyword = OmniboxInfo::GetKeyword(extension);
    228     if (!keyword.empty()) {
    229       // Load the omnibox icon so it will be ready to display in the URL bar.
    230       omnibox_popup_icon_manager_.LoadIcon(profile_, extension);
    231       omnibox_icon_manager_.LoadIcon(profile_, extension);
    232 
    233       if (url_service_) {
    234         url_service_->Load();
    235         if (url_service_->loaded()) {
    236           url_service_->RegisterExtensionKeyword(extension->id(),
    237                                                  extension->name(),
    238                                                  keyword);
    239         } else {
    240           pending_extensions_.insert(extension);
    241         }
    242       }
    243     }
    244   } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) {
    245     const Extension* extension =
    246         content::Details<UnloadedExtensionInfo>(details)->extension;
    247     if (!OmniboxInfo::GetKeyword(extension).empty()) {
    248       if (url_service_) {
    249         if (url_service_->loaded())
    250           url_service_->UnregisterExtensionKeyword(extension->id());
    251         else
    252           pending_extensions_.erase(extension);
    253       }
    254     }
    255   } else {
    256     DCHECK(type == chrome::NOTIFICATION_TEMPLATE_URL_SERVICE_LOADED);
    257     // Load pending extensions.
    258     for (PendingExtensions::const_iterator i(pending_extensions_.begin());
    259          i != pending_extensions_.end(); ++i) {
    260       url_service_->RegisterExtensionKeyword((*i)->id(),
    261                                              (*i)->name(),
    262                                              OmniboxInfo::GetKeyword(*i));
    263     }
    264     pending_extensions_.clear();
    265   }
    266 }
    267 
    268 gfx::Image OmniboxAPI::GetOmniboxIcon(const std::string& extension_id) {
    269   return gfx::Image::CreateFrom1xBitmap(
    270       omnibox_icon_manager_.GetIcon(extension_id));
    271 }
    272 
    273 gfx::Image OmniboxAPI::GetOmniboxPopupIcon(const std::string& extension_id) {
    274   return gfx::Image::CreateFrom1xBitmap(
    275       omnibox_popup_icon_manager_.GetIcon(extension_id));
    276 }
    277 
    278 template <>
    279 void ProfileKeyedAPIFactory<OmniboxAPI>::DeclareFactoryDependencies() {
    280   DependsOn(ExtensionSystemFactory::GetInstance());
    281   DependsOn(TemplateURLServiceFactory::GetInstance());
    282 }
    283 
    284 bool OmniboxSendSuggestionsFunction::RunImpl() {
    285   scoped_ptr<SendSuggestions::Params> params(
    286       SendSuggestions::Params::Create(*args_));
    287   EXTENSION_FUNCTION_VALIDATE(params);
    288 
    289   content::NotificationService::current()->Notify(
    290       chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY,
    291       content::Source<Profile>(profile_->GetOriginalProfile()),
    292       content::Details<SendSuggestions::Params>(params.get()));
    293 
    294   return true;
    295 }
    296 
    297 bool OmniboxSetDefaultSuggestionFunction::RunImpl() {
    298   scoped_ptr<SetDefaultSuggestion::Params> params(
    299       SetDefaultSuggestion::Params::Create(*args_));
    300   EXTENSION_FUNCTION_VALIDATE(params);
    301 
    302   if (SetOmniboxDefaultSuggestion(profile(),
    303                                   extension_id(),
    304                                   params->suggestion)) {
    305     content::NotificationService::current()->Notify(
    306         chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
    307         content::Source<Profile>(profile_->GetOriginalProfile()),
    308         content::NotificationService::NoDetails());
    309   }
    310 
    311   return true;
    312 }
    313 
    314 // This function converts style information populated by the JSON schema
    315 // compiler into an ACMatchClassifications object.
    316 ACMatchClassifications StyleTypesToACMatchClassifications(
    317     const omnibox::SuggestResult &suggestion) {
    318   ACMatchClassifications match_classifications;
    319   if (suggestion.description_styles) {
    320     string16 description = UTF8ToUTF16(suggestion.description);
    321     std::vector<int> styles(description.length(), 0);
    322 
    323     for (std::vector<linked_ptr<omnibox::SuggestResult::DescriptionStylesType> >
    324          ::iterator i = suggestion.description_styles->begin();
    325          i != suggestion.description_styles->end(); ++i) {
    326       omnibox::SuggestResult::DescriptionStylesType* style = i->get();
    327 
    328       int length = description.length();
    329       if (style->length)
    330         length = *style->length;
    331 
    332       size_t offset = style->offset >= 0 ? style->offset :
    333           std::max(0, static_cast<int>(description.length()) + style->offset);
    334 
    335       int type_class;
    336       switch (style->type) {
    337         case omnibox::SuggestResult::DescriptionStylesType::TYPE_URL:
    338           type_class = AutocompleteMatch::ACMatchClassification::URL;
    339           break;
    340         case omnibox::SuggestResult::DescriptionStylesType::TYPE_MATCH:
    341           type_class = AutocompleteMatch::ACMatchClassification::MATCH;
    342           break;
    343         case omnibox::SuggestResult::DescriptionStylesType::TYPE_DIM:
    344           type_class = AutocompleteMatch::ACMatchClassification::DIM;
    345           break;
    346         default:
    347           type_class = AutocompleteMatch::ACMatchClassification::NONE;
    348           return match_classifications;
    349       }
    350 
    351       for (size_t j = offset; j < offset + length && j < styles.size(); ++j)
    352         styles[j] |= type_class;
    353     }
    354 
    355     for (size_t i = 0; i < styles.size(); ++i) {
    356       if (i == 0 || styles[i] != styles[i-1])
    357         match_classifications.push_back(
    358             ACMatchClassification(i, styles[i]));
    359     }
    360   } else {
    361     match_classifications.push_back(
    362         ACMatchClassification(0, ACMatchClassification::NONE));
    363   }
    364 
    365   return match_classifications;
    366 }
    367 
    368 void ApplyDefaultSuggestionForExtensionKeyword(
    369     Profile* profile,
    370     const TemplateURL* keyword,
    371     const string16& remaining_input,
    372     AutocompleteMatch* match) {
    373   DCHECK(keyword->IsExtensionKeyword());
    374 
    375 
    376   scoped_ptr<omnibox::SuggestResult> suggestion(
    377       GetOmniboxDefaultSuggestion(profile, keyword->GetExtensionId()));
    378   if (!suggestion || suggestion->description.empty())
    379     return;  // fall back to the universal default
    380 
    381   const string16 kPlaceholderText(ASCIIToUTF16("%s"));
    382   const string16 kReplacementText(ASCIIToUTF16("<input>"));
    383 
    384   string16 description = UTF8ToUTF16(suggestion->description);
    385   ACMatchClassifications& description_styles = match->contents_class;
    386   description_styles = StyleTypesToACMatchClassifications(*suggestion);
    387 
    388   // Replace "%s" with the user's input and adjust the style offsets to the
    389   // new length of the description.
    390   size_t placeholder(description.find(kPlaceholderText, 0));
    391   if (placeholder != string16::npos) {
    392     string16 replacement =
    393         remaining_input.empty() ? kReplacementText : remaining_input;
    394     description.replace(placeholder, kPlaceholderText.length(), replacement);
    395 
    396     for (size_t i = 0; i < description_styles.size(); ++i) {
    397       if (description_styles[i].offset > placeholder)
    398         description_styles[i].offset += replacement.length() - 2;
    399     }
    400   }
    401 
    402   match->contents.assign(description);
    403 }
    404 
    405 }  // namespace extensions
    406