| // Copyright (c) 2012 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/autocomplete/extension_app_provider.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "base/strings/string16.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_system_factory.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/history/history_service.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/history/url_database.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/extensions/application_launch.h" |
| #include "chrome/browser/ui/webui/ntp/core_app_launcher_handler.h" |
| #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" |
| #include "content/public/browser/notification_source.h" |
| #include "extensions/common/extension.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| ExtensionAppProvider::ExtensionAppProvider( |
| AutocompleteProviderListener* listener, |
| Profile* profile) |
| : AutocompleteProvider(listener, profile, |
| AutocompleteProvider::TYPE_EXTENSION_APP) { |
| // Notifications of extensions loading and unloading always come from the |
| // non-incognito profile, but we need to see them regardless, as the incognito |
| // windows can be affected. |
| registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, |
| content::Source<Profile>(profile_->GetOriginalProfile())); |
| registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED, |
| content::Source<Profile>(profile_->GetOriginalProfile())); |
| RefreshAppList(); |
| } |
| |
| // static. |
| void ExtensionAppProvider::LaunchAppFromOmnibox( |
| const AutocompleteMatch& match, |
| Profile* profile, |
| WindowOpenDisposition disposition) { |
| ExtensionService* service = |
| extensions::ExtensionSystemFactory::GetForProfile(profile)-> |
| extension_service(); |
| const extensions::Extension* extension = |
| service->GetInstalledApp(match.destination_url); |
| // While the Omnibox popup is open, the extension can be updated, changing |
| // its URL and leaving us with no extension being found. In this case, we |
| // ignore the request. |
| if (!extension) |
| return; |
| |
| CoreAppLauncherHandler::RecordAppLaunchType( |
| extension_misc::APP_LAUNCH_OMNIBOX_APP, |
| extension->GetType()); |
| |
| OpenApplication(AppLaunchParams(profile, extension, disposition)); |
| } |
| |
| void ExtensionAppProvider::AddExtensionAppForTesting( |
| const ExtensionApp& extension_app) { |
| extension_apps_.push_back(extension_app); |
| } |
| |
| AutocompleteMatch ExtensionAppProvider::CreateAutocompleteMatch( |
| const AutocompleteInput& input, |
| const ExtensionApp& app, |
| size_t name_match_index, |
| size_t url_match_index) { |
| // TODO(finnur): Figure out what type to return here, might want to have |
| // the extension icon/a generic icon show up in the Omnibox. |
| AutocompleteMatch match(this, 0, false, |
| AutocompleteMatchType::EXTENSION_APP); |
| match.fill_into_edit = |
| app.should_match_against_launch_url ? app.launch_url : input.text(); |
| match.destination_url = GURL(app.launch_url); |
| match.allowed_to_be_default_match = true; |
| match.contents = AutocompleteMatch::SanitizeString(app.name); |
| AutocompleteMatch::ClassifyLocationInString(name_match_index, |
| input.text().length(), app.name.length(), ACMatchClassification::NONE, |
| &match.contents_class); |
| if (app.should_match_against_launch_url) { |
| match.description = app.launch_url; |
| AutocompleteMatch::ClassifyLocationInString(url_match_index, |
| input.text().length(), app.launch_url.length(), |
| ACMatchClassification::URL, &match.description_class); |
| } |
| match.relevance = CalculateRelevance( |
| input.type(), |
| input.text().length(), |
| name_match_index != base::string16::npos ? |
| app.name.length() : app.launch_url.length(), |
| match.destination_url); |
| return match; |
| } |
| |
| void ExtensionAppProvider::Start(const AutocompleteInput& input, |
| bool minimal_changes) { |
| matches_.clear(); |
| |
| if ((input.type() == AutocompleteInput::INVALID) || |
| (input.type() == AutocompleteInput::FORCED_QUERY)) |
| return; |
| |
| if (input.text().empty()) |
| return; |
| |
| for (ExtensionApps::const_iterator app = extension_apps_.begin(); |
| app != extension_apps_.end(); ++app) { |
| // See if the input matches this extension application. |
| const base::string16& name = app->name; |
| base::string16::const_iterator name_iter = |
| std::search(name.begin(), name.end(), |
| input.text().begin(), input.text().end(), |
| base::CaseInsensitiveCompare<char16>()); |
| bool matches_name = name_iter != name.end(); |
| size_t name_match_index = matches_name ? |
| static_cast<size_t>(name_iter - name.begin()) : base::string16::npos; |
| |
| bool matches_url = false; |
| size_t url_match_index = base::string16::npos; |
| if (app->should_match_against_launch_url) { |
| const base::string16& url = app->launch_url; |
| base::string16::const_iterator url_iter = |
| std::search(url.begin(), url.end(), |
| input.text().begin(), input.text().end(), |
| base::CaseInsensitiveCompare<char16>()); |
| matches_url = url_iter != url.end() && |
| input.type() != AutocompleteInput::FORCED_QUERY; |
| url_match_index = matches_url ? |
| static_cast<size_t>(url_iter - url.begin()) : base::string16::npos; |
| } |
| |
| if (matches_name || matches_url) { |
| // We have a match, might be a partial match. |
| matches_.push_back(CreateAutocompleteMatch( |
| input, *app, name_match_index, url_match_index)); |
| } |
| } |
| } |
| |
| ExtensionAppProvider::~ExtensionAppProvider() { |
| } |
| |
| void ExtensionAppProvider::RefreshAppList() { |
| ExtensionService* extension_service = |
| extensions::ExtensionSystemFactory::GetForProfile(profile_)-> |
| extension_service(); |
| if (!extension_service) |
| return; // During testing, there is no extension service. |
| const ExtensionSet* extensions = extension_service->extensions(); |
| extension_apps_.clear(); |
| for (ExtensionSet::const_iterator iter = extensions->begin(); |
| iter != extensions->end(); ++iter) { |
| const extensions::Extension* app = iter->get(); |
| if (!app->ShouldDisplayInAppLauncher()) |
| continue; |
| // Note: Apps that appear in the NTP only are not added here since this |
| // provider is currently only used in the app launcher. |
| |
| if (profile_->IsOffTheRecord() && |
| !extension_util::CanLoadInIncognito(app, extension_service)) |
| continue; |
| |
| GURL launch_url = app->is_platform_app() ? |
| app->url() : extensions::AppLaunchInfo::GetFullLaunchURL(app); |
| DCHECK(launch_url.is_valid()); |
| |
| ExtensionApp extension_app = { |
| UTF8ToUTF16(app->name()), |
| UTF8ToUTF16(launch_url.spec()), |
| // Only hosted apps have recognizable URLs that users might type in, |
| // packaged apps and hosted apps use chrome-extension:// URLs that are |
| // normally not shown to users. |
| app->is_hosted_app() |
| }; |
| extension_apps_.push_back(extension_app); |
| } |
| } |
| |
| void ExtensionAppProvider::Observe(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| RefreshAppList(); |
| } |
| |
| int ExtensionAppProvider::CalculateRelevance(AutocompleteInput::Type type, |
| int input_length, |
| int target_length, |
| const GURL& url) { |
| // If you update the algorithm here, please remember to update the tables in |
| // autocomplete.h also. |
| const int kMaxRelevance = 1425; |
| |
| if (input_length == target_length) |
| return kMaxRelevance; |
| |
| // We give a boost proportionally based on how much of the input matches the |
| // app name, up to a maximum close to 200 (we can be close to, but we'll never |
| // reach 200 because the 100% match is taken care of above). |
| double fraction_boost = static_cast<double>(200) * |
| input_length / target_length; |
| |
| // We also give a boost relative to how often the user has previously typed |
| // the Extension App URL/selected the Extension App suggestion from this |
| // provider (boost is between 200-400). |
| double type_count_boost = 0; |
| HistoryService* const history_service = |
| HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS); |
| history::URLDatabase* url_db = history_service ? |
| history_service->InMemoryDatabase() : NULL; |
| if (url_db) { |
| history::URLRow info; |
| url_db->GetRowForURL(url, &info); |
| type_count_boost = |
| 400 * (1.0 - (std::pow(static_cast<double>(2), -info.typed_count()))); |
| } |
| int relevance = 575 + static_cast<int>(type_count_boost) + |
| static_cast<int>(fraction_boost); |
| DCHECK_LE(relevance, kMaxRelevance); |
| return relevance; |
| } |