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_tab_util.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/browser_iterator.h"
17 #include "chrome/browser/ui/browser_list.h"
18 #include "chrome/browser/ui/tabs/tab_strip_model.h"
19 #include "chrome/common/extensions/extension_constants.h"
20 #include "content/public/browser/favicon_status.h"
21 #include "content/public/browser/navigation_controller.h"
22 #include "content/public/browser/navigation_entry.h"
23 #include "content/public/browser/notification_service.h"
24 #include "content/public/browser/notification_types.h"
25 #include "content/public/browser/web_contents.h"
26
27 using base::DictionaryValue;
28 using base::ListValue;
29 using base::FundamentalValue;
30 using content::NavigationController;
31 using content::WebContents;
32
33 namespace extensions {
34
35 namespace {
36
37 namespace tabs = api::tabs;
38
WillDispatchTabUpdatedEvent(WebContents * contents,const base::DictionaryValue * changed_properties,content::BrowserContext * context,const Extension * extension,base::ListValue * event_args)39 void WillDispatchTabUpdatedEvent(
40 WebContents* contents,
41 const base::DictionaryValue* changed_properties,
42 content::BrowserContext* context,
43 const Extension* extension,
44 base::ListValue* event_args) {
45 // Overwrite the second argument with the appropriate properties dictionary,
46 // depending on extension permissions.
47 base::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
TabEntry()59 TabsEventRouter::TabEntry::TabEntry() : complete_waiting_on_load_(false),
60 url_() {
61 }
62
UpdateLoadState(const WebContents * contents)63 base::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 base::DictionaryValue* changed_properties = new base::DictionaryValue();
74 changed_properties->SetString(tabs_constants::kStatusKey,
75 tabs_constants::kStatusValueComplete);
76 return changed_properties;
77 }
78
DidNavigate(const WebContents * contents)79 base::DictionaryValue* TabsEventRouter::TabEntry::DidNavigate(
80 const WebContents* contents) {
81 // Send "loading" state change.
82 complete_waiting_on_load_ = true;
83 base::DictionaryValue* changed_properties = new base::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
TabsEventRouter(Profile * profile)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
~TabsEventRouter()117 TabsEventRouter::~TabsEventRouter() {
118 BrowserList::RemoveObserver(this);
119 }
120
OnBrowserAdded(Browser * browser)121 void TabsEventRouter::OnBrowserAdded(Browser* browser) {
122 RegisterForBrowserNotifications(browser);
123 }
124
RegisterForBrowserNotifications(Browser * browser)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
RegisterForTabNotifications(WebContents * contents)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
UnregisterForTabNotifications(WebContents * contents)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
OnBrowserRemoved(Browser * browser)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
OnBrowserSetLastActive(Browser * browser)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
WillDispatchTabCreatedEvent(WebContents * contents,bool active,content::BrowserContext * context,const Extension * extension,base::ListValue * event_args)178 static void WillDispatchTabCreatedEvent(WebContents* contents,
179 bool active,
180 content::BrowserContext* context,
181 const Extension* extension,
182 base::ListValue* event_args) {
183 base::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
TabCreatedAt(WebContents * contents,int index,bool active)190 void TabsEventRouter::TabCreatedAt(WebContents* contents,
191 int index,
192 bool active) {
193 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
194 scoped_ptr<base::ListValue> args(new base::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 EventRouter::Get(profile)->BroadcastEvent(event.Pass());
201
202 RegisterForTabNotifications(contents);
203 }
204
TabInsertedAt(WebContents * contents,int index,bool active)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<base::ListValue> args(new base::ListValue);
218 args->Append(new FundamentalValue(tab_id));
219
220 base::DictionaryValue* object_args = new base::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
TabDetachedAt(WebContents * contents,int index)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<base::ListValue> args(new base::ListValue);
240 args->Append(
241 new FundamentalValue(ExtensionTabUtil::GetTabId(contents)));
242
243 base::DictionaryValue* object_args = new base::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
TabClosingAt(TabStripModel * tab_strip_model,WebContents * contents,int index)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<base::ListValue> args(new base::ListValue);
264 args->Append(new FundamentalValue(tab_id));
265
266 base::DictionaryValue* object_args = new base::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
ActiveTabChanged(WebContents * old_contents,WebContents * new_contents,int index,int reason)285 void TabsEventRouter::ActiveTabChanged(WebContents* old_contents,
286 WebContents* new_contents,
287 int index,
288 int reason) {
289 scoped_ptr<base::ListValue> args(new base::ListValue);
290 int tab_id = ExtensionTabUtil::GetTabId(new_contents);
291 args->Append(new FundamentalValue(tab_id));
292
293 base::DictionaryValue* object_args = new base::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<base::ListValue>(args->DeepCopy()),
310 gesture);
311 DispatchEvent(profile,
312 tabs::OnActiveChanged::kEventName,
313 scoped_ptr<base::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
TabSelectionChanged(TabStripModel * tab_strip_model,const ui::ListSelectionModel & old_model)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<base::ListValue> all_tabs(new base::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<base::ListValue> args(new base::ListValue);
340 scoped_ptr<base::DictionaryValue> select_info(new base::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<base::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
TabMoved(WebContents * contents,int from_index,int to_index)362 void TabsEventRouter::TabMoved(WebContents* contents,
363 int from_index,
364 int to_index) {
365 scoped_ptr<base::ListValue> args(new base::ListValue);
366 args->Append(
367 new FundamentalValue(ExtensionTabUtil::GetTabId(contents)));
368
369 base::DictionaryValue* object_args = new base::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
TabUpdated(WebContents * contents,bool did_navigate)386 void TabsEventRouter::TabUpdated(WebContents* contents, bool did_navigate) {
387 TabEntry* entry = GetTabEntry(contents);
388 scoped_ptr<base::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
FaviconUrlUpdated(WebContents * contents)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<base::DictionaryValue> changed_properties(
407 new base::DictionaryValue);
408 changed_properties->SetString(
409 tabs_constants::kFaviconUrlKey,
410 entry->GetFavicon().url.possibly_invalid_spec());
411 DispatchTabUpdatedEvent(contents, changed_properties.Pass());
412 }
413
DispatchEvent(Profile * profile,const std::string & event_name,scoped_ptr<base::ListValue> args,EventRouter::UserGestureState user_gesture)414 void TabsEventRouter::DispatchEvent(
415 Profile* profile,
416 const std::string& event_name,
417 scoped_ptr<base::ListValue> args,
418 EventRouter::UserGestureState user_gesture) {
419 EventRouter* event_router = EventRouter::Get(profile);
420 if (!profile_->IsSameProfile(profile) || !event_router)
421 return;
422
423 scoped_ptr<Event> event(new Event(event_name, args.Pass()));
424 event->restrict_to_browser_context = profile;
425 event->user_gesture = user_gesture;
426 event_router->BroadcastEvent(event.Pass());
427 }
428
DispatchSimpleBrowserEvent(Profile * profile,const int window_id,const std::string & event_name)429 void TabsEventRouter::DispatchSimpleBrowserEvent(
430 Profile* profile, const int window_id, const std::string& event_name) {
431 if (!profile_->IsSameProfile(profile))
432 return;
433
434 scoped_ptr<base::ListValue> args(new base::ListValue);
435 args->Append(new FundamentalValue(window_id));
436
437 DispatchEvent(profile,
438 event_name,
439 args.Pass(),
440 EventRouter::USER_GESTURE_UNKNOWN);
441 }
442
DispatchTabUpdatedEvent(WebContents * contents,scoped_ptr<base::DictionaryValue> changed_properties)443 void TabsEventRouter::DispatchTabUpdatedEvent(
444 WebContents* contents,
445 scoped_ptr<base::DictionaryValue> changed_properties) {
446 DCHECK(changed_properties);
447 DCHECK(contents);
448
449 // The state of the tab (as seen from the extension point of view) has
450 // changed. Send a notification to the extension.
451 scoped_ptr<base::ListValue> args_base(new base::ListValue);
452
453 // First arg: The id of the tab that changed.
454 args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents));
455
456 // Second arg: An object containing the changes to the tab state. Filled in
457 // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the
458 // extension has the tabs permission.
459
460 // Third arg: An object containing the state of the tab. Filled in by
461 // WillDispatchTabUpdatedEvent.
462 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
463
464 scoped_ptr<Event> event(
465 new Event(tabs::OnUpdated::kEventName, args_base.Pass()));
466 event->restrict_to_browser_context = profile;
467 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
468 event->will_dispatch_callback =
469 base::Bind(&WillDispatchTabUpdatedEvent,
470 contents,
471 changed_properties.get());
472 EventRouter::Get(profile)->BroadcastEvent(event.Pass());
473 }
474
GetTabEntry(WebContents * contents)475 TabsEventRouter::TabEntry* TabsEventRouter::GetTabEntry(WebContents* contents) {
476 int tab_id = ExtensionTabUtil::GetTabId(contents);
477 std::map<int, TabEntry>::iterator i = tab_entries_.find(tab_id);
478 if (tab_entries_.end() == i)
479 return NULL;
480 return &i->second;
481 }
482
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)483 void TabsEventRouter::Observe(int type,
484 const content::NotificationSource& source,
485 const content::NotificationDetails& details) {
486 if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) {
487 NavigationController* source_controller =
488 content::Source<NavigationController>(source).ptr();
489 TabUpdated(source_controller->GetWebContents(), true);
490 } else if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) {
491 // Tab was destroyed after being detached (without being re-attached).
492 WebContents* contents = content::Source<WebContents>(source).ptr();
493 registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
494 content::Source<NavigationController>(&contents->GetController()));
495 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
496 content::Source<WebContents>(contents));
497 registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED,
498 content::Source<WebContents>(contents));
499 } else if (type == chrome::NOTIFICATION_FAVICON_UPDATED) {
500 bool icon_url_changed = *content::Details<bool>(details).ptr();
501 if (icon_url_changed)
502 FaviconUrlUpdated(content::Source<WebContents>(source).ptr());
503 } else {
504 NOTREACHED();
505 }
506 }
507
TabChangedAt(WebContents * contents,int index,TabChangeType change_type)508 void TabsEventRouter::TabChangedAt(WebContents* contents,
509 int index,
510 TabChangeType change_type) {
511 TabUpdated(contents, false);
512 }
513
TabReplacedAt(TabStripModel * tab_strip_model,WebContents * old_contents,WebContents * new_contents,int index)514 void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model,
515 WebContents* old_contents,
516 WebContents* new_contents,
517 int index) {
518 // Notify listeners that the next tabs closing or being added are due to
519 // WebContents being swapped.
520 const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents);
521 const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents);
522 scoped_ptr<base::ListValue> args(new base::ListValue);
523 args->Append(new FundamentalValue(new_tab_id));
524 args->Append(new FundamentalValue(old_tab_id));
525
526 DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()),
527 tabs::OnReplaced::kEventName,
528 args.Pass(),
529 EventRouter::USER_GESTURE_UNKNOWN);
530
531 // Update tab_entries_.
532 const int removed_count = tab_entries_.erase(old_tab_id);
533 DCHECK_GT(removed_count, 0);
534 UnregisterForTabNotifications(old_contents);
535
536 if (!GetTabEntry(new_contents)) {
537 tab_entries_[new_tab_id] = TabEntry();
538 RegisterForTabNotifications(new_contents);
539 }
540 }
541
TabPinnedStateChanged(WebContents * contents,int index)542 void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) {
543 TabStripModel* tab_strip = NULL;
544 int tab_index;
545
546 if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) {
547 scoped_ptr<base::DictionaryValue> changed_properties(
548 new base::DictionaryValue());
549 changed_properties->SetBoolean(tabs_constants::kPinnedKey,
550 tab_strip->IsTabPinned(tab_index));
551 DispatchTabUpdatedEvent(contents, changed_properties.Pass());
552 }
553 }
554
555 } // namespace extensions
556