blob: 2c650ddfa139157768e4770d02a74a2bccf1efbd [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/extensions/extension_keybinding_registry.h"
#include <memory>
#include <utility>
#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/active_tab_permission_granter.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/media_keys_listener_manager.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/event_router.h"
#include "extensions/common/command.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_constants.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ui/ash/media_client_impl.h"
#endif
namespace {
const char kOnCommandEventName[] = "commands.onCommand";
} // namespace
namespace extensions {
ExtensionKeybindingRegistry::ExtensionKeybindingRegistry(
content::BrowserContext* context,
ExtensionFilter extension_filter,
Delegate* delegate)
: browser_context_(context),
extension_filter_(extension_filter),
delegate_(delegate),
shortcut_handling_suspended_(false) {
extension_registry_observation_.Observe(
ExtensionRegistry::Get(browser_context_));
command_service_observation_.Observe(CommandService::Get(browser_context_));
media_keys_listener_ = ui::MediaKeysListener::Create(
this, ui::MediaKeysListener::Scope::kFocused);
}
ExtensionKeybindingRegistry::~ExtensionKeybindingRegistry() {
}
void ExtensionKeybindingRegistry::SetShortcutHandlingSuspended(bool suspended) {
shortcut_handling_suspended_ = suspended;
OnShortcutHandlingSuspended(suspended);
}
void ExtensionKeybindingRegistry::RemoveExtensionKeybinding(
const Extension* extension,
const std::string& command_name) {
bool any_media_keys_removed = false;
auto it = event_targets_.begin();
while (it != event_targets_.end()) {
TargetList& target_list = it->second;
auto target = target_list.begin();
while (target != target_list.end()) {
if (target->first == extension->id() &&
(command_name.empty() || command_name == target->second))
target = target_list.erase(target);
else
target++;
}
auto old = it++;
if (target_list.empty()) {
// Let each platform-specific implementation get a chance to clean up.
RemoveExtensionKeybindingImpl(old->first, command_name);
if (Command::IsMediaKey(old->first)) {
any_media_keys_removed = true;
if (media_keys_listener_)
media_keys_listener_->StopWatchingMediaKey(old->first.key_code());
}
event_targets_.erase(old);
// If a specific command_name was requested, it has now been deleted so no
// further work is required.
if (!command_name.empty())
break;
}
}
// If we're no longer listening to any media keys, tell the browser that
// it can start handling media keys.
if (any_media_keys_removed && !IsListeningToAnyMediaKeys()) {
if (content::MediaKeysListenerManager::
IsMediaKeysListenerManagerEnabled()) {
content::MediaKeysListenerManager* media_keys_listener_manager =
content::MediaKeysListenerManager::GetInstance();
DCHECK(media_keys_listener_manager);
media_keys_listener_manager->EnableInternalMediaKeyHandling();
} else {
#if BUILDFLAG(IS_CHROMEOS_ASH)
MediaClientImpl::Get()->DisableCustomMediaKeyHandler(browser_context_,
this);
#endif
}
}
}
void ExtensionKeybindingRegistry::Init() {
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_);
if (!registry)
return; // ExtensionRegistry can be null during testing.
for (const scoped_refptr<const extensions::Extension>& extension :
registry->enabled_extensions())
if (ExtensionMatchesFilter(extension.get()))
AddExtensionKeybindings(extension.get(), std::string());
}
bool ExtensionKeybindingRegistry::ShouldIgnoreCommand(
const std::string& command) const {
return command == manifest_values::kPageActionCommandEvent ||
command == manifest_values::kBrowserActionCommandEvent ||
command == manifest_values::kActionCommandEvent;
}
bool ExtensionKeybindingRegistry::NotifyEventTargets(
const ui::Accelerator& accelerator) {
return ExecuteCommands(accelerator, std::string());
}
void ExtensionKeybindingRegistry::CommandExecuted(
const std::string& extension_id, const std::string& command) {
const Extension* extension = ExtensionRegistry::Get(browser_context_)
->enabled_extensions()
.GetByID(extension_id);
if (!extension)
return;
std::vector<base::Value> args;
args.emplace_back(base::Value(command));
std::unique_ptr<base::Value> tab_value;
if (delegate_) {
content::WebContents* web_contents =
delegate_->GetWebContentsForExtension();
// Grant before sending the event so that the permission is granted before
// the extension acts on the command. NOTE: The Global Commands handler does
// not set the delegate as it deals only with named commands (not
// page/browser actions that are associated with the current page directly).
ActiveTabPermissionGranter* granter =
web_contents ? extensions::TabHelper::FromWebContents(web_contents)
->active_tab_permission_granter()
: nullptr;
if (granter) {
granter->GrantIfRequested(extension);
}
if (web_contents) {
// The action APIs (browserAction, pageAction, action) are only available
// to blessed extension contexts. As such, we deterministically know that
// the right context type here is blessed.
constexpr Feature::Context context_type =
Feature::BLESSED_EXTENSION_CONTEXT;
ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
ExtensionTabUtil::GetScrubTabBehavior(extension, context_type,
web_contents);
tab_value = ExtensionTabUtil::CreateTabObject(
web_contents, scrub_tab_behavior, extension)
->ToValue();
}
}
if (!tab_value) {
// No currently-active tab. Push a null value.
tab_value = std::make_unique<base::Value>();
}
args.push_back(base::Value::FromUniquePtrValue(std::move(tab_value)));
auto event =
std::make_unique<Event>(events::COMMANDS_ON_COMMAND, kOnCommandEventName,
std::move(args), browser_context_);
event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
EventRouter::Get(browser_context_)
->DispatchEventToExtension(extension_id, std::move(event));
}
bool ExtensionKeybindingRegistry::IsAcceleratorRegistered(
const ui::Accelerator& accelerator) const {
return event_targets_.find(accelerator) != event_targets_.end();
}
void ExtensionKeybindingRegistry::AddEventTarget(
const ui::Accelerator& accelerator,
const std::string& extension_id,
const std::string& command_name) {
event_targets_[accelerator].push_back(
std::make_pair(extension_id, command_name));
// Shortcuts except media keys have only one target in the list. See comment
// about |event_targets_|.
if (!Command::IsMediaKey(accelerator)) {
DCHECK_EQ(1u, event_targets_[accelerator].size());
} else {
if (media_keys_listener_)
media_keys_listener_->StartWatchingMediaKey(accelerator.key_code());
// Tell the browser that it should not handle media keys, since we're going
// to handle them.
if (content::MediaKeysListenerManager::
IsMediaKeysListenerManagerEnabled()) {
content::MediaKeysListenerManager* media_keys_listener_manager =
content::MediaKeysListenerManager::GetInstance();
DCHECK(media_keys_listener_manager);
media_keys_listener_manager->DisableInternalMediaKeyHandling();
} else {
#if BUILDFLAG(IS_CHROMEOS_ASH)
MediaClientImpl::Get()->EnableCustomMediaKeyHandler(browser_context_,
this);
#endif
}
}
}
bool ExtensionKeybindingRegistry::GetFirstTarget(
const ui::Accelerator& accelerator,
std::string* extension_id,
std::string* command_name) const {
auto targets = event_targets_.find(accelerator);
if (targets == event_targets_.end())
return false;
DCHECK(!targets->second.empty());
auto first_target = targets->second.begin();
*extension_id = first_target->first;
*command_name = first_target->second;
return true;
}
bool ExtensionKeybindingRegistry::IsEventTargetsEmpty() const {
return event_targets_.empty();
}
void ExtensionKeybindingRegistry::OnExtensionLoaded(
content::BrowserContext* browser_context,
const Extension* extension) {
if (ExtensionMatchesFilter(extension))
AddExtensionKeybindings(extension, std::string());
}
void ExtensionKeybindingRegistry::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionReason reason) {
if (ExtensionMatchesFilter(extension))
RemoveExtensionKeybinding(extension, std::string());
}
void ExtensionKeybindingRegistry::OnExtensionCommandAdded(
const std::string& extension_id,
const Command& command) {
const Extension* extension = ExtensionRegistry::Get(browser_context_)
->enabled_extensions()
.GetByID(extension_id);
// During install and uninstall the extension won't be found. We'll catch
// those events above, with the OnExtension[Unloaded|Loaded], so we ignore
// this event.
if (!extension || !ExtensionMatchesFilter(extension))
return;
// Component extensions trigger OnExtensionLoaded() for extension
// installs as well as loads. This can cause adding of multiple key
// targets.
if (extension->location() == mojom::ManifestLocation::kComponent)
return;
AddExtensionKeybindings(extension, command.command_name());
}
void ExtensionKeybindingRegistry::OnExtensionCommandRemoved(
const std::string& extension_id,
const Command& command) {
const Extension* extension = ExtensionRegistry::Get(browser_context_)
->enabled_extensions()
.GetByID(extension_id);
// During install and uninstall the extension won't be found. We'll catch
// those events above, with the OnExtension[Unloaded|Loaded], so we ignore
// this event.
if (!extension || !ExtensionMatchesFilter(extension))
return;
RemoveExtensionKeybinding(extension, command.command_name());
}
void ExtensionKeybindingRegistry::OnCommandServiceDestroying() {
command_service_observation_.Reset();
}
void ExtensionKeybindingRegistry::OnMediaKeysAccelerator(
const ui::Accelerator& accelerator) {
NotifyEventTargets(accelerator);
}
bool ExtensionKeybindingRegistry::ExtensionMatchesFilter(
const extensions::Extension* extension)
{
switch (extension_filter_) {
case ALL_EXTENSIONS:
return true;
case PLATFORM_APPS_ONLY:
return extension->is_platform_app();
default:
NOTREACHED();
}
return false;
}
bool ExtensionKeybindingRegistry::ExecuteCommands(
const ui::Accelerator& accelerator,
const std::string& extension_id) {
auto targets = event_targets_.find(accelerator);
if (targets == event_targets_.end() || targets->second.empty())
return false;
bool executed = false;
for (TargetList::const_iterator it = targets->second.begin();
it != targets->second.end(); it++) {
if (!extensions::EventRouter::Get(browser_context_)
->ExtensionHasEventListener(it->first, kOnCommandEventName))
continue;
if (extension_id.empty() || it->first == extension_id) {
CommandExecuted(it->first, it->second);
executed = true;
}
}
return executed;
}
bool ExtensionKeybindingRegistry::IsListeningToAnyMediaKeys() const {
for (const auto& accelerator_target : event_targets_) {
if (Command::IsMediaKey(accelerator_target.first))
return true;
}
return false;
}
} // namespace extensions