Bug 1200639 - add LaterRun.jsm to show pages on the Nth run of a new profile, r?jaws draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Tue, 19 Jan 2016 15:47:19 +0000
changeset 324049 caeaaa0d5277381bb78c0705c1f3eab4bca13153
parent 324048 7b09496e783d3c87b2c602314b3efc52e8e4a852
child 513319 415747244047e4a49ee66d97ceeee7b6c39a5250
push id9836
push usergijskruitbosch@gmail.com
push dateThu, 21 Jan 2016 20:57:36 +0000
reviewersjaws
bugs1200639
milestone46.0a1
Bug 1200639 - add LaterRun.jsm to show pages on the Nth run of a new profile, r?jaws
browser/app/profile/firefox.js
browser/components/nsBrowserContentHandler.js
browser/modules/LaterRun.jsm
browser/modules/moz.build
browser/modules/test/xpcshell/test_LaterRun.js
browser/modules/test/xpcshell/xpcshell.ini
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1626,8 +1626,10 @@ pref("toolkit.pageThumbs.minWidth", 280)
 pref("toolkit.pageThumbs.minHeight", 190);
 
 #ifdef NIGHTLY_BUILD
 // Enable speech synthesis, only Nightly for now
 pref("media.webspeech.synth.enabled", true);
 #endif
 
 pref("browser.esedbreader.loglevel", "Error");
+
+pref("browser.laterrun.enabled", false);
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.importGlobalProperties(["URLSearchParams"]);
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/AppConstants.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "LaterRun",
+                                  "resource:///modules/LaterRun.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShellService",
                                   "resource:///modules/ShellService.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
@@ -518,16 +520,18 @@ nsBrowserContentHandler.prototype = {
       } catch (ex) {}
       override = needHomepageOverride(prefb);
       if (override != OVERRIDE_NONE) {
         switch (override) {
           case OVERRIDE_NEW_PROFILE:
             // New profile.
             overridePage = Services.urlFormatter.formatURLPref("startup.homepage_welcome_url");
             additionalPage = Services.urlFormatter.formatURLPref("startup.homepage_welcome_url.additional");
+            // Turn on 'later run' pages for new profiles.
+            LaterRun.enabled = true;
             break;
           case OVERRIDE_NEW_MSTONE:
             // Check whether we will restore a session. If we will, we assume
             // that this is an "update" session. This does not take crashes
             // into account because that requires waiting for the session file
             // to be read. If a crash occurs after updating, before restarting,
             // we may open the startPage in addition to restoring the session.
             var ss = Components.classes["@mozilla.org/browser/sessionstartup;1"]
@@ -559,16 +563,20 @@ nsBrowserContentHandler.prototype = {
       if (firstUseOnWindows10URL && firstUseOnWindows10URL.length) {
         additionalPage = firstUseOnWindows10URL;
         if (override == OVERRIDE_NEW_PROFILE) {
           additionalPage += "&utm_content=firstrun";
         }
       }
     }
 
+    if (!additionalPage) {
+      additionalPage = LaterRun.getURL() || "";
+    }
+
     if (additionalPage && additionalPage != "about:blank") {
       if (overridePage) {
         overridePage += "|" + additionalPage;
       } else {
         overridePage = additionalPage;
       }
     }
 
new file mode 100644
--- /dev/null
+++ b/browser/modules/LaterRun.jsm
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["LaterRun"];
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setInterval", "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearInterval", "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource://gre/modules/RecentWindow.jsm");
+
+const kEnabledPref = "browser.laterrun.enabled";
+const kPagePrefRoot = "browser.laterrun.pages.";
+// Number of sessions we've been active in
+const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount";
+// Time the profile was created at:
+const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime";
+
+// After 50 sessions or 1 month since install, assume we will no longer be
+// interested in showing anything to "new" users
+const kSelfDestructSessionLimit = 50;
+const kSelfDestructHoursLimit = 31 * 24;
+
+class Page {
+  constructor({pref, minimumHoursSinceInstall, minimumSessionCount, requireBoth, url}) {
+    this.pref = pref;
+    this.minimumHoursSinceInstall = minimumHoursSinceInstall || 0;
+    this.minimumSessionCount = minimumSessionCount || 1;
+    this.requireBoth = requireBoth || false;
+    this.url = url;
+  }
+
+  get hasRun() {
+    return Preferences.get(this.pref + "hasRun", false);
+  }
+
+  applies(sessionInfo) {
+    if (this.hasRun) {
+      return false;
+    }
+    if (this.requireBoth) {
+      return sessionInfo.sessionCount >= this.minimumSessionCount &&
+             sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall;
+    }
+    return sessionInfo.sessionCount >= this.minimumSessionCount ||
+           sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall;
+  }
+}
+
+let LaterRun = {
+  init() {
+    if (!this.enabled) {
+      return;
+    }
+    // If this is the first run, set the time we were installed
+    if (!Preferences.has(kProfileCreationTime)) {
+      // We need to store seconds in order to fit within int prefs.
+      Preferences.set(kProfileCreationTime, Math.floor(Date.now() / 1000));
+    }
+    this.sessionCount++;
+
+    if (this.hoursSinceInstall > kSelfDestructHoursLimit ||
+        this.sessionCount > kSelfDestructSessionLimit) {
+      this.selfDestruct();
+      return;
+    }
+  },
+
+  // The enabled, hoursSinceInstall and sessionCount properties mirror the
+  // preferences system, and are here for convenience.
+  get enabled() {
+    return Preferences.get(kEnabledPref, false);
+  },
+
+  set enabled(val) {
+    let wasEnabled = this.enabled;
+    Preferences.set(kEnabledPref, val);
+    if (val && !wasEnabled) {
+      this.init();
+    }
+  },
+
+  get hoursSinceInstall() {
+    let installStamp = Preferences.get(kProfileCreationTime, Date.now() / 1000);
+    return Math.floor((Date.now() / 1000 - installStamp) / 3600);
+  },
+
+  get sessionCount() {
+    if (this._sessionCount) {
+      return this._sessionCount;
+    }
+    return this._sessionCount = Preferences.get(kSessionCountPref, 0);
+  },
+
+  set sessionCount(val) {
+    this._sessionCount = val;
+    Preferences.set(kSessionCountPref, val);
+  },
+
+  // Because we don't want to keep incrementing this indefinitely for no reason,
+  // we will turn ourselves off after a set amount of time/sessions (see top of
+  // file).
+  selfDestruct() {
+    Preferences.set(kEnabledPref, false);
+  },
+
+  // Create an array of Page objects based on the currently set prefs
+  readPages() {
+    // Enumerate all the pages.
+    let allPrefsForPages = Services.prefs.getChildList(kPagePrefRoot);
+    let pageDataStore = new Map();
+    for (let pref of allPrefsForPages) {
+      let [slug, prop] = pref.substring(kPagePrefRoot.length).split(".");
+      if (!pageDataStore.has(slug)) {
+        pageDataStore.set(slug, {pref: pref.substring(0, pref.length - prop.length)});
+      }
+      let defaultPrefValue = 0;
+      if (prop == "requireBoth" || prop == "hasRun") {
+        defaultPrefValue = false;
+      } else if (prop == "url") {
+        defaultPrefValue = "";
+      }
+      pageDataStore.get(slug)[prop] = Preferences.get(pref, defaultPrefValue);
+    }
+    let rv = [];
+    for (let [, pageData] of pageDataStore) {
+      if (pageData.url && pageData.url.startsWith("https")) {
+        rv.push(new Page(pageData));
+      }
+    }
+    return rv;
+  },
+
+  // Return a URL for display as a 'later run' page if its criteria are matched,
+  // or null otherwise.
+  // NB: will only return one page at a time; if multiple pages match, it's up
+  // to the preference service which one gets shown first, and the next one
+  // will be shown next startup instead.
+  getURL() {
+    if (!this.enabled) {
+      return null;
+    }
+    let pages = this.readPages();
+    let page = pages.find(page => page.applies(this));
+    if (page) {
+      Services.prefs.setBoolPref(page.pref + "hasRun", true);
+      return page.url;
+    }
+    return null;
+  },
+};
+
+LaterRun.init();
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -24,16 +24,17 @@ EXTRA_JS_MODULES += [
     'ContentWebRTC.jsm',
     'CustomizationTabPreloader.jsm',
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'HiddenFrame.jsm',
+    'LaterRun.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'PanelFrame.jsm',
     'PluginContent.jsm',
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RecentWindow.jsm',
     'RemotePrompt.jsm',
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/xpcshell/test_LaterRun.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const kEnabledPref = "browser.laterrun.enabled";
+const kPagePrefRoot = "browser.laterrun.pages.";
+const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount";
+const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource:///modules/LaterRun.jsm");
+
+Services.prefs.setBoolPref(kEnabledPref, true);
+
+add_task(function* test_page_applies() {
+  Services.prefs.setCharPref(kPagePrefRoot + "test_LaterRun_unittest.url", "https://www.mozilla.org/");
+  Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall", 10);
+  Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount", 3);
+
+  let pages = LaterRun.readPages();
+  // We have to filter the pages because it's possible Firefox ships with other URLs
+  // that get included in this test.
+  pages = pages.filter(page => page.pref == kPagePrefRoot + "test_LaterRun_unittest.");
+  Assert.equal(pages.length, 1, "Got 1 page");
+  let page = pages[0];
+  Assert.equal(page.pref, kPagePrefRoot + "test_LaterRun_unittest.", "Should know its own pref");
+  Assert.equal(page.minimumHoursSinceInstall, 10, "Needs to have 10 hours since install");
+  Assert.equal(page.minimumSessionCount, 3, "Needs to have 3 sessions");
+  Assert.equal(page.requireBoth, false, "Either requirement is enough");
+  Assert.equal(page.url, "https://www.mozilla.org/", "URL is stored correctly");
+
+  Assert.ok(page.applies({hoursSinceInstall: 1, sessionCount: 3}),
+            "Applies when session count has been met.");
+  Assert.ok(page.applies({hoursSinceInstall: 1, sessionCount: 4}),
+            "Applies when session count has been exceeded.");
+  Assert.ok(page.applies({hoursSinceInstall: 10, sessionCount: 2}),
+            "Applies when total session time has been met.");
+  Assert.ok(page.applies({hoursSinceInstall: 20, sessionCount: 2}),
+            "Applies when total session time has been exceeded.");
+  Assert.ok(page.applies({hoursSinceInstall: 10, sessionCount: 3}),
+            "Applies when both time and session count have been met.");
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 1}),
+            "Does not apply when neither time and session count have been met.");
+
+  page.requireBoth = true;
+
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 3}),
+            "Does not apply when only session count has been met.");
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 4}),
+            "Does not apply when only session count has been exceeded.");
+  Assert.ok(!page.applies({hoursSinceInstall: 10, sessionCount: 2}),
+            "Does not apply when only total session time has been met.");
+  Assert.ok(!page.applies({hoursSinceInstall: 20, sessionCount: 2}),
+            "Does not apply when only total session time has been exceeded.");
+  Assert.ok(page.applies({hoursSinceInstall: 10, sessionCount: 3}),
+            "Applies when both time and session count have been met.");
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 1}),
+            "Does not apply when neither time and session count have been met.");
+
+  // Check that pages that have run never apply:
+  Services.prefs.setBoolPref(kPagePrefRoot + "test_LaterRun_unittest.hasRun", true);
+  page.requireBoth = false;
+
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 3}),
+            "Does not apply when page has already run (sessionCount equal).");
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 4}),
+            "Does not apply when page has already run (sessionCount exceeding).");
+  Assert.ok(!page.applies({hoursSinceInstall: 10, sessionCount: 2}),
+            "Does not apply when page has already run (hoursSinceInstall equal).");
+  Assert.ok(!page.applies({hoursSinceInstall: 20, sessionCount: 2}),
+            "Does not apply when page has already run (hoursSinceInstall exceeding).");
+  Assert.ok(!page.applies({hoursSinceInstall: 10, sessionCount: 3}),
+            "Does not apply when page has already run (both criteria equal).");
+  Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 1}),
+            "Does not apply when page has already run (both criteria insufficient anyway).");
+
+  clearAllPagePrefs();
+});
+
+add_task(function* test_get_URL() {
+  Services.prefs.setIntPref(kProfileCreationTime, Math.floor((Date.now() - 11 * 60 * 60 * 1000) / 1000));
+  Services.prefs.setCharPref(kPagePrefRoot + "test_LaterRun_unittest.url", "https://www.mozilla.org/");
+  Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall", 10);
+  Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount", 3);
+  let pages = LaterRun.readPages();
+  // We have to filter the pages because it's possible Firefox ships with other URLs
+  // that get included in this test.
+  pages = pages.filter(page => page.pref == kPagePrefRoot + "test_LaterRun_unittest.");
+  Assert.equal(pages.length, 1, "Should only be 1 matching page");
+  let page = pages[0];
+  let url;
+  do {
+    url = LaterRun.getURL();
+    // We have to loop because it's possible Firefox ships with other URLs that get triggered by
+    // this test.
+  } while (url && url != "https://www.mozilla.org/");
+  Assert.equal(url, "https://www.mozilla.org/", "URL should be as expected when prefs are set.");
+  Assert.ok(Services.prefs.prefHasUserValue(kPagePrefRoot + "test_LaterRun_unittest.hasRun"), "Should have set pref");
+  Assert.ok(Services.prefs.getBoolPref(kPagePrefRoot + "test_LaterRun_unittest.hasRun"), "Should have set pref to true");
+  Assert.ok(page.hasRun, "Other page objects should know it has run, too.");
+
+  clearAllPagePrefs();
+});
+
+add_task(function* test_insecure_urls() {
+  Services.prefs.setCharPref(kPagePrefRoot + "test_LaterRun_unittest.url", "http://www.mozilla.org/");
+  Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall", 10);
+  Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount", 3);
+  let pages = LaterRun.readPages();
+  // We have to filter the pages because it's possible Firefox ships with other URLs
+  // that get triggered in this test.
+  pages = pages.filter(page => page.pref == kPagePrefRoot + "test_LaterRun_unittest.");
+  Assert.equal(pages.length, 0, "URL with non-https scheme should get ignored");
+  clearAllPagePrefs();
+});
+
+add_task(function* test_dynamic_pref_getter_setter() {
+  delete LaterRun._sessionCount;
+  Services.prefs.setIntPref(kSessionCountPref, 0);
+  Assert.equal(LaterRun.sessionCount, 0, "Should start at 0");
+
+  LaterRun.sessionCount++;
+  Assert.equal(LaterRun.sessionCount, 1, "Should increment.");
+  Assert.equal(Services.prefs.getIntPref(kSessionCountPref), 1, "Should update pref");
+});
+
+function clearAllPagePrefs() {
+  let allChangedPrefs = Services.prefs.getChildList(kPagePrefRoot);
+  for (let pref of allChangedPrefs) {
+    Services.prefs.clearUserPref(pref);
+  }
+}
+
--- a/browser/modules/test/xpcshell/xpcshell.ini
+++ b/browser/modules/test/xpcshell/xpcshell.ini
@@ -2,8 +2,9 @@
 head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_DirectoryLinksProvider.js]
 [test_SitePermissions.js]
 [test_TabGroupsMigrator.js]
+[test_LaterRun.js]