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