910e62b5创建于 1月15日历史提交
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/commands/command_service.h"

#include <memory>
#include <string_view>
#include <utility>
#include <vector>

#include "base/lazy_instance.h"
#include "base/observer_list.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_commands_global_registry.h"
#include "chrome/browser/extensions/extension_keybinding_registry.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/accelerator_utils.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "extensions/browser/extension_function_registry.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_system.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/commands/commands_handler.h"
#include "extensions/common/command.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/feature_switch.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ui/base/accelerators/command.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {
namespace {

const char kExtension[] = "extension";
const char kCommandName[] = "command_name";
const char kGlobal[] = "global";

// A preference that stores keybinding state associated with extension commands.
const char kCommands[] = "commands";

// Preference key name for saving the extension-suggested key.
const char kSuggestedKey[] = "suggested_key";

// Preference key name for saving whether the extension-suggested key was
// actually assigned.
const char kSuggestedKeyWasAssigned[] = "was_assigned";

std::string GetPlatformKeybindingKeyForAccelerator(
    const ui::Accelerator& accelerator,
    const ExtensionId& extension_id) {
  std::string key = Command::CommandPlatform() + ":" +
                    Command::AcceleratorToString(accelerator);

  // Media keys have a 1-to-many relationship with targets, unlike regular
  // shortcut (1-to-1 relationship). That means two or more extensions can
  // register for the same media key so the extension ID needs to be added to
  // the key to make sure the key is unique.
  if (accelerator.IsMediaKey()) {
    key += ":" + extension_id;
  }

  return key;
}

bool IsForCurrentPlatform(const std::string& key) {
  return base::StartsWith(key, Command::CommandPlatform() + ":",
                          base::CompareCase::SENSITIVE);
}

// Merge |suggested_key_prefs| into the saved preferences for the extension. We
// merge rather than overwrite to preserve existing was_assigned preferences.
void MergeSuggestedKeyPrefs(const ExtensionId& extension_id,
                            ExtensionPrefs* extension_prefs,
                            base::Value::Dict suggested_key_prefs) {
  const base::Value::Dict* current_prefs =
      extension_prefs->ReadPrefAsDict(extension_id, kCommands);
  if (current_prefs) {
    base::Value::Dict new_prefs = current_prefs->Clone();
    new_prefs.Merge(std::move(suggested_key_prefs));
    suggested_key_prefs = std::move(new_prefs);
  }

  extension_prefs->UpdateExtensionPref(
      extension_id, kCommands, base::Value(std::move(suggested_key_prefs)));
}

// Clears the "was_assigned" preference for a list of `removed_commands`. This
// is called when a keybinding is removed to signify that the suggested key is
// no longer assigned, so it can be auto-assigned again in the future.
void ClearSuggestedKeyWasAssignedPrefs(
    const ExtensionId& extension_id,
    ExtensionPrefs& extension_prefs,
    const std::vector<Command>& removed_commands) {
  if (removed_commands.empty()) {
    return;
  }

  ExtensionPrefs::ScopedDictionaryUpdate updater(&extension_prefs, extension_id,
                                                 kCommands);
  std::unique_ptr<prefs::DictionaryValueUpdate> current_prefs = updater.Get();
  if (!current_prefs) {
    return;
  }

  for (const Command& removed_command : removed_commands) {
    std::unique_ptr<prefs::DictionaryValueUpdate> command_prefs;
    if (current_prefs->GetDictionary(removed_command.command_name(),
                                     &command_prefs)) {
      command_prefs->Remove(kSuggestedKeyWasAssigned);
    }
  }
}

// Returns true if a command is relevant for the given `query_type`.
bool IsCommandRelevant(CommandService::QueryType query_type,
                       bool is_active,
                       bool user_modified) {
  switch (query_type) {
    case CommandService::ALL:
      return true;
    case CommandService::ACTIVE:
      return is_active;
    case CommandService::ACTIVE_OR_USER_MODIFIED:
      // We want to be able to include commands that were explicitly unset by
      // the user (via ACTIVE_OR_USER_MODIFIED) so that we don't override
      // their preference with the default binding. See crbug.com/436279086.
      return is_active || user_modified;
  }
}

}  // namespace

// static
void CommandService::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterDictionaryPref(
      prefs::kExtensionCommands,
      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}

CommandService::CommandService(content::BrowserContext* context)
    : profile_(Profile::FromBrowserContext(context)) {
  extension_registry_observation_.Observe(ExtensionRegistry::Get(profile_));
}

CommandService::~CommandService() {
  for (auto& observer : observers_)
    observer.OnCommandServiceDestroying();
}

static base::LazyInstance<BrowserContextKeyedAPIFactory<CommandService>>::
    DestructorAtExit g_command_service_factory = LAZY_INSTANCE_INITIALIZER;

// static
BrowserContextKeyedAPIFactory<CommandService>*
CommandService::GetFactoryInstance() {
  return g_command_service_factory.Pointer();
}

// static
CommandService* CommandService::Get(content::BrowserContext* context) {
  return BrowserContextKeyedAPIFactory<CommandService>::Get(context);
}

bool CommandService::GetNamedCommands(const ExtensionId& extension_id,
                                      QueryType type,
                                      CommandScope scope,
                                      ui::CommandMap* command_map) const {
  const Extension* extension =
      GetExtensionInEnabledOrDisabledExtensions(extension_id);
  if (!extension) {
    return false;
  }

  command_map->clear();
  const ui::CommandMap* commands = CommandsInfo::GetNamedCommands(extension);
  if (!commands)
    return false;

  for (const auto& named_command : *commands) {
    // Look up to see if the user has overridden how the command should work.
    Command saved_command =
        FindCommandByName(extension_id, named_command.second.command_name());
    ui::Accelerator shortcut_assigned = saved_command.accelerator();

    bool user_modified = IsCommandShortcutUserModified(
        extension, named_command.second.command_name());
    bool is_active = shortcut_assigned.key_code() != ui::VKEY_UNKNOWN;
    if (!IsCommandRelevant(type, is_active, user_modified)) {
      continue;
    }

    ui::Command command = named_command.second;
    if (scope != ANY_SCOPE && ((scope == GLOBAL) != saved_command.global()))
      continue;

    if (is_active || user_modified) {
      command.set_accelerator(shortcut_assigned);
    }
    command.set_global(saved_command.global());

    (*command_map)[named_command.second.command_name()] = command;
  }

  return !command_map->empty();
}

bool CommandService::AddKeybindingPref(const ui::Accelerator& accelerator,
                                       const ExtensionId& extension_id,
                                       const std::string& command_name,
                                       bool allow_overrides,
                                       bool global) {
  // Nothing needs to be done if the existing command is the same as the desired
  // new one.
  Command existing_command = FindCommandByName(extension_id, command_name);
  if (existing_command.command_name() == command_name &&
      existing_command.accelerator() == accelerator &&
      existing_command.global() == global) {
    return true;
  }

  // Media Keys are allowed to be used by named command only.
  DCHECK(!accelerator.IsMediaKey() ||
         !Command::IsActionRelatedCommand(command_name));

  ScopedDictPrefUpdate updater(profile_->GetPrefs(), prefs::kExtensionCommands);
  base::Value::Dict& bindings = updater.Get();

  std::string key = GetPlatformKeybindingKeyForAccelerator(accelerator,
                                                           extension_id);

  if (bindings.Find(key)) {
    if (!allow_overrides)
      return false;  // Already taken.

    // If the shortcut has been assigned to another command, it should be
    // removed before overriding, so that |ExtensionKeybindingRegistry| can get
    // a chance to do clean-up.
    const base::Value::Dict* item = bindings.FindDict(key);
    const ExtensionId* old_extension_id = item->FindString(kExtension);
    const std::string* old_command_name = item->FindString(kCommandName);
    RemoveKeybindingPrefs(old_extension_id ? *old_extension_id : std::string(),
                          old_command_name ? *old_command_name : std::string());
  }

  // If the command that is taking a new shortcut already has a shortcut, remove
  // it before assigning the new one.
  if (existing_command.accelerator().key_code() != ui::VKEY_UNKNOWN)
    RemoveKeybindingPrefs(extension_id, command_name);

  if (accelerator.key_code() != ui::VKEY_UNKNOWN) {
    // Set the keybinding pref.
    base::Value::Dict keybinding;
    keybinding.Set(kExtension, extension_id);
    keybinding.Set(kCommandName, command_name);
    keybinding.Set(kGlobal, global);
    bindings.Set(key, std::move(keybinding));
  }

  // Set the was_assigned pref for the suggested key.
  base::Value::Dict command_keys;
  command_keys.Set(kSuggestedKeyWasAssigned, true);
  base::Value::Dict suggested_key_prefs;
  suggested_key_prefs.Set(command_name, base::Value(std::move(command_keys)));
  MergeSuggestedKeyPrefs(extension_id, ExtensionPrefs::Get(profile_),
                         std::move(suggested_key_prefs));

  for (auto& observer : observers_) {
    if (accelerator.key_code() != ui::VKEY_UNKNOWN) {
      observer.OnExtensionCommandAdded(extension_id, command_name);
    } else {
      observer.OnExtensionCommandRemoved(extension_id, command_name);
    }
  }

  return true;
}

void CommandService::OnExtensionWillBeInstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    bool is_update,
    const std::string& old_name) {
  UpdateKeybindings(extension);
}

void CommandService::OnExtensionUninstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    extensions::UninstallReason reason) {
  // Adding a component extensions will only trigger install the first time on a
  // clean profile or on a version increase (see
  // ComponentLoader::AddComponentExtension). It will, however, always trigger
  // an uninstall on removal. See http://crbug.com/458612. Isolate this case and
  // ignore it.
  if (reason == extensions::UNINSTALL_REASON_COMPONENT_REMOVED)
    return;

  RemoveKeybindingPrefs(extension->id(), std::string());
}

void CommandService::UpdateKeybindingPrefs(const ExtensionId& extension_id,
                                           const std::string& command_name,
                                           const std::string& keystroke) {
  Command command = FindCommandByName(extension_id, command_name);

  // The extension command might be assigned another shortcut. Remove that
  // shortcut before proceeding.
  RemoveKeybindingPrefs(extension_id, command_name);

  ui::Accelerator accelerator =
      Command::StringToAccelerator(keystroke, command_name);
  AddKeybindingPref(accelerator, extension_id, command_name,
                    true, command.global());
}

bool CommandService::SetScope(const ExtensionId& extension_id,
                              const std::string& command_name,
                              bool global) {
  Command command = FindCommandByName(extension_id, command_name);
  if (global == command.global())
    return false;

  // Pre-existing shortcuts must be removed before proceeding because the
  // handlers for global and non-global extensions are not one and the same.
  RemoveKeybindingPrefs(extension_id, command_name);
  AddKeybindingPref(command.accelerator(), extension_id,
                    command_name, true, global);
  return true;
}

Command CommandService::FindCommandByName(const ExtensionId& extension_id,
                                          const std::string& command) const {
  const base::Value::Dict& bindings =
      profile_->GetPrefs()->GetDict(prefs::kExtensionCommands);
  for (const auto it : bindings) {
    const ExtensionId* extension = it.second.GetDict().FindString(kExtension);
    if (!extension || *extension != extension_id)
      continue;
    const std::string* command_name =
        it.second.GetDict().FindString(kCommandName);
    if (!command_name || *command_name != command)
      continue;
    // Format stored in Preferences is: "Platform:Shortcut[:ExtensionId]".
    std::string shortcut = it.first;
    if (!IsForCurrentPlatform(shortcut))
      continue;
    std::optional<bool> global = it.second.GetDict().FindBool(kGlobal);

    std::vector<std::string_view> tokens = base::SplitStringPiece(
        shortcut, ":", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
    CHECK(tokens.size() >= 2);

    return Command(*command_name, std::u16string(), std::string(tokens[1]),
                   global.value_or(false));
  }

  return Command();
}

void CommandService::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void CommandService::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

const Extension* CommandService::GetExtensionInEnabledOrDisabledExtensions(
    const ExtensionId& extension_id) const {
  const ExtensionSet& enabled_extensions =
      ExtensionRegistry::Get(profile_)->enabled_extensions();
  const Extension* enabled_extension = enabled_extensions.GetByID(extension_id);
  if (enabled_extension) {
    return enabled_extension;
  }
  const ExtensionSet& disabled_extensions =
      ExtensionRegistry::Get(profile_)->disabled_extensions();
  return disabled_extensions.GetByID(extension_id);
}

bool CommandService::IsUpgradeFromMV2ToMV3(
    const Extension* extension,
    const std::string& existing_command_name) const {
  bool browser_or_page_action_command_in_bindings =
      existing_command_name == manifest_values::kBrowserActionCommandEvent ||
      existing_command_name == manifest_values::kPageActionCommandEvent;
  return browser_or_page_action_command_in_bindings &&
         CommandsInfo::GetActionCommand(extension);
}

void CommandService::UpdateKeybindings(const Extension* extension) {
  if (GetExtensionInEnabledOrDisabledExtensions(extension->id())) {
    RemoveRelinquishedKeybindings(extension);
  }
  AssignKeybindings(extension);
  UpdateExtensionSuggestedCommandPrefs(extension);
  RemoveDefunctExtensionSuggestedCommandPrefs(extension);
}

void CommandService::RemoveRelinquishedKeybindings(const Extension* extension) {
  // Remove keybindings if they have been removed by the extension and the user
  // has not modified them.
  ui::CommandMap existing_command_map;
  if (GetNamedCommands(extension->id(), CommandService::ACTIVE_OR_USER_MODIFIED,
                       CommandService::REGULAR, &existing_command_map)) {
    const ui::CommandMap* new_command_map =
        CommandsInfo::GetNamedCommands(extension);
    for (ui::CommandMap::const_iterator it = existing_command_map.begin();
         it != existing_command_map.end(); ++it) {
      std::string command_name = it->first;
      if (new_command_map->find(command_name) == new_command_map->end() &&
          !IsCommandShortcutUserModified(extension, command_name)) {
        RemoveKeybindingPrefs(extension->id(), command_name);
      }
    }
  }

  auto remove_overrides_if_unused = [this, extension](ActionInfo::Type type) {
    Command existing_command;
    if (!GetExtensionActionCommand(extension->id(), type,
                                   CommandService::ACTIVE_OR_USER_MODIFIED,
                                   &existing_command, nullptr)) {
      // No keybindings to remove.
      return;
    }

    const std::string& existing_command_name = existing_command.command_name();
    bool is_shortcut_user_modified =
        IsCommandShortcutUserModified(extension, existing_command_name);
    bool is_upgrade_from_mv2_to_mv3 =
        IsUpgradeFromMV2ToMV3(extension, existing_command_name);
    if (is_shortcut_user_modified && is_upgrade_from_mv2_to_mv3) {
      // TODO(jlulejian): Could this be an out param to IsUpgradeFromMV2ToMV3?
      const Command* action_command = CommandsInfo::GetActionCommand(extension);
      AddKeybindingPref(existing_command.accelerator(), extension->id(),
                        action_command->command_name(), true,
                        action_command->global());
    } else if (is_shortcut_user_modified) {
      // Don't relinquish user-modified shortcuts otherwise.
      return;
    }

    const Command* new_command = nullptr;
    switch (type) {
      case ActionInfo::Type::kAction:
        new_command = CommandsInfo::GetActionCommand(extension);
        break;
      case ActionInfo::Type::kBrowser:
        new_command = CommandsInfo::GetBrowserActionCommand(extension);
        break;
      case ActionInfo::Type::kPage:
        new_command = CommandsInfo::GetPageActionCommand(extension);
        break;
    }

    // The shortcuts should be removed if there is no command specified in the
    // new extension, or the only command specified is synthesized (i.e.,
    // assigned to ui::VKEY_UNKNOWN), which happens for browser action commands.
    // See CommandsHandler::MaybeSetActionDefault().
    // TODO(devlin): Should this logic apply to ActionInfo::Type::kAction?
    // See https://crbug.com/893373.
    const bool should_relinquish =
        !new_command ||
        (type == ActionInfo::Type::kBrowser &&
         new_command->accelerator().key_code() == ui::VKEY_UNKNOWN);

    if (!should_relinquish)
      return;

    RemoveKeybindingPrefs(extension->id(), existing_command_name);
  };

  // TODO(crbug.com/40124879): Extensions shouldn't be able to specify
  // commands for actions they don't have, so we should just be able to query
  // for a single action type.
  for (ActionInfo::Type type :
       {ActionInfo::Type::kAction, ActionInfo::Type::kBrowser,
        ActionInfo::Type::kPage}) {
    remove_overrides_if_unused(type);
  }
}

void CommandService::AssignKeybindings(const Extension* extension) {
  const ui::CommandMap* commands = CommandsInfo::GetNamedCommands(extension);
  if (!commands)
    return;

  for (const auto& named_command : *commands) {
    const ui::Command command = named_command.second;
    if (CanAutoAssign(command, extension)) {
      AddKeybindingPref(command.accelerator(),
                        extension->id(),
                        command.command_name(),
                        false,  // Overwriting not allowed.
                        command.global());
    }
  }

  const Command* browser_action_command =
      CommandsInfo::GetBrowserActionCommand(extension);
  if (browser_action_command &&
      CanAutoAssign(*browser_action_command, extension)) {
    AddKeybindingPref(browser_action_command->accelerator(),
                      extension->id(),
                      browser_action_command->command_name(),
                      false,   // Overwriting not allowed.
                      false);  // Not global.
  }

  const Command* page_action_command =
      CommandsInfo::GetPageActionCommand(extension);
  if (page_action_command && CanAutoAssign(*page_action_command, extension)) {
    AddKeybindingPref(page_action_command->accelerator(),
                      extension->id(),
                      page_action_command->command_name(),
                      false,   // Overwriting not allowed.
                      false);  // Not global.
  }

  const Command* action_command = CommandsInfo::GetActionCommand(extension);
  if (action_command && CanAutoAssign(*action_command, extension)) {
    AddKeybindingPref(action_command->accelerator(), extension->id(),
                      action_command->command_name(),
                      false,   // Overwriting not allowed.
                      false);  // Not global.
  }
}

bool CommandService::CanAutoAssign(const ui::Command& command,
                                   const Extension* extension) {
  // Extensions are allowed to auto-assign updated keys if the user has not
  // changed from the previous value.
  if (IsCommandShortcutUserModified(extension, command.command_name()))
    return false;

  // Media Keys are non-exclusive, so allow auto-assigning them.
  if (command.accelerator().IsMediaKey()) {
    return true;
  }

  if (command.global()) {
    if (Command::IsActionRelatedCommand(command.command_name()))
      return false;  // Browser and page actions are not global in nature.

    if (extension->permissions_data()->HasAPIPermission(
            mojom::APIPermissionID::kCommandsAccessibility))
      return true;

    // Global shortcuts are restricted to (Ctrl|Command)+Shift+[0-9].
#if BUILDFLAG(IS_MAC)
    if (!command.accelerator().IsCmdDown())
      return false;
#else
    if (!command.accelerator().IsCtrlDown())
      return false;
#endif
    if (!command.accelerator().IsShiftDown())
      return false;
    return (command.accelerator().key_code() >= ui::VKEY_0 &&
            command.accelerator().key_code() <= ui::VKEY_9);
  }

  // Not a global command, check if the command is a Chrome shortcut.
  return !IsChromeAccelerator(command.accelerator());
}

void CommandService::UpdateExtensionSuggestedCommandPrefs(
    const Extension* extension) {
  base::Value::Dict suggested_key_prefs;

  const ui::CommandMap* commands = CommandsInfo::GetNamedCommands(extension);
  if (commands) {
    for (const auto& named_command : *commands) {
      const ui::Command command = named_command.second;
      base::Value::Dict command_keys;
      command_keys.Set(kSuggestedKey,
                       Command::AcceleratorToString(command.accelerator()));
      suggested_key_prefs.Set(command.command_name(), std::move(command_keys));
    }
  }

  const Command* browser_action_command =
      CommandsInfo::GetBrowserActionCommand(extension);
  // The browser action command may be defaulted to an unassigned accelerator if
  // a browser action is specified by the extension but a keybinding is not
  // declared. See CommandsHandler::MaybeSetActionDefault.
  if (browser_action_command &&
      browser_action_command->accelerator().key_code() != ui::VKEY_UNKNOWN) {
    base::Value::Dict command_keys;
    command_keys.Set(kSuggestedKey, Command::AcceleratorToString(
                                        browser_action_command->accelerator()));
    suggested_key_prefs.Set(browser_action_command->command_name(),
                            std::move(command_keys));
  }

  const Command* action_command = CommandsInfo::GetActionCommand(extension);
  if (action_command &&
      action_command->accelerator().key_code() != ui::VKEY_UNKNOWN) {
    base::Value::Dict command_keys;
    command_keys.Set(kSuggestedKey, Command::AcceleratorToString(
                                        action_command->accelerator()));
    suggested_key_prefs.Set(action_command->command_name(),
                            std::move(command_keys));
  }

  const Command* page_action_command =
      CommandsInfo::GetPageActionCommand(extension);
  if (page_action_command) {
    base::Value::Dict command_keys;
    command_keys.Set(kSuggestedKey, Command::AcceleratorToString(
                                        page_action_command->accelerator()));
    suggested_key_prefs.Set(page_action_command->command_name(),
                            std::move(command_keys));
  }

  // Merge into current prefs, if present.
  MergeSuggestedKeyPrefs(extension->id(), ExtensionPrefs::Get(profile_),
                         std::move(suggested_key_prefs));
}

void CommandService::RemoveDefunctExtensionSuggestedCommandPrefs(
    const Extension* extension) {
  ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile_);
  const base::Value::Dict* current_prefs =
      extension_prefs->ReadPrefAsDict(extension->id(), kCommands);

  if (current_prefs) {
    base::Value::Dict suggested_key_prefs = current_prefs->Clone();

    const ui::CommandMap* named_commands =
        CommandsInfo::GetNamedCommands(extension);

    const Command* browser_action_command =
        CommandsInfo::GetBrowserActionCommand(extension);
    for (const auto [key, _] : *current_prefs) {
      if (key == manifest_values::kBrowserActionCommandEvent) {
        // The browser action command may be defaulted to an unassigned
        // accelerator if a browser action is specified by the extension but a
        // keybinding is not declared. See
        // CommandsHandler::MaybeSetActionDefault.
        if (!browser_action_command ||
            browser_action_command->accelerator().key_code() ==
                ui::VKEY_UNKNOWN) {
          suggested_key_prefs.Remove(key);
        }
      } else if (key == manifest_values::kPageActionCommandEvent) {
        if (!CommandsInfo::GetPageActionCommand(extension))
          suggested_key_prefs.Remove(key);
      } else if (key == manifest_values::kActionCommandEvent) {
        if (!CommandsInfo::GetActionCommand(extension))
          suggested_key_prefs.Remove(key);
      } else if (named_commands) {
        if (named_commands->find(key) == named_commands->end())
          suggested_key_prefs.Remove(key);
      }
    }

    extension_prefs->UpdateExtensionPref(
        extension->id(), kCommands,
        base::Value(std::move(suggested_key_prefs)));
  }
}

bool CommandService::IsCommandShortcutUserModified(
    const Extension* extension,
    const std::string& command_name) const {
  // Get the previous suggested key, if any.
  ui::Accelerator suggested_key;
  std::optional<bool> suggested_key_was_assigned;
  ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile_);
  const base::Value::Dict* commands_prefs =
      extension_prefs->ReadPrefAsDict(extension->id(), kCommands);
  if (commands_prefs) {
    const base::Value::Dict* suggested_key_prefs =
        commands_prefs->FindDict(command_name);
    if (suggested_key_prefs) {
      const std::string* suggested_key_string =
          suggested_key_prefs->FindString(kSuggestedKey);
      if (suggested_key_string) {
        suggested_key =
            Command::StringToAccelerator(*suggested_key_string, command_name);
      }
      suggested_key_was_assigned =
          suggested_key_prefs->FindBool(kSuggestedKeyWasAssigned);
    }
  }

  // Get the active shortcut from the prefs, if any.
  Command active_command = FindCommandByName(extension->id(), command_name);

  return suggested_key_was_assigned.value_or(false)
             ? active_command.accelerator() != suggested_key
             : active_command.accelerator().key_code() != ui::VKEY_UNKNOWN;
}

void CommandService::RemoveKeybindingPrefs(const ExtensionId& extension_id,
                                           const std::string& command_name) {
  ScopedDictPrefUpdate updater(profile_->GetPrefs(), prefs::kExtensionCommands);
  base::Value::Dict& bindings = updater.Get();

  typedef std::vector<std::string> KeysToRemove;
  KeysToRemove keys_to_remove;
  std::vector<Command> removed_commands;
  for (const auto it : bindings) {
    // Removal of keybinding preference should be limited to current platform.
    if (!IsForCurrentPlatform(it.first))
      continue;

    const base::Value::Dict& dict = it.second.GetDict();
    const ExtensionId* extension = dict.FindString(kExtension);

    if (extension && *extension == extension_id) {
      // If |command_name| is specified, delete only that command. Otherwise,
      // delete all commands.
      const std::string* command = dict.FindString(kCommandName);
      if (command && !command_name.empty() && command_name != *command)
        continue;

      removed_commands.push_back(FindCommandByName(extension_id, *command));
      keys_to_remove.push_back(it.first);
    }
  }

  for (KeysToRemove::const_iterator it = keys_to_remove.begin();
       it != keys_to_remove.end(); ++it) {
    std::string key = *it;
    bindings.Remove(key);
  }

  // When a keybinding is removed, we also clear the "was_assigned" bit in the
  // extension prefs.
  ClearSuggestedKeyWasAssignedPrefs(
      extension_id, *ExtensionPrefs::Get(profile_), removed_commands);

  for (const Command& removed_command : removed_commands) {
    for (auto& observer : observers_)
      observer.OnExtensionCommandRemoved(extension_id,
                                         removed_command.command_name());
  }
}

bool CommandService::GetExtensionActionCommand(const ExtensionId& extension_id,
                                               ActionInfo::Type action_type,
                                               QueryType query_type,
                                               Command* command,
                                               bool* active) const {
  const Extension* extension =
      GetExtensionInEnabledOrDisabledExtensions(extension_id);
  if (!extension) {
    return false;
  }

  if (active)
    *active = false;

  const Command* requested_command = nullptr;
  switch (action_type) {
    case ActionInfo::Type::kBrowser:
      requested_command = CommandsInfo::GetBrowserActionCommand(extension);
      break;
    case ActionInfo::Type::kPage:
      requested_command = CommandsInfo::GetPageActionCommand(extension);
      break;
    case ActionInfo::Type::kAction:
      requested_command = CommandsInfo::GetActionCommand(extension);
      break;
  }
  if (!requested_command)
    return false;

  // Look up to see if the user has overridden how the command should work.
  Command saved_command =
      FindCommandByName(extension_id, requested_command->command_name());
  ui::Accelerator shortcut_assigned = saved_command.accelerator();

  bool is_active = shortcut_assigned.key_code() != ui::VKEY_UNKNOWN;
  if (active)
    *active = is_active;

  bool user_modified = IsCommandShortcutUserModified(
      extension, requested_command->command_name());
  if (!IsCommandRelevant(query_type, is_active, user_modified)) {
    return false;
  }

  *command = *requested_command;
  if (is_active || user_modified) {
    command->set_accelerator(shortcut_assigned);
  }

  return true;
}

template <>
void BrowserContextKeyedAPIFactory<
    CommandService>::DeclareFactoryDependencies() {
  DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance());
}

}  // namespace extensions