| // 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/ui/toolbar/toolbar_actions_model.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram_base.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/one_shot_event.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/stl_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "chrome/browser/extensions/extension_message_bubble_controller.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/tab_helper.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/extensions/extension_action_view_controller.h" |
| #include "chrome/browser/ui/extensions/extension_message_bubble_factory.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h" |
| #include "chrome/browser/ui/toolbar/toolbar_actions_model_factory.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/notification_details.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_action_manager.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/pref_names.h" |
| #include "extensions/browser/unloaded_extension_reason.h" |
| #include "extensions/common/extension_set.h" |
| #include "extensions/common/manifest_constants.h" |
| |
| ToolbarActionsModel::ToolbarActionsModel( |
| Profile* profile, |
| extensions::ExtensionPrefs* extension_prefs) |
| : profile_(profile), |
| extension_prefs_(extension_prefs), |
| prefs_(profile_->GetPrefs()), |
| extension_action_api_(extensions::ExtensionActionAPI::Get(profile_)), |
| extension_registry_(extensions::ExtensionRegistry::Get(profile_)), |
| extension_action_manager_( |
| extensions::ExtensionActionManager::Get(profile_)), |
| actions_initialized_(false), |
| has_active_bubble_(false) { |
| extensions::ExtensionSystem::Get(profile_)->ready().Post( |
| FROM_HERE, base::BindOnce(&ToolbarActionsModel::OnReady, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // We only care about watching toolbar-order prefs if not in incognito mode. |
| pref_change_registrar_.Init(prefs_); |
| pref_change_registrar_.Add( |
| extensions::pref_names::kPinnedExtensions, |
| base::BindRepeating(&ToolbarActionsModel::UpdatePinnedActionIds, |
| base::Unretained(this))); |
| } |
| |
| ToolbarActionsModel::~ToolbarActionsModel() {} |
| |
| // static |
| ToolbarActionsModel* ToolbarActionsModel::Get(Profile* profile) { |
| return ToolbarActionsModelFactory::GetForProfile(profile); |
| } |
| |
| void ToolbarActionsModel::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ToolbarActionsModel::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void ToolbarActionsModel::OnExtensionActionUpdated( |
| extensions::ExtensionAction* extension_action, |
| content::WebContents* web_contents, |
| content::BrowserContext* browser_context) { |
| // Notify observers if the extension exists and is in the model. |
| if (HasAction(extension_action->extension_id())) { |
| for (Observer& observer : observers_) |
| observer.OnToolbarActionUpdated(extension_action->extension_id()); |
| } |
| } |
| |
| void ToolbarActionsModel::OnExtensionLoaded( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension) { |
| // We don't want to add the same extension twice. It may have already been |
| // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user |
| // hides the browser action and then disables and enables the extension. |
| if (!HasAction(extension->id()) && ShouldAddExtension(extension)) |
| AddAction(extension->id()); |
| } |
| |
| void ToolbarActionsModel::OnExtensionUnloaded( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UnloadedExtensionReason reason) { |
| RemoveAction(extension->id()); |
| } |
| |
| void ToolbarActionsModel::OnExtensionUninstalled( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UninstallReason reason) { |
| if (profile_->IsOffTheRecord()) { |
| // The on-the-record version will update the prefs; incognito is read-only. |
| return; |
| } |
| |
| // Remove the extension id from the ordered list, if it exists (the extension |
| // might not be represented in the list because it might not have an icon). |
| RemovePref(extension->id()); |
| } |
| |
| void ToolbarActionsModel::OnExtensionManagementSettingsChanged() { |
| UpdatePinnedActionIds(); |
| } |
| |
| void ToolbarActionsModel::RemovePref(const ActionId& action_id) { |
| // The extension is already unloaded at this point, and so shouldn't be in |
| // the active pinned set. |
| DCHECK(!IsActionPinned(action_id)); |
| auto stored_pinned_actions = extension_prefs_->GetPinnedExtensions(); |
| auto iter = std::find(stored_pinned_actions.begin(), |
| stored_pinned_actions.end(), action_id); |
| if (iter != stored_pinned_actions.end()) { |
| stored_pinned_actions.erase(iter); |
| extension_prefs_->SetPinnedExtensions(stored_pinned_actions); |
| } |
| } |
| |
| void ToolbarActionsModel::OnReady() { |
| InitializeActionList(); |
| |
| // Wait until the extension system is ready before observing any further |
| // changes so that the toolbar buttons can be shown in their stable ordering |
| // taken from prefs. |
| extension_registry_observation_.Observe(extension_registry_); |
| extension_action_observation_.Observe(extension_action_api_); |
| |
| auto* management = |
| extensions::ExtensionManagementFactory::GetForBrowserContext(profile_); |
| extension_management_observation_.Observe(management); |
| |
| actions_initialized_ = true; |
| for (Observer& observer : observers_) |
| observer.OnToolbarModelInitialized(); |
| } |
| |
| bool ToolbarActionsModel::ShouldAddExtension( |
| const extensions::Extension* extension) { |
| // In incognito mode, don't add any extensions that aren't incognito-enabled. |
| if (profile_->IsOffTheRecord() && |
| !extensions::util::IsIncognitoEnabled(extension->id(), profile_)) |
| return false; |
| |
| // In this case, we don't care about the browser action visibility, because |
| // we want to show each extension regardless. |
| return extension_action_manager_->GetExtensionAction(*extension) != nullptr; |
| } |
| |
| void ToolbarActionsModel::AddAction(const ActionId& action_id) { |
| // We only use AddAction() once the system is initialized. |
| CHECK(actions_initialized_); |
| |
| action_ids_.insert(action_id); |
| |
| for (Observer& observer : observers_) |
| observer.OnToolbarActionAdded(action_id); |
| |
| UpdatePinnedActionIds(); |
| } |
| |
| void ToolbarActionsModel::RemoveAction(const ActionId& action_id) { |
| const bool did_erase = action_ids_.erase(action_id) > 0; |
| // TODO(devlin): Can we DCHECK did_erase? |
| if (!did_erase) |
| return; |
| |
| UpdatePinnedActionIds(); |
| |
| for (Observer& observer : observers_) |
| observer.OnToolbarActionRemoved(action_id); |
| } |
| |
| std::unique_ptr<extensions::ExtensionMessageBubbleController> |
| ToolbarActionsModel::GetExtensionMessageBubbleController(Browser* browser) { |
| std::unique_ptr<extensions::ExtensionMessageBubbleController> controller; |
| if (has_active_bubble()) |
| return controller; |
| controller = ExtensionMessageBubbleFactory(browser).GetController(); |
| if (controller) |
| controller->SetIsActiveBubble(); |
| return controller; |
| } |
| |
| bool ToolbarActionsModel::IsActionPinned(const ActionId& action_id) const { |
| return base::Contains(pinned_action_ids_, action_id); |
| } |
| |
| bool ToolbarActionsModel::IsActionForcePinned(const ActionId& action_id) const { |
| auto* management = |
| extensions::ExtensionManagementFactory::GetForBrowserContext(profile_); |
| return base::Contains(management->GetForcePinnedList(), action_id); |
| } |
| |
| void ToolbarActionsModel::MovePinnedAction(const ActionId& action_id, |
| size_t target_index) { |
| // If pinned actions are empty, we're going to have a real bad time (with |
| // out-of-bounds access, size_t wraps, etc). Keep this a hard CHECK (not a |
| // DCHECK). |
| CHECK(!pinned_action_ids_.empty()); |
| DCHECK(!profile_->IsOffTheRecord()) |
| << "Changing action position is disallowed in incognito."; |
| |
| auto current_position_on_toolbar = std::find( |
| pinned_action_ids_.begin(), pinned_action_ids_.end(), action_id); |
| DCHECK(current_position_on_toolbar != pinned_action_ids_.end()); |
| size_t current_index_on_toolbar = |
| current_position_on_toolbar - pinned_action_ids_.begin(); |
| |
| if (current_index_on_toolbar == target_index) |
| return; |
| |
| bool is_left_to_right_move = target_index > current_index_on_toolbar; |
| |
| auto stored_pinned_actions = extension_prefs_->GetPinnedExtensions(); |
| auto target_position = stored_pinned_actions.end(); |
| |
| // Moving pinned actions is a bit tricky (unless we move it to the end - in |
| // which case it's trivial). We need to store the updated state in prefs, but |
| // the prefs also contain pin state information for unloaded (but still |
| // installed) extensions. Thus, we can't just reorder the pinned_action_ids_ |
| // (which only include loaded extensions), and set those directly. |
| // |
| // Instead, we look at the destination of the action in the toolbar, and |
| // find the ID of the action to its right (if any). Then in the stored prefs, |
| // find that action, and insert the moved action to its left. |
| // |
| // For example: |
| // Consider the pinned extension order in prefs is "A [B C] D E", where |
| // B and C are unloaded extensions. Assume we want to A to index 1 on the |
| // toolbar (swapping A and D). We would look for the new action to its |
| // right (E), and insert it in prefs to the left of it. Thus, the new pref |
| // order would be "[B C] D A E". |
| |
| const bool move_to_end = target_index >= pinned_action_ids_.size() - 1; |
| if (!move_to_end) { |
| size_t new_index_to_right = |
| is_left_to_right_move ? target_index + 1 : target_index; |
| DCHECK_LT(new_index_to_right, pinned_action_ids_.size()); |
| |
| target_position = |
| std::find(stored_pinned_actions.begin(), stored_pinned_actions.end(), |
| pinned_action_ids_[new_index_to_right]); |
| DCHECK(target_position != stored_pinned_actions.end()); |
| } |
| |
| auto current_position_in_prefs = std::find( |
| stored_pinned_actions.begin(), stored_pinned_actions.end(), action_id); |
| DCHECK(current_position_in_prefs != stored_pinned_actions.end()); |
| |
| // Rotate |action_id| to be in the target position. |
| if (is_left_to_right_move) { |
| std::rotate(current_position_in_prefs, std::next(current_position_in_prefs), |
| target_position); |
| } else { |
| std::rotate(target_position, current_position_in_prefs, |
| std::next(current_position_in_prefs)); |
| } |
| |
| extension_prefs_->SetPinnedExtensions(stored_pinned_actions); |
| // The |pinned_action_ids_| should be updated as a result of updating the |
| // preference. |
| DCHECK(pinned_action_ids_ == GetFilteredPinnedActionIds()); |
| } |
| |
| // Combine the currently enabled extensions that have browser actions (which |
| // we get from the ExtensionRegistry) with the ordering we get from the pref |
| // service. For robustness we use a somewhat inefficient process: |
| // 1. Create a vector of actions sorted by their pref values. This vector may |
| // have holes. |
| // 2. Create a vector of actions that did not have a pref value. |
| // 3. Remove holes from the sorted vector and append the unsorted vector. |
| void ToolbarActionsModel::InitializeActionList() { |
| CHECK(action_ids_.empty()); // We shouldn't have any actions yet. |
| |
| if (profile_->IsOffTheRecord()) |
| IncognitoPopulate(); |
| else |
| Populate(); |
| |
| // Set |pinned_action_ids_| directly to avoid notifying observers that they |
| // have changed even though they haven't. |
| pinned_action_ids_ = GetFilteredPinnedActionIds(); |
| |
| if (!profile_->IsOffTheRecord() && !action_ids_.empty()) { |
| base::UmaHistogramCounts100("Extensions.Toolbar.PinnedExtensionCount2", |
| pinned_action_ids_.size()); |
| double percentage_double = |
| double{pinned_action_ids_.size()} / double{action_ids_.size()} * 100.0; |
| int percentage = int{percentage_double}; |
| base::UmaHistogramPercentageObsoleteDoNotUse( |
| "Extensions.Toolbar.PinnedExtensionPercentage3", percentage); |
| } |
| } |
| |
| void ToolbarActionsModel::Populate() { |
| DCHECK(!profile_->IsOffTheRecord()); |
| |
| // Add the extension action ids to all_actions. |
| const extensions::ExtensionSet& extensions = |
| extension_registry_->enabled_extensions(); |
| for (const scoped_refptr<const extensions::Extension>& extension : |
| extensions) { |
| if (!ShouldAddExtension(extension.get())) |
| continue; |
| action_ids_.insert(extension->id()); |
| } |
| |
| // Histogram names are prefixed with "ExtensionToolbarModel" rather than |
| // "ToolbarActionsModel" for historical reasons. |
| UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount", |
| action_ids_.size()); |
| |
| if (!action_ids_.empty()) { |
| // If all actions are pinned, report kSampleType_MAX. |
| UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsVisible", |
| pinned_action_ids_.size() == action_ids_.size() |
| ? base::HistogramBase::kSampleType_MAX |
| : pinned_action_ids_.size()); |
| } |
| } |
| |
| bool ToolbarActionsModel::HasAction(const ActionId& action_id) const { |
| return base::Contains(action_ids_, action_id); |
| } |
| |
| void ToolbarActionsModel::IncognitoPopulate() { |
| DCHECK(profile_->IsOffTheRecord()); |
| const ToolbarActionsModel* original_model = |
| ToolbarActionsModel::Get(profile_->GetOriginalProfile()); |
| |
| // Only extensions enabled in incognito mode are added to the incognito mode |
| // toolbar. |
| base::flat_set<ActionId> incognito_ids = original_model->action_ids_; |
| base::EraseIf(incognito_ids, [this](const ActionId& id) { |
| return !ShouldAddExtension(GetExtensionById(id)); |
| }); |
| action_ids_ = std::move(incognito_ids); |
| } |
| |
| void ToolbarActionsModel::SetActionVisibility(const ActionId& action_id, |
| bool is_now_visible) { |
| DCHECK_NE(is_now_visible, IsActionPinned(action_id)); |
| DCHECK(!IsActionForcePinned(action_id)); |
| DCHECK(!profile_->IsOffTheRecord()) |
| << "Changing action pin state is disallowed in incognito."; |
| |
| auto stored_pinned_action_ids = extension_prefs_->GetPinnedExtensions(); |
| DCHECK_NE(is_now_visible, |
| base::Contains(stored_pinned_action_ids, action_id)); |
| if (is_now_visible) { |
| stored_pinned_action_ids.push_back(action_id); |
| } else { |
| base::Erase(stored_pinned_action_ids, action_id); |
| } |
| extension_prefs_->SetPinnedExtensions(stored_pinned_action_ids); |
| // The |pinned_action_ids_| should be updated as a result of updating the |
| // preference. |
| DCHECK(pinned_action_ids_ == GetFilteredPinnedActionIds()); |
| } |
| |
| const extensions::Extension* ToolbarActionsModel::GetExtensionById( |
| const ActionId& action_id) const { |
| return extension_registry_->enabled_extensions().GetByID(action_id); |
| } |
| |
| void ToolbarActionsModel::UpdatePinnedActionIds() { |
| // If extensions are not ready, defer to later Populate() call. |
| if (!actions_initialized_) |
| return; |
| |
| std::vector<ActionId> pinned_extensions = GetFilteredPinnedActionIds(); |
| if (pinned_extensions == pinned_action_ids_) |
| return; |
| |
| pinned_action_ids_ = pinned_extensions; |
| for (Observer& observer : observers_) |
| observer.OnToolbarPinnedActionsChanged(); |
| } |
| |
| std::vector<ToolbarActionsModel::ActionId> |
| ToolbarActionsModel::GetFilteredPinnedActionIds() const { |
| // Force-pinned extensions should always be present in the output vector. |
| extensions::ExtensionIdList pinned = extension_prefs_->GetPinnedExtensions(); |
| auto* management = |
| extensions::ExtensionManagementFactory::GetForBrowserContext(profile_); |
| // O(n^2), but there are typically very few force-pinned extensions. |
| base::ranges::copy_if( |
| management->GetForcePinnedList(), std::back_inserter(pinned), |
| [&pinned](const std::string& id) { return !base::Contains(pinned, id); }); |
| |
| // TODO(pbos): Make sure that the pinned IDs are pruned from ExtensionPrefs on |
| // startup so that we don't keep saving stale IDs. |
| std::vector<ActionId> filtered_action_ids; |
| for (auto& action_id : pinned) { |
| if (HasAction(action_id)) |
| filtered_action_ids.push_back(action_id); |
| } |
| return filtered_action_ids; |
| } |