mobile/android/modules/HomeProvider.jsm

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 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 "use strict";
michael@0 7
michael@0 8 this.EXPORTED_SYMBOLS = [ "HomeProvider" ];
michael@0 9
michael@0 10 const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
michael@0 11
michael@0 12 Cu.import("resource://gre/modules/Messaging.jsm");
michael@0 13 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 14 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 15 Cu.import("resource://gre/modules/Services.jsm");
michael@0 16 Cu.import("resource://gre/modules/Sqlite.jsm");
michael@0 17 Cu.import("resource://gre/modules/Task.jsm");
michael@0 18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 19
michael@0 20 /*
michael@0 21 * SCHEMA_VERSION history:
michael@0 22 * 1: Create HomeProvider (bug 942288)
michael@0 23 * 2: Add filter column to items table (bug 942295/975841)
michael@0 24 */
michael@0 25 const SCHEMA_VERSION = 2;
michael@0 26
michael@0 27 XPCOMUtils.defineLazyGetter(this, "DB_PATH", function() {
michael@0 28 return OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
michael@0 29 });
michael@0 30
michael@0 31 const PREF_STORAGE_LAST_SYNC_TIME_PREFIX = "home.storage.lastSyncTime.";
michael@0 32 const PREF_SYNC_UPDATE_MODE = "home.sync.updateMode";
michael@0 33 const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
michael@0 34
michael@0 35 XPCOMUtils.defineLazyGetter(this, "gSyncCheckIntervalSecs", function() {
michael@0 36 return Services.prefs.getIntPref(PREF_SYNC_CHECK_INTERVAL_SECS);
michael@0 37 });
michael@0 38
michael@0 39 XPCOMUtils.defineLazyServiceGetter(this,
michael@0 40 "gUpdateTimerManager", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
michael@0 41
michael@0 42 /**
michael@0 43 * All SQL statements should be defined here.
michael@0 44 */
michael@0 45 const SQL = {
michael@0 46 createItemsTable:
michael@0 47 "CREATE TABLE items (" +
michael@0 48 "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
michael@0 49 "dataset_id TEXT NOT NULL, " +
michael@0 50 "url TEXT," +
michael@0 51 "title TEXT," +
michael@0 52 "description TEXT," +
michael@0 53 "image_url TEXT," +
michael@0 54 "filter TEXT," +
michael@0 55 "created INTEGER" +
michael@0 56 ")",
michael@0 57
michael@0 58 dropItemsTable:
michael@0 59 "DROP TABLE items",
michael@0 60
michael@0 61 insertItem:
michael@0 62 "INSERT INTO items (dataset_id, url, title, description, image_url, filter, created) " +
michael@0 63 "VALUES (:dataset_id, :url, :title, :description, :image_url, :filter, :created)",
michael@0 64
michael@0 65 deleteFromDataset:
michael@0 66 "DELETE FROM items WHERE dataset_id = :dataset_id"
michael@0 67 }
michael@0 68
michael@0 69 /**
michael@0 70 * Technically this function checks to see if the user is on a local network,
michael@0 71 * but we express this as "wifi" to the user.
michael@0 72 */
michael@0 73 function isUsingWifi() {
michael@0 74 let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
michael@0 75 return (network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET);
michael@0 76 }
michael@0 77
michael@0 78 function getNowInSeconds() {
michael@0 79 return Math.round(Date.now() / 1000);
michael@0 80 }
michael@0 81
michael@0 82 function getLastSyncPrefName(datasetId) {
michael@0 83 return PREF_STORAGE_LAST_SYNC_TIME_PREFIX + datasetId;
michael@0 84 }
michael@0 85
michael@0 86 // Whether or not we've registered an update timer.
michael@0 87 var gTimerRegistered = false;
michael@0 88
michael@0 89 // Map of datasetId -> { interval: <integer>, callback: <function> }
michael@0 90 var gSyncCallbacks = {};
michael@0 91
michael@0 92 /**
michael@0 93 * nsITimerCallback implementation. Checks to see if it's time to sync any registered datasets.
michael@0 94 *
michael@0 95 * @param timer The timer which has expired.
michael@0 96 */
michael@0 97 function syncTimerCallback(timer) {
michael@0 98 for (let datasetId in gSyncCallbacks) {
michael@0 99 let lastSyncTime = 0;
michael@0 100 try {
michael@0 101 lastSyncTime = Services.prefs.getIntPref(getLastSyncPrefName(datasetId));
michael@0 102 } catch(e) { }
michael@0 103
michael@0 104 let now = getNowInSeconds();
michael@0 105 let { interval: interval, callback: callback } = gSyncCallbacks[datasetId];
michael@0 106
michael@0 107 if (lastSyncTime < now - interval) {
michael@0 108 let success = HomeProvider.requestSync(datasetId, callback);
michael@0 109 if (success) {
michael@0 110 Services.prefs.setIntPref(getLastSyncPrefName(datasetId), now);
michael@0 111 }
michael@0 112 }
michael@0 113 }
michael@0 114 }
michael@0 115
michael@0 116 this.HomeStorage = function(datasetId) {
michael@0 117 this.datasetId = datasetId;
michael@0 118 };
michael@0 119
michael@0 120 this.ValidationError = function(message) {
michael@0 121 this.name = "ValidationError";
michael@0 122 this.message = message;
michael@0 123 };
michael@0 124 ValidationError.prototype = new Error();
michael@0 125 ValidationError.prototype.constructor = ValidationError;
michael@0 126
michael@0 127 this.HomeProvider = Object.freeze({
michael@0 128 ValidationError: ValidationError,
michael@0 129
michael@0 130 /**
michael@0 131 * Returns a storage associated with a given dataset identifer.
michael@0 132 *
michael@0 133 * @param datasetId
michael@0 134 * (string) Unique identifier for the dataset.
michael@0 135 *
michael@0 136 * @return HomeStorage
michael@0 137 */
michael@0 138 getStorage: function(datasetId) {
michael@0 139 return new HomeStorage(datasetId);
michael@0 140 },
michael@0 141
michael@0 142 /**
michael@0 143 * Checks to see if it's an appropriate time to sync.
michael@0 144 *
michael@0 145 * @param datasetId Unique identifier for the dataset to sync.
michael@0 146 * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
michael@0 147 *
michael@0 148 * @return boolean Whether or not we were able to sync.
michael@0 149 */
michael@0 150 requestSync: function(datasetId, callback) {
michael@0 151 // Make sure it's a good time to sync.
michael@0 152 if ((Services.prefs.getIntPref(PREF_SYNC_UPDATE_MODE) === 1) && !isUsingWifi()) {
michael@0 153 Cu.reportError("HomeProvider: Failed to sync because device is not on a local network");
michael@0 154 return false;
michael@0 155 }
michael@0 156
michael@0 157 callback(datasetId);
michael@0 158 return true;
michael@0 159 },
michael@0 160
michael@0 161 /**
michael@0 162 * Specifies that a sync should be requested for the given dataset and update interval.
michael@0 163 *
michael@0 164 * @param datasetId Unique identifier for the dataset to sync.
michael@0 165 * @param interval Update interval in seconds. By default, this is throttled to 3600 seconds (1 hour).
michael@0 166 * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
michael@0 167 */
michael@0 168 addPeriodicSync: function(datasetId, interval, callback) {
michael@0 169 // Warn developers if they're expecting more frequent notifications that we allow.
michael@0 170 if (interval < gSyncCheckIntervalSecs) {
michael@0 171 Cu.reportError("HomeProvider: Warning for dataset " + datasetId +
michael@0 172 " : Sync notifications are throttled to " + gSyncCheckIntervalSecs + " seconds");
michael@0 173 }
michael@0 174
michael@0 175 gSyncCallbacks[datasetId] = {
michael@0 176 interval: interval,
michael@0 177 callback: callback
michael@0 178 };
michael@0 179
michael@0 180 if (!gTimerRegistered) {
michael@0 181 gUpdateTimerManager.registerTimer("home-provider-sync-timer", syncTimerCallback, gSyncCheckIntervalSecs);
michael@0 182 gTimerRegistered = true;
michael@0 183 }
michael@0 184 },
michael@0 185
michael@0 186 /**
michael@0 187 * Removes a periodic sync timer.
michael@0 188 *
michael@0 189 * @param datasetId Dataset to sync.
michael@0 190 */
michael@0 191 removePeriodicSync: function(datasetId) {
michael@0 192 delete gSyncCallbacks[datasetId];
michael@0 193 Services.prefs.clearUserPref(getLastSyncPrefName(datasetId));
michael@0 194 // You can't unregister a update timer, so we don't try to do that.
michael@0 195 }
michael@0 196 });
michael@0 197
michael@0 198 var gDatabaseEnsured = false;
michael@0 199
michael@0 200 /**
michael@0 201 * Creates the database schema.
michael@0 202 */
michael@0 203 function createDatabase(db) {
michael@0 204 return Task.spawn(function create_database_task() {
michael@0 205 yield db.execute(SQL.createItemsTable);
michael@0 206 });
michael@0 207 }
michael@0 208
michael@0 209 /**
michael@0 210 * Migrates the database schema to a new version.
michael@0 211 */
michael@0 212 function upgradeDatabase(db, oldVersion, newVersion) {
michael@0 213 return Task.spawn(function upgrade_database_task() {
michael@0 214 for (let v = oldVersion + 1; v <= newVersion; v++) {
michael@0 215 switch(v) {
michael@0 216 case 2:
michael@0 217 // Recreate the items table discarding any
michael@0 218 // existing data.
michael@0 219 yield db.execute(SQL.dropItemsTable);
michael@0 220 yield db.execute(SQL.createItemsTable);
michael@0 221 break;
michael@0 222 }
michael@0 223 }
michael@0 224 });
michael@0 225 }
michael@0 226
michael@0 227 /**
michael@0 228 * Opens a database connection and makes sure that the database schema version
michael@0 229 * is correct, performing migrations if necessary. Consumers should be sure
michael@0 230 * to close any database connections they open.
michael@0 231 *
michael@0 232 * @return Promise
michael@0 233 * @resolves Handle on an opened SQLite database.
michael@0 234 */
michael@0 235 function getDatabaseConnection() {
michael@0 236 return Task.spawn(function get_database_connection_task() {
michael@0 237 let db = yield Sqlite.openConnection({ path: DB_PATH });
michael@0 238 if (gDatabaseEnsured) {
michael@0 239 throw new Task.Result(db);
michael@0 240 }
michael@0 241
michael@0 242 try {
michael@0 243 // Check to see if we need to perform any migrations.
michael@0 244 let dbVersion = parseInt(yield db.getSchemaVersion());
michael@0 245
michael@0 246 // getSchemaVersion() returns a 0 int if the schema
michael@0 247 // version is undefined.
michael@0 248 if (dbVersion === 0) {
michael@0 249 yield createDatabase(db);
michael@0 250 } else if (dbVersion < SCHEMA_VERSION) {
michael@0 251 yield upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
michael@0 252 }
michael@0 253
michael@0 254 yield db.setSchemaVersion(SCHEMA_VERSION);
michael@0 255 } catch(e) {
michael@0 256 // Close the DB connection before passing the exception to the consumer.
michael@0 257 yield db.close();
michael@0 258 throw e;
michael@0 259 }
michael@0 260
michael@0 261 gDatabaseEnsured = true;
michael@0 262 throw new Task.Result(db);
michael@0 263 });
michael@0 264 }
michael@0 265
michael@0 266 /**
michael@0 267 * Validates an item to be saved to the DB.
michael@0 268 *
michael@0 269 * @param item
michael@0 270 * (object) item object to be validated.
michael@0 271 */
michael@0 272 function validateItem(datasetId, item) {
michael@0 273 if (!item.url) {
michael@0 274 throw new ValidationError('HomeStorage: All rows must have an URL: datasetId = ' +
michael@0 275 datasetId);
michael@0 276 }
michael@0 277
michael@0 278 if (!item.image_url && !item.title && !item.description) {
michael@0 279 throw new ValidationError('HomeStorage: All rows must have at least an image URL, ' +
michael@0 280 'or a title or a description: datasetId = ' + datasetId);
michael@0 281 }
michael@0 282 }
michael@0 283
michael@0 284 var gRefreshTimers = {};
michael@0 285
michael@0 286 /**
michael@0 287 * Sends a message to Java to refresh the given dataset. Delays sending
michael@0 288 * messages to avoid successive refreshes, which can result in flashing views.
michael@0 289 */
michael@0 290 function refreshDataset(datasetId) {
michael@0 291 // Bail if there's already a refresh timer waiting to fire
michael@0 292 if (gRefreshTimers[datasetId]) {
michael@0 293 return;
michael@0 294 }
michael@0 295
michael@0 296 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 297 timer.initWithCallback(function(timer) {
michael@0 298 delete gRefreshTimers[datasetId];
michael@0 299
michael@0 300 sendMessageToJava({
michael@0 301 type: "HomePanels:RefreshDataset",
michael@0 302 datasetId: datasetId
michael@0 303 });
michael@0 304 }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
michael@0 305
michael@0 306 gRefreshTimers[datasetId] = timer;
michael@0 307 }
michael@0 308
michael@0 309 HomeStorage.prototype = {
michael@0 310 /**
michael@0 311 * Saves data rows to the DB.
michael@0 312 *
michael@0 313 * @param data
michael@0 314 * An array of JS objects represnting row items to save.
michael@0 315 * Each object may have the following properties:
michael@0 316 * - url (string)
michael@0 317 * - title (string)
michael@0 318 * - description (string)
michael@0 319 * - image_url (string)
michael@0 320 * - filter (string)
michael@0 321 * @param options
michael@0 322 * A JS object holding additional cofiguration properties.
michael@0 323 * The following properties are currently supported:
michael@0 324 * - replace (boolean): Whether or not to replace existing items.
michael@0 325 *
michael@0 326 * @return Promise
michael@0 327 * @resolves When the operation has completed.
michael@0 328 */
michael@0 329 save: function(data, options) {
michael@0 330 return Task.spawn(function save_task() {
michael@0 331 let db = yield getDatabaseConnection();
michael@0 332 try {
michael@0 333 yield db.executeTransaction(function save_transaction() {
michael@0 334 if (options && options.replace) {
michael@0 335 yield db.executeCached(SQL.deleteFromDataset, { dataset_id: this.datasetId });
michael@0 336 }
michael@0 337
michael@0 338 // Insert data into DB.
michael@0 339 for (let item of data) {
michael@0 340 validateItem(this.datasetId, item);
michael@0 341
michael@0 342 // XXX: Directly pass item as params? More validation for item?
michael@0 343 let params = {
michael@0 344 dataset_id: this.datasetId,
michael@0 345 url: item.url,
michael@0 346 title: item.title,
michael@0 347 description: item.description,
michael@0 348 image_url: item.image_url,
michael@0 349 filter: item.filter,
michael@0 350 created: Date.now()
michael@0 351 };
michael@0 352 yield db.executeCached(SQL.insertItem, params);
michael@0 353 }
michael@0 354 }.bind(this));
michael@0 355 } finally {
michael@0 356 yield db.close();
michael@0 357 }
michael@0 358
michael@0 359 refreshDataset(this.datasetId);
michael@0 360 }.bind(this));
michael@0 361 },
michael@0 362
michael@0 363 /**
michael@0 364 * Deletes all rows associated with this storage.
michael@0 365 *
michael@0 366 * @return Promise
michael@0 367 * @resolves When the operation has completed.
michael@0 368 */
michael@0 369 deleteAll: function() {
michael@0 370 return Task.spawn(function delete_all_task() {
michael@0 371 let db = yield getDatabaseConnection();
michael@0 372 try {
michael@0 373 let params = { dataset_id: this.datasetId };
michael@0 374 yield db.executeCached(SQL.deleteFromDataset, params);
michael@0 375 } finally {
michael@0 376 yield db.close();
michael@0 377 }
michael@0 378
michael@0 379 refreshDataset(this.datasetId);
michael@0 380 }.bind(this));
michael@0 381 }
michael@0 382 };

mercurial