зеркало из https://github.com/mozilla/gecko-dev.git
408 строки
12 KiB
JavaScript
408 строки
12 KiB
JavaScript
// -*- 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));
|
|
}
|
|
};
|