Bug 592821 - update Private Browsing API for forwards-compatibility with electrolysis (r=adw)
authorIrakli Gozalishvili <rfobic@gmail.com>
Wed, 13 Oct 2010 19:43:55 +0200
changeset 876 de840ab37caf93718618e67dd4e090cdf0e41470
parent 875 6a0e49e1abc968cc227b1adaa4d4aec77239802f
child 877 7c173906997105398c0a94d51cbac2d5392faef8
push id385
push userrfobic@gmail.com
push dateWed, 13 Oct 2010 17:44:26 +0000
reviewersadw
bugs592821
Bug 592821 - update Private Browsing API for forwards-compatibility with electrolysis (r=adw)
packages/addon-kit/docs/private-browsing.md
packages/addon-kit/lib/private-browsing.js
packages/addon-kit/tests/test-private-browsing.js
--- a/packages/addon-kit/docs/private-browsing.md
+++ b/packages/addon-kit/docs/private-browsing.md
@@ -1,10 +1,11 @@
 <!-- contributed by Paul O’Shannessy [paul@oshannessy.com]  -->
 <!-- edited by Noelle Murata [fiveinchpixie@gmail.com]  -->
+<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] -->
 
 
 The `private-browsing` module allows you to access the private browsing service
 - detecting if it is active and adding callbacks for transitioning into and out
 of private browsing mode.
 
 Private browsing is a singleton, so in most cases it will be easiest to store it
 in a variable.
@@ -27,108 +28,37 @@ browsing.
     
     // Enter private browsing mode
     pb.active = true;
     
     // Exit private browsing mode
     pb.active = false;
 
 
-## Callbacks ##
-
-Transitioning into or out of private browsing mode causes a set of callbacks to
-be triggered. The first callback happens before the transition actually starts.
-From this callback, you can cancel the transition. The next callback happens
-immediately after the mode has been transitioned. The third gets called after
-the browser has restored the session. These will be explained in more detail
-below.
-
-
-### Adding and Removing Callback Functions ###
-
-The callbacks are stored as `collection`s, so there are a number of ways to
-access them.
+## Events ##
 
-    // Define our callback function
-    function onStartCallback () { /* do something */ }
-    
-    // The simplest way will be simple assignment
-    pb.onStart = onStartCallback;
-
-    // Let's say we had another callback that we also wanted to run
-    function onStartCallback2 () { /* do something */ }
-    
-    // We can add onStartCallback2 to the list very easily
-    pb.onStart.add(onStartCallback2);
-    
-    // Alternatively, we can assign both at the same time
-    pb.onStart = [onStartCallback, onStartCAllback2]
-    
-    // We can also remove callbacks.
-    // If we want to just remove all of them, we can just assign an empty array
-    pb.onStart = [];
-    
-    // We can also remove a specific callback.
-    pb.onStart.remove(onStartCallback2);
-
-
-### Available Callbacks ###
+When the browser starts or stops private browsing mode, the following events
+are emitted:
 
-<api name="onBeforeStart">
-@property {collection}
-Each `onBeforeStart` callback is called when something triggers the browser to
-enter private browsing mode. *`cancel`* is a one-time-use function that can be
-called if your code would like to prevent the browser from entering private
-browsing. Calling *`cancel`* from outside of your callback has no effect.
-</api>
-
-    pb.onBeforeStart = function (cancel) {
-      // Do something and realize you need to prevent private browsing...
-      cancel();
-    }
-
-<api name="onStart">
-@property {collection}
-Each `onStart` callback is called when the browser has actually entered private
-browsing mode. This only happens if nothing has cancelled the transition
-(which can be done by you or other extensions).
-</api>
+### start ###
+Emitted when the browser starts private browsing mode.
+    
+    pb.on("start", function() {
+      // Do something when the browser starts private browsing mode.
+    });
+    
+ 
+### stop ###
+Emitted when the browser stops private browsing mode.
 
-<api name="onAfterStart">
-@property {collection}
-Each `onAfterStart` callback is called after the browser has fully transitioned
-into private browsing. While the other callbacks will be called synchronously,
-this callback is asynchronous and will happen only after the private browsing
-session has been loaded.
-</api>
-
-<api name="onBeforeStop">
-@property {collection}
-Each `onBeforeStop` callback is called when something triggers the browser to
-leave private browsing mode. Just like `onAfterStart`, *`cancel`* is a
-one-time-use function that can be used to cancel the transition from private
-browsing mode.
-</api>
-
-<api name="onStop">
-@property {collection}
-Each `onStop` callback is called when the browser has actually left private
-browsing mode. This only happens if nothing has cancelled the transition
-(which can be done by you or other extensions).
-</api>
-
-<api name="onAfterStop">
-@property {collection}
-Each `onAfterStop` callback is called after the browser has fully transitioned
-out of private browsing. While the other callbacks will be called synchronously,
-this callback is asynchronous and will happen only after the browsing session
-has been restored.
-</api>
-
+    
+    pb.on("stop", function() {
+      // Do something when the browser stops private browsing mode.
+    });
+    
 
 
 ## Supported Applications ##
 
 This module is available in all applications. However, only Firefox will ever
 transition into or out of private browsing mode. For all other applications,
-`pb.active` will always return `false`, and none of your callbacks will actually
-be run.
+`pb.active` will always return `false`, and none of the events will be emitted.
 
--- a/packages/addon-kit/lib/private-browsing.js
+++ b/packages/addon-kit/lib/private-browsing.js
@@ -15,109 +15,79 @@
  *
  * The Initial Developer of the Original Code is
  * Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Paul O’Shannessy <paul@oshannessy.com>
+ *  Irakli Gozalishvili <gozala@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const {Cc,Ci} = require("chrome");
-const collection = require("collection");
 const observers = require("observer-service");
-const errors = require("errors");
+const { EventEmitter } = require("events");
+const { setTimeout } = require("timer");
+const unload = require("unload");
+
+const ON_START = "start";
+const ON_STOP = "stop";
+const ON_TRANSITION = "private-browsing-transition-complete";
 
 let pbService;
 // Currently, only Firefox implements the private browsing service.
 if (require("xul-app").is("Firefox")) {
   pbService = Cc["@mozilla.org/privatebrowsing;1"].
               getService(Ci.nsIPrivateBrowsingService);
 }
 
-// make pb.active work
-exports.__defineGetter__("active", function () {
-  return pbService ? pbService.privateBrowsingEnabled : false;
-});
-
-exports.__defineSetter__("active", function (val) {
-  if (pbService) {
-    pbService.privateBrowsingEnabled = val;
-  }
-});
-
-// add our collection properties
-collection.addCollectionProperty(exports, "onBeforeStart");
-collection.addCollectionProperty(exports, "onStart");
-collection.addCollectionProperty(exports, "onAfterStart");
-collection.addCollectionProperty(exports, "onBeforeStop");
-collection.addCollectionProperty(exports, "onStop");
-collection.addCollectionProperty(exports, "onAfterStop");
-
-// implement functions to serve as delegators for various observer topics
-function onBeforeTransition(subject, data) {
-  subject.QueryInterface(Ci.nsISupportsPRBool);
-  // If subject is already true (by way of another observer), exit early.
-  if (subject.data)
-    return;
-
-  let callbacks = data == "enter" ? exports.onBeforeStart :
-                  data == "exit"  ? exports.onBeforeStop  : [];
-
-  for (let callback in callbacks) {
-    let cancelled = false;
-    // Since we're calling a user-defined callback, we need to catchAndLog it.
-    errors.catchAndLog(function () {
-      callback.call(exports, function () cancelled = true);
-    })();
+const privateBrowsing = EventEmitter.compose({
+  constructor: function PrivateBrowsing() {
+    // Binding method to instance since it will be used with `setTimeout`.
+    this._emit = this._emit.bind(this);
+    this.unload = this.unload.bind(this);
+    // Report unhandled errors from listeners
+    this.on("error", console.exception.bind(console));
+    unload.ensure(this);
+    // We only need to add observers if `pbService` exists.
+    if (pbService) {
+      observers.add(ON_TRANSITION, this.onTransition.bind(this));
+      this._active = pbService.privateBrowsingEnabled;
+    }
+  },
+  unload: function _destructor() {
+    this._removeAllListeners(ON_START);
+    this._removeAllListeners(ON_STOP);
+  },
+  // We don't need to do anything with cancel here.
+  onTransition: function onTransition() {
+    let active = this._active = pbService.privateBrowsingEnabled;
+    setTimeout(this._emit, 0, active ? ON_START : ON_STOP);
+  },
+  get active() this._active,
+  set active(value) {
+    if (pbService)
+      pbService.privateBrowsingEnabled = !!value;
+  },
+  _active: false
+})()
 
-    // If cancel() was called, then we want to make sure the PB transition is
-    // cancelled and also stop executing any other callbacks we have.
-    if (cancelled) {
-      subject.data = true;
-      break;
-    }
-  }
-}
+Object.defineProperty(exports, "active", {
+  get: function() privateBrowsing.active,
+  set: function(value) privateBrowsing.active = value
+});
+exports.on = privateBrowsing.on;
+exports.removeListener = privateBrowsing.removeListener;
 
-// We don't need to do anything with cancel here.
-function onTransition(subject, data) {
-  let callbacks = data == "enter" ? exports.onStart :
-                  data == "exit"  ? exports.onStop  : [];
-  for (let callback in callbacks) {
-    errors.catchAndLog(function () {
-      callback.call(exports);
-    })();
-  }
-}
-
-function onAfterTransition(subject, data) {
-  // "private-browsing-transition-complete" isn't sent with "enter"/"exit", so
-  // determine which it was based on if PB is active.
-  let callbacks = exports.active ? exports.onAfterStart : exports.onAfterStop;
-  for (let callback in callbacks) {
-    errors.catchAndLog(function () {
-      callback.call(exports);
-    })();
-  }
-}
-
-// We only need to add observers if pbService exists.
-if (pbService) {
-  observers.add("private-browsing-cancel-vote", onBeforeTransition);
-  observers.add("private-browsing", onTransition);
-  observers.add("private-browsing-transition-complete", onAfterTransition);
-}
-
--- a/packages/addon-kit/tests/test-private-browsing.js
+++ b/packages/addon-kit/tests/test-private-browsing.js
@@ -15,16 +15,17 @@
  *
  * The Initial Developer of the Original Code is
  * Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Paul O’Shannessy <paul@oshannessy.com>
+ *  Irakli Gozalishvili <gozala@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -41,254 +42,126 @@ let {Cc,Ci} = require("chrome");
 let pbService;
 // Currently, only Firefox implements the private browsing service.
 if (require("xul-app").is("Firefox")) {
   pbService = Cc["@mozilla.org/privatebrowsing;1"].
               getService(Ci.nsIPrivateBrowsingService);
 }
 
 if (pbService) {
-  // a method used to reset all the callbacks for the next test run
-  function reset(active) {
-    active = active === undefined ? false : active;
-    pb.onBeforeStart = [];
-    pb.onStart = [];
-    pb.onBeforeStop = [];
-    pb.onStop = [];
-    // Use the service since that's guaranteed
-    pbService.privateBrowsingEnabled = active;
-  }
-
 
   // tests that active has the same value as the private browsing service expects
   exports.testGetActive = function (test) {
-    reset();
     test.assertEqual(pb.active, false,
                      "private-browsing.active is correct without modifying PB service");
 
     pbService.privateBrowsingEnabled = true;
-    test.assertEqual(pb.active, true,
-                     "private-browsing.active is correct after modifying PB service");
-  }
+    test.assert(pb.active,
+                "private-browsing.active is correct after modifying PB service");
+  };
 
 
   // tests that setting active does put the browser into private browsing mode
   exports.testSetActive = function (test) {
-    reset();
     pb.active = true;
     test.assertEqual(pbService.privateBrowsingEnabled, true,
                      "private-browsing.active=true enables private browsing mode");
+
     pb.active = false;
     test.assertEqual(pbService.privateBrowsingEnabled, false,
                      "private-browsing.active=false disables private browsing mode");
-  }
-
+  };
 
-  // tests the basic cases for onBeforeStart callbacks
-  exports.testSimpleOnBeforeStart = function (test) {
-    reset();
-    let count = 0;
-    function simpleOnBeforeStart(cancelFn) {
-      count++;
-    }
-    pb.onBeforeStart = simpleOnBeforeStart;
+  exports.testStart = function(test) {
+    test.waitUntilDone();
+    pb.on("start", function onStart() {
+      test.assert(pbService.privateBrowsingEnabled,
+                  "private mode is active when \"start\" event is emitted");
+      test.assert(pb.active,
+                  "`active` is `true` when \"start\" event is emitted");
+      pb.removeListener("start", onStart);
+      test.done();
+    });
     pb.active = true;
-    test.assertEqual(count, 1, "All onBeforeStart methods were called");
-    test.assertEqual(pb.active, true,
-                     "onBeforeStart didn't cancel when it wasn't supposed");
-    pb.active = false;
+  };
 
-    // Now let's make it more complicated...
-    function simpleOnBeforeStart2(cancelFn) {
-      count += 2;
-    }
-    pb.onBeforeStart = [simpleOnBeforeStart, simpleOnBeforeStart2];
+  exports.testStop = function(test) {
+    test.waitUntilDone();
+    pb.on("stop", function onStop() {
+      test.assertEqual(pbService.privateBrowsingEnabled, false,
+                       "private mode is disabled when stop event is emitted");
+      test.assertEqual(pb.active, false,
+                       "`active` is `false` when stop event is emitted");
+      pb.removeListener("stop", onStop);
+      test.done();
+    });
     pb.active = true;
-    test.assertEqual(count, 4, "All onBeforeStart methods were called");
-    test.assertEqual(pb.active, true,
-                     "onBeforeStart didn't cancel when it wasn't supposed");
-  }
+    pb.active = false;
+  };
 
+  exports.testBothListeners = function(test) {
+    test.waitUntilDone();
+    let stop = false;
+    let start = false;
 
-  // tests the basic cases for onStart callbacks
-  exports.testSimpleOnStart = function (test) {
-    reset();
-    let count = 0;
-    function simpleOnStart() {
-      count++;
+    function onStop() {
+      test.assertEqual(stop, false,
+                       "stop callback must be called only once");
+      test.assertEqual(pbService.privateBrowsingEnabled, false,
+                       "private mode is disabled when stop event is emitted");
+      test.assertEqual(pb.active, false,
+                       "`active` is `false` when stop event is emitted");
+
+      pb.on("start", finish);
+      pb.removeListener("start", onStart);
+      pb.removeListener("start", onStart2);
+      pb.enable = false;
+      pb.active = true;
+      stop = true;
     }
-    pb.onStart = simpleOnStart;
-    pb.active = true;
-    test.assertEqual(count, 1, "simpleonStart was called");
-    pb.active = false;
 
-    // Now let's make it more complicated...
-    function simpleOnStart2() {
-      count += 2;
+    function onStart() {
+      test.assertEqual(false, start,
+                       "stop callback must be called only once");
+      test.assert(pbService.privateBrowsingEnabled,
+                  "private mode is active when start event is emitted");
+      test.assert(pb.active,
+                  "`active` is `true` when start event is emitted");
+
+      pb.on("stop", onStop);
+      pb.active = false;
+      start = true;
     }
-    pb.onStart = [simpleOnStart, simpleOnStart2];
-    pb.active = true;
-    test.assertEqual(count, 4, "simpleOnStart was called");
-  }
-
 
-  // tests that canceling from inside onBeforeStart prevents onStart callbacks from running
-  exports.testOnBeforeStartCancel = function (test) {
-    reset();
-    let wasActivated = false;
-    pb.onBeforeStart = function (cancelFn) {
-      cancelFn();
+    function onStart2() {
+      test.assert(start, "start listener must be called already");
+      test.assertEqual(false, stop, "stop callback must not be called yet");
     }
-    pb.onStart = function () {
-      wasActivated = true;
-    }
-    pb.active = true;
 
-    test.assertEqual(pb.active, false, "Private Browsing enter was cancelled");
-    test.assertEqual(wasActivated, false, "onStart wasn't called");
-  }
-
+    function finish() {
+      test.assert(pbService.privateBrowsingEnabled, true,
+                  "private mode is active when start event is emitted");
+      test.assert(pb.active,
+                  "`active` is `true` when start event is emitted");
 
-  // tests the basic case for onAfterStart callbacks
-  exports.testSimpleOnAfterStart = function (test) {
-    test.waitUntilDone();
-    reset();
-    pb.onAfterStart = function () {
-      test.assert(true, "onAfterStart was called");
+      pb.removeListener("start", finish);
+      pb.removeListener("stop", onStop);
+
+      pb.active = false;
+
+      test.assertEqual(pbService.privateBrowsingEnabled, false);
+      test.assertEqual(pb.active, false);
+
       test.done();
     }
-    pb.active = true;
-  }
 
-
-  // tests the basic cases for onBeforeStop callbacks
-  exports.testSimpleOnBeforeStop = function (test) {
-    reset(true);
-    let count = 0;
-    function simpleOnBeforeStop(cancelFn) {
-      count++;
-    }
-    pb.onBeforeStop = simpleOnBeforeStop;
-    pb.active = false;
-    test.assertEqual(count, 1, "All onBeforeStop methods were called");
-    test.assertEqual(pb.active, false,
-                     "onBeforeStop didn't cancel when it wasn't supposed");
-    pb.active = true;
-
-    // Now let's make it more complicated...
-    function simpleOnBeforeStop2(cancelFn) {
-      count += 2;
-    }
-    pb.onBeforeStop = [simpleOnBeforeStop, simpleOnBeforeStop2];
-    pb.active = false;
-    test.assertEqual(count, 4, "All onBeforeStop methods were called");
-    test.assertEqual(pb.active, false,
-                     "onBeforeStop didn't cancel when it wasn't supposed");
-  }
-
-
-  // tests the basic cases for onStop callbacks
-  exports.testSimpleOnStop = function (test) {
-    reset(true);
-    let count = 0;
-    function simpleOnStop() {
-      count++;
-    }
-    pb.onStop = simpleOnStop;
-    pb.active = false;
-    test.assertEqual(count, 1, "All onStop methods were called");
-    pb.active = true;
-
-    // Now let's make it more complicated...
-    function simpleOnStop2() {
-      count += 2;
-    }
-    pb.onStop = [simpleOnStop, simpleOnStop2];
-    pb.active = false;
-    test.assertEqual(count, 4, "All onStop methods were called");
-  }
-
-
-  // tests that canceling from inside onBeforeStop prevents onStop callbacks from running
-  exports.testOnBeforeStopCancel = function (test) {
-    reset();
-    let wasDeactivated = false;
-    pb.onBeforeStop = function (cancelFn) {
-      cancelFn();
-    }
-    pb.onStop = function () {
-      wasDeactivated = true;
-    }
-    pb.active = true;
-
-    test.assertEqual(pb.active, true, "Private Browsing exit was cancelled");
-    test.assertEqual(wasDeactivated, false, "onStop wasn't called");
-  }
-
-
-  // tests the basic case for onAfterStop callbacks
-  exports.testSimpleOnAfterStop = function (test) {
-    test.waitUntilDone();
-    reset();
-    pb.onAfterStop = function () {
-      test.assert(true, "onAfterStop was called");
-      test.done();
-    }
-    pb.active = true;
-  }
-
-
-  // tests that |this| is |pb| inside each of the Start callbacks
-  exports.testCallbackThisStart = function (test) {
-    test.waitUntilDone();
-    reset();
-    pb.onBeforeStart = function (cancel) {
-      test.assertEqual(this, pb, "|this| == pb in onBeforeStart");
-    };
-    pb.onStart = function () {
-      test.assertEqual(this, pb, "|this| == pb in onStart");
-    };
-    pb.onAfterStart = function () {
-      test.assertEqual(this, pb, "|this| == pb in onAfterStart");
-      test.done();
-    };
-    pb.active = true;
-  }
-
-
-  // test that |this| is |pb| inside each of the Stop callbacks
-  exports.testCallbackThisStop = function (test) {
-    test.waitUntilDone();
-    reset(true);
-    pb.onBeforeStop = function (cancel) {
-      test.assertEqual(this, pb, "|this| == pb in onBeforeStop");
-    };
-    pb.onStop = function () {
-      test.assertEqual(this, pb, "|this| == pb in onStop");
-    };
-    pb.onAfterStop = function () {
-      test.assertEqual(this, pb, "|this| == pb in onAfterStop");
-      test.done();
-    };
-    pb.active = false;
-  }
+    pb.on("start", onStart);
+    pb.on("start", onStart2);
+    pbService.privateBrowsingEnabled = true;
+  };
 }
 else {
   // tests for the case where private browsing doesn't exist
   exports.testNoImpl = function (test) {
     test.assertEqual(pb.active, false,
                      "pb.active returns false when private browsing isn't supported");
-
-
-    // Setting pb.active = true shouldn't have any effect. Also, no callbacks
-    // should have been called. We'll just test one callback since they are
-    // under the same code path.
-    let wasActivated = false;
-    pb.onStart = function () {
-      wasActivated = true;
-    }
-    pb.active = true;
-    test.assertEqual(pb.active, false,
-                     "pb.active returns false even when set to true");
-    test.assertEqual(wasActivated, false,
-                     "onStart callback wasn't run when PB isn't supported");
-  }
+  };
 }