Bug 826058 - Hosted app install/update tests. r=fabrice, r=ted
authorDale Harvey <dale@arandomurl.com>
Thu, 04 Apr 2013 17:58:44 -0700
changeset 127737 e63cb4c3e06320521b32cf08465f66fa728d35f3
parent 127736 8161280e740e290a8082a9e2a6c6766b23460a05
child 127738 503dea706f82dcc671e73aae1b2ce8226bec8bb9
push id24512
push userryanvm@gmail.com
push dateFri, 05 Apr 2013 20:13:49 +0000
treeherdermozilla-central@139b6ba547fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice, ted
bugs826058
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 826058 - Hosted app install/update tests. r=fabrice, r=ted
dom/apps/tests/Makefile.in
dom/apps/tests/file_app.sjs
dom/apps/tests/file_app.template.html
dom/apps/tests/file_cached_app.template.appcache
dom/apps/tests/file_cached_app.template.webapp
dom/apps/tests/file_hosted_app.template.webapp
dom/apps/tests/test_app_update.html
dom/browser-element/mochitest/browserElement_AppFramePermission.js
dom/indexedDB/test/webapp_clearBrowserData.js
testing/mochitest/b2g.json
testing/specialpowers/components/SpecialPowersObserver.js
testing/specialpowers/content/SpecialPowersObserverAPI.js
testing/specialpowers/content/specialpowersAPI.js
--- a/dom/apps/tests/Makefile.in
+++ b/dom/apps/tests/Makefile.in
@@ -6,13 +6,22 @@ DEPTH            = @DEPTH@
 topsrcdir        = @top_srcdir@
 srcdir           = @srcdir@
 VPATH            = @srcdir@
 
 relativesrcdir   = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
+MOCHITEST_FILES = \
+  test_app_update.html \
+  file_app.sjs \
+  file_app.template.html \
+  file_hosted_app.template.webapp \
+  file_cached_app.template.webapp \
+  file_cached_app.template.appcache \
+  $(NULL)
+
 MOCHITEST_CHROME_FILES = \
   test_apps_service.xul \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_app.sjs
@@ -0,0 +1,119 @@
+var gBasePath = "tests/dom/apps/tests/";
+var gAppTemplatePath = "tests/dom/apps/tests/file_app.template.html";
+var gAppcacheTemplatePath = "tests/dom/apps/tests/file_cached_app.template.appcache";
+
+function makeResource(templatePath, version, apptype) {
+  var res = readTemplate(templatePath).replace(/VERSIONTOKEN/g, version)
+                                      .replace(/APPTYPETOKEN/g, apptype);
+
+  // Hack - This is necessary to make the tests pass, but hbambas says it
+  // shouldn't be necessary. Comment it out and watch the tests fail.
+  if (templatePath == gAppTemplatePath && apptype == 'cached') {
+    res = res.replace('<html>', '<html manifest="file_app.sjs?apptype=cached&getappcache=true">');
+  }
+
+  return res;
+}
+
+function handleRequest(request, response) {
+  var query = getQuery(request);
+
+  // If this is a version update, update state and return.
+  if ("setVersion" in query) {
+    setState('version', query.setVersion);
+    response.setHeader("Content-Type", "text/html", false);
+    response.setHeader("Access-Control-Allow-Origin", "*", false);
+    response.write('OK');
+    return;
+  }
+
+  // Get the app type.
+  var apptype = query.apptype;
+  if (apptype != 'hosted' && apptype != 'cached')
+    throw "Invalid app type: " + apptype;
+
+  // Get the version from server state and handle the etag.
+  var version = Number(getState('version'));
+  var etag = getEtag(request, version);
+  dump("Server Etag: " + etag + "\n");
+
+  if (etagMatches(request, etag)) {
+    dump("Etags Match. Sending 304\n");
+    response.setStatusLine(request.httpVersion, "304", "Not Modified");
+    return;
+  }
+
+  response.setHeader("Etag", etag, false);
+  if (request.hasHeader("If-None-Match"))
+    dump("Client Etag: " + request.getHeader("If-None-Match") + "\n");
+  else
+    dump("No Client Etag\n");
+
+  // Check if we're generating a webapp manifest.
+  if ('getmanifest' in query) {
+    var template = gBasePath + 'file_' + apptype + '_app.template.webapp';
+    response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
+    response.write(makeResource(template, version, apptype));
+    return;
+  }
+
+  // If apptype==cached, we might be generating the appcache manifest.
+  //
+  // NB: Among other reasons, we use the same sjs file here so that the version
+  //     state is shared.
+  if (apptype == 'cached' && 'getappcache' in query) {
+    response.setHeader("Content-Type", "text/cache-manifest", false);
+    response.write(makeResource(gAppcacheTemplatePath, version, apptype));
+    return;
+  }
+
+  // Generate the app.
+  response.setHeader("Content-Type", "text/html", false);
+  response.write(makeResource(gAppTemplatePath, version, apptype));
+}
+
+function getEtag(request, version) {
+  return request.queryString.replace(/&/g, '-').replace(/=/g, '-') + '-' + version;
+}
+
+function etagMatches(request, etag) {
+  return request.hasHeader("If-None-Match") && request.getHeader("If-None-Match") == etag;
+}
+
+function getQuery(request) {
+  var query = {};
+  request.queryString.split('&').forEach(function (val) {
+    var [name, value] = val.split('=');
+    query[name] = unescape(value);
+  });
+  return query;
+}
+
+// Copy-pasted incantations. There ought to be a better way to synchronously read
+// a file into a string, but I guess we're trying to discourage that.
+function readTemplate(path) {
+  var file = Components.classes["@mozilla.org/file/directory_service;1"].
+                        getService(Components.interfaces.nsIProperties).
+                        get("CurWorkD", Components.interfaces.nsILocalFile);
+  var fis  = Components.classes['@mozilla.org/network/file-input-stream;1'].
+                        createInstance(Components.interfaces.nsIFileInputStream);
+  var cis = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+                       createInstance(Components.interfaces.nsIConverterInputStream);
+  var split = path.split("/");
+  for(var i = 0; i < split.length; ++i) {
+    file.append(split[i]);
+  }
+  fis.init(file, -1, -1, false);
+  cis.init(fis, "UTF-8", 0, 0);
+
+  var data = "";
+  let (str = {}) {
+    let read = 0;
+    do {
+      read = cis.readString(0xffffffff, str); // read as much as we can and put it in str.value
+      data += str.value;
+    } while (read != 0);
+  }
+  cis.close();
+  return data;
+}
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_app.template.html
@@ -0,0 +1,63 @@
+<html>
+<head>
+<script>
+
+function sendMessage(msg) {
+  alert(msg);
+}
+
+function ok(p, msg) {
+  if (p)
+    sendMessage("OK: " + msg);
+  else
+    sendMessage("KO: " + msg);
+}
+
+function is(a, b, msg) {
+  if (a == b)
+    sendMessage("OK: " + a + " == " + b + " - " + msg);
+  else
+    sendMessage("KO: " + a + " != " + b + " - " + msg);
+}
+
+function installed(p) {
+  if (p)
+    sendMessage("IS_INSTALLED");
+  else
+    sendMessage("NOT_INSTALLED");
+}
+
+function finish() {
+  sendMessage("VERSION: MyWebApp vVERSIONTOKEN");
+  sendMessage("DONE");
+}
+
+function cbError() {
+  ok(false, "Error callback invoked");
+  finish();
+}
+
+function go() {
+  ok(true, "Launched app");
+  var request = window.navigator.mozApps.getSelf();
+  request.onsuccess = function() { var app = request.result; checkApp(app); }
+  request.onerror = cbError;
+}
+
+function checkApp(app) {
+  // If the app is installed, |app| will be non-null. If it is, verify its state.
+  installed(!!app);
+  if (app) {
+    var appName = "Really Rapid Release (APPTYPETOKEN)";
+    is(app.manifest.name, appName, "Manifest name should be correct");
+    is(app.origin, "http://test", "App origin should be correct");
+    is(app.installOrigin, "http://mochi.test:8888", "Install origin should be correct");
+  }
+  finish();
+}
+
+</script>
+</head>
+<body onload="go();">
+App Body. Version: VERSIONTOKEN
+</body></html>
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_cached_app.template.appcache
@@ -0,0 +1,3 @@
+CACHE MANIFEST
+# Version VERSIONTOKEN
+/tests/dom/apps/tests/file_app.sjs?apptype=cached
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_cached_app.template.webapp
@@ -0,0 +1,6 @@
+{
+  "name": "Really Rapid Release (cached)",
+  "description": "Updated even faster than Firefox, just to annoy slashdotters.",
+  "launch_path": "/tests/dom/apps/tests/file_app.sjs?apptype=cached",
+  "appcache_path": "/tests/dom/apps/tests/file_app.sjs?apptype=cached&getappcache=true"
+}
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_hosted_app.template.webapp
@@ -0,0 +1,5 @@
+{
+  "name": "Really Rapid Release (hosted)",
+  "description": "Updated even faster than Firefox, just to annoy slashdotters.",
+  "launch_path": "/tests/dom/apps/tests/file_app.sjs?apptype=hosted"
+}
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/test_app_update.html
@@ -0,0 +1,237 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=826058
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 826058</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.7">
+
+  /** Test for Bug 826058 **/
+
+  SimpleTest.waitForExplicitFinish();
+
+  var gBaseURL = 'http://test/tests/dom/apps/tests/';
+  var gHostedManifestURL = gBaseURL + 'file_app.sjs?apptype=hosted&getmanifest=true';
+  var gCachedManifestURL = gBaseURL + 'file_app.sjs?apptype=cached&getmanifest=true';
+  var gGenerator = runTest();
+  var mozBrowserFramesEnabledValue = undefined;
+  var launchableValue = undefined;
+
+  function go() {
+    SpecialPowers.pushPermissions(
+      [{ "type": "browser", "allow": 1, "context": document },
+       { "type": "embed-apps", "allow": 1, "context": document },
+       { "type": "webapps-manage", "allow": 1, "context": document }],
+      function() { gGenerator.next() });
+  }
+
+  function continueTest() {
+    try { gGenerator.next(); }
+    catch (e) { dump("Got exception: " + e + "\n"); }
+  }
+
+  function cbError() {
+    ok(false, "Error callback invoked: " + this.error.name);
+    finish();
+  }
+
+  function runTest() {
+    // Set up.
+
+    try {
+      mozBrowserFramesEnabledValue =
+        SpecialPowers.getBoolPref("dom.mozBrowserFramesEnabled");
+    } catch(e) {}
+
+    launchableValue = SpecialPowers.setAllAppsLaunchable(true);
+    SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
+
+    setAppVersion(1, continueTest);
+    yield;
+    SpecialPowers.autoConfirmAppInstall(continueTest);
+    yield;
+
+    // Load the app, uninstalled.
+    checkAppState(null, false, 1, continueTest);
+    yield;
+
+    // Bump the version and install the app.
+    setAppVersion(2, continueTest);
+    yield;
+
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield;
+    var app = request.result;
+    ok(app, "App is non-null");
+
+    // Check the app a few times.
+    checkAppState(app, true, 2, continueTest);
+    yield;
+    checkAppState(app, true, 2, continueTest);
+    yield;
+
+    // Bump the version and check the app again. The app is not cached, so the
+    // version bump takes effect.
+    setAppVersion(3, continueTest);
+    yield;
+    checkAppState(app, true, 3, continueTest);
+    yield;
+
+    // Uninstall the app.
+    request = navigator.mozApps.mgmt.uninstall(app);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield;
+
+    // Check the uninstalled app.
+    checkAppState(app, false, 3, continueTest);
+    yield;
+
+    // Install the cached app.
+    setAppVersion(3, continueTest);
+    yield;
+    ok(true, "Installing cached app");
+    var request = navigator.mozApps.install(gCachedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield;
+    var app = request.result;
+    ok(app, "App is non-null");
+    if (app.installState == "pending") {
+      ok(true, "App is pending. Waiting for progress");
+      app.onprogress = function() ok(true, "Got download progress");
+      app.ondownloadsuccess = continueTest;
+      app.ondownloaderror = cbError;
+      yield;
+    }
+    is(app.installState, "installed", "App is installed");
+
+    // Check the cached app.
+    checkAppState(app, true, 3, continueTest);
+    yield;
+
+    // Check for updates. The current infrastructure always returns a new appcache
+    // manifest, so there should always be an update.
+    var lastCheck = app.lastUpdateCheck;
+    ok(true, "Setting callbacks");
+    app.ondownloadapplied = function() ok(true, "downloadapplied fired.");
+    app.ondownloadavailable = function() ok(false, "downloadavailable fired");
+    ok(true, "Checking for updates");
+    var request = app.checkForUpdate();
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield;
+    todo(app.lastUpdateCheck > lastCheck, "lastUpdateCheck updated appropriately");
+
+
+    // Uninstall the app.
+    request = navigator.mozApps.mgmt.uninstall(app);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield;
+    ok(true, "Uninstalled app");
+
+    // Check the uninstalled app.
+    checkAppState(app, false, 3, continueTest);
+    yield;
+
+    // All done.
+    ok(true, "All done");
+    finish();
+  }
+
+  function setAppVersion(version, cb) {
+    var xhr = new XMLHttpRequest();
+    xhr.addEventListener("load", function() { is(xhr.responseText, "OK", "setVersion OK"); cb(); });
+    xhr.addEventListener("error", cbError);
+    xhr.addEventListener("abort", cbError);
+    xhr.open('GET', gBaseURL + 'file_app.sjs?setVersion=' + version, true);
+    xhr.send();
+  }
+
+  // This function checks the state of an installed app. It does the following:
+  //
+  // * Check various state on the app object itself.
+  // * Launch the app.
+  // * Listen for messages from the app, verifying state.
+  // * Close the app.
+  // * Invoke the callback.
+  function checkAppState(app, installed, version, cb) {
+    // Check state on the app object.
+    if (installed)
+      is(app.installState, "installed", "Checking installed app");
+    else
+      ok(true, "Checking uninstalled app");
+
+    // Set up the app. We need to set the attributes before the app is inserted
+    // into the DOM.
+    var ifr = document.createElement('iframe');
+    ifr.setAttribute('mozbrowser', 'true');
+    ifr.setAttribute('mozapp', app ? app.manifestURL : gHostedManifestURL);
+    ifr.setAttribute('src', getAppURL(app));
+    var domParent = document.getElementById('container');
+
+    // Set us up to listen for messages from the app.
+    var listener = function(e) {
+      var message = e.detail.message;
+      if (/OK/.exec(message)) {
+        ok(true, "Message from app: " + message);
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from app: " + message);
+      } else if (/IS_INSTALLED/.exec(message)) {
+        ok(installed, "App is installed");
+      } else if (/NOT_INSTALLED/.exec(message)) {
+        ok(!installed, "App is not installed");
+      } else if (/VERSION/.exec(message)) {
+        is(message, "VERSION: MyWebApp v" + version, "Version should be correct");
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Messaging from app complete");
+        ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(ifr);
+        cb();
+      }
+    }
+    ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
+
+    // Add the iframe to the DOM, triggering the launch.
+    domParent.appendChild(ifr);
+  }
+
+  // Returns that appropriate path for the app associated with the manifest,
+  // or the base sjs file if app is null.
+  function getAppURL(app) {
+    if (!app)
+      return gBaseURL + "file_app.sjs?apptype=hosted";
+    return app.origin + app.manifest.launch_path;
+  }
+
+  function finish() {
+    SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", mozBrowserFramesEnabledValue);
+    //SpecialPowers.setAllAppsLaunchable(launchableValue);
+    SimpleTest.finish();
+  }
+
+  function doReload() {
+    window.location.reload(true);
+  }
+
+  </script>
+</head>
+<body onload="go()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=826058">Mozilla Bug 826058</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<div id="container"></div>
+<button onclick="doReload()">Reload Page</button>
+</body>
+</html>
--- a/dom/browser-element/mochitest/browserElement_AppFramePermission.js
+++ b/dom/browser-element/mochitest/browserElement_AppFramePermission.js
@@ -4,28 +4,23 @@
 // Bug 777384 - Test mozapp permission.
 "use strict";
 
 SimpleTest.waitForExplicitFinish();
 browserElementTestHelpers.setEnabledPref(true);
 browserElementTestHelpers.addPermission();
 
 function makeAllAppsLaunchable() {
-  var Webapps = {};
-  SpecialPowers.Cu.import("resource://gre/modules/Webapps.jsm", Webapps);
-  var appRegistry = SpecialPowers.wrap(Webapps.DOMApplicationRegistry);
-
-  var originalValue = appRegistry.allAppsLaunchable;
-  appRegistry.allAppsLaunchable = true;
+  var originalValue = SpecialPowers.setAllAppsLaunchable(true);
 
   // Clean up after ourselves once tests are done so the test page is unloaded.
   window.addEventListener("unload", function restoreAllAppsLaunchable(event) {
     if (event.target == window.document) {
       window.removeEventListener("unload", restoreAllAppsLaunchable, false);
-      appRegistry.allAppsLaunchable = originalValue;
+      SpecialPowers.setAllAppsLaunchable(originalValue);
     }
   }, false);
 }
 makeAllAppsLaunchable();
 
 function testAppElement(expectAnApp, callback) {
   var iframe = document.createElement('iframe');
   SpecialPowers.wrap(iframe).mozbrowser = true;
--- a/dom/indexedDB/test/webapp_clearBrowserData.js
+++ b/dom/indexedDB/test/webapp_clearBrowserData.js
@@ -116,32 +116,26 @@ function start()
     return;
   }
 
   SpecialPowers.addPermission("browser", true, document);
   SpecialPowers.addPermission("browser", true, { manifestURL: manifestURL,
                                                  isInBrowserElement: false });
   SpecialPowers.addPermission("embed-apps", true, document);
 
-  let Webapps = {};
-  SpecialPowers.wrap(Components)
-               .utils.import("resource://gre/modules/Webapps.jsm", Webapps);
-  let appRegistry = SpecialPowers.wrap(Webapps.DOMApplicationRegistry);
-
-  let originalAllAppsLaunchable = appRegistry.allAppsLaunchable;
-  appRegistry.allAppsLaunchable = true;
+  let originalAllAppsLaunchable = SpecialPowers.setAllAppsLaunchable(true);
 
   window.addEventListener("unload", function cleanup(event) {
     if (event.target == document) {
       window.removeEventListener("unload", cleanup, false);
 
       SpecialPowers.removePermission("browser", location.href);
       SpecialPowers.removePermission("browser",
                                      location.protocol + "//" + appDomain);
       SpecialPowers.removePermission("embed-apps", location.href);
-      appRegistry.allAppsLaunchable = originalAllAppsLaunchable;
+      SpecialPowers.setAllAppsLaunchable(originalAllAppsLaunchable);
     }
   }, false);
 
   SpecialPowers.pushPrefEnv({
     "set": [["dom.mozBrowserFramesEnabled", true]]
   }, runTest);
 }
--- a/testing/mochitest/b2g.json
+++ b/testing/mochitest/b2g.json
@@ -1,14 +1,15 @@
 {
 "runtests": {
     "caps": "",
     "content": "",
     "docshell": "",
     "dom": "",
+    "dom/apps": "",
     "layout": ""
   },
 "excludetests": {
 	"caps/tests/mochitest/test_app_principal_equality.html": " should not be able to access the other frames - got true, expected false",
 	"content/xbl/":"",
 	"content/xul" : "",
         "content/html/content/test/" : "",
         "content/media/" : "",
--- a/testing/specialpowers/components/SpecialPowersObserver.js
+++ b/testing/specialpowers/components/SpecialPowersObserver.js
@@ -52,16 +52,17 @@ SpecialPowersObserver.prototype = new Sp
       case "chrome-document-global-created":
         if (!this._isFrameScriptLoaded) {
           // Register for any messages our API needs us to handle
           this._messageManager.addMessageListener("SPPrefService", this);
           this._messageManager.addMessageListener("SPProcessCrashService", this);
           this._messageManager.addMessageListener("SPPingService", this);
           this._messageManager.addMessageListener("SpecialPowers.Quit", this);
           this._messageManager.addMessageListener("SPPermissionManager", this);
+          this._messageManager.addMessageListener("SPWebAppService", this);
 
           this._messageManager.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
           this._messageManager.loadFrameScript(CHILD_SCRIPT_API, true);
           this._messageManager.loadFrameScript(CHILD_SCRIPT, true);
           this._isFrameScriptLoaded = true;
         }
         break;
 
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.js
+++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js
@@ -250,14 +250,27 @@ SpecialPowersObserverAPI.prototype = {
             return false;
             break;
           default:
             throw new SpecialPowersException("Invalid operation for " +
                                              "SPPermissionManager");
         }
         break;
 
+      case "SPWebAppService":
+        let Webapps = {};
+        Components.utils.import("resource://gre/modules/Webapps.jsm", Webapps);
+        switch (aMessage.json.op) {
+          case "set-launchable":
+            let val = Webapps.DOMApplicationRegistry.allAppsLaunchable;
+            Webapps.DOMApplicationRegistry.allAppsLaunchable = aMessage.json.launchable;
+            return val;
+          default:
+            throw new SpecialPowersException("Invalid operation for SPWebAppsService");
+        }
+        break;
+
       default:
         throw new SpecialPowersException("Unrecognized Special Powers API");
     }
   }
 };
 
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -829,16 +829,26 @@ SpecialPowersAPI.prototype = {
   // Disables the app install prompt for the duration of this test. There is
   // no need to re-enable the prompt at the end of the test.
   //
   // The provided callback is invoked once the prompt is disabled.
   autoConfirmAppInstall: function(cb) {
     this.pushPrefEnv({set: [['dom.mozApps.auto_confirm_install', true]]}, cb);
   },
 
+  // Allow tests to disable the per platform app validity checks so we can
+  // test higher level WebApp functionality without full platform support.
+  setAllAppsLaunchable: function(launchable) {
+    var message = {
+      op: "set-launchable",
+      launchable: launchable
+    };
+    return this._sendSyncMessage("SPWebAppService", message);
+  },
+
   addObserver: function(obs, notification, weak) {
     var obsvc = Cc['@mozilla.org/observer-service;1']
                    .getService(Ci.nsIObserverService);
     obsvc.addObserver(obs, notification, weak);
   },
   removeObserver: function(obs, notification) {
     var obsvc = Cc['@mozilla.org/observer-service;1']
                    .getService(Ci.nsIObserverService);