extension_toolbar_model.cc revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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/extensions/extension_toolbar_model.h"
6
7#include <string>
8
9#include "base/metrics/histogram.h"
10#include "base/metrics/histogram_base.h"
11#include "base/prefs/pref_service.h"
12#include "chrome/browser/chrome_notification_types.h"
13#include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
14#include "chrome/browser/extensions/extension_action.h"
15#include "chrome/browser/extensions/extension_action_manager.h"
16#include "chrome/browser/extensions/extension_service.h"
17#include "chrome/browser/extensions/extension_tab_util.h"
18#include "chrome/browser/extensions/extension_toolbar_model_factory.h"
19#include "chrome/browser/extensions/extension_util.h"
20#include "chrome/browser/extensions/tab_helper.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/browser/ui/browser.h"
23#include "chrome/browser/ui/tabs/tab_strip_model.h"
24#include "chrome/common/pref_names.h"
25#include "content/public/browser/notification_details.h"
26#include "content/public/browser/notification_source.h"
27#include "content/public/browser/web_contents.h"
28#include "extensions/browser/extension_prefs.h"
29#include "extensions/browser/extension_registry.h"
30#include "extensions/browser/extension_system.h"
31#include "extensions/browser/pref_names.h"
32#include "extensions/common/extension.h"
33#include "extensions/common/extension_set.h"
34#include "extensions/common/feature_switch.h"
35
36namespace extensions {
37
38bool ExtensionToolbarModel::Observer::BrowserActionShowPopup(
39    const extensions::Extension* extension) {
40  return false;
41}
42
43ExtensionToolbarModel::ExtensionToolbarModel(
44    Profile* profile,
45    extensions::ExtensionPrefs* extension_prefs)
46    : profile_(profile),
47      extension_prefs_(extension_prefs),
48      prefs_(profile_->GetPrefs()),
49      extensions_initialized_(false),
50      is_highlighting_(false),
51      extension_registry_observer_(this),
52      weak_ptr_factory_(this) {
53  extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
54
55  registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY,
56                 content::Source<Profile>(profile_));
57  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED,
58                 content::Source<Profile>(profile_));
59  registrar_.Add(
60      this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED,
61      content::Source<extensions::ExtensionPrefs>(extension_prefs_));
62
63  visible_icon_count_ = prefs_->GetInteger(
64      extensions::pref_names::kToolbarSize);
65  pref_change_registrar_.Init(prefs_);
66  pref_change_callback_ =
67      base::Bind(&ExtensionToolbarModel::OnExtensionToolbarPrefChange,
68                 base::Unretained(this));
69  pref_change_registrar_.Add(extensions::pref_names::kToolbar,
70                             pref_change_callback_);
71}
72
73ExtensionToolbarModel::~ExtensionToolbarModel() {
74}
75
76// static
77ExtensionToolbarModel* ExtensionToolbarModel::Get(Profile* profile) {
78  return ExtensionToolbarModelFactory::GetForProfile(profile);
79}
80
81void ExtensionToolbarModel::AddObserver(Observer* observer) {
82  observers_.AddObserver(observer);
83}
84
85void ExtensionToolbarModel::RemoveObserver(Observer* observer) {
86  observers_.RemoveObserver(observer);
87}
88
89void ExtensionToolbarModel::MoveBrowserAction(const Extension* extension,
90                                              int index) {
91  ExtensionList::iterator pos = std::find(toolbar_items_.begin(),
92      toolbar_items_.end(), extension);
93  if (pos == toolbar_items_.end()) {
94    NOTREACHED();
95    return;
96  }
97  toolbar_items_.erase(pos);
98
99  ExtensionIdList::iterator pos_id;
100  pos_id = std::find(last_known_positions_.begin(),
101                     last_known_positions_.end(), extension->id());
102  if (pos_id != last_known_positions_.end())
103    last_known_positions_.erase(pos_id);
104
105  int i = 0;
106  bool inserted = false;
107  for (ExtensionList::iterator iter = toolbar_items_.begin();
108       iter != toolbar_items_.end();
109       ++iter, ++i) {
110    if (i == index) {
111      pos_id = std::find(last_known_positions_.begin(),
112                         last_known_positions_.end(), (*iter)->id());
113      last_known_positions_.insert(pos_id, extension->id());
114
115      toolbar_items_.insert(iter, make_scoped_refptr(extension));
116      inserted = true;
117      break;
118    }
119  }
120
121  if (!inserted) {
122    DCHECK_EQ(index, static_cast<int>(toolbar_items_.size()));
123    index = toolbar_items_.size();
124
125    toolbar_items_.push_back(make_scoped_refptr(extension));
126    last_known_positions_.push_back(extension->id());
127  }
128
129  FOR_EACH_OBSERVER(Observer, observers_, BrowserActionMoved(extension, index));
130
131  UpdatePrefs();
132}
133
134ExtensionToolbarModel::Action ExtensionToolbarModel::ExecuteBrowserAction(
135    const Extension* extension,
136    Browser* browser,
137    GURL* popup_url_out,
138    bool should_grant) {
139  content::WebContents* web_contents = NULL;
140  int tab_id = 0;
141  if (!extensions::ExtensionTabUtil::GetDefaultTab(
142          browser, &web_contents, &tab_id)) {
143    return ACTION_NONE;
144  }
145
146  ExtensionAction* browser_action =
147      extensions::ExtensionActionManager::Get(profile_)->
148      GetBrowserAction(*extension);
149
150  // For browser actions, visibility == enabledness.
151  if (!browser_action->GetIsVisible(tab_id))
152    return ACTION_NONE;
153
154  if (should_grant) {
155    extensions::TabHelper::FromWebContents(web_contents)->
156        active_tab_permission_granter()->GrantIfRequested(extension);
157  }
158
159  if (browser_action->HasPopup(tab_id)) {
160    if (popup_url_out)
161      *popup_url_out = browser_action->GetPopupUrl(tab_id);
162    return ACTION_SHOW_POPUP;
163  }
164
165  extensions::ExtensionActionAPI::BrowserActionExecuted(
166      browser->profile(), *browser_action, web_contents);
167  return ACTION_NONE;
168}
169
170void ExtensionToolbarModel::SetVisibleIconCount(int count) {
171  visible_icon_count_ =
172      count == static_cast<int>(toolbar_items_.size()) ? -1 : count;
173  // Only set the prefs if we're not in highlight mode. Highlight mode is
174  // designed to be a transitory state, and should not persist across browser
175  // restarts (though it may be re-entered).
176  if (!is_highlighting_) {
177    prefs_->SetInteger(extensions::pref_names::kToolbarSize,
178                       visible_icon_count_);
179  }
180}
181
182void ExtensionToolbarModel::OnExtensionLoaded(
183    content::BrowserContext* browser_context,
184    const Extension* extension) {
185  // We don't want to add the same extension twice. It may have already been
186  // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user
187  // hides the browser action and then disables and enables the extension.
188  for (size_t i = 0; i < toolbar_items_.size(); i++) {
189    if (toolbar_items_[i].get() == extension)
190      return;
191  }
192  if (ExtensionActionAPI::GetBrowserActionVisibility(extension_prefs_,
193                                                     extension->id())) {
194    AddExtension(extension);
195  }
196}
197
198void ExtensionToolbarModel::OnExtensionUnloaded(
199    content::BrowserContext* browser_context,
200    const Extension* extension,
201    UnloadedExtensionInfo::Reason reason) {
202  RemoveExtension(extension);
203}
204
205void ExtensionToolbarModel::Observe(
206    int type,
207    const content::NotificationSource& source,
208    const content::NotificationDetails& details) {
209  ExtensionService* extension_service =
210      ExtensionSystem::Get(profile_)->extension_service();
211  DCHECK(extension_service);
212  if (!extension_service->is_ready())
213    return;
214
215  switch (type) {
216    case chrome::NOTIFICATION_EXTENSIONS_READY:
217      InitializeExtensionList(extension_service);
218      break;
219    case chrome::NOTIFICATION_EXTENSION_UNINSTALLED: {
220      const Extension* extension =
221          content::Details<const Extension>(details).ptr();
222      UninstalledExtension(extension);
223      break;
224    }
225    case chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED: {
226      const Extension* extension = extension_service->GetExtensionById(
227          *content::Details<const std::string>(details).ptr(), true);
228      if (ExtensionActionAPI::GetBrowserActionVisibility(extension_prefs_,
229                                                         extension->id())) {
230        AddExtension(extension);
231      } else {
232        RemoveExtension(extension);
233      }
234      break;
235    }
236    default:
237      NOTREACHED() << "Received unexpected notification";
238  }
239}
240
241size_t ExtensionToolbarModel::FindNewPositionFromLastKnownGood(
242    const Extension* extension) {
243  // See if we have last known good position for this extension.
244  size_t new_index = 0;
245  // Loop through the ID list of known positions, to count the number of visible
246  // browser action icons preceding |extension|.
247  for (ExtensionIdList::const_iterator iter_id = last_known_positions_.begin();
248       iter_id < last_known_positions_.end(); ++iter_id) {
249    if ((*iter_id) == extension->id())
250      return new_index;  // We've found the right position.
251    // Found an id, need to see if it is visible.
252    for (ExtensionList::const_iterator iter_ext = toolbar_items_.begin();
253         iter_ext < toolbar_items_.end(); ++iter_ext) {
254      if ((*iter_ext)->id().compare(*iter_id) == 0) {
255        // This extension is visible, update the index value.
256        ++new_index;
257        break;
258      }
259    }
260  }
261
262  return -1;
263}
264
265void ExtensionToolbarModel::AddExtension(const Extension* extension) {
266  // We only care about extensions with browser actions.
267  if (!ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension))
268    return;
269
270  size_t new_index = -1;
271
272  // See if we have a last known good position for this extension.
273  ExtensionIdList::iterator last_pos = std::find(last_known_positions_.begin(),
274                                                 last_known_positions_.end(),
275                                                 extension->id());
276  if (last_pos != last_known_positions_.end()) {
277    new_index = FindNewPositionFromLastKnownGood(extension);
278    if (new_index != toolbar_items_.size()) {
279      toolbar_items_.insert(toolbar_items_.begin() + new_index,
280                            make_scoped_refptr(extension));
281    } else {
282      toolbar_items_.push_back(make_scoped_refptr(extension));
283    }
284  } else {
285    // This is a never before seen extension, that was added to the end. Make
286    // sure to reflect that.
287    toolbar_items_.push_back(make_scoped_refptr(extension));
288    last_known_positions_.push_back(extension->id());
289    new_index = toolbar_items_.size() - 1;
290    UpdatePrefs();
291  }
292
293  // If we're currently highlighting, then even though we add a browser action
294  // to the full list (|toolbar_items_|, there won't be another *visible*
295  // browser action, which was what the observers care about.
296  if (!is_highlighting_) {
297    FOR_EACH_OBSERVER(Observer, observers_,
298                      BrowserActionAdded(extension, new_index));
299  }
300}
301
302void ExtensionToolbarModel::RemoveExtension(const Extension* extension) {
303  ExtensionList::iterator pos =
304      std::find(toolbar_items_.begin(), toolbar_items_.end(), extension);
305  if (pos == toolbar_items_.end())
306    return;
307
308  toolbar_items_.erase(pos);
309
310  // If we're in highlight mode, we also have to remove the extension from
311  // the highlighted list.
312  if (is_highlighting_) {
313    pos = std::find(highlighted_items_.begin(),
314                    highlighted_items_.end(),
315                    extension);
316    if (pos != highlighted_items_.end()) {
317      highlighted_items_.erase(pos);
318      FOR_EACH_OBSERVER(Observer, observers_, BrowserActionRemoved(extension));
319      // If the highlighted list is now empty, we stop highlighting.
320      if (highlighted_items_.empty())
321        StopHighlighting();
322    }
323  } else {
324    FOR_EACH_OBSERVER(Observer, observers_, BrowserActionRemoved(extension));
325  }
326
327  UpdatePrefs();
328}
329
330void ExtensionToolbarModel::UninstalledExtension(const Extension* extension) {
331  // Remove the extension id from the ordered list, if it exists (the extension
332  // might not be represented in the list because it might not have an icon).
333  ExtensionIdList::iterator pos =
334      std::find(last_known_positions_.begin(),
335                last_known_positions_.end(), extension->id());
336
337  if (pos != last_known_positions_.end()) {
338    last_known_positions_.erase(pos);
339    UpdatePrefs();
340  }
341}
342
343// Combine the currently enabled extensions that have browser actions (which
344// we get from the ExtensionService) with the ordering we get from the
345// pref service. For robustness we use a somewhat inefficient process:
346// 1. Create a vector of extensions sorted by their pref values. This vector may
347// have holes.
348// 2. Create a vector of extensions that did not have a pref value.
349// 3. Remove holes from the sorted vector and append the unsorted vector.
350void ExtensionToolbarModel::InitializeExtensionList(ExtensionService* service) {
351  DCHECK(service->is_ready());
352
353  last_known_positions_ = extension_prefs_->GetToolbarOrder();
354  Populate(last_known_positions_, service);
355
356  extensions_initialized_ = true;
357  FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
358}
359
360void ExtensionToolbarModel::Populate(
361    const ExtensionIdList& positions,
362    ExtensionService* service) {
363  // Items that have explicit positions.
364  ExtensionList sorted;
365  sorted.resize(positions.size(), NULL);
366  // The items that don't have explicit positions.
367  ExtensionList unsorted;
368
369  ExtensionActionManager* extension_action_manager =
370      ExtensionActionManager::Get(profile_);
371
372  // Create the lists.
373  int hidden = 0;
374  for (extensions::ExtensionSet::const_iterator it =
375           service->extensions()->begin();
376       it != service->extensions()->end(); ++it) {
377    const Extension* extension = it->get();
378    if (!extension_action_manager->GetBrowserAction(*extension))
379      continue;
380    if (!ExtensionActionAPI::GetBrowserActionVisibility(
381            extension_prefs_, extension->id())) {
382      ++hidden;
383      continue;
384    }
385
386    ExtensionIdList::const_iterator pos =
387        std::find(positions.begin(), positions.end(), extension->id());
388    if (pos != positions.end())
389      sorted[pos - positions.begin()] = extension;
390    else
391      unsorted.push_back(make_scoped_refptr(extension));
392  }
393
394  // Erase current icons.
395  for (size_t i = 0; i < toolbar_items_.size(); i++) {
396    FOR_EACH_OBSERVER(
397        Observer, observers_, BrowserActionRemoved(toolbar_items_[i].get()));
398  }
399  toolbar_items_.clear();
400
401  // Merge the lists.
402  toolbar_items_.reserve(sorted.size() + unsorted.size());
403  for (ExtensionList::const_iterator iter = sorted.begin();
404       iter != sorted.end(); ++iter) {
405    // It's possible for the extension order to contain items that aren't
406    // actually loaded on this machine.  For example, when extension sync is on,
407    // we sync the extension order as-is but double-check with the user before
408    // syncing NPAPI-containing extensions, so if one of those is not actually
409    // synced, we'll get a NULL in the list.  This sort of case can also happen
410    // if some error prevents an extension from loading.
411    if (iter->get() != NULL)
412      toolbar_items_.push_back(*iter);
413  }
414  toolbar_items_.insert(toolbar_items_.end(), unsorted.begin(),
415                        unsorted.end());
416
417  UMA_HISTOGRAM_COUNTS_100(
418      "ExtensionToolbarModel.BrowserActionsPermanentlyHidden", hidden);
419  UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount",
420                           toolbar_items_.size());
421
422  if (!toolbar_items_.empty()) {
423    // Visible count can be -1, meaning: 'show all'. Since UMA converts negative
424    // values to 0, this would be counted as 'show none' unless we convert it to
425    // max.
426    UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsVisible",
427                             visible_icon_count_ == -1 ?
428                                 base::HistogramBase::kSampleType_MAX :
429                                 visible_icon_count_);
430  }
431
432  // Inform observers.
433  for (size_t i = 0; i < toolbar_items_.size(); i++) {
434    FOR_EACH_OBSERVER(
435        Observer, observers_, BrowserActionAdded(toolbar_items_[i].get(), i));
436  }
437}
438
439void ExtensionToolbarModel::UpdatePrefs() {
440  if (!extension_prefs_)
441    return;
442
443  // Don't observe change caused by self.
444  pref_change_registrar_.Remove(pref_names::kToolbar);
445  extension_prefs_->SetToolbarOrder(last_known_positions_);
446  pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_);
447}
448
449int ExtensionToolbarModel::IncognitoIndexToOriginal(int incognito_index) {
450  int original_index = 0, i = 0;
451  for (ExtensionList::iterator iter = toolbar_items_.begin();
452       iter != toolbar_items_.end();
453       ++iter, ++original_index) {
454    if (util::IsIncognitoEnabled((*iter)->id(), profile_)) {
455      if (incognito_index == i)
456        break;
457      ++i;
458    }
459  }
460  return original_index;
461}
462
463int ExtensionToolbarModel::OriginalIndexToIncognito(int original_index) {
464  int incognito_index = 0, i = 0;
465  for (ExtensionList::iterator iter = toolbar_items_.begin();
466       iter != toolbar_items_.end();
467       ++iter, ++i) {
468    if (original_index == i)
469      break;
470    if (util::IsIncognitoEnabled((*iter)->id(), profile_))
471      ++incognito_index;
472  }
473  return incognito_index;
474}
475
476void ExtensionToolbarModel::OnExtensionToolbarPrefChange() {
477  // If extensions are not ready, defer to later Populate() call.
478  if (!extensions_initialized_)
479    return;
480
481  // Recalculate |last_known_positions_| to be |pref_positions| followed by
482  // ones that are only in |last_known_positions_|.
483  ExtensionIdList pref_positions = extension_prefs_->GetToolbarOrder();
484  size_t pref_position_size = pref_positions.size();
485  for (size_t i = 0; i < last_known_positions_.size(); ++i) {
486    if (std::find(pref_positions.begin(), pref_positions.end(),
487                  last_known_positions_[i]) == pref_positions.end()) {
488      pref_positions.push_back(last_known_positions_[i]);
489    }
490  }
491  last_known_positions_.swap(pref_positions);
492
493  // Re-populate.
494  Populate(last_known_positions_,
495           ExtensionSystem::Get(profile_)->extension_service());
496
497  if (last_known_positions_.size() > pref_position_size) {
498    // Need to update pref because we have extra icons. But can't call
499    // UpdatePrefs() directly within observation closure.
500    base::MessageLoop::current()->PostTask(
501        FROM_HERE,
502        base::Bind(&ExtensionToolbarModel::UpdatePrefs,
503                   weak_ptr_factory_.GetWeakPtr()));
504  }
505}
506
507bool ExtensionToolbarModel::ShowBrowserActionPopup(const Extension* extension) {
508  ObserverListBase<Observer>::Iterator it(observers_);
509  Observer* obs = NULL;
510  while ((obs = it.GetNext()) != NULL) {
511    // Stop after first popup since it should only show in the active window.
512    if (obs->BrowserActionShowPopup(extension))
513      return true;
514  }
515  return false;
516}
517
518void ExtensionToolbarModel::EnsureVisibility(
519    const ExtensionIdList& extension_ids) {
520  if (visible_icon_count_ == -1)
521    return;  // Already showing all.
522
523  // Otherwise, make sure we have enough room to show all the extensions
524  // requested.
525  if (visible_icon_count_ < static_cast<int>(extension_ids.size())) {
526    SetVisibleIconCount(extension_ids.size());
527
528    // Inform observers.
529    FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
530  }
531
532  if (visible_icon_count_ == -1)
533    return;  // May have been set to max by SetVisibleIconCount.
534
535  // Guillotine's Delight: Move an orange noble to the front of the line.
536  for (ExtensionIdList::const_iterator it = extension_ids.begin();
537       it != extension_ids.end(); ++it) {
538    for (ExtensionList::const_iterator extension = toolbar_items_.begin();
539         extension != toolbar_items_.end(); ++extension) {
540      if ((*extension)->id() == (*it)) {
541        if (extension - toolbar_items_.begin() >= visible_icon_count_)
542          MoveBrowserAction(*extension, 0);
543        break;
544      }
545    }
546  }
547}
548
549bool ExtensionToolbarModel::HighlightExtensions(
550    const ExtensionIdList& extension_ids) {
551  highlighted_items_.clear();
552
553  for (ExtensionIdList::const_iterator id = extension_ids.begin();
554       id != extension_ids.end();
555       ++id) {
556    for (ExtensionList::const_iterator extension = toolbar_items_.begin();
557         extension != toolbar_items_.end();
558         ++extension) {
559      if (*id == (*extension)->id())
560        highlighted_items_.push_back(*extension);
561    }
562  }
563
564  // If we have any items in |highlighted_items_|, then we entered highlighting
565  // mode.
566  if (highlighted_items_.size()) {
567    old_visible_icon_count_ = visible_icon_count_;
568    is_highlighting_ = true;
569    if (visible_icon_count_ != -1 &&
570        visible_icon_count_ < static_cast<int>(extension_ids.size())) {
571      SetVisibleIconCount(extension_ids.size());
572      FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
573    }
574
575    FOR_EACH_OBSERVER(Observer, observers_, HighlightModeChanged(true));
576    return true;
577  }
578
579  // Otherwise, we didn't enter highlighting mode (and, in fact, exited it if
580  // we were otherwise in it).
581  if (is_highlighting_)
582    StopHighlighting();
583  return false;
584}
585
586void ExtensionToolbarModel::StopHighlighting() {
587  if (is_highlighting_) {
588    highlighted_items_.clear();
589    is_highlighting_ = false;
590    if (old_visible_icon_count_ != visible_icon_count_) {
591      SetVisibleIconCount(old_visible_icon_count_);
592      FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
593    }
594    FOR_EACH_OBSERVER(Observer, observers_, HighlightModeChanged(false));
595  }
596};
597
598}  // namespace extensions
599