extension_toolbar_model.cc revision 0529e5d033099cbfc42635f6f6183833b09dff6e
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
35namespace extensions {
36
37bool ExtensionToolbarModel::Observer::BrowserActionShowPopup(
38    const extensions::Extension* extension) {
39  return false;
40}
41
42ExtensionToolbarModel::ExtensionToolbarModel(
43    Profile* profile,
44    extensions::ExtensionPrefs* extension_prefs)
45    : profile_(profile),
46      extension_prefs_(extension_prefs),
47      prefs_(profile_->GetPrefs()),
48      extensions_initialized_(false),
49      is_highlighting_(false),
50      weak_ptr_factory_(this) {
51  registrar_.Add(this,
52                 chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
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  // Only set the prefs if we're not in highlight mode. Highlight mode is
175  // designed to be a transitory state, and should not persist across browser
176  // restarts (though it may be re-entered).
177  if (!is_highlighting_) {
178    prefs_->SetInteger(extensions::pref_names::kToolbarSize,
179                       visible_icon_count_);
180  }
181}
182
183void ExtensionToolbarModel::Observe(
184    int type,
185    const content::NotificationSource& source,
186    const content::NotificationDetails& details) {
187  ExtensionService* extension_service =
188      extensions::ExtensionSystem::Get(profile_)->extension_service();
189  if (!extension_service || !extension_service->is_ready())
190    return;
191
192  if (type == chrome::NOTIFICATION_EXTENSIONS_READY) {
193    InitializeExtensionList(extension_service);
194    return;
195  }
196
197  const Extension* extension = NULL;
198  if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED) {
199    extension = content::Details<extensions::UnloadedExtensionInfo>(
200        details)->extension;
201  } else if (type ==
202      chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED) {
203    extension = extension_service->GetExtensionById(
204        *content::Details<const std::string>(details).ptr(), true);
205  } else {
206    extension = content::Details<const Extension>(details).ptr();
207  }
208  if (type == chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED) {
209    // We don't want to add the same extension twice. It may have already been
210    // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user
211    // hides the browser action and then disables and enables the extension.
212    for (size_t i = 0; i < toolbar_items_.size(); i++) {
213      if (toolbar_items_[i].get() == extension)
214        return;  // Already exists.
215    }
216    if (ExtensionActionAPI::GetBrowserActionVisibility(
217            extension_prefs_, extension->id())) {
218      AddExtension(extension);
219    }
220  } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED) {
221    RemoveExtension(extension);
222  } else if (type == chrome::NOTIFICATION_EXTENSION_UNINSTALLED) {
223    UninstalledExtension(extension);
224  } else if (type ==
225      chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED) {
226    if (ExtensionActionAPI::GetBrowserActionVisibility(
227            extension_prefs_, extension->id())) {
228      AddExtension(extension);
229    } else {
230      RemoveExtension(extension);
231    }
232  } else {
233    NOTREACHED() << "Received unexpected notification";
234  }
235}
236
237size_t ExtensionToolbarModel::FindNewPositionFromLastKnownGood(
238    const Extension* extension) {
239  // See if we have last known good position for this extension.
240  size_t new_index = 0;
241  // Loop through the ID list of known positions, to count the number of visible
242  // browser action icons preceding |extension|.
243  for (ExtensionIdList::const_iterator iter_id = last_known_positions_.begin();
244       iter_id < last_known_positions_.end(); ++iter_id) {
245    if ((*iter_id) == extension->id())
246      return new_index;  // We've found the right position.
247    // Found an id, need to see if it is visible.
248    for (ExtensionList::const_iterator iter_ext = toolbar_items_.begin();
249         iter_ext < toolbar_items_.end(); ++iter_ext) {
250      if ((*iter_ext)->id().compare(*iter_id) == 0) {
251        // This extension is visible, update the index value.
252        ++new_index;
253        break;
254      }
255    }
256  }
257
258  return -1;
259}
260
261void ExtensionToolbarModel::AddExtension(const Extension* extension) {
262  // We only care about extensions with browser actions.
263  if (!ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension))
264    return;
265
266  size_t new_index = -1;
267
268  // See if we have a last known good position for this extension.
269  ExtensionIdList::iterator last_pos = std::find(last_known_positions_.begin(),
270                                                 last_known_positions_.end(),
271                                                 extension->id());
272  if (last_pos != last_known_positions_.end()) {
273    new_index = FindNewPositionFromLastKnownGood(extension);
274    if (new_index != toolbar_items_.size()) {
275      toolbar_items_.insert(toolbar_items_.begin() + new_index,
276                            make_scoped_refptr(extension));
277    } else {
278      toolbar_items_.push_back(make_scoped_refptr(extension));
279    }
280  } else {
281    // This is a never before seen extension, that was added to the end. Make
282    // sure to reflect that.
283    toolbar_items_.push_back(make_scoped_refptr(extension));
284    last_known_positions_.push_back(extension->id());
285    new_index = toolbar_items_.size() - 1;
286    UpdatePrefs();
287  }
288
289  // If we're currently highlighting, then even though we add a browser action
290  // to the full list (|toolbar_items_|, there won't be another *visible*
291  // browser action, which was what the observers care about.
292  if (!is_highlighting_) {
293    FOR_EACH_OBSERVER(Observer, observers_,
294                      BrowserActionAdded(extension, new_index));
295  }
296}
297
298void ExtensionToolbarModel::RemoveExtension(const Extension* extension) {
299  ExtensionList::iterator pos =
300      std::find(toolbar_items_.begin(), toolbar_items_.end(), extension);
301  if (pos == toolbar_items_.end())
302    return;
303
304  toolbar_items_.erase(pos);
305
306  // If we're in highlight mode, we also have to remove the extension from
307  // the highlighted list.
308  if (is_highlighting_) {
309    pos = std::find(highlighted_items_.begin(),
310                    highlighted_items_.end(),
311                    extension);
312    if (pos != highlighted_items_.end()) {
313      highlighted_items_.erase(pos);
314      FOR_EACH_OBSERVER(Observer, observers_, BrowserActionRemoved(extension));
315      // If the highlighted list is now empty, we stop highlighting.
316      if (highlighted_items_.empty())
317        StopHighlighting();
318    }
319  } else {
320    FOR_EACH_OBSERVER(Observer, observers_, BrowserActionRemoved(extension));
321  }
322
323  UpdatePrefs();
324}
325
326void ExtensionToolbarModel::UninstalledExtension(const Extension* extension) {
327  // Remove the extension id from the ordered list, if it exists (the extension
328  // might not be represented in the list because it might not have an icon).
329  ExtensionIdList::iterator pos =
330      std::find(last_known_positions_.begin(),
331                last_known_positions_.end(), extension->id());
332
333  if (pos != last_known_positions_.end()) {
334    last_known_positions_.erase(pos);
335    UpdatePrefs();
336  }
337}
338
339// Combine the currently enabled extensions that have browser actions (which
340// we get from the ExtensionService) with the ordering we get from the
341// pref service. For robustness we use a somewhat inefficient process:
342// 1. Create a vector of extensions sorted by their pref values. This vector may
343// have holes.
344// 2. Create a vector of extensions that did not have a pref value.
345// 3. Remove holes from the sorted vector and append the unsorted vector.
346void ExtensionToolbarModel::InitializeExtensionList(ExtensionService* service) {
347  DCHECK(service->is_ready());
348
349  last_known_positions_ = extension_prefs_->GetToolbarOrder();
350  Populate(last_known_positions_, service);
351
352  extensions_initialized_ = true;
353  FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
354}
355
356void ExtensionToolbarModel::Populate(
357    const ExtensionIdList& positions,
358    ExtensionService* service) {
359  // Items that have explicit positions.
360  ExtensionList sorted;
361  sorted.resize(positions.size(), NULL);
362  // The items that don't have explicit positions.
363  ExtensionList unsorted;
364
365  ExtensionActionManager* extension_action_manager =
366      ExtensionActionManager::Get(profile_);
367
368  // Create the lists.
369  int hidden = 0;
370  for (extensions::ExtensionSet::const_iterator it =
371           service->extensions()->begin();
372       it != service->extensions()->end(); ++it) {
373    const Extension* extension = it->get();
374    if (!extension_action_manager->GetBrowserAction(*extension))
375      continue;
376    if (!ExtensionActionAPI::GetBrowserActionVisibility(
377            extension_prefs_, extension->id())) {
378      ++hidden;
379      continue;
380    }
381
382    ExtensionIdList::const_iterator pos =
383        std::find(positions.begin(), positions.end(), extension->id());
384    if (pos != positions.end())
385      sorted[pos - positions.begin()] = extension;
386    else
387      unsorted.push_back(make_scoped_refptr(extension));
388  }
389
390  // Erase current icons.
391  for (size_t i = 0; i < toolbar_items_.size(); i++) {
392    FOR_EACH_OBSERVER(
393        Observer, observers_, BrowserActionRemoved(toolbar_items_[i].get()));
394  }
395  toolbar_items_.clear();
396
397  // Merge the lists.
398  toolbar_items_.reserve(sorted.size() + unsorted.size());
399  for (ExtensionList::const_iterator iter = sorted.begin();
400       iter != sorted.end(); ++iter) {
401    // It's possible for the extension order to contain items that aren't
402    // actually loaded on this machine.  For example, when extension sync is on,
403    // we sync the extension order as-is but double-check with the user before
404    // syncing NPAPI-containing extensions, so if one of those is not actually
405    // synced, we'll get a NULL in the list.  This sort of case can also happen
406    // if some error prevents an extension from loading.
407    if (iter->get() != NULL)
408      toolbar_items_.push_back(*iter);
409  }
410  toolbar_items_.insert(toolbar_items_.end(), unsorted.begin(),
411                        unsorted.end());
412
413  UMA_HISTOGRAM_COUNTS_100(
414      "ExtensionToolbarModel.BrowserActionsPermanentlyHidden", hidden);
415  UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount",
416                           toolbar_items_.size());
417
418  if (!toolbar_items_.empty()) {
419    // Visible count can be -1, meaning: 'show all'. Since UMA converts negative
420    // values to 0, this would be counted as 'show none' unless we convert it to
421    // max.
422    UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsVisible",
423                             visible_icon_count_ == -1 ?
424                                 base::HistogramBase::kSampleType_MAX :
425                                 visible_icon_count_);
426  }
427
428  // Inform observers.
429  for (size_t i = 0; i < toolbar_items_.size(); i++) {
430    FOR_EACH_OBSERVER(
431        Observer, observers_, BrowserActionAdded(toolbar_items_[i].get(), i));
432  }
433}
434
435void ExtensionToolbarModel::UpdatePrefs() {
436  if (!extension_prefs_)
437    return;
438
439  // Don't observe change caused by self.
440  pref_change_registrar_.Remove(pref_names::kToolbar);
441  extension_prefs_->SetToolbarOrder(last_known_positions_);
442  pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_);
443}
444
445int ExtensionToolbarModel::IncognitoIndexToOriginal(int incognito_index) {
446  int original_index = 0, i = 0;
447  for (ExtensionList::iterator iter = toolbar_items_.begin();
448       iter != toolbar_items_.end();
449       ++iter, ++original_index) {
450    if (util::IsIncognitoEnabled((*iter)->id(), profile_)) {
451      if (incognito_index == i)
452        break;
453      ++i;
454    }
455  }
456  return original_index;
457}
458
459int ExtensionToolbarModel::OriginalIndexToIncognito(int original_index) {
460  int incognito_index = 0, i = 0;
461  for (ExtensionList::iterator iter = toolbar_items_.begin();
462       iter != toolbar_items_.end();
463       ++iter, ++i) {
464    if (original_index == i)
465      break;
466    if (util::IsIncognitoEnabled((*iter)->id(), profile_))
467      ++incognito_index;
468  }
469  return incognito_index;
470}
471
472void ExtensionToolbarModel::OnExtensionToolbarPrefChange() {
473  // If extensions are not ready, defer to later Populate() call.
474  if (!extensions_initialized_)
475    return;
476
477  // Recalculate |last_known_positions_| to be |pref_positions| followed by
478  // ones that are only in |last_known_positions_|.
479  ExtensionIdList pref_positions = extension_prefs_->GetToolbarOrder();
480  size_t pref_position_size = pref_positions.size();
481  for (size_t i = 0; i < last_known_positions_.size(); ++i) {
482    if (std::find(pref_positions.begin(), pref_positions.end(),
483                  last_known_positions_[i]) == pref_positions.end()) {
484      pref_positions.push_back(last_known_positions_[i]);
485    }
486  }
487  last_known_positions_.swap(pref_positions);
488
489  // Re-populate.
490  Populate(last_known_positions_,
491           ExtensionSystem::Get(profile_)->extension_service());
492
493  if (last_known_positions_.size() > pref_position_size) {
494    // Need to update pref because we have extra icons. But can't call
495    // UpdatePrefs() directly within observation closure.
496    base::MessageLoop::current()->PostTask(
497        FROM_HERE,
498        base::Bind(&ExtensionToolbarModel::UpdatePrefs,
499                   weak_ptr_factory_.GetWeakPtr()));
500  }
501}
502
503bool ExtensionToolbarModel::ShowBrowserActionPopup(const Extension* extension) {
504  ObserverListBase<Observer>::Iterator it(observers_);
505  Observer* obs = NULL;
506  while ((obs = it.GetNext()) != NULL) {
507    // Stop after first popup since it should only show in the active window.
508    if (obs->BrowserActionShowPopup(extension))
509      return true;
510  }
511  return false;
512}
513
514void ExtensionToolbarModel::EnsureVisibility(
515    const ExtensionIdList& extension_ids) {
516  if (visible_icon_count_ == -1)
517    return;  // Already showing all.
518
519  // Otherwise, make sure we have enough room to show all the extensions
520  // requested.
521  if (visible_icon_count_ < static_cast<int>(extension_ids.size())) {
522    SetVisibleIconCount(extension_ids.size());
523
524    // Inform observers.
525    FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
526  }
527
528  if (visible_icon_count_ == -1)
529    return;  // May have been set to max by SetVisibleIconCount.
530
531  // Guillotine's Delight: Move an orange noble to the front of the line.
532  for (ExtensionIdList::const_iterator it = extension_ids.begin();
533       it != extension_ids.end(); ++it) {
534    for (ExtensionList::const_iterator extension = toolbar_items_.begin();
535         extension != toolbar_items_.end(); ++extension) {
536      if ((*extension)->id() == (*it)) {
537        if (extension - toolbar_items_.begin() >= visible_icon_count_)
538          MoveBrowserAction(*extension, 0);
539        break;
540      }
541    }
542  }
543}
544
545bool ExtensionToolbarModel::HighlightExtensions(
546    const ExtensionIdList& extension_ids) {
547  highlighted_items_.clear();
548
549  for (ExtensionIdList::const_iterator id = extension_ids.begin();
550       id != extension_ids.end();
551       ++id) {
552    for (ExtensionList::const_iterator extension = toolbar_items_.begin();
553         extension != toolbar_items_.end();
554         ++extension) {
555      if (*id == (*extension)->id())
556        highlighted_items_.push_back(*extension);
557    }
558  }
559
560  // If we have any items in |highlighted_items_|, then we entered highlighting
561  // mode.
562  if (highlighted_items_.size()) {
563    old_visible_icon_count_ = visible_icon_count_;
564    is_highlighting_ = true;
565    if (visible_icon_count_ != -1 &&
566        visible_icon_count_ < static_cast<int>(extension_ids.size())) {
567      SetVisibleIconCount(extension_ids.size());
568      FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
569    }
570
571    FOR_EACH_OBSERVER(Observer, observers_, HighlightModeChanged(true));
572    return true;
573  }
574
575  // Otherwise, we didn't enter highlighting mode (and, in fact, exited it if
576  // we were otherwise in it).
577  if (is_highlighting_)
578    StopHighlighting();
579  return false;
580}
581
582void ExtensionToolbarModel::StopHighlighting() {
583  if (is_highlighting_) {
584    highlighted_items_.clear();
585    is_highlighting_ = false;
586    if (old_visible_icon_count_ != visible_icon_count_) {
587      SetVisibleIconCount(old_visible_icon_count_);
588      FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
589    }
590    FOR_EACH_OBSERVER(Observer, observers_, HighlightModeChanged(false));
591  }
592};
593
594}  // namespace extensions
595