Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/extensions/api/tabs/tabs_event_router.h" | 5 #include "chrome/browser/extensions/api/tabs/tabs_event_router.h" |
| 6 | 6 |
| 7 #include "base/command_line.h" | |
| 7 #include "base/json/json_writer.h" | 8 #include "base/json/json_writer.h" |
| 8 #include "base/values.h" | 9 #include "base/values.h" |
| 9 #include "chrome/browser/chrome_notification_types.h" | 10 #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_constants.h" |
| 11 #include "chrome/browser/extensions/api/tabs/tabs_windows_api.h" | 12 #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/api/tabs/windows_event_router.h" |
| 13 #include "chrome/browser/extensions/extension_tab_util.h" | 14 #include "chrome/browser/extensions/extension_tab_util.h" |
| 14 #include "chrome/browser/profiles/profile.h" | 15 #include "chrome/browser/profiles/profile.h" |
| 15 #include "chrome/browser/ui/browser.h" | 16 #include "chrome/browser/ui/browser.h" |
| 16 #include "chrome/browser/ui/browser_iterator.h" | 17 #include "chrome/browser/ui/browser_iterator.h" |
| 17 #include "chrome/browser/ui/browser_list.h" | 18 #include "chrome/browser/ui/browser_list.h" |
| 18 #include "chrome/browser/ui/tabs/tab_strip_model.h" | 19 #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| 20 #include "chrome/browser/ui/tabs/tab_utils.h" | |
| 21 #include "chrome/common/chrome_switches.h" | |
| 19 #include "chrome/common/extensions/extension_constants.h" | 22 #include "chrome/common/extensions/extension_constants.h" |
| 20 #include "content/public/browser/favicon_status.h" | 23 #include "content/public/browser/favicon_status.h" |
| 21 #include "content/public/browser/navigation_controller.h" | 24 #include "content/public/browser/navigation_controller.h" |
| 22 #include "content/public/browser/navigation_entry.h" | 25 #include "content/public/browser/navigation_entry.h" |
| 23 #include "content/public/browser/notification_service.h" | 26 #include "content/public/browser/notification_service.h" |
| 24 #include "content/public/browser/notification_types.h" | 27 #include "content/public/browser/notification_types.h" |
| 25 #include "content/public/browser/web_contents.h" | 28 #include "content/public/browser/web_contents.h" |
| 26 | 29 |
| 27 using base::DictionaryValue; | 30 using base::DictionaryValue; |
| 28 using base::ListValue; | 31 using base::ListValue; |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 51 properties_value); | 54 properties_value); |
| 52 event_args->Set(1, properties_value); | 55 event_args->Set(1, properties_value); |
| 53 | 56 |
| 54 // Overwrite the third arg with our tab value as seen by this extension. | 57 // Overwrite the third arg with our tab value as seen by this extension. |
| 55 event_args->Set(2, ExtensionTabUtil::CreateTabValue(contents, extension)); | 58 event_args->Set(2, ExtensionTabUtil::CreateTabValue(contents, extension)); |
| 56 return true; | 59 return true; |
| 57 } | 60 } |
| 58 | 61 |
| 59 } // namespace | 62 } // namespace |
| 60 | 63 |
| 61 TabsEventRouter::TabEntry::TabEntry() : complete_waiting_on_load_(false), | 64 TabsEventRouter::TabEntry::TabEntry() |
| 62 url_() { | 65 : contents_(NULL), |
| 66 complete_waiting_on_load_(false), | |
| 67 was_audible_(false), | |
| 68 was_muted_(false) { | |
| 63 } | 69 } |
| 64 | 70 |
| 65 base::DictionaryValue* TabsEventRouter::TabEntry::UpdateLoadState( | 71 TabsEventRouter::TabEntry::TabEntry(content::WebContents* contents) |
| 66 const WebContents* contents) { | 72 : contents_(contents), |
| 73 complete_waiting_on_load_(false), | |
| 74 was_audible_(contents->WasRecentlyAudible()), | |
| 75 was_muted_(contents->IsAudioMuted()) { | |
| 76 } | |
| 77 | |
| 78 scoped_ptr<base::DictionaryValue> TabsEventRouter::TabEntry::UpdateLoadState() { | |
| 67 // The tab may go in & out of loading (for instance if iframes navigate). | 79 // The tab may go in & out of loading (for instance if iframes navigate). |
| 68 // We only want to respond to the first change from loading to !loading after | 80 // We only want to respond to the first change from loading to !loading after |
| 69 // the NAV_ENTRY_COMMITTED was fired. | 81 // the NAV_ENTRY_COMMITTED was fired. |
| 70 if (!complete_waiting_on_load_ || contents->IsLoading()) | 82 scoped_ptr<base::DictionaryValue> changed_properties( |
| 71 return NULL; | 83 new base::DictionaryValue()); |
| 84 if (!complete_waiting_on_load_ || contents_->IsLoading()) { | |
| 85 return changed_properties.Pass(); | |
| 86 } | |
| 72 | 87 |
| 73 // Send "complete" state change. | 88 // Send "complete" state change. |
| 74 complete_waiting_on_load_ = false; | 89 complete_waiting_on_load_ = false; |
| 75 base::DictionaryValue* changed_properties = new base::DictionaryValue(); | |
| 76 changed_properties->SetString(tabs_constants::kStatusKey, | 90 changed_properties->SetString(tabs_constants::kStatusKey, |
| 77 tabs_constants::kStatusValueComplete); | 91 tabs_constants::kStatusValueComplete); |
| 78 return changed_properties; | 92 return changed_properties.Pass(); |
| 79 } | 93 } |
| 80 | 94 |
| 81 base::DictionaryValue* TabsEventRouter::TabEntry::DidNavigate( | 95 scoped_ptr<base::DictionaryValue> TabsEventRouter::TabEntry::DidNavigate() { |
| 82 const WebContents* contents) { | |
| 83 // Send "loading" state change. | 96 // Send "loading" state change. |
| 84 complete_waiting_on_load_ = true; | 97 complete_waiting_on_load_ = true; |
| 85 base::DictionaryValue* changed_properties = new base::DictionaryValue(); | 98 scoped_ptr<base::DictionaryValue> changed_properties( |
| 99 new base::DictionaryValue()); | |
| 86 changed_properties->SetString(tabs_constants::kStatusKey, | 100 changed_properties->SetString(tabs_constants::kStatusKey, |
| 87 tabs_constants::kStatusValueLoading); | 101 tabs_constants::kStatusValueLoading); |
| 88 | 102 |
| 89 if (contents->GetURL() != url_) { | 103 if (contents_->GetURL() != url_) { |
| 90 url_ = contents->GetURL(); | 104 url_ = contents_->GetURL(); |
| 91 changed_properties->SetString(tabs_constants::kUrlKey, url_.spec()); | 105 changed_properties->SetString(tabs_constants::kUrlKey, url_.spec()); |
| 92 } | 106 } |
| 93 | 107 |
| 94 return changed_properties; | 108 return changed_properties.Pass(); |
| 109 } | |
| 110 | |
| 111 bool TabsEventRouter::TabEntry::SetAudible(bool new_val) { | |
| 112 if (was_audible_ == new_val) | |
| 113 return false; | |
| 114 was_audible_ = new_val; | |
| 115 return true; | |
| 116 } | |
| 117 | |
| 118 bool TabsEventRouter::TabEntry::SetMuted(bool new_val) { | |
| 119 if (was_muted_ == new_val) | |
| 120 return false; | |
| 121 was_muted_ = new_val; | |
| 122 return true; | |
| 95 } | 123 } |
| 96 | 124 |
| 97 TabsEventRouter::TabsEventRouter(Profile* profile) : profile_(profile) { | 125 TabsEventRouter::TabsEventRouter(Profile* profile) : profile_(profile) { |
| 98 DCHECK(!profile->IsOffTheRecord()); | 126 DCHECK(!profile->IsOffTheRecord()); |
| 99 | 127 |
| 100 BrowserList::AddObserver(this); | 128 BrowserList::AddObserver(this); |
| 101 | 129 |
| 102 // Init() can happen after the browser is running, so catch up with any | 130 // Init() can happen after the browser is running, so catch up with any |
| 103 // windows that already exist. | 131 // windows that already exist. |
| 104 for (chrome::BrowserIterator it; !it.done(); it.Next()) { | 132 for (chrome::BrowserIterator it; !it.done(); it.Next()) { |
| 105 RegisterForBrowserNotifications(*it); | 133 RegisterForBrowserNotifications(*it); |
| 106 | 134 |
| 107 // Also catch up our internal bookkeeping of tab entries. | 135 // Also catch up our internal bookkeeping of tab entries. |
| 108 Browser* browser = *it; | 136 Browser* browser = *it; |
| 109 if (browser->tab_strip_model()) { | 137 if (browser->tab_strip_model()) { |
| 110 for (int i = 0; i < browser->tab_strip_model()->count(); ++i) { | 138 for (int i = 0; i < browser->tab_strip_model()->count(); ++i) { |
| 111 WebContents* contents = browser->tab_strip_model()->GetWebContentsAt(i); | 139 WebContents* contents = browser->tab_strip_model()->GetWebContentsAt(i); |
| 112 int tab_id = ExtensionTabUtil::GetTabId(contents); | 140 int tab_id = ExtensionTabUtil::GetTabId(contents); |
| 113 tab_entries_[tab_id] = TabEntry(); | 141 tab_entries_[tab_id] = TabEntry(contents); |
| 114 } | 142 } |
| 115 } | 143 } |
| 116 } | 144 } |
| 117 } | 145 } |
| 118 | 146 |
| 119 TabsEventRouter::~TabsEventRouter() { | 147 TabsEventRouter::~TabsEventRouter() { |
| 120 BrowserList::RemoveObserver(this); | 148 BrowserList::RemoveObserver(this); |
| 121 } | 149 } |
| 122 | 150 |
| 123 void TabsEventRouter::OnBrowserAdded(Browser* browser) { | 151 void TabsEventRouter::OnBrowserAdded(Browser* browser) { |
| (...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 208 | 236 |
| 209 RegisterForTabNotifications(contents); | 237 RegisterForTabNotifications(contents); |
| 210 } | 238 } |
| 211 | 239 |
| 212 void TabsEventRouter::TabInsertedAt(WebContents* contents, | 240 void TabsEventRouter::TabInsertedAt(WebContents* contents, |
| 213 int index, | 241 int index, |
| 214 bool active) { | 242 bool active) { |
| 215 // If tab is new, send created event. | 243 // If tab is new, send created event. |
| 216 int tab_id = ExtensionTabUtil::GetTabId(contents); | 244 int tab_id = ExtensionTabUtil::GetTabId(contents); |
| 217 if (!GetTabEntry(contents)) { | 245 if (!GetTabEntry(contents)) { |
| 218 tab_entries_[tab_id] = TabEntry(); | 246 tab_entries_[tab_id] = TabEntry(contents); |
| 219 | 247 |
| 220 TabCreatedAt(contents, index, active); | 248 TabCreatedAt(contents, index, active); |
| 221 return; | 249 return; |
| 222 } | 250 } |
| 223 | 251 |
| 224 scoped_ptr<base::ListValue> args(new base::ListValue); | 252 scoped_ptr<base::ListValue> args(new base::ListValue); |
| 225 args->Append(new FundamentalValue(tab_id)); | 253 args->Append(new FundamentalValue(tab_id)); |
| 226 | 254 |
| 227 base::DictionaryValue* object_args = new base::DictionaryValue(); | 255 base::DictionaryValue* object_args = new base::DictionaryValue(); |
| 228 object_args->Set(tabs_constants::kNewWindowIdKey, | 256 object_args->Set(tabs_constants::kNewWindowIdKey, |
| (...skipping 154 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 383 new FundamentalValue(to_index)); | 411 new FundamentalValue(to_index)); |
| 384 args->Append(object_args); | 412 args->Append(object_args); |
| 385 | 413 |
| 386 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); | 414 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| 387 DispatchEvent(profile, | 415 DispatchEvent(profile, |
| 388 tabs::OnMoved::kEventName, | 416 tabs::OnMoved::kEventName, |
| 389 args.Pass(), | 417 args.Pass(), |
| 390 EventRouter::USER_GESTURE_UNKNOWN); | 418 EventRouter::USER_GESTURE_UNKNOWN); |
| 391 } | 419 } |
| 392 | 420 |
| 393 void TabsEventRouter::TabUpdated(WebContents* contents, bool did_navigate) { | 421 void TabsEventRouter::TabUpdated( |
| 394 TabEntry* entry = GetTabEntry(contents); | 422 TabsEventRouter::TabEntry* entry, |
| 395 scoped_ptr<base::DictionaryValue> changed_properties; | 423 scoped_ptr<base::DictionaryValue> changed_properties) { |
| 424 CHECK(entry->web_contents()); | |
| 396 | 425 |
| 397 CHECK(entry); | 426 const char* muteSwitch = switches::kEnableTabAudioMuting; |
|
not at google - send to devlin
2015/06/01 20:33:56
I don't think this alias is necessary, though for
Jared Sohn
2015/06/02 08:54:38
Oversight; removed.
On 2015/06/01 at 20:33:56, ka
| |
| 427 if (base::CommandLine::ForCurrentProcess()->HasSwitch(muteSwitch)) { | |
| 428 bool audible = entry->web_contents()->WasRecentlyAudible(); | |
| 429 if (entry->SetAudible(audible)) { | |
| 430 changed_properties->SetBoolean(tabs_constants::kAudibleKey, audible); | |
| 431 } | |
| 398 | 432 |
| 399 if (did_navigate) | 433 bool muted = entry->web_contents()->IsAudioMuted(); |
| 400 changed_properties.reset(entry->DidNavigate(contents)); | 434 if (entry->SetMuted(muted)) { |
| 401 else | 435 changed_properties->SetBoolean(tabs_constants::kMutedKey, muted); |
| 402 changed_properties.reset(entry->UpdateLoadState(contents)); | 436 changed_properties->SetString( |
| 437 tabs_constants::kMutedCauseKey, | |
| 438 chrome::GetTabAudioMutedCause(entry->web_contents())); | |
| 439 } | |
| 440 } | |
| 403 | 441 |
| 404 if (changed_properties) | 442 if (!changed_properties->empty()) { |
| 405 DispatchTabUpdatedEvent(contents, changed_properties.Pass()); | 443 DispatchTabUpdatedEvent(entry->web_contents(), changed_properties.Pass()); |
| 444 } | |
| 406 } | 445 } |
| 407 | 446 |
| 408 void TabsEventRouter::FaviconUrlUpdated(WebContents* contents) { | 447 void TabsEventRouter::FaviconUrlUpdated(WebContents* contents) { |
| 409 content::NavigationEntry* entry = | 448 content::NavigationEntry* entry = |
| 410 contents->GetController().GetVisibleEntry(); | 449 contents->GetController().GetVisibleEntry(); |
| 411 if (!entry || !entry->GetFavicon().valid) | 450 if (!entry || !entry->GetFavicon().valid) |
| 412 return; | 451 return; |
| 413 scoped_ptr<base::DictionaryValue> changed_properties( | 452 scoped_ptr<base::DictionaryValue> changed_properties( |
| 414 new base::DictionaryValue); | 453 new base::DictionaryValue); |
| 415 changed_properties->SetString( | 454 changed_properties->SetString( |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 486 return NULL; | 525 return NULL; |
| 487 return &i->second; | 526 return &i->second; |
| 488 } | 527 } |
| 489 | 528 |
| 490 void TabsEventRouter::Observe(int type, | 529 void TabsEventRouter::Observe(int type, |
| 491 const content::NotificationSource& source, | 530 const content::NotificationSource& source, |
| 492 const content::NotificationDetails& details) { | 531 const content::NotificationDetails& details) { |
| 493 if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) { | 532 if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) { |
| 494 NavigationController* source_controller = | 533 NavigationController* source_controller = |
| 495 content::Source<NavigationController>(source).ptr(); | 534 content::Source<NavigationController>(source).ptr(); |
| 496 TabUpdated(source_controller->GetWebContents(), true); | 535 TabEntry* entry = GetTabEntry(source_controller->GetWebContents()); |
| 536 CHECK(entry); | |
| 537 TabUpdated(entry, entry->DidNavigate()); | |
| 497 } else if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) { | 538 } else if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) { |
| 498 // Tab was destroyed after being detached (without being re-attached). | 539 // Tab was destroyed after being detached (without being re-attached). |
| 499 WebContents* contents = content::Source<WebContents>(source).ptr(); | 540 WebContents* contents = content::Source<WebContents>(source).ptr(); |
| 500 registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, | 541 registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, |
| 501 content::Source<NavigationController>(&contents->GetController())); | 542 content::Source<NavigationController>(&contents->GetController())); |
| 502 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, | 543 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, |
| 503 content::Source<WebContents>(contents)); | 544 content::Source<WebContents>(contents)); |
| 504 registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED, | 545 registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED, |
| 505 content::Source<WebContents>(contents)); | 546 content::Source<WebContents>(contents)); |
| 506 } else if (type == chrome::NOTIFICATION_FAVICON_UPDATED) { | 547 } else if (type == chrome::NOTIFICATION_FAVICON_UPDATED) { |
| 507 bool icon_url_changed = *content::Details<bool>(details).ptr(); | 548 bool icon_url_changed = *content::Details<bool>(details).ptr(); |
| 508 if (icon_url_changed) | 549 if (icon_url_changed) |
| 509 FaviconUrlUpdated(content::Source<WebContents>(source).ptr()); | 550 FaviconUrlUpdated(content::Source<WebContents>(source).ptr()); |
| 510 } else { | 551 } else { |
| 511 NOTREACHED(); | 552 NOTREACHED(); |
| 512 } | 553 } |
| 513 } | 554 } |
| 514 | 555 |
| 515 void TabsEventRouter::TabChangedAt(WebContents* contents, | 556 void TabsEventRouter::TabChangedAt(WebContents* contents, |
| 516 int index, | 557 int index, |
| 517 TabChangeType change_type) { | 558 TabChangeType change_type) { |
| 518 TabUpdated(contents, false); | 559 TabEntry* entry = GetTabEntry(contents); |
| 560 CHECK(entry); | |
| 561 TabUpdated(entry, entry->UpdateLoadState()); | |
| 519 } | 562 } |
| 520 | 563 |
| 521 void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model, | 564 void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model, |
| 522 WebContents* old_contents, | 565 WebContents* old_contents, |
| 523 WebContents* new_contents, | 566 WebContents* new_contents, |
| 524 int index) { | 567 int index) { |
| 525 // Notify listeners that the next tabs closing or being added are due to | 568 // Notify listeners that the next tabs closing or being added are due to |
| 526 // WebContents being swapped. | 569 // WebContents being swapped. |
| 527 const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents); | 570 const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents); |
| 528 const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents); | 571 const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents); |
| 529 scoped_ptr<base::ListValue> args(new base::ListValue); | 572 scoped_ptr<base::ListValue> args(new base::ListValue); |
| 530 args->Append(new FundamentalValue(new_tab_id)); | 573 args->Append(new FundamentalValue(new_tab_id)); |
| 531 args->Append(new FundamentalValue(old_tab_id)); | 574 args->Append(new FundamentalValue(old_tab_id)); |
| 532 | 575 |
| 533 DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()), | 576 DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()), |
| 534 tabs::OnReplaced::kEventName, | 577 tabs::OnReplaced::kEventName, |
| 535 args.Pass(), | 578 args.Pass(), |
| 536 EventRouter::USER_GESTURE_UNKNOWN); | 579 EventRouter::USER_GESTURE_UNKNOWN); |
| 537 | 580 |
| 538 // Update tab_entries_. | 581 // Update tab_entries_. |
| 539 const int removed_count = tab_entries_.erase(old_tab_id); | 582 const int removed_count = tab_entries_.erase(old_tab_id); |
| 540 DCHECK_GT(removed_count, 0); | 583 DCHECK_GT(removed_count, 0); |
| 541 UnregisterForTabNotifications(old_contents); | 584 UnregisterForTabNotifications(old_contents); |
| 542 | 585 |
| 543 if (!GetTabEntry(new_contents)) { | 586 if (!GetTabEntry(new_contents)) { |
| 544 tab_entries_[new_tab_id] = TabEntry(); | 587 tab_entries_[new_tab_id] = TabEntry(new_contents); |
| 545 RegisterForTabNotifications(new_contents); | 588 RegisterForTabNotifications(new_contents); |
| 546 } | 589 } |
| 547 } | 590 } |
| 548 | 591 |
| 549 void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) { | 592 void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) { |
| 550 TabStripModel* tab_strip = NULL; | 593 TabStripModel* tab_strip = NULL; |
| 551 int tab_index; | 594 int tab_index; |
| 552 | 595 |
| 553 if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) { | 596 if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) { |
| 554 scoped_ptr<base::DictionaryValue> changed_properties( | 597 scoped_ptr<base::DictionaryValue> changed_properties( |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 579 // Dispatch the |onZoomChange| event. | 622 // Dispatch the |onZoomChange| event. |
| 580 Profile* profile = Profile::FromBrowserContext( | 623 Profile* profile = Profile::FromBrowserContext( |
| 581 data.web_contents->GetBrowserContext()); | 624 data.web_contents->GetBrowserContext()); |
| 582 DispatchEvent(profile, | 625 DispatchEvent(profile, |
| 583 tabs::OnZoomChange::kEventName, | 626 tabs::OnZoomChange::kEventName, |
| 584 api::tabs::OnZoomChange::Create(zoom_change_info), | 627 api::tabs::OnZoomChange::Create(zoom_change_info), |
| 585 EventRouter::USER_GESTURE_UNKNOWN); | 628 EventRouter::USER_GESTURE_UNKNOWN); |
| 586 } | 629 } |
| 587 | 630 |
| 588 } // namespace extensions | 631 } // namespace extensions |
| OLD | NEW |