summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/HomeProvider.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/HomeProvider.jsm')
-rw-r--r--mobile/android/modules/HomeProvider.jsm407
1 files changed, 407 insertions, 0 deletions
diff --git a/mobile/android/modules/HomeProvider.jsm b/mobile/android/modules/HomeProvider.jsm
new file mode 100644
index 000000000..bca8fa526
--- /dev/null
+++ b/mobile/android/modules/HomeProvider.jsm
@@ -0,0 +1,407 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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";
+
+this.EXPORTED_SYMBOLS = [ "HomeProvider" ];
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/*
+ * SCHEMA_VERSION history:
+ * 1: Create HomeProvider (bug 942288)
+ * 2: Add filter column to items table (bug 942295/975841)
+ * 3: Add background_color and background_url columns (bug 1157539)
+ */
+const SCHEMA_VERSION = 3;
+
+// The maximum number of items you can attempt to save at once.
+const MAX_SAVE_COUNT = 100;
+
+XPCOMUtils.defineLazyGetter(this, "DB_PATH", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
+});
+
+const PREF_STORAGE_LAST_SYNC_TIME_PREFIX = "home.storage.lastSyncTime.";
+const PREF_SYNC_UPDATE_MODE = "home.sync.updateMode";
+const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
+
+XPCOMUtils.defineLazyGetter(this, "gSyncCheckIntervalSecs", function() {
+ return Services.prefs.getIntPref(PREF_SYNC_CHECK_INTERVAL_SECS);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "gUpdateTimerManager", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
+
+/**
+ * All SQL statements should be defined here.
+ */
+const SQL = {
+ createItemsTable:
+ "CREATE TABLE items (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "dataset_id TEXT NOT NULL, " +
+ "url TEXT," +
+ "title TEXT," +
+ "description TEXT," +
+ "image_url TEXT," +
+ "background_color TEXT," +
+ "background_url TEXT," +
+ "filter TEXT," +
+ "created INTEGER" +
+ ")",
+
+ dropItemsTable:
+ "DROP TABLE items",
+
+ insertItem:
+ "INSERT INTO items (dataset_id, url, title, description, image_url, background_color, background_url, filter, created) " +
+ "VALUES (:dataset_id, :url, :title, :description, :image_url, :background_color, :background_url, :filter, :created)",
+
+ deleteFromDataset:
+ "DELETE FROM items WHERE dataset_id = :dataset_id",
+
+ addColumnBackgroundColor:
+ "ALTER TABLE items ADD COLUMN background_color TEXT",
+
+ addColumnBackgroundUrl:
+ "ALTER TABLE items ADD COLUMN background_url TEXT",
+}
+
+/**
+ * Technically this function checks to see if the user is on a local network,
+ * but we express this as "wifi" to the user.
+ */
+function isUsingWifi() {
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ return (network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET);
+}
+
+function getNowInSeconds() {
+ return Math.round(Date.now() / 1000);
+}
+
+function getLastSyncPrefName(datasetId) {
+ return PREF_STORAGE_LAST_SYNC_TIME_PREFIX + datasetId;
+}
+
+// Whether or not we've registered an update timer.
+var gTimerRegistered = false;
+
+// Map of datasetId -> { interval: <integer>, callback: <function> }
+var gSyncCallbacks = {};
+
+/**
+ * nsITimerCallback implementation. Checks to see if it's time to sync any registered datasets.
+ *
+ * @param timer The timer which has expired.
+ */
+function syncTimerCallback(timer) {
+ for (let datasetId in gSyncCallbacks) {
+ let lastSyncTime = 0;
+ try {
+ lastSyncTime = Services.prefs.getIntPref(getLastSyncPrefName(datasetId));
+ } catch(e) { }
+
+ let now = getNowInSeconds();
+ let { interval: interval, callback: callback } = gSyncCallbacks[datasetId];
+
+ if (lastSyncTime < now - interval) {
+ let success = HomeProvider.requestSync(datasetId, callback);
+ if (success) {
+ Services.prefs.setIntPref(getLastSyncPrefName(datasetId), now);
+ }
+ }
+ }
+}
+
+this.HomeStorage = function(datasetId) {
+ this.datasetId = datasetId;
+};
+
+this.ValidationError = function(message) {
+ this.name = "ValidationError";
+ this.message = message;
+};
+ValidationError.prototype = new Error();
+ValidationError.prototype.constructor = ValidationError;
+
+this.HomeProvider = Object.freeze({
+ ValidationError: ValidationError,
+
+ /**
+ * Returns a storage associated with a given dataset identifer.
+ *
+ * @param datasetId
+ * (string) Unique identifier for the dataset.
+ *
+ * @return HomeStorage
+ */
+ getStorage: function(datasetId) {
+ return new HomeStorage(datasetId);
+ },
+
+ /**
+ * Checks to see if it's an appropriate time to sync.
+ *
+ * @param datasetId Unique identifier for the dataset to sync.
+ * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
+ *
+ * @return boolean Whether or not we were able to sync.
+ */
+ requestSync: function(datasetId, callback) {
+ // Make sure it's a good time to sync.
+ if ((Services.prefs.getIntPref(PREF_SYNC_UPDATE_MODE) === 1) && !isUsingWifi()) {
+ Cu.reportError("HomeProvider: Failed to sync because device is not on a local network");
+ return false;
+ }
+
+ callback(datasetId);
+ return true;
+ },
+
+ /**
+ * Specifies that a sync should be requested for the given dataset and update interval.
+ *
+ * @param datasetId Unique identifier for the dataset to sync.
+ * @param interval Update interval in seconds. By default, this is throttled to 3600 seconds (1 hour).
+ * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
+ */
+ addPeriodicSync: function(datasetId, interval, callback) {
+ // Warn developers if they're expecting more frequent notifications that we allow.
+ if (interval < gSyncCheckIntervalSecs) {
+ Cu.reportError("HomeProvider: Warning for dataset " + datasetId +
+ " : Sync notifications are throttled to " + gSyncCheckIntervalSecs + " seconds");
+ }
+
+ gSyncCallbacks[datasetId] = {
+ interval: interval,
+ callback: callback
+ };
+
+ if (!gTimerRegistered) {
+ gUpdateTimerManager.registerTimer("home-provider-sync-timer", syncTimerCallback, gSyncCheckIntervalSecs);
+ gTimerRegistered = true;
+ }
+ },
+
+ /**
+ * Removes a periodic sync timer.
+ *
+ * @param datasetId Dataset to sync.
+ */
+ removePeriodicSync: function(datasetId) {
+ delete gSyncCallbacks[datasetId];
+ Services.prefs.clearUserPref(getLastSyncPrefName(datasetId));
+ // You can't unregister a update timer, so we don't try to do that.
+ }
+});
+
+var gDatabaseEnsured = false;
+
+/**
+ * Creates the database schema.
+ */
+function createDatabase(db) {
+ return Task.spawn(function create_database_task() {
+ yield db.execute(SQL.createItemsTable);
+ });
+}
+
+/**
+ * Migrates the database schema to a new version.
+ */
+function upgradeDatabase(db, oldVersion, newVersion) {
+ return Task.spawn(function upgrade_database_task() {
+ switch (oldVersion) {
+ case 1:
+ // Migration from v1 to latest:
+ // Recreate the items table discarding any
+ // existing data.
+ yield db.execute(SQL.dropItemsTable);
+ yield db.execute(SQL.createItemsTable);
+ break;
+
+ case 2:
+ // Migration from v2 to latest:
+ // Add new columns: background_color, background_url
+ yield db.execute(SQL.addColumnBackgroundColor);
+ yield db.execute(SQL.addColumnBackgroundUrl);
+ break;
+ }
+ });
+}
+
+/**
+ * Opens a database connection and makes sure that the database schema version
+ * is correct, performing migrations if necessary. Consumers should be sure
+ * to close any database connections they open.
+ *
+ * @return Promise
+ * @resolves Handle on an opened SQLite database.
+ */
+function getDatabaseConnection() {
+ return Task.spawn(function get_database_connection_task() {
+ let db = yield Sqlite.openConnection({ path: DB_PATH });
+ if (gDatabaseEnsured) {
+ throw new Task.Result(db);
+ }
+
+ try {
+ // Check to see if we need to perform any migrations.
+ let dbVersion = parseInt(yield db.getSchemaVersion());
+
+ // getSchemaVersion() returns a 0 int if the schema
+ // version is undefined.
+ if (dbVersion === 0) {
+ yield createDatabase(db);
+ } else if (dbVersion < SCHEMA_VERSION) {
+ yield upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
+ }
+
+ yield db.setSchemaVersion(SCHEMA_VERSION);
+ } catch(e) {
+ // Close the DB connection before passing the exception to the consumer.
+ yield db.close();
+ throw e;
+ }
+
+ gDatabaseEnsured = true;
+ throw new Task.Result(db);
+ });
+}
+
+/**
+ * Validates an item to be saved to the DB.
+ *
+ * @param item
+ * (object) item object to be validated.
+ */
+function validateItem(datasetId, item) {
+ if (!item.url) {
+ throw new ValidationError('HomeStorage: All rows must have an URL: datasetId = ' +
+ datasetId);
+ }
+
+ if (!item.image_url && !item.title && !item.description) {
+ throw new ValidationError('HomeStorage: All rows must have at least an image URL, ' +
+ 'or a title or a description: datasetId = ' + datasetId);
+ }
+}
+
+var gRefreshTimers = {};
+
+/**
+ * Sends a message to Java to refresh the given dataset. Delays sending
+ * messages to avoid successive refreshes, which can result in flashing views.
+ */
+function refreshDataset(datasetId) {
+ // Bail if there's already a refresh timer waiting to fire
+ if (gRefreshTimers[datasetId]) {
+ return;
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(function(timer) {
+ delete gRefreshTimers[datasetId];
+
+ Messaging.sendRequest({
+ type: "HomePanels:RefreshDataset",
+ datasetId: datasetId
+ });
+ }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ gRefreshTimers[datasetId] = timer;
+}
+
+HomeStorage.prototype = {
+ /**
+ * Saves data rows to the DB.
+ *
+ * @param data
+ * An array of JS objects represnting row items to save.
+ * Each object may have the following properties:
+ * - url (string)
+ * - title (string)
+ * - description (string)
+ * - image_url (string)
+ * - filter (string)
+ * @param options
+ * A JS object holding additional cofiguration properties.
+ * The following properties are currently supported:
+ * - replace (boolean): Whether or not to replace existing items.
+ *
+ * @return Promise
+ * @resolves When the operation has completed.
+ */
+ save: function(data, options) {
+ if (data && data.length > MAX_SAVE_COUNT) {
+ throw "save failed for dataset = " + this.datasetId +
+ ": you cannot save more than " + MAX_SAVE_COUNT + " items at once";
+ }
+
+ return Task.spawn(function save_task() {
+ let db = yield getDatabaseConnection();
+ try {
+ yield db.executeTransaction(function save_transaction() {
+ if (options && options.replace) {
+ yield db.executeCached(SQL.deleteFromDataset, { dataset_id: this.datasetId });
+ }
+
+ // Insert data into DB.
+ for (let item of data) {
+ validateItem(this.datasetId, item);
+
+ // XXX: Directly pass item as params? More validation for item?
+ let params = {
+ dataset_id: this.datasetId,
+ url: item.url,
+ title: item.title,
+ description: item.description,
+ image_url: item.image_url,
+ background_color: item.background_color,
+ background_url: item.background_url,
+ filter: item.filter,
+ created: Date.now()
+ };
+ yield db.executeCached(SQL.insertItem, params);
+ }
+ }.bind(this));
+ } finally {
+ yield db.close();
+ }
+
+ refreshDataset(this.datasetId);
+ }.bind(this));
+ },
+
+ /**
+ * Deletes all rows associated with this storage.
+ *
+ * @return Promise
+ * @resolves When the operation has completed.
+ */
+ deleteAll: function() {
+ return Task.spawn(function delete_all_task() {
+ let db = yield getDatabaseConnection();
+ try {
+ let params = { dataset_id: this.datasetId };
+ yield db.executeCached(SQL.deleteFromDataset, params);
+ } finally {
+ yield db.close();
+ }
+
+ refreshDataset(this.datasetId);
+ }.bind(this));
+ }
+};