Bug 1237820: Track whether a user has been offered a sideloaded add-on or not. r=rhelmer draft
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 21 Jan 2016 11:52:01 -0800
changeset 325990 985d89739d7b6e191ed7eb827635623280d01482
parent 325989 050018247a87ce534b3aa656164b9c01bf08190e
child 325991 a1fcfc09640df8aa90dfd9d1938d6c4cda332d71
push id10072
push userdtownsend@mozilla.com
push dateTue, 26 Jan 2016 22:34:45 +0000
reviewersrhelmer
bugs1237820
milestone47.0a1
Bug 1237820: Track whether a user has been offered a sideloaded add-on or not. r=rhelmer Previously we just checked every newly sideloaded add-on to decide whether to offer it to the user for opt-in. This adds a new "seen" property (naming could be better if you have other suggestions) which tracks whether we've ever shown the opt-in UI for the add-on. It defaults to true for all add-ons and is only set to false for sideloaded add-ons that default to being disabled on install. The seen flag can be set to true through the API but cannot be reset to false as that would allow add-ons to forcibly re-present themselves to the user when disabled. The opt-in UI sets the seen flag to true only when it has focus which fixes a long-standing bug where if you accept the first add-on you see and restart the other tabs might not show up. The one slight downside of this approach is that it now requires loading the full add-ons database on every startup in order to check the seen flag for all installed add-ons. There are hacky ways we might get around this but they all involve overloading prefs with even more object data. The good thing is that we do the load and check asynchronously after most of startup is complete and the UI is fully loaded so there shouldn't be any percieved impact to startup time. I've run multiple talos runs to verify that none of the numbers appear to regress.
browser/components/nsBrowserGlue.js
toolkit/mozapps/extensions/content/newaddon.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/browser/browser_newaddon.js
toolkit/mozapps/extensions/test/browser/head.js
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js
toolkit/mozapps/extensions/test/xpcshell/test_seen.js
toolkit/mozapps/extensions/test/xpcshell/test_startup.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1136,29 +1136,34 @@ BrowserGlue.prototype = {
 
     // If there are plugins installed that are outdated, and the user hasn't
     // been warned about them yet, open the plugins update page.
     if (Services.prefs.getBoolPref(PREF_PLUGINS_NOTIFYUSER))
       this._showPluginUpdatePage();
 
     // For any add-ons that were installed disabled and can be enabled offer
     // them to the user.
-    let changedIDs = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED);
-    if (changedIDs.length > 0) {
-      let win = RecentWindow.getMostRecentBrowserWindow();
-      AddonManager.getAddonsByIDs(changedIDs, function(aAddons) {
-        aAddons.forEach(function(aAddon) {
-          // If the add-on isn't user disabled or can't be enabled then skip it.
-          if (!aAddon.userDisabled || !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE))
-            return;
-
-          win.openUILinkIn("about:newaddon?id=" + aAddon.id, "tab");
-        })
-      });
-    }
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    AddonManager.getAllAddons(addons => {
+      for (let addon of addons) {
+        // If this add-on has already seen (or seen is undefined for non-XPI
+        // add-ons) then skip it.
+        if (addon.seen !== false) {
+          continue;
+        }
+
+        // If this add-on cannot be enabled (either already enabled or
+        // appDisabled) then skip it.
+        if (!(addon.permissions & AddonManager.PERM_CAN_ENABLE)) {
+          continue;
+        }
+
+        win.openUILinkIn("about:newaddon?id=" + addon.id, "tab");
+      }
+    });
 
     let signingRequired;
     if (AppConstants.MOZ_REQUIRE_SIGNING) {
       signingRequired = true;
     } else {
       signingRequired = Services.prefs.getBoolPref("xpinstall.signatures.required");
     }
 
--- a/toolkit/mozapps/extensions/content/newaddon.js
+++ b/toolkit/mozapps/extensions/content/newaddon.js
@@ -34,20 +34,20 @@ function initialize() {
   if (!id) {
     window.location = "about:blank";
     return;
   }
 
   let bundle = Services.strings.createBundle("chrome://mozapps/locale/extensions/newaddon.properties");
 
   AddonManager.getAddonByID(id, function(aAddon) {
-    // If the add-on doesn't exist or it is already enabled or it cannot be
-    // enabled then this UI is useless, just close it. This shouldn't normally
-    // happen unless session restore restores the tab
-    if (!aAddon || !aAddon.userDisabled ||
+    // If the add-on doesn't exist or it is already enabled or it has already
+    // been seen or it cannot be enabled then this UI is useless, just close it.
+    // This shouldn't normally happen unless session restore restores the tab.
+    if (!aAddon || !aAddon.userDisabled || aAddon.seen ||
         !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE)) {
       window.close();
       return;
     }
 
     gAddon = aAddon;
 
     document.getElementById("addon-info").setAttribute("type", aAddon.type);
@@ -74,16 +74,24 @@ function initialize() {
     if (uri instanceof Ci.nsIFileURL) {
       let location = bundle.formatStringFromName("location", [uri.file.path], 1);
       locationLabel.value = location;
       locationLabel.setAttribute("tooltiptext", location);
     } else {
       document.getElementById("location").hidden = true;
     }
 
+    // Only mark the add-on as seen if the page actually gets focus
+    if (document.hasFocus()) {
+      aAddon.markAsSeen();
+    }
+    else {
+      document.addEventListener("focus", () => aAddon.markAsSeen(), false);
+    }
+
     var event = document.createEvent("Events");
     event.initEvent("AddonDisplayed", true, true);
     document.dispatchEvent(event);
   });
 }
 
 function unload() {
   AddonManager.removeAddonListener(EnableListener);
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -6521,16 +6521,17 @@ AddonInternal.prototype = {
   active: false,
   visible: false,
   userDisabled: false,
   appDisabled: false,
   softDisabled: false,
   sourceURI: null,
   releaseNotesURI: null,
   foreignInstall: false,
+  seen: true,
   skinnable: false,
 
   get selectedLocale() {
     if (this._selectedLocale)
       return this._selectedLocale;
     let locale = Locale.findClosestLocale(this.locales);
     this._selectedLocale = locale ? locale : this.defaultLocale;
     return this._selectedLocale;
@@ -6805,16 +6806,25 @@ function AddonWrapper(aAddon) {
   wrapperMap.set(this, aAddon);
 }
 
 AddonWrapper.prototype = {
   get __AddonInternal__() {
     return AppConstants.DEBUG ? addonFor(this) : undefined;
   },
 
+  get seen() {
+    return addonFor(this).seen;
+  },
+
+  markAsSeen: function() {
+    addonFor(this).seen = true;
+    XPIDatabase.saveChanges();
+  },
+
   get type() {
     return getExternalType(addonFor(this).type);
   },
 
   get aboutURL() {
     return this.isActive ? addonFor(this)["aboutURL"] : null;
   },
 
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -73,17 +73,18 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "internalName", "updateURL", "updateKey", "optionsURL",
                           "optionsType", "aboutURL", "icons", "iconURL", "icon64URL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "descriptor", "installDate",
                           "updateDate", "applyBackgroundUpdates", "bootstrap",
                           "skinnable", "size", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall", "hasBinaryComponents",
                           "strictCompatibility", "locales", "targetApplications",
-                          "targetPlatforms", "multiprocessCompatible", "signedState"];
+                          "targetPlatforms", "multiprocessCompatible", "signedState",
+                          "seen"];
 
 // Properties that should be migrated where possible from an old database. These
 // shouldn't include properties that can be read directly from install.rdf files
 // or calculated
 const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
                             "sourceURI", "applyBackgroundUpdates",
                             "releaseNotesURI", "foreignInstall", "syncGUID"];
 
@@ -276,17 +277,18 @@ function stepStatement(aStatement) {
  * @param  aTarget
  *         An optional target object to copy the properties to
  * @return the object that the properties were copied onto
  */
 function copyProperties(aObject, aProperties, aTarget) {
   if (!aTarget)
     aTarget = {};
   aProperties.forEach(function(aProp) {
-    aTarget[aProp] = aObject[aProp];
+    if (aProp in aObject)
+      aTarget[aProp] = aObject[aProp];
   });
   return aTarget;
 }
 
 /**
  * Copies properties from a mozIStorageRow to an object. If no target object is
  * passed a new object will be created and returned.
  *
@@ -1297,16 +1299,17 @@ this.XPIDatabase = {
    * @return The DBAddonInternal that was added to the database
    */
   updateAddonMetadata: function(aOldAddon, aNewAddon, aDescriptor) {
     this.removeAddonMetadata(aOldAddon);
     aNewAddon.syncGUID = aOldAddon.syncGUID;
     aNewAddon.installDate = aOldAddon.installDate;
     aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
     aNewAddon.foreignInstall = aOldAddon.foreignInstall;
+    aNewAddon.seen = aOldAddon.seen;
     aNewAddon.active = (aNewAddon.visible && !aNewAddon.disabled && !aNewAddon.pendingUninstall);
 
     // addAddonMetadata does a saveChanges()
     return this.addAddonMetadata(aNewAddon, aDescriptor);
   },
 
   /**
    * Synchronously removes an add-on from the database.
@@ -1701,16 +1704,17 @@ this.XPIDatabaseReconcile = {
     if (isDetectedInstall && aNewAddon.foreignInstall) {
       // If the add-on is a foreign install and is in a scope where add-ons
       // that were dropped in should default to disabled then disable it
       let disablingScopes = Preferences.get(PREF_EM_AUTO_DISABLED_SCOPES, 0);
       if (aInstallLocation.scope & disablingScopes) {
         logger.warn("Disabling foreign installed add-on " + aNewAddon.id + " in "
             + aInstallLocation.name);
         aNewAddon.userDisabled = true;
+        aNewAddon.seen = false;
       }
     }
 
     return XPIDatabase.addAddonMetadata(aNewAddon, aAddonState.descriptor);
   },
 
   /**
    * Called when an add-on has been removed.
--- a/toolkit/mozapps/extensions/test/browser/browser_newaddon.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_newaddon.js
@@ -1,43 +1,48 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests the new add-on tab
 
 var gProvider;
 
-function loadPage(aURL, aCallback) {
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(aURL);
-  gBrowser.addEventListener("AddonDisplayed", function(event) {
-    gBrowser.removeEventListener("AddonDisplayed", arguments.callee, false);
+function loadPage(aURL, aCallback, aBackground = false) {
+  let tab = gBrowser.addTab();
+  if (!aBackground)
+    gBrowser.selectedTab = tab;
+  let browser = tab.linkedBrowser;
+  browser.loadURI(aURL);
+  browser.addEventListener("AddonDisplayed", function(event) {
+    browser.removeEventListener("AddonDisplayed", arguments.callee, false);
 
-    aCallback(gBrowser.selectedTab);
+    aCallback(tab);
   });
 }
 
 function test() {
   waitForExplicitFinish();
 
   gProvider = new MockProvider();
 
   gProvider.createAddons([{
     id: "addon1@tests.mozilla.org",
     name: "Test 1",
     version: "5.3",
     userDisabled: true,
+    seen: false,
     operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
   }, {
     id: "addon2@tests.mozilla.org",
     name: "Test 2",
     version: "7.1",
     creator: "Dave Townsend",
-    userDisabled: true
+    userDisabled: true,
+    seen: false
   }]);
 
   run_next_test();
 }
 
 function end_test() {
   finish();
 }
@@ -49,26 +54,29 @@ add_test(function() {
     is(doc.getElementById("name").value, "Test 1 5.3", "Should say the right name");
 
     is_element_hidden(doc.getElementById("author"), "Should be no author displayed");
     is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
 
     is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
        "Should be showing the right buttons");
 
-    EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
-                                       {}, aTab.linkedBrowser.contentWindow);
+    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+      ok(aAddon.seen, "Add-on should have been marked as seen");
 
-    is(gBrowser.tabs.length, 1, "Page should have been closed");
+      EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+                                         {}, aTab.linkedBrowser.contentWindow);
 
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+      is(gBrowser.tabs.length, 1, "Page should have been closed");
+
       ok(aAddon.userDisabled, "Add-on should not have been enabled");
 
       ok(!aAddon.isActive, "Add-on should not be running");
 
+      aAddon.seen = false;
       run_next_test();
     });
   });
 });
 
 // Tests that enabling a restartless add-on works
 add_test(function() {
   loadPage("about:newaddon?id=addon1@tests.mozilla.org", function(aTab) {
@@ -76,29 +84,33 @@ add_test(function() {
     is(doc.getElementById("name").value, "Test 1 5.3", "Should say the right name");
 
     is_element_hidden(doc.getElementById("author"), "Should be no author displayed");
     is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
 
     is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
        "Should be showing the right buttons");
 
-    EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
-                                       {}, aTab.linkedBrowser.contentWindow);
+    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+      ok(aAddon.seen, "Add-on should have been marked as seen");
+
+      EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
+                                         {}, aTab.linkedBrowser.contentWindow);
 
-    EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
-                                       {}, aTab.linkedBrowser.contentWindow);
+      EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+                                         {}, aTab.linkedBrowser.contentWindow);
 
-    is(gBrowser.tabs.length, 1, "Page should have been closed");
+      is(gBrowser.tabs.length, 1, "Page should have been closed");
 
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
       ok(!aAddon.userDisabled, "Add-on should now have been enabled");
 
       ok(aAddon.isActive, "Add-on should now be running");
 
+      aAddon.userDisabled = true;
+      aAddon.seen = false;
       run_next_test();
     });
   });
 });
 
 // Tests that ignoring a non-restartless add-on works
 add_test(function() {
   loadPage("about:newaddon?id=addon2@tests.mozilla.org", function(aTab) {
@@ -107,26 +119,29 @@ add_test(function() {
 
     is_element_visible(doc.getElementById("author"), "Should be an author displayed");
     is(doc.getElementById("author").value, "By Dave Townsend", "Should have the right author");
     is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
 
     is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
        "Should be showing the right buttons");
 
-    EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
-                                       {}, aTab.linkedBrowser.contentWindow);
+    AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
+      ok(aAddon.seen, "Add-on should have been marked as seen");
 
-    is(gBrowser.tabs.length, 1, "Page should have been closed");
+      EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+                                         {}, aTab.linkedBrowser.contentWindow);
 
-    AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
+      is(gBrowser.tabs.length, 1, "Page should have been closed");
+
       ok(aAddon.userDisabled, "Add-on should not have been enabled");
 
       ok(!aAddon.isActive, "Add-on should not be running");
 
+      aAddon.seen = false;
       run_next_test();
     });
   });
 });
 
 // Tests that enabling a non-restartless add-on works
 add_test(function() {
   loadPage("about:newaddon?id=addon2@tests.mozilla.org", function(aTab) {
@@ -135,26 +150,28 @@ add_test(function() {
 
     is_element_visible(doc.getElementById("author"), "Should be an author displayed");
     is(doc.getElementById("author").value, "By Dave Townsend", "Should have the right author");
     is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
 
     is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
        "Should be showing the right buttons");
 
-    EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
-                                       {}, aTab.linkedBrowser.contentWindow);
+    AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
+      ok(aAddon.seen, "Add-on should have been marked as seen");
+
+      EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
+                                         {}, aTab.linkedBrowser.contentWindow);
 
-    EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
-                                       {}, aTab.linkedBrowser.contentWindow);
+      EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+                                         {}, aTab.linkedBrowser.contentWindow);
 
-    is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("restartPanel"),
-       "Should be showing the right buttons");
+      is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("restartPanel"),
+         "Should be showing the right buttons");
 
-    AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
       ok(!aAddon.userDisabled, "Add-on should now have been enabled");
 
       ok(!aAddon.isActive, "Add-on should not be running");
 
       ok(doc.getElementById("allow").disabled, "Should have disabled checkbox");
 
       EventUtils.synthesizeMouseAtCenter(doc.getElementById("cancel-button"),
                                          {}, aTab.linkedBrowser.contentWindow);
@@ -175,12 +192,41 @@ add_test(function() {
                                          {}, aTab.linkedBrowser.contentWindow);
 
       ok(aAddon.userDisabled, "Add-on should not have been enabled");
 
       ok(!aAddon.isActive, "Add-on should not be running");
 
       is(gBrowser.tabs.length, 1, "Page should have been closed");
 
+      aAddon.seen = false;
       run_next_test();
     });
   });
 });
+
+// Tests that opening the page in the background doesn't mark as seen
+add_test(function() {
+  loadPage("about:newaddon?id=addon1@tests.mozilla.org", function(aTab) {
+    var doc = aTab.linkedBrowser.contentDocument;
+    is(doc.getElementById("name").value, "Test 1 5.3", "Should say the right name");
+
+    is_element_hidden(doc.getElementById("author"), "Should be no author displayed");
+    is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
+
+    is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+       "Should be showing the right buttons");
+
+    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+      ok(!aAddon.seen, "Add-on should not have been marked as seen.");
+
+      gBrowser.selectedTab = aTab;
+
+      waitForFocus(function() {
+        ok(aAddon.seen, "Add-on should have been marked as seen after focusing the tab.");
+
+        gBrowser.removeTab(aTab);
+
+        run_next_test();
+      }, aTab.linkedBrowser.contentWindow);
+    });
+  }, true);
+});
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -1222,16 +1222,20 @@ MockAddon.prototype = {
   cancelUninstall: function() {
     if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL))
       throw Components.Exception("Add-on is not pending uninstall");
 
     this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
   },
 
+  markAsSeen: function() {
+    this.seen = true;
+  },
+
   _updateActiveState: function(currentActive, newActive) {
     if (currentActive == newActive)
       return;
 
     if (newActive == this.isActive) {
       this.pendingOperations -= (newActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE);
       AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
     }
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1934,19 +1934,21 @@ function do_exception_wrap(func) {
       do_report_unexpected_exception(e);
     }
   };
 }
 
 /**
  * Change the schema version of the JSON extensions database
  */
-function changeXPIDBVersion(aNewVersion) {
+function changeXPIDBVersion(aNewVersion, aMutator = undefined) {
   let jData = loadJSON(gExtensionsJSON);
   jData.schemaVersion = aNewVersion;
+  if (aMutator)
+    aMutator(jData);
   saveJSON(jData, gExtensionsJSON);
 }
 
 /**
  * Load a file into a string
  */
 function loadFile(aFile) {
   let data = "";
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
@@ -158,67 +158,74 @@ function run_test() {
                                                                       t2]) {
     // addon1 was user and app enabled in the old extensions.rdf
     do_check_neq(a1, null);
     do_check_false(a1.userDisabled);
     do_check_false(a1.appDisabled);
     do_check_true(a1.isActive);
     do_check_true(isExtensionInAddonsList(profileDir, a1.id));
     do_check_false(a1.hasBinaryComponents);
+    do_check_true(a1.seen);
 
     // addon2 was user disabled and app enabled in the old extensions.rdf
     do_check_neq(a2, null);
     do_check_true(a2.userDisabled);
     do_check_false(a2.appDisabled);
     do_check_false(a2.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a2.id));
     do_check_false(a2.hasBinaryComponents);
+    do_check_true(a2.seen);
 
     // addon3 was pending user disable and app disabled in the old extensions.rdf
     do_check_neq(a3, null);
     do_check_true(a3.userDisabled);
     do_check_true(a3.appDisabled);
     do_check_false(a3.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a3.id));
     do_check_false(a3.hasBinaryComponents);
+    do_check_true(a3.seen);
 
     // addon4 was pending user enable and app disabled in the old extensions.rdf
     do_check_neq(a4, null);
     do_check_false(a4.userDisabled);
     do_check_true(a4.appDisabled);
     do_check_false(a4.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a4.id));
     do_check_false(a4.hasBinaryComponents);
+    do_check_true(a4.seen);
 
     // addon5 was disabled and compatible but a new version has been installed
     // since, it should still be disabled but should be incompatible
     do_check_neq(a5, null);
     do_check_true(a5.userDisabled);
     do_check_true(a5.appDisabled);
     do_check_false(a5.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a5.id));
     do_check_false(a5.hasBinaryComponents);
+    do_check_true(a5.seen);
 
     // addon6, addon7 and addon8 will have been lost as they were staged in the
     // pre-Firefox 4.0 directory
     do_check_eq(a6, null);
     do_check_eq(a7, null);
     do_check_eq(a8, null);
 
     // Theme 1 was previously enabled
     do_check_neq(t1, null);
     do_check_false(t1.userDisabled);
     do_check_false(t1.appDisabled);
     do_check_true(t1.isActive);
     do_check_true(isThemeInAddonsList(profileDir, t1.id));
     do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+    do_check_true(t1.seen);
 
     // Theme 2 was previously disabled
-    do_check_neq(t1, null);
+    do_check_neq(t2, null);
     do_check_true(t2.userDisabled);
     do_check_false(t2.appDisabled);
     do_check_false(t2.isActive);
     do_check_false(isThemeInAddonsList(profileDir, t2.id));
     do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+    do_check_true(t2.seen);
 
     do_execute_soon(do_test_finished);
   });
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js
@@ -198,62 +198,70 @@ function run_test() {
                                function([a1, a2, a3, a4, a5, a6, a7, a8]) {
     // addon1 was enabled in the database
     do_check_neq(a1, null);
     do_check_false(a1.userDisabled);
     do_check_false(a1.appDisabled);
     do_check_true(a1.isActive);
     do_check_false(a1.strictCompatibility);
     do_check_false(a1.foreignInstall);
+    do_check_true(a1.seen);
     // addon2 was disabled in the database
     do_check_neq(a2, null);
     do_check_true(a2.userDisabled);
     do_check_false(a2.appDisabled);
     do_check_false(a2.isActive);
     do_check_false(a2.strictCompatibility);
     do_check_false(a2.foreignInstall);
+    do_check_true(a2.seen);
     // addon3 was pending-disable in the database
     do_check_neq(a3, null);
     do_check_true(a3.userDisabled);
     do_check_false(a3.appDisabled);
     do_check_false(a3.isActive);
     do_check_false(a3.strictCompatibility);
     do_check_false(a3.foreignInstall);
+    do_check_true(a3.seen);
     // addon4 was pending-enable in the database
     do_check_neq(a4, null);
     do_check_false(a4.userDisabled);
     do_check_false(a4.appDisabled);
     do_check_true(a4.isActive);
     do_check_true(a4.strictCompatibility);
     do_check_false(a4.foreignInstall);
+    do_check_true(a4.seen);
     // addon5 was enabled in the database but needed a compatibility update
     do_check_neq(a5, null);
     do_check_false(a5.userDisabled);
     do_check_false(a5.appDisabled);
     do_check_true(a5.isActive);
     do_check_false(a5.strictCompatibility);
     do_check_false(a5.foreignInstall);
+    do_check_true(a5.seen);
     // addon6 was disabled and compatible but a new version has been installed
     // since, it should still be disabled but should be incompatible
     do_check_neq(a6, null);
     do_check_true(a6.userDisabled);
     do_check_true(a6.appDisabled);
     do_check_false(a6.isActive);
     do_check_false(a6.strictCompatibility);
     do_check_false(a6.foreignInstall);
+    do_check_true(a6.seen);
     // addon7 is in the global install location so should be a foreignInstall
     do_check_neq(a7, null);
     do_check_false(a7.userDisabled);
     do_check_false(a7.appDisabled);
     do_check_true(a7.isActive);
     do_check_false(a7.strictCompatibility);
     do_check_true(a7.foreignInstall);
+    do_check_true(a7.seen);
     // addon8 is in the user install location so should be a foreignInstall
     do_check_neq(a8, null);
     do_check_false(a8.userDisabled);
     do_check_false(a8.appDisabled);
     do_check_true(a8.isActive);
     do_check_false(a8.strictCompatibility);
     do_check_true(a8.foreignInstall);
+    do_check_true(a8.seen);
 
     do_execute_soon(do_test_finished);
   });
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
@@ -161,62 +161,69 @@ function run_test() {
                                                                       a4, a5, a6,
                                                                       a7, t1, t2]) {
     // addon1 was user and app enabled in the old extensions.rdf
     do_check_neq(a1, null);
     do_check_false(a1.userDisabled);
     do_check_false(a1.appDisabled);
     do_check_true(a1.isActive);
     do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+    do_check_true(a1.seen);
 
     // addon2 was user disabled and app enabled in the old extensions.rdf
     do_check_neq(a2, null);
     do_check_true(a2.userDisabled);
     do_check_false(a2.appDisabled);
     do_check_false(a2.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+    do_check_true(a2.seen);
 
     // addon3 was pending user disable and app disabled in the old extensions.rdf
     do_check_neq(a3, null);
     do_check_true(a3.userDisabled);
     do_check_true(a3.appDisabled);
     do_check_false(a3.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+    do_check_true(a3.seen);
 
     // addon4 was pending user enable and app disabled in the old extensions.rdf
     do_check_neq(a4, null);
     do_check_false(a4.userDisabled);
     do_check_true(a4.appDisabled);
     do_check_false(a4.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+    do_check_true(a4.seen);
 
     // addon5 was disabled and compatible but a new version has been installed
     // since, it should still be disabled but should be incompatible
     do_check_neq(a5, null);
     do_check_true(a5.userDisabled);
     do_check_true(a5.appDisabled);
     do_check_false(a5.isActive);
     do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+    do_check_true(a5.seen);
 
     // addon6 and addon7 will have been lost as they were staged in the
     // pre-Firefox 4.0 directory
     do_check_eq(a6, null);
     do_check_eq(a7, null);
 
     // Theme 1 was previously disabled
     do_check_neq(t1, null);
     do_check_true(t1.userDisabled);
     do_check_false(t1.appDisabled);
     do_check_false(t1.isActive);
     do_check_true(isThemeInAddonsList(profileDir, t1.id));
     do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+    do_check_true(t1.seen);
 
     // Theme 2 was previously disabled
-    do_check_neq(t1, null);
+    do_check_neq(t2, null);
     do_check_true(t2.userDisabled);
     do_check_false(t2.appDisabled);
     do_check_false(t2.isActive);
     do_check_false(isThemeInAddonsList(profileDir, t2.id));
     do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+    do_check_true(t2.seen);
 
     do_execute_soon(do_test_finished);
   });
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js
@@ -167,17 +167,22 @@ function prepare_profile() {
 }
 
 function perform_migration() {
   shutdownManager();
   
   // Turn on disabling for all scopes
   Services.prefs.setIntPref("extensions.autoDisableScopes", 15);
 
-  changeXPIDBVersion(1);
+  changeXPIDBVersion(1, data => {
+    // Delete the seen property from all add-ons to make sure it defaults to true
+    for (let addon of data.addons) {
+      delete addon.seen;
+    }
+  });
   Services.prefs.setIntPref("extensions.databaseSchema", 1);
 
   gAppInfo.version = "2"
   startupManager(true);
   test_results();
 }
 
 function test_results() {
@@ -200,104 +205,113 @@ function test_results() {
     // addon1 was enabled
     do_check_neq(a1, null);
     do_check_eq(a1.syncGUID, oldSyncGUIDs[a1.id]);
     do_check_false(a1.userDisabled);
     do_check_false(a1.appDisabled);
     do_check_true(a1.isActive);
     do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
     do_check_true(a1.foreignInstall);
+    do_check_true(a1.seen);
     do_check_false(a1.hasBinaryComponents);
     do_check_false(a1.strictCompatibility);
 
     // addon2 was disabled
     do_check_neq(a2, null);
     do_check_eq(a2.syncGUID, oldSyncGUIDs[a2.id]);
     do_check_true(a2.userDisabled);
     do_check_false(a2.appDisabled);
     do_check_false(a2.isActive);
     do_check_eq(a2.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE);
     do_check_true(a2.foreignInstall);
+    do_check_true(a2.seen);
     do_check_false(a2.hasBinaryComponents);
     do_check_false(a2.strictCompatibility);
 
     // addon3 was pending-disable in the database
     do_check_neq(a3, null);
     do_check_eq(a3.syncGUID, oldSyncGUIDs[a3.id]);
     do_check_true(a3.userDisabled);
     do_check_false(a3.appDisabled);
     do_check_false(a3.isActive);
     do_check_eq(a3.applyBackgroundUpdates, AddonManager.AUTOUPDATE_ENABLE);
     do_check_true(a3.foreignInstall);
+    do_check_true(a3.seen);
     do_check_false(a3.hasBinaryComponents);
     do_check_false(a3.strictCompatibility);
 
     // addon4 was pending-enable in the database
     do_check_neq(a4, null);
     do_check_eq(a4.syncGUID, oldSyncGUIDs[a4.id]);
     do_check_false(a4.userDisabled);
     do_check_false(a4.appDisabled);
     do_check_true(a4.isActive);
     do_check_eq(a4.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
     do_check_true(a4.foreignInstall);
+    do_check_true(a4.seen);
     do_check_false(a4.hasBinaryComponents);
     do_check_true(a4.strictCompatibility);
 
     // addon5 was enabled in the database but needed a compatibility update
     do_check_neq(a5, null);
     do_check_false(a5.userDisabled);
     do_check_false(a5.appDisabled);
     do_check_true(a5.isActive);
     do_check_eq(a4.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
     do_check_true(a5.foreignInstall);
+    do_check_true(a5.seen);
     do_check_false(a5.hasBinaryComponents);
     do_check_false(a5.strictCompatibility);
 
     // addon6 was disabled and compatible but a new version has been installed
     do_check_neq(a6, null);
     do_check_eq(a6.syncGUID, oldSyncGUIDs[a6.id]);
     do_check_eq(a6.version, "2.0");
     do_check_true(a6.userDisabled);
     do_check_false(a6.appDisabled);
     do_check_false(a6.isActive);
     do_check_eq(a6.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
     do_check_true(a6.foreignInstall);
+    do_check_true(a6.seen);
     do_check_eq(a6.sourceURI.spec, "http://localhost:" + gPort + "/addons/test_migrate4_6.xpi");
     do_check_eq(a6.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
     do_check_false(a6.hasBinaryComponents);
     do_check_false(a6.strictCompatibility);
 
     // addon7 was installed manually
     do_check_neq(a7, null);
     do_check_eq(a7.version, "1.0");
     do_check_false(a7.userDisabled);
     do_check_false(a7.appDisabled);
     do_check_true(a7.isActive);
     do_check_eq(a7.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
     do_check_false(a7.foreignInstall);
+    do_check_true(a7.seen);
     do_check_eq(a7.sourceURI.spec, "http://localhost:" + gPort + "/addons/test_migrate4_7.xpi");
     do_check_eq(a7.releaseNotesURI, null);
     do_check_false(a7.hasBinaryComponents);
     do_check_false(a7.strictCompatibility);
 
     // addon8 was enabled and has binary components
     do_check_neq(a8, null);
     do_check_false(a8.userDisabled);
     do_check_false(a8.appDisabled);
     do_check_true(a8.isActive);
     do_check_false(a8.foreignInstall);
+    do_check_true(a8.seen);
     do_check_true(a8.hasBinaryComponents);
     do_check_false(a8.strictCompatibility);
 
     // addon9 is the active theme
     do_check_neq(a9, null);
     do_check_false(a9.userDisabled);
     do_check_false(a9.appDisabled);
     do_check_true(a9.isActive);
     do_check_false(a9.foreignInstall);
+    do_check_true(a9.seen);
     do_check_false(a9.hasBinaryComponents);
     do_check_true(a9.strictCompatibility);
 
     testserver.stop(do_test_finished);
   });
 }
 
 function run_test() {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_seen.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+
+let profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+startupManager();
+
+// Installing an add-on through the API should mark it as seen
+add_task(function*() {
+  let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+  yield promiseCompleteAllInstalls([install]);
+  do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+  do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+  let addon = install.addon;
+  do_check_eq(addon.version, "1.0");
+  do_check_false(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_false(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  // Installing an update should retain that
+  install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), resolve));
+  yield promiseCompleteAllInstalls([install]);
+  do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+  do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+  addon = install.addon;
+  do_check_eq(addon.version, "2.0");
+  do_check_false(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_false(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+  let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+  // Make sure the startup code will detect sideloaded updates
+  setExtensionModifiedTime(path, path.lastModifiedTime - 10000);
+
+  startupManager();
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "1.0");
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  yield promiseShutdownManager();
+
+  // Sideloading an update shouldn't change the state
+  manuallyUninstall(profileDir, ID);
+  manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir, ID);
+
+  startupManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "2.0");
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+  let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+  // Make sure the startup code will detect sideloaded updates
+  setExtensionModifiedTime(path, path.lastModifiedTime - 10000);
+
+  startupManager();
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "1.0");
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  // Updating through the API shouldn't change the state
+  install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), resolve));
+  yield promiseCompleteAllInstalls([install]);
+  do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+  do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+  addon = install.addon;
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "2.0");
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+  let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+  // Make sure the startup code will detect sideloaded updates
+  setExtensionModifiedTime(path, path.lastModifiedTime - 10000);
+
+  startupManager();
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "1.0");
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+  addon.markAsSeen();
+  do_check_true(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_true(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  yield promiseShutdownManager();
+
+  // Sideloading an update shouldn't change the state
+  manuallyUninstall(profileDir, ID);
+  manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir, ID);
+
+  startupManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "2.0");
+  do_check_true(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+  let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+  // Make sure the startup code will detect sideloaded updates
+  setExtensionModifiedTime(path, path.lastModifiedTime - 10000);
+
+  startupManager();
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "1.0");
+  do_check_true(addon.foreignInstall);
+  do_check_false(addon.seen);
+  addon.markAsSeen();
+  do_check_true(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_true(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  // Updating through the API shouldn't change the state
+  install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), resolve));
+  yield promiseCompleteAllInstalls([install]);
+  do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+  do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+  addon = install.addon;
+  do_check_true(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_eq(addon.version, "2.0");
+  do_check_true(addon.foreignInstall);
+  do_check_true(addon.seen);
+
+  addon.uninstall();
+  yield promiseShutdownManager();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
@@ -207,44 +207,47 @@ function run_test_1() {
     do_check_eq(a1.name, "Test 1");
     do_check_true(isExtensionInAddonsList(profileDir, a1.id));
     do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
     do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
     do_check_in_crash_annotation(addon1.id, addon1.version);
     do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
     do_check_eq(a1.sourceURI, null);
     do_check_true(a1.foreignInstall);
+    do_check_false(a1.seen);
 
     do_check_neq(a2, null);
     do_check_eq(a2.id, "addon2@tests.mozilla.org");
     do_check_neq(a2.syncGUID, null);
     do_check_true(a2.syncGUID.length >= 9);
     do_check_eq(a2.version, "2.0");
     do_check_eq(a2.name, "Test 2");
     do_check_true(isExtensionInAddonsList(profileDir, a2.id));
     do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
     do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
     do_check_in_crash_annotation(addon2.id, addon2.version);
     do_check_eq(a2.scope, AddonManager.SCOPE_PROFILE);
     do_check_eq(a2.sourceURI, null);
     do_check_true(a2.foreignInstall);
+    do_check_false(a2.seen);
 
     do_check_neq(a3, null);
     do_check_eq(a3.id, "addon3@tests.mozilla.org");
     do_check_neq(a3.syncGUID, null);
     do_check_true(a3.syncGUID.length >= 9);
     do_check_eq(a3.version, "3.0");
     do_check_eq(a3.name, "Test 3");
     do_check_true(isExtensionInAddonsList(profileDir, a3.id));
     do_check_true(hasFlag(a3.permissions, AddonManager.PERM_CAN_UNINSTALL));
     do_check_true(hasFlag(a3.permissions, AddonManager.PERM_CAN_UPGRADE));
     do_check_in_crash_annotation(addon3.id, addon3.version);
     do_check_eq(a3.scope, AddonManager.SCOPE_PROFILE);
     do_check_eq(a3.sourceURI, null);
     do_check_true(a3.foreignInstall);
+    do_check_false(a3.seen);
 
     do_check_eq(a4, null);
     do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
     dest = profileDir.clone();
     dest.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
     do_check_false(dest.exists());
 
     do_check_eq(a5, null);
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -275,16 +275,17 @@ skip-if = os == "android"
 [test_update_ignorecompat.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_updatecheck.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses hardcoded ports in xpi files.
 [test_json_updatecheck.js]
+[test_seen.js]
 [test_updateid.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses hardcoded ports in xpi files.
 [test_update_compatmode.js]
 [test_upgrade.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -4,16 +4,17 @@ tags = addons
 head = head_addons.js
 tail =
 firefox-appdir = browser
 dupe-manifest =
 support-files =
   data/**
   xpcshell-shared.ini
 
+
 [test_addon_path_service.js]
 [test_asyncBlocklistLoad.js]
 [test_cacheflush.js]
 [test_DeferredSave.js]
 [test_gmpProvider.js]
 skip-if = appname != "firefox"
 [test_hotfix_cert.js]
 [test_isReady.js]