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 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 59 TabsEventRouter::TabEntry::TabEntry() : complete_waiting_on_load_(false), 60 url_() { 61 } 62 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 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 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 ZoomController::FromWebContents(contents)->AddObserver(this); 153 } 154 155 void TabsEventRouter::UnregisterForTabNotifications(WebContents* contents) { 156 registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, 157 content::Source<NavigationController>(&contents->GetController())); 158 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 159 content::Source<WebContents>(contents)); 160 registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED, 161 content::Source<WebContents>(contents)); 162 163 ZoomController::FromWebContents(contents)->RemoveObserver(this); 164 } 165 166 void TabsEventRouter::OnBrowserRemoved(Browser* browser) { 167 if (!profile_->IsSameProfile(browser->profile())) 168 return; 169 170 // Stop listening to TabStripModel events for this browser. 171 browser->tab_strip_model()->RemoveObserver(this); 172 } 173 174 void TabsEventRouter::OnBrowserSetLastActive(Browser* browser) { 175 TabsWindowsAPI* tabs_window_api = TabsWindowsAPI::Get(profile_); 176 if (tabs_window_api) { 177 tabs_window_api->windows_event_router()->OnActiveWindowChanged( 178 browser ? browser->extension_window_controller() : NULL); 179 } 180 } 181 182 static void WillDispatchTabCreatedEvent(WebContents* contents, 183 bool active, 184 content::BrowserContext* context, 185 const Extension* extension, 186 base::ListValue* event_args) { 187 base::DictionaryValue* tab_value = ExtensionTabUtil::CreateTabValue( 188 contents, extension); 189 event_args->Clear(); 190 event_args->Append(tab_value); 191 tab_value->SetBoolean(tabs_constants::kSelectedKey, active); 192 } 193 194 void TabsEventRouter::TabCreatedAt(WebContents* contents, 195 int index, 196 bool active) { 197 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 198 scoped_ptr<base::ListValue> args(new base::ListValue); 199 scoped_ptr<Event> event(new Event(tabs::OnCreated::kEventName, args.Pass())); 200 event->restrict_to_browser_context = profile; 201 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED; 202 event->will_dispatch_callback = 203 base::Bind(&WillDispatchTabCreatedEvent, contents, active); 204 EventRouter::Get(profile)->BroadcastEvent(event.Pass()); 205 206 RegisterForTabNotifications(contents); 207 } 208 209 void TabsEventRouter::TabInsertedAt(WebContents* contents, 210 int index, 211 bool active) { 212 // If tab is new, send created event. 213 int tab_id = ExtensionTabUtil::GetTabId(contents); 214 if (!GetTabEntry(contents)) { 215 tab_entries_[tab_id] = TabEntry(); 216 217 TabCreatedAt(contents, index, active); 218 return; 219 } 220 221 scoped_ptr<base::ListValue> args(new base::ListValue); 222 args->Append(new FundamentalValue(tab_id)); 223 224 base::DictionaryValue* object_args = new base::DictionaryValue(); 225 object_args->Set(tabs_constants::kNewWindowIdKey, 226 new FundamentalValue( 227 ExtensionTabUtil::GetWindowIdOfTab(contents))); 228 object_args->Set(tabs_constants::kNewPositionKey, 229 new FundamentalValue(index)); 230 args->Append(object_args); 231 232 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 233 DispatchEvent(profile, tabs::OnAttached::kEventName, args.Pass(), 234 EventRouter::USER_GESTURE_UNKNOWN); 235 } 236 237 void TabsEventRouter::TabDetachedAt(WebContents* contents, int index) { 238 if (!GetTabEntry(contents)) { 239 // The tab was removed. Don't send detach event. 240 return; 241 } 242 243 scoped_ptr<base::ListValue> args(new base::ListValue); 244 args->Append( 245 new FundamentalValue(ExtensionTabUtil::GetTabId(contents))); 246 247 base::DictionaryValue* object_args = new base::DictionaryValue(); 248 object_args->Set(tabs_constants::kOldWindowIdKey, 249 new FundamentalValue( 250 ExtensionTabUtil::GetWindowIdOfTab(contents))); 251 object_args->Set(tabs_constants::kOldPositionKey, 252 new FundamentalValue(index)); 253 args->Append(object_args); 254 255 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 256 DispatchEvent(profile, 257 tabs::OnDetached::kEventName, 258 args.Pass(), 259 EventRouter::USER_GESTURE_UNKNOWN); 260 } 261 262 void TabsEventRouter::TabClosingAt(TabStripModel* tab_strip_model, 263 WebContents* contents, 264 int index) { 265 int tab_id = ExtensionTabUtil::GetTabId(contents); 266 267 scoped_ptr<base::ListValue> args(new base::ListValue); 268 args->Append(new FundamentalValue(tab_id)); 269 270 base::DictionaryValue* object_args = new base::DictionaryValue(); 271 object_args->SetInteger(tabs_constants::kWindowIdKey, 272 ExtensionTabUtil::GetWindowIdOfTab(contents)); 273 object_args->SetBoolean(tabs_constants::kWindowClosing, 274 tab_strip_model->closing_all()); 275 args->Append(object_args); 276 277 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 278 DispatchEvent(profile, 279 tabs::OnRemoved::kEventName, 280 args.Pass(), 281 EventRouter::USER_GESTURE_UNKNOWN); 282 283 int removed_count = tab_entries_.erase(tab_id); 284 DCHECK_GT(removed_count, 0); 285 286 UnregisterForTabNotifications(contents); 287 } 288 289 void TabsEventRouter::ActiveTabChanged(WebContents* old_contents, 290 WebContents* new_contents, 291 int index, 292 int reason) { 293 scoped_ptr<base::ListValue> args(new base::ListValue); 294 int tab_id = ExtensionTabUtil::GetTabId(new_contents); 295 args->Append(new FundamentalValue(tab_id)); 296 297 base::DictionaryValue* object_args = new base::DictionaryValue(); 298 object_args->Set(tabs_constants::kWindowIdKey, 299 new FundamentalValue( 300 ExtensionTabUtil::GetWindowIdOfTab(new_contents))); 301 args->Append(object_args); 302 303 // The onActivated event replaced onActiveChanged and onSelectionChanged. The 304 // deprecated events take two arguments: tabId, {windowId}. 305 Profile* profile = 306 Profile::FromBrowserContext(new_contents->GetBrowserContext()); 307 EventRouter::UserGestureState gesture = 308 reason & CHANGE_REASON_USER_GESTURE 309 ? EventRouter::USER_GESTURE_ENABLED 310 : EventRouter::USER_GESTURE_NOT_ENABLED; 311 DispatchEvent(profile, 312 tabs::OnSelectionChanged::kEventName, 313 scoped_ptr<base::ListValue>(args->DeepCopy()), 314 gesture); 315 DispatchEvent(profile, 316 tabs::OnActiveChanged::kEventName, 317 scoped_ptr<base::ListValue>(args->DeepCopy()), 318 gesture); 319 320 // The onActivated event takes one argument: {windowId, tabId}. 321 args->Remove(0, NULL); 322 object_args->Set(tabs_constants::kTabIdKey, 323 new FundamentalValue(tab_id)); 324 DispatchEvent(profile, tabs::OnActivated::kEventName, args.Pass(), gesture); 325 } 326 327 void TabsEventRouter::TabSelectionChanged( 328 TabStripModel* tab_strip_model, 329 const ui::ListSelectionModel& old_model) { 330 ui::ListSelectionModel::SelectedIndices new_selection = 331 tab_strip_model->selection_model().selected_indices(); 332 scoped_ptr<base::ListValue> all_tabs(new base::ListValue); 333 334 for (size_t i = 0; i < new_selection.size(); ++i) { 335 int index = new_selection[i]; 336 WebContents* contents = tab_strip_model->GetWebContentsAt(index); 337 if (!contents) 338 break; 339 int tab_id = ExtensionTabUtil::GetTabId(contents); 340 all_tabs->Append(new FundamentalValue(tab_id)); 341 } 342 343 scoped_ptr<base::ListValue> args(new base::ListValue); 344 scoped_ptr<base::DictionaryValue> select_info(new base::DictionaryValue); 345 346 select_info->Set( 347 tabs_constants::kWindowIdKey, 348 new FundamentalValue( 349 ExtensionTabUtil::GetWindowIdOfTabStripModel(tab_strip_model))); 350 351 select_info->Set(tabs_constants::kTabIdsKey, all_tabs.release()); 352 args->Append(select_info.release()); 353 354 // The onHighlighted event replaced onHighlightChanged. 355 Profile* profile = tab_strip_model->profile(); 356 DispatchEvent(profile, 357 tabs::OnHighlightChanged::kEventName, 358 scoped_ptr<base::ListValue>(args->DeepCopy()), 359 EventRouter::USER_GESTURE_UNKNOWN); 360 DispatchEvent(profile, 361 tabs::OnHighlighted::kEventName, 362 args.Pass(), 363 EventRouter::USER_GESTURE_UNKNOWN); 364 } 365 366 void TabsEventRouter::TabMoved(WebContents* contents, 367 int from_index, 368 int to_index) { 369 scoped_ptr<base::ListValue> args(new base::ListValue); 370 args->Append( 371 new FundamentalValue(ExtensionTabUtil::GetTabId(contents))); 372 373 base::DictionaryValue* object_args = new base::DictionaryValue(); 374 object_args->Set(tabs_constants::kWindowIdKey, 375 new FundamentalValue( 376 ExtensionTabUtil::GetWindowIdOfTab(contents))); 377 object_args->Set(tabs_constants::kFromIndexKey, 378 new FundamentalValue(from_index)); 379 object_args->Set(tabs_constants::kToIndexKey, 380 new FundamentalValue(to_index)); 381 args->Append(object_args); 382 383 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 384 DispatchEvent(profile, 385 tabs::OnMoved::kEventName, 386 args.Pass(), 387 EventRouter::USER_GESTURE_UNKNOWN); 388 } 389 390 void TabsEventRouter::TabUpdated(WebContents* contents, bool did_navigate) { 391 TabEntry* entry = GetTabEntry(contents); 392 scoped_ptr<base::DictionaryValue> changed_properties; 393 394 CHECK(entry); 395 396 if (did_navigate) 397 changed_properties.reset(entry->DidNavigate(contents)); 398 else 399 changed_properties.reset(entry->UpdateLoadState(contents)); 400 401 if (changed_properties) 402 DispatchTabUpdatedEvent(contents, changed_properties.Pass()); 403 } 404 405 void TabsEventRouter::FaviconUrlUpdated(WebContents* contents) { 406 content::NavigationEntry* entry = 407 contents->GetController().GetVisibleEntry(); 408 if (!entry || !entry->GetFavicon().valid) 409 return; 410 scoped_ptr<base::DictionaryValue> changed_properties( 411 new base::DictionaryValue); 412 changed_properties->SetString( 413 tabs_constants::kFaviconUrlKey, 414 entry->GetFavicon().url.possibly_invalid_spec()); 415 DispatchTabUpdatedEvent(contents, changed_properties.Pass()); 416 } 417 418 void TabsEventRouter::DispatchEvent( 419 Profile* profile, 420 const std::string& event_name, 421 scoped_ptr<base::ListValue> args, 422 EventRouter::UserGestureState user_gesture) { 423 EventRouter* event_router = EventRouter::Get(profile); 424 if (!profile_->IsSameProfile(profile) || !event_router) 425 return; 426 427 scoped_ptr<Event> event(new Event(event_name, args.Pass())); 428 event->restrict_to_browser_context = profile; 429 event->user_gesture = user_gesture; 430 event_router->BroadcastEvent(event.Pass()); 431 } 432 433 void TabsEventRouter::DispatchSimpleBrowserEvent( 434 Profile* profile, const int window_id, const std::string& event_name) { 435 if (!profile_->IsSameProfile(profile)) 436 return; 437 438 scoped_ptr<base::ListValue> args(new base::ListValue); 439 args->Append(new FundamentalValue(window_id)); 440 441 DispatchEvent(profile, 442 event_name, 443 args.Pass(), 444 EventRouter::USER_GESTURE_UNKNOWN); 445 } 446 447 void TabsEventRouter::DispatchTabUpdatedEvent( 448 WebContents* contents, 449 scoped_ptr<base::DictionaryValue> changed_properties) { 450 DCHECK(changed_properties); 451 DCHECK(contents); 452 453 // The state of the tab (as seen from the extension point of view) has 454 // changed. Send a notification to the extension. 455 scoped_ptr<base::ListValue> args_base(new base::ListValue); 456 457 // First arg: The id of the tab that changed. 458 args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents)); 459 460 // Second arg: An object containing the changes to the tab state. Filled in 461 // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the 462 // extension has the tabs permission. 463 464 // Third arg: An object containing the state of the tab. Filled in by 465 // WillDispatchTabUpdatedEvent. 466 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 467 468 scoped_ptr<Event> event( 469 new Event(tabs::OnUpdated::kEventName, args_base.Pass())); 470 event->restrict_to_browser_context = profile; 471 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED; 472 event->will_dispatch_callback = 473 base::Bind(&WillDispatchTabUpdatedEvent, 474 contents, 475 changed_properties.get()); 476 EventRouter::Get(profile)->BroadcastEvent(event.Pass()); 477 } 478 479 TabsEventRouter::TabEntry* TabsEventRouter::GetTabEntry(WebContents* contents) { 480 int tab_id = ExtensionTabUtil::GetTabId(contents); 481 std::map<int, TabEntry>::iterator i = tab_entries_.find(tab_id); 482 if (tab_entries_.end() == i) 483 return NULL; 484 return &i->second; 485 } 486 487 void TabsEventRouter::Observe(int type, 488 const content::NotificationSource& source, 489 const content::NotificationDetails& details) { 490 if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) { 491 NavigationController* source_controller = 492 content::Source<NavigationController>(source).ptr(); 493 TabUpdated(source_controller->GetWebContents(), true); 494 } else if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) { 495 // Tab was destroyed after being detached (without being re-attached). 496 WebContents* contents = content::Source<WebContents>(source).ptr(); 497 registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, 498 content::Source<NavigationController>(&contents->GetController())); 499 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 500 content::Source<WebContents>(contents)); 501 registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED, 502 content::Source<WebContents>(contents)); 503 } else if (type == chrome::NOTIFICATION_FAVICON_UPDATED) { 504 bool icon_url_changed = *content::Details<bool>(details).ptr(); 505 if (icon_url_changed) 506 FaviconUrlUpdated(content::Source<WebContents>(source).ptr()); 507 } else { 508 NOTREACHED(); 509 } 510 } 511 512 void TabsEventRouter::TabChangedAt(WebContents* contents, 513 int index, 514 TabChangeType change_type) { 515 TabUpdated(contents, false); 516 } 517 518 void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model, 519 WebContents* old_contents, 520 WebContents* new_contents, 521 int index) { 522 // Notify listeners that the next tabs closing or being added are due to 523 // WebContents being swapped. 524 const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents); 525 const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents); 526 scoped_ptr<base::ListValue> args(new base::ListValue); 527 args->Append(new FundamentalValue(new_tab_id)); 528 args->Append(new FundamentalValue(old_tab_id)); 529 530 DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()), 531 tabs::OnReplaced::kEventName, 532 args.Pass(), 533 EventRouter::USER_GESTURE_UNKNOWN); 534 535 // Update tab_entries_. 536 const int removed_count = tab_entries_.erase(old_tab_id); 537 DCHECK_GT(removed_count, 0); 538 UnregisterForTabNotifications(old_contents); 539 540 if (!GetTabEntry(new_contents)) { 541 tab_entries_[new_tab_id] = TabEntry(); 542 RegisterForTabNotifications(new_contents); 543 } 544 } 545 546 void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) { 547 TabStripModel* tab_strip = NULL; 548 int tab_index; 549 550 if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) { 551 scoped_ptr<base::DictionaryValue> changed_properties( 552 new base::DictionaryValue()); 553 changed_properties->SetBoolean(tabs_constants::kPinnedKey, 554 tab_strip->IsTabPinned(tab_index)); 555 DispatchTabUpdatedEvent(contents, changed_properties.Pass()); 556 } 557 } 558 559 void TabsEventRouter::OnZoomChanged( 560 const ZoomController::ZoomChangedEventData& data) { 561 DCHECK(data.web_contents); 562 int tab_id = ExtensionTabUtil::GetTabId(data.web_contents); 563 if (tab_id < 0) 564 return; 565 566 // Prepare the zoom change information. 567 api::tabs::OnZoomChange::ZoomChangeInfo zoom_change_info; 568 zoom_change_info.tab_id = tab_id; 569 zoom_change_info.old_zoom_factor = 570 content::ZoomLevelToZoomFactor(data.old_zoom_level); 571 zoom_change_info.new_zoom_factor = 572 content::ZoomLevelToZoomFactor(data.new_zoom_level); 573 ZoomModeToZoomSettings(data.zoom_mode, 574 &zoom_change_info.zoom_settings); 575 576 // Dispatch the |onZoomChange| event. 577 Profile* profile = Profile::FromBrowserContext( 578 data.web_contents->GetBrowserContext()); 579 DispatchEvent(profile, 580 tabs::OnZoomChange::kEventName, 581 api::tabs::OnZoomChange::Create(zoom_change_info), 582 EventRouter::USER_GESTURE_UNKNOWN); 583 } 584 585 } // namespace extensions 586