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,934 @@
/* 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';
/* global asyncStorage */
/* global ERROR_REQUEST_SYNC_REGISTRATION */
/* global ERROR_SYNC_APP_GENERIC */
/* global ERROR_SYNC_APP_KILLED */
/* global ERROR_SYNC_REQUEST */
/* global ERROR_UNVERIFIED_ACCOUNT */
/* global FxAccountsClient */
/* global IACHandler */
/* global LazyLoader */
/* global SettingsListener */
/* global SyncErrors */
/* global SyncStateMachine */
/* global SyncRecoverableErrors */
/* global uuid */
/**
* This module manages the lifecycle of Firefox Sync on Firefox OS.
* It connects the Settings application where the Firefox Sync UI is presented
* to the user with the Sync app that is responsible of doing the actual
* synchronization of the user's data.
*
* SyncManager reacts to changes done on the Settings Sync UI that triggers
* IAC requests or Settings API changes and translates these changes into
* actions. It makes use of the SyncStateMachine to control that these actions
* are possible on the current situation of Sync and communicates with the
* Sync application via IAC API messages when needed.
*
* The IAC based API that is exposed to the Settings app (or any other
* future consumer that requires to manage Sync) expects IAC messages with
* a single property 'name' that can have any of the following values:
* 'enable': requests to enable Sync.
* 'disable': requests to disable Sync.
* 'sync': requests a sync on demand. Although we have scheduled
* synchronizations, we allow on demand sync requests.
* 'getInfo': gets the current information about the state of Sync
* including the state (enabled, disabled, etc.), the
* last time a successful sync happened and the last error
* if there's any.
*
* It also triggers one message through the same IAC API with name
* 'onsyncchange' when something (state, error, lastSync) changes on Sync.
*
* SyncManager also communicates with the Sync app via IAC. Every time a
* synchronization is required (either because the requestSync timer fires or
* because the user requested a sync on demand), we wake the Sync app up and
* give it the required information to perform the synchronization that
* includes the URL of the Syncto server, a valid FxA assertion and its
* associated encryption keys and the list of collections to be synchronized.
*/
(function(exports) {
const FXA_EVENT = 'mozFxAccountsUnsolChromeEvent';
const SYNC_TASK = 'gaia::system::firefoxsync';
const SYNC_REQUEST_IAC_KEYWORD = 'gaia::sync::request';
const SYNC_MANAGEMENT_API_IAC_KEYWORD = 'gaia-sync-management';
const COLLECTIONS = ['history', 'passwords', 'bookmarks'];
const SETTINGS = [
'sync.collections.bookmarks.enabled',
'sync.collections.history.enabled',
'sync.collections.passwords.enabled',
'sync.collections.bookmarks.limit',
'sync.collections.history.limit',
'sync.collections.passwords.limit',
'sync.collections.bookmarks.readonly',
'sync.collections.history.readonly',
'sync.collections.passwords.readonly',
'sync.fxa.audience',
'sync.scheduler.interval',
'sync.scheduler.wifionly',
'sync.server.url'
];
// Keys of asyncStorage persisted data.
const SYNC_STATE = 'sync.state';
const SYNC_STATE_ERROR = 'sync.state.error';
const SYNC_LAST_TIME = 'sync.lastTime';
const SYNC_USER = 'sync.user';
function SyncManager() {}
SyncManager.prototype = {
DEBUG: false,
store: new Map(),
settings: new Map(),
start() {
this.debug('Start');
return new Promise((resolve, reject) => {
LazyLoader.load('/shared/js/sync/errors.js', () => {
navigator.mozSetMessageHandler('request-sync', event => {
this.debug('Sync request');
if (event.task != SYNC_TASK) {
return;
}
SyncStateMachine.sync();
});
// SyncStateMachine event listeners.
['ondisabled',
'ondisabling',
'onenabled',
'onenabling',
'onerrored',
'onsyncing'].forEach(event => {
SyncStateMachine[event] = this[event].bind(this);
});
// IAC based sync management API.
this.oniacrequestListener = this.oniacrequest.bind(this);
window.addEventListener('iac-' + SYNC_MANAGEMENT_API_IAC_KEYWORD,
this.oniacrequestListener);
// Sync app lifecycle.
this.onappterminatedListener = this.onappterminated.bind(this);
window.addEventListener('appterminated',
this.onappterminatedListener);
// We need to wait until getting the initial value for all settings.
this.observeSettings().then(() => {
this.debug('All settings observed');
return this.initStore();
}).then(() => {
this.debug('Sync.state setting initial value', this.state);
// SyncStateMachine starts by default with 'disabled', so we can
// request to enable sync or stay in the disabled state.
// We need to make sure that we didn't end up in an inconsistent
// state during the latest run (i.e. error, success, syncing,
// enabling, disabling).
switch(this.state) {
case 'enabled':
case 'success':
case 'syncing':
SyncStateMachine.enable();
resolve();
break;
case 'disabling':
case 'enabling':
this.updateState('disabled');
resolve();
break;
case 'errored':
if (this.error == ERROR_UNVERIFIED_ACCOUNT) {
// We try to enable Sync. This will check if the account is
// verified or not. If it's not verified, we go back to the
// errored state, otherwise, the user logs in.
SyncStateMachine.enable();
resolve();
break;
}
this.updateState('disabled');
resolve();
break;
case 'disabled':
resolve();
break;
default:
console.error('Unexpected sync.state value', this.state);
reject();
break;
}
});
});
});
},
// Mostly for testing purposes.
// We won't likely stop this ever in "real life".
stop() {
window.removeEventListener('iac-' + SYNC_MANAGEMENT_API_IAC_KEYWORD,
this.oniacrequestListener);
window.removeEventListener(FXA_EVENT, this.fxaEventHandler);
window.removeEventListener('appterminated',
this.onappterminatedListener);
},
/** Getters and setters **/
set state(state) {
state = state || SyncStateMachine.state;
this.debug('Setting state', state);
this.store.set(SYNC_STATE, state);
this.updateStateDeferred = new Promise(resolve => {
asyncStorage.setItem(SYNC_STATE, state, () => {
if (state === 'disabled' || state === 'enabled' ||
state === 'enabling') {
this.updateStatePreference().then(() => {
this.notifyStateChange();
resolve();
});
return;
}
this.notifyStateChange();
});
});
},
get state() {
var state = this.store.get(SYNC_STATE);
return state ? state : 'disabled';
},
set error(error) {
this.store.set(SYNC_STATE_ERROR, error);
asyncStorage.setItem(SYNC_STATE_ERROR, error);
},
get error() {
return this.store.get(SYNC_STATE_ERROR);
},
set lastSync(now) {
var lastSync = now;
this.store.set(SYNC_LAST_TIME, lastSync);
asyncStorage.setItem(SYNC_LAST_TIME, lastSync);
},
get lastSync() {
return this.store.get(SYNC_LAST_TIME);
},
set user(user) {
// We need to check if there's an user logged in during the enabled
// state handler, so we cannot wait for the asyncStorage write.
this.store.set(SYNC_USER, user);
asyncStorage.setItem(SYNC_USER, user, () => {}, () => {
// This should never happen, but if something goes wrong storing the
// user we delete the user from memory and disable Sync to avoid
// inconsistent states.
this.store.delete(SYNC_USER);
SyncStateMachine.disable();
});
},
get user() {
return this.store.get(SYNC_USER);
},
/** Event handlers **/
// We expose a very simple IAC based API to allow the Settings app to
// manage Sync.
//
// We expect IAC messages to contain a property named 'name' with any
// of the following values corresponding to the requests that we are
// able to handle:
// - enable
// - disable
// - sync
// - getInfo
//
// SyncStateMachine is responsible of checking if the request is valid in
// the current situation and progressing it accordingly by publishing the
// corresponding events.
oniacrequest(event) {
if (!event || !event.detail) {
console.error('Wrong IAC Sync event');
return;
}
var request = event.detail;
switch(request.name) {
case 'enable':
case 'disable':
case 'sync':
try {
SyncStateMachine[request.name]();
} catch(e) {
console.error(e);
}
break;
case 'getInfo':
if (!request.id) {
console.warn('IAC request ignored. Missing id');
return;
}
this.getAccount().then(() => {
this.managementPortMessage({
id: request.id,
state: this.state,
error: this.state == 'errored' ? this.error : undefined,
lastSync: this.lastSync,
user: this.user
});
});
break;
default:
console.warn('Unknown IAC Sync request', request.name);
break;
}
},
ondisabled() {
this.debug('ondisabled observed');
this.updateState();
this.user = null;
this.lastSync = null;
this.error = null;
this.cleanup();
},
ondisabling() {
this.debug('ondisabling observed');
this.updateState();
// On TV because the login on Sync is tied to a login on FxA, login out
// from Sync means login out from FxA.
// Keep logout *after* the cleanup call where we remove the FxA event
// listeners.
LazyLoader.load('js/fx_accounts_client.js', () => {
FxAccountsClient.logout();
// Go back to default settings, so new users won't inherit old users
// preferences, but we save user settings before that.
this.saveUserSettings().then(() => {
return this.restoreDefaultSettings();
}).then(SyncStateMachine.success);
});
},
onenabled(from) {
this.debug('onenabled observed');
this.updateState();
// If we got to this point with no user, something went wrong, so we
// need to disable Sync.
if (!this.user) {
this.debug('No user');
SyncStateMachine.disable();
return;
}
this.registerSyncRequest().then(() => {
this.debug('Sync request registered');
// After login in with a new account or rebooting the device,
// we trigger a sync request.
if (from === 'enabling') {
SyncStateMachine.sync();
}
}).catch(error => {
console.error('Could not register sync request', error);
SyncStateMachine.error(ERROR_REQUEST_SYNC_REGISTRATION);
});
},
onenabling() {
this.debug('onenabling observed');
this.updateState();
// Because we need to bind this to the event handler, we need to save
// a reference to it so we can properly remove it later.
this.fxaEventHandler = this.onfxa.bind(this);
window.addEventListener(FXA_EVENT, this.fxaEventHandler);
// We need to verify that we have everything that we need to start
// synchronizing data. This is: a valid FxA, the ability to obtain
// a valid assertion and encryption keys and the remote crypto/keys
// record.
var args = [];
this.updateStateDeferred.then(() =>{
return this.getAssertion();
}).then(assertion => {
args.push(assertion);
return this.getKeys();
}).then(keys => {
args.push(keys);
return this.getAccount();
}).then(() => {
// We request a sync without any collection. This should fetch the
// crypto/keys object. If it is available, we will successfully
// continue the enabling process. If it is not available, that probably
// means that this FxA has never been used before with Sync.
// In that case, until we are able to create new Sync user, we need
// to disable Sync and let the user know about this situation.
return this.trySync.apply(this, args);
}).then(() => {
// We save default settings so we can revert user changes to
// the default values when the user logs out from Sync. So new
// users don't inherit old users preferences.
return this.saveDefaultSettings();
}).then(() => {
// We also restore previously saved user settings if they are
// available.
return this.restoreUserSettings();
}).then(() => {
SyncStateMachine.success();
}).catch(error => {
// XXX Bug 1200284 - Normalize all Firefox Accounts error reporting.
error = error.message || error.name || error.error || error;
console.error('Could not enable sync', error);
/**
* XXX Until we have a way to create new Sync users, we won't progress
* the UNVERIFIED_ACCOUNT error. Instead, we consider this error
* as the INVALID_SYNC_USER one, so we can present a more sane UX
* to the user.
*
if (error == 'UNVERIFIED_ACCOUNT') {
this.getAccount().then(() => {
SyncStateMachine.error(ERROR_UNVERIFIED_ACCOUNT);
});
return;
}*/
error = SyncErrors[error] || error;
SyncStateMachine.error(error);
});
},
onerrored(from, to, error) {
this.debug(`onerrored observed ${error} while transitioning from ${from}
to ${to}`);
if (!error) {
return;
}
this.error = error;
// We don't update the state until we set the error.
this.updateState();
// If the error is recoverable and we are coming from the syncing
// state, we go back to the enabled state, otherwise, we disable Sync.
if (SyncRecoverableErrors.indexOf(error) > -1 && from === 'syncing') {
SyncStateMachine.enable();
return;
}
this.debug('Unrecoverable error');
SyncStateMachine.disable();
},
onsyncing() {
this.debug('onsyncsyncing observed');
this.updateState();
// In case that we are syncing on user demand, we unregister the
// sync request.
// It's harmless to unregister an expired request, so it's also ok
// to do it for a periodic sync.
this.unregisterSyncRequest();
if (!navigator.onLine) {
SyncStateMachine.success();
return;
}
var collections = {};
COLLECTIONS.forEach(name => {
if (this.settings.has('sync.collections.' + name + '.enabled')) {
collections[name] = {
readonly: this.settings.get('sync.collections.' + name +
'.readonly'),
limit: this.settings.get('sync.collections.' + name + '.limit')
};
}
});
if (!Object.keys(collections).length) {
SyncStateMachine.success();
return;
}
var args = [];
this.getAssertion().then(assertion => {
args.push(assertion);
return this.getKeys();
}).then(keys => {
args.push(keys);
args.push(collections);
this.doSync.apply(this, args);
}).catch(error => {
console.error('Sync error', error);
SyncStateMachine.error(ERROR_SYNC_REQUEST);
});
},
onfxa(event) {
// We don't care about FxA events if we are already disabling.
if (this.state === 'disabling') {
return;
}
if (!event || !event.detail) {
console.error('Wrong event');
return;
}
switch (event.detail.eventName) {
case 'onlogout':
// Logging out from Firefox Accounts should disable Sync.
SyncStateMachine.disable();
break;
case 'onverified':
// Once the user verified her account, we can continue with the
// enabling procedure.
if (this.state == 'errored' &&
this.error == ERROR_UNVERIFIED_ACCOUNT) {
this.getAccount().then(() => {
if (this.user) {
SyncStateMachine.enable();
}
});
}
break;
}
},
onappterminated(event) {
// If the Sync app is closed, we need to release the reference to
// its IAC port, so we can get a new connection on the next sync
// request. Unfortunately, the IAC API doesn't take care of
// notifying when a port is dead. So we need to do it manually.
if (!this._port) {
return;
}
this.isSyncApp(event.detail ? event.detail.origin : null).then(() => {
this._port = null;
});
},
onportclose() {
// The synchronizer app can be killed while it's handling a sync
// request. In that case, we need to record an error and notify
// the state machine about it. Otherwise we could end up on a
// permanent 'syncing' state.
if (this.state !== 'syncing') {
return;
}
this.debug('Synchronizer closed before completing the sync request');
SyncStateMachine.error(ERROR_SYNC_APP_KILLED);
},
/** Helpers **/
observeSettings: function() {
var setSettingObserver = setting => {
return new Promise(resolve => {
SettingsListener.observe(setting, false, enabled => {
this.debug('Observed ' + setting + ' changed to ' + enabled);
enabled ? this.settings.set(setting, enabled)
: this.settings.delete(setting);
resolve();
});
});
};
var promises = [];
SETTINGS.forEach(setting => {
promises.push(setSettingObserver(setting));
});
return Promise.all(promises);
},
saveSettings(prefix) {
var saveSetting = setting => {
return new Promise((resolve, reject) => {
asyncStorage.getItem(prefix + setting, value => {
if (value !== null) {
resolve();
return;
}
this.debug(`Saving setting ${prefix + setting}`);
var req = navigator.mozSettings.createLock().get(setting);
req.onerror = reject;
req.onsuccess = () => {
var _value = req.result[setting];
_value ? this.settings.set(setting, _value)
: this.settings.delete(setting);
asyncStorage.setItem(prefix + setting, _value, () => {
this.debug(`Saved default setting ${prefix + setting} as
${_value}`);
resolve();
});
};
});
});
};
var promises = [];
SETTINGS.forEach(setting => {
promises.push(saveSetting(setting));
});
return Promise.all(promises);
},
saveDefaultSettings() {
this.debug('Saving default settings');
return this.saveSettings('');
},
saveUserSettings() {
this.debug('Saving user settings');
return this.saveSettings(this.user + '.');
},
restoreSettings(prefix) {
var revertSetting = setting => {
return new Promise((resolve, reject) => {
asyncStorage.getItem(prefix + setting, value => {
if (value === null || value === undefined) {
resolve();
return;
}
this.debug(`Reverting ${prefix + setting} to ${value}`);
// We need to remove the setting from asyncStorage to allow
// default setting changes within OTA updates.
asyncStorage.removeItem(prefix + setting, () => {
var settingObject = {};
settingObject[setting] = value;
var req = navigator.mozSettings.createLock().set(settingObject);
req.onerror = error => {
console.error(error);
reject();
};
req.onsuccess = () => {
this.debug(`Reverted setting ${setting} to ${value}`);
resolve();
};
});
});
});
};
var promises = [];
SETTINGS.forEach(setting => {
promises.push(revertSetting(setting));
});
return Promise.all(promises);
},
restoreDefaultSettings() {
this.debug('Restoring default settings');
return this.restoreSettings('');
},
restoreUserSettings() {
this.debug('Restoring user settings');
return this.restoreSettings(this.user + '.');
},
initStore: function() {
var promises = [];
[SYNC_STATE,
SYNC_STATE_ERROR,
SYNC_LAST_TIME].forEach(key => {
var promise = new Promise(resolve => {
asyncStorage.getItem(key, value => {
this.debug(key + ': ' + value);
this.store.set(key, value);
resolve();
});
});
promises.push(promise);
});
SETTINGS.forEach(setting => {
var promise = new Promise(resolve => {
asyncStorage.removeItem(setting, resolve);
promises.push(promise);
});
});
return Promise.all(promises);
},
updateState: function(state) {
this.state = state || SyncStateMachine.state;
},
registerSyncRequest() {
return navigator.sync.registrations().then(registrations => {
var existingReg;
registrations.some(reg => {
if (reg.task === SYNC_TASK) {
return !!(existingReg = reg);
}
});
if (existingReg) {
this.debug('Sync request already exists');
return Promise.resolve();
}
this.debug('Registering sync request', location.href);
var minInterval = this.DEBUG ? 120 :
this.settings.get('sync.scheduler.interval');
var wifiOnly = this.settings.has('sync.scheduler.wifionly') ?
this.settings.get('sync.scheduler.wifionly') : false;
return navigator.sync.register(SYNC_TASK, {
minInterval: minInterval, // minInterval is in seconds.
oneShot: true,
wifiOnly: wifiOnly,
data: null,
wakeUpPage: location.href
});
});
},
unregisterSyncRequest() {
navigator.sync.unregister(SYNC_TASK).catch(e => {
// A failure unregistering the sync task might be expected
// depending on the state we are (i.e. enabling). In any case,
// we show a warning, so a bug can be identified or discarded.
console.warn('Could not unregister sync task', e,
SyncStateMachine.state);
});
},
getKeys() {
return new Promise((resolve, reject) => {
LazyLoader.load('js/fx_accounts_client.js', () => {
FxAccountsClient.getKeys(resolve, reject);
});
});
},
getAssertion() {
return new Promise((resolve, reject) => {
LazyLoader.load('js/fx_accounts_client.js', () => {
FxAccountsClient.getAssertion({
audience: this.settings.get('sync.fxa.audience')
}, resolve, reject);
});
});
},
getAccount() {
return new Promise((resolve, reject) => {
LazyLoader.load('js/fx_accounts_client.js', () => {
FxAccountsClient.getAccount(account => {
this.user = account ? account.email : null;
resolve(account);
}, reject);
});
});
},
managementPortMessage(message) {
var port = IACHandler.getPort(SYNC_MANAGEMENT_API_IAC_KEYWORD, this);
if (!port) {
this.debug('No management API consumers');
// We may not have any consumers yet. So in that case
// we simply bail out here.
return;
}
port.postMessage(message);
},
notifyStateChange() {
this.managementPortMessage({
name: 'onsyncchange',
state: this.state,
error: this.error,
lastSync: this.lastSync,
user: this.user
});
},
connect() {
if (this._port) {
return Promise.resolve(this._port);
}
return new Promise((resolve, reject) => {
navigator.mozApps.getSelf().onsuccess = event => {
var app = event.target.result;
app.connect(SYNC_REQUEST_IAC_KEYWORD, {
// For now we only allow one synchronizer app.
pageURLs: 'app://sync.gaiamobile.org'
}).then(ports => {
if (!ports || !ports.length) {
return reject();
}
this._port = ports[0];
this._port.onclose = this.onportclose.bind(this);
resolve(this._port);
}).catch(error => {
console.error(error);
reject(error);
});
};
});
},
iacRequest(request) {
return new Promise((resolve, reject) => {
this.connect().then(port => {
LazyLoader.load('/shared/js/uuid.js', () => {
var id = uuid();
request.id = id;
port.postMessage(request);
port.onmessage = event => {
var message = event.data;
if (!message || (message.id != id)) {
return;
}
resolve(message);
};
});
});
});
},
doSync(assertion, keys, collections) {
this.debug('Syncing with', JSON.stringify(collections));
this.iacRequest({
name: 'sync',
URL: this.settings.get('sync.server.url'),
assertion: assertion,
keys: keys,
collections: collections
}).then(result => {
if (result && result.error) {
var error = result.error;
error = error.message ? error.message : error;
console.error('Error while trying to sync', error);
error = SyncErrors[error] || ERROR_SYNC_APP_GENERIC;
SyncStateMachine.error(error);
return;
}
this.debug('Sync succeded');
this.lastSync = Date.now();
SyncStateMachine.success();
});
},
trySync: function(assertion, keys) {
return this.iacRequest({
name: 'sync',
URL: this.settings.get('sync.server.url'),
assertion: assertion,
keys: keys,
collections: {}
}).then(result => {
if (result && result.error) {
var error = result.error;
error = error.message ? error.message : error;
console.error('Error trying sync', error);
throw error;
}
});
},
cancelSync() {
this.iacRequest({
name: 'cancel'
});
},
isSyncApp: function(origin) {
if (!origin) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
navigator.mozApps.getSelf().onsuccess = event => {
var app = event.target.result;
app.getConnections().then(connections => {
connections.forEach(connection => {
if (connection.keyword != SYNC_REQUEST_IAC_KEYWORD ||
connection.subscriber.indexOf(origin) < 0) {
return;
}
resolve();
});
reject();
});
};
});
},
cleanup() {
this.unregisterSyncRequest();
window.removeEventListener(FXA_EVENT, this.fxaEventHandler);
// If we disabled Sync while a sync request was in progress, we need to
// cancel the request.
if (this.state === 'syncing') {
this.cancelSync();
}
},
updateStatePreference: function() {
// Changing the value of this setting changes the value of
// the 'services.sync.enabled' preference as well.
// We don't retrieve the Sync keys unless this pref is set to true.
const CHROME_EVENT = 'mozPrefChromeEvent';
return new Promise((resolve, reject) => {
var onprefchange = event => {
this.debug('Preference change ', event.detail);
if (event.detail.prefName !== 'services.sync.enabled') {
return;
}
window.removeEventListener(CHROME_EVENT, onprefchange);
resolve();
};
window.addEventListener(CHROME_EVENT, onprefchange);
navigator.mozSettings.createLock().set({
'services.sync.enabled': this.state !== 'disabled'
}).onerror = reject;
});
},
debug() {
if (this.DEBUG) {
console.log('[SyncManager] ' + Array.slice(arguments).concat());
}
}
};
var syncManager = new SyncManager();
syncManager.start();
// Exported for testing purposes only.
exports.SyncManager = SyncManager;
exports.SyncManagerSettings = SETTINGS;
}(window));