webbrowser.js (24367B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 var { 8 DevToolsServer, 9 } = require("resource://devtools/server/devtools-server.js"); 10 var { 11 ActorRegistry, 12 } = require("resource://devtools/server/actors/utils/actor-registry.js"); 13 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 14 15 loader.lazyRequireGetter( 16 this, 17 "RootActor", 18 "resource://devtools/server/actors/root.js", 19 true 20 ); 21 loader.lazyRequireGetter( 22 this, 23 "TabDescriptorActor", 24 "resource://devtools/server/actors/descriptors/tab.js", 25 true 26 ); 27 loader.lazyRequireGetter( 28 this, 29 "WebExtensionDescriptorActor", 30 "resource://devtools/server/actors/descriptors/webextension.js", 31 true 32 ); 33 loader.lazyRequireGetter( 34 this, 35 "WorkerDescriptorActorList", 36 "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js", 37 true 38 ); 39 loader.lazyRequireGetter( 40 this, 41 "ServiceWorkerRegistrationActorList", 42 "resource://devtools/server/actors/worker/service-worker-registration-list.js", 43 true 44 ); 45 loader.lazyRequireGetter( 46 this, 47 "ProcessActorList", 48 "resource://devtools/server/actors/process.js", 49 true 50 ); 51 const lazy = {}; 52 loader.lazyGetter(lazy, "AddonManager", () => { 53 return ChromeUtils.importESModule( 54 "resource://gre/modules/AddonManager.sys.mjs", 55 { global: "shared" } 56 ).AddonManager; 57 }); 58 59 /** 60 * Browser-specific actors. 61 */ 62 63 /** 64 * Retrieve the window type of the top-level window |window|. 65 */ 66 function appShellDOMWindowType(window) { 67 /* This is what nsIWindowMediator's enumerator checks. */ 68 return window.document.documentElement.getAttribute("windowtype"); 69 } 70 71 /** 72 * Send Debugger:Shutdown events to all "navigator:browser" windows. 73 */ 74 function sendShutdownEvent() { 75 for (const win of Services.wm.getEnumerator( 76 DevToolsServer.chromeWindowType 77 )) { 78 const evt = win.document.createEvent("Event"); 79 evt.initEvent("Debugger:Shutdown", true, false); 80 win.document.documentElement.dispatchEvent(evt); 81 } 82 } 83 84 exports.sendShutdownEvent = sendShutdownEvent; 85 86 /** 87 * Construct a root actor appropriate for use in a server running in a 88 * browser. The returned root actor: 89 * - respects the factories registered with ActorRegistry.addGlobalActor, 90 * - uses a BrowserTabList to supply target actors for tabs, 91 * - sends all navigator:browser window documents a Debugger:Shutdown event 92 * when it exits. 93 * 94 * * @param connection DevToolsServerConnection 95 * The conection to the client. 96 */ 97 exports.createRootActor = function createRootActor(connection) { 98 return new RootActor(connection, { 99 tabList: new BrowserTabList(connection), 100 addonList: new BrowserAddonList(connection), 101 workerList: new WorkerDescriptorActorList(connection, {}), 102 serviceWorkerRegistrationList: new ServiceWorkerRegistrationActorList( 103 connection 104 ), 105 processList: new ProcessActorList(), 106 globalActorFactories: ActorRegistry.globalActorFactories, 107 onShutdown: sendShutdownEvent, 108 }); 109 }; 110 111 /** 112 * A live list of TabDescriptorActors representing the current browser tabs, 113 * to be provided to the root actor to answer 'listTabs' requests. 114 * 115 * This object also takes care of listening for TabClose events and 116 * onCloseWindow notifications, and exiting the target actors concerned. 117 * 118 * (See the documentation for RootActor for the definition of the "live 119 * list" interface.) 120 * 121 * @param connection DevToolsServerConnection 122 * The connection in which this list's target actors may participate. 123 * 124 * Some notes: 125 * 126 * This constructor is specific to the desktop browser environment; it 127 * maintains the tab list by tracking XUL windows and their XUL documents' 128 * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining 129 * an accurate list of open tabs in this context? 130 * 131 * - Opening and closing XUL windows: 132 * 133 * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop 134 * windows) are opened and closed. It is not notified of individual content 135 * browser tabs coming and going within such a XUL window. That seems 136 * reasonable enough; it's concerned with XUL windows, not tab elements in the 137 * window's XUL document. 138 * 139 * However, even if we attach TabOpen and TabClose event listeners to each XUL 140 * window as soon as it is created: 141 * 142 * - we do not receive a TabOpen event for the initial empty tab of a new XUL 143 * window; and 144 * 145 * - we do not receive TabClose events for the tabs of a XUL window that has 146 * been closed. 147 * 148 * This means that TabOpen and TabClose events alone are not sufficient to 149 * maintain an accurate list of live tabs and mark target actors as closed 150 * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and 151 * exit all actors for tabs that were in the closing window. 152 * 153 * Since this is a bit hairy, we don't make each individual attached target 154 * actor responsible for noticing when it has been closed; we watch for that, 155 * and promise to call each actor's 'exit' method when it's closed, regardless 156 * of how we learn the news. 157 * 158 * - nsIWindowMediator locks 159 * 160 * nsIWindowMediator holds a lock protecting its list of top-level windows 161 * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's 162 * GetEnumerator method also tries to acquire that lock. Thus, enumerating 163 * windows from within a listener method deadlocks (bug 873589). Rah. One 164 * can sometimes work around this by leaving the enumeration for a later 165 * tick. 166 * 167 * - Dragging tabs between windows: 168 * 169 * When a tab is dragged from one desktop window to another, we receive a 170 * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL 171 * elements do not really move from one document to the other (although their 172 * linked browser's content window objects do). 173 * 174 * However, while we could thus assume that each tab stays with the XUL window 175 * it belonged to when it was created, I'm not sure this is behavior one should 176 * rely upon. When a XUL window is closed, we take the less efficient, more 177 * conservative approach of simply searching the entire table for actors that 178 * belong to the closing XUL window, rather than trying to somehow track which 179 * XUL window each tab belongs to. 180 */ 181 function BrowserTabList(connection) { 182 this._connection = connection; 183 184 /* 185 * The XUL document of a tabbed browser window has "tab" elements, whose 186 * 'linkedBrowser' JavaScript properties are "browser" elements; those 187 * browsers' 'contentWindow' properties are wrappers on the tabs' content 188 * window objects. 189 * 190 * This map's keys are "browser" XUL elements; it maps each browser element 191 * to the target actor we've created for its content window, if we've created 192 * one. This map serves several roles: 193 * 194 * - During iteration, we use it to find actors we've created previously. 195 * 196 * - On a TabClose event, we use it to find the tab's target actor and exit it. 197 * 198 * - When the onCloseWindow handler is called, we iterate over it to find all 199 * tabs belonging to the closing XUL window, and exit them. 200 * 201 * - When it's empty, and the onListChanged hook is null, we know we can 202 * stop listening for events and notifications. 203 * 204 * We listen for TabClose events and onCloseWindow notifications in order to 205 * send onListChanged notifications, but also to tell actors when their 206 * referent has gone away and remove entries for dead browsers from this map. 207 * If that code is working properly, neither this map nor the actors in it 208 * should ever hold dead tabs alive. 209 */ 210 this._actorByBrowser = new Map(); 211 212 /* The current onListChanged handler, or null. */ 213 this._onListChanged = null; 214 215 /* 216 * True if we've been iterated over since we last called our onListChanged 217 * hook. 218 */ 219 this._mustNotify = false; 220 221 /* True if we're testing, and should throw if consistency checks fail. */ 222 this._testing = false; 223 224 this._onPageTitleChangedEvent = this._onPageTitleChangedEvent.bind(this); 225 } 226 227 BrowserTabList.prototype.constructor = BrowserTabList; 228 229 BrowserTabList.prototype.destroy = function () { 230 this._actorByBrowser.clear(); 231 this.onListChanged = null; 232 }; 233 234 /** 235 * Get the selected browser for the given navigator:browser window. 236 * 237 * @private 238 * @param window nsIChromeWindow 239 * The navigator:browser window for which you want the selected browser. 240 * @return Element|null 241 * The currently selected xul:browser element, if any. Note that the 242 * browser window might not be loaded yet - the function will return 243 * |null| in such cases. 244 */ 245 BrowserTabList.prototype._getSelectedBrowser = function (window) { 246 return window.gBrowser ? window.gBrowser.selectedBrowser : null; 247 }; 248 249 /** 250 * Produces an iterable (in this case a generator) to enumerate all available 251 * browser tabs. 252 */ 253 BrowserTabList.prototype._getBrowsers = function* () { 254 // Iterate over all navigator:browser XUL windows. 255 for (const win of Services.wm.getEnumerator( 256 DevToolsServer.chromeWindowType 257 )) { 258 // For each tab in this XUL window, ensure that we have an actor for 259 // it, reusing existing actors where possible. 260 for (const browser of this._getChildren(win)) { 261 yield browser; 262 } 263 } 264 }; 265 266 BrowserTabList.prototype._getChildren = function (window) { 267 if (!window.gBrowser) { 268 return []; 269 } 270 const { gBrowser } = window; 271 if (!gBrowser.browsers) { 272 return []; 273 } 274 return gBrowser.browsers.filter(browser => { 275 // Filter tabs that are closing. listTabs calls made right after TabClose 276 // events still list tabs in process of being closed. 277 const tab = gBrowser.getTabForBrowser(browser); 278 return !tab.closing; 279 }); 280 }; 281 282 BrowserTabList.prototype.getList = async function () { 283 // As a sanity check, make sure all the actors presently in our map get 284 // picked up when we iterate over all windows' tabs. 285 const initialMapSize = this._actorByBrowser.size; 286 this._foundCount = 0; 287 288 const actors = []; 289 290 for (const browser of this._getBrowsers()) { 291 try { 292 const actor = await this._getActorForBrowser(browser); 293 actors.push(actor); 294 } catch (e) { 295 if (e.error === "tabDestroyed") { 296 // Ignore the error if a tab was destroyed while retrieving the tab list. 297 continue; 298 } 299 300 // Forward unexpected errors. 301 throw e; 302 } 303 } 304 305 if (this._testing && initialMapSize !== this._foundCount) { 306 throw new Error("_actorByBrowser map contained actors for dead tabs"); 307 } 308 309 this._mustNotify = true; 310 this._checkListening(); 311 312 return actors; 313 }; 314 315 BrowserTabList.prototype._getActorForBrowser = async function (browser) { 316 // Do we have an existing actor for this browser? If not, create one. 317 let actor = this._actorByBrowser.get(browser); 318 if (actor) { 319 this._foundCount++; 320 return actor; 321 } 322 323 actor = new TabDescriptorActor(this._connection, browser); 324 this._actorByBrowser.set(browser, actor); 325 this._checkListening(); 326 return actor; 327 }; 328 329 /** 330 * Return the tab descriptor : 331 * - for the tab matching a browserId if one is passed 332 * - OR the currently selected tab if no browserId is passed. 333 * 334 * @param {number} browserId: use to match any tab 335 */ 336 BrowserTabList.prototype.getTab = function ({ browserId }) { 337 if (typeof browserId == "number") { 338 const browsingContext = BrowsingContext.getCurrentTopByBrowserId(browserId); 339 if (!browsingContext) { 340 return Promise.reject({ 341 error: "noTab", 342 message: `Unable to find tab with browserId '${browserId}' (no browsing-context)`, 343 }); 344 } 345 const browser = browsingContext.embedderElement; 346 if (!browser) { 347 return Promise.reject({ 348 error: "noTab", 349 message: `Unable to find tab with browserId '${browserId}' (no embedder element)`, 350 }); 351 } 352 return this._getActorForBrowser(browser); 353 } 354 355 const topAppWindow = Services.wm.getMostRecentWindow( 356 DevToolsServer.chromeWindowType 357 ); 358 if (topAppWindow) { 359 const selectedBrowser = this._getSelectedBrowser(topAppWindow); 360 return this._getActorForBrowser(selectedBrowser); 361 } 362 return Promise.reject({ 363 error: "noTab", 364 message: "Unable to find any selected browser", 365 }); 366 }; 367 368 Object.defineProperty(BrowserTabList.prototype, "onListChanged", { 369 enumerable: true, 370 configurable: true, 371 get() { 372 return this._onListChanged; 373 }, 374 set(v) { 375 if (v !== null && typeof v !== "function") { 376 throw new Error( 377 "onListChanged property may only be set to 'null' or a function" 378 ); 379 } 380 this._onListChanged = v; 381 this._checkListening(); 382 }, 383 }); 384 385 /** 386 * The set of tabs has changed somehow. Call our onListChanged handler, if 387 * one is set, and if we haven't already called it since the last iteration. 388 */ 389 BrowserTabList.prototype._notifyListChanged = function () { 390 if (!this._onListChanged) { 391 return; 392 } 393 if (this._mustNotify) { 394 this._onListChanged(); 395 this._mustNotify = false; 396 } 397 }; 398 399 /** 400 * Exit |actor|, belonging to |browser|, and notify the onListChanged 401 * handle if needed. 402 */ 403 BrowserTabList.prototype._handleActorClose = function (actor, browser) { 404 if (this._testing) { 405 if (this._actorByBrowser.get(browser) !== actor) { 406 throw new Error( 407 "TabDescriptorActor not stored in map under given browser" 408 ); 409 } 410 if (actor.browser !== browser) { 411 throw new Error("actor's browser and map key don't match"); 412 } 413 } 414 415 this._actorByBrowser.delete(browser); 416 actor.destroy(); 417 418 this._notifyListChanged(); 419 this._checkListening(); 420 }; 421 422 /** 423 * Make sure we are listening or not listening for activity elsewhere in 424 * the browser, as appropriate. Other than setting up newly created XUL 425 * windows, all listener / observer management should happen here. 426 */ 427 BrowserTabList.prototype._checkListening = function () { 428 /* 429 * If we have an onListChanged handler that we haven't sent an announcement 430 * to since the last iteration, we need to watch for tab creation as well as 431 * change of the currently selected tab and tab title changes of tabs in 432 * parent process via TabAttrModified (tabs oop uses DOMTitleChanges). 433 * 434 * Oddly, we don't need to watch for 'close' events here. If our actor list 435 * is empty, then either it was empty the last time we iterated, and no 436 * close events are possible, or it was not empty the last time we 437 * iterated, but all the actors have since been closed, and we must have 438 * sent a notification already when they closed. 439 */ 440 this._listenForEventsIf( 441 this._onListChanged && this._mustNotify, 442 "_listeningForTabOpen", 443 ["TabOpen", "TabSelect", "TabAttrModified"] 444 ); 445 446 /* If we have live actors, we need to be ready to mark them dead. */ 447 this._listenForEventsIf( 448 this._actorByBrowser.size > 0, 449 "_listeningForTabClose", 450 ["TabClose"] 451 ); 452 453 /* 454 * We must listen to the window mediator in either case, since that's the 455 * only way to find out about tabs that come and go when top-level windows 456 * are opened and closed. 457 */ 458 this._listenToMediatorIf( 459 (this._onListChanged && this._mustNotify) || this._actorByBrowser.size > 0 460 ); 461 462 /* 463 * We also listen for title changed events on the browser. 464 */ 465 this._listenForEventsIf( 466 this._onListChanged && this._mustNotify, 467 "_listeningForTitleChange", 468 ["pagetitlechanged"], 469 this._onPageTitleChangedEvent 470 ); 471 }; 472 473 /** 474 * Add or remove event listeners for all XUL windows. 475 * 476 * @param shouldListen boolean 477 * True if we should add event handlers; false if we should remove them. 478 * @param guard string 479 * The name of a guard property of 'this', indicating whether we're 480 * already listening for those events. 481 * @param eventNames array of strings 482 * An array of event names. 483 */ 484 BrowserTabList.prototype._listenForEventsIf = function ( 485 shouldListen, 486 guard, 487 eventNames, 488 listener = this 489 ) { 490 if (!shouldListen !== !this[guard]) { 491 const op = shouldListen ? "addEventListener" : "removeEventListener"; 492 for (const win of Services.wm.getEnumerator( 493 DevToolsServer.chromeWindowType 494 )) { 495 for (const name of eventNames) { 496 win[op](name, listener, false); 497 } 498 } 499 this[guard] = shouldListen; 500 } 501 }; 502 503 /* 504 * Event listener for pagetitlechanged event. 505 */ 506 BrowserTabList.prototype._onPageTitleChangedEvent = function (event) { 507 switch (event.type) { 508 case "pagetitlechanged": { 509 const browser = event.target; 510 this._onDOMTitleChanged(browser); 511 break; 512 } 513 } 514 }; 515 516 /** 517 * Handle "DOMTitleChanged" event. 518 */ 519 BrowserTabList.prototype._onDOMTitleChanged = DevToolsUtils.makeInfallible( 520 function (browser) { 521 const actor = this._actorByBrowser.get(browser); 522 if (actor) { 523 this._notifyListChanged(); 524 this._checkListening(); 525 } 526 } 527 ); 528 529 /** 530 * Implement nsIDOMEventListener. 531 */ 532 BrowserTabList.prototype.handleEvent = DevToolsUtils.makeInfallible(function ( 533 event 534 ) { 535 // If event target has `linkedBrowser`, the event target can be assumed <tab> element. 536 // Else, event target is assumed <browser> element, use the target as it is. 537 const browser = event.target.linkedBrowser || event.target; 538 switch (event.type) { 539 case "TabOpen": 540 case "TabSelect": { 541 /* Don't create a new actor; iterate will take care of that. Just notify. */ 542 this._notifyListChanged(); 543 this._checkListening(); 544 break; 545 } 546 case "TabClose": { 547 const actor = this._actorByBrowser.get(browser); 548 if (actor) { 549 this._handleActorClose(actor, browser); 550 } 551 break; 552 } 553 case "TabAttrModified": { 554 // Remote <browser> title changes are handled via DOMTitleChange message 555 // TabAttrModified is only here for browsers in parent process which 556 // don't send this message. 557 if (browser.isRemoteBrowser) { 558 break; 559 } 560 const actor = this._actorByBrowser.get(browser); 561 if (actor) { 562 // TabAttrModified is fired in various cases, here only care about title 563 // changes 564 if (event.detail.changed.includes("label")) { 565 this._notifyListChanged(); 566 this._checkListening(); 567 } 568 } 569 break; 570 } 571 } 572 }, "BrowserTabList.prototype.handleEvent"); 573 574 /* 575 * If |shouldListen| is true, ensure we've registered a listener with the 576 * window mediator. Otherwise, ensure we haven't registered a listener. 577 */ 578 BrowserTabList.prototype._listenToMediatorIf = function (shouldListen) { 579 if (!shouldListen !== !this._listeningToMediator) { 580 const op = shouldListen ? "addListener" : "removeListener"; 581 Services.wm[op](this); 582 this._listeningToMediator = shouldListen; 583 } 584 }; 585 586 /** 587 * nsIWindowMediatorListener implementation. 588 * 589 * See _onTabClosed for explanation of why we needn't actually tweak any 590 * actors or tables here. 591 * 592 * An nsIWindowMediatorListener's methods get passed all sorts of windows; we 593 * only care about the tab containers. Those have 'gBrowser' members. 594 */ 595 BrowserTabList.prototype.onOpenWindow = DevToolsUtils.makeInfallible(function ( 596 window 597 ) { 598 const handleLoad = DevToolsUtils.makeInfallible(() => { 599 /* We don't want any further load events from this window. */ 600 window.removeEventListener("load", handleLoad); 601 602 if (appShellDOMWindowType(window) !== DevToolsServer.chromeWindowType) { 603 return; 604 } 605 606 // Listen for future tab activity. 607 if (this._listeningForTabOpen) { 608 window.addEventListener("TabOpen", this); 609 window.addEventListener("TabSelect", this); 610 window.addEventListener("TabAttrModified", this); 611 } 612 if (this._listeningForTabClose) { 613 window.addEventListener("TabClose", this); 614 } 615 if (this._listeningForTitleChange) { 616 window.messageManager.addMessageListener("DOMTitleChanged", this); 617 } 618 619 // As explained above, we will not receive a TabOpen event for this 620 // document's initial tab, so we must notify our client of the new tab 621 // this will have. 622 this._notifyListChanged(); 623 }); 624 625 /* 626 * You can hardly do anything at all with a XUL window at this point; it 627 * doesn't even have its document yet. Wait until its document has 628 * loaded, and then see what we've got. This also avoids 629 * nsIWindowMediator enumeration from within listeners (bug 873589). 630 */ 631 window = window 632 .QueryInterface(Ci.nsIInterfaceRequestor) 633 .getInterface(Ci.nsIDOMWindow); 634 635 window.addEventListener("load", handleLoad); 636 }, "BrowserTabList.prototype.onOpenWindow"); 637 638 BrowserTabList.prototype.onCloseWindow = DevToolsUtils.makeInfallible(function ( 639 window 640 ) { 641 if (window instanceof Ci.nsIAppWindow) { 642 window = window.docShell.domWindow; 643 } 644 645 if (appShellDOMWindowType(window) !== DevToolsServer.chromeWindowType) { 646 return; 647 } 648 649 /* 650 * nsIWindowMediator deadlocks if you call its GetEnumerator method from 651 * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so 652 * handle the close in a different tick. 653 */ 654 Services.tm.dispatchToMainThread( 655 DevToolsUtils.makeInfallible(() => { 656 /* 657 * Scan the entire map for actors representing tabs that were in this 658 * top-level window, and exit them. 659 */ 660 for (const [browser, actor] of this._actorByBrowser) { 661 /* The browser document of a closed window has no default view. */ 662 if (!browser.ownerGlobal) { 663 this._handleActorClose(actor, browser); 664 } 665 } 666 }, "BrowserTabList.prototype.onCloseWindow's delayed body") 667 ); 668 }, "BrowserTabList.prototype.onCloseWindow"); 669 670 exports.BrowserTabList = BrowserTabList; 671 672 function BrowserAddonList(connection) { 673 this._connection = connection; 674 this._actorByAddonId = new Map(); 675 this._onListChanged = null; 676 } 677 678 BrowserAddonList.prototype.getList = async function () { 679 const addons = await lazy.AddonManager.getAllAddons(); 680 for (const addon of addons) { 681 let actor = this._actorByAddonId.get(addon.id); 682 if (!actor) { 683 actor = new WebExtensionDescriptorActor(this._connection, addon); 684 this._actorByAddonId.set(addon.id, actor); 685 } 686 } 687 688 return Array.from(this._actorByAddonId, ([_, actor]) => actor); 689 }; 690 691 Object.defineProperty(BrowserAddonList.prototype, "onListChanged", { 692 enumerable: true, 693 configurable: true, 694 get() { 695 return this._onListChanged; 696 }, 697 set(v) { 698 if (v !== null && typeof v != "function") { 699 throw new Error( 700 "onListChanged property may only be set to 'null' or a function" 701 ); 702 } 703 this._onListChanged = v; 704 this._adjustListener(); 705 }, 706 }); 707 708 /** 709 * AddonManager listener must implement onDisabled. 710 */ 711 BrowserAddonList.prototype.onDisabled = function () { 712 this._onAddonManagerUpdated(); 713 }; 714 715 /** 716 * AddonManager listener must implement onEnabled. 717 */ 718 BrowserAddonList.prototype.onEnabled = function () { 719 this._onAddonManagerUpdated(); 720 }; 721 722 /** 723 * AddonManager listener must implement onInstalled. 724 */ 725 BrowserAddonList.prototype.onInstalled = function () { 726 this._onAddonManagerUpdated(); 727 }; 728 729 /** 730 * AddonManager listener must implement onOperationCancelled. 731 */ 732 BrowserAddonList.prototype.onOperationCancelled = function () { 733 this._onAddonManagerUpdated(); 734 }; 735 736 /** 737 * AddonManager listener must implement onUninstalling. 738 */ 739 BrowserAddonList.prototype.onUninstalling = function () { 740 this._onAddonManagerUpdated(); 741 }; 742 743 /** 744 * AddonManager listener must implement onUninstalled. 745 */ 746 BrowserAddonList.prototype.onUninstalled = function (addon) { 747 this._actorByAddonId.delete(addon.id); 748 this._onAddonManagerUpdated(); 749 }; 750 751 BrowserAddonList.prototype._onAddonManagerUpdated = function () { 752 this._notifyListChanged(); 753 this._adjustListener(); 754 }; 755 756 BrowserAddonList.prototype._notifyListChanged = function () { 757 if (this._onListChanged) { 758 this._onListChanged(); 759 } 760 }; 761 762 BrowserAddonList.prototype._adjustListener = function () { 763 if (this._onListChanged) { 764 // As long as the callback exists, we need to listen for changes 765 // so we can notify about add-on changes. 766 lazy.AddonManager.addAddonListener(this); 767 } else if (this._actorByAddonId.size === 0) { 768 // When the callback does not exist, we only need to keep listening 769 // if the actor cache will need adjusting when add-ons change. 770 lazy.AddonManager.removeAddonListener(this); 771 } 772 }; 773 774 exports.BrowserAddonList = BrowserAddonList;