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:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
926
gaia/apps/system/js/sync_manager.js
Executable file
926
gaia/apps/system/js/sync_manager.js
Executable file
|
|
@ -0,0 +1,926 @@
|
|||
/* 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 BaseModule */
|
||||
/* global ERROR_INVALID_SYNC_ACCOUNT */
|
||||
/* global ERROR_NO_KEY_FETCH_TOKEN */
|
||||
/* 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 Service */
|
||||
/* global SyncErrors */
|
||||
/* global SyncRecoverableErrors */
|
||||
/* global Telemetry */
|
||||
/* 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'];
|
||||
|
||||
// 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';
|
||||
|
||||
var SyncManager = function() {};
|
||||
|
||||
SyncManager.STATES = [];
|
||||
|
||||
SyncManager.SERVICES = [];
|
||||
|
||||
SyncManager.SETTINGS = [
|
||||
/** READ ONLY SETTINGS **/
|
||||
|
||||
// The user can choose the collections she wants to sync. These settings
|
||||
// indicates the user choice per collection.
|
||||
'sync.collections.history.enabled',
|
||||
'sync.collections.passwords.enabled',
|
||||
'sync.collections.bookmarks.enabled',
|
||||
|
||||
// Setting any of these two settings to true will make the synchronization
|
||||
// of the collection readonly. That means that we will only be retrieving
|
||||
// data from Firefox Sync and we won't be pushing any of the local
|
||||
// modifications to the collections source.
|
||||
'sync.collections.history.readonly',
|
||||
'sync.collections.passwords.readonly',
|
||||
'sync.collections.bookmarks.readonly',
|
||||
|
||||
// Limit the number of records fetched from the server in a single request.
|
||||
// Once the sync engine supports pagination, the limit will be the page
|
||||
// size.
|
||||
'sync.collections.history.limit',
|
||||
'sync.collections.passwords.limit',
|
||||
'sync.collections.bookmarks.limit',
|
||||
|
||||
'sync.server.url',
|
||||
'sync.scheduler.interval',
|
||||
'sync.scheduler.wifionly',
|
||||
|
||||
'sync.fxa.audience'
|
||||
];
|
||||
|
||||
SyncManager.EVENTS = [
|
||||
/** IAC based sync management API **/
|
||||
'iac-' + SYNC_MANAGEMENT_API_IAC_KEYWORD,
|
||||
|
||||
/** Sync State Machine events **/
|
||||
'onsyncdisabled',
|
||||
'onsyncdisabling',
|
||||
'onsyncenabled',
|
||||
'onsyncenabling',
|
||||
'onsyncerrored',
|
||||
'onsyncsyncing',
|
||||
|
||||
/** Sync app lifecycle **/
|
||||
'appterminated'
|
||||
];
|
||||
|
||||
SyncManager.SUB_MODULES = [
|
||||
'SyncStateMachine'
|
||||
];
|
||||
|
||||
BaseModule.create(SyncManager, {
|
||||
name: 'SyncManager',
|
||||
DEBUG: false,
|
||||
EVENT_PREFIX: 'sync',
|
||||
store: new Map(),
|
||||
|
||||
/** BaseModule life cycle **/
|
||||
|
||||
_start: function() {
|
||||
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;
|
||||
}
|
||||
Service.request('SyncStateMachine:sync');
|
||||
});
|
||||
|
||||
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':
|
||||
Service.request('SyncStateMachine:enable').then(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.
|
||||
Service.request('SyncStateMachine:enable').then(resolve);
|
||||
break;
|
||||
}
|
||||
this.updateState('disabled');
|
||||
resolve();
|
||||
break;
|
||||
case 'disabled':
|
||||
resolve();
|
||||
break;
|
||||
default:
|
||||
console.error('Unexpected sync.state value', this.state);
|
||||
reject();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_stop: function() {},
|
||||
|
||||
/** 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.
|
||||
'_handle_iac-gaia-sync-management': function(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 {
|
||||
Service.request('SyncStateMachine:' + request.name).then(() => {
|
||||
return LazyLoader.load('/shared/js/sync/telemetry.js');
|
||||
}).then(() => {
|
||||
Telemetry.logUserAction(request.name);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
} 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.error,
|
||||
lastSync: this.lastSync,
|
||||
user: this.user
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown IAC Sync request', request.name);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/** SyncStateMachine event handlers **/
|
||||
|
||||
_handle_onsyncdisabled: function() {
|
||||
this.debug('onsyncdisabled observed');
|
||||
this.updateState();
|
||||
this.user = null;
|
||||
this.lastSync = null;
|
||||
this.error = null;
|
||||
this.cleanup();
|
||||
},
|
||||
|
||||
_handle_onsyncdisabling: function() {
|
||||
this.debug('onsyncdisabling observed');
|
||||
this.updateState();
|
||||
|
||||
// Go back to default settings, so new users won't inherit old users'
|
||||
// preferences.
|
||||
this.saveUserSettings().then(() => {
|
||||
return this.restoreDefaultSettings();
|
||||
}).then(() => {
|
||||
Service.request('SyncStateMachine:success');
|
||||
});
|
||||
},
|
||||
|
||||
_handle_onsyncenabled: function(event) {
|
||||
this.debug('onsyncenabled 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');
|
||||
Service.request('SyncStateMachine:disable');
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerSyncRequest().then(() => {
|
||||
this.debug('Sync request registered');
|
||||
var from = event.detail && event.detail.from;
|
||||
// After login in with a new account or rebooting the device,
|
||||
// we trigger a sync request.
|
||||
if (from === 'enabling') {
|
||||
Service.request('SyncStateMachine:sync');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Could not register sync request', error);
|
||||
Service.request('SyncStateMachine:error',
|
||||
ERROR_REQUEST_SYNC_REGISTRATION);
|
||||
});
|
||||
},
|
||||
|
||||
_handle_onsyncenabling: function() {
|
||||
this.debug('onsyncenabling 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._handle_fxaEvent.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(() => {
|
||||
return this.restoreUserSettings();
|
||||
}).then(() => {
|
||||
Service.request('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);
|
||||
|
||||
if (error == 'UNVERIFIED_ACCOUNT') {
|
||||
this.getAccount().then(() => {
|
||||
Service.request('SyncStateMachine:error', ERROR_UNVERIFIED_ACCOUNT);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
error = SyncErrors[error] || error;
|
||||
|
||||
Service.request('SyncStateMachine:error', error);
|
||||
});
|
||||
},
|
||||
|
||||
_handle_onsyncerrored: function(event) {
|
||||
this.debug('onsyncerrored observed');
|
||||
|
||||
var error = event.detail && Array.isArray(event.detail.args) ?
|
||||
event.detail.args[0] : null;
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = error;
|
||||
|
||||
// We don't update the state until we set the error.
|
||||
this.updateState();
|
||||
|
||||
var from = event.detail && event.detail.from;
|
||||
|
||||
// There are some cases where we want to show to the user
|
||||
// the enabled state but warn her about the invalid state of her
|
||||
// account. So we stay in the errored state.
|
||||
if ([ERROR_UNVERIFIED_ACCOUNT].indexOf(error) > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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') ||
|
||||
// XXX ERROR_INVALID_SYNC_ACCOUNT is an unrecoverable error, but we
|
||||
// temporarily treat it in a different way depending on the device.
|
||||
// On TVs, we show a dialog asking the user to go to Desktop or
|
||||
// Android to create an account and we move to the disabled state.
|
||||
// On phones, we move to the enabled state, but we show a warning to
|
||||
// the user about her account being empty.
|
||||
error == ERROR_INVALID_SYNC_ACCOUNT) {
|
||||
Service.request('SyncStateMachine:enable');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current FxA login does not contain the token to fetch sync keys
|
||||
// we need to log the user out. Unfortunately the FxA auth server does not
|
||||
// allow to obtain this token outside of the initial sign in flow.
|
||||
if (error == ERROR_NO_KEY_FETCH_TOKEN) {
|
||||
// No need to disable Sync here as we will be doing it as soon as the
|
||||
// logout is done.
|
||||
FxAccountsClient.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug('Unrecoverable error');
|
||||
Service.request('SyncStateMachine:disable');
|
||||
},
|
||||
|
||||
_handle_onsyncsyncing: function() {
|
||||
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) {
|
||||
Service.request('SyncStateMachine:success');
|
||||
return;
|
||||
}
|
||||
|
||||
var collections = {};
|
||||
COLLECTIONS.forEach(name => {
|
||||
if (this._settings['sync.collections.' + name + '.enabled']) {
|
||||
collections[name] = {
|
||||
readonly: this._settings['sync.collections.' + name + '.readonly'],
|
||||
limit: this._settings['sync.collections.' + name + '.limit']
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!Object.keys(collections).length) {
|
||||
Service.request('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);
|
||||
Service.request('SyncStateMachine:error', ERROR_SYNC_REQUEST);
|
||||
});
|
||||
},
|
||||
|
||||
_handle_fxaEvent: function(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.
|
||||
Service.request('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) {
|
||||
Service.request('SyncStateMachine:enable');
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_handle_appterminated(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');
|
||||
Service.request('SyncStateMachine:error', ERROR_SYNC_APP_KILLED);
|
||||
},
|
||||
|
||||
/** Helpers **/
|
||||
|
||||
initStore: function() {
|
||||
var promises = [];
|
||||
[SYNC_STATE,
|
||||
SYNC_STATE_ERROR,
|
||||
SYNC_LAST_TIME,
|
||||
SYNC_USER].forEach(key => {
|
||||
var promise = new Promise(resolve => {
|
||||
asyncStorage.getItem(key, value => {
|
||||
this.debug(key + ': ' + value);
|
||||
this.store.set(key, value);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
promises.push(promise);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
updateState: function(state) {
|
||||
this.state = state || Service.query('SyncStateMachine.state');
|
||||
},
|
||||
|
||||
registerSyncRequest: function() {
|
||||
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);
|
||||
|
||||
// XXX We need to default to the settings value (3600 and false)
|
||||
// because of
|
||||
// Bug 1206004 - BaseModule._settings is not be available during
|
||||
// BaseModule._start
|
||||
var minInterval = this.DEBUG ? 120
|
||||
: this._settings['sync.scheduler.interval'] || 3600;
|
||||
var wifiOnly = this._settings['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: function() {
|
||||
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,
|
||||
Service.query('SyncStateMachine.state'));
|
||||
});
|
||||
},
|
||||
|
||||
getKeys: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
LazyLoader.load('js/fx_accounts_client.js', () => {
|
||||
FxAccountsClient.getKeys(resolve, reject);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getAssertion: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
LazyLoader.load('js/fx_accounts_client.js', () => {
|
||||
FxAccountsClient.getAssertion({
|
||||
audience: this._settings['sync.fxa.audience']
|
||||
}, resolve, reject);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getAccount: function() {
|
||||
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: function(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: function() {
|
||||
this.managementPortMessage({
|
||||
name: 'onsyncchange',
|
||||
state: this.state,
|
||||
error: this.error,
|
||||
lastSync: this.lastSync,
|
||||
user: this.user
|
||||
});
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
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: function(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: function(assertion, keys, collections) {
|
||||
this.debug('Syncing with', JSON.stringify(collections));
|
||||
this.iacRequest({
|
||||
name: 'sync',
|
||||
URL: this._settings['sync.server.url'],
|
||||
assertion: assertion,
|
||||
keys: keys,
|
||||
collections: collections
|
||||
}).then(result => {
|
||||
if (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;
|
||||
|
||||
Service.request('SyncStateMachine:error', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we made a successfull synchronization we are good and so
|
||||
// we don't care about past errors anymore.
|
||||
this.error = null;
|
||||
|
||||
this.debug('Sync succeded');
|
||||
this.lastSync = Date.now();
|
||||
|
||||
Service.request('SyncStateMachine:success');
|
||||
});
|
||||
},
|
||||
|
||||
trySync: function(assertion, keys) {
|
||||
return this.iacRequest({
|
||||
name: 'sync',
|
||||
URL: this._settings['sync.server.url'],
|
||||
assertion: assertion,
|
||||
keys: keys,
|
||||
collections: {}
|
||||
}).then(result => {
|
||||
if (result && result.error) {
|
||||
console.error('Error trying sync', result.error.message);
|
||||
throw result.error.message;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
cancelSync: function() {
|
||||
this.iacRequest({
|
||||
name: 'cancel'
|
||||
});
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
saveSettings(prefix) {
|
||||
var saveSetting = setting => {
|
||||
return new Promise(resolve => {
|
||||
asyncStorage.getItem(prefix + setting, value => {
|
||||
if (value !== null) {
|
||||
this.debug('Setting value already saved', setting);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.debug('Saving setting', prefix + setting,
|
||||
this._settings[setting]);
|
||||
asyncStorage.setItem(prefix + setting, this._settings[setting],
|
||||
resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var promises = [];
|
||||
SyncManager.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 => {
|
||||
this.debug('Restoring setting ' + prefix + setting, value);
|
||||
if (value === null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
// 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 = resolve;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var promises = [];
|
||||
SyncManager.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 + '.');
|
||||
}
|
||||
}, {
|
||||
state: {
|
||||
set: function(state) {
|
||||
state = state || Service.query('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: function() {
|
||||
var state = this.store.get(SYNC_STATE);
|
||||
return state ? state : 'disabled';
|
||||
}
|
||||
},
|
||||
|
||||
error: {
|
||||
set: function(error) {
|
||||
this.store.set(SYNC_STATE_ERROR, error);
|
||||
asyncStorage.setItem(SYNC_STATE_ERROR, error);
|
||||
},
|
||||
get: function() {
|
||||
return this.store.get(SYNC_STATE_ERROR);
|
||||
}
|
||||
},
|
||||
|
||||
lastSync: {
|
||||
set: function(now) {
|
||||
var lastSync = now;
|
||||
this.store.set(SYNC_LAST_TIME, lastSync);
|
||||
asyncStorage.setItem(SYNC_LAST_TIME, lastSync);
|
||||
},
|
||||
get: function() {
|
||||
return this.store.get(SYNC_LAST_TIME);
|
||||
}
|
||||
},
|
||||
|
||||
user: {
|
||||
set: function(user) {
|
||||
// We need to check if there's a 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);
|
||||
Service.request('SyncStateMachine:disable');
|
||||
});
|
||||
},
|
||||
get: function() {
|
||||
return this.store.get(SYNC_USER);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Exported only for testing purposes.
|
||||
exports.SyncManager = SyncManager;
|
||||
}(window));
|
||||
Loading…
Reference in a new issue