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