mobile/android/components/Snippets.js

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
michael@0 6
michael@0 7 Cu.import("resource://gre/modules/Accounts.jsm");
michael@0 8 Cu.import("resource://gre/modules/Services.jsm");
michael@0 9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 10
michael@0 11 XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
michael@0 12 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
michael@0 13 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
michael@0 14 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
michael@0 15
michael@0 16
michael@0 17 XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
michael@0 18 XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
michael@0 19
michael@0 20 // URL to fetch snippets, in the urlFormatter service format.
michael@0 21 const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
michael@0 22
michael@0 23 // URL to send stats data to metrics.
michael@0 24 const SNIPPETS_STATS_URL_PREF = "browser.snippets.statsUrl";
michael@0 25
michael@0 26 // URL to fetch country code, a value that's cached and refreshed once per month.
michael@0 27 const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
michael@0 28
michael@0 29 // Timestamp when we last updated the user's country code.
michael@0 30 const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
michael@0 31
michael@0 32 // Pref where we'll cache the user's country.
michael@0 33 const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
michael@0 34
michael@0 35 // Pref where we store an array IDs of snippets that should not be shown again
michael@0 36 const SNIPPETS_REMOVED_IDS_PREF = "browser.snippets.removedIds";
michael@0 37
michael@0 38 // How frequently we update the user's country code from the server (30 days).
michael@0 39 const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 86400000*30;
michael@0 40
michael@0 41 // Should be bumped up if the snippets content format changes.
michael@0 42 const SNIPPETS_VERSION = 1;
michael@0 43
michael@0 44 XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
michael@0 45 let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
michael@0 46 return Services.urlFormatter.formatURL(updateURL);
michael@0 47 });
michael@0 48
michael@0 49 // Where we cache snippets data
michael@0 50 XPCOMUtils.defineLazyGetter(this, "gSnippetsPath", function() {
michael@0 51 return OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
michael@0 52 });
michael@0 53
michael@0 54 XPCOMUtils.defineLazyGetter(this, "gStatsURL", function() {
michael@0 55 return Services.prefs.getCharPref(SNIPPETS_STATS_URL_PREF);
michael@0 56 });
michael@0 57
michael@0 58 // Where we store stats about which snippets have been shown
michael@0 59 XPCOMUtils.defineLazyGetter(this, "gStatsPath", function() {
michael@0 60 return OS.Path.join(OS.Constants.Path.profileDir, "snippets-stats.txt");
michael@0 61 });
michael@0 62
michael@0 63 XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
michael@0 64 return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
michael@0 65 });
michael@0 66
michael@0 67 XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
michael@0 68 try {
michael@0 69 return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
michael@0 70 } catch (e) {
michael@0 71 // Return an empty string if the country code pref isn't set yet.
michael@0 72 return "";
michael@0 73 }
michael@0 74 });
michael@0 75
michael@0 76 XPCOMUtils.defineLazyGetter(this, "gChromeWin", function() {
michael@0 77 return Services.wm.getMostRecentWindow("navigator:browser");
michael@0 78 });
michael@0 79
michael@0 80 /**
michael@0 81 * Updates snippet data and country code (if necessary).
michael@0 82 */
michael@0 83 function update() {
michael@0 84 // Check to see if we should update the user's country code from the geo server.
michael@0 85 let lastUpdate = 0;
michael@0 86 try {
michael@0 87 lastUpdate = parseFloat(Services.prefs.getCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF));
michael@0 88 } catch (e) {}
michael@0 89
michael@0 90 if (Date.now() - lastUpdate > SNIPPETS_GEO_UPDATE_INTERVAL_MS) {
michael@0 91 // We should update the snippets after updating the country code,
michael@0 92 // so that we can filter snippets to add to the banner.
michael@0 93 updateCountryCode(updateSnippets);
michael@0 94 } else {
michael@0 95 updateSnippets();
michael@0 96 }
michael@0 97 }
michael@0 98
michael@0 99 /**
michael@0 100 * Fetches the user's country code from the geo server and stores the value in a pref.
michael@0 101 *
michael@0 102 * @param callback function called once country code is updated
michael@0 103 */
michael@0 104 function updateCountryCode(callback) {
michael@0 105 _httpGetRequest(gGeoURL, function(responseText) {
michael@0 106 // Store the country code in a pref.
michael@0 107 let data = JSON.parse(responseText);
michael@0 108 Services.prefs.setCharPref(SNIPPETS_COUNTRY_CODE_PREF, data.country_code);
michael@0 109
michael@0 110 // Set last update time.
michael@0 111 Services.prefs.setCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF, Date.now());
michael@0 112
michael@0 113 callback();
michael@0 114 });
michael@0 115 }
michael@0 116
michael@0 117 /**
michael@0 118 * Loads snippets from snippets server, caches the response, and
michael@0 119 * updates the home banner with the new set of snippets.
michael@0 120 */
michael@0 121 function updateSnippets() {
michael@0 122 _httpGetRequest(gSnippetsURL, function(responseText) {
michael@0 123 try {
michael@0 124 let messages = JSON.parse(responseText);
michael@0 125 updateBanner(messages);
michael@0 126
michael@0 127 // Only cache the response if it is valid JSON.
michael@0 128 cacheSnippets(responseText);
michael@0 129 } catch (e) {
michael@0 130 Cu.reportError("Error parsing snippets responseText: " + e);
michael@0 131 }
michael@0 132 });
michael@0 133 }
michael@0 134
michael@0 135 /**
michael@0 136 * Caches snippets server response text to `snippets.json` in profile directory.
michael@0 137 *
michael@0 138 * @param response responseText returned from snippets server
michael@0 139 */
michael@0 140 function cacheSnippets(response) {
michael@0 141 let data = gEncoder.encode(response);
michael@0 142 let promise = OS.File.writeAtomic(gSnippetsPath, data, { tmpPath: gSnippetsPath + ".tmp" });
michael@0 143 promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
michael@0 144 }
michael@0 145
michael@0 146 /**
michael@0 147 * Loads snippets from cached `snippets.json`.
michael@0 148 */
michael@0 149 function loadSnippetsFromCache() {
michael@0 150 let promise = OS.File.read(gSnippetsPath);
michael@0 151 promise.then(array => {
michael@0 152 let messages = JSON.parse(gDecoder.decode(array));
michael@0 153 updateBanner(messages);
michael@0 154 }, e => {
michael@0 155 if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
michael@0 156 Services.console.logStringMessage("Couldn't show snippets because cache does not exist yet.");
michael@0 157 } else {
michael@0 158 Cu.reportError("Error loading snippets from cache: " + e);
michael@0 159 }
michael@0 160 });
michael@0 161 }
michael@0 162
michael@0 163 // Array of the message ids added to the home banner, used to remove
michael@0 164 // older set of snippets when new ones are available.
michael@0 165 var gMessageIds = [];
michael@0 166
michael@0 167 /**
michael@0 168 * Updates set of snippets in the home banner message rotation.
michael@0 169 *
michael@0 170 * @param messages JSON array of message data JSON objects.
michael@0 171 * Each message object should have the following properties:
michael@0 172 * - id (?): Unique identifier for this snippets message
michael@0 173 * - text (string): Text to show as banner message
michael@0 174 * - url (string): URL to open when banner is clicked
michael@0 175 * - icon (data URI): Icon to appear in banner
michael@0 176 * - target_geo (string): Country code for where this message should be shown (e.g. "US")
michael@0 177 */
michael@0 178 function updateBanner(messages) {
michael@0 179 // Remove the current messages, if there are any.
michael@0 180 gMessageIds.forEach(function(id) {
michael@0 181 Home.banner.remove(id);
michael@0 182 })
michael@0 183 gMessageIds = [];
michael@0 184
michael@0 185 try {
michael@0 186 let removedSnippetIds = JSON.parse(Services.prefs.getCharPref(SNIPPETS_REMOVED_IDS_PREF));
michael@0 187 messages = messages.filter(function(message) {
michael@0 188 // Only include the snippet if it has not been previously removed.
michael@0 189 return removedSnippetIds.indexOf(message.id) === -1;
michael@0 190 });
michael@0 191 } catch (e) {
michael@0 192 // If the pref doesn't exist, there aren't any snippets to filter out.
michael@0 193 }
michael@0 194
michael@0 195 messages.forEach(function(message) {
michael@0 196 // Don't add this message to the banner if it's not supposed to be shown in this country.
michael@0 197 if ("target_geo" in message && message.target_geo != gCountryCode) {
michael@0 198 return;
michael@0 199 }
michael@0 200 let id = Home.banner.add({
michael@0 201 text: message.text,
michael@0 202 icon: message.icon,
michael@0 203 onclick: function() {
michael@0 204 let parentId = gChromeWin.BrowserApp.selectedTab.id;
michael@0 205 gChromeWin.BrowserApp.addTab(message.url, { parentId: parentId });
michael@0 206 UITelemetry.addEvent("action.1", "banner", null, message.id);
michael@0 207 },
michael@0 208 ondismiss: function() {
michael@0 209 // Remove this snippet from the banner, and store its id so we'll never show it again.
michael@0 210 Home.banner.remove(id);
michael@0 211 removeSnippet(message.id);
michael@0 212 UITelemetry.addEvent("cancel.1", "banner", null, message.id);
michael@0 213 },
michael@0 214 onshown: function() {
michael@0 215 // 10% of the time, record the snippet id and a timestamp
michael@0 216 if (Math.random() < .1) {
michael@0 217 writeStat(message.id, new Date().toISOString());
michael@0 218 }
michael@0 219 }
michael@0 220 });
michael@0 221 // Keep track of the message we added so that we can remove it later.
michael@0 222 gMessageIds.push(id);
michael@0 223 });
michael@0 224 }
michael@0 225
michael@0 226 /**
michael@0 227 * Appends snippet id to the end of `snippets-removed.txt`
michael@0 228 *
michael@0 229 * @param snippetId unique id for snippet, sent from snippets server
michael@0 230 */
michael@0 231 function removeSnippet(snippetId) {
michael@0 232 let removedSnippetIds;
michael@0 233 try {
michael@0 234 removedSnippetIds = JSON.parse(Services.prefs.getCharPref(SNIPPETS_REMOVED_IDS_PREF));
michael@0 235 } catch (e) {
michael@0 236 removedSnippetIds = [];
michael@0 237 }
michael@0 238
michael@0 239 removedSnippetIds.push(snippetId);
michael@0 240 Services.prefs.setCharPref(SNIPPETS_REMOVED_IDS_PREF, JSON.stringify(removedSnippetIds));
michael@0 241 }
michael@0 242
michael@0 243 /**
michael@0 244 * Appends snippet id and timestamp to the end of `snippets-stats.txt`.
michael@0 245 *
michael@0 246 * @param snippetId unique id for snippet, sent from snippets server
michael@0 247 * @param timestamp in ISO8601
michael@0 248 */
michael@0 249 function writeStat(snippetId, timestamp) {
michael@0 250 let data = gEncoder.encode(snippetId + "," + timestamp + ";");
michael@0 251
michael@0 252 Task.spawn(function() {
michael@0 253 try {
michael@0 254 let file = yield OS.File.open(gStatsPath, { append: true, write: true });
michael@0 255 try {
michael@0 256 yield file.write(data);
michael@0 257 } finally {
michael@0 258 yield file.close();
michael@0 259 }
michael@0 260 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
michael@0 261 // If the file doesn't exist yet, create it.
michael@0 262 yield OS.File.writeAtomic(gStatsPath, data, { tmpPath: gStatsPath + ".tmp" });
michael@0 263 }
michael@0 264 }).then(null, e => Cu.reportError("Error writing snippets stats: " + e));
michael@0 265 }
michael@0 266
michael@0 267 /**
michael@0 268 * Reads snippets stats data from `snippets-stats.txt` and sends the data to metrics.
michael@0 269 */
michael@0 270 function sendStats() {
michael@0 271 let promise = OS.File.read(gStatsPath);
michael@0 272 promise.then(array => sendStatsRequest(gDecoder.decode(array)), e => {
michael@0 273 if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
michael@0 274 // If the file doesn't exist, there aren't any stats to send.
michael@0 275 } else {
michael@0 276 Cu.reportError("Error eading snippets stats: " + e);
michael@0 277 }
michael@0 278 });
michael@0 279 }
michael@0 280
michael@0 281 /**
michael@0 282 * Sends stats to metrics about which snippets have been shown.
michael@0 283 * Appends snippet ids and timestamps as parameters to a GET request.
michael@0 284 * e.g. https://snippets-stats.mozilla.org/mobile?s1=3825&t1=2013-11-17T18:27Z&s2=6326&t2=2013-11-18T18:27Z
michael@0 285 *
michael@0 286 * @param data contents of stats data file
michael@0 287 */
michael@0 288 function sendStatsRequest(data) {
michael@0 289 let params = [];
michael@0 290 let stats = data.split(";");
michael@0 291
michael@0 292 // The last item in the array will be an empty string, so stop before then.
michael@0 293 for (let i = 0; i < stats.length - 1; i++) {
michael@0 294 let stat = stats[i].split(",");
michael@0 295 params.push("s" + i + "=" + encodeURIComponent(stat[0]));
michael@0 296 params.push("t" + i + "=" + encodeURIComponent(stat[1]));
michael@0 297 }
michael@0 298
michael@0 299 let url = gStatsURL + "?" + params.join("&");
michael@0 300
michael@0 301 // Remove the file after succesfully sending the data.
michael@0 302 _httpGetRequest(url, removeStats);
michael@0 303 }
michael@0 304
michael@0 305 /**
michael@0 306 * Removes text file where we store snippets stats.
michael@0 307 */
michael@0 308 function removeStats() {
michael@0 309 let promise = OS.File.remove(gStatsPath);
michael@0 310 promise.then(null, e => Cu.reportError("Error removing snippets stats: " + e));
michael@0 311 }
michael@0 312
michael@0 313 /**
michael@0 314 * Helper function to make HTTP GET requests.
michael@0 315 *
michael@0 316 * @param url where we send the request
michael@0 317 * @param callback function that is called with the xhr responseText
michael@0 318 */
michael@0 319 function _httpGetRequest(url, callback) {
michael@0 320 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
michael@0 321 try {
michael@0 322 xhr.open("GET", url, true);
michael@0 323 } catch (e) {
michael@0 324 Cu.reportError("Error opening request to " + url + ": " + e);
michael@0 325 return;
michael@0 326 }
michael@0 327 xhr.onerror = function onerror(e) {
michael@0 328 Cu.reportError("Error making request to " + url + ": " + e.error);
michael@0 329 }
michael@0 330 xhr.onload = function onload(event) {
michael@0 331 if (xhr.status !== 200) {
michael@0 332 Cu.reportError("Request to " + url + " returned status " + xhr.status);
michael@0 333 return;
michael@0 334 }
michael@0 335 if (callback) {
michael@0 336 callback(xhr.responseText);
michael@0 337 }
michael@0 338 }
michael@0 339 xhr.send(null);
michael@0 340 }
michael@0 341
michael@0 342 function loadSyncPromoBanner() {
michael@0 343 Accounts.anySyncAccountsExist().then(
michael@0 344 (exist) => {
michael@0 345 // Don't show the banner if sync accounts exist.
michael@0 346 if (exist) {
michael@0 347 return;
michael@0 348 }
michael@0 349
michael@0 350 let stringBundle = Services.strings.createBundle("chrome://browser/locale/sync.properties");
michael@0 351 let text = stringBundle.GetStringFromName("promoBanner.message.text");
michael@0 352 let link = stringBundle.GetStringFromName("promoBanner.message.link");
michael@0 353
michael@0 354 let id = Home.banner.add({
michael@0 355 text: text + "<a href=\"#\">" + link + "</a>",
michael@0 356 icon: "drawable://sync_promo",
michael@0 357 onclick: function() {
michael@0 358 // Remove the message, so that it won't show again for the rest of the app lifetime.
michael@0 359 Home.banner.remove(id);
michael@0 360 Accounts.launchSetup();
michael@0 361
michael@0 362 UITelemetry.addEvent("action.1", "banner", null, "syncpromo");
michael@0 363 },
michael@0 364 ondismiss: function() {
michael@0 365 // Remove the sync promo message from the banner and never try to show it again.
michael@0 366 Home.banner.remove(id);
michael@0 367 Services.prefs.setBoolPref("browser.snippets.syncPromo.enabled", false);
michael@0 368
michael@0 369 UITelemetry.addEvent("cancel.1", "banner", null, "syncpromo");
michael@0 370 }
michael@0 371 });
michael@0 372 },
michael@0 373 (err) => {
michael@0 374 Cu.reportError("Error checking whether sync account exists: " + err);
michael@0 375 }
michael@0 376 );
michael@0 377 }
michael@0 378
michael@0 379 function Snippets() {}
michael@0 380
michael@0 381 Snippets.prototype = {
michael@0 382 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]),
michael@0 383 classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"),
michael@0 384
michael@0 385 observe: function(subject, topic, data) {
michael@0 386 switch(topic) {
michael@0 387 case "profile-after-change":
michael@0 388 Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
michael@0 389 break;
michael@0 390 case "browser-delayed-startup-finished":
michael@0 391 Services.obs.removeObserver(this, "browser-delayed-startup-finished", false);
michael@0 392 if (Services.prefs.getBoolPref("browser.snippets.syncPromo.enabled")) {
michael@0 393 loadSyncPromoBanner();
michael@0 394 }
michael@0 395
michael@0 396 if (Services.prefs.getBoolPref("browser.snippets.enabled")) {
michael@0 397 loadSnippetsFromCache();
michael@0 398 }
michael@0 399 break;
michael@0 400 }
michael@0 401 },
michael@0 402
michael@0 403 // By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
michael@0 404 notify: function(timer) {
michael@0 405 if (!Services.prefs.getBoolPref("browser.snippets.enabled")) {
michael@0 406 return;
michael@0 407 }
michael@0 408 update();
michael@0 409 sendStats();
michael@0 410 }
michael@0 411 };
michael@0 412
michael@0 413 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);

mercurial