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