Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,72 @@
define(function(require, exports, module) {
'use strict';
var Authentication = require('common/error').Authentication;
var InvalidServer = require('common/error').InvalidServer;
var Responder = require('common/responder');
var nextTick = require('common/next_tick');
var notification = require('notification');
/**
* Global error handler / default handling for errors.
*/
function ErrorController() {
Responder.call(this);
this._handlers = Object.create(null);
}
module.exports = ErrorController;
ErrorController.prototype = {
__proto__: Responder.prototype,
/**
* URL in which account errors are dispatched to.
*/
accountErrorUrl: '/update-account/',
/**
* Dispatch an error event.
*
* If this type of event has been captured will be dispatched directly to
* the callback provided. Otherwise the default behaviour will be triggered.
*
* @param {Calendar.Error} error to dispatch.
*/
dispatch: function(error) {
if (error instanceof Authentication || error instanceof InvalidServer) {
this.handleAuthenticate(error.detail.account);
}
this.emit('error', error);
},
/**
* Default handler for authentication errors.
*
* @param {Object} account to notify user about.
* @param {Function} [callback] optional callback.
*/
handleAuthenticate: function(account, callback) {
if (!account) {
return console.error('attempting to trigger reauth without an account');
}
// only trigger notification the first time there is an error.
if (!account.error || account.error.count !== 1) {
return nextTick(callback);
}
var lock = navigator.requestWakeLock('cpu');
var titleL10n = 'notification-error-sync-title';
var bodyL10n = 'notification-error-sync-description';
var url = this.accountErrorUrl + account._id;
notification.sendNotification(titleL10n, bodyL10n, url).then(() => {
callback && callback();
lock.unlock();
});
}
};
});

View file

@ -0,0 +1,108 @@
/* global mozIntl */
define(function(require, exports) {
'use strict';
var calc = require('common/calc');
var co = require('ext/co');
var core = require('core');
var debug = require('common/debug')('controllers/notifications');
var messageHandler = require('message_handler');
var notification = require('notification');
exports.observe = function() {
debug('Will start notifications controller...');
messageHandler.responder.on('alarm', exports.onAlarm);
};
exports.unobserve = function() {
messageHandler.responder.off('alarm', exports.onAlarm);
};
exports.onAlarm = co.wrap(function *(alarm) {
var storeFactory = core.storeFactory;
var alarmStore = storeFactory.get('Alarm');
// Fetch our copy of the alarm since this alarm may
// have come from the alarms api and therefore would
// not be up-to-date wrt whether or not it had
// previously been fired.
alarm = yield alarmStore.get(alarm._id);
// The event may have been deleted or rescheduled after mozAlarms queued it.
if (!alarm || alarm.fired) {
return;
}
debug('Will request cpu wake lock...');
var lock = navigator.requestWakeLock('cpu');
debug('Received cpu lock. Will issue notification...');
try {
yield issueNotification(alarm);
} catch (err) {
console.error('[controllers/notifications]', err.toString());
return;
} finally {
// release cpu lock with or without errors
debug('Will release cpu wake lock...');
lock.unlock();
}
// Save that we've fired this alarm.
alarm.fired = true;
yield alarmStore.persist(alarm);
});
var issueNotification = co.wrap(function *(alarm) {
if (alarm.fired) {
// Don't send notification if we've already delivered this alarm
return;
}
var storeFactory = core.storeFactory;
var eventStore = storeFactory.get('Event');
var busytimeStore = storeFactory.get('Busytime');
var trans = core.db.transaction(['busytimes', 'events']);
// Find the event and busytime associated with this alarm.
var [event, busytime] = yield Promise.all([
eventStore.get(alarm.eventId, trans),
busytimeStore.get(alarm.busytimeId, trans)
]);
// just a safeguard on the very unlikely case that busytime or event
// doesn't exist anymore (should be really hard to happen)
if (!event) {
throw new Error(`can't find event with ID: ${alarm.eventId}`);
}
if (!busytime) {
throw new Error(`can't find busytime with ID: ${alarm.busytimeId}`);
}
var begins = calc.dateFromTransport(busytime.start);
var formatter = mozIntl._gaia.RelativeDate(navigator.languages);
var distance = yield formatter.format(begins);
var now = new Date();
var alarmType = begins > now ?
'alarm-start-notice' :
'alarm-started-notice';
var titleL10n = {
id: alarmType,
args: {
title: event.remote.title,
distance: distance
}
};
var bodyL10n = { raw: event.remote.description || '' };
debug('Will send event notification with title', titleL10n,
'body:', bodyL10n);
return notification.sendNotification(
titleL10n,
bodyL10n,
`/alarm-display/${busytime._id}`,
{ id: event.remote.id }
);
});
});

View file

@ -0,0 +1,259 @@
/**
* @fileoverview Period sync controller manages
*
* 1. Seeding first sync alarm when app starts.
* 2. Syncing when a sync alarm fires and issuing a new sync alarm.
* 3. Invalidating any existing sync alarms and issuing a new one
* when the sync interval changes.
*/
define(function(require, exports) {
'use strict';
var Responder = require('common/responder');
var core = require('core');
var debug = require('common/debug')('controllers/periodic_sync');
var messageHandler = require('message_handler');
/**
* Cached alarm previously sent to alarms db.
* @type {Object}
*/
var syncAlarm;
/**
* Cached sync value (every x minutes) from scheduling.
* @type {Number}
*/
var prevSyncFrequency;
/**
* Most recently set sync interval (every x minutes).
* @type {Number}
*/
var syncFrequency;
/**
* Cached promise representing pending sync operation.
* @type {Promise}
*/
var syncing;
/**
* Cached promise representing pending schedule operation.
* @type {Promise}
*/
var scheduling;
var events = new Responder();
exports.events = events;
var accounts;
var settings;
exports.observe = function() {
debug('Will start periodic sync controller...');
var storeFactory = core.storeFactory;
accounts = storeFactory.get('Account');
settings = storeFactory.get('Setting');
return Promise.all([
settings.getValue('syncAlarm'),
settings.getValue('syncFrequency')
])
.then(values => {
[syncAlarm, syncFrequency] = values;
// Trigger whenever there is a change to the accounts collection
// since we need to re-evaluate whether periodic sync is still necessary.
accounts.on('persist', onAccountsChange);
accounts.on('remove', onAccountsChange);
// Listen to the settings collection for a change to sync frequency so that
// we can update any alarms we've sent to the alarms api accordingly.
debug('Will listen for syncFrequencyChange...');
settings.on('syncFrequencyChange', exports);
// Listen for sync event from alarms api.
messageHandler.responder.on('sync', exports);
return scheduleSync();
});
};
exports.unobserve = function() {
syncAlarm = null;
prevSyncFrequency = null;
syncFrequency = null;
syncing = null;
scheduling = null;
accounts.off('persist', onAccountsChange);
accounts.off('remove', onAccountsChange);
settings.off('syncFrequencyChange', exports);
messageHandler.responder.off('sync', exports);
};
exports.handleEvent = function(event) {
switch (event.type) {
case 'sync':
// Gets triggered by mozSetMessageHandler alarm event.
return onSync();
case 'syncFrequencyChange':
// Gets triggered by settings db change to sync frequency.
debug('Received syncFrequencyChange!');
return onSyncFrequencyChange(event.data[0]);
}
};
/**
* 1. Wait until we're done with any previous work.
* 2. Sync.
* 3. Schedule the next periodic sync.
*/
function onSync() {
return sync().then(scheduleSync);
}
/**
* 1. Wait until we're done with any previous work.
* 2. Schedule a periodic sync at the new interval.
*/
function onSyncFrequencyChange(value) {
debug('Sync frequency changed to', value);
syncFrequency = value;
return maybeScheduleSync();
}
/**
* No syncable accounts => revoke any previously scheduled sync alarms.
* Syncable accounts and no previously scheduled sync => schedule new sync.
*/
function onAccountsChange() {
debug('Looking up syncable accounts...');
return accounts.syncableAccounts().then(syncable => {
if (!syncable || !syncable.length) {
debug('There are no syncable accounts!');
revokePreviousAlarm();
events.emit('pause');
return;
}
debug('There are', syncable.length, 'syncable accounts');
if (!syncAlarmIssued()) {
debug('The first syncable account was just added.');
return scheduleSync();
}
});
}
function sync() {
if (!syncing) {
syncing = new Promise((resolve, reject) => {
debug('Will request cpu and wifi wake locks...');
var cpuLock = navigator.requestWakeLock('cpu');
var wifiLock = navigator.requestWakeLock('wifi');
debug('Will start periodic sync...');
core.syncController.all(() => {
debug('Sync complete! Will release cpu and wifi wake locks...');
cpuLock.unlock();
wifiLock.unlock();
events.emit('sync');
syncing = null;
resolve();
});
});
}
return syncing;
}
function scheduleSync() {
if (scheduling) {
return scheduling;
}
scheduling = accounts.syncableAccounts()
.then(syncable => {
if (!syncable || !syncable.length) {
debug('There seem to be no syncable accounts, will defer scheduling...');
return Promise.resolve();
}
debug('Will schedule periodic sync in:', syncFrequency);
// Cache the sync interval which we're sending to the alarms api.
prevSyncFrequency = syncFrequency;
revokePreviousAlarm();
return issueSyncAlarm().then(cacheSyncAlarm).then(maybeScheduleSync);
})
.then(() => {
events.emit('schedule');
scheduling = null;
})
.catch(error => {
debug('Error scheduling sync:', error);
console.error(error.toString());
scheduling = null;
});
return scheduling;
}
// TODO: When navigator.mozAlarms.remove (one day) returns a DOMRequest,
// we should make this async...
function revokePreviousAlarm() {
if (!syncAlarmIssued()) {
debug('No sync alarms issued, nothing to revoke...');
return;
}
debug('Will revoke alarm', syncAlarm.alarmId);
var alarms = navigator.mozAlarms;
alarms.remove(syncAlarm.alarmId);
}
function issueSyncAlarm() {
if (!prevSyncFrequency) {
debug('Periodic sync disabled!');
return Promise.resolve({ alarmId: null, start: null, end: null });
}
var start = new Date();
var end = new Date(
start.getTime() +
prevSyncFrequency * 60 * 1000 // minutes to ms
);
var alarms = navigator.mozAlarms;
var request = alarms.add(end, 'ignoreTimezone', { type: 'sync' });
return new Promise((resolve, reject) => {
request.onsuccess = function() {
resolve({ alarmId: this.result, start: start, end: end });
};
request.onerror = function() {
reject(this.error);
};
});
}
function cacheSyncAlarm(alarm) {
debug('Will save alarm:', alarm);
syncAlarm = alarm;
return settings.set('syncAlarm', syncAlarm);
}
function maybeScheduleSync() {
if (syncFrequency === prevSyncFrequency) {
// Nothing to do!
return Promise.resolve();
}
if (scheduling) {
return scheduling.then(scheduleSync);
}
return scheduleSync();
}
function syncAlarmIssued() {
return syncAlarm && !!syncAlarm.alarmId;
}
});

View file

@ -0,0 +1,248 @@
define(function(require, exports, module) {
'use strict';
var Responder = require('common/responder');
var core = require('core');
var debug = require('common/debug')('controllers/recurring_events');
var nextTick = require('common/next_tick');
function RecurringEvents() {
this.accounts = core.storeFactory.get('Account');
Responder.call(this);
}
module.exports = RecurringEvents;
RecurringEvents.prototype = {
__proto__: Responder.prototype,
startEvent: 'expandStart',
completeEvent: 'expandComplete',
/**
* Adds N number of days to the window to expand
* events until. Its very important for this number
* to be greater then the maximum number of days displayed
* in the month view (or a view with more days) otherwise
* the view may be loaded without actually expanding all
* the visible days.
*
* @type Number
*/
paddingInDays: 85,
/**
* Amount of time (in MS) to wait between triggering
* the recurring event expansions.
*/
waitBeforeMove: 750,
/**
* We need to limit the number of tries on expansions
* otherwise its possible we never complete during error
* or long recurring event.
*/
maximumExpansions: 25,
/**
* private timeout (as in setTimeout id) use with waitBeforeMove.
*/
_moveTimeout: null,
/**
* True when queue is running...
*/
pending: false,
unobserve: function() {
core.timeController.removeEventListener(
'monthChange',
this
);
core.syncController.removeEventListener(
'syncComplete',
this
);
},
observe: function() {
var time = core.timeController;
// expand initial time this is necessary
// for cases where user has device off for long periods of time.
if (time.position) {
this.queueExpand(time.position);
}
// register observers
time.on('monthChange', this);
// we must re-expand after sync so events at least
// expand to the current position....
core.syncController.on('syncComplete', this);
},
handleEvent: function(event) {
switch (event.type) {
case 'syncComplete':
this.queueExpand(
core.timeController.position
);
break;
case 'monthChange':
if (this._moveTimeout !== null) {
clearTimeout(this._moveTimeout);
this._moveTimeout = null;
}
// trigger the event queue when we move
this._moveTimeout = setTimeout(
// data[0] is the new date.
this.queueExpand.bind(this, event.data[0]),
this.waitBeforeMove
);
break;
}
},
/**
* Attempts to expand provider until no events require expansion.
*
* @param {Date} expandDate expands up to this date.
* @param {Calendar.Provider.Abstract} provider instance.
* @param {Function} callback
* fired when maximumExpansions is hit or
* no more events require expansion.
*
*/
_expandProvider: function(expandDate, provider, callback) {
debug('Will attempt to expand provider until:', expandDate);
var tries = 0;
var max = this.maximumExpansions;
function attemptCompleteExpand() {
debug('Will try to complete expansion (tries = ' + tries + ')');
if (tries >= max) {
return callback(new Error(
'could not complete expansion after "' + tries + '"'
));
}
provider.ensureRecurrencesExpanded(expandDate, function(err, didExpand) {
if (err) {
return callback(err);
}
debug('Expansion attempt did expand:', didExpand);
if (!didExpand) {
// successfully expanded and no events need expansion
// for this date anymore...
callback();
} else {
tries++;
// attempt another expand without stack.
nextTick(attemptCompleteExpand);
}
});
}
attemptCompleteExpand();
},
/**
* Queues an expansion. If the given date is before
* any dates in the stack it will be discarded.
*/
queueExpand: function(expandTo) {
if (this.pending) {
if (!this._next) {
this._next = expandTo;
} else if (expandTo > this._next) {
this._next = expandTo;
}
// don't start the queue if pending...
return;
}
// either way we need to process an event
// so increment pending for running and non-running cases.
this.pending = true;
this.emit('expandStart');
var self = this;
function expandNext(date) {
self.expand(date, function() {
if (date === self._next) {
self._next = null;
}
var next = self._next;
// when the queue is empty emit expandComplete
if (!next) {
self.pending = false;
self.emit('expandComplete');
return;
}
expandNext(next);
});
}
expandNext(expandTo);
},
/**
* Ensures we have time converage until the given date.
* Additional time will be added to the date see .paddingInDays.
*
* @param {Date} expandTo date to expand to.
*/
expand: function(expandTo, callback) {
debug('expand', expandTo);
this.accounts.all((err, accounts) => {
if (err) {
return callback(err);
}
// add minimum padding...
var expandDate = new Date(expandTo.valueOf());
expandDate.setDate(expandDate.getDate() + this.paddingInDays);
var providers = this._getExpandableProviders(accounts);
var pending = providers.length;
if (!pending) {
return nextTick(callback);
}
providers.forEach(provider => {
this._expandProvider(expandDate, provider, () => {
if (--pending <= 0) {
callback();
}
});
});
});
},
_getExpandableProviders: function(accounts) {
var providers = [];
Object.keys(accounts).forEach(key => {
var account = accounts[key];
var provider = core.providerFactory.get(account.providerType);
if (provider &&
provider.canExpandRecurringEvents &&
providers.indexOf(provider) === -1) {
providers.push(provider);
}
});
return providers;
}
};
});

View file

@ -0,0 +1,21 @@
define(function(require, exports, module) {
'use strict';
var Manager = require('worker/manager');
var debug = require('common/debug')('controllers/service');
function Service() {
Manager.call(this);
}
module.exports = Service;
Service.prototype = {
__proto__: Manager.prototype,
start: function() {
debug('Will load and initialize worker...');
this.add('caldav', '/js/caldav_worker.js');
}
};
});

View file

@ -0,0 +1,174 @@
define(function(require, exports, module) {
'use strict';
var Responder = require('common/responder');
var core = require('core');
var isOffline = require('common/is_offline');
/**
* Handles all synchronization related
* tasks. The intent is that this will
* be the focal point for any view
* to observe sync events and this
* controller will decide when to actually
* tell the stores when to sync.
*/
function Sync() {
this.pending = 0;
Responder.call(this);
}
module.exports = Sync;
Sync.prototype = {
__proto__: Responder.prototype,
startEvent: 'syncStart',
completeEvent: 'syncComplete',
_incrementPending: function() {
if (!this.pending) {
this.emit('syncStart');
}
this.pending++;
},
_resolvePending: function() {
if (!(--this.pending)) {
this.emit('syncComplete');
}
if (this.pending < 0) {
dump('\n\n Error calendar sync .pending is < 0 \n\n');
}
},
/**
* Sync all accounts, calendars, events.
* There is no callback for all intentionally.
*
* Use:
*
* controller.once('syncComplete', cb);
*
*/
all: function(callback) {
// this is for backwards compatibility... in reality we should remove
// callbacks from .all.
if (callback) {
this.once('syncComplete', callback);
}
if (isOffline()) {
this.emit('offline');
this.emit('syncComplete');
return;
}
var account = core.storeFactory.get('Account');
account.all(function(err, list) {
for (var key in list) {
this.account(list[key]);
}
// If we have nothing to sync
if (!this.pending) {
this.emit('syncComplete');
}
}.bind(this));
},
/**
* Initiates a sync for a single calendar.
*
* @param {Object} account parent of calendar.
* @param {Object} calendar specific calendar to sync.
* @param {Function} [callback] optional callback.
*/
calendar: function(account, calendar, callback) {
var store = core.storeFactory.get('Calendar');
var self = this;
this._incrementPending();
store.sync(account, calendar, err => {
self._resolvePending();
this.handleError(err, callback);
});
},
/**
* Initiates a sync of a single account and all
* associated calendars (calendars that exist after
* the full sync of the account itself).
*
* The contract is if an callback is given the callback MUST handle the
* error given. The default behaviour is to bubble up the error up to the
* error controller.
*
* @param {Object} account sync target.
* @param {Function} [callback] optional callback.
*/
account: function(account, callback) {
var storeFactory = core.storeFactory;
var accountStore = storeFactory.get('Account');
var calendarStore = storeFactory.get('Calendar');
var self = this;
this._incrementPending();
accountStore.sync(account, err => {
if (err) {
self._resolvePending();
return this.handleError(err, callback);
}
var pending = 0;
function next() {
if (!(--pending)) {
self._resolvePending();
if (callback) {
callback();
}
}
}
function fetchCalendars(err, calendars) {
if (err) {
self._resolvePending();
return self.handleError(err, callback);
}
for (var key in calendars) {
pending++;
self.calendar(account, calendars[key], next);
}
}
// find all calendars
calendarStore.remotesByAccount(
account._id,
fetchCalendars
);
});
},
/**
* Private helper for choosing how to dispatch errors.
* When given a callback the callback will be called otherwise the error
* controller will be invoked.
*/
handleError: function(err, callback) {
if (callback) {
return callback(err);
}
core.errorController.dispatch(err);
}
};
});

View file

@ -0,0 +1,172 @@
define(function(require, exports, module) {
'use strict';
// Controls the selected/displayed dates by all the calendar views.
// ---
// All views that needs to listen date selection/navigation changes, or set
// a new value, should do it through this module (eg. day/week/month views and
// DayObserver)
var isSameDate = require('common/calc').isSameDate;
var Responder = require('common/responder');
function Time() {
Responder.call(this);
this._timeCache = Object.create(null);
}
module.exports = Time;
Time.prototype = {
__proto__: Responder.prototype,
/**
* Current position in time.
* Includes year, month and day.
*
* @type {Date}
*/
_position: null,
/**
* Current center point of cached
* time spans. This is not the last
* loaded timespan but the last
* requested timespan.
*
* @type {Calendar.Timespan}
*/
_currentTimespan: null,
/**
* Hash that contains
* the pieces of the current _position.
* (month, day, year)
*/
_timeCache: null,
/**
* The time 'scale' of the current
* state of the calendar.
*
* Usually one of: ['day', 'month', 'week']
* @type {String}
*/
_scale: null,
/**
* private state of mostRecentDayType
*/
_mostRecentDayType: 'day',
/**
* When true will lock the cache so no records are
* purged. This is critical during sync because some
* records may not yet be in the database.
*/
cacheLocked: false,
/**
* Returns the most recently changed
* day type either 'day' or 'selectedDay'
*/
get mostRecentDayType() {
return this._mostRecentDayType;
},
get mostRecentDay() {
if (this.mostRecentDayType === 'selectedDay') {
return this.selectedDay;
} else {
return this.position;
}
},
get timespan() {
return this._currentTimespan;
},
get scale() {
return this._scale;
},
set scale(value) {
var oldValue = this._scale;
if (value !== oldValue) {
this._scale = value;
this.emit('scaleChange', value, oldValue);
}
},
get selectedDay() {
return this._selectedDay;
},
set selectedDay(value) {
var day = this._selectedDay;
this._mostRecentDayType = 'selectedDay';
if (!day || !isSameDate(day, value)) {
this._selectedDay = value;
this.emit('selectedDayChange', value, day);
}
},
/**
* Helper function to 'move' state of calendar
* to the most recently modified day type.
*
* (in the case where selectedDay was changed after day)
*/
moveToMostRecentDay: function() {
if (this.mostRecentDayType === 'selectedDay') {
this.move(this.selectedDay);
}
},
_updateCache: function(type, value) {
var old = this._timeCache[type];
if (!old || !isSameDate(value, old)) {
this._timeCache[type] = value;
this.emit(type + 'Change', value, old);
}
},
get month() {
return this._timeCache.month;
},
get day() {
return this._timeCache.day;
},
get year() {
return this._timeCache.year;
},
get position() {
return this._position;
},
/**
* Sets position of controller
* in time.
*
* @param {Date} date position to move to.
*/
move: function(date) {
var year = date.getFullYear();
var month = date.getMonth();
var yearDate = new Date(year, 0, 1);
var monthDate = new Date(year, month, 1);
this._position = date;
this._mostRecentDayType = 'day';
this._updateCache('year', yearDate);
this._updateCache('month', monthDate);
this._updateCache('day', date);
}
};
});