Bug 1381368 - Use AsyncPref for onboarding module. draft
authorRex Lee <rexboy@mozilla.com>
Mon, 17 Jul 2017 19:04:36 +0800
changeset 614173 c0da0d86dfc7f25201a266dde9aca9ed9031a2d1
parent 614015 5928d905c0bc0b28f5488b236444c7d7991cf8d4
child 638804 b2e155f71fb028e310cc0e0867a9c3e5cd9442e5
push id69946
push userbmo:rexboy@mozilla.com
push dateMon, 24 Jul 2017 08:44:10 +0000
bugs1381368
milestone56.0a1
Bug 1381368 - Use AsyncPref for onboarding module. MozReview-Commit-ID: F1h3l51lvXg
browser/extensions/onboarding/bootstrap.js
browser/extensions/onboarding/content/onboarding.js
toolkit/modules/AsyncPrefs.jsm
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -4,18 +4,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* globals  APP_STARTUP, ADDON_INSTALL */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OnboardingTourType",
   "resource://onboarding/modules/OnboardingTourType.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
-  "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
   "resource://gre/modules/FxAccounts.jsm");
 
 const BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished";
 const BROWSER_SESSION_STORE_NOTIFICATION = "sessionstore-windows-restored";
 const PREF_WHITELIST = [
@@ -36,47 +34,16 @@ const PREF_WHITELIST = [
   "onboarding-tour-private-browsing",
   "onboarding-tour-search",
   "onboarding-tour-singlesearch",
   "onboarding-tour-sync",
 ].forEach(tourId => PREF_WHITELIST.push(`browser.onboarding.tour.${tourId}.completed`));
 
 let waitingForBrowserReady = true;
 
-/**
- * Set pref. Why no `getPrefs` function is due to the priviledge level.
- * We cannot set prefs inside a framescript but can read.
- * For simplicity and effeciency, we still read prefs inside the framescript.
- *
- * @param {Array} prefs the array of prefs to set.
- *   The array element carrys info to set pref, should contain
- *   - {String} name the pref name, such as `browser.onboarding.hidden`
- *   - {*} value the value to set
- **/
-function setPrefs(prefs) {
-  prefs.forEach(pref => {
-    if (PREF_WHITELIST.includes(pref.name)) {
-      Preferences.set(pref.name, pref.value);
-    }
-  });
-}
-
-/**
- * Listen and process events from content.
- */
-function initContentMessageListener() {
-  Services.mm.addMessageListener("Onboarding:OnContentMessage", msg => {
-    switch (msg.data.action) {
-      case "set-prefs":
-        setPrefs(msg.data.params);
-        break;
-    }
-  });
-}
-
 let syncTourChecker = {
   registered: false,
 
   observe() {
     this.setComplete();
   },
 
   init() {
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -4,27 +4,41 @@
 
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs",
+  "resource://gre/modules/AsyncPrefs.jsm");
+
+
 
 const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css";
 const ABOUT_HOME_URL = "about:home";
 const ABOUT_NEWTAB_URL = "about:newtab";
 const BUNDLE_URI = "chrome://onboarding/locale/onboarding.properties";
 const UITOUR_JS_URI = "resource://onboarding/lib/UITour-lib.js";
 const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
 const BRAND_SHORT_NAME = Services.strings
                      .createBundle("chrome://branding/locale/brand.properties")
                      .GetStringFromName("brandShortName");
 const PROMPT_COUNT_PREF = "browser.onboarding.notification.prompt-count";
+const NOTIFICATION_LAST_TIME_TOUR_CHANGE_PREF = "browser.onboarding.notification.last-time-of-changing-tour-sec";
+const NOTIFICATION_ID_QUEUE_PREF = "browser.onboarding.notification.tour-ids-queue";
+const NOTIFICATION_FINISHED_PREF = "browser.onboarding.notification.finished";
+const NOTIFICATION_MUTE_DURATION_ON_FIRST_PREF = "browser.onboarding.notification.mute-duration-on-first-session-ms"
+const NOTIFICATION_MAX_COUNT_PER_TOUR_PREF = "browser.onboarding.notification.max-prompt-count-per-tour";
+const NOTIFICATION_MAX_LIFETIME_PER_TOUR_PREF = "browser.onboarding.notification.max-life-time-per-tour-ms";
+const HIDDEN_PREF = "browser.onboarding.hidden";
+const ENABLED_PREF = "browser.onboarding.enabled";
+const TOUR_TYPE_PREF = "browser.onboarding.tour-type";
 
 /**
  * Add any number of tours, key is the tourId, value should follow the format below
  * "tourId": { // The short tour id which could be saved in pref
  *   // The unique tour id
  *   id: "onboarding-tour-addons",
  *   // The string id of tour name which would be displayed on the navigation bar
  *   tourNameId: "onboarding.tour-addon",
@@ -312,17 +326,17 @@ var onboardingTourset = {
 class Onboarding {
   constructor(contentWindow) {
     this.init(contentWindow);
   }
 
   async init(contentWindow) {
     this._window = contentWindow;
     this._tours = [];
-    this._tourType = Services.prefs.getStringPref("browser.onboarding.tour-type", "update");
+    this._tourType = Services.prefs.getStringPref(TOUR_TYPE_PREF, "update");
 
     let tourIds = this._getTourIDList();
     tourIds.forEach(tourId => {
       if (onboardingTourset[tourId]) {
         this._tours.push(onboardingTourset[tourId]);
       }
     });
 
@@ -404,17 +418,17 @@ class Onboarding {
   }
 
   _initPrefObserver() {
     if (this._prefsObserved) {
       return;
     }
 
     this._prefsObserved = new Map();
-    this._prefsObserved.set("browser.onboarding.hidden", prefValue => {
+    this._prefsObserved.set(HIDDEN_PREF, prefValue => {
       if (prefValue) {
         this.destroy();
       }
     });
     this._tours.forEach(tour => {
       let tourId = tour.id;
       this._prefsObserved.set(`browser.onboarding.tour.${tourId}.completed`, () => {
         this.markTourCompletionState(tourId);
@@ -429,25 +443,16 @@ class Onboarding {
     if (this._prefsObserved) {
       for (let [name, callback] of this._prefsObserved) {
         Preferences.ignore(name, callback);
       }
       this._prefsObserved = null;
     }
   }
 
-  /**
-   * @param {String} action the action to ask the chrome to do
-   * @param {Array} params the parameters for the action
-   */
-  sendMessageToChrome(action, params) {
-    sendAsyncMessage("Onboarding:OnContentMessage", {
-      action, params
-    });
-  }
 
   handleEvent(evt) {
     if (evt.type === "resize") {
       this._window.cancelIdleCallback(this._resizeTimerId);
       this._resizeTimerId =
         this._window.requestIdleCallback(() => this._resizeUI());
 
       return;
@@ -537,118 +542,96 @@ class Onboarding {
   isTourCompleted(tourId) {
     return Preferences.get(`browser.onboarding.tour.${tourId}.completed`, false);
   }
 
   setToursCompleted(tourIds) {
     let params = [];
     tourIds.forEach(id => {
       if (!this.isTourCompleted(id)) {
-        params.push({
-          name: `browser.onboarding.tour.${id}.completed`,
-          value: true
-        });
+        AsyncPrefs.set(`browser.onboarding.tour.${id}.completed`, true);
       }
     });
-    if (params.length > 0) {
-      this.sendMessageToChrome("set-prefs", params);
-    }
   }
 
   markTourCompletionState(tourId) {
     // We are doing lazy load so there might be no items.
     if (this._tourItems && this._tourItems.length > 0 && this.isTourCompleted(tourId)) {
       let targetItem = this._tourItems.find(item => item.id == tourId);
       targetItem.classList.add("onboarding-complete");
     }
   }
 
   _muteNotificationOnFirstSession() {
-    if (Preferences.isSet("browser.onboarding.notification.tour-ids-queue")) {
+    if (Preferences.isSet(NOTIFICATION_ID_QUEUE_PREF)) {
       // There is a queue. We had prompted before, this must not be the 1st session.
       return false;
     }
 
-    let muteDuration = Preferences.get("browser.onboarding.notification.mute-duration-on-first-session-ms");
+    let muteDuration = Preferences.get(NOTIFICATION_MUTE_DURATION_ON_FIRST_PREF);
     if (muteDuration == 0) {
       // Don't mute when this is set to 0 on purpose.
       return false;
     }
 
     // Reuse the `last-time-of-changing-tour-sec` to save the time that
     // we try to prompt on the 1st session.
-    let lastTime = 1000 * Preferences.get("browser.onboarding.notification.last-time-of-changing-tour-sec", 0);
+    let lastTime = 1000 * Preferences.get(NOTIFICATION_LAST_TIME_TOUR_CHANGE_PREF, 0);
     if (lastTime <= 0) {
-      this.sendMessageToChrome("set-prefs", [{
-        name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
-        value: Math.floor(Date.now() / 1000)
-      }]);
+      AsyncPrefs.set(NOTIFICATION_LAST_TIME_TOUR_CHANGE_PREF, Math.floor(Date.now() / 1000));
       return true;
     }
     return Date.now() - lastTime <= muteDuration;
   }
 
   _isTimeForNextTourNotification() {
-    let promptCount = Preferences.get("browser.onboarding.notification.prompt-count", 0);
-    let maxCount = Preferences.get("browser.onboarding.notification.max-prompt-count-per-tour");
+    let promptCount = Preferences.get(PROMPT_COUNT_PREF, 0);
+    let maxCount = Preferences.get(NOTIFICATION_MAX_COUNT_PER_TOUR_PREF);
     if (promptCount >= maxCount) {
       return true;
     }
 
-    let lastTime = 1000 * Preferences.get("browser.onboarding.notification.last-time-of-changing-tour-sec", 0);
-    let maxTime = Preferences.get("browser.onboarding.notification.max-life-time-per-tour-ms");
+    let lastTime = 1000 * Preferences.get(NOTIFICATION_LAST_TIME_TOUR_CHANGE_PREF, 0);
+    let maxTime = Preferences.get(NOTIFICATION_MAX_LIFETIME_PER_TOUR_PREF);
     if (lastTime && Date.now() - lastTime >= maxTime) {
       return true;
     }
 
     return false;
   }
 
   _removeTourFromNotificationQueue(tourId) {
     let params = [];
     let queue = this._getNotificationQueue();
-    params.push({
-      name: "browser.onboarding.notification.tour-ids-queue",
-      value: queue.filter(id => id != tourId).join(",")
-    });
-    params.push({
-      name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
-      value: 0
-    });
-    params.push({
-      name: "browser.onboarding.notification.prompt-count",
-      value: 0
-    });
-    this.sendMessageToChrome("set-prefs", params);
+    AsyncPrefs.set(NOTIFICATION_ID_QUEUE_PREF, queue.filter(id => id != tourId).join(","));
+    AsyncPrefs.set(NOTIFICATION_LAST_TIME_TOUR_CHANGE_PREF, 0);
+    AsyncPrefs.set(PROMPT_COUNT_PREF, 0);
   }
 
   _getNotificationQueue() {
     let queue = "";
-    if (Preferences.isSet("browser.onboarding.notification.tour-ids-queue")) {
-      queue = Preferences.get("browser.onboarding.notification.tour-ids-queue");
+    if (Preferences.isSet(NOTIFICATION_ID_QUEUE_PREF)) {
+      queue = Preferences.get(NOTIFICATION_ID_QUEUE_PREF);
     } else {
       // For each tour, it only gets 2 chances to prompt with notification
       // (each chance includes 8 impressions or 5-days max life time)
       // if user never interact with it.
       // Assume there are tour #0 ~ #5. Here would form the queue as
       // "#0,#1,#2,#3,#4,#5,#0,#1,#2,#3,#4,#5".
       // Then we would loop through this queue and remove prompted tour from the queue
       // until the queue is empty.
       let ids = this._tours.map(tour => tour.id).join(",");
       queue = `${ids},${ids}`;
-      this.sendMessageToChrome("set-prefs", [{
-        name: "browser.onboarding.notification.tour-ids-queue",
-        value: queue
-      }]);
+      AsyncPrefs.set(NOTIFICATION_ID_QUEUE_PREF, queue);
     }
     return queue ? queue.split(",") : [];
   }
 
   showNotification() {
-    if (Preferences.get("browser.onboarding.notification.finished", false)) {
+    if (Preferences.get(NOTIFICATION_FINISHED_PREF, false)) {
       return;
     }
 
     if (this._muteNotificationOnFirstSession()) {
       return;
     }
 
     let queue = this._getNotificationQueue();
@@ -658,26 +641,18 @@ class Onboarding {
       queue.shift();
     }
     // We don't want to prompt completed tour.
     while (queue.length > 0 && this.isTourCompleted(queue[0])) {
       queue.shift();
     }
 
     if (queue.length == 0) {
-      this.sendMessageToChrome("set-prefs", [
-        {
-          name: "browser.onboarding.notification.finished",
-          value: true
-        },
-        {
-          name: "browser.onboarding.notification.tour-ids-queue",
-          value: ""
-        }
-      ]);
+      AsyncPrefs.set(NOTIFICATION_FINISHED_PREF, true);
+      AsyncPrefs.set(NOTIFICATION_ID_QUEUE_PREF, "");
       return;
     }
     let targetTourId = queue[0];
     let targetTour = this._tours.find(tour => tour.id == targetTourId);
 
     // Show the target tour notification
     this._notificationBar = this._renderNotificationBar();
     this._notificationBar.addEventListener("click", this);
@@ -691,36 +666,23 @@ class Onboarding {
     tourTitle.textContent = notificationStrings.title;
     let tourMessage = this._notificationBar.querySelector("#onboarding-notification-tour-message");
     tourMessage.textContent = notificationStrings.message;
     this._notificationBar.classList.add("onboarding-opened");
 
     let params = [];
     if (startQueueLength != queue.length) {
       // We just change tour so update the time, the count and the queue
-      params.push({
-        name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
-        value: Math.floor(Date.now() / 1000)
-      });
-      params.push({
-        name: PROMPT_COUNT_PREF,
-        value: 1
-      });
-      params.push({
-        name: "browser.onboarding.notification.tour-ids-queue",
-        value: queue.join(",")
-      });
+      AsyncPrefs.set(NOTIFICATION_LAST_TIME_TOUR_CHANGE_PREF, Math.floor(Date.now() / 1000));
+      AsyncPrefs.set(PROMPT_COUNT_PREF, 1);
+      AsyncPrefs.set(NOTIFICATION_ID_QUEUE_PREF, queue.join(","));
     } else {
       let promptCount = Preferences.get(PROMPT_COUNT_PREF, 0);
-      params.push({
-        name: PROMPT_COUNT_PREF,
-        value: promptCount + 1
-      });
+      AsyncPrefs.set(PROMPT_COUNT_PREF, promptCount + 1);
     }
-    this.sendMessageToChrome("set-prefs", params);
   }
 
   hideNotification() {
     if (this._notificationBar) {
       this._notificationBar.classList.remove("onboarding-opened");
     }
   }
 
@@ -746,26 +708,19 @@ class Onboarding {
                                  "onboarding.notification-icon-tooltip-updated",
       [BRAND_SHORT_NAME], 1);
     div.querySelector("#onboarding-notification-icon").setAttribute("data-tooltip", toolTip);
     return div;
   }
 
   hide() {
     this.setToursCompleted(this._tours.map(tour => tour.id));
-    this.sendMessageToChrome("set-prefs", [
-      {
-        name: "browser.onboarding.hidden",
-        value: true
-      },
-      {
-        name: "browser.onboarding.notification.finished",
-        value: true
-      }
-    ]);
+
+    AsyncPrefs.set(HIDDEN_PREF, true);
+    AsyncPrefs.set(NOTIFICATION_FINISHED_PREF, true);
   }
 
   _renderOverlay() {
     let div = this._window.document.createElement("div");
     div.id = "onboarding-overlay";
     // We use `innerHTML` for more friendly reading.
     // The security should be fine because this is not from an external input.
     div.innerHTML = `
@@ -863,18 +818,18 @@ class Onboarding {
     let script = doc.createElement("script");
     script.type = "text/javascript";
     script.src = uri;
     doc.head.appendChild(script);
   }
 }
 
 // Load onboarding module only when we enable it.
-if (Services.prefs.getBoolPref("browser.onboarding.enabled", false) &&
-    !Services.prefs.getBoolPref("browser.onboarding.hidden", false)) {
+if (Services.prefs.getBoolPref(ENABLED_PREF, false) &&
+    !Services.prefs.getBoolPref(HIDDEN_PREF, false)) {
 
   addEventListener("load", function onLoad(evt) {
     if (!content || evt.target != content.document) {
       return;
     }
     removeEventListener("load", onLoad);
 
     let window = evt.target.defaultView;
--- a/toolkit/modules/AsyncPrefs.jsm
+++ b/toolkit/modules/AsyncPrefs.jsm
@@ -21,16 +21,31 @@ const kAllowedPrefs = new Set([
   "narrate.rate",
   "narrate.voice",
 
   "reader.font_size",
   "reader.font_type",
   "reader.color_scheme",
   "reader.content_width",
   "reader.line_height",
+
+  "browser.onboarding.tour.onboarding-tour-private-browsing.completed",
+  "browser.onboarding.tour.onboarding-tour-addons.completed",
+  "browser.onboarding.tour.onboarding-tour-tour-customize.completed",
+  "browser.onboarding.tour.onboarding-tour-search.completed",
+  "browser.onboarding.tour.onboarding-tour-default-browser.completed",
+  "browser.onboarding.tour.onboarding-tour-sync.completed",
+  "browser.onboarding.tour.onboarding-tour-library.completed",
+  "browser.onboarding.tour.onboarding-tour-singlesearch.completed",
+  "browser.onboarding.tour.onboarding-tour-performance.completed",
+  "browser.onboarding.notification.last-time-of-changing-tour-sec",
+  "browser.onboarding.notification.tour-ids-queue",
+  "browser.onboarding.notification.prompt-count",
+  "browser.onboarding.notification.finished",
+  "browser.onboarding.hidden",
 ]);
 
 const kPrefTypeMap = new Map([
   ["boolean", Services.prefs.PREF_BOOL],
   ["number", Services.prefs.PREF_INT],
   ["string", Services.prefs.PREF_STRING],
 ]);