mobile/android/modules/HomeProvider.jsm

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:ecc91bb5a235
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/. */
5
6 "use strict";
7
8 this.EXPORTED_SYMBOLS = [ "HomeProvider" ];
9
10 const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
11
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");
19
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;
26
27 XPCOMUtils.defineLazyGetter(this, "DB_PATH", function() {
28 return OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
29 });
30
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";
34
35 XPCOMUtils.defineLazyGetter(this, "gSyncCheckIntervalSecs", function() {
36 return Services.prefs.getIntPref(PREF_SYNC_CHECK_INTERVAL_SECS);
37 });
38
39 XPCOMUtils.defineLazyServiceGetter(this,
40 "gUpdateTimerManager", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
41
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 ")",
57
58 dropItemsTable:
59 "DROP TABLE items",
60
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)",
64
65 deleteFromDataset:
66 "DELETE FROM items WHERE dataset_id = :dataset_id"
67 }
68
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 }
77
78 function getNowInSeconds() {
79 return Math.round(Date.now() / 1000);
80 }
81
82 function getLastSyncPrefName(datasetId) {
83 return PREF_STORAGE_LAST_SYNC_TIME_PREFIX + datasetId;
84 }
85
86 // Whether or not we've registered an update timer.
87 var gTimerRegistered = false;
88
89 // Map of datasetId -> { interval: <integer>, callback: <function> }
90 var gSyncCallbacks = {};
91
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) { }
103
104 let now = getNowInSeconds();
105 let { interval: interval, callback: callback } = gSyncCallbacks[datasetId];
106
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 }
115
116 this.HomeStorage = function(datasetId) {
117 this.datasetId = datasetId;
118 };
119
120 this.ValidationError = function(message) {
121 this.name = "ValidationError";
122 this.message = message;
123 };
124 ValidationError.prototype = new Error();
125 ValidationError.prototype.constructor = ValidationError;
126
127 this.HomeProvider = Object.freeze({
128 ValidationError: ValidationError,
129
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 },
141
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 }
156
157 callback(datasetId);
158 return true;
159 },
160
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 }
174
175 gSyncCallbacks[datasetId] = {
176 interval: interval,
177 callback: callback
178 };
179
180 if (!gTimerRegistered) {
181 gUpdateTimerManager.registerTimer("home-provider-sync-timer", syncTimerCallback, gSyncCheckIntervalSecs);
182 gTimerRegistered = true;
183 }
184 },
185
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 });
197
198 var gDatabaseEnsured = false;
199
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 }
208
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 }
226
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 }
241
242 try {
243 // Check to see if we need to perform any migrations.
244 let dbVersion = parseInt(yield db.getSchemaVersion());
245
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 }
253
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 }
260
261 gDatabaseEnsured = true;
262 throw new Task.Result(db);
263 });
264 }
265
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 }
277
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 }
283
284 var gRefreshTimers = {};
285
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 }
295
296 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
297 timer.initWithCallback(function(timer) {
298 delete gRefreshTimers[datasetId];
299
300 sendMessageToJava({
301 type: "HomePanels:RefreshDataset",
302 datasetId: datasetId
303 });
304 }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
305
306 gRefreshTimers[datasetId] = timer;
307 }
308
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 }
337
338 // Insert data into DB.
339 for (let item of data) {
340 validateItem(this.datasetId, item);
341
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 }
358
359 refreshDataset(this.datasetId);
360 }.bind(this));
361 },
362
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 }
378
379 refreshDataset(this.datasetId);
380 }.bind(this));
381 }
382 };

mercurial