mobile/android/modules/HomeProvider.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial