1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
6 
7 #include <algorithm>
8 #include <memory>
9 #include <string>
10 
11 #include "base/bind.h"
12 #include "base/location.h"
13 #include "base/metrics/histogram_base.h"
14 #include "base/metrics/histogram_functions.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/one_shot_event.h"
17 #include "base/ranges/algorithm.h"
18 #include "base/single_thread_task_runner.h"
19 #include "base/stl_util.h"
20 #include "base/threading/thread_task_runner_handle.h"
21 #include "chrome/browser/chrome_notification_types.h"
22 #include "chrome/browser/extensions/extension_management.h"
23 #include "chrome/browser/extensions/extension_message_bubble_controller.h"
24 #include "chrome/browser/extensions/extension_tab_util.h"
25 #include "chrome/browser/extensions/tab_helper.h"
26 #include "chrome/browser/profiles/profile.h"
27 #include "chrome/browser/ui/browser.h"
28 #include "chrome/browser/ui/extensions/extension_action_view_controller.h"
29 #include "chrome/browser/ui/extensions/extension_message_bubble_factory.h"
30 #include "chrome/browser/ui/tabs/tab_strip_model.h"
31 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
32 #include "chrome/browser/ui/toolbar/toolbar_actions_model_factory.h"
33 #include "chrome/browser/ui/ui_features.h"
34 #include "components/prefs/pref_service.h"
35 #include "content/public/browser/notification_details.h"
36 #include "content/public/browser/notification_source.h"
37 #include "content/public/browser/web_contents.h"
38 #include "extensions/browser/extension_action_manager.h"
39 #include "extensions/browser/extension_system.h"
40 #include "extensions/browser/extension_util.h"
41 #include "extensions/browser/pref_names.h"
42 #include "extensions/browser/unloaded_extension_reason.h"
43 #include "extensions/common/extension_set.h"
44 #include "extensions/common/manifest_constants.h"
45 
ToolbarActionsModel(Profile * profile,extensions::ExtensionPrefs * extension_prefs)46 ToolbarActionsModel::ToolbarActionsModel(
47     Profile* profile,
48     extensions::ExtensionPrefs* extension_prefs)
49     : profile_(profile),
50       extension_prefs_(extension_prefs),
51       prefs_(profile_->GetPrefs()),
52       extension_action_api_(extensions::ExtensionActionAPI::Get(profile_)),
53       extension_registry_(extensions::ExtensionRegistry::Get(profile_)),
54       extension_action_manager_(
55           extensions::ExtensionActionManager::Get(profile_)),
56       actions_initialized_(false),
57       highlight_type_(HIGHLIGHT_NONE),
58       has_active_bubble_(false) {
59   extensions::ExtensionSystem::Get(profile_)->ready().Post(
60       FROM_HERE, base::BindOnce(&ToolbarActionsModel::OnReady,
61                                 weak_ptr_factory_.GetWeakPtr()));
62   visible_icon_count_ =
63       prefs_->GetInteger(extensions::pref_names::kToolbarSize);
64 
65   // We only care about watching toolbar-order prefs if not in incognito mode.
66   const bool watch_toolbar_order = !profile_->IsOffTheRecord();
67   const bool watch_pinned_extensions =
68       base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu);
69   if (watch_toolbar_order || watch_pinned_extensions) {
70     pref_change_registrar_.Init(prefs_);
71     pref_change_callback_ =
72         base::Bind(&ToolbarActionsModel::OnActionToolbarPrefChange,
73                    base::Unretained(this));
74 
75     if (watch_toolbar_order) {
76       pref_change_registrar_.Add(extensions::pref_names::kToolbar,
77                                  pref_change_callback_);
78     }
79 
80     if (watch_pinned_extensions) {
81       pref_change_registrar_.Add(extensions::pref_names::kPinnedExtensions,
82                                  pref_change_callback_);
83     }
84   }
85 }
86 
~ToolbarActionsModel()87 ToolbarActionsModel::~ToolbarActionsModel() {}
88 
89 // static
Get(Profile * profile)90 ToolbarActionsModel* ToolbarActionsModel::Get(Profile* profile) {
91   return ToolbarActionsModelFactory::GetForProfile(profile);
92 }
93 
AddObserver(Observer * observer)94 void ToolbarActionsModel::AddObserver(Observer* observer) {
95   observers_.AddObserver(observer);
96 }
97 
RemoveObserver(Observer * observer)98 void ToolbarActionsModel::RemoveObserver(Observer* observer) {
99   observers_.RemoveObserver(observer);
100 }
101 
MoveActionIcon(const ActionId & id,size_t index)102 void ToolbarActionsModel::MoveActionIcon(const ActionId& id, size_t index) {
103   auto pos = action_ids_.begin();
104   while (pos != action_ids_.end() && *pos != id)
105     ++pos;
106   if (pos == action_ids_.end()) {
107     NOTREACHED();
108     return;
109   }
110 
111   ActionId action = *pos;
112   action_ids_.erase(pos);
113 
114   auto pos_id =
115       std::find(last_known_positions_.begin(), last_known_positions_.end(), id);
116   if (pos_id != last_known_positions_.end())
117     last_known_positions_.erase(pos_id);
118 
119   if (index < action_ids_.size()) {
120     // If the index is not at the end, find the action currently at |index|, and
121     // insert |action| before it in |action_ids_| and |action|'s id in
122     // |last_known_positions_|.
123     auto iter = action_ids_.begin() + index;
124     last_known_positions_.insert(std::find(last_known_positions_.begin(),
125                                            last_known_positions_.end(), *iter),
126                                  id);
127     action_ids_.insert(iter, action);
128   } else {
129     // Otherwise, put |action| and |id| at the end.
130     DCHECK_EQ(action_ids_.size(), index);
131     action_ids_.push_back(action);
132     last_known_positions_.push_back(id);
133   }
134 
135   for (Observer& observer : observers_)
136     observer.OnToolbarActionMoved(id, index);
137   UpdatePrefs();
138 }
139 
SetVisibleIconCount(size_t count)140 void ToolbarActionsModel::SetVisibleIconCount(size_t count) {
141   visible_icon_count_ = (count >= action_ids_.size()) ? -1 : count;
142 
143   // Only set the prefs if we're not in highlight mode and the profile is not
144   // incognito. Highlight mode is designed to be a transitory state, and should
145   // not persist across browser restarts (though it may be re-entered), and we
146   // don't store anything in incognito.
147   if (!is_highlighting() && !profile_->IsOffTheRecord()) {
148     prefs_->SetInteger(extensions::pref_names::kToolbarSize,
149                        visible_icon_count_);
150   }
151 
152   for (Observer& observer : observers_)
153     observer.OnToolbarVisibleCountChanged();
154 }
155 
OnExtensionActionUpdated(extensions::ExtensionAction * extension_action,content::WebContents * web_contents,content::BrowserContext * browser_context)156 void ToolbarActionsModel::OnExtensionActionUpdated(
157     extensions::ExtensionAction* extension_action,
158     content::WebContents* web_contents,
159     content::BrowserContext* browser_context) {
160   // Notify observers if the extension exists and is in the model.
161   if (HasAction(extension_action->extension_id())) {
162     for (Observer& observer : observers_)
163       observer.OnToolbarActionUpdated(extension_action->extension_id());
164   }
165 }
166 
167 std::vector<std::unique_ptr<ToolbarActionViewController>>
CreateActions(Browser * browser,ExtensionsContainer * main_bar,bool in_overflow_mode)168 ToolbarActionsModel::CreateActions(Browser* browser,
169                                    ExtensionsContainer* main_bar,
170                                    bool in_overflow_mode) {
171   DCHECK(browser);
172   DCHECK(main_bar);
173   std::vector<std::unique_ptr<ToolbarActionViewController>> action_list;
174 
175   // action_ids() might not equate to |action_ids_| in the case where a
176   // subset is highlighted.
177   for (const ActionId& action_id : action_ids()) {
178     action_list.push_back(
179         CreateActionForId(browser, main_bar, in_overflow_mode, action_id));
180   }
181 
182   return action_list;
183 }
184 
185 std::unique_ptr<ToolbarActionViewController>
CreateActionForId(Browser * browser,ExtensionsContainer * main_bar,bool in_overflow_mode,const ActionId & action_id)186 ToolbarActionsModel::CreateActionForId(Browser* browser,
187                                        ExtensionsContainer* main_bar,
188                                        bool in_overflow_mode,
189                                        const ActionId& action_id) {
190   // We should never have uninitialized actions in action_ids().
191   DCHECK(!action_id.empty());
192   // Get the extension.
193   const extensions::Extension* extension = GetExtensionById(action_id);
194   DCHECK(extension);
195 
196   // Create and add an ExtensionActionViewController for the extension.
197   return std::make_unique<ExtensionActionViewController>(
198       extension, browser,
199       extension_action_manager_->GetExtensionAction(*extension), main_bar,
200       in_overflow_mode);
201 }
202 
OnExtensionLoaded(content::BrowserContext * browser_context,const extensions::Extension * extension)203 void ToolbarActionsModel::OnExtensionLoaded(
204     content::BrowserContext* browser_context,
205     const extensions::Extension* extension) {
206   // We don't want to add the same extension twice. It may have already been
207   // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user
208   // hides the browser action and then disables and enables the extension.
209   if (!HasAction(extension->id()))
210     AddExtension(extension);
211 }
212 
OnExtensionUnloaded(content::BrowserContext * browser_context,const extensions::Extension * extension,extensions::UnloadedExtensionReason reason)213 void ToolbarActionsModel::OnExtensionUnloaded(
214     content::BrowserContext* browser_context,
215     const extensions::Extension* extension,
216     extensions::UnloadedExtensionReason reason) {
217   bool was_visible_and_has_overflow =
218       IsActionVisible(extension->id()) && !all_icons_visible();
219   RemoveExtension(extension);
220   // If the extension was previously visible and there are overflowed
221   // extensions, and this extension is being uninstalled, we reduce the visible
222   // count so that we don't pop out a previously-hidden extension.
223   if (was_visible_and_has_overflow &&
224       reason == extensions::UnloadedExtensionReason::UNINSTALL)
225     SetVisibleIconCount(visible_icon_count() - 1);
226 }
227 
OnExtensionUninstalled(content::BrowserContext * browser_context,const extensions::Extension * extension,extensions::UninstallReason reason)228 void ToolbarActionsModel::OnExtensionUninstalled(
229     content::BrowserContext* browser_context,
230     const extensions::Extension* extension,
231     extensions::UninstallReason reason) {
232   // Remove the extension id from the ordered list, if it exists (the extension
233   // might not be represented in the list because it might not have an icon).
234   RemovePref(extension->id());
235 }
236 
OnLoadFailure(content::BrowserContext * browser_context,const base::FilePath & extension_path,const std::string & error)237 void ToolbarActionsModel::OnLoadFailure(
238     content::BrowserContext* browser_context,
239     const base::FilePath& extension_path,
240     const std::string& error) {
241   for (ToolbarActionsModel::Observer& observer : observers_) {
242     observer.OnToolbarActionLoadFailed();
243   }
244 }
245 
OnExtensionManagementSettingsChanged()246 void ToolbarActionsModel::OnExtensionManagementSettingsChanged() {
247   OnActionToolbarPrefChange();
248 }
249 
RemovePref(const ActionId & action_id)250 void ToolbarActionsModel::RemovePref(const ActionId& action_id) {
251   auto pos = std::find(last_known_positions_.begin(),
252                        last_known_positions_.end(), action_id);
253 
254   if (pos != last_known_positions_.end()) {
255     last_known_positions_.erase(pos);
256     UpdatePrefs();
257   }
258 
259   if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu)) {
260     // The extension is already unloaded at this point, and so shouldn't be in
261     // the active pinned set.
262     DCHECK(!IsActionPinned(action_id));
263     auto stored_pinned_actions = extension_prefs_->GetPinnedExtensions();
264     auto iter = std::find(stored_pinned_actions.begin(),
265                           stored_pinned_actions.end(), action_id);
266     if (iter != stored_pinned_actions.end()) {
267       stored_pinned_actions.erase(iter);
268       extension_prefs_->SetPinnedExtensions(stored_pinned_actions);
269     }
270   }
271 }
272 
OnReady()273 void ToolbarActionsModel::OnReady() {
274   InitializeActionList();
275 
276   load_error_reporter_observer_.Add(
277       extensions::LoadErrorReporter::GetInstance());
278 
279   // Wait until the extension system is ready before observing any further
280   // changes so that the toolbar buttons can be shown in their stable ordering
281   // taken from prefs.
282   extension_registry_observer_.Add(extension_registry_);
283   extension_action_observer_.Add(extension_action_api_);
284 
285   auto* management =
286       extensions::ExtensionManagementFactory::GetForBrowserContext(profile_);
287   extension_management_observer_.Add(management);
288 
289   actions_initialized_ = true;
290   for (Observer& observer : observers_)
291     observer.OnToolbarModelInitialized();
292 }
293 
FindNewPositionFromLastKnownGood(const ActionId & action)294 size_t ToolbarActionsModel::FindNewPositionFromLastKnownGood(
295     const ActionId& action) {
296   // See if we have last known good position for this action.
297   size_t new_index = 0;
298   // Loop through the ID list of known positions, to count the number of
299   // visible action icons preceding |action|'s id.
300   for (const ActionId& last_pos_id : last_known_positions_) {
301     if (last_pos_id == action)
302       return new_index;  // We've found the right position.
303     // Found an action, need to see if it is visible.
304     for (const ActionId& action_id : action_ids_) {
305       if (action_id == last_pos_id) {
306         // This extension is visible, update the index value.
307         ++new_index;
308         break;
309       }
310     }
311   }
312 
313   // Position not found.
314   return action_ids_.size();
315 }
316 
ShouldAddExtension(const extensions::Extension * extension)317 bool ToolbarActionsModel::ShouldAddExtension(
318     const extensions::Extension* extension) {
319   // In incognito mode, don't add any extensions that aren't incognito-enabled.
320   if (profile_->IsOffTheRecord() &&
321       !extensions::util::IsIncognitoEnabled(extension->id(), profile_))
322     return false;
323 
324   // In this case, we don't care about the browser action visibility, because
325   // we want to show each extension regardless.
326   return extension_action_manager_->GetExtensionAction(*extension) != nullptr;
327 }
328 
AddExtension(const extensions::Extension * extension)329 void ToolbarActionsModel::AddExtension(const extensions::Extension* extension) {
330   if (!ShouldAddExtension(extension))
331     return;
332 
333   AddAction(extension->id());
334 }
335 
AddAction(const ActionId & action_id)336 void ToolbarActionsModel::AddAction(const ActionId& action_id) {
337   // We only use AddAction() once the system is initialized.
338   CHECK(actions_initialized_);
339 
340   // See if we have a last known good position for this extension.
341   bool is_new_extension = !base::Contains(last_known_positions_, action_id);
342 
343   // New extensions go at the right (end) of the visible extensions. Other
344   // extensions go at their previous position.
345   size_t new_index = 0;
346   if (is_new_extension) {
347     new_index = visible_icon_count();
348     // For the last-known position, we use the index of the extension that is
349     // just before this extension, plus one. (Note that this isn't the same
350     // as new_index + 1, because last_known_positions_ can include disabled
351     // extensions.)
352     int new_last_known_index = new_index == 0
353                                    ? 0
354                                    : std::find(last_known_positions_.begin(),
355                                                last_known_positions_.end(),
356                                                action_ids_[new_index - 1]) -
357                                          last_known_positions_.begin() + 1;
358     // In theory, the extension before this one should always
359     // be in last known positions, but if something funny happened with prefs,
360     // make sure we handle it.
361     // TODO(devlin): Track down these cases so we can CHECK this.
362     new_last_known_index =
363         std::min<int>(new_last_known_index, last_known_positions_.size());
364     last_known_positions_.insert(
365         last_known_positions_.begin() + new_last_known_index, action_id);
366     UpdatePrefs();
367   } else {
368     new_index = FindNewPositionFromLastKnownGood(action_id);
369   }
370 
371   action_ids_.insert(action_ids_.begin() + new_index, action_id);
372 
373   // If we're currently highlighting, then even though we add a browser action
374   // to the full list (|action_ids_|, there won't be another *visible*
375   // browser action, which was what the observers care about.
376   if (!is_highlighting()) {
377     for (Observer& observer : observers_)
378       observer.OnToolbarActionAdded(action_id, new_index);
379 
380     int visible_count_delta = 0;
381     if (is_new_extension && !all_icons_visible()) {
382       // If this is a new extension (and not all extensions are visible), we
383       // expand the toolbar out so that the new one can be seen.
384       visible_count_delta = 1;
385     } else if (profile_->IsOffTheRecord()) {
386       // If this is an incognito profile, we also have to check to make sure the
387       // overflow matches the main bar's status.
388       ToolbarActionsModel* main_model =
389           ToolbarActionsModel::Get(profile_->GetOriginalProfile());
390       // Find what the index will be in the main bar. Because Observer calls are
391       // nondeterministic, we can't just assume the main bar will have the
392       // extension and look it up.
393       size_t main_index =
394           main_model->FindNewPositionFromLastKnownGood(action_id);
395       bool visible =
396           is_new_extension || main_index < main_model->visible_icon_count();
397       // We may need to adjust the visible count if the incognito bar isn't
398       // showing all icons and this one is visible, or if it is showing all
399       // icons and this is hidden.
400       if (visible && !all_icons_visible())
401         visible_count_delta = 1;
402       else if (!visible && all_icons_visible())
403         visible_count_delta = -1;
404     }
405 
406     if (visible_count_delta)
407       SetVisibleIconCount(visible_icon_count() + visible_count_delta);
408   }
409 
410   UpdatePinnedActionIds();
411 }
412 
RemoveAction(const ActionId & action_id)413 void ToolbarActionsModel::RemoveAction(const ActionId& action_id) {
414   auto pos = std::find(action_ids_.begin(), action_ids_.end(), action_id);
415 
416   if (pos == action_ids_.end())
417     return;
418 
419   // If our visible count is set to the current size, we need to decrement it.
420   if (visible_icon_count_ == static_cast<int>(action_ids_.size()))
421     SetVisibleIconCount(action_ids_.size() - 1);
422 
423   action_ids_.erase(pos);
424 
425   UpdatePinnedActionIds();
426 
427   // If we're in highlight mode, we also have to remove the action from
428   // the highlighted list.
429   if (is_highlighting()) {
430     pos = std::find(highlighted_action_ids_.begin(),
431                     highlighted_action_ids_.end(), action_id);
432     if (pos != highlighted_action_ids_.end()) {
433       highlighted_action_ids_.erase(pos);
434       for (Observer& observer : observers_)
435         observer.OnToolbarActionRemoved(action_id);
436       // If the highlighted list is now empty, we stop highlighting.
437       if (highlighted_action_ids_.empty())
438         StopHighlighting();
439     }
440   } else {
441     for (Observer& observer : observers_)
442       observer.OnToolbarActionRemoved(action_id);
443   }
444 
445   UpdatePrefs();
446 }
447 
448 std::unique_ptr<extensions::ExtensionMessageBubbleController>
GetExtensionMessageBubbleController(Browser * browser)449 ToolbarActionsModel::GetExtensionMessageBubbleController(Browser* browser) {
450   std::unique_ptr<extensions::ExtensionMessageBubbleController> controller;
451   if (has_active_bubble())
452     return controller;
453   controller = ExtensionMessageBubbleFactory(browser).GetController();
454   if (controller)
455     controller->SetIsActiveBubble();
456   return controller;
457 }
458 
IsActionPinned(const ActionId & action_id) const459 bool ToolbarActionsModel::IsActionPinned(const ActionId& action_id) const {
460   DCHECK(base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu));
461   return base::Contains(pinned_action_ids_, action_id);
462 }
463 
IsActionForcePinned(const ActionId & action_id) const464 bool ToolbarActionsModel::IsActionForcePinned(const ActionId& action_id) const {
465   DCHECK(base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu));
466   auto* management =
467       extensions::ExtensionManagementFactory::GetForBrowserContext(profile_);
468   return base::Contains(management->GetForcePinnedList(), action_id);
469 }
470 
MovePinnedAction(const ActionId & action_id,size_t target_index)471 void ToolbarActionsModel::MovePinnedAction(const ActionId& action_id,
472                                            size_t target_index) {
473   DCHECK(base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu));
474 
475   auto new_pinned_action_ids = pinned_action_ids_;
476 
477   auto current_position = std::find(new_pinned_action_ids.begin(),
478                                     new_pinned_action_ids.end(), action_id);
479   DCHECK(current_position != new_pinned_action_ids.end());
480 
481   const bool move_to_end = size_t{target_index} >= new_pinned_action_ids.size();
482   auto target_position =
483       move_to_end ? std::prev(new_pinned_action_ids.end())
484                   : std::next(new_pinned_action_ids.begin(), target_index);
485 
486   // Rotate |action_id| to be in the target position.
487   if (target_position < current_position) {
488     std::rotate(target_position, current_position, std::next(current_position));
489   } else {
490     std::rotate(current_position, std::next(current_position),
491                 std::next(target_position));
492   }
493 
494   extension_prefs_->SetPinnedExtensions(new_pinned_action_ids);
495   // The |pinned_action_ids_| should be updated as a result of updating the
496   // preference.
497   DCHECK(pinned_action_ids_ == new_pinned_action_ids);
498 }
499 
RemoveExtension(const extensions::Extension * extension)500 void ToolbarActionsModel::RemoveExtension(
501     const extensions::Extension* extension) {
502   RemoveAction(extension->id());
503 }
504 
505 // Combine the currently enabled extensions that have browser actions (which
506 // we get from the ExtensionRegistry) with the ordering we get from the pref
507 // service. For robustness we use a somewhat inefficient process:
508 // 1. Create a vector of actions sorted by their pref values. This vector may
509 // have holes.
510 // 2. Create a vector of actions that did not have a pref value.
511 // 3. Remove holes from the sorted vector and append the unsorted vector.
InitializeActionList()512 void ToolbarActionsModel::InitializeActionList() {
513   CHECK(action_ids_.empty());  // We shouldn't have any actions yet.
514 
515   last_known_positions_ = extension_prefs_->GetToolbarOrder();
516 
517   if (profile_->IsOffTheRecord())
518     IncognitoPopulate();
519   else
520     Populate();
521 
522   if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu)) {
523     if (!extension_prefs_->IsPinnedExtensionsMigrationComplete() &&
524         !profile_->IsOffTheRecord()) {
525       // Migrate extensions visible in the toolbar to pinned extensions.
526       auto new_pinned_action_ids = std::vector<ActionId>(
527           action_ids_.begin(), action_ids_.begin() + visible_icon_count());
528       extension_prefs_->SetPinnedExtensions(new_pinned_action_ids);
529       extension_prefs_->MarkPinnedExtensionsMigrationComplete();
530     }
531     // Set |pinned_action_ids_| directly to avoid notifying observers that they
532     // have changed even though they haven't.
533     pinned_action_ids_ = GetFilteredPinnedActionIds();
534 
535     if (!profile_->IsOffTheRecord() && !action_ids_.empty()) {
536       base::UmaHistogramCounts100("Extensions.Toolbar.PinnedExtensionCount2",
537                                   pinned_action_ids_.size());
538       double percentage_double = double{pinned_action_ids_.size()} /
539                                  double{action_ids_.size()} * 100.0;
540       int percentage = int{percentage_double};
541       base::UmaHistogramPercentageObsoleteDoNotUse(
542           "Extensions.Toolbar.PinnedExtensionPercentage3", percentage);
543     }
544   }
545 }
546 
Populate()547 void ToolbarActionsModel::Populate() {
548   DCHECK(!profile_->IsOffTheRecord());
549 
550   std::vector<ActionId> all_actions;
551   // Ids of actions that have explicit positions.
552   std::vector<ActionId> sorted(last_known_positions_.size(), ActionId());
553   // Ids of actions that don't have explicit positions.
554   std::vector<ActionId> unsorted;
555 
556   // Populate the lists.
557 
558   // Add the extension action ids to all_actions.
559   const extensions::ExtensionSet& extensions =
560       extension_registry_->enabled_extensions();
561   for (const scoped_refptr<const extensions::Extension>& extension :
562        extensions) {
563     if (!ShouldAddExtension(extension.get()))
564       continue;
565 
566     all_actions.push_back(extension->id());
567   }
568 
569   // Add each action id to the appropriate list. Since the |sorted| list is
570   // created with enough room for each id in |positions| (which helps with
571   // proper order insertion), holes can be present if there isn't an action
572   // for each id. This is handled below when we add the actions to
573   // |action_ids_| to ensure that there are never any holes in
574   // |action_ids_| itself (or, relatedly, CreateActions()).
575   for (const ActionId& action : all_actions) {
576     std::vector<ActionId>::const_iterator pos = std::find(
577         last_known_positions_.begin(), last_known_positions_.end(), action);
578     if (pos != last_known_positions_.end()) {
579       sorted[pos - last_known_positions_.begin()] = action;
580     } else {
581       // Unknown action - push it to the back of unsorted, and add it to the
582       // list of ids at the end.
583       unsorted.push_back(action);
584       last_known_positions_.push_back(action);
585     }
586   }
587 
588   // Merge the lists.
589   sorted.insert(sorted.end(), unsorted.begin(), unsorted.end());
590   action_ids_.reserve(sorted.size());
591 
592   // We don't notify observers of the added extension yet. Rather, observers
593   // should wait for the "OnToolbarModelInitialized" notification, and then
594   // bulk-update. (This saves a lot of bouncing-back-and-forth here, and allows
595   // observers to ensure that the extension system is always initialized before
596   // using the extensions).
597   for (const ActionId& action : sorted) {
598     // Since |sorted| can have holes in it, they will be empty ActionIds.
599     // Ignore them.
600     if (action.empty())
601       continue;
602 
603     // It's possible for the extension order to contain actions that aren't
604     // actually loaded on this machine.  For example, when extension sync is
605     // on, we sync the extension order as-is but double-check with the user
606     // before syncing NPAPI-containing extensions, so if one of those is not
607     // actually synced, we'll get a NULL in the list.  This sort of case can
608     // also happen if some error prevents an extension from loading.
609     if (!GetExtensionById(action))
610       continue;
611 
612     action_ids_.push_back(action);
613   }
614 
615   // Histogram names are prefixed with "ExtensionToolbarModel" rather than
616   // "ToolbarActionsModel" for historical reasons.
617   UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount",
618                            action_ids_.size());
619 
620   if (!action_ids_.empty()) {
621     // Visible count can be -1, meaning: 'show all'. Since UMA converts negative
622     // values to 0, this would be counted as 'show none' unless we convert it to
623     // max.
624     UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsVisible",
625                              visible_icon_count_ == -1
626                                  ? base::HistogramBase::kSampleType_MAX
627                                  : visible_icon_count_);
628   }
629 }
630 
HasAction(const ActionId & action_id) const631 bool ToolbarActionsModel::HasAction(const ActionId& action_id) const {
632   return base::Contains(action_ids_, action_id);
633 }
634 
IncognitoPopulate()635 void ToolbarActionsModel::IncognitoPopulate() {
636   DCHECK(profile_->IsOffTheRecord());
637   const ToolbarActionsModel* original_model =
638       ToolbarActionsModel::Get(profile_->GetOriginalProfile());
639 
640   // Find the absolute value of the original model's count.
641   int original_visible = original_model->visible_icon_count();
642 
643   // In incognito mode, we show only those actions that are incognito-enabled
644   // Further, any actions that were overflowed in regular mode are still
645   // overflowed. Order is the same as in regular mode.
646   visible_icon_count_ = 0;
647 
648   for (auto iter = original_model->action_ids_.begin();
649        iter != original_model->action_ids_.end(); ++iter) {
650     // We should never have an uninitialized action in the model.
651     DCHECK(!iter->empty());
652     // The extension might not be shown in incognito mode.
653     if (!ShouldAddExtension(GetExtensionById(*iter)))
654       continue;
655     action_ids_.push_back(*iter);
656     if (iter - original_model->action_ids_.begin() < original_visible)
657       ++visible_icon_count_;
658   }
659 }
660 
UpdatePrefs()661 void ToolbarActionsModel::UpdatePrefs() {
662   if (!extension_prefs_ || profile_->IsOffTheRecord())
663     return;
664 
665   // Don't observe change caused by self.
666   pref_change_registrar_.Remove(extensions::pref_names::kToolbar);
667   extension_prefs_->SetToolbarOrder(last_known_positions_);
668   pref_change_registrar_.Add(extensions::pref_names::kToolbar,
669                              pref_change_callback_);
670 }
671 
SetActionVisibility(const ActionId & action_id,bool is_now_visible)672 void ToolbarActionsModel::SetActionVisibility(const ActionId& action_id,
673                                               bool is_now_visible) {
674   if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu)) {
675     DCHECK_NE(is_now_visible, IsActionPinned(action_id));
676     DCHECK(!IsActionForcePinned(action_id));
677     auto new_pinned_action_ids = pinned_action_ids_;
678     if (is_now_visible) {
679       new_pinned_action_ids.push_back(action_id);
680     } else {
681       base::Erase(new_pinned_action_ids, action_id);
682     }
683     extension_prefs_->SetPinnedExtensions(new_pinned_action_ids);
684     // The |pinned_action_ids_| should be updated as a result of updating the
685     // preference.
686     DCHECK(pinned_action_ids_ == new_pinned_action_ids);
687     return;
688   }
689 
690   DCHECK(HasAction(action_id));
691 
692   int new_size = 0;
693   int new_index = 0;
694   if (is_now_visible) {
695     // If this action used to be hidden, we can't possibly be showing all.
696     DCHECK_LT(visible_icon_count(), action_ids_.size());
697     // Grow the bar by one and move the action to the end of the visibles.
698     new_size = visible_icon_count() + 1;
699     new_index = new_size - 1;
700   } else {
701     // If we're hiding one, we must be showing at least one.
702     DCHECK_GE(visible_icon_count(), 0u);
703     // Shrink the bar by one and move the action to the beginning of the
704     // overflow menu.
705     new_size = visible_icon_count() - 1;
706     new_index = new_size;
707   }
708   SetVisibleIconCount(new_size);
709   MoveActionIcon(action_id, new_index);
710 }
711 
OnActionToolbarPrefChange()712 void ToolbarActionsModel::OnActionToolbarPrefChange() {
713   // If extensions are not ready, defer to later Populate() call.
714   if (!actions_initialized_)
715     return;
716 
717   UpdatePinnedActionIds();
718 
719   // Recalculate |last_known_positions_| to be |pref_positions| followed by
720   // ones that are only in |last_known_positions_|.
721   std::vector<ActionId> pref_positions = extension_prefs_->GetToolbarOrder();
722   size_t pref_position_size = pref_positions.size();
723   for (size_t i = 0; i < last_known_positions_.size(); ++i) {
724     if (!base::Contains(pref_positions, last_known_positions_[i])) {
725       pref_positions.push_back(last_known_positions_[i]);
726     }
727   }
728   last_known_positions_.swap(pref_positions);
729 
730   // Loop over the updated list of last known positions, moving any extensions
731   // that are in the wrong place.
732   auto desired_pos = action_ids_.begin();
733   for (const ActionId& id : last_known_positions_) {
734     auto current_pos = std::find_if(
735         action_ids_.begin(), action_ids_.end(),
736         [&id](const ActionId& action_id) { return action_id == id; });
737     if (current_pos == action_ids_.end())
738       continue;
739 
740     if (current_pos != desired_pos) {
741       if (current_pos < desired_pos)
742         std::rotate(current_pos, current_pos + 1, desired_pos + 1);
743       else
744         std::rotate(desired_pos, current_pos, current_pos + 1);
745       // Notify the observers to keep them up to date, unless we're highlighting
746       // (in which case we're deliberately only showing a subset of actions).
747       if (!is_highlighting()) {
748         for (Observer& observer : observers_) {
749           observer.OnToolbarActionMoved(id, desired_pos - action_ids_.begin());
750         }
751       }
752     }
753     ++desired_pos;
754   }
755 
756   if (last_known_positions_.size() > pref_position_size) {
757     // Need to update pref because we have extra icons. But can't call
758     // UpdatePrefs() directly within observation closure.
759     base::ThreadTaskRunnerHandle::Get()->PostTask(
760         FROM_HERE, base::BindOnce(&ToolbarActionsModel::UpdatePrefs,
761                                   weak_ptr_factory_.GetWeakPtr()));
762   }
763 }
764 
HighlightActions(const std::vector<ActionId> & ids_to_highlight,HighlightType highlight_type)765 bool ToolbarActionsModel::HighlightActions(
766     const std::vector<ActionId>& ids_to_highlight,
767     HighlightType highlight_type) {
768   highlighted_action_ids_.clear();
769 
770   for (const ActionId& id_to_highlight : ids_to_highlight) {
771     for (const ActionId& action_id : action_ids_) {
772       if (action_id == id_to_highlight)
773         highlighted_action_ids_.push_back(action_id);
774     }
775   }
776 
777   // If we have any actions in |highlighted_action_ids_|, then we entered
778   // highlighting mode.
779   if (!highlighted_action_ids_.empty()) {
780     // It's important that |highlight_type_| is changed immediately before the
781     // observers are notified since it changes the result of action_ids().
782     highlight_type_ = highlight_type;
783     for (Observer& observer : observers_)
784       observer.OnToolbarHighlightModeChanged(true);
785 
786     // We set the visible icon count after the highlight mode change because
787     // the UI actions are created/destroyed during highlight, and doing that
788     // prior to changing the size allows us to still have smooth animations.
789     if (visible_icon_count() < ids_to_highlight.size())
790       SetVisibleIconCount(ids_to_highlight.size());
791 
792     return true;
793   }
794 
795   // Otherwise, we didn't enter highlighting mode (and, in fact, exited it if
796   // we were otherwise in it).
797   if (is_highlighting())
798     StopHighlighting();
799   return false;
800 }
801 
StopHighlighting()802 void ToolbarActionsModel::StopHighlighting() {
803   if (is_highlighting()) {
804     // It's important that |highlight_type_| is changed immediately before the
805     // observers are notified since it changes the result of action_ids().
806     highlight_type_ = HIGHLIGHT_NONE;
807     for (Observer& observer : observers_)
808       observer.OnToolbarHighlightModeChanged(false);
809 
810     // For the same reason, we don't clear |highlighted_action_ids_| until after
811     // the mode changed.
812     highlighted_action_ids_.clear();
813 
814     // We set the visible icon count after the highlight mode change because
815     // the UI actions are created/destroyed during highlight, and doing that
816     // prior to changing the size allows us to still have smooth animations.
817     int saved_icon_count =
818         prefs_->GetInteger(extensions::pref_names::kToolbarSize);
819     if (saved_icon_count != visible_icon_count_)
820       SetVisibleIconCount(saved_icon_count);
821   }
822 }
823 
GetExtensionById(const ActionId & action_id) const824 const extensions::Extension* ToolbarActionsModel::GetExtensionById(
825     const ActionId& action_id) const {
826   return extension_registry_->enabled_extensions().GetByID(action_id);
827 }
828 
IsActionVisible(const ActionId & action_id) const829 bool ToolbarActionsModel::IsActionVisible(const ActionId& action_id) const {
830   size_t index = 0u;
831   while (action_ids().size() > index && action_ids()[index] != action_id)
832     ++index;
833   return index < visible_icon_count();
834 }
835 
UpdatePinnedActionIds()836 void ToolbarActionsModel::UpdatePinnedActionIds() {
837   if (!base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu))
838     return;
839   std::vector<ActionId> pinned_extensions = GetFilteredPinnedActionIds();
840   if (pinned_extensions == pinned_action_ids_)
841     return;
842 
843   pinned_action_ids_ = pinned_extensions;
844   for (Observer& observer : observers_)
845     observer.OnToolbarPinnedActionsChanged();
846 }
847 
848 std::vector<ToolbarActionsModel::ActionId>
GetFilteredPinnedActionIds() const849 ToolbarActionsModel::GetFilteredPinnedActionIds() const {
850   // Force-pinned extensions should always be present in the output vector.
851   extensions::ExtensionIdList pinned = extension_prefs_->GetPinnedExtensions();
852   auto* management =
853       extensions::ExtensionManagementFactory::GetForBrowserContext(profile_);
854   // O(n^2), but there are typically very few force-pinned extensions.
855   base::ranges::copy_if(
856       management->GetForcePinnedList(), std::back_inserter(pinned),
857       [&pinned](const std::string& id) { return !base::Contains(pinned, id); });
858 
859   // TODO(pbos): Make sure that the pinned IDs are pruned from ExtensionPrefs on
860   // startup so that we don't keep saving stale IDs.
861   std::vector<ActionId> filtered_action_ids;
862   for (auto& action_id : pinned) {
863     if (HasAction(action_id))
864       filtered_action_ids.push_back(action_id);
865   }
866   return filtered_action_ids;
867 }
868