blob: e51bfbecd91dcd9a7fbdc9c7a0c2c754af769e84 [file] [log] [blame]
// 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;
}