| // 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)); | 
 |  | 
 |   // Fetch the newly-updated command, and notify the observers. | 
 |   Command command = FindCommandByName(extension_id, command_name); | 
 |   for (auto& observer : observers_) { | 
 |     if (accelerator.key_code() != ui::VKEY_UNKNOWN) { | 
 |       observer.OnExtensionCommandAdded(extension_id, command); | 
 |     } else { | 
 |       observer.OnExtensionCommandRemoved(extension_id, command); | 
 |     } | 
 |   } | 
 |  | 
 |   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 !chrome::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); | 
 |   } | 
 | } | 
 |  | 
 | 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 |