@@ -9,6 +9,15 @@ define(function(require) {
console.log('cronsync-main: ' + str);
}

function makeData(accountIds, interval, date) {
return {
type: 'sync',
accountIds: accountIds,
interval: interval,
timestamp: date.getTime()
};
}

// Creates a string key from an array of string IDs. Uses a space
// separator since that cannot show up in an ID.
function makeAccountKey(accountIds) {
@@ -24,9 +33,8 @@ define(function(require) {

// Makes sure two arrays have the same values, account IDs.
function hasSameValues(ary1, ary2) {
if (ary1.length !== ary2.length) {
if (ary1.length !== ary2.length)
return false;
}

var hasMismatch = ary1.some(function(item, i) {
return item !== ary2[i];
@@ -36,10 +44,13 @@ define(function(require) {
}

if (navigator.mozSetMessageHandler) {
navigator.mozSetMessageHandler('request-sync',
function onRequestSync(e) {
var data = e.data;
// Need to acquire the wake locks during this notification
navigator.mozSetMessageHandler('alarm', function onAlarm(alarm) {
// Do not bother with alarms that are not sync alarms.
var data = alarm.data;
if (!data || data.type !== 'sync')
return;

// Need to acquire the wake locks during this alarm notification
// turn of the event loop -- later turns are not guaranteed to
// be up and running. However, knowing when to release the locks
// is only known to the front end, so publish event about it.
@@ -61,10 +72,7 @@ define(function(require) {
makeAccountKey(data.accountIds), locks);
}

debug('request-sync started at ' + (new Date()));

dispatcher._sendMessage('requestSync',
[data.accountIds, data.interval]);
dispatcher._sendMessage('alarm', [data.accountIds, data.interval]);
});
}

@@ -95,157 +103,161 @@ define(function(require) {
},

/**
* Clears all sync-based tasks. Normally not called, except perhaps for
* Clears all sync-based alarms. Normally not called, except perhaps for
* tests or debugging.
*/
clearAll: function() {
var navSync = navigator.sync;
if (!navSync) {
var mozAlarms = navigator.mozAlarms;
if (!mozAlarms)
return;
}

navSync.registrations().then(function(registrations) {
if (!registrations.length) {
var r = mozAlarms.getAll();

r.onsuccess = function(event) {
var alarms = event.target.result;
if (!alarms)
return;
}

registrations.forEach(function(registeredTask) {
navSync.unregister(registeredTask.task);
alarms.forEach(function(alarm) {
if (alarm.data && alarm.data.type === 'sync')
mozAlarms.remove(alarm.id);
});
}.bind(this),
function(err) {
console.error('cronsync-main clearAll navigator.sync.registrations ' +
'error: ' + err);
}.bind(this));
}.bind(this);
r.onerror = function(err) {
console.error('cronsync-main clearAll mozAlarms.getAll: error: ' +
err);
}.bind(this);
},

/**
* Makes sure there is an sync task set for every account in
* Makes sure there is an alarm set for every account in
* the list.
* @param {Object} syncData. An object with keys that are
* 'interval' + intervalInMilliseconds, and values are arrays
* of account IDs that should be synced at that interval.
*/
ensureSync: function (syncData) {
var navSync = navigator.sync;
if (!navSync) {
console.warn('no navigator.sync support!');
// Let backend know work has finished, even though it was a no-op.
this._sendMessage('syncEnsured');
var mozAlarms = navigator.mozAlarms;
if (!mozAlarms) {
console.warn('no mozAlarms support!');
return;
}

debug('ensureSync called');

navSync.registrations().then(function(registrations) {
var request = mozAlarms.getAll();

request.onsuccess = function(event) {
debug('success!');

// Find all IDs being tracked by sync tasks
var expiredTasks = [],
okTaskIntervals = {},
uniqueTasks = {};
var alarms = event.target.result;
// If there are no alarms a falsey value may be returned. We want
// to not die and also make sure to signal we completed, so just make
// an empty list.
if (!alarms) {
alarms = [];
}

// Find all IDs being tracked by alarms
var expiredAlarmIds = [],
okAlarmIntervals = {},
uniqueAlarms = {};

alarms.forEach(function(alarm) {
// Only care about sync alarms.
if (!alarm.data || !alarm.data.type || alarm.data.type !== 'sync')
return;

registrations.forEach(function(task) {
// minInterval in seconds, but use milliseconds for sync values
// internally.
var intervalKey = 'interval' + (task.minInterval * 1000),
var intervalKey = 'interval' + alarm.data.interval,
wantedAccountIds = syncData[intervalKey];

if (!wantedAccountIds || !hasSameValues(wantedAccountIds,
task.data.accountIds)) {
debug('account array mismatch, canceling existing sync task');
expiredTasks.push(task);
alarm.data.accountIds)) {
debug('account array mismatch, canceling existing alarm');
expiredAlarmIds.push(alarm.id);
} else {
// Confirm the existing sync task is still good.
// Confirm the existing alarm is still good.
var interval = toInterval(intervalKey),
now = Date.now(),
alarmTime = alarm.data.timestamp,
accountKey = makeAccountKey(wantedAccountIds);

// If the interval is nonzero, and there is no other task found
// If the interval is nonzero, and there is no other alarm found
// for that account combo, and if it is not in the past and if it
// is not too far in the future, it is OK to keep.
if (interval && !uniqueTasks.hasOwnProperty(accountKey)) {
debug('existing sync task is OK: ' + interval);
uniqueTasks[accountKey] = true;
okTaskIntervals[intervalKey] = true;
if (interval && !uniqueAlarms.hasOwnProperty(accountKey) &&
alarmTime > now && alarmTime < now + interval) {
debug('existing alarm is OK');
uniqueAlarms[accountKey] = true;
okAlarmIntervals[intervalKey] = true;
} else {
debug('existing sync task is out of interval range, canceling');
expiredTasks.push(task);
debug('existing alarm is out of interval range, canceling');
expiredAlarmIds.push(alarm.id);
}
}
});

expiredTasks.forEach(function(expiredTask) {
navSync.unregister(expiredTask.task);
expiredAlarmIds.forEach(function(alarmId) {
mozAlarms.remove(alarmId);
});

var taskMax = 0,
taskCount = 0,
var alarmMax = 0,
alarmCount = 0,
self = this;

// Called when sync tasks are confirmed to be set.
// Called when alarms are confirmed to be set.
function done() {
taskCount += 1;
if (taskCount < taskMax) {
alarmCount += 1;
if (alarmCount < alarmMax)
return;
}

debug('ensureSync completed');
// Indicate ensureSync has completed because the
// back end is waiting to hear sync task was set
// before triggering sync complete.
// back end is waiting to hear alarm was set before
// triggering sync complete.
self._sendMessage('syncEnsured');
}

Object.keys(syncData).forEach(function(intervalKey) {
// Skip if the existing sync task is already good.
if (okTaskIntervals.hasOwnProperty(intervalKey)) {
// Skip if the existing alarm is already good.
if (okAlarmIntervals.hasOwnProperty(intervalKey))
return;
}

var interval = toInterval(intervalKey),
accountIds = syncData[intervalKey];
accountIds = syncData[intervalKey],
date = new Date(Date.now() + interval);

// Do not set an timer for a 0 interval, bad things happen.
if (!interval) {
if (!interval)
return;
}

taskMax += 1;

navSync.register('interval' + interval, {
// minInterval is in seconds.
minInterval: interval / 1000,
oneShot: false,
data: {
accountIds: accountIds,
interval: interval
},
wifiOnly: false,
// TODO: allow this to be more generic, getting this passed in
// from the page using this module. This assumes the current page
// without query strings or fragment IDs is the desired entry point.
wakeUpPage: location.href.split('?')[0].split('#')[0] })
.then(function() {
debug('success: navigator.sync.register for ' + 'IDs: ' +
accountIds +
alarmMax += 1;

var alarmRequest = mozAlarms.add(date, 'ignoreTimezone',
makeData(accountIds, interval, date));

alarmRequest.onsuccess = function() {
debug('success: mozAlarms.add for ' + 'IDs: ' + accountIds +
' at ' + interval + 'ms');
done();
}, function(err) {
console.error('cronsync-main navigator.sync.register for IDs: ' +
};

alarmRequest.onerror = function(err) {
console.error('cronsync-main mozAlarms.add for IDs: ' +
accountIds +
' failed: ' + err);
});
};
});

// If no sync tasks were added, indicate ensureSync is done.
if (!taskMax) {
// If no alarms were added, indicate ensureSync is done.
if (!alarmMax)
done();
}
}.bind(this),
function(err) {
console.error('cronsync-main ensureSync navigator.sync.register: ' +
'error: ' + err);
});
}.bind(this);

request.onerror = function(err) {
console.error('cronsync-main ensureSync mozAlarms.getAll: error: ' +
err);
};
}
};

@@ -285,8 +285,8 @@ window.htmlCacheRestorePendingMessage = [];
if (navigator.mozHasPendingMessage('activity')) {
window.htmlCacheRestorePendingMessage.push('activity');
}
if (navigator.mozHasPendingMessage('request-sync')) {
window.htmlCacheRestorePendingMessage.push('request-sync');
if (navigator.mozHasPendingMessage('alarm')) {
window.htmlCacheRestorePendingMessage.push('alarm');
}
if (navigator.mozHasPendingMessage('notification')) {
window.htmlCacheRestorePendingMessage.push('notification');
@@ -204,9 +204,9 @@ if (appMessages.hasPending('activity') ||
console.log('email waitForAppMessage');
}

if (appMessages.hasPending('request-sync')) {
// There is an sync task, do not use the cache node, start fresh,
// as we were woken up just for the sync task.
if (appMessages.hasPending('alarm')) {
// There is an alarm, do not use the cache node, start fresh,
// as we were woken up just for the alarm.
cachedNode = null;
startedInBackground = true;
console.log('email startedInBackground');
@@ -1,10 +1,9 @@
/*jshint browser: true */
/*global define, console, Notification */
/*global define, console, plog, Notification */
'use strict';
define(function(require) {

var cronSyncStartTime,
appSelf = require('app_self'),
var appSelf = require('app_self'),
evt = require('evt'),
model = require('model'),
mozL10n = require('l10n!'),
@@ -95,7 +94,6 @@ define(function(require) {

api.oncronsyncstart = function(accountIds) {
console.log('email oncronsyncstart: ' + accountIds);
cronSyncStartTime = Date.now();
var accountKey = makeAccountKey(accountIds);
waitingOnCron[accountKey] = true;
};
@@ -229,9 +227,13 @@ define(function(require) {
});

if (!hasBeenVisible && !stillWaiting) {
console.log('sync completed in ' +
((Date.now() - cronSyncStartTime) / 1000) +
' seconds, closing mail app');
var msg = 'mail sync complete, closing mail app';
if (typeof plog === 'function') {
plog(msg);
} else {
console.log(msg);
}

window.close();
}
}
@@ -8,11 +8,12 @@
"url": "https://github.com/mozilla-b2g/gaia"
},
"messages": [
{ "notification": "/index.html" },
{ "request-sync": "/index.html" }
{ "alarm": "/index.html" },
{ "notification": "/index.html" }
],
"permissions": {
"themeable":{},
"alarms":{},
"browser": {},
"audio-channel-notification":{},
"contacts":{ "access": "readcreate" },
@@ -19,20 +19,23 @@ EmailSync.prototype = {

/**
* Does the work to trigger a sync using helpers in
* mock_navigator_moz_set_message_handler.js.
* mock_navigator_mozalarms.js. Assumes the mock
* mock_navigator_mozalarms.js was already injected.
*/
triggerSync: function() {
// trigger sync in Email App
this.client.executeScript(function() {
var interval = 1000;
var date = new Date(Date.now() + interval).getTime();
var task = {
var alarm = {
data: {
type: 'sync',
accountIds: ['0', '1'],
interval: interval
interval: interval,
timestamp: date
}
};
return window.wrappedJSObject.fireMessageHandler(task, 'request-sync');
return window.wrappedJSObject.fireMessageHandler(alarm);
});
}
};
@@ -18,20 +18,23 @@ marionette('email notifications, set interval', function() {
}
}, this);

function getSynRegistrations(client) {
var regs;
function getAlarms(client) {
var alarms;
client.waitFor(function() {
regs = client.executeScript(function() {
var nav = window.wrappedJSObject.navigator;
return nav.__mozFakeSyncRegistrations;
alarms = client.executeScript(function() {
var alarms = window.wrappedJSObject.navigator.__mozFakeAlarms;
if (alarms && alarms.length) {
return alarms;
}
});
return regs;
return alarms;
});
return regs;
return alarms;
}

setup(function() {
app = new Email(client);
client.contentScript.inject(SHARED_PATH + '/mock_navigator_mozalarms.js');
app.launch();
});

@@ -50,6 +53,12 @@ marionette('email notifications, set interval', function() {
var emailData = new EmailData(client);
emailData.waitForCurrentAccountUpdate('syncInterval', 3600000);

// Make sure an alarm was set.
var alarms = getAlarms(client);
assert(alarms.length === 1, 'have one alarm');
var alarm = alarms[0];
assert(alarm.interval === 3600000, 'has correct interval value');

// Close the app, relaunch and make sure syncInterval was
// persisted correctly
app.close();
@@ -16,21 +16,18 @@ Services.obs.addObserver(function(document) {
}

var window = document.defaultView.wrappedJSObject;
var messageHandlers = {};
var messageHandler;

window.navigator.__defineGetter__('mozSetMessageHandler', function() {
return function(type, callback) {
messageHandlers[type] = callback;
if (type === 'alarm') {
messageHandler = callback;
}
};
});

window.__defineGetter__('fireMessageHandler', function() {
return function(data, type) {
// For historical purposes, this only supported alarm callbacks. To avoid
// force updating code that still assumes that, still assume that as a
// default.
type = type || 'alarm';
var messageHandler = messageHandlers[type];
return function(data) {
if (messageHandler && typeof(messageHandler) === 'function') {
messageHandler(data);
return true;