Home | History | Annotate | Download | only in tabs
      1 // Copyright 2013 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/api/tabs/tabs_event_router.h"
      6 
      7 #include "base/json/json_writer.h"
      8 #include "base/values.h"
      9 #include "chrome/browser/chrome_notification_types.h"
     10 #include "chrome/browser/extensions/api/tabs/tabs_constants.h"
     11 #include "chrome/browser/extensions/api/tabs/tabs_windows_api.h"
     12 #include "chrome/browser/extensions/api/tabs/windows_event_router.h"
     13 #include "chrome/browser/extensions/extension_system.h"
     14 #include "chrome/browser/extensions/extension_tab_util.h"
     15 #include "chrome/browser/profiles/profile.h"
     16 #include "chrome/browser/ui/browser.h"
     17 #include "chrome/browser/ui/browser_iterator.h"
     18 #include "chrome/browser/ui/browser_list.h"
     19 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     20 #include "chrome/common/extensions/extension_constants.h"
     21 #include "content/public/browser/favicon_status.h"
     22 #include "content/public/browser/navigation_controller.h"
     23 #include "content/public/browser/navigation_entry.h"
     24 #include "content/public/browser/notification_service.h"
     25 #include "content/public/browser/notification_types.h"
     26 #include "content/public/browser/web_contents.h"
     27 
     28 using base::DictionaryValue;
     29 using base::ListValue;
     30 using base::FundamentalValue;
     31 using content::NavigationController;
     32 using content::WebContents;
     33 
     34 namespace extensions {
     35 
     36 namespace {
     37 
     38 namespace tabs = api::tabs;
     39 
     40 void WillDispatchTabUpdatedEvent(WebContents* contents,
     41                                  const DictionaryValue* changed_properties,
     42                                  content::BrowserContext* context,
     43                                  const Extension* extension,
     44                                  ListValue* event_args) {
     45   // Overwrite the second argument with the appropriate properties dictionary,
     46   // depending on extension permissions.
     47   DictionaryValue* properties_value = changed_properties->DeepCopy();
     48   ExtensionTabUtil::ScrubTabValueForExtension(contents,
     49                                               extension,
     50                                               properties_value);
     51   event_args->Set(1, properties_value);
     52 
     53   // Overwrite the third arg with our tab value as seen by this extension.
     54   event_args->Set(2, ExtensionTabUtil::CreateTabValue(contents, extension));
     55 }
     56 
     57 }  // namespace
     58 
     59 TabsEventRouter::TabEntry::TabEntry() : complete_waiting_on_load_(false),
     60                                         url_() {
     61 }
     62 
     63 DictionaryValue* TabsEventRouter::TabEntry::UpdateLoadState(
     64     const WebContents* contents) {
     65   // The tab may go in & out of loading (for instance if iframes navigate).
     66   // We only want to respond to the first change from loading to !loading after
     67   // the NAV_ENTRY_COMMITTED was fired.
     68   if (!complete_waiting_on_load_ || contents->IsLoading())
     69     return NULL;
     70 
     71   // Send "complete" state change.
     72   complete_waiting_on_load_ = false;
     73   DictionaryValue* changed_properties = new DictionaryValue();
     74   changed_properties->SetString(tabs_constants::kStatusKey,
     75                                 tabs_constants::kStatusValueComplete);
     76   return changed_properties;
     77 }
     78 
     79 DictionaryValue* TabsEventRouter::TabEntry::DidNavigate(
     80     const WebContents* contents) {
     81   // Send "loading" state change.
     82   complete_waiting_on_load_ = true;
     83   DictionaryValue* changed_properties = new DictionaryValue();
     84   changed_properties->SetString(tabs_constants::kStatusKey,
     85                                 tabs_constants::kStatusValueLoading);
     86 
     87   if (contents->GetURL() != url_) {
     88     url_ = contents->GetURL();
     89     changed_properties->SetString(tabs_constants::kUrlKey, url_.spec());
     90   }
     91 
     92   return changed_properties;
     93 }
     94 
     95 TabsEventRouter::TabsEventRouter(Profile* profile) : profile_(profile) {
     96   DCHECK(!profile->IsOffTheRecord());
     97 
     98   BrowserList::AddObserver(this);
     99 
    100   // Init() can happen after the browser is running, so catch up with any
    101   // windows that already exist.
    102   for (chrome::BrowserIterator it; !it.done(); it.Next()) {
    103     RegisterForBrowserNotifications(*it);
    104 
    105     // Also catch up our internal bookkeeping of tab entries.
    106     Browser* browser = *it;
    107     if (browser->tab_strip_model()) {
    108       for (int i = 0; i < browser->tab_strip_model()->count(); ++i) {
    109         WebContents* contents = browser->tab_strip_model()->GetWebContentsAt(i);
    110         int tab_id = ExtensionTabUtil::GetTabId(contents);
    111         tab_entries_[tab_id] = TabEntry();
    112       }
    113     }
    114   }
    115 }
    116 
    117 TabsEventRouter::~TabsEventRouter() {
    118   BrowserList::RemoveObserver(this);
    119 }
    120 
    121 void TabsEventRouter::OnBrowserAdded(Browser* browser) {
    122   RegisterForBrowserNotifications(browser);
    123 }
    124 
    125 void TabsEventRouter::RegisterForBrowserNotifications(Browser* browser) {
    126   if (!profile_->IsSameProfile(browser->profile()))
    127     return;
    128   // Start listening to TabStripModel events for this browser.
    129   TabStripModel* tab_strip = browser->tab_strip_model();
    130   tab_strip->AddObserver(this);
    131 
    132   for (int i = 0; i < tab_strip->count(); ++i) {
    133     RegisterForTabNotifications(tab_strip->GetWebContentsAt(i));
    134   }
    135 }
    136 
    137 void TabsEventRouter::RegisterForTabNotifications(WebContents* contents) {
    138   registrar_.Add(
    139       this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
    140       content::Source<NavigationController>(&contents->GetController()));
    141 
    142   // Observing NOTIFICATION_WEB_CONTENTS_DESTROYED is necessary because it's
    143   // possible for tabs to be created, detached and then destroyed without
    144   // ever having been re-attached and closed. This happens in the case of
    145   // a devtools WebContents that is opened in window, docked, then closed.
    146   registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
    147                  content::Source<WebContents>(contents));
    148 
    149   registrar_.Add(this, chrome::NOTIFICATION_FAVICON_UPDATED,
    150                  content::Source<WebContents>(contents));
    151 }
    152 
    153 void TabsEventRouter::UnregisterForTabNotifications(WebContents* contents) {
    154   registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
    155       content::Source<NavigationController>(&contents->GetController()));
    156   registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
    157       content::Source<WebContents>(contents));
    158   registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED,
    159       content::Source<WebContents>(contents));
    160 }
    161 
    162 void TabsEventRouter::OnBrowserRemoved(Browser* browser) {
    163   if (!profile_->IsSameProfile(browser->profile()))
    164     return;
    165 
    166   // Stop listening to TabStripModel events for this browser.
    167   browser->tab_strip_model()->RemoveObserver(this);
    168 }
    169 
    170 void TabsEventRouter::OnBrowserSetLastActive(Browser* browser) {
    171   TabsWindowsAPI* tabs_window_api = TabsWindowsAPI::Get(profile_);
    172   if (tabs_window_api) {
    173     tabs_window_api->windows_event_router()->OnActiveWindowChanged(
    174         browser ? browser->extension_window_controller() : NULL);
    175   }
    176 }
    177 
    178 static void WillDispatchTabCreatedEvent(WebContents* contents,
    179                                         bool active,
    180                                         content::BrowserContext* context,
    181                                         const Extension* extension,
    182                                         ListValue* event_args) {
    183   DictionaryValue* tab_value = ExtensionTabUtil::CreateTabValue(
    184       contents, extension);
    185   event_args->Clear();
    186   event_args->Append(tab_value);
    187   tab_value->SetBoolean(tabs_constants::kSelectedKey, active);
    188 }
    189 
    190 void TabsEventRouter::TabCreatedAt(WebContents* contents,
    191                                    int index,
    192                                    bool active) {
    193   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
    194   scoped_ptr<ListValue> args(new ListValue);
    195   scoped_ptr<Event> event(new Event(tabs::OnCreated::kEventName, args.Pass()));
    196   event->restrict_to_browser_context = profile;
    197   event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
    198   event->will_dispatch_callback =
    199       base::Bind(&WillDispatchTabCreatedEvent, contents, active);
    200   ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass());
    201 
    202   RegisterForTabNotifications(contents);
    203 }
    204 
    205 void TabsEventRouter::TabInsertedAt(WebContents* contents,
    206                                        int index,
    207                                        bool active) {
    208   // If tab is new, send created event.
    209   int tab_id = ExtensionTabUtil::GetTabId(contents);
    210   if (!GetTabEntry(contents)) {
    211     tab_entries_[tab_id] = TabEntry();
    212 
    213     TabCreatedAt(contents, index, active);
    214     return;
    215   }
    216 
    217   scoped_ptr<ListValue> args(new ListValue);
    218   args->Append(new FundamentalValue(tab_id));
    219 
    220   DictionaryValue* object_args = new DictionaryValue();
    221   object_args->Set(tabs_constants::kNewWindowIdKey,
    222                    new FundamentalValue(
    223                        ExtensionTabUtil::GetWindowIdOfTab(contents)));
    224   object_args->Set(tabs_constants::kNewPositionKey,
    225                    new FundamentalValue(index));
    226   args->Append(object_args);
    227 
    228   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
    229   DispatchEvent(profile, tabs::OnAttached::kEventName, args.Pass(),
    230                 EventRouter::USER_GESTURE_UNKNOWN);
    231 }
    232 
    233 void TabsEventRouter::TabDetachedAt(WebContents* contents, int index) {
    234   if (!GetTabEntry(contents)) {
    235     // The tab was removed. Don't send detach event.
    236     return;
    237   }
    238 
    239   scoped_ptr<ListValue> args(new ListValue);
    240   args->Append(
    241       new FundamentalValue(ExtensionTabUtil::GetTabId(contents)));
    242 
    243   DictionaryValue* object_args = new DictionaryValue();
    244   object_args->Set(tabs_constants::kOldWindowIdKey,
    245                    new FundamentalValue(
    246                        ExtensionTabUtil::GetWindowIdOfTab(contents)));
    247   object_args->Set(tabs_constants::kOldPositionKey,
    248                    new FundamentalValue(index));
    249   args->Append(object_args);
    250 
    251   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
    252   DispatchEvent(profile,
    253                 tabs::OnDetached::kEventName,
    254                 args.Pass(),
    255                 EventRouter::USER_GESTURE_UNKNOWN);
    256 }
    257 
    258 void TabsEventRouter::TabClosingAt(TabStripModel* tab_strip_model,
    259                                    WebContents* contents,
    260                                    int index) {
    261   int tab_id = ExtensionTabUtil::GetTabId(contents);
    262 
    263   scoped_ptr<ListValue> args(new ListValue);
    264   args->Append(new FundamentalValue(tab_id));
    265 
    266   DictionaryValue* object_args = new DictionaryValue();
    267   object_args->SetInteger(tabs_constants::kWindowIdKey,
    268                           ExtensionTabUtil::GetWindowIdOfTab(contents));
    269   object_args->SetBoolean(tabs_constants::kWindowClosing,
    270                           tab_strip_model->closing_all());
    271   args->Append(object_args);
    272 
    273   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
    274   DispatchEvent(profile,
    275                 tabs::OnRemoved::kEventName,
    276                 args.Pass(),
    277                 EventRouter::USER_GESTURE_UNKNOWN);
    278 
    279   int removed_count = tab_entries_.erase(tab_id);
    280   DCHECK_GT(removed_count, 0);
    281 
    282   UnregisterForTabNotifications(contents);
    283 }
    284 
    285 void TabsEventRouter::ActiveTabChanged(WebContents* old_contents,
    286                                        WebContents* new_contents,
    287                                        int index,
    288                                        int reason) {
    289   scoped_ptr<ListValue> args(new ListValue);
    290   int tab_id = ExtensionTabUtil::GetTabId(new_contents);
    291   args->Append(new FundamentalValue(tab_id));
    292 
    293   DictionaryValue* object_args = new DictionaryValue();
    294   object_args->Set(tabs_constants::kWindowIdKey,
    295                    new FundamentalValue(
    296                        ExtensionTabUtil::GetWindowIdOfTab(new_contents)));
    297   args->Append(object_args);
    298 
    299   // The onActivated event replaced onActiveChanged and onSelectionChanged. The
    300   // deprecated events take two arguments: tabId, {windowId}.
    301   Profile* profile =
    302       Profile::FromBrowserContext(new_contents->GetBrowserContext());
    303   EventRouter::UserGestureState gesture =
    304       reason & CHANGE_REASON_USER_GESTURE
    305       ? EventRouter::USER_GESTURE_ENABLED
    306       : EventRouter::USER_GESTURE_NOT_ENABLED;
    307   DispatchEvent(profile,
    308                 tabs::OnSelectionChanged::kEventName,
    309                 scoped_ptr<ListValue>(args->DeepCopy()),
    310                 gesture);
    311   DispatchEvent(profile,
    312                 tabs::OnActiveChanged::kEventName,
    313                 scoped_ptr<ListValue>(args->DeepCopy()),
    314                 gesture);
    315 
    316   // The onActivated event takes one argument: {windowId, tabId}.
    317   args->Remove(0, NULL);
    318   object_args->Set(tabs_constants::kTabIdKey,
    319                    new FundamentalValue(tab_id));
    320   DispatchEvent(profile, tabs::OnActivated::kEventName, args.Pass(), gesture);
    321 }
    322 
    323 void TabsEventRouter::TabSelectionChanged(
    324     TabStripModel* tab_strip_model,
    325     const ui::ListSelectionModel& old_model) {
    326   ui::ListSelectionModel::SelectedIndices new_selection =
    327       tab_strip_model->selection_model().selected_indices();
    328   scoped_ptr<ListValue> all_tabs(new ListValue);
    329 
    330   for (size_t i = 0; i < new_selection.size(); ++i) {
    331     int index = new_selection[i];
    332     WebContents* contents = tab_strip_model->GetWebContentsAt(index);
    333     if (!contents)
    334       break;
    335     int tab_id = ExtensionTabUtil::GetTabId(contents);
    336     all_tabs->Append(new FundamentalValue(tab_id));
    337   }
    338 
    339   scoped_ptr<ListValue> args(new ListValue);
    340   scoped_ptr<DictionaryValue> select_info(new DictionaryValue);
    341 
    342   select_info->Set(
    343       tabs_constants::kWindowIdKey,
    344       new FundamentalValue(
    345           ExtensionTabUtil::GetWindowIdOfTabStripModel(tab_strip_model)));
    346 
    347   select_info->Set(tabs_constants::kTabIdsKey, all_tabs.release());
    348   args->Append(select_info.release());
    349 
    350   // The onHighlighted event replaced onHighlightChanged.
    351   Profile* profile = tab_strip_model->profile();
    352   DispatchEvent(profile,
    353                 tabs::OnHighlightChanged::kEventName,
    354                 scoped_ptr<ListValue>(args->DeepCopy()),
    355                 EventRouter::USER_GESTURE_UNKNOWN);
    356   DispatchEvent(profile,
    357                 tabs::OnHighlighted::kEventName,
    358                 args.Pass(),
    359                 EventRouter::USER_GESTURE_UNKNOWN);
    360 }
    361 
    362 void TabsEventRouter::TabMoved(WebContents* contents,
    363                                int from_index,
    364                                int to_index) {
    365   scoped_ptr<ListValue> args(new ListValue);
    366   args->Append(
    367       new FundamentalValue(ExtensionTabUtil::GetTabId(contents)));
    368 
    369   DictionaryValue* object_args = new DictionaryValue();
    370   object_args->Set(tabs_constants::kWindowIdKey,
    371                    new FundamentalValue(
    372                        ExtensionTabUtil::GetWindowIdOfTab(contents)));
    373   object_args->Set(tabs_constants::kFromIndexKey,
    374                    new FundamentalValue(from_index));
    375   object_args->Set(tabs_constants::kToIndexKey,
    376                    new FundamentalValue(to_index));
    377   args->Append(object_args);
    378 
    379   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
    380   DispatchEvent(profile,
    381                 tabs::OnMoved::kEventName,
    382                 args.Pass(),
    383                 EventRouter::USER_GESTURE_UNKNOWN);
    384 }
    385 
    386 void TabsEventRouter::TabUpdated(WebContents* contents, bool did_navigate) {
    387   TabEntry* entry = GetTabEntry(contents);
    388   scoped_ptr<DictionaryValue> changed_properties;
    389 
    390   CHECK(entry);
    391 
    392   if (did_navigate)
    393     changed_properties.reset(entry->DidNavigate(contents));
    394   else
    395     changed_properties.reset(entry->UpdateLoadState(contents));
    396 
    397   if (changed_properties)
    398     DispatchTabUpdatedEvent(contents, changed_properties.Pass());
    399 }
    400 
    401 void TabsEventRouter::FaviconUrlUpdated(WebContents* contents) {
    402     content::NavigationEntry* entry =
    403         contents->GetController().GetVisibleEntry();
    404     if (!entry || !entry->GetFavicon().valid)
    405       return;
    406     scoped_ptr<DictionaryValue> changed_properties(new DictionaryValue);
    407     changed_properties->SetString(
    408         tabs_constants::kFaviconUrlKey,
    409         entry->GetFavicon().url.possibly_invalid_spec());
    410     DispatchTabUpdatedEvent(contents, changed_properties.Pass());
    411 }
    412 
    413 void TabsEventRouter::DispatchEvent(
    414     Profile* profile,
    415     const std::string& event_name,
    416     scoped_ptr<ListValue> args,
    417     EventRouter::UserGestureState user_gesture) {
    418   if (!profile_->IsSameProfile(profile) ||
    419       !ExtensionSystem::Get(profile)->event_router())
    420     return;
    421 
    422   scoped_ptr<Event> event(new Event(event_name, args.Pass()));
    423   event->restrict_to_browser_context = profile;
    424   event->user_gesture = user_gesture;
    425   ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass());
    426 }
    427 
    428 void TabsEventRouter::DispatchSimpleBrowserEvent(
    429     Profile* profile, const int window_id, const std::string& event_name) {
    430   if (!profile_->IsSameProfile(profile))
    431     return;
    432 
    433   scoped_ptr<ListValue> args(new ListValue);
    434   args->Append(new FundamentalValue(window_id));
    435 
    436   DispatchEvent(profile,
    437                 event_name,
    438                 args.Pass(),
    439                 EventRouter::USER_GESTURE_UNKNOWN);
    440 }
    441 
    442 void TabsEventRouter::DispatchTabUpdatedEvent(
    443     WebContents* contents, scoped_ptr<DictionaryValue> changed_properties) {
    444   DCHECK(changed_properties);
    445   DCHECK(contents);
    446 
    447   // The state of the tab (as seen from the extension point of view) has
    448   // changed.  Send a notification to the extension.
    449   scoped_ptr<ListValue> args_base(new ListValue);
    450 
    451   // First arg: The id of the tab that changed.
    452   args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents));
    453 
    454   // Second arg: An object containing the changes to the tab state.  Filled in
    455   // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the
    456   // extension has the tabs permission.
    457 
    458   // Third arg: An object containing the state of the tab. Filled in by
    459   // WillDispatchTabUpdatedEvent.
    460   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
    461 
    462   scoped_ptr<Event> event(
    463       new Event(tabs::OnUpdated::kEventName, args_base.Pass()));
    464   event->restrict_to_browser_context = profile;
    465   event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
    466   event->will_dispatch_callback =
    467       base::Bind(&WillDispatchTabUpdatedEvent,
    468                  contents,
    469                  changed_properties.get());
    470   ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass());
    471 }
    472 
    473 TabsEventRouter::TabEntry* TabsEventRouter::GetTabEntry(
    474     const WebContents* contents) {
    475   int tab_id = ExtensionTabUtil::GetTabId(contents);
    476   std::map<int, TabEntry>::iterator i = tab_entries_.find(tab_id);
    477   if (tab_entries_.end() == i)
    478     return NULL;
    479   return &i->second;
    480 }
    481 
    482 void TabsEventRouter::Observe(int type,
    483                               const content::NotificationSource& source,
    484                               const content::NotificationDetails& details) {
    485   if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) {
    486     NavigationController* source_controller =
    487         content::Source<NavigationController>(source).ptr();
    488     TabUpdated(source_controller->GetWebContents(), true);
    489   } else if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) {
    490     // Tab was destroyed after being detached (without being re-attached).
    491     WebContents* contents = content::Source<WebContents>(source).ptr();
    492     registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
    493         content::Source<NavigationController>(&contents->GetController()));
    494     registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
    495         content::Source<WebContents>(contents));
    496     registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED,
    497         content::Source<WebContents>(contents));
    498   } else if (type == chrome::NOTIFICATION_FAVICON_UPDATED) {
    499     bool icon_url_changed = *content::Details<bool>(details).ptr();
    500     if (icon_url_changed)
    501       FaviconUrlUpdated(content::Source<WebContents>(source).ptr());
    502   } else {
    503     NOTREACHED();
    504   }
    505 }
    506 
    507 void TabsEventRouter::TabChangedAt(WebContents* contents,
    508                                    int index,
    509                                    TabChangeType change_type) {
    510   TabUpdated(contents, false);
    511 }
    512 
    513 void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model,
    514                                     WebContents* old_contents,
    515                                     WebContents* new_contents,
    516                                     int index) {
    517   // Notify listeners that the next tabs closing or being added are due to
    518   // WebContents being swapped.
    519   const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents);
    520   const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents);
    521   scoped_ptr<ListValue> args(new ListValue);
    522   args->Append(new FundamentalValue(new_tab_id));
    523   args->Append(new FundamentalValue(old_tab_id));
    524 
    525   DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()),
    526                 tabs::OnReplaced::kEventName,
    527                 args.Pass(),
    528                 EventRouter::USER_GESTURE_UNKNOWN);
    529 
    530   // Update tab_entries_.
    531   const int removed_count = tab_entries_.erase(old_tab_id);
    532   DCHECK_GT(removed_count, 0);
    533   UnregisterForTabNotifications(old_contents);
    534 
    535   if (!GetTabEntry(new_contents)) {
    536     tab_entries_[new_tab_id] = TabEntry();
    537     RegisterForTabNotifications(new_contents);
    538   }
    539 }
    540 
    541 void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) {
    542   TabStripModel* tab_strip = NULL;
    543   int tab_index;
    544 
    545   if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) {
    546     scoped_ptr<DictionaryValue> changed_properties(new DictionaryValue());
    547     changed_properties->SetBoolean(tabs_constants::kPinnedKey,
    548                                    tab_strip->IsTabPinned(tab_index));
    549     DispatchTabUpdatedEvent(contents, changed_properties.Pass());
    550   }
    551 }
    552 
    553 }  // namespace extensions
    554