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