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
688
gaia/apps/settings/js/modules/addon_manager.js
Executable file
688
gaia/apps/settings/js/modules/addon_manager.js
Executable file
|
|
@ -0,0 +1,688 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var mozApps = require('modules/navigator/mozApps');
|
||||
var App = require('modules/app');
|
||||
var AppsCache = require('modules/apps_cache');
|
||||
var ObservableArray = require('modules/mvvm/observable_array');
|
||||
var MatchPattern = require('shared/addons/match_pattern');
|
||||
|
||||
/**
|
||||
* AddonManager is a singleton with methods for querying the currently
|
||||
* installed addons, enabling, disabling, and uninstalling addons, and
|
||||
* for obtaining a list of apps targeted by a specified add-on. The changes to
|
||||
* installed addons are propagated via an observable array.
|
||||
*
|
||||
* AddonManager uses the AppsCache module for efficient access to the full
|
||||
* list of installed apps.
|
||||
*
|
||||
* @class AddonManager
|
||||
* @requires module:modules/navigator/mozApps
|
||||
* @requires module:modules/apps_cache
|
||||
* @requires module:modules/mvvm/observable_array
|
||||
* @returns {AddonManager}
|
||||
*/
|
||||
function AddonManager() {
|
||||
this._addons = ObservableArray([]);
|
||||
this._bindEvents();
|
||||
|
||||
AppsCache.apps().then((apps) => {
|
||||
apps.some((app) => {
|
||||
if (this._isAddon(app)) {
|
||||
this._addons.push(App(app));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
AddonManager.prototype = {
|
||||
/**
|
||||
* Bind to install and uninstall events.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
*/
|
||||
_bindEvents: function() {
|
||||
var _boundUpdateAddons = this._updateAddons.bind(this);
|
||||
// Bug 1215546 & 1218298
|
||||
// The 'oninstall' event is fired when user started installing the
|
||||
// app, but manifest is not available yet. In order to get the
|
||||
// manifest, we need to listen to 'downloadsuccess' on the app
|
||||
// (not mozApps.mgmt), and that's when manifest will be ready.
|
||||
AppsCache.addEventListener('oninstall', (evt) => {
|
||||
var app = evt && evt.application;
|
||||
// Bug 1220195
|
||||
// If installed via WebIDE, _isAddon will return true, and we should
|
||||
// run _boundUpdateAddons immediately because 'downloadsuccess' would
|
||||
// never fire. If installed from Marketplace we need to wait for the
|
||||
// 'downloadsuccess' event and run _boundUpdateAddons later.
|
||||
if (this._isAddon(app)) {
|
||||
_boundUpdateAddons(evt);
|
||||
} else {
|
||||
app.addEventListener('downloadsuccess', _boundUpdateAddons);
|
||||
}
|
||||
});
|
||||
AppsCache.addEventListener('onuninstall', _boundUpdateAddons);
|
||||
mozApps.mgmt.addEventListener('enabledstatechange', _boundUpdateAddons);
|
||||
},
|
||||
|
||||
/**
|
||||
* We have to update the addon array based on incoming evt.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {Object} evt
|
||||
*/
|
||||
_updateAddons: function(evt) {
|
||||
var app = evt && evt.application;
|
||||
var type = evt.type;
|
||||
var index;
|
||||
|
||||
if (this._isAddon(app)) {
|
||||
if ((type === 'downloadsuccess' || type === 'install') &&
|
||||
!this._alreadyExists(app)) {
|
||||
this._addons.push(App(app));
|
||||
} else if (type === 'uninstall') {
|
||||
index = this._findAddonIndex(app);
|
||||
if (index !== -1) {
|
||||
this._addons.splice(index, 1);
|
||||
}
|
||||
} else if (type === 'enabledstatechange') {
|
||||
index = this._findAddonIndex(app);
|
||||
if (index !== -1) {
|
||||
this._addons.get(index)._enabled = app.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an addon already exists.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} addon
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_alreadyExists: function(addon) {
|
||||
return this._findAddonIndex(addon) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the index of an addon.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} addon
|
||||
* @returns {Number}
|
||||
*/
|
||||
_findAddonIndex: function(addon) {
|
||||
return this._addons.array.findIndex((elem) => {
|
||||
return addon.manifestURL === elem.instance.manifestURL;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Addons can only apply to apps with the same or lower privilege. Returns
|
||||
* a bloolean indicating whether the addon can be applied on the app.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} addon
|
||||
* @param {DOMApplication} app
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_privilegeCheck: function(addon, app) {
|
||||
var addonManifest = this._getManifest(addon);
|
||||
var appManifest = this._getManifest(app);
|
||||
return addonManifest.type === appManifest.type ||
|
||||
addonManifest.type === 'certified' ||
|
||||
addonManifest.type === 'privileged';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the manifest.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} app
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getManifest: function(app) {
|
||||
return app.manifest || app.updateManifest;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether reboot is required when disabling the addon.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} app
|
||||
* @param {Array} contentScripts
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_needsReboot: function({app, contentScripts}) {
|
||||
var manifest = this._getManifest(app);
|
||||
var role = manifest.role;
|
||||
// If the addon affects a system app
|
||||
if (role === 'system' || role === 'homescreen') {
|
||||
// and the app is customized with a script, then we need reboot
|
||||
return contentScripts.some(c => c.js.length > 0);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether restart is required when disabling the addon.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} app
|
||||
* @param {Array} contentScripts
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_needsRestart: function({app, contentScripts}) {
|
||||
return contentScripts.some(c => c.js.length > 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether this app is an addon.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} app
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isAddon: function(app) {
|
||||
var manifest = this._getManifest(app);
|
||||
return manifest && manifest.role === 'addon';
|
||||
},
|
||||
|
||||
/**
|
||||
* This internal utility function returns an array of objects describing
|
||||
* the apps that are affected by the given addon.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} addon
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_getAffectedApps: function(addon) {
|
||||
if (!this._isAddon(addon)) {
|
||||
return Promise.reject('not an addon');
|
||||
}
|
||||
|
||||
var manifest = this._getManifest(addon);
|
||||
var customizations = manifest && manifest.customizations;
|
||||
// content_scripts field is internally copied from manifest.json to
|
||||
// manifest.webapp
|
||||
var contentScripts = manifest && manifest.content_scripts;
|
||||
|
||||
// If the addon specifies customizations, then it is obsolete and does not
|
||||
// work
|
||||
if (customizations) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// If there are no content scripts or content scripts list is empty, the
|
||||
// addon does not affect any apps.
|
||||
if (!contentScripts || contentScripts.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return AppsCache.apps()
|
||||
.then(apps => apps.filter(app => {
|
||||
var manifest = this._getManifest(app);
|
||||
// Ignore apps that are themselves add-ons or if the addon doesn't
|
||||
// have high enough privileges to affect this app then we can just
|
||||
// return now.
|
||||
return manifest.role !== 'addon' &&
|
||||
this._privilegeCheck(addon, app);
|
||||
}))
|
||||
// Match apps based on add-on's content sctipts.
|
||||
.then(apps => {
|
||||
var matches = [];
|
||||
apps.forEach(app => {
|
||||
var matched;
|
||||
if ((matched = this._matchContentScripts(contentScripts, app))) {
|
||||
matches.push({ app: app, contentScripts: matched });
|
||||
}
|
||||
});
|
||||
return matches;
|
||||
})
|
||||
.catch(() => []);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of content scripts (from an addon manifest), if available,
|
||||
* that affect the app by a given addon. We implement getAddonTargets()
|
||||
* using this information , derive the 'reboot' and 'restart' return
|
||||
* values of disableAddon() from it and also use this function to apply
|
||||
* addon filters.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} addon
|
||||
* @param {DOMApplication} app
|
||||
* @returns {Array?} appliedContentScripts if available
|
||||
*/
|
||||
_matchContentScripts: function(contentScripts, app) {
|
||||
var manifest = this._getManifest(app);
|
||||
// Get the URL of the app origin plus its launch path to
|
||||
// compare against each of the filters.
|
||||
// XXX: Note that we and do not check the paths used by
|
||||
// activity handlers or any other paths in the manifest.
|
||||
var launchPath = manifest.launch_path || '';
|
||||
var launchURL = new URL(launchPath, app.origin);
|
||||
|
||||
// For each content script, compile the pattern string MatchPattern
|
||||
var patterns = contentScripts.map(contentScript => {
|
||||
return {
|
||||
matches: new MatchPattern(contentScript.matches),
|
||||
excludeMatches: new MatchPattern(contentScript.exclude_matches)
|
||||
};
|
||||
});
|
||||
|
||||
var appliedContentScripts = [];
|
||||
|
||||
// Now loop through the patterns to see what content scripts are applied
|
||||
// to this app
|
||||
for(var i = 0; i < patterns.length; i++) {
|
||||
var pattern = patterns[i];
|
||||
if (!pattern.excludeMatches.matches(launchURL) &&
|
||||
pattern.matches.matches(launchURL)) {
|
||||
appliedContentScripts.push(contentScripts[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If any content scripts were applied to this app, return the list of
|
||||
// appliedContentScripts
|
||||
if (appliedContentScripts.length > 0) {
|
||||
return appliedContentScripts;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert exported add-on into an Array Buffer.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {Blob} blob
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_blobToArrayBuffer: function(blob) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = function() {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.onerror = function(reason) {
|
||||
reject(reason);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Upack an add-on script.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {ArrayBuffer} arrayBuffer
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_unpackScript: function(arrayBuffer) {
|
||||
return this._JSZip.then(JSZip => {
|
||||
var zip = new JSZip();
|
||||
zip.load(arrayBuffer);
|
||||
|
||||
var applicationZipFile = zip.file('application.zip');
|
||||
if (!applicationZipFile) { return; }
|
||||
|
||||
var applicationZip = new JSZip();
|
||||
applicationZip.load(applicationZipFile.asArrayBuffer());
|
||||
|
||||
var scriptFile = applicationZip.file('main.js');
|
||||
if (!scriptFile) { return; }
|
||||
|
||||
return(scriptFile.asText());
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a new add-on.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {JSON} manifest add-on manifest
|
||||
* @param {String} script add-on script
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_generate: function(manifest, script) {
|
||||
return this._JSZip.then(JSZip => {
|
||||
var id = 'addon' + Math.round(Math.random() * 100000000);
|
||||
var applicationZip = new JSZip();
|
||||
|
||||
// Ensure that we are creating an addon with a new id.
|
||||
manifest.role = 'addon';
|
||||
manifest.type = 'certified';
|
||||
manifest.origin = 'app://' + id + '.gaiamobile.org';
|
||||
manifest.activities = manifest.activities || {};
|
||||
|
||||
applicationZip.file('manifest.webapp', JSON.stringify(manifest));
|
||||
applicationZip.file('main.js', script);
|
||||
|
||||
var packageZip = new JSZip();
|
||||
packageZip.file('metadata.json', JSON.stringify({
|
||||
installOrigin: 'http://gaiamobile.org',
|
||||
manifestURL: 'app://' + id + '.gaiamobile.org/update.webapp',
|
||||
version: 1
|
||||
}));
|
||||
packageZip.file('update.webapp', JSON.stringify({
|
||||
name: manifest.name,
|
||||
package_path: '/application.zip'
|
||||
}));
|
||||
packageZip.file('application.zip',
|
||||
applicationZip.generate({ type: 'arraybuffer' }));
|
||||
|
||||
return new Blob([packageZip.generate({ type: 'arraybuffer' })],
|
||||
{ type: 'application/zip' });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Lazily require jszip. It is currently used only for renaming Add-ons.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get _JSZip() {
|
||||
return new Promise(resolve => { require(['vendor/jszip'], resolve); });
|
||||
},
|
||||
|
||||
/**
|
||||
* Install add-on, but importing it using a memory-backed blob.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {Blob} blob add-on blob
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_install: function(blob) {
|
||||
return navigator.mozApps.mgmt.import(blob).then(addon => {
|
||||
// Enable the addon by default.
|
||||
var app = { instance: addon };
|
||||
this.enableAddon(app);
|
||||
return app;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether an addon is enabled.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isEnabled: function(addon) {
|
||||
return this._isAddon(addon.instance) && addon.instance.enabled === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether an addon can be deleted.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
canDelete: function(addon) {
|
||||
return this._isAddon(addon.instance) && addon.instance.removable === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable an addon
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
*/
|
||||
enableAddon: function(addon) {
|
||||
if (this._isAddon(addon.instance) && !addon.instance.enabled) {
|
||||
mozApps.mgmt.setEnabled(addon.instance, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable an addon. Returns a string indicating whether reboot or restart
|
||||
* required for disabling the addon completely.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
*/
|
||||
disableAddon: function(addon) {
|
||||
if (this._isAddon(addon.instance) && addon.instance.enabled) {
|
||||
mozApps.mgmt.setEnabled(addon.instance, false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an addon.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @returns {Promise}
|
||||
*/
|
||||
deleteAddon: function(addon) {
|
||||
if (!this._isAddon(addon.instance)) {
|
||||
return Promise.reject('not an addon');
|
||||
}
|
||||
|
||||
if (!addon.instance.removable) {
|
||||
return Promise.reject('addon is not deletable');
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = mozApps.mgmt.uninstall(addon.instance);
|
||||
request.onsuccess = function() {
|
||||
resolve();
|
||||
};
|
||||
request.onerror = function() {
|
||||
reject();
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename an addon.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @param {String} name addon name
|
||||
* @returns {Promise}
|
||||
*/
|
||||
renameAddon: function(addon, name) {
|
||||
if (!name) {
|
||||
return Promise.reject('no name given');
|
||||
}
|
||||
|
||||
if (!this._isAddon(addon.instance)) {
|
||||
return Promise.reject('not an addon');
|
||||
}
|
||||
|
||||
var manifest = this._getManifest(addon.instance);
|
||||
if (name === manifest.name) {
|
||||
return Promise.reject('name is unchanged');
|
||||
}
|
||||
|
||||
var newManifest, addonBlob;
|
||||
// To rename an addon we are creating a new DOMApplication that will have
|
||||
// an updated name. Other manifest attributes are preserved.
|
||||
// Export an add-on as a blob.
|
||||
return addon.instance.export().then(blob => {
|
||||
addonBlob = blob;
|
||||
// Copy over manifest object.
|
||||
newManifest = Object.assign({}, manifest);
|
||||
newManifest.name = name;
|
||||
// Delete original add-on.
|
||||
return this.deleteAddon(addon);
|
||||
}).then(() => {
|
||||
// Convert exported add-on into an Array Buffer.
|
||||
return this._blobToArrayBuffer(addonBlob);
|
||||
}).then(this._unpackScript.bind(this)).then(script => {
|
||||
// Generate a new add-on using the original manifest and a new name.
|
||||
return this._generate(newManifest, script);
|
||||
}).then(this._install.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Share an addon.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @returns {Promise}
|
||||
*/
|
||||
shareAddon: function(addon) {
|
||||
if (!this._isAddon(addon.instance)) {
|
||||
return Promise.reject('not an addon');
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var activity = new window.MozActivity({
|
||||
name: 'share',
|
||||
data: {
|
||||
type: 'app',
|
||||
app: addon.instance.manifestURL
|
||||
}
|
||||
});
|
||||
|
||||
activity.onsuccess = function() {
|
||||
resolve();
|
||||
};
|
||||
activity.onerror = function() {
|
||||
reject((activity.error && activity.error.name) || 'activity error');
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return an array of the apps that are targeted by any of this
|
||||
* addon's content scripts. Note, however, that addons can also target
|
||||
* arbitrary web pages and hosted apps. This test only applies to
|
||||
* installed packaged apps and so may be of limited utility.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAddonTargets: function(addon) {
|
||||
return this._getAffectedApps(addon.instance).then((affectedApps) => {
|
||||
return affectedApps.map((x) => x.app);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a string indicating whether reboot or restart is required for
|
||||
* disabling the addon.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {App} addon
|
||||
* @returns {Promise.<String>}
|
||||
*/
|
||||
getAddonDisableType: function(addon) {
|
||||
return this._getAffectedApps(addon.instance).then((affectedApps) => {
|
||||
var needsReboot = affectedApps.some(this._needsReboot.bind(this));
|
||||
if (needsReboot) {
|
||||
return 'reboot';
|
||||
}
|
||||
|
||||
var needsRestart = affectedApps.some(this._needsRestart.bind(this));
|
||||
if (needsRestart) {
|
||||
return 'restart';
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the App object that matches the given manifest url.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {String} manifestURL
|
||||
* @returns {Promise.<App>}
|
||||
*/
|
||||
findAddonByManifestURL: function(manifestURL) {
|
||||
return AppsCache.apps().then(() => {
|
||||
return this._addons.array.find((addon) => {
|
||||
return addon.instance.manifestURL === manifestURL;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a promise for a boolean flag indicating whether an addon affects
|
||||
* an application or not..
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AddonManager.prototype
|
||||
* @param {DOMApplication} addon addon to check
|
||||
* @param {String} manifestURL app manifest URL to check
|
||||
* @returns {Promise.<Boolean>}
|
||||
*/
|
||||
addonAffectsApp: function(addon, manifestURL) {
|
||||
if (!manifestURL) {
|
||||
return Promise.reject('No manifestURL given for an app');
|
||||
}
|
||||
if (!this._isAddon(addon.instance)) {
|
||||
return Promise.reject('not an addon');
|
||||
}
|
||||
|
||||
var manifest = this._getManifest(addon.instance);
|
||||
var customizations = manifest && manifest.customizations;
|
||||
// content_scripts field is internally copied from manifest.json to
|
||||
// manifest.webapp
|
||||
var contentScripts = manifest && manifest.content_scripts;
|
||||
|
||||
// If the addon specifies customizations, then it is obsolete and does not
|
||||
// work
|
||||
if (customizations) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// If there are no content scripts or content scripts list is empty, the
|
||||
// addon does not affect any apps.
|
||||
if (!contentScripts || contentScripts.length === 0) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return AppsCache.apps()
|
||||
.then(apps => apps.find(app => app.manifestURL === manifestURL))
|
||||
.then(app => app ?
|
||||
this._matchContentScripts(contentScripts, app) : undefined)
|
||||
.then(appliedContentScripts => !!appliedContentScripts)
|
||||
.catch(() => false);
|
||||
},
|
||||
|
||||
get length() {
|
||||
return this._addons.length;
|
||||
},
|
||||
|
||||
get addons() {
|
||||
return this._addons;
|
||||
}
|
||||
};
|
||||
|
||||
return new AddonManager();
|
||||
});
|
||||
52
gaia/apps/settings/js/modules/apn/apn_const.js
Executable file
52
gaia/apps/settings/js/modules/apn/apn_const.js
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* The apn constants
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var APN_TYPES = ['default', 'mms', 'supl', 'dun', 'ims', 'fota'];
|
||||
|
||||
var CP_APN_KEY = 'ril.data.cp.apns';
|
||||
var DEFAULT_APN_KEY = 'ril.data.default.apns';
|
||||
|
||||
// Used for detecting if a new icc card has been inserted.
|
||||
var CACHED_ICCIDS_KEY = 'apn.cached.iccids';
|
||||
|
||||
var APN_LIST_KEY = 'apn.list';
|
||||
var APN_SELECTIONS_KEY = 'apn.selections';
|
||||
var APN_SETTINGS_KEY = 'ril.data.apnSettings';
|
||||
var DEFAULT_APN_SETTINGS_KEY = 'ril.data.default.apnSettings';
|
||||
|
||||
var MCC_SETTINGS_KEY = 'operatorvariant.mcc';
|
||||
var MNC_SETTINGS_KEY = 'operatorvariant.mnc';
|
||||
|
||||
var EU_ROAMING_ENABLED_KEY = 'eu-roaming.enabled';
|
||||
var EU_ROAMING_FILE_PATH = '/resources/eu-roaming.json';
|
||||
|
||||
var APN_PROPS = [
|
||||
'carrier', 'apn', 'user', 'passwd', 'httpproxyhost', 'httpproxyport',
|
||||
'mmsc', 'mmsproxy', 'mmsport', 'authtype', 'types', 'protocol',
|
||||
'roaming_protocol', 'mtu',
|
||||
/**
|
||||
* _id is an internal property for identifying apn items received via
|
||||
* client provisioning.
|
||||
*/
|
||||
'_id'
|
||||
];
|
||||
|
||||
return {
|
||||
get APN_TYPES() { return APN_TYPES; },
|
||||
get CP_APN_KEY() { return CP_APN_KEY; },
|
||||
get DEFAULT_APN_KEY() { return DEFAULT_APN_KEY; },
|
||||
get CACHED_ICCIDS_KEY() { return CACHED_ICCIDS_KEY; },
|
||||
get APN_LIST_KEY() { return APN_LIST_KEY; },
|
||||
get APN_SELECTIONS_KEY() { return APN_SELECTIONS_KEY; },
|
||||
get APN_SETTINGS_KEY() { return APN_SETTINGS_KEY; },
|
||||
get DEFAULT_APN_SETTINGS_KEY() { return DEFAULT_APN_SETTINGS_KEY; },
|
||||
get MCC_SETTINGS_KEY() { return MCC_SETTINGS_KEY; },
|
||||
get MNC_SETTINGS_KEY() { return MNC_SETTINGS_KEY; },
|
||||
get EU_ROAMING_ENABLED_KEY() { return EU_ROAMING_ENABLED_KEY; },
|
||||
get EU_ROAMING_FILE_PATH() { return EU_ROAMING_FILE_PATH; },
|
||||
get APN_PROPS() { return APN_PROPS; }
|
||||
};
|
||||
});
|
||||
46
gaia/apps/settings/js/modules/apn/apn_item.js
Executable file
46
gaia/apps/settings/js/modules/apn/apn_item.js
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* ApnItem is a wrapper of apn objects.
|
||||
*
|
||||
* @module modules/apn/apn_item
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var APN_CATEGORY = {
|
||||
PRESET: 'preset',
|
||||
CUSTOM: 'custom',
|
||||
EU: 'eu'
|
||||
};
|
||||
|
||||
/**
|
||||
* @class ApnItem
|
||||
* @params {String} id
|
||||
* @params {String} category
|
||||
* @params {Object} apn
|
||||
* @returns {ApnItem}
|
||||
*/
|
||||
function ApnItem(id, category, apn) {
|
||||
this._id = id;
|
||||
this._category = category;
|
||||
this._apn = apn;
|
||||
}
|
||||
|
||||
ApnItem.prototype = {
|
||||
get id() {
|
||||
return this._id;
|
||||
},
|
||||
get category() {
|
||||
return this._category;
|
||||
},
|
||||
get apn() {
|
||||
return this._apn;
|
||||
}
|
||||
};
|
||||
|
||||
var constructor = function ctor_apn_item(id, category, apn) {
|
||||
return new ApnItem(id, category, apn);
|
||||
};
|
||||
constructor.APN_CATEGORY = APN_CATEGORY;
|
||||
|
||||
return constructor;
|
||||
});
|
||||
266
gaia/apps/settings/js/modules/apn/apn_list.js
Executable file
266
gaia/apps/settings/js/modules/apn/apn_list.js
Executable file
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* ApnList stores ApnItem objects representing apns come from the apn.json
|
||||
* database, client provisioning messages, and user's creation. Each ApnItem has
|
||||
* an id assigned upon the creation. The id is used to recored users' apn
|
||||
* selection.
|
||||
*
|
||||
* @module modules/apn/apn_list
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var AsyncStorage = require('modules/async_storage');
|
||||
var ApnUtils = require('modules/apn/apn_utils');
|
||||
var ApnItem = require('modules/apn/apn_item');
|
||||
|
||||
/**
|
||||
* @class ApnList
|
||||
* @requires module:modules/async_storage
|
||||
* @requires module:modules/apn/apn_utils
|
||||
* @requires module:modules/apn/apn_item
|
||||
* @returns {ApnList}
|
||||
*/
|
||||
function ApnList(key) {
|
||||
this._key = key;
|
||||
this._apnItems = null;
|
||||
this._pendingTaskCount = 0;
|
||||
this._promiseChain = Promise.resolve();
|
||||
}
|
||||
|
||||
ApnList.prototype = {
|
||||
/**
|
||||
* As the operations should not be performed concurrently. We use this
|
||||
* function to enusre the operations are performed one by one.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @param {Function} task
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_schedule: function al__schedule(task) {
|
||||
var that = this;
|
||||
this._promiseChain = this._promiseChain.then(function() {
|
||||
that._pendingTaskCount++;
|
||||
return task().then(function(result) {
|
||||
that._pendingTaskCount--;
|
||||
if (that._pendingTaskCount === 0) {
|
||||
return that._commit().then(function() {
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
return this._promiseChain;
|
||||
},
|
||||
|
||||
/**
|
||||
* _apnItems are wrapped apn items. The function unwraps the items so
|
||||
* that they can be stored to async storage.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @returns {Array}
|
||||
*/
|
||||
_export: function al__export() {
|
||||
return this._apnItems.map(function(apnItem) {
|
||||
// Convert the apn items to static objects for storing.
|
||||
var apnClone = ApnUtils.clone(apnItem.apn);
|
||||
apnClone.id = apnItem.id;
|
||||
apnClone.category = apnItem.category;
|
||||
return apnClone;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the current copy of apn items to async storage.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_commit: function al__commit() {
|
||||
if (!this._apnItems) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
var apns = this._export();
|
||||
return AsyncStorage.setItem(this._key, apns);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The internal add function.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @params {Object} apn
|
||||
* @params {ApnItem.APN_CATEGORY} category
|
||||
* @returns {Promise.<String>} The apn id
|
||||
*/
|
||||
_add: function al__add(apn, category) {
|
||||
return this.items().then(function(apnItems) {
|
||||
if (!apnItems) {
|
||||
// set default apn items
|
||||
this._apnItems = apnItems = [];
|
||||
}
|
||||
|
||||
// Set carrier to _custom_ if the category is custom for backward
|
||||
// compatibility
|
||||
category = category || ApnItem.APN_CATEGORY.CUSTOM;
|
||||
if (category === ApnItem.APN_CATEGORY.CUSTOM) {
|
||||
apn.carrier = '_custom_';
|
||||
}
|
||||
var apnItem = ApnItem(ApnUtils.generateId(), category, apn);
|
||||
|
||||
apnItems.unshift(apnItem);
|
||||
return apnItem.id;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* The internal remove function.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @params {String} id
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_remove: function al__remove(id) {
|
||||
return this.items().then(function(apnItems) {
|
||||
if (!apnItems) {
|
||||
return Promise.reject('no apn items');
|
||||
}
|
||||
|
||||
var index = apnItems.findIndex((apnItem) => apnItem.id === id);
|
||||
if (index >= 0) {
|
||||
apnItems = apnItems.splice(index, 1);
|
||||
} else {
|
||||
return Promise.reject('apn not found');
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* The internal update function.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @params {String} id
|
||||
* @params {Object} apn
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_update: function al__update(id, apn) {
|
||||
return this.items().then(function(apnItems) {
|
||||
if (!apnItems) {
|
||||
return Promise.reject('no apn items');
|
||||
}
|
||||
|
||||
var index = apnItems.findIndex((apnItem) => apnItem.id === id);
|
||||
if (index >= 0) {
|
||||
var currentApn = apnItems[index].apn;
|
||||
for (var p in apn) {
|
||||
// id and category are not allowed to be changed
|
||||
if (p === 'id' || p === 'category') {
|
||||
continue;
|
||||
}
|
||||
currentApn[p] = apn[p];
|
||||
}
|
||||
} else {
|
||||
return Promise.reject('apn not found');
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all apn items of the list.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnList.prototype
|
||||
* @returns {Promise.<Array.<ApnItem>>}
|
||||
*/
|
||||
items: function al_items() {
|
||||
// Because add/remove/update depend on this function so we should not
|
||||
// schedule it or we will get a dead lock. We return the current copy of
|
||||
// apn items.
|
||||
if (this._apnItems) {
|
||||
return Promise.resolve(this._apnItems);
|
||||
} else {
|
||||
return AsyncStorage.getItem(this._key).then(function(apns) {
|
||||
if (apns) {
|
||||
this._apnItems = apns.map(function(apn) {
|
||||
return ApnItem(apn.id, apn.category, apn);
|
||||
});
|
||||
return this._apnItems;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the apn item with a specified id.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnList.prototype
|
||||
* @returns {Promise.<ApnItem>}
|
||||
*/
|
||||
item: function al_item(id) {
|
||||
// Return the apn item based on the current copy if apn items.
|
||||
return this.items().then(function(apnItems) {
|
||||
if (!apnItems || !id) {
|
||||
return null;
|
||||
} else {
|
||||
return apnItems.find((apnItem) => apnItem.id === id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an apn to the list with specified category. Returns the id of the
|
||||
* newly added apn.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnList.prototype
|
||||
* @params {Object} apn
|
||||
* @params {ApnItem.APN_CATEGORY} category
|
||||
* @returns {Promise.<String>} The apn id
|
||||
*/
|
||||
add: function al_apns(apn, category) {
|
||||
var apnClone = ApnUtils.clone(apn);
|
||||
return this._schedule(this._add.bind(this, apnClone, category));
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an apn from the list.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnList.prototype
|
||||
* @params {String} id
|
||||
* @returns {Promise}
|
||||
*/
|
||||
remove: function al_remove(id) {
|
||||
return this._schedule(this._remove.bind(this, id));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an apn. All properties expect for "id" and "category" will be
|
||||
* overwritten based on the passed apn object.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnList.prototype
|
||||
* @params {String} id
|
||||
* @params {Object} apn
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update: function al_update(id, apn) {
|
||||
var apnClone = ApnUtils.clone(apn);
|
||||
return this._schedule(this._update.bind(this, id, apnClone));
|
||||
}
|
||||
};
|
||||
|
||||
return function ctor_apn_list(key) {
|
||||
return new ApnList(key);
|
||||
};
|
||||
});
|
||||
192
gaia/apps/settings/js/modules/apn/apn_selections.js
Executable file
192
gaia/apps/settings/js/modules/apn/apn_selections.js
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* ApnSelections stores the id of the apn being used on each apn type. The
|
||||
* selections are provided in terms of Observable. Changes to the selection
|
||||
* can be observed and then be saved to the settings database.
|
||||
* Implementation details please refer to {@link ApnSelections}.
|
||||
*
|
||||
* @module modules/apn/apn_selections
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var ApnConst = require('modules/apn/apn_const');
|
||||
|
||||
var ICC_COUNT = navigator.mozMobileConnections.length;
|
||||
var APN_TYPES = ApnConst.APN_TYPES;
|
||||
var APN_SELECTIONS_KEY = ApnConst.APN_SELECTIONS_KEY;
|
||||
|
||||
/**
|
||||
* @class ApnSelections
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @requires module:modules/settings_cache
|
||||
* @requires module:modules/apn/apn_const
|
||||
* @returns {ApnSelections}
|
||||
*/
|
||||
function ApnSelections() {
|
||||
this._readyPromise = null;
|
||||
this._apnSelections = [];
|
||||
}
|
||||
|
||||
ApnSelections.prototype = {
|
||||
/**
|
||||
* Converts the apn selections to static objects for storing.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSelections.prototype
|
||||
* @returns {Promise.<Array>}
|
||||
*/
|
||||
_export: function as__export() {
|
||||
return this._apnSelections.map(function(apnSelection) {
|
||||
var obj = {};
|
||||
APN_TYPES.forEach(function(apnType) {
|
||||
obj[apnType] = apnSelection[apnType];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the current selection to the settings database.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSelections.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_commit: function as__commit() {
|
||||
return new Promise(function(resolve) {
|
||||
var obj = {};
|
||||
obj[APN_SELECTIONS_KEY] = this._export();
|
||||
var req = navigator.mozSettings.createLock().set(obj);
|
||||
req.onsuccess = function() { resolve(); };
|
||||
req.onerror = function() {
|
||||
console.error('write apn selections failed');
|
||||
resolve();
|
||||
};
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers observers so that we can save the selection when it changes.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSelections.prototype
|
||||
*/
|
||||
_observeApnSelections: function as__observeApnSelections(apnSelection) {
|
||||
APN_TYPES.forEach(function(apnType) {
|
||||
apnSelection.observe(apnType, this._commit.bind(this));
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates empty selections.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSelections.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_createEmptySelections: function as__createEmptySelections() {
|
||||
var emptySelections = [];
|
||||
var emptySelection = {};
|
||||
APN_TYPES.forEach((apnType) => emptySelection[apnType] = null);
|
||||
|
||||
for (var i = 0; i < ICC_COUNT; i++) {
|
||||
emptySelections.push(emptySelection);
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var obj = {};
|
||||
obj[APN_SELECTIONS_KEY] = emptySelections;
|
||||
var req = navigator.mozSettings.createLock().set(obj);
|
||||
req.onsuccess = req.onerror =
|
||||
function() { resolve(emptySelections); };
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the selections based on the values stored in the settings
|
||||
* database.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSelections.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_ready: function as__ready() {
|
||||
// This ensures that the ready process being executed only once.
|
||||
if (!this._readyPromise) {
|
||||
var that = this;
|
||||
this._readyPromise = new Promise(function(resolve) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
resolve(results[APN_SELECTIONS_KEY]);
|
||||
});
|
||||
}).then(function(apnSelections) {
|
||||
if (apnSelections) {
|
||||
return apnSelections;
|
||||
} else {
|
||||
return that._createEmptySelections();
|
||||
}
|
||||
}).then(function(apnSelections) {
|
||||
// Turn the selections to observables
|
||||
apnSelections.forEach(function(selection, index) {
|
||||
var observableApnSelection = Observable(selection);
|
||||
that._observeApnSelections(observableApnSelection);
|
||||
that._apnSelections[index] = observableApnSelection;
|
||||
});
|
||||
|
||||
// Clear the entire apn selection states when the selections are
|
||||
// cleared by other apps (usually the wap push app).
|
||||
navigator.mozSettings.addObserver(APN_SELECTIONS_KEY,
|
||||
function(event) {
|
||||
if (event.settingValue === null) {
|
||||
that._readyPromise = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return this._readyPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the apn selection of a sim slot. The selection object is an
|
||||
* Observable in which the apn types and apn ids are stored as key-value
|
||||
* pairs.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSelections.prototype
|
||||
* @params {Number} serviceId
|
||||
* @returns {Promise.<Observable>}
|
||||
*/
|
||||
get: function as_get(serviceId) {
|
||||
return this._ready().then(function() {
|
||||
return this._apnSelections[serviceId];
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the apn selection to null.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSelections.prototype
|
||||
* @params {Number} serviceId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
clear: function as_clear(serviceId) {
|
||||
var apnSelection = this._apnSelections[serviceId];
|
||||
if (!apnSelection) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this._ready().then(function() {
|
||||
APN_TYPES.forEach(function(apnType) {
|
||||
apnSelection[apnType] = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return function() {
|
||||
return new ApnSelections();
|
||||
};
|
||||
});
|
||||
245
gaia/apps/settings/js/modules/apn/apn_settings.js
Executable file
245
gaia/apps/settings/js/modules/apn/apn_settings.js
Executable file
|
|
@ -0,0 +1,245 @@
|
|||
/**
|
||||
* ApnSettings provides functions for manipulating the apn settings in the
|
||||
* settings database.
|
||||
* Implementation details please refer to {@link ApnSettings}.
|
||||
*
|
||||
* @module modules/apn/apn_settings
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var ApnUtils = require('modules/apn/apn_utils');
|
||||
var ApnConst = require('modules/apn/apn_const');
|
||||
|
||||
var APN_SETTINGS_KEY = ApnConst.APN_SETTINGS_KEY;
|
||||
var DEFAULT_APN_SETTINGS_KEY = ApnConst.DEFAULT_APN_SETTINGS_KEY;
|
||||
|
||||
/**
|
||||
* @class ApnSettings
|
||||
* @returns {ApnSettings}
|
||||
*/
|
||||
function ApnSettings() {
|
||||
this._isReady = false;
|
||||
this._readyPromise = null;
|
||||
this._isWritingDB = false;
|
||||
this._isDirty = false;
|
||||
this._apnSettings = null;
|
||||
this._promiseChain = Promise.resolve();
|
||||
}
|
||||
|
||||
ApnSettings.prototype = {
|
||||
/**
|
||||
* As the operations should not be performed concurrently. We use this
|
||||
* function to enusre the operations are performed one by one.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @param {Function} task
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_schedule: function al__schedule(task) {
|
||||
var that = this;
|
||||
this._promiseChain = this._promiseChain.then(function() {
|
||||
return task().then(function() {
|
||||
return that._commit();
|
||||
});
|
||||
});
|
||||
return this._promiseChain;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the current copy of apn settings to the settings database.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_commit: function al__commit() {
|
||||
if (!this._isDirty) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var that = this;
|
||||
return new Promise(function(resolve) {
|
||||
that._isWritingDB = true;
|
||||
var obj = {};
|
||||
obj[APN_SETTINGS_KEY] = that._apnSettings;
|
||||
var req = navigator.mozSettings.createLock().set(obj);
|
||||
req.onsuccess = req.onerror = function() {
|
||||
that._isDirty = false;
|
||||
that._isWritingDB = false;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers an observer on setting changes because ril.data.apnSettings
|
||||
* could be changed by other apps (system and wap).
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettings.prototype
|
||||
*/
|
||||
_addObservers: function as__registerListeners() {
|
||||
var mozSettings = window.navigator.mozSettings;
|
||||
if (!mozSettings) {
|
||||
return;
|
||||
}
|
||||
mozSettings.addObserver(APN_SETTINGS_KEY, function(event) {
|
||||
if (!this._isWritingDB) {
|
||||
// Do not reflect the change during the committing.
|
||||
this._apnSettings = event.settingValue || [];
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the settings based on the values stored in the settings
|
||||
* database.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_ready: function as__ready() {
|
||||
if (this._isReady) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
// This ensures that the ready process being executed only once.
|
||||
if (!this._readyPromise) {
|
||||
var that = this;
|
||||
this._readyPromise = new Promise(function(resolve) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
that._isReady = true;
|
||||
that._apnSettings = results[APN_SETTINGS_KEY] || [];
|
||||
that._addObservers();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return this._readyPromise;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The internal update function.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @params {Number} serviceId
|
||||
* @params {String} apnType
|
||||
* @params {Object} newApn
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_update: function as__update(serviceId, apnType, newApn) {
|
||||
return this._ready().then(function() {
|
||||
var apns = this._apnSettings[serviceId];
|
||||
if (!apns) {
|
||||
apns = this._apnSettings[serviceId] = [];
|
||||
}
|
||||
|
||||
var index = apns.findIndex(function(apn) {
|
||||
return apn.types.some((type) => apnType === type);
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
if (newApn) {
|
||||
this._isDirty = true;
|
||||
apns.push(newApn);
|
||||
}
|
||||
} else {
|
||||
if (newApn) {
|
||||
if (!ApnUtils.isMatchedApn(apns[index], newApn)) {
|
||||
this._isDirty = true;
|
||||
apns[index] = newApn;
|
||||
}
|
||||
} else {
|
||||
this._isDirty = true;
|
||||
apns.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all apns with of a sim card.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @params {Number} serviceId
|
||||
* @returns {Promise.<Array>}
|
||||
*/
|
||||
getAll: function as_getAll(serviceId) {
|
||||
return this._ready().then(function() {
|
||||
return this._apnSettings[serviceId];
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the apn of the specified apn type.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @params {Number} serviceId
|
||||
* @params {String} apnType
|
||||
* @returns {Promise.<Object>}
|
||||
*/
|
||||
get: function as_get(serviceId, apnType) {
|
||||
return this._ready().then(function() {
|
||||
var apns = this._apnSettings[serviceId];
|
||||
if (apns) {
|
||||
return apns.find((apn) => apn.types.indexOf(apnType) >= 0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an apn and the change will be saved into the settings database.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @params {Number} serviceId
|
||||
* @params {String} apnType
|
||||
* @params {Object} apn
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update: function as_update(serviceId, apnType, apn) {
|
||||
var apnClone = ApnUtils.clone(apn);
|
||||
return this._schedule(
|
||||
this._update.bind(this, serviceId, apnType, apnClone));
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the apn settings to the default value determined in
|
||||
* system/js/operator_variants.js.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettings.prototype
|
||||
* @params {String} serviceId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
restore: function as_restore(serviceId) {
|
||||
var that = this;
|
||||
return this._ready().then(function() {
|
||||
return new Promise(function(resolve) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
resolve(results[DEFAULT_APN_SETTINGS_KEY] || []);
|
||||
});
|
||||
});
|
||||
}).then(function(defaultApnSettings) {
|
||||
return that._schedule(function() {
|
||||
that._isDirty = true;
|
||||
that._apnSettings[serviceId] = defaultApnSettings[serviceId];
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return function ctor_apn_settings() {
|
||||
return new ApnSettings();
|
||||
};
|
||||
});
|
||||
482
gaia/apps/settings/js/modules/apn/apn_settings_manager.js
Executable file
482
gaia/apps/settings/js/modules/apn/apn_settings_manager.js
Executable file
|
|
@ -0,0 +1,482 @@
|
|||
/**
|
||||
* ApnSettingsManager provides functions for managing apn items. When an item
|
||||
* is selected to be used, ApnSettingsManager also helps convert the selection
|
||||
* to the real settings expected by the platform.
|
||||
* ApnSettingsManager does its task by coordinating the following objects:
|
||||
* - ApnList
|
||||
* There is an ApnList object for each sim. ApnList stores ApnItem objects
|
||||
* representing apns come from the apn.json database, client provisioning
|
||||
* messages, and user's creation. Each ApnItem has an id assigned upon
|
||||
* the creation. The id is used to recored users' apn selection.
|
||||
* - ApnSelections
|
||||
* ApnSelections stores the id of the apn being used on each apn
|
||||
* type.
|
||||
* - ApnSettings
|
||||
* ApnSettings wraps the apn settings stored in the moz settings database
|
||||
* and provides simple interface for manipulation.
|
||||
* Implementation details please refer to {@link ApnSettingsManager}.
|
||||
*
|
||||
* @module modules/apn/apn_settings_manager
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var AsyncStorage = require('modules/async_storage');
|
||||
var ApnConst = require('modules/apn/apn_const');
|
||||
var ApnUtils = require('modules/apn/apn_utils');
|
||||
var ApnItem = require('modules/apn/apn_item');
|
||||
var ApnSettings = require('modules/apn/apn_settings');
|
||||
var ApnList = require('modules/apn/apn_list');
|
||||
var ApnSelections = require('modules/apn/apn_selections');
|
||||
|
||||
var APN_TYPES = ApnConst.APN_TYPES;
|
||||
var APN_LIST_KEY = ApnConst.APN_LIST_KEY;
|
||||
var CACHED_ICCIDS_KEY = ApnConst.CACHED_ICCIDS_KEY;
|
||||
|
||||
var RESTORE_MODE = {
|
||||
ALL: 0,
|
||||
ONLY_APN_ITEMS: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* @class ApnSettingsManager
|
||||
* @requires module:modules/async_storage
|
||||
* @requires module:modules/apn/apn_const
|
||||
* @requires module:modules/apn/apn_utils
|
||||
* @requires module:modules/apn/apn_item
|
||||
* @requires module:modules/apn/apn_settings
|
||||
* @requires module:modules/apn/apn_list
|
||||
* @requires module:modules/apn/apn_selections
|
||||
* @returns {ApnSettingsManager}
|
||||
*/
|
||||
function ApnSettingsManager() {
|
||||
this._apnLists = {};
|
||||
this._apnSelections = ApnSelections();
|
||||
this._apnSettings = ApnSettings();
|
||||
|
||||
this._readyPromises = {};
|
||||
|
||||
Object.defineProperty(this, 'RESTORE_MODE', {
|
||||
configurable: false,
|
||||
get: function() {
|
||||
return RESTORE_MODE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ApnSettingsManager.prototype = {
|
||||
/**
|
||||
* Ensures the current apn items are up-to-date. When the current apn
|
||||
* id does not equal to the cached apn id, we should restore the apn items.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_ready: function asc_fromItems(serviceId) {
|
||||
if (!this._readyPromises[serviceId]) {
|
||||
// Check if a new icc card is detected.
|
||||
this._readyPromises[serviceId] = AsyncStorage.getItem(CACHED_ICCIDS_KEY)
|
||||
.then((cachedIccIds) => {
|
||||
var cachedIccId = cachedIccIds && cachedIccIds[serviceId];
|
||||
var curIccId = navigator.mozMobileConnections[serviceId].iccId;
|
||||
if (!curIccId || curIccId === cachedIccId) {
|
||||
return;
|
||||
}
|
||||
|
||||
cachedIccIds = cachedIccIds || {};
|
||||
cachedIccIds[serviceId] = curIccId;
|
||||
// Get the default preset apns by restoring.
|
||||
return Promise.all([
|
||||
// In this case we only restore apn items but not apn settings.
|
||||
this.restore(serviceId, RESTORE_MODE.ONLY_APN_ITEMS),
|
||||
AsyncStorage.setItem(CACHED_ICCIDS_KEY, cachedIccIds)
|
||||
]);
|
||||
});
|
||||
}
|
||||
return this._readyPromises[serviceId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the id of the first preset apn item.
|
||||
*
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
* @returns {Promise.<String>}
|
||||
*/
|
||||
_deriveActiveApnIdFromItems: function asc_fromItems(serviceId, apnType) {
|
||||
return this._apnItems(serviceId, apnType).then(function(apnItems) {
|
||||
if (apnItems.length) {
|
||||
return apnItems.sort(ApnUtils.sortByCategory)[0].id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the id of the apn item that matches the current apn setting of
|
||||
* the specified apn type.
|
||||
*
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
* @returns {Promise.<String>}
|
||||
*/
|
||||
_deriveActiveApnIdFromSettings:
|
||||
function asc_deriveFromSettings(serviceId, apnType) {
|
||||
return Promise.all([
|
||||
this._apnItems(serviceId, apnType),
|
||||
this._apnSettings.get(serviceId, apnType)
|
||||
]).then(function(results) {
|
||||
var apnItems = results[0];
|
||||
var apnInUse = results[1];
|
||||
// apnInUse is the apn that RIL is currently using.
|
||||
if (apnInUse) {
|
||||
var matchedApnItem = apnItems.find(function(apnItem) {
|
||||
return ApnUtils.isMatchedApn(apnItem.apn, apnInUse);
|
||||
});
|
||||
if (matchedApnItem) {
|
||||
// Has matched apn in the existing apns.
|
||||
return matchedApnItem.id;
|
||||
} else {
|
||||
var category = (apnInUse.carrier === '_custom_') ?
|
||||
ApnItem.APN_CATEGORY.CUSTOM : ApnItem.APN_CATEGORY.PRESET;
|
||||
return this.addApn(serviceId, apnInUse, category);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the apn type an apn that is actively being used for.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnId
|
||||
* id of the apn being checked.
|
||||
* @returns {Promise.<String>}
|
||||
*/
|
||||
_getApnAppliedType: function asc_getApnAppliedType(serviceId, apnId) {
|
||||
return this._apnSelections.get(serviceId).then(function(apnSelection) {
|
||||
return APN_TYPES.find((apnType) => apnSelection[apnType] === apnId);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the current apn selection to apn settings to the settings database.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
*/
|
||||
_storeApnSettingByType: function asc_storeApnByType(serviceId, apnType) {
|
||||
return this._apnSelections.get(serviceId).then(function(apnSelection) {
|
||||
var apnList = this._apnList(serviceId);
|
||||
var apnId = apnSelection[apnType];
|
||||
if (apnId) {
|
||||
// Get the apn item of apnType.
|
||||
return apnList.item(apnId);
|
||||
}
|
||||
}.bind(this)).then(function(apnItem) {
|
||||
if (apnItem) {
|
||||
return this._apnSettings.update(serviceId, apnType, apnItem.apn);
|
||||
} else {
|
||||
return this._apnSettings.update(serviceId, apnType, null);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the apn item list of a sim slot.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @returns {ApnList} The apn item list.
|
||||
*/
|
||||
_apnList: function asc_apnList(serviceId) {
|
||||
var apnList = this._apnLists[serviceId];
|
||||
if (!apnList) {
|
||||
this._apnLists[serviceId] = apnList = ApnList(APN_LIST_KEY + serviceId);
|
||||
}
|
||||
return apnList;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the apn items of an apn type for a sim slot.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
* @returns {Promise.<Array.<ApnItem>>} The apn items.
|
||||
*/
|
||||
_apnItems: function asc_apnItems(serviceId, apnType) {
|
||||
return this._ready(serviceId).then(() => {
|
||||
return this._apnList(serviceId).items();
|
||||
}).then((apnItems) => {
|
||||
return apnItems &&
|
||||
apnItems.filter(ApnUtils.apnTypeFilter.bind(null, apnType)) || [];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the apn items of a category.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {ApnList} apnList
|
||||
* @param {Array<Object>} apnsForRestoring
|
||||
* The function use this to restore the apn items.
|
||||
* @param {ApnItem.APN_CATEGORY} category
|
||||
* The category of the items to be resotred.
|
||||
* @returns {Promise.<Array.<String>>} The id of the restored apn items.
|
||||
*/
|
||||
_restoreApnItemsOfCategory:
|
||||
function asc_restoreApnItems(apnList, apnsForRestoring, category) {
|
||||
return apnList.items().then(function(apnItems) {
|
||||
// Remove all existing preset apns.
|
||||
apnItems = apnItems || [];
|
||||
var promises = [];
|
||||
apnItems.filter(function(apnItem) {
|
||||
if (apnItem.category === category) {
|
||||
return true;
|
||||
}
|
||||
}).forEach(function(apnItem) {
|
||||
promises.push(apnList.remove(apnItem.id));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(function() {
|
||||
// Add default preset apns.
|
||||
var promises = [];
|
||||
apnsForRestoring.forEach(function(apns) {
|
||||
promises.push(apnList.add(apns, category));
|
||||
});
|
||||
return Promise.all(promises);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the apn items and apn settings to the default. Only apn items of
|
||||
* the category ApnItem.APN_CATEGORY.PRESET and ApnItem.APN_CATEGORY.EU are
|
||||
* restored. User created apn items (custom apns) will not be affected. The
|
||||
* preset apn items are from the apn.json database and client provisioning
|
||||
* messages.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {String} serviceId
|
||||
* @param {Number} mode
|
||||
* The possible values are defined in RESTORE_MODE. We
|
||||
* restore both apn items and apn settings by default. Only
|
||||
* apn items are restored when mode is
|
||||
* RESTORE_MODE.ONLY_APN_ITEMS.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
restore: function asc_restore(serviceId, mode) {
|
||||
var mobileConnection = navigator.mozMobileConnections[serviceId];
|
||||
var networkType = mobileConnection.data.type;
|
||||
var apnList = this._apnList(serviceId);
|
||||
var that = this;
|
||||
|
||||
return Promise.all([
|
||||
ApnUtils.getOperatorCode(serviceId, 'mcc'),
|
||||
ApnUtils.getOperatorCode(serviceId, 'mnc')
|
||||
]).then(function(values) {
|
||||
// Get default apns and client provisioning apns matching the mcc/mnc
|
||||
// codes.
|
||||
var mcc = values[0];
|
||||
var mnc = values[1];
|
||||
|
||||
return Promise.all([
|
||||
ApnUtils.getEuApns(),
|
||||
ApnUtils.getDefaultApns(mcc, mnc, networkType),
|
||||
ApnUtils.getCpApns(mcc, mnc, networkType)
|
||||
]);
|
||||
}).then(function(results) {
|
||||
// Restore preset and eu apns.
|
||||
var euApns = ApnUtils.separateApnsByType(results[0]);
|
||||
var presetApns = ApnUtils.separateApnsByType(
|
||||
Array.prototype.concat.apply([], results.slice(1)));
|
||||
|
||||
return that._restoreApnItemsOfCategory(
|
||||
apnList, euApns, ApnItem.APN_CATEGORY.EU)
|
||||
.then(function() {
|
||||
return that._restoreApnItemsOfCategory(
|
||||
apnList, presetApns, ApnItem.APN_CATEGORY.PRESET);
|
||||
});
|
||||
}).then(function() {
|
||||
// We simply clear the apn selections. The selections will be restored
|
||||
// based on the current apn settings when it is being queried.
|
||||
return that._apnSelections.clear(serviceId);
|
||||
}).then(function() {
|
||||
if (mode !== RESTORE_MODE.ONLY_APN_ITEMS) {
|
||||
return that._apnSettings.restore(serviceId);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Query apn items matching the mcc/mnc codes in the apn.json
|
||||
* database and the one received through client provisioning messages.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
* @returns {Promise.<Array.<ApnItem>>} The apn items
|
||||
*/
|
||||
queryApns: function asc_queryApns(serviceId, apnType) {
|
||||
return this.getActiveApnId(serviceId, apnType)
|
||||
.then(function(activeApnId) {
|
||||
return this._apnItems(serviceId, apnType).then(function(items) {
|
||||
items.forEach(function(apnItem) {
|
||||
apnItem.active = (apnItem.id === activeApnId);
|
||||
});
|
||||
return items;
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an apn to a sim slot.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {Object} apn
|
||||
* @param {ApnItem.APN_CATEGORY} category
|
||||
* @returns {Promise}
|
||||
*/
|
||||
addApn: function asc_addApn(serviceId, apn, category) {
|
||||
return this._ready(serviceId).then(() => {
|
||||
return this._apnList(serviceId).add(apn,
|
||||
category || ApnItem.APN_CATEGORY.CUSTOM);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an apn from a sim slot.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} id
|
||||
* id of the apn item to be added.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removeApn: function asc_removeApn(serviceId, id) {
|
||||
return this._ready(serviceId).then(() => {
|
||||
return this._apnList(serviceId).remove(id);
|
||||
}).then(() => {
|
||||
// check if the removed apn is actively being used.
|
||||
return this._getApnAppliedType(serviceId, id);
|
||||
}).then((matchedApnType) => {
|
||||
if (matchedApnType) {
|
||||
return this._deriveActiveApnIdFromItems(serviceId, matchedApnType)
|
||||
.then((activeApnId) => {
|
||||
return this.setActiveApnId(serviceId, matchedApnType, activeApnId);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an apn item.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} id
|
||||
* id of the apn item to be updated.
|
||||
* @param {Object} apn
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateApn: function asc_updateApn(serviceId, id, apn) {
|
||||
return this._ready(serviceId).then(() => {
|
||||
return this._apnList(serviceId).update(id, apn);
|
||||
}).then(() => {
|
||||
// check if the updated apn is actively being used.
|
||||
return this._getApnAppliedType(serviceId, id);
|
||||
}).then((matchedApnType) => {
|
||||
if (matchedApnType) {
|
||||
return this._storeApnSettingByType(serviceId, matchedApnType);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the id of the apn that is actively being used for an apn type.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
* @returns {Promise.<String>}
|
||||
*/
|
||||
getActiveApnId: function asc_getActiveApnId(serviceId, apnType) {
|
||||
var that = this;
|
||||
return this._ready(serviceId).then(function() {
|
||||
return that._apnSelections.get(serviceId);
|
||||
}).then(function(apnSelection) {
|
||||
return apnSelection && apnSelection[apnType];
|
||||
}).then(function(activeApnId) {
|
||||
if (activeApnId) {
|
||||
return activeApnId;
|
||||
} else {
|
||||
// If there is no existing active apn id, try to derive the id from
|
||||
// the current apn settings.
|
||||
return that._deriveActiveApnIdFromSettings(serviceId, apnType)
|
||||
.then(function(apnId) {
|
||||
// Set the id as the active apn id.
|
||||
that.setActiveApnId(serviceId, apnType, apnId);
|
||||
return apnId;
|
||||
});
|
||||
}
|
||||
}).then(function(activeApnId) {
|
||||
// If there is still no active apn id, means that the apn settings have
|
||||
// not been set and we need to derive a default id from the current apn
|
||||
// items (stored in the apn list).
|
||||
if (activeApnId) {
|
||||
return activeApnId;
|
||||
} else {
|
||||
return that._deriveActiveApnIdFromItems(serviceId, apnType)
|
||||
.then(function(apnId) {
|
||||
// Set the id as the active apn id.
|
||||
that.setActiveApnId(serviceId, apnType, apnId);
|
||||
return apnId;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the id of an apn that is to be used for an apn type.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf ApnSettingsManager.prototype
|
||||
* @param {Number} serviceId
|
||||
* @param {String} apnType
|
||||
* @param {String} id
|
||||
* @returns {Promise}
|
||||
*/
|
||||
setActiveApnId: function asc_setActiveApnId(serviceId, apnType, id) {
|
||||
return this._ready(serviceId).then(() => {
|
||||
return this._apnSelections.get(serviceId);
|
||||
}).then((apnSelection) => {
|
||||
if (apnSelection[apnType] !== id) {
|
||||
apnSelection[apnType] = id;
|
||||
return this._storeApnSettingByType(serviceId, apnType);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return new ApnSettingsManager();
|
||||
});
|
||||
200
gaia/apps/settings/js/modules/apn/apn_utils.js
Executable file
200
gaia/apps/settings/js/modules/apn/apn_utils.js
Executable file
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* The apn utilities
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var ApnHelper = require('shared/apn_helper');
|
||||
var ApnConst = require('modules/apn/apn_const');
|
||||
var ApnItem = require('modules/apn/apn_item');
|
||||
var LazyLoader = require('shared/lazy_loader');
|
||||
|
||||
var CP_APN_KEY = ApnConst.CP_APN_KEY;
|
||||
var DEFAULT_APN_KEY = ApnConst.DEFAULT_APN_KEY;
|
||||
var MCC_SETTINGS_KEY = ApnConst.MCC_SETTINGS_KEY;
|
||||
var MNC_SETTINGS_KEY = ApnConst.MNC_SETTINGS_KEY;
|
||||
var APN_PROPS = ApnConst.APN_PROPS;
|
||||
var EU_ROAMING_FILE_PATH = ApnConst.EU_ROAMING_FILE_PATH;
|
||||
|
||||
function _getOperatorCode(serviceId, type) {
|
||||
var value;
|
||||
var key;
|
||||
|
||||
if (type === 'mcc') {
|
||||
value = '000';
|
||||
key = MCC_SETTINGS_KEY;
|
||||
} else if (type === 'mnc') {
|
||||
value = '00';
|
||||
key = MNC_SETTINGS_KEY;
|
||||
} else {
|
||||
return Promise.reject('invalid type');
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
var values = results[key];
|
||||
if (values && Array.isArray(values) && values[serviceId]) {
|
||||
value = values[serviceId];
|
||||
}
|
||||
resolve(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function. Filter APNs by apn type.
|
||||
*
|
||||
* @param {String} type
|
||||
* The apn type we would like to include.
|
||||
* @param {ApnItem} apnItem
|
||||
*/
|
||||
function _apnTypeFilter(type, apnItem) {
|
||||
if (!type || !apnItem || !apnItem.apn.types) {
|
||||
return false;
|
||||
} else if (type === '*') {
|
||||
return true;
|
||||
}
|
||||
return apnItem.apn.types.indexOf(type) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query <apn> elements matching the mcc/mnc arguments in the apn.json
|
||||
* database
|
||||
*
|
||||
* @param {String} mcc
|
||||
* @param {String} mnc
|
||||
* @param {String} networkType
|
||||
* The network type which the APN must be compatible with.
|
||||
*/
|
||||
function _getDefaultApns(mcc, mnc, networkType) {
|
||||
// XXX: should fallback to the JSON file if we don't get the apns
|
||||
return new Promise(function(resolve, reject) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
var apns = results[DEFAULT_APN_KEY] || {};
|
||||
resolve(ApnHelper.getCompatible(apns, mcc, mnc, networkType));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query <apn> elements matching the mcc/mnc arguments in the database
|
||||
* received through client provisioning messages.
|
||||
*
|
||||
* @param {String} mcc
|
||||
* @param {String} mnc
|
||||
* @param {String} networkType
|
||||
* The network type which the APN must be compatible with.
|
||||
*/
|
||||
function _getCpApns(mcc, mnc, networkType) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
var apns = results[CP_APN_KEY] || {};
|
||||
resolve(ApnHelper.getCompatible(apns, mcc, mnc, networkType));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var _euApnChecked = false;
|
||||
var _euApns = null;
|
||||
/**
|
||||
* Return the EU apns for roaming.
|
||||
*/
|
||||
function _getEuApns() {
|
||||
if (_euApnChecked) {
|
||||
return Promise.resolve(_euApns);
|
||||
} else {
|
||||
return LazyLoader.getJSON(EU_ROAMING_FILE_PATH).then(function(result) {
|
||||
_euApnChecked = true;
|
||||
// Only return eu apns when both home and foreign operators are
|
||||
// specified.
|
||||
if (result.home && result.foreign &&
|
||||
Object.keys(result.home).length > 0 &&
|
||||
Object.keys(result.foreign).length > 0) {
|
||||
_euApns = result.defaultApns;
|
||||
}
|
||||
return _euApns;
|
||||
}).catch(function() {
|
||||
_euApnChecked = true;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _generateId() {
|
||||
// should refine this
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
function _cloneApn(apn) {
|
||||
var newApn = {};
|
||||
for (var p in apn) {
|
||||
newApn[p] = apn[p];
|
||||
}
|
||||
return newApn;
|
||||
}
|
||||
|
||||
function _separateApnsByType(apns) {
|
||||
if (!apns) {
|
||||
return [];
|
||||
}
|
||||
return apns.reduce(function(result, apn) {
|
||||
// separate the apn by type
|
||||
apn.types.forEach(function(type) {
|
||||
var cloneApn = _cloneApn(apn);
|
||||
cloneApn.types = [type];
|
||||
result.push(cloneApn);
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function _isMatchedApn(apn1, apn2) {
|
||||
if (apn1 == null || apn2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the type of apn1 is the subset of apn2
|
||||
if (!apn1.types.every(function(type) {
|
||||
return (apn2.types.indexOf(type) !== -1);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return APN_PROPS.every(function(prop) {
|
||||
if (prop === 'types') {
|
||||
// we've already check this property
|
||||
return true;
|
||||
} else {
|
||||
return apn1[prop] === apn2[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _sortByCategory(apn1, apn2) {
|
||||
if (apn1.category === ApnItem.APN_CATEGORY.PRESET) {
|
||||
return true;
|
||||
} else if (apn2.category === ApnItem.APN_CATEGORY.PRESET) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function _clone(apn) {
|
||||
return JSON.parse(JSON.stringify(apn));
|
||||
}
|
||||
|
||||
return {
|
||||
getOperatorCode: _getOperatorCode,
|
||||
apnTypeFilter: _apnTypeFilter,
|
||||
getDefaultApns: _getDefaultApns,
|
||||
getCpApns: _getCpApns,
|
||||
getEuApns: _getEuApns,
|
||||
generateId: _generateId,
|
||||
separateApnsByType: _separateApnsByType,
|
||||
isMatchedApn: _isMatchedApn,
|
||||
sortByCategory: _sortByCategory,
|
||||
clone: _clone
|
||||
};
|
||||
});
|
||||
25
gaia/apps/settings/js/modules/app.js
Executable file
25
gaia/apps/settings/js/modules/app.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
var App = Module.create(function App(app) {
|
||||
this.super(Observable).call(this);
|
||||
this._app = app;
|
||||
this._enabled = app.enabled;
|
||||
}).extend(Observable);
|
||||
|
||||
Observable.defineObservableProperty(App.prototype, 'enabled', {
|
||||
readonly: true
|
||||
});
|
||||
|
||||
Object.defineProperty(App.prototype, 'instance', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return this._app;
|
||||
}
|
||||
});
|
||||
|
||||
return App;
|
||||
});
|
||||
92
gaia/apps/settings/js/modules/app_storage.js
Executable file
92
gaia/apps/settings/js/modules/app_storage.js
Executable file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* AppStorage is a singleton that caches app storage values for
|
||||
* app storage and root panel fast access
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
/**
|
||||
* @class AppStorage
|
||||
* @requires module:modules/base/module
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @returns {AppStorage}
|
||||
*/
|
||||
var AppStorage = Module.create(function AppStorage() {
|
||||
this.super(Observable).call(this);
|
||||
this._appStorage = navigator.getDeviceStorage('apps');
|
||||
|
||||
this._attachListeners();
|
||||
this._getSpaceInfo();
|
||||
}).extend(Observable);
|
||||
|
||||
Observable.defineObservableProperty(AppStorage.prototype,
|
||||
'usedPercentage', {
|
||||
readonly: true,
|
||||
value: 0
|
||||
});
|
||||
|
||||
Observable.defineObservableProperty(AppStorage.prototype,
|
||||
'totalSize', {
|
||||
readonly: true,
|
||||
value: 0
|
||||
});
|
||||
|
||||
Observable.defineObservableProperty(AppStorage.prototype,
|
||||
'usedSize', {
|
||||
readonly: true,
|
||||
value: 0
|
||||
});
|
||||
|
||||
Observable.defineObservableProperty(AppStorage.prototype,
|
||||
'freeSize', {
|
||||
readonly: true,
|
||||
value: 0
|
||||
});
|
||||
|
||||
|
||||
AppStorage.prototype.updateInfo = function as_updateInfo() {
|
||||
this._getSpaceInfo();
|
||||
};
|
||||
|
||||
AppStorage.prototype._attachListeners = function as_attachListeners() {
|
||||
this._appStorage.addEventListener('change', this);
|
||||
};
|
||||
|
||||
AppStorage.prototype.handleEvent = function as_handler(evt) {
|
||||
switch (evt.type) {
|
||||
case 'change':
|
||||
this._getSpaceInfo();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
AppStorage.prototype._getSpaceInfo = function as_getSpaceInfo() {
|
||||
var deviceStorage = this._appStorage;
|
||||
|
||||
if (!deviceStorage) {
|
||||
console.error('Cannot get DeviceStorage for: app');
|
||||
return;
|
||||
}
|
||||
deviceStorage.freeSpace().onsuccess = (e) => {
|
||||
this._freeSize = e.target.result;
|
||||
deviceStorage.usedSpace().onsuccess = (e) => {
|
||||
this._usedSize = e.target.result;
|
||||
// calculate the percentage to show a space usage bar
|
||||
this._totalSize =
|
||||
this._usedSize + this._freeSize;
|
||||
var usedPercentage = (this._totalSize === 0) ?
|
||||
0 : (this._usedSize * 100 / this._totalSize);
|
||||
if (usedPercentage > 100) {
|
||||
usedPercentage = 100;
|
||||
}
|
||||
this._usedPercentage = usedPercentage;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
return AppStorage();
|
||||
});
|
||||
|
||||
166
gaia/apps/settings/js/modules/apps_cache.js
Executable file
166
gaia/apps/settings/js/modules/apps_cache.js
Executable file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* AppsCache is a singleton that will encapsulate mozApps.mgmt inside and
|
||||
* expose neede API to callers. The reason why we need this is because
|
||||
* mozApps.mgmt.getAll() costs a lot and the better way to do so is cache
|
||||
* them inside this AppManager and automatically update it when install /
|
||||
* uninstall event is received.
|
||||
*
|
||||
* API:
|
||||
*
|
||||
* AppsCache.apps().then(function(apps) {
|
||||
* // installed apps in FxOS
|
||||
* });
|
||||
*
|
||||
* @module AppsCache
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var mozApps = window.navigator.mozApps;
|
||||
var mozAppsMgmt = mozApps && mozApps.mgmt;
|
||||
|
||||
var AppsCache = function() {
|
||||
this._initPromise = null;
|
||||
this._apps = [];
|
||||
this._eventHandlers = {};
|
||||
this._eventHandlers.oninstall = [];
|
||||
this._eventHandlers.onuninstall = [];
|
||||
};
|
||||
|
||||
AppsCache.prototype = {
|
||||
/**
|
||||
* apps is a method that would return a promise that would return
|
||||
* cached apps.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf AppsCache
|
||||
* @return {Promise}
|
||||
*/
|
||||
apps: function am_apps() {
|
||||
var self = this;
|
||||
return this._init().then(function() {
|
||||
return self._apps;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Entry point
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AppsCache
|
||||
* @return {Promise}
|
||||
*/
|
||||
_init: function am__init() {
|
||||
var self = this;
|
||||
if (!this._initPromise) {
|
||||
this._initPromise = this._initApps().then(function() {
|
||||
self._initEvents();
|
||||
});
|
||||
}
|
||||
return this._initPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Init needed events
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AppsCache
|
||||
*/
|
||||
_initEvents: function am__initEvents() {
|
||||
var self = this;
|
||||
|
||||
mozAppsMgmt.oninstall = function(evt) {
|
||||
var newApp = evt.application;
|
||||
var existing = false;
|
||||
self._apps.some((app, i) => {
|
||||
if (app.manifestURL === newApp.manifestURL) {
|
||||
self._apps[i] = newApp;
|
||||
existing = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
self._apps.push(newApp);
|
||||
}
|
||||
self._eventHandlers.oninstall.forEach(function(eventHandler) {
|
||||
eventHandler(evt);
|
||||
});
|
||||
};
|
||||
|
||||
mozAppsMgmt.onuninstall = function(evt) {
|
||||
var manifestURL = evt.application.manifestURL;
|
||||
var appRemoved = false;
|
||||
self._apps = self._apps.filter(app => {
|
||||
if (app.manifestURL === manifestURL) {
|
||||
appRemoved = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (appRemoved) {
|
||||
self._eventHandlers.onuninstall.forEach(function(eventHandler) {
|
||||
eventHandler(evt);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Init apps
|
||||
*
|
||||
* @access private
|
||||
* @memberOf AppsCache
|
||||
*/
|
||||
_initApps: function am__initApps() {
|
||||
var self = this;
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
var req = mozAppsMgmt.getAll();
|
||||
req.onsuccess = function success(evt) {
|
||||
self._apps = evt.target.result;
|
||||
resolve(self._apps);
|
||||
};
|
||||
req.onerror = function error(evt) {
|
||||
console.log('failed to get installed apps');
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Caller can add events when app is installed/uninstalled
|
||||
*
|
||||
* @access public
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
* @memberOf AppsCache
|
||||
*/
|
||||
addEventListener: function am_addEventListener(eventName, callback) {
|
||||
if (this._eventHandlers[eventName]) {
|
||||
this._eventHandlers[eventName].push(callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Caller can remove events when app is installed/uninstalled
|
||||
*
|
||||
* @access public
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
* @memberOf AppsCache
|
||||
*/
|
||||
removeEventListener: function am_removeEventListener(eventName, callback) {
|
||||
if (this._eventHandlers[eventName]) {
|
||||
var index = this._eventHandlers[eventName].indexOf(callback);
|
||||
if (index >= 0) {
|
||||
this._eventHandlers[eventName].splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var appsCache = new AppsCache();
|
||||
return appsCache;
|
||||
});
|
||||
21
gaia/apps/settings/js/modules/async_storage.js
Executable file
21
gaia/apps/settings/js/modules/async_storage.js
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* The promise version of async storage.
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var AsyncStorage = require('shared/async_storage');
|
||||
|
||||
return {
|
||||
getItem: function as_getItem(key) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
AsyncStorage.getItem(key, resolve);
|
||||
});
|
||||
},
|
||||
setItem: function as_setItem(key, value) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
AsyncStorage.setItem(key, value, resolve);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
137
gaia/apps/settings/js/modules/base/dependency_graph.js
Executable file
137
gaia/apps/settings/js/modules/base/dependency_graph.js
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* DependencyGraph is a basic data structure that helps decide the evaluation
|
||||
* order of a certain node.
|
||||
*
|
||||
* @module modules/base/dependency_graph
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
|
||||
/**
|
||||
* @class DependencyGraph
|
||||
* @requires module:modules/base/module
|
||||
* @params {DependencyGraph} dpGraph
|
||||
* The newly created graph will be initialized using
|
||||
* dpGraph.
|
||||
* @returns {DependencyGraph}
|
||||
*/
|
||||
var DependencyGraph = Module.create(function DependencyGraph(dpGraph) {
|
||||
this._nodes = dpGraph ? JSON.parse(JSON.stringify(dpGraph._nodes)) : {};
|
||||
});
|
||||
|
||||
/**
|
||||
* Print the content of the nodes for debugging.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DependencyGraph.prototype
|
||||
*/
|
||||
DependencyGraph.prototype._printNodes = function dg_printNodes() {
|
||||
Object.keys(this._nodes).forEach((name) => {
|
||||
var node = this._nodes[name];
|
||||
console.log('name: ' + name);
|
||||
console.log('children: ' +
|
||||
node.children.reduce((result, name) => {
|
||||
return result + ' ' + name;
|
||||
}, ''));
|
||||
console.log('dependent nodes: ' +
|
||||
node.dependentNodes.reduce((result, name) => {
|
||||
return result + ' ' + name;
|
||||
}, ''));
|
||||
console.log('======================');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Traverse the tree and add the dependent nodes to all nodes in the path.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DependencyGraph.prototype
|
||||
* @param {Object} node
|
||||
* The node that we have new dependent nodes to add to.
|
||||
* @param {Array} dependentNodes
|
||||
* New dependent nodes to add.
|
||||
*/
|
||||
DependencyGraph.prototype._addDependentNodes =
|
||||
function dg_addDependentNodes(node, dependentNodes, traversedNodes) {
|
||||
traversedNodes = traversedNodes || [];
|
||||
if (traversedNodes.indexOf(node.name) >= 0) {
|
||||
traversedNodes.push(node.name);
|
||||
var error = traversedNodes.join(' -> ');
|
||||
this.throw('circular dependency detected! ' + error);
|
||||
}
|
||||
|
||||
if (dependentNodes) {
|
||||
// add all nodes in "dependendNodes" to the current node
|
||||
dependentNodes.forEach((name) => {
|
||||
if (node.dependentNodes.indexOf(name) < 0) {
|
||||
node.dependentNodes.push(name);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Nothing to add, prepare the nodes to be added to the children.
|
||||
dependentNodes = Array.prototype.slice.call(node.dependentNodes);
|
||||
}
|
||||
|
||||
dependentNodes.push(node.name);
|
||||
traversedNodes.push(node.name);
|
||||
node.children.forEach((name) => {
|
||||
this._addDependentNodes(this._nodes[name], dependentNodes,
|
||||
traversedNodes);
|
||||
});
|
||||
dependentNodes.pop();
|
||||
traversedNodes.pop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add dependency. Calling to this function adds a dependency from the first
|
||||
* parameter towards the second one.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DependencyGraph.prototype
|
||||
* @param {String} name1
|
||||
* The name that depends on the other.
|
||||
* @param {String} name2
|
||||
* The name that is being depended on.
|
||||
*/
|
||||
DependencyGraph.prototype.addDependency = function dg_addDep(name1, name2) {
|
||||
var node1 = this._nodes[name1];
|
||||
var node2 = this._nodes[name2];
|
||||
|
||||
if (!node1) {
|
||||
node1 = this._nodes[name1] = {
|
||||
name: name1,
|
||||
children: [],
|
||||
dependentNodes: []
|
||||
};
|
||||
}
|
||||
|
||||
if (!node2) {
|
||||
node2 = this._nodes[name2] = {
|
||||
name: name2,
|
||||
children: [],
|
||||
dependentNodes: []
|
||||
};
|
||||
}
|
||||
|
||||
if (node1.children.indexOf(name2) < 0) {
|
||||
node1.children.push(name2);
|
||||
this._addDependentNodes(node1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all dependent names of a name.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DependencyGraph.prototype
|
||||
* @param {String} name
|
||||
*/
|
||||
DependencyGraph.prototype.getAllDependent = function dg_getAllDep(name) {
|
||||
var node = this._nodes[name];
|
||||
return node && node.dependentNodes;
|
||||
};
|
||||
|
||||
return DependencyGraph;
|
||||
});
|
||||
137
gaia/apps/settings/js/modules/base/event_emitter.js
Executable file
137
gaia/apps/settings/js/modules/base/event_emitter.js
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
|
||||
/**
|
||||
* EventEmitter implements basic event emitting logics. It also provides
|
||||
* function that augments the ability to other objects.
|
||||
*
|
||||
* @example
|
||||
* var eventEmitter = EventEmitter(['event1', 'event2']);
|
||||
* var eventHandler = function(event) {
|
||||
* console.log(event.detail);
|
||||
* };
|
||||
* var obj = {
|
||||
* handleEvent: function(event) {
|
||||
* console.log(event.detail);
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* eventEmitter.addEventListener('event1', eventHandler);
|
||||
* eventEmitter.addEventListener('event2', obj);
|
||||
*
|
||||
* eventEmitter._emitEvent('event1', 'value1'); // value1
|
||||
* eventEmitter._emitEvent('event2', 'value2'); // value2
|
||||
*
|
||||
* Extending from EventEmitter:
|
||||
* @example
|
||||
* var NewModule = Module.create(function() {
|
||||
* this.super(EventEmitter).call(this, ['event1', 'event2']);
|
||||
* }).extend(EventEmitter);
|
||||
*
|
||||
* var module = NewModule();
|
||||
*
|
||||
* @class EventEmitter
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
var EventEmitter = Module.create(function EventEmitter(eventNames) {
|
||||
if (eventNames && eventNames.length) {
|
||||
this._eventListeners = eventNames.reduce((result, eventName) => {
|
||||
result[eventName] = [];
|
||||
return result;
|
||||
}, {});
|
||||
} else {
|
||||
this.throw('no valid registered events');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Emit an event of a specific type and a given value.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf EventEmitter.prototype
|
||||
* @param {String} eventName
|
||||
* @param {Object} value
|
||||
*/
|
||||
EventEmitter.prototype._emitEvent = function(eventName, value) {
|
||||
var listeners = this._eventListeners[eventName];
|
||||
if (!listeners) {
|
||||
this.throw('invalid event name: ' + eventName);
|
||||
}
|
||||
|
||||
this.debug('_emitEvent:' + eventName + ' ' + value);
|
||||
var eventObj = {
|
||||
type: eventName,
|
||||
detail: value
|
||||
};
|
||||
listeners.forEach((listener) => {
|
||||
listener.call(this, eventObj);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an event listener on an event. If listener is an object,
|
||||
* object.handleEvent is called when the event emits.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf EventEmitter.prototype
|
||||
* @param {String} eventName
|
||||
* @param {(Function|Object)} listener
|
||||
*/
|
||||
EventEmitter.prototype.addEventListener = function(eventName, listener) {
|
||||
var listeners = this._eventListeners[eventName];
|
||||
if (listener && listeners) {
|
||||
switch (typeof listener) {
|
||||
case 'function':
|
||||
break;
|
||||
case 'object':
|
||||
if (typeof listener.handleEvent === 'function') {
|
||||
listener = listener.handleEvent;
|
||||
} else {
|
||||
listener = null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
listener = null;
|
||||
break;
|
||||
}
|
||||
if (listener && listeners.indexOf(listener) < 0) {
|
||||
listeners.push(listener);
|
||||
}
|
||||
} else {
|
||||
this.error('addEventListener: invalid listener for ' + eventName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the event listener from an event.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf EventEmitter.prototype
|
||||
* @param {String} eventName
|
||||
* @param {(Function|Object)} listener
|
||||
*/
|
||||
EventEmitter.prototype.removeEventListener = function(eventName, listener) {
|
||||
var listeners = this._eventListeners[eventName];
|
||||
if (listener && listeners) {
|
||||
var index = -1;
|
||||
var type = typeof listener;
|
||||
switch (type) {
|
||||
case 'function':
|
||||
index = listeners.indexOf(listener);
|
||||
break;
|
||||
case 'object':
|
||||
index = listeners.indexOf(listener.handleEvent);
|
||||
break;
|
||||
}
|
||||
if (index >= 0) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.error('removeEventListener: invalid listener for ' + eventName);
|
||||
}
|
||||
};
|
||||
|
||||
return EventEmitter;
|
||||
});
|
||||
152
gaia/apps/settings/js/modules/base/module.js
Executable file
152
gaia/apps/settings/js/modules/base/module.js
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Module provides methods to create a new module. Any module created here has
|
||||
* the ability of being extended by other modules. Existing properties are
|
||||
* overridden.
|
||||
*
|
||||
* Creating a module
|
||||
* @example
|
||||
* var NewModule = Module.create(function() {
|
||||
* this.value = 100;
|
||||
* });
|
||||
* NewModule.prototype.print = function() { console.log(this.value); };
|
||||
*
|
||||
* var instance = NewModule();
|
||||
* instance.print(); // 100
|
||||
*
|
||||
* Extending a module
|
||||
* @example
|
||||
* var AnotherModule = Module.create();
|
||||
* AnotherModule.prototype.inc = function() { this.value++; };
|
||||
* var ExtendingModule = NewModule.extend(AnotherModule);
|
||||
*
|
||||
* var instance = ExtendingModule();
|
||||
* instance.inc();
|
||||
* instance.print(); // 101
|
||||
*
|
||||
* @module modules/base/module
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
const LOG_LEVEL = {
|
||||
NONE: 0,
|
||||
DEBUG: 1,
|
||||
INFO: 2,
|
||||
WARN: 3,
|
||||
ERROR: 4,
|
||||
ALL: 5
|
||||
};
|
||||
|
||||
var _constructorMap = (function() {
|
||||
var _ctorMap = new Map();
|
||||
var _getConstructor = function(Module) {
|
||||
return _ctorMap.get(Module) || function() {};
|
||||
};
|
||||
var _registerConstructor = function(Module, constructor) {
|
||||
_ctorMap.set(Module, constructor || function() {});
|
||||
};
|
||||
return {
|
||||
getConstructor: _getConstructor,
|
||||
registerConstructor: _registerConstructor
|
||||
};
|
||||
})();
|
||||
|
||||
var _emptyFunction = function() {};
|
||||
var _createLogger = function(name) {
|
||||
switch (name) {
|
||||
case 'DEBUG':
|
||||
return function(msg) {
|
||||
console.log(this._msgPrefix + msg);
|
||||
};
|
||||
case 'INFO':
|
||||
return function(msg) {
|
||||
console.info(this._msgPrefix + msg);
|
||||
};
|
||||
case 'WARN':
|
||||
return function(msg) {
|
||||
console.warn(this._msgPrefix + msg);
|
||||
};
|
||||
case 'ERROR':
|
||||
return function(msg) {
|
||||
console.error(this._msgPrefix + msg);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var ModulePrototype = {
|
||||
get _msgPrefix() {
|
||||
return '[' + this.$name + ']: ';
|
||||
},
|
||||
debug: _emptyFunction,
|
||||
info: _emptyFunction,
|
||||
warn: _emptyFunction,
|
||||
error: _emptyFunction,
|
||||
throw: function(msg) {
|
||||
throw new Error(this._msgPrefix + msg);
|
||||
},
|
||||
set _logLevel(value) {
|
||||
Object.keys(LOG_LEVEL).forEach((name) => {
|
||||
var level = LOG_LEVEL[name];
|
||||
if (value >= level && value > 0) {
|
||||
this[name.toLowerCase()] = _createLogger(name);
|
||||
}
|
||||
});
|
||||
},
|
||||
super: _constructorMap.getConstructor
|
||||
};
|
||||
|
||||
var _extend = function() {
|
||||
switch (arguments.length) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
var Module = arguments[0];
|
||||
for (var prop in Module.prototype) {
|
||||
if (prop === '$name') {
|
||||
continue;
|
||||
}
|
||||
var pd = Object.getOwnPropertyDescriptor(Module.prototype, prop);
|
||||
if (pd) {
|
||||
Object.defineProperty(this.prototype, prop, pd);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Array.prototype.forEach.call(arguments, (Module) => {
|
||||
_extend.call(this, Module);
|
||||
});
|
||||
break;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
// Returns a module function. The function returns an instance of the
|
||||
// module when it is called. The passed constructor is called using the
|
||||
// instance as "this".
|
||||
function _create(constructor) {
|
||||
if (constructor && typeof constructor !== 'function') {
|
||||
throw new Error('[Module]: Invalid constructor');
|
||||
}
|
||||
|
||||
var ModuleFunc = function() {};
|
||||
ModuleFunc.prototype = Object.create(ModulePrototype);
|
||||
ModuleFunc.prototype.$name = constructor && constructor.name || '';
|
||||
|
||||
var Module = function() {
|
||||
var instance = new ModuleFunc();
|
||||
if (constructor) {
|
||||
constructor.apply(instance, arguments);
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
_constructorMap.registerConstructor(Module, constructor);
|
||||
Module.extend = _extend;
|
||||
Module.prototype = ModuleFunc.prototype;
|
||||
return Module;
|
||||
}
|
||||
|
||||
return {
|
||||
LOG_LEVEL: LOG_LEVEL,
|
||||
create: _create
|
||||
};
|
||||
});
|
||||
147
gaia/apps/settings/js/modules/base/task_scheduler.js
Executable file
147
gaia/apps/settings/js/modules/base/task_scheduler.js
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* TaskScheduler helps manage tasks and ensures they are executed in
|
||||
* sequential order. When a task of a certain type is enqueued, all pending
|
||||
* tasks of the same type in the queue are removed. This avoids redundant
|
||||
* queries and improves user perceived performance.
|
||||
*
|
||||
* @module modules/base/task_scheduler
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Defer = require('modules/defer');
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
/**
|
||||
* @class TaskScheduler
|
||||
* @returns {TaskScheduler}
|
||||
*/
|
||||
var TaskScheduler = Module.create(function TaskScheduler() {
|
||||
this.super(Observable).call(this);
|
||||
this.isLocked = false;
|
||||
this._tasks = [];
|
||||
}).extend(Observable);
|
||||
|
||||
Observable.defineObservableProperty(TaskScheduler.prototype, 'isLocked', {
|
||||
value: false
|
||||
});
|
||||
|
||||
const TASK_TYPE = {
|
||||
GENERAL: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* We will keep some constants here as a static variable
|
||||
*
|
||||
* @access public
|
||||
* @memberOf TaskScheduler
|
||||
*/
|
||||
Object.defineProperty(TaskScheduler, 'TASK_TYPE', {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: function() {
|
||||
return TASK_TYPE;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Change the internal state to make sure we are locked.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf TaskScheduler
|
||||
*/
|
||||
TaskScheduler.prototype._lock = function() {
|
||||
this.isLocked = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the internal state to unlocked and execute next task.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf TaskScheduler
|
||||
*/
|
||||
TaskScheduler.prototype._unlock = function() {
|
||||
this.isLocked = false;
|
||||
this._executeNextTask();
|
||||
};
|
||||
|
||||
/**
|
||||
* We will remove any redundant same-type tasks and keep the remaining ones
|
||||
* including the others which are not cancelable (general use task).
|
||||
*
|
||||
* @access private
|
||||
* @memberOf TaskScheduler
|
||||
* @param {String} type
|
||||
* @return {Array} array of tasks
|
||||
*/
|
||||
TaskScheduler.prototype._removeRedundantTasks = function(type) {
|
||||
return this._tasks.filter((task) => {
|
||||
if (!task.cancelable) {
|
||||
return true;
|
||||
} else {
|
||||
return task.type !== type;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute the next task when unlocked.
|
||||
*
|
||||
* @access private
|
||||
* @returns {Promise}
|
||||
* @memberOf TaskScheduler
|
||||
*/
|
||||
TaskScheduler.prototype._executeNextTask = function() {
|
||||
if (this.isLocked) {
|
||||
return;
|
||||
}
|
||||
var nextTask = this._tasks.shift();
|
||||
if (nextTask) {
|
||||
this._lock();
|
||||
return nextTask.func().then((result) => {
|
||||
nextTask.defer.resolve(result);
|
||||
this._unlock();
|
||||
}, () => {
|
||||
nextTask.defer.reject();
|
||||
this._unlock();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the only entry point for caller, and we will enqueue all tasks
|
||||
* one by one and execute them in order.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf TaskScheduler
|
||||
* @param {Object} task
|
||||
* @param {String} task.type
|
||||
* @param {Function} task.func - make sure the func would return a promise
|
||||
* @param {Boolean} task.cancelable
|
||||
* @return {Promise}
|
||||
*/
|
||||
TaskScheduler.prototype.enqueue = function(task) {
|
||||
var defer = Defer();
|
||||
|
||||
// if there is no func, let's directly resolve this task
|
||||
if (!task.func) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!task.type) {
|
||||
// copy the task and make sure we won't change the previous one
|
||||
task = Object.assign({}, task);
|
||||
task.type = TaskScheduler.TASK_TYPE.GENERAL;
|
||||
}
|
||||
|
||||
task.defer = defer;
|
||||
this._tasks = this._removeRedundantTasks(task.type);
|
||||
this._tasks.push(task);
|
||||
this._executeNextTask();
|
||||
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
return TaskScheduler;
|
||||
});
|
||||
121
gaia/apps/settings/js/modules/battery.js
Executable file
121
gaia/apps/settings/js/modules/battery.js
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Battery is an Observable that wraps the platform battery object. It has two
|
||||
* observable properties: level and state.
|
||||
*
|
||||
* @module modules/battery
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var NavigatorBattery = require('modules/navigator/battery');
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
/**
|
||||
* @class Battery
|
||||
* @requires module:modules/navigator/battery
|
||||
* @requires module:modules/base/module
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @returns {Battery}
|
||||
*/
|
||||
var Battery = Module.create(function Battery() {
|
||||
this.super(Observable).call(this);
|
||||
|
||||
this._level = this._getLevel();
|
||||
this._state = this._getState();
|
||||
this._chargingTime = this._getChargingTime();
|
||||
this._dischargingTime = this._getDischargingTime();
|
||||
NavigatorBattery.addEventListener('levelchange', () => {
|
||||
this._level = this._getLevel();
|
||||
});
|
||||
NavigatorBattery.addEventListener('chargingchange', () => {
|
||||
this._state = this._getState();
|
||||
this._chargingTime = this._getChargingTime();
|
||||
this._dischargingTime = this._getDischargingTime();
|
||||
});
|
||||
NavigatorBattery.addEventListener('chargingtimechange', () => {
|
||||
this._chargingTime = this._getChargingTime();
|
||||
});
|
||||
NavigatorBattery.addEventListener('dischargingtimechange', () => {
|
||||
this._dischargingTime = this._getDischargingTime();
|
||||
});
|
||||
}).extend(Observable);
|
||||
|
||||
/**
|
||||
* An observable property indicating the current battery level.
|
||||
*
|
||||
* @access public
|
||||
* @readonly
|
||||
* @memberOf Battery.prototype
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(Battery.prototype,
|
||||
'level', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* An observable property indicating the current battery state.
|
||||
*
|
||||
* @access public
|
||||
* @readonly
|
||||
* @memberOf Battery.prototype
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(Battery.prototype,
|
||||
'state', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* An observable property indicating the current charging time.
|
||||
*
|
||||
* @access public
|
||||
* @readonly
|
||||
* @memberOf Battery.prototype
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(Battery.prototype,
|
||||
'chargingTime', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* An observable property indicating the current discharging time.
|
||||
*
|
||||
* @access public
|
||||
* @readonly
|
||||
* @memberOf Battery.prototype
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(Battery.prototype,
|
||||
'dischargingTime', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
Battery.prototype._getLevel = function() {
|
||||
return Math.min(100, Math.round(NavigatorBattery.level * 100));
|
||||
};
|
||||
|
||||
Battery.prototype._getState = function() {
|
||||
if (NavigatorBattery.charging) {
|
||||
return (this._getLevel() == 100) ? 'charged' : 'charging';
|
||||
} else {
|
||||
return 'unplugged';
|
||||
}
|
||||
};
|
||||
|
||||
Battery.prototype._getChargingTime = function() {
|
||||
return NavigatorBattery.chargingTime;
|
||||
};
|
||||
|
||||
Battery.prototype._getDischargingTime = function() {
|
||||
return NavigatorBattery.dischargingTime;
|
||||
};
|
||||
|
||||
return Battery();
|
||||
});
|
||||
111
gaia/apps/settings/js/modules/bluetooth/bluetooth_adapter_manager.js
Executable file
111
gaia/apps/settings/js/modules/bluetooth/bluetooth_adapter_manager.js
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* BluetoothAdapterManager:
|
||||
* - BluetoothAdapterManager is an Observable that wraps the platform
|
||||
* Bluetooth object for construct default adapter.
|
||||
* - It has only one observable properties: defaultAdapter
|
||||
* BluetoothAdapterManager only update state and does not involve in any UI
|
||||
* logic.
|
||||
*
|
||||
* @module BluetoothAdapterManager
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var NavigatorBluetooth = require('modules/navigator/mozBluetooth');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
var _debug = false;
|
||||
var debug = function() {};
|
||||
if (_debug) {
|
||||
debug = function btam_debug(msg) {
|
||||
console.log('--> [BluetoothAdapterManager]: ' + msg);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @alias module:modules/bluetooth/BluetoothAdapterManager
|
||||
* @requires module:modules/navigator/mozBluetooth
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @return {BluetoothAdapterManager}
|
||||
*/
|
||||
var BluetoothAdapterManager = {
|
||||
/**
|
||||
* The default adapter used to connect to the remote bluetooth devices.
|
||||
*
|
||||
* @readonly
|
||||
* @memberOf BluetoothAdapterManager
|
||||
* @type {Object}
|
||||
*/
|
||||
defaultAdapter: null,
|
||||
|
||||
/**
|
||||
* Init Bluetooth module.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothAdapterManager
|
||||
*/
|
||||
_init: function btam__init() {
|
||||
// Early return while there is no navigator.mozBluetooth module.
|
||||
if (!NavigatorBluetooth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// watch BluetoothManager event
|
||||
this._watchMozBluetoothAttributechanged();
|
||||
|
||||
// init default adapter
|
||||
this._initDefaultAdapter();
|
||||
},
|
||||
|
||||
/**
|
||||
* Watch 'attributechanged' event from mozBluetooth for updating default
|
||||
* adapter information.
|
||||
*
|
||||
* 'attributechanged' event description:
|
||||
* A handler to trigger when bluetooth manager's only property
|
||||
* defaultAdapter has changed.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothAdapterManager
|
||||
*/
|
||||
_watchMozBluetoothAttributechanged:
|
||||
function btam__watchMozBluetoothAttributechanged() {
|
||||
NavigatorBluetooth.addEventListener('attributechanged', (evt) => {
|
||||
for (var i in evt.attrs) {
|
||||
switch (evt.attrs[i]) {
|
||||
case 'defaultAdapter':
|
||||
// Default adapter attribute change.
|
||||
// Usually, it means that we reach new default adapter.
|
||||
this.defaultAdapter = NavigatorBluetooth.defaultAdapter;
|
||||
debug('_watchMozBluetoothAttributechanged(): ' +
|
||||
'this.defaultAdapter = ' + this.defaultAdapter);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* Reach default adapter from platform Bluetooth.
|
||||
*
|
||||
* If the property is accessed on platform in the first time, it will
|
||||
* trigger Gecko::Bluetooth to get default adapter from the chipset.
|
||||
* So the value would be null.
|
||||
* Then we will receive 'attributechanged' event later.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothAdapterManager
|
||||
*/
|
||||
_initDefaultAdapter: function btam__initDefaultAdapter() {
|
||||
this.defaultAdapter = NavigatorBluetooth.defaultAdapter;
|
||||
debug('_initDefaultAdapter(): this.defaultAdapter = ' +
|
||||
this.defaultAdapter);
|
||||
}
|
||||
};
|
||||
|
||||
// Create the observable object using the prototype.
|
||||
var bluetoothAdapterManager = Observable(BluetoothAdapterManager);
|
||||
bluetoothAdapterManager._init();
|
||||
return bluetoothAdapterManager;
|
||||
});
|
||||
198
gaia/apps/settings/js/modules/bluetooth/bluetooth_cod_mapper.js
Executable file
198
gaia/apps/settings/js/modules/bluetooth/bluetooth_cod_mapper.js
Executable file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* BluetoothClassOfDeviceMapper:
|
||||
* - BluetoothClassOfDeviceMapper is a mapper that helps settings/bluetooth
|
||||
* apps to decide the icon type. Since platform exposed the 'number' type
|
||||
* for class of device(Major Service Classes, Major Device Classes,
|
||||
* Minor Device Classes), the helper will map the input number to specific
|
||||
* device type. And the decision is referenced from Baseband.
|
||||
* (Assigned numbers for Baseband identifies the Inquiry Access codes
|
||||
* and Class of Device/Service (CoD) fields.)[3]
|
||||
* According to the specifications described, we define the device type in
|
||||
* Gaia side entirely. And the naming of device type we given is also
|
||||
* mapping to CSS style sheet.
|
||||
*
|
||||
* - [1] Bluetooth API v1, BluetoothDevice icon property:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/BluetoothDevice.icon
|
||||
* - [2] Bluetooth API v2, BluetoothClassOfDevice object:
|
||||
* https://wiki.mozilla.org/B2G/Bluetooth/WebBluetooth-v2/
|
||||
* BluetoothClassOfDevice
|
||||
* - [3] Bluetooth Class of Device Reference:
|
||||
* https://www.bluetooth.org/en-us/specification/assigned-numbers/baseband
|
||||
* - We can update the mapper if the specifications Baseband is updated.
|
||||
*
|
||||
* @module BluetoothClassOfDeviceMapper
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
var _debug = false;
|
||||
var debug = function() {};
|
||||
if (_debug) {
|
||||
debug = function btcodm_debug(msg) {
|
||||
console.log('--> [BluetoothCODMapper]: ' + msg);
|
||||
};
|
||||
}
|
||||
|
||||
var MinorDeviceClasses = {
|
||||
Computer: {
|
||||
0: 'computer', // Uncategorized, code for device not assigned
|
||||
1: 'computer', // Desktop workstation
|
||||
2: 'computer', // Server-class computer
|
||||
3: 'computer', // Laptop
|
||||
4: 'pda', // Handheld PC/PDA (clamshell)
|
||||
5: 'pda', // Palm-size PC/PDA
|
||||
6: 'computer', // Wearable computer (watch size)
|
||||
7: 'computer', // Tablet
|
||||
reserved: 'computer' // All other values reserved. Default type
|
||||
// 'computer' given for reserved case.
|
||||
},
|
||||
|
||||
Phone: {
|
||||
0: 'phone', // Uncategorized, code for device not assigned
|
||||
1: 'phone', // Cellular
|
||||
2: 'phone', // Cordless
|
||||
3: 'phone', // Smartphone
|
||||
4: 'modem', // Wired modem or voice gateway
|
||||
5: 'phone', // Common ISDN access
|
||||
reserved: 'phone' // All other values reserved. Default type
|
||||
// 'phone' given for reserved case.
|
||||
},
|
||||
|
||||
AudioVideo: {
|
||||
0: 'audio-card', // Uncategorized, code not assigned
|
||||
1: 'audio-card', // Wearable Headset Device
|
||||
2: 'audio-card', // Hands-free Device
|
||||
4: 'audio-input-microphone', // Microphone
|
||||
5: 'audio-card', // Loudspeaker
|
||||
6: 'audio-card', // Headphones
|
||||
7: 'audio-card', // Portable Audio
|
||||
8: 'audio-card', // Car audio
|
||||
9: 'audio-card', // Set-top box
|
||||
10: 'audio-card', // HiFi Audio Device
|
||||
11: 'camera-video', // VCR
|
||||
12: 'camera-video', // Video Camera
|
||||
13: 'camera-video', // Camcorder
|
||||
14: 'video-display', // Video Monitor
|
||||
15: 'video-display', // Video Display and Loudspeaker
|
||||
16: 'video-display', // Video Conferencing
|
||||
18: 'audio-card', // Gameing/Toy
|
||||
reserved: 'audio-card' // All other values reserved. Default type
|
||||
// 'audio-card' given for reserved case.
|
||||
},
|
||||
|
||||
Peripheral: {
|
||||
1: 'input-gaming', // Joystick
|
||||
2: 'input-gaming', // Gamepad
|
||||
16: 'input-keyboard', // Keyboard
|
||||
17: 'input-keyboard', // Keyboard
|
||||
18: 'input-keyboard', // Keyboard
|
||||
19: 'input-keyboard', // Keyboard
|
||||
20: 'input-keyboard', // Keyboard
|
||||
21: 'input-keyboard', // Keyboard
|
||||
22: 'input-keyboard', // Keyboard
|
||||
23: 'input-keyboard', // Keyboard
|
||||
24: 'input-keyboard', // Keyboard
|
||||
25: 'input-keyboard', // Keyboard
|
||||
26: 'input-keyboard', // Keyboard
|
||||
27: 'input-keyboard', // Keyboard
|
||||
28: 'input-keyboard', // Keyboard
|
||||
29: 'input-keyboard', // Keyboard
|
||||
30: 'input-keyboard', // Keyboard
|
||||
31: 'input-keyboard', // Keyboard
|
||||
32: 'input-mouse', // Pointing device
|
||||
33: 'input-mouse', // Pointing device
|
||||
34: 'input-mouse', // Pointing device
|
||||
35: 'input-mouse', // Pointing device
|
||||
36: 'input-mouse', // Pointing device
|
||||
37: 'input-tablet', // Digitizer tablet
|
||||
38: 'input-mouse', // Pointing device
|
||||
39: 'input-mouse', // Pointing device
|
||||
40: 'input-mouse', // Pointing device
|
||||
41: 'input-mouse', // Pointing device
|
||||
42: 'input-mouse', // Pointing device
|
||||
43: 'input-mouse', // Pointing device
|
||||
44: 'input-mouse', // Pointing device
|
||||
45: 'input-mouse', // Pointing device
|
||||
46: 'input-mouse', // Pointing device
|
||||
47: 'input-mouse' // Pointing device
|
||||
},
|
||||
|
||||
Imaging: {
|
||||
4: 'video-display', // Display, bit: XXX1XX
|
||||
8: 'camera-photo', // Camera, bit: XX1XXX
|
||||
16: 'scanner', // Scanner, bit: X1XXXX
|
||||
32: 'printer' // Printer, bit: 1XXXXX
|
||||
}
|
||||
};
|
||||
|
||||
var MajorDeviceClasses = {
|
||||
1: MinorDeviceClasses.Computer,
|
||||
2: MinorDeviceClasses.Phone,
|
||||
3: 'network-wireless', // LAN/Network Access Point Major Class
|
||||
4: MinorDeviceClasses.AudioVideo,
|
||||
5: MinorDeviceClasses.Peripheral,
|
||||
6: MinorDeviceClasses.Imaging
|
||||
};
|
||||
|
||||
var BluetoothClassOfDeviceMapper = {
|
||||
/**
|
||||
* The string indicating which kind of icon could be used to represent
|
||||
* the device.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothClassOfDeviceMapper
|
||||
* @param {BluetoothClassOfDevice} cod
|
||||
* @return {String}
|
||||
*/
|
||||
getDeviceType: function btcodm_getDeviceType(cod) {
|
||||
debug('cod.majorDeviceClass = ' + cod.majorDeviceClass);
|
||||
debug('cod.majorServiceClass = ' + cod.majorServiceClass);
|
||||
debug('cod.minorDeviceClass = ' + cod.minorDeviceClass);
|
||||
// Given an empty string to be default type.
|
||||
// Then, we won't show any icon for empty type.
|
||||
var deviceType = '';
|
||||
var majorDeviceClass = MajorDeviceClasses[cod.majorDeviceClass];
|
||||
debug('majorDeviceClass = ' + majorDeviceClass);
|
||||
if (typeof(majorDeviceClass) === 'object') {
|
||||
// drop in other Major Class
|
||||
deviceType = majorDeviceClass[cod.minorDeviceClass] ||
|
||||
majorDeviceClass.reserved || '';
|
||||
debug('return in minor class, type = ' + deviceType);
|
||||
return deviceType;
|
||||
} else if (typeof(majorDeviceClass) === 'string') {
|
||||
// drop in LAN/Network Access Point Major Class
|
||||
debug('return in major class, type = network-wireless');
|
||||
return majorDeviceClass;
|
||||
} else if (cod.majorServiceClass & 0x100) {
|
||||
// Not in any Major Device Class which is defined in Gaia.
|
||||
// Ex: Wearable, Toy, Health.
|
||||
|
||||
// Since there is no icon to display wearable, toy, health devices,
|
||||
// file a bug 1163479[2] to define them for specification needed.
|
||||
// But some of these devices probably service 'Audio' per Bluetooth
|
||||
// specification[1].
|
||||
// Property 'type' may be missed due to CoD of major class is TOY(0x08).
|
||||
// But we need to assign 'type' as 'audio-card' if service class
|
||||
// is 'Audio'. This is for PTS test case TC_AG_COD_BV_02_I.
|
||||
// As HFP specification defines that service class is 'Audio' can
|
||||
// be considered as HFP HF.
|
||||
// [1]: HFP_SPEC_V16.pdf: A device implementing the HF role of HFP shall
|
||||
// set the "Audio" bit in the Service Class field.
|
||||
// [2]: Bug 1163479 - [Gaia][Bluetooth] Device icon definition for
|
||||
// wearable, toy, health(Major Device Class) devices.
|
||||
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1163479)
|
||||
|
||||
// Major Service Class: Audio(Speaker, Microphone, Headset service, ...)
|
||||
deviceType = 'audio-card';
|
||||
debug('return in major service class, type = audio-card');
|
||||
return deviceType;
|
||||
} else {
|
||||
// Not in any Class which is defined in Gaia.
|
||||
debug('not mapping in any class, return type = ' + deviceType);
|
||||
return deviceType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return BluetoothClassOfDeviceMapper;
|
||||
});
|
||||
953
gaia/apps/settings/js/modules/bluetooth/bluetooth_connection_manager.js
Executable file
953
gaia/apps/settings/js/modules/bluetooth/bluetooth_connection_manager.js
Executable file
|
|
@ -0,0 +1,953 @@
|
|||
/**
|
||||
* BluetoothConnectionManager:
|
||||
* BluetoothConnectionManager only update state and does not involve in any UI
|
||||
* logic.
|
||||
*
|
||||
* @module BluetoothConnectionManager
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
|
||||
var AsyncStorage = require('shared/async_storage');
|
||||
|
||||
var _debug = false;
|
||||
var debug = function() {};
|
||||
if (_debug) {
|
||||
debug = function btam_debug(msg) {
|
||||
console.log('--> [BluetoothConnectionManager]: ' + msg);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @alias module:modules/bluetooth/BluetoothConnectionManager
|
||||
* @requires module:modules/bluetooth/bluetooth_adapter_manager
|
||||
* @requires module:shared/async_storage
|
||||
* @return {BluetoothConnectionManager}
|
||||
*/
|
||||
var BluetoothConnectionManager = {
|
||||
/**
|
||||
* The profiles of connected device that we are defined here.
|
||||
*
|
||||
* @public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @type {Object}
|
||||
*/
|
||||
Profiles: {
|
||||
'hfp': 0x111E, // Handsfree
|
||||
'a2dp': 0x110D // Advanced Audio Distribution Devices
|
||||
},
|
||||
|
||||
/**
|
||||
* The address of device that we are trying to connect.
|
||||
*
|
||||
* @public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @type {String}
|
||||
*/
|
||||
connectingAddress: null,
|
||||
|
||||
/**
|
||||
* A object that we cache the connected devices information(address, device,
|
||||
* connectedProfiles). It will be inited while default adapter is ready.
|
||||
* And these information is coming from profile events.
|
||||
* Each connected device is hashed by device address.
|
||||
*
|
||||
* EX:
|
||||
* _connectedDevicesInfo = {
|
||||
* 'AA:BB:CC:00:11:22': {
|
||||
* 'device': DeviceObject,
|
||||
* 'connectedProfiles': {
|
||||
* 'hfp': true,
|
||||
* 'a2dp': false
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* @private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @type {Object}
|
||||
*/
|
||||
_connectedDevicesInfo: {},
|
||||
|
||||
/**
|
||||
* An instance to maintain that we have created a promise to get connected
|
||||
* devices.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @return {Promise}
|
||||
*/
|
||||
_getConnectedDevicesPromise: null,
|
||||
|
||||
/**
|
||||
* The object maintains listeners' callback per property name.
|
||||
* Each listener would be called as following definition.
|
||||
* 'connecting' - be called when device is connecting.
|
||||
* 'connected': - be called when device is connected.
|
||||
* 'disconnected': - be called when device is disconnected.
|
||||
* 'profileChanged': - be called when profile is changed.
|
||||
*
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @type {Object}
|
||||
*/
|
||||
_listeners: {
|
||||
'connecting': [],
|
||||
'connected': [],
|
||||
'disconnected': [],
|
||||
'profileChanged': []
|
||||
},
|
||||
|
||||
/**
|
||||
* Default adapter of Bluetooth.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @type {BluetoothAdapter}
|
||||
*/
|
||||
_defaultAdapter: null,
|
||||
|
||||
/**
|
||||
* Init BluetoothConnectionManager module.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
*/
|
||||
_init: function btcm__init() {
|
||||
// Observe 'defaultAdapter' property for reaching default adapter.
|
||||
AdapterManager.observe('defaultAdapter',
|
||||
this._onDefaultAdapterChanged.bind(this));
|
||||
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
|
||||
},
|
||||
|
||||
/**
|
||||
* 'defaultAdapter' change event handler from adapter manager for
|
||||
* updating adapter immediately.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothAdapter} newAdapter
|
||||
* @param {BluetoothAdapter} oldAdapter
|
||||
*/
|
||||
_onDefaultAdapterChanged:
|
||||
function btcm__onDefaultAdapterChanged(newAdapter, oldAdapter) {
|
||||
debug('_onDefaultAdapterChanged(): newAdapter = ' + newAdapter);
|
||||
debug('_onDefaultAdapterChanged(): oldAdapter = ' + oldAdapter);
|
||||
|
||||
// save default adapter
|
||||
this._defaultAdapter = newAdapter;
|
||||
|
||||
if (oldAdapter) {
|
||||
// unwatch event since the old adapter is no longer usefull
|
||||
this._unwatchProfilesStatuschanged(oldAdapter);
|
||||
this._unwatchDefaultAdapterOnattributechanged(oldAdapter);
|
||||
}
|
||||
|
||||
if (newAdapter) {
|
||||
// watch event since the new adapter is ready to access
|
||||
this._watchProfilesStatuschanged(newAdapter);
|
||||
this._watchDefaultAdapterOnattributechanged(newAdapter);
|
||||
// restore connection
|
||||
if (newAdapter.state === 'enabled') {
|
||||
this._restoreConnection();
|
||||
}
|
||||
} else {
|
||||
// reset properties only
|
||||
this._resetConnectionInfo();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the cache of connected devices in ConnectionManager.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @return {Promise}
|
||||
*/
|
||||
getConnectedDevices: function btcm_getConnectedDevices() {
|
||||
if (!this._getConnectedDevicesPromise) {
|
||||
this._getConnectedDevicesPromise =
|
||||
this._initConnectedDevicesInfo().then(() => {
|
||||
debug('getConnectedDevices(): resolved with latest cache = ' +
|
||||
JSON.stringify(this._connectedDevicesInfo));
|
||||
return this._connectedDevicesInfo;
|
||||
}, (reason) => {
|
||||
debug('getConnectedDevices(): rejected with reason = ' + reason);
|
||||
this._getConnectedDevicesPromise = null;
|
||||
});
|
||||
}
|
||||
return this._getConnectedDevicesPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Init cache of connected device and save it in cache.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @return {Promise}
|
||||
*/
|
||||
_initConnectedDevicesInfo: function btcm__initConnectedDevicesInfo() {
|
||||
if (!this._defaultAdapter) {
|
||||
return Promise.reject('default adapter is not existed!!');
|
||||
}
|
||||
// Init connection status and profile from platform.
|
||||
// Then, save these connected device information in cache.
|
||||
return this._getConnectedDevicesFromPlatform().then(
|
||||
(connectedDevicesByProfile) => {
|
||||
this._constructDeviceItemsMap(connectedDevicesByProfile);
|
||||
}, (reason) => {
|
||||
debug('_initConnectedDevicesInfo(): rejected in ' +
|
||||
'_getConnectedDevicesFromPlatform');
|
||||
return Promise.reject(reason);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will update each device item in maintaining map.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {Object} connectedDevices
|
||||
*/
|
||||
_constructDeviceItemsMap:
|
||||
function btcm__constructDeviceItemsMap(connectedDevices) {
|
||||
debug('_constructDeviceItemsMap(): connectedDevices = ' +
|
||||
JSON.stringify(connectedDevices));
|
||||
if (!connectedDevices) {
|
||||
// Return empty object while there is no any connected devices.
|
||||
debug('_constructDeviceItemsMap(): early return with empty object');
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(this.Profiles).map((profileID) => {
|
||||
connectedDevices[profileID].forEach((connectedDevice) => {
|
||||
var connectionDeviceInfo = {
|
||||
address: connectedDevice.address,
|
||||
connected: true,
|
||||
profileID: profileID,
|
||||
device: connectedDevice
|
||||
};
|
||||
debug('_constructDeviceItemsMap(): connectionDeviceInfo = ' +
|
||||
JSON.stringify(connectionDeviceInfo));
|
||||
this._initConnectedDevicesCache(connectionDeviceInfo);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Init the cache which is maintained for connection devices.
|
||||
* And the input is gotten from platform API adapter.getConnectedDevices().
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {Object} options
|
||||
* @param {String} options.address - address of the device
|
||||
* @param {Boolean} options.connected - is connected or not
|
||||
* @param {Object} options.profileID - profile ID of the connection type
|
||||
* @param {Object} options.device - connect device, Bluetooth Object
|
||||
*/
|
||||
_initConnectedDevicesCache:
|
||||
function btcm__initConnectedDevicesCache(options) {
|
||||
debug('_initConnectedDevicesCache(): options = ' +
|
||||
JSON.stringify(options));
|
||||
// hash by device address
|
||||
var info = this._connectedDevicesInfo[options.address];
|
||||
if (info) {
|
||||
// Already have profiles, update it for other profile.
|
||||
info.connectedProfiles[options.profileID] = options.connected;
|
||||
} else {
|
||||
// Not have profiles yet, create for it.
|
||||
// If options.device is existed, save the connected device.
|
||||
// Otherwise, given null in this property.
|
||||
var connectedDevice = (options.device) ? options.device : null;
|
||||
info = {
|
||||
'device': connectedDevice,
|
||||
'connectedProfiles': {}
|
||||
};
|
||||
info.connectedProfiles[options.profileID] = options.connected;
|
||||
}
|
||||
// Save the device/profile in map.
|
||||
this._connectedDevicesInfo[options.address] = info;
|
||||
|
||||
// If there is no profile connected,
|
||||
// remove the device item from cache since it is already disconnected.
|
||||
var dataToCheckConnectedProfile = {
|
||||
address: options.address,
|
||||
connectedDevices: this._connectedDevicesInfo
|
||||
};
|
||||
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
|
||||
delete this._connectedDevicesInfo[options.address];
|
||||
}
|
||||
// Return the latest cache which is just updated here.
|
||||
debug('_initConnectedDevicesCache(): this._connectedDevicesInfo = ' +
|
||||
JSON.stringify(this._connectedDevicesInfo));
|
||||
return this._connectedDevicesInfo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the cache which is maintained for connection devices.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {Object} options
|
||||
* @param {String} options.address - address of the device
|
||||
* @param {Boolean} options.connected - is connected or not
|
||||
* @param {Object} options.profileID - profile ID of the connection type
|
||||
* @param {Object} options.device - connect device, Bluetooth Object
|
||||
*/
|
||||
_updateConnectedDevices:
|
||||
function btcm__updateConnectedDevices(options) {
|
||||
return this.getConnectedDevices().then((connectedDevicesInfo) => {
|
||||
debug('_updateConnectedDevices(): connectedDevicesInfo = ' +
|
||||
JSON.stringify(connectedDevicesInfo));
|
||||
|
||||
// hash by device address
|
||||
var info =
|
||||
(connectedDevicesInfo) ? connectedDevicesInfo[options.address] : null;
|
||||
if (info) {
|
||||
// Already have profiles, update it for other profile.
|
||||
info.connectedProfiles[options.profileID] = options.connected;
|
||||
} else {
|
||||
// Not have profiles yet, create for it.
|
||||
// If options.device is existed, save the connected device.
|
||||
// Otherwise, given null in this property.
|
||||
var connectedDevice = (options.device) ? options.device : null;
|
||||
info = {
|
||||
'device': connectedDevice,
|
||||
'connectedProfiles': {}
|
||||
};
|
||||
info.connectedProfiles[options.profileID] = options.connected;
|
||||
}
|
||||
// Save the device/profile in map.
|
||||
this._connectedDevicesInfo[options.address] = info;
|
||||
|
||||
// If there is no profile connected,
|
||||
// remove the device item from cache since it is already disconnected.
|
||||
var dataToCheckConnectedProfile = {
|
||||
address: options.address,
|
||||
connectedDevices: this._connectedDevicesInfo
|
||||
};
|
||||
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
|
||||
delete this._connectedDevicesInfo[options.address];
|
||||
}
|
||||
// Return the latest cache which is just updated here.
|
||||
debug('_updateConnectedDevices(): this._connectedDevicesInfo = ' +
|
||||
JSON.stringify(this._connectedDevicesInfo));
|
||||
return Promise.resolve(this._connectedDevicesInfo);
|
||||
}, () => {
|
||||
debug('_updateConnectedDevices(): rejected with some exception');
|
||||
return Promise.reject('rejected with some exception');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Only reset properties since there is no available default adapter.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
*/
|
||||
_resetConnectionInfo: function btcm__resetConnectionInfo() {
|
||||
// Reset connection status.
|
||||
this.connectingAddress = null;
|
||||
// Clean up the instance to get connected devices
|
||||
// while new adapter is ready.
|
||||
this._getConnectedDevicesPromise = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Watch 'onattributechanged' event from default adapter for watching
|
||||
* adapter enabled/disabled status.
|
||||
*
|
||||
* Description of 'onattributechanged' event:
|
||||
* A handler to trigger when one of the local bluetooth adapter's properties
|
||||
* has changed. Note access to the changed property in this event handler
|
||||
* would get the updated value.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothAdapter} adapter
|
||||
*/
|
||||
_watchDefaultAdapterOnattributechanged:
|
||||
function btcm__watchDefaultAdapterOnattributechanged(adapter) {
|
||||
adapter.addEventListener('attributechanged',
|
||||
this._onAdapterAttributeChanged.bind(this, adapter));
|
||||
},
|
||||
|
||||
/**
|
||||
* Unwatch 'onattributechanged' event from default adapter since adapter is
|
||||
* removed.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothAdapter} adapter
|
||||
*/
|
||||
_unwatchDefaultAdapterOnattributechanged:
|
||||
function btcm__unwatchDefaultAdapterOnattributechanged(adapter) {
|
||||
adapter.removeEventListener('attributechanged',
|
||||
this._onAdapterAttributeChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* 'onattributechanged' event handler from default adapter for reaching
|
||||
* adapter enabled/disabled status.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothAdapter} adapter
|
||||
* @param {event} evt
|
||||
*/
|
||||
_onAdapterAttributeChanged:
|
||||
function btcm__onAdapterAttributeChanged(adapter, evt) {
|
||||
for (var i in evt.attrs) {
|
||||
debug('_onAdapterAttributeChanged(): ' + evt.attrs[i]);
|
||||
switch (evt.attrs[i]) {
|
||||
case 'state':
|
||||
if (adapter.state === 'enabled') {
|
||||
// Restore connection while default adapter state is enabled.
|
||||
this._restoreConnection();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Watch every of profile events('onhfpstatuschanged','ona2dpstatuschanged')
|
||||
* from default adapter for updating device connected status immediately.
|
||||
*
|
||||
* Description of 'onhfpstatuschanged' event:
|
||||
* Specifies an event listener to receive hfpstatuschanged events.
|
||||
* Those events occur when an HFP connection status changes.
|
||||
*
|
||||
* Description of 'ona2dpstatuschanged' event:
|
||||
* Specifies an event listener to receive a2dpstatuschanged events.
|
||||
* Those events occur when an A2DP connection status changes.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothAdapter} adapter
|
||||
*/
|
||||
_watchProfilesStatuschanged:
|
||||
function btcm__watchProfilesStatuschanged(adapter) {
|
||||
var eventName;
|
||||
for (var profileID in this.Profiles) {
|
||||
eventName = 'on' + profileID + 'statuschanged';
|
||||
adapter[eventName] =
|
||||
this._onProfileStatuschangeHandler.bind(this, profileID);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unwatch every of profile events('onhfpstatuschanged',
|
||||
* 'ona2dpstatuschanged') from default adapter since adapter is removed.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothAdapter} adapter
|
||||
*/
|
||||
_unwatchProfilesStatuschanged:
|
||||
function btc__unwatchProfilesStatuschanged(adapter) {
|
||||
var eventName;
|
||||
for (var profileID in this.Profiles) {
|
||||
eventName = 'on' + profileID + 'statuschanged';
|
||||
adapter[eventName] = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 'onhfpstatuschanged', 'ona2dpstatuschanged' events handler from
|
||||
* default adapter for updating device connected status.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {String} profileID
|
||||
* @param {event} evt
|
||||
*/
|
||||
_onProfileStatuschangeHandler:
|
||||
function btcm__onProfileStatuschangeHandler(profileID, evt) {
|
||||
debug('_onProfileStatuschangeHandler(): profileID = ' + profileID +
|
||||
', evt.address = ' + evt.address + ', evt.status = ' + evt.status);
|
||||
var options = {
|
||||
address: evt.address,
|
||||
connected: evt.status,
|
||||
profileID: profileID
|
||||
};
|
||||
// Update connection status.
|
||||
this._updateConnectionStatus(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Query async storage to restore connection.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
*/
|
||||
_restoreConnection: function btcm__restoreConnection() {
|
||||
if (!this._defaultAdapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reconnect the one kept in the async storage.
|
||||
AsyncStorage.getItem('device.connected', (address) => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the restore device is already connected or not.
|
||||
this.getConnectedDevices().then((connectedDevices) => {
|
||||
if (connectedDevices && connectedDevices[address]) {
|
||||
// Do an early return since the restore device is connected already.
|
||||
debug('_restoreConnection(): early return cause connected already');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the device which is wanted to connect from paired devices.
|
||||
var restoreDevice = this._getPairedDeviceByAddress(address);
|
||||
if (restoreDevice) {
|
||||
this.connectingAddress = restoreDevice.address;
|
||||
// Fire 'connecting' event.
|
||||
var event = {
|
||||
type: 'connecting',
|
||||
detail: {
|
||||
address: restoreDevice.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
this._connect(restoreDevice).then(() => {
|
||||
debug('_restoreConnection(): restore connection successfully');
|
||||
}, (reason) => {
|
||||
debug('_restoreConnection(): restore connection failed, ' +
|
||||
'reason = ' + reason);
|
||||
// No available profiles are connected. Reset connecting address.
|
||||
this.connectingAddress = null;
|
||||
// Then, fire 'disconnected' event.
|
||||
event = {
|
||||
type: 'disconnected',
|
||||
detail: {
|
||||
address: restoreDevice.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Record connected device so if Bluetooth is turned off and then on
|
||||
* we can restore the connection.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {String} action - to set or remove item for recording connection
|
||||
* @param {String} address - the address of connected device
|
||||
*/
|
||||
_recordConnection: function btcm__recordConnection(action, address) {
|
||||
if (action === 'set') {
|
||||
// record connected device so if Bluetooth is turned off and then on
|
||||
// we can restore the connection
|
||||
AsyncStorage.setItem('device.connected', address);
|
||||
debug('_recordConnection(): set item');
|
||||
} else if ((action === 'remove') &&
|
||||
(this._defaultAdapter.state === 'enabled')) {
|
||||
// Clean up the connected device from async storage
|
||||
// which is recorded before.
|
||||
// Only remove the record while Bluetooth state is enabled.
|
||||
// Because the request also comes while Bluetooth is turned off.
|
||||
AsyncStorage.removeItem('device.connected');
|
||||
debug('_recordConnection(): remove item');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update connection status.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {Object} options
|
||||
* @param {String} options.address - address of the device
|
||||
* @param {Boolean} options.connected - is connected or not
|
||||
* @param {Object} options.profileID - profile ID of the connection type
|
||||
*/
|
||||
_updateConnectionStatus: function btcm___updateConnectionStatus(options) {
|
||||
debug('_updateConnectionStatus(): address = ' + options.address +
|
||||
', connected = ' + options.connected +
|
||||
', profileID = ' + options.profileID);
|
||||
this.connectingAddress = null;
|
||||
|
||||
// Update the profile in the cache of connected device info.
|
||||
this._updateConnectedDevices(options).then((connectedDevicesInfo) => {
|
||||
debug('_updateConnectionStatus(): _updateConnectedDevices() ' +
|
||||
'resolved with connectedDevicesInfo = ' +
|
||||
JSON.stringify(connectedDevicesInfo));
|
||||
// Prepare latest data to check connected profile.
|
||||
var dataToCheckConnectedProfile = {
|
||||
address: options.address,
|
||||
connectedDevices: connectedDevicesInfo
|
||||
};
|
||||
// Fire 'connected'/'disconnected' event according to
|
||||
// the connection profile. Then, record connection.
|
||||
var event;
|
||||
if (options.connected) {
|
||||
// Fire 'connected' event.
|
||||
event = {
|
||||
type: 'connected',
|
||||
detail: {
|
||||
address: options.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
// Record connection.
|
||||
this._recordConnection('set', options.address);
|
||||
} else {
|
||||
// If there is no profile connected,
|
||||
// we have to remove the record connection.
|
||||
// And fire 'disconnected' event for outer modules.
|
||||
if (!this._hasConnectedProfileByAddress(
|
||||
dataToCheckConnectedProfile)) {
|
||||
// Record connection. Only remove the record while Bluetooth state
|
||||
// is enabled. Because the event also comes while Bluetooth is
|
||||
// turned off.
|
||||
if (this._defaultAdapter.state === 'enabled') {
|
||||
this._recordConnection('remove');
|
||||
}
|
||||
// Fire 'disconnected' event.
|
||||
event = {
|
||||
type: 'disconnected',
|
||||
detail: {
|
||||
address: options.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Fire 'profileChanged' event.
|
||||
var newProfiles;
|
||||
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
|
||||
newProfiles = null;
|
||||
} else {
|
||||
newProfiles = connectedDevicesInfo[options.address].connectedProfiles;
|
||||
}
|
||||
event = {
|
||||
type: 'profileChanged',
|
||||
detail: {
|
||||
address: options.address,
|
||||
profiles: newProfiles
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
}, (reason) => {
|
||||
debug('_updateConnectionStatus(): miss to update in rejected case, ' +
|
||||
'reason = ' + reason);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* It provides a convenient function for panel to connect with a device.
|
||||
* And the panel no need to care about connected device currently.
|
||||
* The method will disconnect current connected device first.
|
||||
* Then, it will start connecting a paired device with the device's adapter.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothDevice} device
|
||||
* @return {Promise}
|
||||
*/
|
||||
connect: function btcm_connect(device) {
|
||||
if (!this._defaultAdapter) {
|
||||
return Promise.reject('default adapter is not existed!!');
|
||||
}
|
||||
|
||||
// Disconnect current connected device first.
|
||||
debug('connect(): Want to connect device address = ' + device.address +
|
||||
', name = ' + device.name);
|
||||
|
||||
var connectedDevices = [];
|
||||
return this.getConnectedDevices().then((connectedDevicesInfo) => {
|
||||
for (var address in connectedDevicesInfo) {
|
||||
if (connectedDevicesInfo[address].device !== null) {
|
||||
connectedDevices.push(connectedDevicesInfo[address].device);
|
||||
debug('connect(): push device cache in queue = ' +
|
||||
JSON.stringify(connectedDevices));
|
||||
} else {
|
||||
var regetPairedDevice = this._getPairedDeviceByAddress(address);
|
||||
connectedDevices.push(regetPairedDevice);
|
||||
debug('connect(): push _getPairedDeviceByAddress in queue = ' +
|
||||
JSON.stringify(regetPairedDevice));
|
||||
}
|
||||
}
|
||||
|
||||
debug('connect(): Will disconnect these connected devices = ' +
|
||||
JSON.stringify(connectedDevices));
|
||||
|
||||
// Disconnect these connected device before
|
||||
// service to connect with new device.
|
||||
return Promise.all(connectedDevices.map((connectedDevice) => {
|
||||
return this.disconnect(connectedDevice);
|
||||
})).then(() => {
|
||||
// All connected devices is disconnected.
|
||||
// We can start to connect the new request.
|
||||
debug('connect(): Start to connect with wanted device address = ' +
|
||||
device.address);
|
||||
return this._connect(device).then(() => {
|
||||
debug('connect(): Resolved');
|
||||
}, (reason) => {
|
||||
debug('connect(): reason = ' + reason);
|
||||
return Promise.reject(reason);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will connect the input device with the device's adapter.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothDevice} device
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_connect: function btcm__connect(device) {
|
||||
if (!this._defaultAdapter) {
|
||||
return Promise.reject('default adapter is not existed!!');
|
||||
}
|
||||
|
||||
// Save the connecting address
|
||||
this.connectingAddress = device.address;
|
||||
// Fire 'connecting' event.
|
||||
var event = {
|
||||
type: 'connecting',
|
||||
detail: {
|
||||
address: device.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
debug('_connect(): before start to connect, stop discovery first');
|
||||
|
||||
// Note on Bluedroid stack, discovery has to be stopped before connect
|
||||
// (i.e., call stopDiscovery() before connect())
|
||||
// otherwise stack callbacks with connect failure.
|
||||
return this._defaultAdapter.stopDiscovery().then(() => {
|
||||
debug('_connect(): start connecting device, ' +
|
||||
'address = ' + device.address);
|
||||
return this._defaultAdapter.connect(device).then(() => {
|
||||
debug('_connect(): resolve, already connected with address = ' +
|
||||
device.address);
|
||||
}, () => {
|
||||
// No available profiles are connected. Reset connecting address.
|
||||
this.connectingAddress = null;
|
||||
// Fire 'disconnected' event.
|
||||
event = {
|
||||
type: 'disconnected',
|
||||
detail: {
|
||||
address: device.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
debug('_connect(): reject with connection failed');
|
||||
return Promise.reject('connection failed');
|
||||
});
|
||||
}, () => {
|
||||
// Cannot connect with the device since stopDiscovery failed.
|
||||
this.connectingAddress = null;
|
||||
// Fire 'disconnected' event.
|
||||
event = {
|
||||
type: 'disconnected',
|
||||
detail: {
|
||||
address: device.address
|
||||
}
|
||||
};
|
||||
this._emitEvent(event);
|
||||
debug('_connect(): reject with stop discovery failed');
|
||||
return Promise.reject('stop discovery failed');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will disconnect the input device with the device's adapter.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {BluetoothDevice} device
|
||||
* @returns {Promise}
|
||||
*/
|
||||
disconnect: function btcm_disconnect(device) {
|
||||
if (!this._defaultAdapter) {
|
||||
return Promise.reject('default adapter is not existed!!');
|
||||
}
|
||||
|
||||
return this._defaultAdapter.disconnect(device).then(() => {
|
||||
debug('disconnect(): onsuccess(): resolve');
|
||||
}, () => {
|
||||
debug('disconnect(): onerror(): reject with disconnect failed');
|
||||
return Promise.reject('disconnect failed');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will get all connected devices profiles
|
||||
* which we are interested in. Profile: HFP, A2DP.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @returns {Promise} resolve: connectedDevices
|
||||
* @returns {Promise} reject: reason
|
||||
*/
|
||||
_getConnectedDevicesFromPlatform:
|
||||
function btcm__getConnectedDevicesFromPlatform() {
|
||||
// Get connected device via profiles HFP, A2DP
|
||||
return Promise.all(Object.keys(this.Profiles).map((profile) => {
|
||||
return this._getConnectedDevicesByProfile(this.Profiles[profile]);
|
||||
})).then((connectedDevices) => {
|
||||
// Update each connected devices in map.
|
||||
var collectedConnectedDevicesByProfile = {};
|
||||
Object.keys(this.Profiles).forEach((profile, index) => {
|
||||
collectedConnectedDevicesByProfile[profile] = connectedDevices[index];
|
||||
});
|
||||
debug('_getConnectedDevicesFromPlatform(): ' +
|
||||
'collectedConnectedDevicesByProfile = ' +
|
||||
JSON.stringify(collectedConnectedDevicesByProfile));
|
||||
return Promise.resolve(collectedConnectedDevicesByProfile);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will get connected device by inputed profile.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {String} profileID
|
||||
* @returns {Promise} resolve: the connected devices in array
|
||||
* @returns {Promise} reject: reason
|
||||
*/
|
||||
_getConnectedDevicesByProfile:
|
||||
function btcm__getConnectedDevicesByProfile(profileID) {
|
||||
if (!this._defaultAdapter) {
|
||||
debug('_getConnectedDevicesByProfile(): reject with no adapter');
|
||||
return Promise.reject('default adapter is not existed!!');
|
||||
}
|
||||
|
||||
if (this._defaultAdapter.state === 'disabled') {
|
||||
debug('_getConnectedDevicesByProfile(): resolve with empty array ' +
|
||||
'since it is impossible to connect with any device');
|
||||
return Promise.reject('getConnectedDevices in disabled state');
|
||||
}
|
||||
|
||||
return this._defaultAdapter.getConnectedDevices(profileID).then(
|
||||
(connectedDevice) => {
|
||||
debug('_getConnectedDevicesByProfile(): resolved with ' +
|
||||
'connectedDevice = ' + JSON.stringify(connectedDevice));
|
||||
return Promise.resolve(connectedDevice || []);
|
||||
}, (reason) => {
|
||||
debug('_getConnectedDevicesByProfile(): rejected with ' +
|
||||
'reason = ' + reason);
|
||||
return Promise.reject(reason);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get device from paired devices by address.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {String} address
|
||||
*/
|
||||
_getPairedDeviceByAddress:
|
||||
function btcm__getPairedDeviceByAddress(address) {
|
||||
if (!this._defaultAdapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var pairedDevices = this._defaultAdapter.getPairedDevices();
|
||||
if (pairedDevices.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
for (var i in pairedDevices) {
|
||||
if (pairedDevices[i].address === address) {
|
||||
return pairedDevices[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find out there is any profile still connected.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {Object} options
|
||||
* @param {String} options.connectedDevices - connected devices info
|
||||
* @param {String} options.address - the address of device
|
||||
*/
|
||||
_hasConnectedProfileByAddress:
|
||||
function btcm__hasConnectedProfileByAddress(options) {
|
||||
if (!options.connectedDevices[options.address]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasConnectedProfile = false;
|
||||
for (var profileID in this.Profiles) {
|
||||
var connectedProfiles =
|
||||
options.connectedDevices[options.address].connectedProfiles;
|
||||
if (connectedProfiles && (connectedProfiles[profileID] === true)) {
|
||||
hasConnectedProfile = true;
|
||||
}
|
||||
}
|
||||
return hasConnectedProfile;
|
||||
},
|
||||
|
||||
/**
|
||||
* A function to emit event to each registered listener by the input type.
|
||||
*
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {Object} options
|
||||
* @param {String} options.type - type of event name
|
||||
* @param {Object} options.detail - the object pass to the listener
|
||||
*/
|
||||
_emitEvent: function btcm__emitEvent(options) {
|
||||
this._listeners[options.type].forEach(function(listener) {
|
||||
listener(options);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will provide event listener for outer modules to regist.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
addEventListener: function btcm_addEventListener(eventName, callback) {
|
||||
if (callback && (this._listeners.hasOwnProperty(eventName))) {
|
||||
this._listeners[eventName].push(callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The method will provide event listener for outer modules to un-regist.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothConnectionManager
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
removeEventListener:
|
||||
function btcm_removeEventListener(eventName, callback) {
|
||||
if (callback && (this._listeners.hasOwnProperty(eventName))) {
|
||||
var index = this._listeners[eventName].indexOf(callback);
|
||||
if (index >= 0) {
|
||||
this._listeners[eventName].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BluetoothConnectionManager._init();
|
||||
return BluetoothConnectionManager;
|
||||
});
|
||||
1043
gaia/apps/settings/js/modules/bluetooth/bluetooth_context.js
Executable file
1043
gaia/apps/settings/js/modules/bluetooth/bluetooth_context.js
Executable file
File diff suppressed because it is too large
Load diff
158
gaia/apps/settings/js/modules/bluetooth/bluetooth_device.js
Executable file
158
gaia/apps/settings/js/modules/bluetooth/bluetooth_device.js
Executable file
|
|
@ -0,0 +1,158 @@
|
|||
/**
|
||||
* BluetoothDevice:
|
||||
* - BluetoothDevice is an Observable that wraps the platform
|
||||
* BluetoothDevice object.
|
||||
* - It has some observable properties: name, paired, cod
|
||||
* BluetoothDevice only update device information and does not involve in any
|
||||
* UI logic.
|
||||
*
|
||||
* @module BluetoothDevice
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var BtClassOfDeviceMapper = require('modules/bluetooth/bluetooth_cod_mapper');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
var _debug = false;
|
||||
var debug = function() {};
|
||||
if (_debug) {
|
||||
debug = function btd_debug(msg) {
|
||||
console.log('--> [BluetoothDevice]: ' + msg);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a function to update connection info.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothDevice
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.connectionStatus - is connected or not
|
||||
* @param {Object} options.profiles - profiles of the connection type
|
||||
*/
|
||||
var updateConnectionInfo = function(options) {
|
||||
debug('updateConnectionInfo(): this.address = ' + this.address +
|
||||
', this.name = ' + this.name +
|
||||
', options = ' + JSON.stringify(options));
|
||||
if (options.connectionStatus) {
|
||||
this.connectionStatus = options.connectionStatus;
|
||||
}
|
||||
|
||||
if (options.profiles) {
|
||||
this.profiles = Object.create(options.profiles);
|
||||
}
|
||||
|
||||
debug('updateConnectionInfo(): this.connectionStatus = ' +
|
||||
this.connectionStatus + ', this.profiles = ' +
|
||||
JSON.stringify(this.profiles));
|
||||
};
|
||||
|
||||
/**
|
||||
* Provide a function to update description text.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf BluetoothDevice
|
||||
*/
|
||||
var updateDescriptionText = function() {
|
||||
debug('updateDescriptionText():');
|
||||
// Define description for found device.
|
||||
if (this.paired === false) {
|
||||
this.descriptionText = 'tapToConnect';
|
||||
} else if (this.paired === 'pairing') {
|
||||
this.descriptionText = 'pairing';
|
||||
} else if (this.paired === true) {
|
||||
this.descriptionText = 'paired';
|
||||
// Define description for paired device.
|
||||
if (this.connectionStatus === 'connecting') {
|
||||
this.descriptionText = 'connecting';
|
||||
} else if (this.connectionStatus === 'connected') {
|
||||
if (this.profiles) {
|
||||
var hfpConnected = this.profiles.hfp;
|
||||
var a2dpConnected = this.profiles.a2dp;
|
||||
if (hfpConnected && a2dpConnected) {
|
||||
this.descriptionText = 'connectedWithDeviceMedia';
|
||||
} else if (hfpConnected) {
|
||||
this.descriptionText = 'connectedWithDevice';
|
||||
} else if (a2dpConnected) {
|
||||
this.descriptionText = 'connectedWithMedia';
|
||||
} else {
|
||||
this.descriptionText = 'connectedWithNoProfileInfo';
|
||||
}
|
||||
} else {
|
||||
this.descriptionText = 'connectedWithNoProfileInfo';
|
||||
}
|
||||
} else if (this.connectionStatus === 'disconnected') {
|
||||
this.descriptionText = 'disconnected';
|
||||
}
|
||||
}
|
||||
debug('updateDescriptionText(): this.descriptionText = ' +
|
||||
this.descriptionText);
|
||||
};
|
||||
|
||||
/**
|
||||
* @class BluetoothDevice
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @param {BluetoothDevice} device
|
||||
* @return {Observable} observableBluetoothDevice
|
||||
*/
|
||||
return function ctor_bluetooth_device(device) {
|
||||
var type = BtClassOfDeviceMapper.getDeviceType(device.cod);
|
||||
var connectionStatus = device.connectionStatus || 'disconnected';
|
||||
var observableBluetoothDevice = Observable({
|
||||
name: device.name,
|
||||
paired: device.paired,
|
||||
address: device.address,
|
||||
type: type,
|
||||
connectionStatus: connectionStatus,
|
||||
profiles: null,
|
||||
descriptionText: '',
|
||||
get data() {return device;},
|
||||
updateConnectionInfo: updateConnectionInfo,
|
||||
updateDescriptionText: updateDescriptionText
|
||||
});
|
||||
|
||||
/**
|
||||
* Observe 'paired', 'connectionStatus', and 'profiles' properties changed
|
||||
* event in init function. Once these properties changed, we can update
|
||||
* corrected description for the device.
|
||||
*/
|
||||
observableBluetoothDevice._init = function btd__init() {
|
||||
this.observe('paired', this.updateDescriptionText.bind(this));
|
||||
this.observe('connectionStatus', this.updateDescriptionText.bind(this));
|
||||
this.observe('profiles', this.updateDescriptionText.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* watch 'onattributechanged' event
|
||||
* A function to receive device which is just found via discovery handler.
|
||||
* And set a function to handle 'onattributechanged' event.
|
||||
*/
|
||||
device.onattributechanged = function btd_onDeviceAttributeChanged(evt) {
|
||||
for (var i in evt.attrs) {
|
||||
debug('onDeviceAttributeChanged(): ' + evt.attrs[i]);
|
||||
switch (evt.attrs[i]) {
|
||||
case 'name':
|
||||
observableBluetoothDevice.name = device.name;
|
||||
break;
|
||||
case 'paired':
|
||||
debug('onDeviceAttributeChanged(): ' +
|
||||
'device.paired = ' + device.paired);
|
||||
observableBluetoothDevice.paired = device.paired;
|
||||
break;
|
||||
case 'cod':
|
||||
debug('onDeviceAttributeChanged(): ' +
|
||||
'device.cod = ' + device.cod);
|
||||
observableBluetoothDevice.type =
|
||||
BtClassOfDeviceMapper.getDeviceType(device.cod);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
observableBluetoothDevice._init();
|
||||
return observableBluetoothDevice;
|
||||
};
|
||||
});
|
||||
36
gaia/apps/settings/js/modules/customized_network_type_map.js
Executable file
36
gaia/apps/settings/js/modules/customized_network_type_map.js
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
const KEY = 'operatorResources.data.icon';
|
||||
|
||||
var initialized = false;
|
||||
var map = {};
|
||||
|
||||
function getCustomizedNetworkTypeMap() {
|
||||
if (initialized) {
|
||||
return Promise.resolve(map);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
var req = navigator.mozSettings.createLock().get(KEY);
|
||||
req.onsuccess = function() {
|
||||
initialized = true;
|
||||
var result = req.result[KEY];
|
||||
if (result && Object.keys(result).length) {
|
||||
map = Object.create(result);
|
||||
}
|
||||
resolve(map);
|
||||
};
|
||||
req.onerror = function() {
|
||||
initialized = true;
|
||||
console.error('Error loading ' + KEY + ' settings. ' +
|
||||
req.error && req.error.name);
|
||||
resolve(map);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
get: getCustomizedNetworkTypeMap
|
||||
});
|
||||
});
|
||||
382
gaia/apps/settings/js/modules/date_time.js
Executable file
382
gaia/apps/settings/js/modules/date_time.js
Executable file
|
|
@ -0,0 +1,382 @@
|
|||
/**
|
||||
* DateTime is a singleton that caches date, time, timezone values for
|
||||
* Date & Time panel fast access
|
||||
*
|
||||
* @module DateTime
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
var settings = window.navigator.mozSettings;
|
||||
|
||||
// keys
|
||||
var _kClockAutoEnabled = 'time.clock.automatic-update.enabled';
|
||||
var _kTimezoneAutoEnabled = 'time.timezone.automatic-update.enabled';
|
||||
var _kClockAutoAvailable = 'time.clock.automatic-update.available';
|
||||
var _kTimezoneAutoAvailable = 'time.timezone.automatic-update.available';
|
||||
var _kTimezone = 'time.timezone';
|
||||
var _kUserSelected = 'time.timezone.user-selected';
|
||||
var _kLocaleTime = 'locale.hour12';
|
||||
|
||||
// handler
|
||||
var _updateDateTimeout = null;
|
||||
var _updateTimeTimeout = null;
|
||||
|
||||
var DateTime = Module.create(function DateTime() {
|
||||
this.super(Observable).call(this);
|
||||
this._init();
|
||||
}).extend(Observable);
|
||||
|
||||
|
||||
// public observers
|
||||
/**
|
||||
* Auto setting Date & Time.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype, 'clockAutoEnabled', {
|
||||
readonly: true,
|
||||
value: true
|
||||
});
|
||||
|
||||
/**
|
||||
* SIM provide Auto clock function.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype,
|
||||
'clockAutoAvailable', {
|
||||
readonly: true,
|
||||
value: true
|
||||
});
|
||||
|
||||
/**
|
||||
* SIM provide Auto timezone function.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype,
|
||||
'timezoneAutoAvailable', {
|
||||
readonly: true,
|
||||
value: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Current Timezone.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype, 'timezone', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* User Selected Timezone.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype,
|
||||
'userSelectedTimezone', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* Current Date string.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype, 'date', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* Localized current time string.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {String}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype, 'time', {
|
||||
readonly: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* Current AM/PM or 24 state.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Observable.defineObservableProperty(DateTime.prototype, 'currentHour12', {
|
||||
readonly: true,
|
||||
value: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Init DateTime module.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype._init = function() {
|
||||
this._mozTime = window.navigator.mozTime;
|
||||
if (!this._mozTime) {
|
||||
console.error('Could not get window.navigator.mozTime');
|
||||
return;
|
||||
}
|
||||
|
||||
this._boundSetTimeAutoEnabled = function(event) {
|
||||
this._clockAutoEnabled = event.settingValue;
|
||||
// also set timezoneAutoEnabled
|
||||
this._setTimezoneAutoEnabled(event.settingValue);
|
||||
}.bind(this);
|
||||
this._boundSetClockAutoAvailable = function(event) {
|
||||
this._clockAutoAvailable = event.settingValue;
|
||||
}.bind(this);
|
||||
this._boundSetTimezoneAutoAvailable = function(event) {
|
||||
this._timezoneAutoAvailable = event.settingValue;
|
||||
}.bind(this);
|
||||
this._boundSetTimezone = function(event) {
|
||||
this._timezone = event.settingValue;
|
||||
}.bind(this);
|
||||
this._boundUserSelectedTimezone = function(event) {
|
||||
this._userSelectedTimezone = event.settingValue;
|
||||
}.bind(this);
|
||||
this._boundCurrentHour12 = function(event) {
|
||||
this._currentHour12 = event.settingValue;
|
||||
|
||||
this._autoUpdateDateTime();
|
||||
}.bind(this);
|
||||
|
||||
this._getDefaults();
|
||||
this._attachListeners();
|
||||
};
|
||||
|
||||
DateTime.prototype._attachListeners = function() {
|
||||
window.navigator.mozSettings.addObserver(_kClockAutoEnabled,
|
||||
this._boundSetTimeAutoEnabled);
|
||||
window.navigator.mozSettings.addObserver(_kClockAutoAvailable,
|
||||
this._boundSetClockAutoAvailable);
|
||||
window.navigator.mozSettings.addObserver(_kTimezoneAutoAvailable,
|
||||
this._boundSetTimezoneAutoAvailable);
|
||||
window.navigator.mozSettings.addObserver(_kTimezone,
|
||||
this._boundSetTimezone);
|
||||
window.navigator.mozSettings.addObserver(_kUserSelected,
|
||||
this._boundUserSelectedTimezone);
|
||||
window.navigator.mozSettings.addObserver(_kLocaleTime,
|
||||
this._boundCurrentHour12);
|
||||
// Listen to 'DOMRetranslated' to get the latest l10n resource.
|
||||
document.addEventListener('DOMRetranslated', this);
|
||||
// Listen to 'moztimechange' to update clock
|
||||
window.addEventListener('moztimechange', this);
|
||||
};
|
||||
|
||||
DateTime.prototype.handleEvent = function(evt) {
|
||||
switch (evt.type) {
|
||||
case 'moztimechange':
|
||||
this._autoUpdateDateTime();
|
||||
break;
|
||||
case 'DOMRetranslated':
|
||||
var d = new Date();
|
||||
this._date = this._formatDate(d);
|
||||
this._time = this._formatTime(d);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* fill in default values to public observers
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype._getDefaults = function() {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
this._clockAutoEnabled = results[_kClockAutoEnabled];
|
||||
this._clockAutoAvailable = results[_kClockAutoAvailable];
|
||||
this._timezoneAutoAvailable = results[_kTimezoneAutoAvailable];
|
||||
this._timezone = results[_kTimezone];
|
||||
this._currentHour12 = results[_kLocaleTime];
|
||||
// render date/time after get proper format
|
||||
this._autoUpdateDateTime();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto update date and time
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype._autoUpdateDateTime = function() {
|
||||
window.clearTimeout(_updateDateTimeout);
|
||||
window.clearTimeout(_updateTimeTimeout);
|
||||
this._autoUpdateDate();
|
||||
this._autoUpdateTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Date periodically.
|
||||
*
|
||||
* DONT call this function directly,
|
||||
* call _autoUpdateDateTime instead.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype._autoUpdateDate = function() {
|
||||
var d = new Date();
|
||||
this._date = this._formatDate(d);
|
||||
|
||||
var remainMillisecond = (24 - d.getHours()) * 3600 * 1000 -
|
||||
d.getMinutes() * 60 * 1000 - d.getMilliseconds();
|
||||
_updateDateTimeout = window.setTimeout(
|
||||
function updateDateTimeout() {
|
||||
this._autoUpdateDate();
|
||||
}.bind(this), remainMillisecond);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Time periodically.
|
||||
*
|
||||
* DONT call this function directly,
|
||||
* call _autoUpdateDateTime instead.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype._autoUpdateTime = function() {
|
||||
var d = new Date();
|
||||
this._time = this._formatTime(d);
|
||||
|
||||
var remainMillisecond = (60 - d.getSeconds()) * 1000;
|
||||
_updateTimeTimeout = window.setTimeout(
|
||||
function updateTimeTimeout() {
|
||||
this._autoUpdateTime();
|
||||
}.bind(this), remainMillisecond);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return preffered date format.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
* @param {Date} input as Date
|
||||
* @returns {String}
|
||||
*/
|
||||
DateTime.prototype._formatDate = function(d) {
|
||||
return d.toLocaleString(navigator.languages, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return preffered time format.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
* @param {Date} input as Date
|
||||
* @returns {String}
|
||||
*/
|
||||
DateTime.prototype._formatTime = function(d) {
|
||||
return d.toLocaleString(navigator.languages, {
|
||||
hour12: navigator.mozHour12,
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change system time
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype.setTime = function(type, value) {
|
||||
var newDate = new Date();
|
||||
var tokens;
|
||||
|
||||
switch (type) {
|
||||
case 'date':
|
||||
tokens = value.split('-');
|
||||
newDate.setYear(tokens[0]);
|
||||
newDate.setMonth(tokens[1] - 1);
|
||||
newDate.setDate(tokens[2]);
|
||||
break;
|
||||
|
||||
case 'time':
|
||||
tokens = value.split(':');
|
||||
newDate.setHours(parseInt(tokens[0]));
|
||||
newDate.setMinutes(parseInt(tokens[1]));
|
||||
break;
|
||||
}
|
||||
this._mozTime.set(newDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change TimezoneAutoEnabled settings.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype._setTimezoneAutoEnabled = function(enabled) {
|
||||
var cset = {};
|
||||
cset[_kTimezoneAutoEnabled] = enabled;
|
||||
settings.createLock().set(cset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Change UserSelectedTimezone settings.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype.setUserSelectedTimezone = function(selected) {
|
||||
var cset = {};
|
||||
cset[_kTimezone] = selected;
|
||||
settings.createLock().set(cset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change hour12 settings.
|
||||
* this valie is used to determine 12 or 24 time format.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf DateTime
|
||||
*/
|
||||
DateTime.prototype.setCurrentHour12 = function(selected) {
|
||||
var cset = {};
|
||||
cset[_kLocaleTime] = selected;
|
||||
settings.createLock().set(cset);
|
||||
};
|
||||
|
||||
DateTime.prototype.getHour12ForCurrentLocale = function() {
|
||||
var format = new Intl.DateTimeFormat(navigator.languages, {
|
||||
hour: 'numeric'
|
||||
});
|
||||
return format.resolvedOptions().hour12;
|
||||
};
|
||||
|
||||
return DateTime();
|
||||
});
|
||||
29
gaia/apps/settings/js/modules/defer.js
Executable file
29
gaia/apps/settings/js/modules/defer.js
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Defer is a factory that would create a wrapped Promise object with exposed
|
||||
* resolve / reject API so that we can easily resolve / reject promise from
|
||||
* outside.
|
||||
*
|
||||
* This is mostly useful when you are going to queue promises for later use.
|
||||
*
|
||||
* API:
|
||||
*
|
||||
* var defer = Defer();
|
||||
* defer.resolve('return value');
|
||||
* defer.reject('reason')
|
||||
*
|
||||
* @module Defer
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Defer = function() {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
};
|
||||
|
||||
return function ctor_defer() {
|
||||
return new Defer();
|
||||
};
|
||||
});
|
||||
26
gaia/apps/settings/js/modules/dialog/alert_dialog.js
Executable file
26
gaia/apps/settings/js/modules/dialog/alert_dialog.js
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var BaseDialog = require('modules/dialog/base_dialog');
|
||||
|
||||
var AlertDialog = function(panelDOM, options) {
|
||||
BaseDialog.call(this, panelDOM, options);
|
||||
};
|
||||
|
||||
AlertDialog.prototype = Object.create(BaseDialog.prototype);
|
||||
AlertDialog.prototype.constructor = AlertDialog;
|
||||
AlertDialog.prototype.DIALOG_CLASS = 'alert-dialog';
|
||||
AlertDialog.prototype.TRANSITION_CLASS = 'fade';
|
||||
|
||||
AlertDialog.prototype.bindEvents = function() {
|
||||
var self = this;
|
||||
|
||||
this.getSubmitButton().onclick = function() {
|
||||
self._options.onWrapSubmit();
|
||||
};
|
||||
};
|
||||
|
||||
return function ctor_alertDialog(panelDOM, options) {
|
||||
return new AlertDialog(panelDOM, options);
|
||||
};
|
||||
});
|
||||
124
gaia/apps/settings/js/modules/dialog/base_dialog.js
Executable file
124
gaia/apps/settings/js/modules/dialog/base_dialog.js
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var BaseDialog = function(panelDOM, options) {
|
||||
this.panel = panelDOM;
|
||||
this._options = options || {};
|
||||
};
|
||||
|
||||
BaseDialog.prototype.DIALOG_CLASS = 'dialog';
|
||||
BaseDialog.prototype.TRANSITION_CLASS = 'fade';
|
||||
BaseDialog.prototype.SUBMIT_BUTTON_SELECTOR = '[type="submit"]';
|
||||
BaseDialog.prototype.CANCEL_BUTTON_SELECTOR = '[type="reset"]';
|
||||
BaseDialog.prototype.MESSAGE_SELECTOR = '.settings-dialog-message';
|
||||
BaseDialog.prototype.TITLE_SELECTOR = '.settings-dialog-title';
|
||||
|
||||
BaseDialog.prototype.init = function bd_init() {
|
||||
// We can override animation class from options
|
||||
this.TRANSITION_CLASS = this._options.transition || this.TRANSITION_CLASS;
|
||||
this.panel.classList.add(this.DIALOG_CLASS);
|
||||
this.panel.classList.add(this.TRANSITION_CLASS);
|
||||
};
|
||||
|
||||
BaseDialog.prototype.initUI = function bd_initUI() {
|
||||
var message = this._options.message;
|
||||
var title = this._options.title;
|
||||
var submitButton = this._options.submitButton;
|
||||
var cancelButton = this._options.cancelButton;
|
||||
|
||||
this._updateMessage(message);
|
||||
this._updateTitle(title);
|
||||
this._updateSubmitButton(submitButton);
|
||||
this._updateCancelButton(cancelButton);
|
||||
};
|
||||
|
||||
BaseDialog.prototype.bindEvents = function bd_bindEvent() {
|
||||
var self = this;
|
||||
|
||||
this.getSubmitButton().onclick = function() {
|
||||
self._options.onWrapSubmit();
|
||||
};
|
||||
|
||||
this.getCancelButton().onclick = function() {
|
||||
self._options.onWrapCancel();
|
||||
};
|
||||
};
|
||||
|
||||
BaseDialog.prototype._updateMessage = function bd__updateMessage(message) {
|
||||
var messageDOM = this.panel.querySelector(this.MESSAGE_SELECTOR);
|
||||
if (messageDOM && message) {
|
||||
message = this._getWrapL10nObject(message);
|
||||
document.l10n.setAttributes(messageDOM, message.id, message.args);
|
||||
}
|
||||
};
|
||||
|
||||
BaseDialog.prototype._updateTitle = function bd__updateTitle(title) {
|
||||
var titleDOM = this.panel.querySelector(this.TITLE_SELECTOR);
|
||||
if (titleDOM && title) {
|
||||
title = this._getWrapL10nObject(title);
|
||||
document.l10n.setAttributes(titleDOM, title.id, title.args);
|
||||
}
|
||||
};
|
||||
|
||||
BaseDialog.prototype._updateSubmitButton = function bd__update(options) {
|
||||
var buttonDOM = this.getSubmitButton();
|
||||
if (buttonDOM && options) {
|
||||
options = this._getWrapL10nObject(options);
|
||||
document.l10n.setAttributes(buttonDOM, options.id, options.args);
|
||||
buttonDOM.className = options.style || 'recommend';
|
||||
}
|
||||
};
|
||||
|
||||
BaseDialog.prototype._updateCancelButton = function bd__updateText(options) {
|
||||
var buttonDOM = this.getCancelButton();
|
||||
if (buttonDOM && options) {
|
||||
options = this._getWrapL10nObject(options);
|
||||
document.l10n.setAttributes(buttonDOM, options.id, options.args);
|
||||
buttonDOM.className = options.style || '';
|
||||
}
|
||||
};
|
||||
|
||||
BaseDialog.prototype._getWrapL10nObject =
|
||||
function bd__getWrapL10nObject(input) {
|
||||
if (typeof input === 'string') {
|
||||
return {id: input, args: null};
|
||||
} else if (typeof input === 'object') {
|
||||
if (typeof input.id === 'undefined') {
|
||||
throw new Error('You forgot to put l10nId - ' +
|
||||
JSON.stringify(input));
|
||||
} else {
|
||||
return {id: input.id, args: input.args || null, style: input.style};
|
||||
}
|
||||
} else {
|
||||
throw new Error('You are using the wrong L10nObject, ' +
|
||||
'please check its format again');
|
||||
}
|
||||
};
|
||||
|
||||
BaseDialog.prototype.getDOM = function bd_getDOM() {
|
||||
return this.panel;
|
||||
};
|
||||
|
||||
BaseDialog.prototype.getSubmitButton = function bd_getSubmitButton() {
|
||||
return this.panel.querySelector(this.SUBMIT_BUTTON_SELECTOR);
|
||||
};
|
||||
|
||||
BaseDialog.prototype.getCancelButton = function bd_getCancelButton() {
|
||||
return this.panel.querySelector(this.CANCEL_BUTTON_SELECTOR);
|
||||
};
|
||||
|
||||
BaseDialog.prototype.cleanup = function bd_cleanup() {
|
||||
// We only have to restore system-wise panels instead of custom panels
|
||||
if (this.DIALOG_CLASS !== 'panel-dialog') {
|
||||
this._updateTitle('settings-' + this.DIALOG_CLASS + '-header');
|
||||
this._updateSubmitButton('ok');
|
||||
this._updateCancelButton('cancel');
|
||||
}
|
||||
|
||||
// clear all added classes
|
||||
this.panel.classList.remove(this.DIALOG_CLASS);
|
||||
this.panel.classList.remove(this.TRANSITION_CLASS);
|
||||
};
|
||||
|
||||
return BaseDialog;
|
||||
});
|
||||
30
gaia/apps/settings/js/modules/dialog/confirm_dialog.js
Executable file
30
gaia/apps/settings/js/modules/dialog/confirm_dialog.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var BaseDialog = require('modules/dialog/base_dialog');
|
||||
|
||||
var ConfirmDialog = function(panelDOM, options) {
|
||||
BaseDialog.call(this, panelDOM, options);
|
||||
};
|
||||
|
||||
ConfirmDialog.prototype = Object.create(BaseDialog.prototype);
|
||||
ConfirmDialog.prototype.constructor = ConfirmDialog;
|
||||
ConfirmDialog.prototype.DIALOG_CLASS = 'confirm-dialog';
|
||||
ConfirmDialog.prototype.TRANSITION_CLASS = 'fade';
|
||||
|
||||
ConfirmDialog.prototype.bindEvents = function() {
|
||||
var self = this;
|
||||
|
||||
this.getSubmitButton().onclick = function() {
|
||||
self._options.onWrapSubmit();
|
||||
};
|
||||
|
||||
this.getCancelButton().onclick = function() {
|
||||
self._options.onWrapCancel();
|
||||
};
|
||||
};
|
||||
|
||||
return function ctor_confirmDialog(panelDOM, options) {
|
||||
return new ConfirmDialog(panelDOM, options);
|
||||
};
|
||||
});
|
||||
18
gaia/apps/settings/js/modules/dialog/panel_dialog.js
Executable file
18
gaia/apps/settings/js/modules/dialog/panel_dialog.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var BaseDialog = require('modules/dialog/base_dialog');
|
||||
|
||||
var PanelDialog = function(panelDOM, options) {
|
||||
BaseDialog.call(this, panelDOM, options);
|
||||
};
|
||||
|
||||
PanelDialog.prototype = Object.create(BaseDialog.prototype);
|
||||
PanelDialog.prototype.constructor = PanelDialog;
|
||||
PanelDialog.prototype.DIALOG_CLASS = 'panel-dialog';
|
||||
PanelDialog.prototype.TRANSITION_CLASS = 'fade';
|
||||
|
||||
return function ctor_PanelDialog(panelDOM, options) {
|
||||
return new PanelDialog(panelDOM, options);
|
||||
};
|
||||
});
|
||||
44
gaia/apps/settings/js/modules/dialog/prompt_dialog.js
Executable file
44
gaia/apps/settings/js/modules/dialog/prompt_dialog.js
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var BaseDialog = require('modules/dialog/base_dialog');
|
||||
|
||||
var PromptDialog = function(panelDOM, options) {
|
||||
BaseDialog.call(this, panelDOM, options);
|
||||
};
|
||||
|
||||
PromptDialog.prototype = Object.create(BaseDialog.prototype);
|
||||
PromptDialog.prototype.constructor = PromptDialog;
|
||||
PromptDialog.prototype.DIALOG_CLASS = 'prompt-dialog';
|
||||
PromptDialog.prototype.TRANSITION_CLASS = 'fade';
|
||||
PromptDialog.prototype.INPUT_SELECTOR = '.settings-dialog-input';
|
||||
|
||||
PromptDialog.prototype.bindEvents = function() {
|
||||
var self = this;
|
||||
|
||||
this.getSubmitButton().onclick = function() {
|
||||
self._options.onWrapSubmit();
|
||||
};
|
||||
|
||||
this.getCancelButton().onclick = function() {
|
||||
self._options.onWrapCancel();
|
||||
};
|
||||
};
|
||||
|
||||
PromptDialog.prototype.initUI = function() {
|
||||
BaseDialog.prototype.initUI.call(this);
|
||||
this.getInput().value = this._options.defaultValue || '';
|
||||
};
|
||||
|
||||
PromptDialog.prototype.getInput = function() {
|
||||
return this.panel.querySelector(this.INPUT_SELECTOR);
|
||||
};
|
||||
|
||||
PromptDialog.prototype.getResult = function() {
|
||||
return this.getInput().value;
|
||||
};
|
||||
|
||||
return function ctor_promptDialog(panelDOM, options) {
|
||||
return new PromptDialog(panelDOM, options);
|
||||
};
|
||||
});
|
||||
302
gaia/apps/settings/js/modules/dialog_manager.js
Executable file
302
gaia/apps/settings/js/modules/dialog_manager.js
Executable file
|
|
@ -0,0 +1,302 @@
|
|||
/**
|
||||
* DialogManager is a singleton that will help DialogService to load panels
|
||||
* and control any transitions on panels.
|
||||
*
|
||||
* API:
|
||||
*
|
||||
* DialogManager.open(dialog, options);
|
||||
* DialogManager.close(dialog, options);
|
||||
*
|
||||
* @module DialogManager
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var PanelCache = require('modules/panel_cache');
|
||||
var LazyLoader = require('shared/lazy_loader');
|
||||
|
||||
var DialogManager = function() {
|
||||
this.OVERLAY_SELECTOR = '.settings-dialog-overlay';
|
||||
|
||||
this._overlayDOM = document.querySelector(this.OVERLAY_SELECTOR);
|
||||
};
|
||||
|
||||
DialogManager.prototype = {
|
||||
/**
|
||||
* load panel based on passed in panelId
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {String} panelId
|
||||
* @return {Promise}
|
||||
*/
|
||||
_loadPanel: function dm__loadPanel(panelId) {
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
var panelElement = document.getElementById(panelId);
|
||||
if (panelElement.dataset.rendered) { // already initialized
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
panelElement.dataset.rendered = true;
|
||||
|
||||
// XXX remove SubPanel loader once sub panel are modulized
|
||||
if (panelElement.dataset.requireSubPanels) {
|
||||
// load the panel and its sub-panels (dependencies)
|
||||
// (load the main panel last because it contains the scripts)
|
||||
var selector = 'section[id^="' + panelElement.id + '-"]';
|
||||
var subPanels = document.querySelectorAll(selector);
|
||||
for (var i = 0, il = subPanels.length; i < il; i++) {
|
||||
LazyLoader.load([subPanels[i]]);
|
||||
}
|
||||
LazyLoader.load([panelElement], resolve);
|
||||
} else {
|
||||
LazyLoader.load([panelElement], resolve);
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* promised version of PanelCache.get()
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {String} panelId
|
||||
* @return {Promise}
|
||||
*/
|
||||
_getPanel: function dm__getPanel(panelId) {
|
||||
var promise = new Promise(function(resolve) {
|
||||
PanelCache.get(panelId, function(panel) {
|
||||
resolve(panel);
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* this is used to control visibility of overlayDOM
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {Boolean} show
|
||||
*/
|
||||
_showOverlay: function dm__showOverlay(show) {
|
||||
this._overlayDOM.hidden = !show;
|
||||
},
|
||||
|
||||
/**
|
||||
* It is used to control the timing of transitions so that we can make sure
|
||||
* whether animation is done or not.
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {String} method
|
||||
* @param {BaseDialog} dialog
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
_transit: function dm__transit(method, dialog, options) {
|
||||
var promise = new Promise(function(resolve) {
|
||||
var panel = dialog.panel;
|
||||
|
||||
panel.addEventListener('transitionend', function paintWait(evt) {
|
||||
if ((method === 'close' || method === 'open') &&
|
||||
evt.propertyName === 'visibility') {
|
||||
// After transition, we have to `hide` the panel, otherwise
|
||||
// the panel would still exist on the layer and would block
|
||||
// the scrolling event.
|
||||
if (method === 'close') {
|
||||
panel.hidden = true;
|
||||
}
|
||||
panel.removeEventListener('transitionend', paintWait);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Before transition, we have to `show` the panel, otherwise
|
||||
// the panel before applying transition class.
|
||||
if (method === 'open') {
|
||||
panel.hidden = false;
|
||||
}
|
||||
|
||||
// We need to apply class later otherwise Gecko can't apply
|
||||
// this transition and 150ms is an approximate number after doing
|
||||
// several rounds of manual tests.
|
||||
setTimeout(function() {
|
||||
if (method === 'open') {
|
||||
// Let's unhide the panel first
|
||||
panel.classList.add('current');
|
||||
} else {
|
||||
panel.classList.remove('current');
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do necessary works to open panel like loading panel, doing transition
|
||||
* and call related functions.
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {BaseDialog} dialog
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
_open: function dm__open(dialog, options) {
|
||||
var self = this;
|
||||
var foundPanel;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function() {
|
||||
// 1: load panel
|
||||
return self._loadPanel(dialog.panel.id);
|
||||
})
|
||||
.then(function() {
|
||||
// 2: Get that panel
|
||||
return self._getPanel(dialog.panel.id);
|
||||
})
|
||||
.then(function(panel) {
|
||||
// 3: call beforeShow
|
||||
foundPanel = panel;
|
||||
return foundPanel.beforeShow(dialog.panel, options);
|
||||
})
|
||||
.then(function() {
|
||||
// 4. UI stuffs + transition
|
||||
dialog.init();
|
||||
dialog.initUI();
|
||||
dialog.bindEvents();
|
||||
|
||||
if (dialog.TRANSITION_CLASS === 'zoom-in-80') {
|
||||
self._showOverlay(true);
|
||||
}
|
||||
|
||||
return self._transit('open', dialog, options);
|
||||
})
|
||||
.then(function() {
|
||||
// 5. show that panel as a dialog
|
||||
return foundPanel.show(dialog.panel, options);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Do necessary works to close panel like loading panel, doing transition
|
||||
* and call related functions.
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {BaseDialog} dialog
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
_close: function dm__close(dialog, options) {
|
||||
var self = this;
|
||||
var foundPanel;
|
||||
var cachedResult;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function() {
|
||||
// 1: Get that panel
|
||||
return self._getPanel(dialog.panel.id);
|
||||
})
|
||||
.then(function(panel) {
|
||||
// 2: Let's validate to see whether we can close this dialog or not.
|
||||
foundPanel = panel;
|
||||
|
||||
var promise;
|
||||
// custom dialog - onSubmit
|
||||
if (foundPanel.onSubmit && options._type === 'submit') {
|
||||
promise = foundPanel.onSubmit();
|
||||
// custom dialog - onCancel
|
||||
} else if (foundPanel.onCancel && options._type === 'cancel') {
|
||||
promise = foundPanel.onCancel();
|
||||
// if no onSubmit & onCancel, pass directly
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise;
|
||||
})
|
||||
.then(function(result) {
|
||||
cachedResult = result;
|
||||
|
||||
// 3: call beforeHide
|
||||
return foundPanel.beforeHide();
|
||||
})
|
||||
.then(function() {
|
||||
// 4. transition
|
||||
return self._transit('close', dialog, options);
|
||||
})
|
||||
.then(function() {
|
||||
// 5. call hide
|
||||
return foundPanel.hide();
|
||||
})
|
||||
.then(function() {
|
||||
// 6. Get result and cleanup dialog
|
||||
var result;
|
||||
|
||||
// for prompt dialog, we have to get its own result from input text.
|
||||
if (dialog.DIALOG_CLASS === 'prompt-dialog') {
|
||||
result = dialog.getResult();
|
||||
} else if (cachedResult) {
|
||||
result = cachedResult;
|
||||
}
|
||||
|
||||
dialog.cleanup();
|
||||
|
||||
if (dialog.TRANSITION_CLASS === 'zoom-in-80') {
|
||||
self._showOverlay(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* It is a bridge to call open or close function.
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access private
|
||||
* @param {String} method
|
||||
* @param {BaseDialog} dialog
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
_navigate: function dm__navigate(method, dialog, options) {
|
||||
method = (method === 'open') ? '_open' : '_close';
|
||||
return this[method](dialog, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* DialogService would use this exposed API to open dialog.
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access public
|
||||
* @param {BaseDialog} dialog
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
open: function dm_open(dialog, options) {
|
||||
return this._navigate('open', dialog, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* DialogService would use this exposed API to close dialog.
|
||||
*
|
||||
* @memberOf DialogManager
|
||||
* @access public
|
||||
* @param {BaseDialog} dialog
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
close: function dm_close(dialog, type, options) {
|
||||
options._type = type;
|
||||
return this._navigate('close', dialog, options);
|
||||
}
|
||||
};
|
||||
|
||||
var dialogManager = new DialogManager();
|
||||
return dialogManager;
|
||||
});
|
||||
52
gaia/apps/settings/js/modules/dialog_panel.js
Executable file
52
gaia/apps/settings/js/modules/dialog_panel.js
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Panel = require('modules/panel');
|
||||
|
||||
var DialogPanel = function ctor_dialogPanel(panelOptions) {
|
||||
var panel = Panel(panelOptions);
|
||||
|
||||
var _emptyFunc = function() {};
|
||||
var submitFunction = _emptyFunc;
|
||||
var cancelFunction = _emptyFunc;
|
||||
|
||||
// Because we need to bridge onWrapSubmit & onWrapCancel with panelOptions
|
||||
// together, we have to override panel.init() to make sure we can access
|
||||
// these API here.
|
||||
//
|
||||
// When init() is called, we will automatically make submitFunction &
|
||||
// cancelFunction's reference work and link to onWrapSubmit & onWrapCancel,
|
||||
// in this way, we can call `this.submit()` & `this.cancel()` in
|
||||
// panelOptions.
|
||||
var originalInit = panel.init;
|
||||
panel.init = function(panel, userOptions) {
|
||||
submitFunction = userOptions.onWrapSubmit.bind(userOptions);
|
||||
cancelFunction = userOptions.onWrapCancel.bind(userOptions);
|
||||
return originalInit.apply(panel, arguments);
|
||||
};
|
||||
|
||||
// We have to extend two more functions for it
|
||||
panel.onSubmit = panelOptions.onSubmit || _emptyFunc;
|
||||
panel.onCancel = panelOptions.onCancel || _emptyFunc;
|
||||
|
||||
// Let's rebind the scope on options
|
||||
panel.onSubmit = panel.onSubmit.bind(panelOptions);
|
||||
panel.onCancel = panel.onCancel.bind(panelOptions);
|
||||
|
||||
// Callee can use `this.submit()` to programmatically submit()
|
||||
// this dialog.
|
||||
panelOptions.submit = function() {
|
||||
submitFunction();
|
||||
};
|
||||
|
||||
// Callee can use `this.cancel()` to programmatically cancel()
|
||||
// this dialog.
|
||||
panelOptions.cancel = function() {
|
||||
cancelFunction();
|
||||
};
|
||||
|
||||
return panel;
|
||||
};
|
||||
|
||||
return DialogPanel;
|
||||
});
|
||||
256
gaia/apps/settings/js/modules/dialog_service.js
Executable file
256
gaia/apps/settings/js/modules/dialog_service.js
Executable file
|
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* DialogService is a singleton that provides few ways for you to show/hide
|
||||
* dialogs. Here, we predefined alert/confirm/prompt dialogs to replace
|
||||
* window.alert/window.confirm/window.prompt if you want any further controls
|
||||
* of animations and UI.
|
||||
*
|
||||
* And also, there is one more dialog called panelDialog that would be used
|
||||
* when you are going to show any predefined panel in dialog way.
|
||||
*
|
||||
* API:
|
||||
*
|
||||
* 1. Alert dialog
|
||||
*
|
||||
* DialogService.alert({
|
||||
* id: 'MessageId',
|
||||
* args: {}
|
||||
* }, {
|
||||
* title: { id: 'TitleId', args: {} }
|
||||
* })
|
||||
* .then(function(result) {
|
||||
* var type = result.type;
|
||||
* });
|
||||
*
|
||||
* NOTE:
|
||||
* If there is no args in locales, you can direclty pass l10nId without args.
|
||||
*
|
||||
* DialogService.alert('MessageId', {
|
||||
* title: 'TitleId'
|
||||
* })
|
||||
* .then(function(result) {
|
||||
* var type = result.type;
|
||||
* });
|
||||
*
|
||||
* 2. Confirm dialog
|
||||
*
|
||||
* DialogService.confirm({
|
||||
* id: 'MessageId',
|
||||
* args: {}
|
||||
* }, {
|
||||
* title: { id: 'TitleId', args: {} },
|
||||
* submitButton: { id: 'SubmitButtonId', args: {}, style: 'recommend' },
|
||||
* cancelButton: { id: 'CancelButtonId', args: {} }
|
||||
* })
|
||||
* .then(function(result) {
|
||||
* var type = result.type;
|
||||
* });
|
||||
*
|
||||
* 3. Prompt dialog
|
||||
*
|
||||
* DialogService.prompt({
|
||||
* id: 'MessageId',
|
||||
* args: {}
|
||||
* }, {
|
||||
* title: { id: 'TitleId', args: {} },
|
||||
* defaultValue: 'e.g. test@mozilla.com',
|
||||
* }).then(function(result) {
|
||||
* var type = result.type;
|
||||
* var value = result.value;
|
||||
* });
|
||||
*
|
||||
* 4. Panel dialog
|
||||
*
|
||||
* DialogService.show('screen-lcok', {
|
||||
* transition: 'zoom-in',
|
||||
* }).then(function(result) {
|
||||
* // type would be submit or cancel
|
||||
* var type = result.type;
|
||||
* var value = result.value;
|
||||
* });
|
||||
*
|
||||
* NOTES:
|
||||
* We support some customized options for each dialog, please check the API
|
||||
* below to know what you can customize !
|
||||
*
|
||||
* @module DialogService
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Settings = require('settings');
|
||||
var Defer = require('modules/defer');
|
||||
var DialogManager = require('modules/dialog_manager');
|
||||
|
||||
var PanelDialog = require('modules/dialog/panel_dialog');
|
||||
var AlertDialog = require('modules/dialog/alert_dialog');
|
||||
var ConfirmDialog = require('modules/dialog/confirm_dialog');
|
||||
var PromptDialog = require('modules/dialog/prompt_dialog');
|
||||
|
||||
var DialogService = function() {
|
||||
this._navigating = false;
|
||||
this._pendingRequests = [];
|
||||
this._settingsAlertDialogId = 'settings-alert-dialog';
|
||||
this._settingsBaseDialogId = 'settings-base-dialog';
|
||||
this._settingsConfirmDialogId = 'settings-confirm-dialog';
|
||||
this._settingsPromptDialogId = 'settings-prompt-dialog';
|
||||
};
|
||||
|
||||
DialogService.prototype = {
|
||||
/**
|
||||
* Alert dialog with more controls.
|
||||
*
|
||||
* @memberOf DialogService
|
||||
* @access public
|
||||
* @param {String} message
|
||||
* @param {Object} userOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
alert: function(message, userOptions) {
|
||||
var options = userOptions || {};
|
||||
return this.show(this._settingsAlertDialogId, {
|
||||
type: 'alert',
|
||||
message: message,
|
||||
title: options.title,
|
||||
submitButton: options.submitButton
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm dialog with more controls.
|
||||
*
|
||||
* @memberOf DialogService
|
||||
* @access public
|
||||
* @param {String} message
|
||||
* @param {Object} userOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
confirm: function(message, userOptions) {
|
||||
var options = userOptions || {};
|
||||
return this.show(this._settingsConfirmDialogId, {
|
||||
type: 'confirm',
|
||||
message: message,
|
||||
title: options.title,
|
||||
submitButton: options.submitButton,
|
||||
cancelButton: options.cancelButton
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompt dialog with more controls.
|
||||
*
|
||||
* @memberOf DialogService
|
||||
* @access public
|
||||
* @param {String} message
|
||||
* @param {Object} userOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
prompt: function(message, userOptions) {
|
||||
var options = userOptions || {};
|
||||
return this.show(this._settingsPromptDialogId, {
|
||||
type: 'prompt',
|
||||
message: message,
|
||||
title: options.title,
|
||||
defaultValue: options.defaultValue,
|
||||
submitButton: options.submitButton,
|
||||
cancelButton: options.cancelButton
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Panel dialog. If you are going to show any panel as a dialog,
|
||||
* you have to use this method to show them.
|
||||
*
|
||||
* @memberOf DialogService
|
||||
* @access public
|
||||
* @param {String} panelId
|
||||
* @param {Object} userOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
show: function dm_show(panelId, userOptions, _pendingDefer) {
|
||||
var self = this;
|
||||
var defer;
|
||||
var dialog;
|
||||
var dialogDOM = document.getElementById(panelId);
|
||||
var currentPanel = Settings.currentPanel;
|
||||
var options = userOptions || {};
|
||||
|
||||
if (_pendingDefer) {
|
||||
defer = _pendingDefer;
|
||||
} else {
|
||||
defer = Defer();
|
||||
}
|
||||
|
||||
if (this._navigating) {
|
||||
this._pendingRequests.push({
|
||||
defer: defer,
|
||||
panelId: panelId,
|
||||
userOptions: userOptions
|
||||
});
|
||||
} else {
|
||||
if ('#' + panelId === currentPanel) {
|
||||
defer.reject('You are showing the same panel #' + panelId);
|
||||
} else {
|
||||
options.onWrapSubmit = function() {
|
||||
DialogManager.close(dialog, 'submit', options)
|
||||
.then(function(result) {
|
||||
defer.resolve({
|
||||
type: 'submit',
|
||||
value: result
|
||||
});
|
||||
self._navigating = false;
|
||||
self._execPendingRequest();
|
||||
});
|
||||
};
|
||||
|
||||
options.onWrapCancel = function() {
|
||||
DialogManager.close(dialog, 'cancel', options)
|
||||
.then(function(result) {
|
||||
defer.resolve({
|
||||
type: 'cancel',
|
||||
value: result
|
||||
});
|
||||
self._navigating = false;
|
||||
self._execPendingRequest();
|
||||
});
|
||||
};
|
||||
|
||||
switch (options.type) {
|
||||
case 'alert':
|
||||
dialog = AlertDialog(dialogDOM, options);
|
||||
break;
|
||||
case 'confirm':
|
||||
dialog = ConfirmDialog(dialogDOM, options);
|
||||
break;
|
||||
case 'prompt':
|
||||
dialog = PromptDialog(dialogDOM, options);
|
||||
break;
|
||||
default:
|
||||
dialog = PanelDialog(dialogDOM, options);
|
||||
break;
|
||||
}
|
||||
this._navigating = true;
|
||||
DialogManager.open(dialog, options);
|
||||
}
|
||||
}
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method can help us pop up any pending request and would try to
|
||||
* show it after previous request was done.
|
||||
*
|
||||
* @memberOf DialogService
|
||||
* @access private
|
||||
*/
|
||||
_execPendingRequest: function() {
|
||||
var request = this._pendingRequests.pop();
|
||||
if (request) {
|
||||
this.show(request.panelId, request.userOptions, request.defer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var dialogService = new DialogService();
|
||||
return dialogService;
|
||||
});
|
||||
123
gaia/apps/settings/js/modules/fdn_context.js
Executable file
123
gaia/apps/settings/js/modules/fdn_context.js
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* FdnContext is a module that you can easily fetch out FDN related info.
|
||||
*
|
||||
* @module FdnContext
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
var FdnContext = {
|
||||
/**
|
||||
* cached fdnContacts
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
_fdnContacts: [],
|
||||
|
||||
/**
|
||||
* internal function to help us clone object
|
||||
*
|
||||
* @param {Object} obj
|
||||
*/
|
||||
_cloneObject: function(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
|
||||
/**
|
||||
* we can use this method to get wrapped contacts information.
|
||||
*
|
||||
* @param {Number} cardIndex
|
||||
* @type {Function}
|
||||
* @return {Promise}
|
||||
*/
|
||||
getContacts: function(cardIndex) {
|
||||
var promise = new Promise((resolve, reject) => {
|
||||
var iccId = navigator.mozMobileConnections[cardIndex].iccId;
|
||||
|
||||
if (!iccId) {
|
||||
console.log('can\'t get right iccId');
|
||||
reject();
|
||||
}
|
||||
|
||||
var icc = navigator.mozIccManager.getIccById(iccId);
|
||||
if (!icc) {
|
||||
console.log('Could not retrieve ICC object');
|
||||
reject();
|
||||
}
|
||||
|
||||
var request = icc.readContacts('fdn');
|
||||
|
||||
request.onerror = (error) => {
|
||||
console.log('we got error when reading contacts from icc');
|
||||
console.log(error);
|
||||
reject();
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
var result = this._fdnContacts[cardIndex] = request.result;
|
||||
var contacts = [];
|
||||
for (var i = 0, l = result.length; i < l; i++) {
|
||||
contacts.push({
|
||||
id: i,
|
||||
name: result[i].name || '',
|
||||
number: result[i].tel[0].value || ''
|
||||
});
|
||||
}
|
||||
resolve(contacts);
|
||||
};
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This function returns a FDN contact object matching the
|
||||
* requested action.
|
||||
*
|
||||
* mozIccManager.updateContact works like this:
|
||||
* - no id: create a new contact
|
||||
* - existing id + name and number: update a contact
|
||||
* - existing id + empty name and number: remove a contact
|
||||
*
|
||||
* @type {Function}
|
||||
* @param {String} action
|
||||
* @param {Number} options.cardIndex
|
||||
* @param {Object} options.contact
|
||||
* @return {mozContact}
|
||||
*/
|
||||
createAction: function(action, options) {
|
||||
var simContact = {};
|
||||
var cardIndex = options.cardIndex;
|
||||
var contact = options.contact;
|
||||
switch (action) {
|
||||
case 'add':
|
||||
simContact.name = [contact.name];
|
||||
simContact.tel = [{
|
||||
value: contact.number
|
||||
}];
|
||||
break;
|
||||
case 'edit':
|
||||
simContact =
|
||||
this._cloneObject(this._fdnContacts[cardIndex][contact.id]);
|
||||
simContact.name[0] = contact.name;
|
||||
simContact.tel[0].value = contact.number;
|
||||
break;
|
||||
case 'remove':
|
||||
simContact =
|
||||
this._cloneObject(this._fdnContacts[cardIndex][contact.id]);
|
||||
simContact.name[0] = '';
|
||||
simContact.tel[0].value = '';
|
||||
break;
|
||||
}
|
||||
|
||||
var result = new window.mozContact(simContact);
|
||||
if ('id' in simContact) {
|
||||
result.id = simContact.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
return FdnContext;
|
||||
});
|
||||
301
gaia/apps/settings/js/modules/keyboard_context.js
Executable file
301
gaia/apps/settings/js/modules/keyboard_context.js
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* KeyboardContext provides installed keyboard apps and enabled keyboard layouts
|
||||
* in terms of ObservableArrays. It listens to the events from KeyboardHelper
|
||||
* and update the ObservableArrays.
|
||||
* KeyboardHelper helps on the following things:
|
||||
* - Get all installed keyboard apps and layouts.
|
||||
* - Enable or disable keyboard layouts.
|
||||
* - Notify keyboard layout changes via the 'keyboardsrefresh' event.
|
||||
* KeyboardContext handles only data and does not involve in any UI logic.
|
||||
*
|
||||
* @module KeyboardContext
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
var ObservableArray = require('modules/mvvm/observable_array');
|
||||
var KeyboardHelper = require('shared/keyboard_helper');
|
||||
var ManifestHelper = require('shared/manifest_helper');
|
||||
|
||||
// stores layout indexed by app manifestURL and layoutId
|
||||
var _layoutDict = null;
|
||||
|
||||
var _keyboards = ObservableArray([]);
|
||||
var _enabledLayouts = ObservableArray([]);
|
||||
|
||||
var _isReady = false;
|
||||
var _parsingApps = false;
|
||||
var _callbacks = [];
|
||||
var _defaultEnabledCallbacks = [];
|
||||
|
||||
/**
|
||||
* @alias module:Keyboard
|
||||
* @class Keyboard
|
||||
* @param {String} name
|
||||
The name of the keyboard.
|
||||
* @param {String} description
|
||||
The description of the keyboard.
|
||||
* @param {String} launchPath
|
||||
The launch path of the keyboard.
|
||||
* @param {Array} layouts
|
||||
All layouts included in the keyboard.
|
||||
* @param {DOMApplication} app
|
||||
The keyboard app object.
|
||||
* @returns {Keyboard}
|
||||
*/
|
||||
var Keyboard = function(name, description, launchPath, layouts, app) {
|
||||
var _observable = Observable({
|
||||
name: name,
|
||||
description: description,
|
||||
launchPath: launchPath,
|
||||
layouts: layouts,
|
||||
app: app
|
||||
});
|
||||
|
||||
return _observable;
|
||||
};
|
||||
|
||||
/**
|
||||
* @alias module:Layout
|
||||
* @class Layout
|
||||
* @param {Object} Contain the following properties
|
||||
* - id
|
||||
The id of the layout.
|
||||
* - appName
|
||||
The name of the keyboard app containing the layout.
|
||||
* - appManifestURL
|
||||
The manifest url of the keyboard app containing
|
||||
the layout.
|
||||
* - name
|
||||
The name of the layout.
|
||||
* - nameL10nId
|
||||
The l10n id of the layout name.
|
||||
* - description
|
||||
The description of the layout.
|
||||
* - types
|
||||
The supported input types of the layout.
|
||||
* - enabled
|
||||
The value indicating if the layout is enabled or not.
|
||||
* @returns {Layout}
|
||||
*/
|
||||
var Layout = function(options) {
|
||||
var _observable = Observable({
|
||||
id: options.id,
|
||||
appName: options.appName,
|
||||
name: options.name,
|
||||
nameL10nId: options.nameL10nId,
|
||||
description: options.description,
|
||||
types: options.types,
|
||||
enabled: options.enabled
|
||||
});
|
||||
|
||||
// Layout enabled changed.
|
||||
_observable.observe('enabled', function(newValue, oldValue) {
|
||||
if (!_parsingApps) {
|
||||
KeyboardHelper.setLayoutEnabled(
|
||||
options.appManifestURL, options.id, newValue);
|
||||
// only check the defaults if we disabled a checkbox
|
||||
if (!newValue) {
|
||||
KeyboardHelper.checkDefaults(function(layouts, missingTypes) {
|
||||
refreshEnabledLayouts(layouts);
|
||||
notifyDefaultEnabled(layouts, missingTypes);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return _observable;
|
||||
};
|
||||
|
||||
var _waitForLayouts;
|
||||
|
||||
function refreshEnabledLayouts(reEnabledLayouts) {
|
||||
reEnabledLayouts.forEach(function(layout) {
|
||||
var app = _layoutDict[layout.app.manifestURL];
|
||||
if (app) {
|
||||
app[layout.layoutId].enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function notifyDefaultEnabled(layouts, missingTypes) {
|
||||
_defaultEnabledCallbacks.forEach(function withCallbacks(callback) {
|
||||
callback(layouts[0], missingTypes[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLayouts(layouts, reason) {
|
||||
function mapLayout(layout) {
|
||||
var app = _layoutDict[layout.app.manifestURL];
|
||||
if (!app) {
|
||||
app = _layoutDict[layout.app.manifestURL] = {};
|
||||
}
|
||||
if (app[layout.layoutId]) {
|
||||
app[layout.layoutId].enabled = layout.enabled;
|
||||
return app[layout.layoutId];
|
||||
}
|
||||
|
||||
app[layout.layoutId] = Layout({
|
||||
id: layout.layoutId,
|
||||
appName: layout.manifest.name,
|
||||
appManifestURL: layout.app.manifestURL,
|
||||
name: layout.inputManifest.name,
|
||||
nameL10nId: layout.inputManifest.nameL10nId,
|
||||
description: layout.inputManifest.description,
|
||||
types: layout.inputManifest.types,
|
||||
enabled: layout.enabled});
|
||||
return app[layout.layoutId];
|
||||
}
|
||||
|
||||
function reduceApps(carry, layout) {
|
||||
// if we already found this app, add it to the layouts
|
||||
if (!carry.some(function checkApp(app) {
|
||||
if (app.app === layout.app) {
|
||||
app.layouts.push(mapLayout(layout));
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
carry.push({
|
||||
app: layout.app,
|
||||
manifest: layout.manifest,
|
||||
layouts: [mapLayout(layout)]
|
||||
});
|
||||
}
|
||||
return carry;
|
||||
}
|
||||
|
||||
function mapKeyboard(app) {
|
||||
return Keyboard(app.manifest.name, app.manifest.description,
|
||||
app.manifest.launch_path, app.layouts, app.app);
|
||||
}
|
||||
|
||||
_parsingApps = true;
|
||||
|
||||
// if we changed apps
|
||||
if (reason.apps) {
|
||||
// re parse every layout
|
||||
_layoutDict = {};
|
||||
var apps = layouts.reduce(reduceApps, []);
|
||||
var keyboards = apps.map(mapKeyboard);
|
||||
_keyboards.reset(keyboards);
|
||||
}
|
||||
var enabled = layouts.filter(function filterEnabled(layout) {
|
||||
return layout.enabled;
|
||||
}).map(mapLayout);
|
||||
_enabledLayouts.reset(enabled);
|
||||
|
||||
_parsingApps = false;
|
||||
|
||||
if (_waitForLayouts) {
|
||||
_waitForLayouts();
|
||||
_waitForLayouts = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
var _init = function(callback) {
|
||||
document.addEventListener('DOMRetranslated', function() {
|
||||
// refresh keyboard and layout in _keyboards
|
||||
_keyboards.forEach(function(keyboard) {
|
||||
var keyboardAppInstance = keyboard.app;
|
||||
var keyboardManifest =
|
||||
new ManifestHelper(keyboardAppInstance.manifest);
|
||||
keyboard.name = keyboardManifest.name;
|
||||
keyboard.description = keyboardManifest.description;
|
||||
keyboard.layouts.forEach(function(layout) {
|
||||
layout.appName = keyboardManifest.name;
|
||||
});
|
||||
});
|
||||
});
|
||||
_waitForLayouts = callback;
|
||||
KeyboardHelper.stopWatching();
|
||||
KeyboardHelper.watchLayouts(updateLayouts);
|
||||
};
|
||||
|
||||
var _ready = function(callback) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isReady) {
|
||||
callback();
|
||||
} else {
|
||||
_callbacks.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Reset the keyboard context. It clears all cached data of installed
|
||||
* keyboards and current enabled layouts.
|
||||
*
|
||||
* @alias module:KeyboardContext#reset
|
||||
*/
|
||||
reset: function kc_reset() {
|
||||
_layoutDict = null;
|
||||
_keyboards = ObservableArray([]);
|
||||
_enabledLayouts = ObservableArray([]);
|
||||
_isReady = false;
|
||||
_parsingApps = false;
|
||||
_callbacks = [];
|
||||
_defaultEnabledCallbacks = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the keyboard context. After the context initialized, we are
|
||||
* able to get the installed keyboards and enabled layouts.
|
||||
*
|
||||
* @alias module:KeyboardContext#init
|
||||
* @param {Function} callback
|
||||
* The callback when the context is initialized.
|
||||
*/
|
||||
init: function kc_init(callback) {
|
||||
_defaultEnabledCallbacks = [];
|
||||
_isReady = false;
|
||||
_init(function() {
|
||||
_isReady = true;
|
||||
_callbacks.forEach(function(callback) {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
_ready(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the installed keyboards in terms of an observable array.
|
||||
*
|
||||
* @alias module:KeyboardContext#keyboards
|
||||
* @param {Function} callback
|
||||
* The result is passed to the callback when ready.
|
||||
*/
|
||||
keyboards: function kc_keyboards(callback) {
|
||||
_ready(function() {
|
||||
callback(_keyboards);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the enabled layouts in terms of an observable array.
|
||||
*
|
||||
* @alias module:KeyboardContext#enabledLayouts
|
||||
* @param {Function} callback
|
||||
* The result is passed to the callback when ready.
|
||||
*/
|
||||
enabledLayouts: function kc_enabledLayouts(callback) {
|
||||
_ready(function() {
|
||||
callback(_enabledLayouts);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a callback to be triggered when the default keyboard is enabled.
|
||||
*
|
||||
* @alias module:KeyboardContext#defaultKeyboardEnabled
|
||||
* @param {Function} callback
|
||||
* The callback to be triggered.
|
||||
*/
|
||||
defaultKeyboardEnabled: function kc_defaultKeyboardEnabled(callback) {
|
||||
_defaultEnabledCallbacks.push(callback);
|
||||
}
|
||||
};
|
||||
});
|
||||
112
gaia/apps/settings/js/modules/media_storage.js
Executable file
112
gaia/apps/settings/js/modules/media_storage.js
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
var SettingsListener = require('shared/settings_listener');
|
||||
|
||||
/**
|
||||
* @class MediaVolume
|
||||
* @requires module:modules/base/module
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @returns {MediaVolume}
|
||||
*/
|
||||
var MediaStorage = Module.create(function MediaStorage() {
|
||||
this.super(Observable).call(this);
|
||||
|
||||
this._defaultMediaVolume = null;
|
||||
this._defaultMediaVolumeKey = 'device.storage.writable.name';
|
||||
|
||||
// Monitor default media volume state
|
||||
SettingsListener.observe(this._defaultMediaVolumeKey, 'sdcard',
|
||||
this._mediaVolumeChangeHandler.bind(this));
|
||||
}).extend(Observable);
|
||||
|
||||
Observable.defineObservableProperty(MediaStorage.prototype,
|
||||
'volumeState', {
|
||||
readonly: true,
|
||||
value: 'available'
|
||||
});
|
||||
|
||||
Observable.defineObservableProperty(MediaStorage.prototype,
|
||||
'freeSize', {
|
||||
readonly: true,
|
||||
value: 0
|
||||
});
|
||||
|
||||
MediaStorage.prototype.handleEvent = function ms_handler(evt) {
|
||||
switch (evt.type) {
|
||||
case 'change':
|
||||
// we are handling storage state changes
|
||||
// possible state: available, unavailable, shared
|
||||
this._updateMediaStorageInfo();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
MediaStorage.prototype._mediaVolumeChangeHandler =
|
||||
function ms__mediaVolumeChangeHandler(defaultName) {
|
||||
if (this._defaultMediaVolume) {
|
||||
this._defaultMediaVolume.removeEventListener('change', this);
|
||||
}
|
||||
this._defaultMediaVolume = this._getDefaultVolume(defaultName);
|
||||
if (this._defaultMediaVolume) {
|
||||
this._defaultMediaVolume.addEventListener('change', this);
|
||||
}
|
||||
this._updateMediaStorageInfo();
|
||||
};
|
||||
|
||||
// Media Storage
|
||||
MediaStorage.prototype._updateMediaStorageInfo =
|
||||
function ms__updateMediaStorageInfo() {
|
||||
if (!this._defaultMediaVolume) {
|
||||
this._updateVolumeState(null, 'unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
var req = this._defaultMediaVolume.available();
|
||||
req.onsuccess = evt => {
|
||||
var state = evt.target.result;
|
||||
var firstVolume = navigator.getDeviceStorages('sdcard')[0];
|
||||
// if the default storage is unavailable, and it's not the
|
||||
// internal storage, we show the internal storage status instead.
|
||||
if (state === 'unavailable' &&
|
||||
this._defaultMediaVolume.storageName !== firstVolume.storageName) {
|
||||
firstVolume.available().onsuccess = e => {
|
||||
this._updateVolumeState(firstVolume, e.target.result);
|
||||
};
|
||||
} else {
|
||||
this._updateVolumeState(this._defaultMediaVolume, state);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
MediaStorage.prototype._updateVolumeState =
|
||||
function ms__updateVolumeState(volume, state) {
|
||||
this._volumeState = state;
|
||||
|
||||
volume.freeSpace().onsuccess = (e) => {
|
||||
this._freeSize = e.target.result;
|
||||
};
|
||||
};
|
||||
|
||||
// util function
|
||||
MediaStorage.prototype._getDefaultVolume =
|
||||
function ms__getDefaultVolume(name) {
|
||||
// Per API design, all media type return the same volumes.
|
||||
// So we use 'sdcard' here for no reason.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=856782#c10
|
||||
var volumes = navigator.getDeviceStorages('sdcard');
|
||||
if (!name || name === '') {
|
||||
return volumes[0];
|
||||
}
|
||||
for (var i = 0; i < volumes.length; ++i) {
|
||||
if (volumes[i].storageName === name) {
|
||||
return volumes[i];
|
||||
}
|
||||
}
|
||||
return volumes[0];
|
||||
};
|
||||
|
||||
return MediaStorage();
|
||||
});
|
||||
207
gaia/apps/settings/js/modules/messaging.js
Executable file
207
gaia/apps/settings/js/modules/messaging.js
Executable file
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* XXX
|
||||
* This module should be used only by `messaging` and `messaging_detail`
|
||||
* panels only.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
// Constants
|
||||
var CBS_KEY = 'ril.cellbroadcast.disabled';
|
||||
var CMAS_KEY = 'cmas.enabled';
|
||||
var CMAS_PREF_KEY = 'cmas.user-pref.enabled';
|
||||
|
||||
var IccHelper = require('shared/icc_helper');
|
||||
var asyncStorage = require('shared/async_storage');
|
||||
var SettingsUtils = require('modules/settings_utils');
|
||||
var SettingsListener = require('shared/settings_listener');
|
||||
var settings = window.navigator.mozSettings;
|
||||
|
||||
var Messaging = function() {
|
||||
this._cardIndex = 0;
|
||||
|
||||
this._cbs = null;
|
||||
this._cbsInput = null;
|
||||
this._cbsInit = false;
|
||||
|
||||
this._cmas = null;
|
||||
this._cmasInput = null;
|
||||
this._cmasInit = false;
|
||||
|
||||
this._boundCBSChanged = this._cbsChanged.bind(this);
|
||||
this._boundCBSInputChanged = this._cbsInputChanged.bind(this);
|
||||
this._boundCMASInputChanged = this._cmasInputChanged.bind(this);
|
||||
};
|
||||
|
||||
Messaging.prototype = {
|
||||
_disableItems: function m__disableItems(panel, disable) {
|
||||
var elementIds = [
|
||||
'menuItem-deliveryReport',
|
||||
'menuItem-readReport',
|
||||
'menuItem-autoRetrieve',
|
||||
'menuItem-wapPush',
|
||||
'menuItem-cellBroadcast',
|
||||
'menuItem-emergencyAlert'
|
||||
];
|
||||
elementIds.forEach(function(id) {
|
||||
var element = panel.querySelector(id);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disable) {
|
||||
element.setAttribute('aria-disabled', true);
|
||||
} else {
|
||||
element.removeAttribute('aria-disabled');
|
||||
}
|
||||
var input = element.querySelector('gaia-switch');
|
||||
if (!input) {
|
||||
input = element.querySelector('select');
|
||||
}
|
||||
input.disabled = disable;
|
||||
});
|
||||
},
|
||||
_setCMAS: function m__setCMAS(enabled) {
|
||||
var cardIndex = this._cardIndex;
|
||||
this._cmasInput.checked = enabled;
|
||||
|
||||
// get first
|
||||
var getReq = settings.createLock().get(CMAS_KEY);
|
||||
getReq.onsuccess = function() {
|
||||
var values = getReq.result[CMAS_KEY];
|
||||
values[cardIndex] = enabled;
|
||||
|
||||
var cmasset = {};
|
||||
cmasset[CMAS_KEY] = values;
|
||||
|
||||
// then set
|
||||
settings.createLock().set(cmasset);
|
||||
};
|
||||
},
|
||||
_cbsChanged: function m__cbsChanged(cbsSettings) {
|
||||
var cardIndex = this._cardIndex;
|
||||
var value = cbsSettings[cardIndex];
|
||||
this._cbsInput.checked = !value;
|
||||
|
||||
if (value) {
|
||||
// when CBS off, CMAS is also off
|
||||
this._setCMAS(false);
|
||||
} else {
|
||||
// when CBS on, retained CMAS pref
|
||||
this._restoreCMASPref();
|
||||
}
|
||||
|
||||
this._cmasInput.disabled = value; // CMAS disabled when CBS off
|
||||
|
||||
if (!this._cbsInit) {
|
||||
this._cbsInput.disabled = false;
|
||||
this._cbs.removeAttribute('aria-disabled');
|
||||
this._cbsInit = true;
|
||||
}
|
||||
},
|
||||
_cbsInputChanged: function m__cbsInputChanged() {
|
||||
var cardIndex = this._cardIndex;
|
||||
var cbsInputChecked = this._cbsInput.checked;
|
||||
|
||||
if (!cbsInputChecked) { // save CMAS pref when CBS off
|
||||
this._storeCMASPref(this._cmasInput.checked);
|
||||
}
|
||||
|
||||
// get first
|
||||
var getReq = settings.createLock().get(CBS_KEY);
|
||||
getReq.onsuccess = function() {
|
||||
var values = getReq.result[CBS_KEY];
|
||||
values[cardIndex] = !cbsInputChecked;
|
||||
|
||||
var cbsset = {};
|
||||
cbsset[CBS_KEY] = values;
|
||||
|
||||
// then set
|
||||
settings.createLock().set(cbsset);
|
||||
};
|
||||
},
|
||||
_cmasInputChanged: function m__cmasInputChanged() {
|
||||
this._setCMAS(this._cmasInput.checked);
|
||||
this._storeCMASPref(this._cmasInput.checked);
|
||||
},
|
||||
// remember last CMAS pref when CBS off
|
||||
_storeCMASPref: function m__storeCMASPref(checked) {
|
||||
var cardIndex = this._cardIndex;
|
||||
asyncStorage.getItem(CMAS_PREF_KEY, function(pref) {
|
||||
var states = pref || [true, true];
|
||||
states[cardIndex] = checked;
|
||||
asyncStorage.setItem(CMAS_PREF_KEY, states);
|
||||
});
|
||||
},
|
||||
// check local storage to bring back CMAS pref
|
||||
_restoreCMASPref: function m__restoreCMASPref() {
|
||||
var cardIndex = this._cardIndex;
|
||||
asyncStorage.getItem(CMAS_PREF_KEY, function(pref) {
|
||||
var states = pref || [true, true];
|
||||
this._setCMAS(states[cardIndex]);
|
||||
|
||||
if (!this._cmasInit) {
|
||||
this._cmasInput.disabled = false;
|
||||
this._cmas.removeAttribute('aria-disabled');
|
||||
this._cmasInit = true;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
initCBS: function m_initCBS(panel, cardIndex) {
|
||||
this._cardIndex = cardIndex;
|
||||
|
||||
// Cell Broadcast
|
||||
this._cbs = panel.querySelector('#menuItem-cellBroadcast');
|
||||
this._cbsInput = this._cbs.querySelector('gaia-switch');
|
||||
this._cbsInit = false;
|
||||
|
||||
// Emergency Alert
|
||||
this._cmas = panel.querySelector('#menuItem-emergencyAlert');
|
||||
this._cmasInput = this._cmas.querySelector('gaia-switch');
|
||||
this._cmasInit = false;
|
||||
|
||||
// cleanup first
|
||||
this._cbsInput.removeEventListener('change', this._boundCBSInputChanged);
|
||||
this._cmasInput.removeEventListener('change',
|
||||
this._boundCMASInputChanged);
|
||||
SettingsListener.unobserve(CBS_KEY, this._boundCBSChanged);
|
||||
|
||||
// reset again
|
||||
this._cbsInput.addEventListener('change', this._boundCBSInputChanged);
|
||||
this._cmasInput.addEventListener('change', this._boundCMASInputChanged);
|
||||
SettingsListener.observe(CBS_KEY, [false, false], this._boundCBSChanged);
|
||||
},
|
||||
disableItems: function m_disableItems(panel) {
|
||||
var self = this;
|
||||
|
||||
if (!settings || !IccHelper || IccHelper.cardState !== 'ready') {
|
||||
this._disableItems(panel, true);
|
||||
return;
|
||||
}
|
||||
|
||||
IccHelper.addEventListener('cardstatechange', function() {
|
||||
self._disableItems(panel, IccHelper.cardState !== 'ready');
|
||||
});
|
||||
|
||||
this._disableItems(panel, false);
|
||||
},
|
||||
injectCBSTemplate: function m_injectCBSTemplate(containerNode) {
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
var templateId = 'template-messaging';
|
||||
SettingsUtils.loadTemplate(templateId, function(html) {
|
||||
if (!html) {
|
||||
reject('We can\'t get the template ' + templateId);
|
||||
} else {
|
||||
containerNode.innerHTML = html;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
|
||||
var messaging = new Messaging();
|
||||
return messaging;
|
||||
});
|
||||
41
gaia/apps/settings/js/modules/mobile/supported_network_info.js
Executable file
41
gaia/apps/settings/js/modules/mobile/supported_network_info.js
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var NetworkType = require('modules/mobile/supported_network_type');
|
||||
|
||||
var SupportedNetworkInfo = {
|
||||
supportedNetworkTypeHelpers: [],
|
||||
|
||||
_getMobileConnectionIndex:
|
||||
function ni_getMobileConnectionIndex(mobileConnection) {
|
||||
return Array.prototype.indexOf.call(navigator.mozMobileConnections,
|
||||
mobileConnection);
|
||||
},
|
||||
|
||||
/**
|
||||
* The function returns an object of the supporting state of category of
|
||||
* network types. The categories are 'gsm', 'cdma', and 'lte'.
|
||||
*/
|
||||
getSupportedNetworkInfo:
|
||||
function getSupportedNetworkInfo(mobileConnection, callback) {
|
||||
if (!navigator.mozMobileConnections) {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
var index = this._getMobileConnectionIndex(mobileConnection);
|
||||
var helper = this.supportedNetworkTypeHelpers[index];
|
||||
if (!helper) {
|
||||
this.supportedNetworkTypeHelpers[index] = helper =
|
||||
NetworkType.SupportedNetworkTypeHelper(
|
||||
mobileConnection.supportedNetworkTypes);
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback(helper);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return SupportedNetworkInfo;
|
||||
});
|
||||
158
gaia/apps/settings/js/modules/mobile/supported_network_type.js
Executable file
158
gaia/apps/settings/js/modules/mobile/supported_network_type.js
Executable file
|
|
@ -0,0 +1,158 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
// The network types displayed on the list for user selection
|
||||
const NETWORK_TYPES = [
|
||||
'wcdma/gsm',
|
||||
'gsm',
|
||||
'wcdma',
|
||||
'wcdma/gsm-auto',
|
||||
'cdma/evdo',
|
||||
'cdma',
|
||||
'evdo',
|
||||
'wcdma/gsm/cdma/evdo',
|
||||
'lte/cdma/evdo',
|
||||
'lte/wcdma/gsm',
|
||||
'lte/wcdma/gsm/cdma/evdo',
|
||||
'lte',
|
||||
'lte/wcdma'
|
||||
];
|
||||
|
||||
// The string map from network types to user friendly strings, which is
|
||||
// the combination of the three categories: 'GSM', 'CDMA', and 'LTE'.
|
||||
// The mapping is determined by the hardware supported types.
|
||||
const NETWORK_TYPE_STRING_MAP = {
|
||||
'CDMA': {
|
||||
'cdma/evdo': 'operator-networkType-auto',
|
||||
'cdma': 'operator-networkType-CDMA',
|
||||
'evdo': 'operator-networkType-EVDO'
|
||||
},
|
||||
'LTE': {
|
||||
'lte': 'operator-networkType-LTE'
|
||||
},
|
||||
'GSM': {
|
||||
'wcdma/gsm': 'operator-networkType-prefer3G',
|
||||
'gsm': 'operator-networkType-2G',
|
||||
'wcdma': 'operator-networkType-3G',
|
||||
'wcdma/gsm-auto': 'operator-networkType-auto'
|
||||
},
|
||||
'GSM,LTE': {
|
||||
'lte/wcdma/gsm': 'operator-networkType-auto',
|
||||
'lte/wcdma': 'operator-networkType-auto-3G-4G',
|
||||
'gsm': 'operator-networkType-2G',
|
||||
'wcdma': 'operator-networkType-3G',
|
||||
'wcdma/gsm-auto': 'operator-networkType-auto-2G-3G',
|
||||
'wcdma/gsm': 'operator-networkType-prefer3G',
|
||||
'lte': 'operator-networkType-LTE'
|
||||
},
|
||||
'CDMA,LTE': {
|
||||
'lte/cdma/evdo': 'operator-networkType-auto',
|
||||
'cdma/evdo': 'operator-networkType-auto-CDMA-EVDO',
|
||||
'cdma': 'operator-networkType-CDMA',
|
||||
'evdo': 'operator-networkType-EVDO',
|
||||
'lte': 'operator-networkType-LTE'
|
||||
},
|
||||
'GSM,CDMA': {
|
||||
'wcdma/gsm': 'operator-networkType-preferWCDMA',
|
||||
'gsm': 'operator-networkType-GSM',
|
||||
'wcdma': 'operator-networkType-WCDMA',
|
||||
'wcdma/gsm-auto': 'operator-networkType-auto-WCDMA-GSM',
|
||||
'cdma/evdo': 'operator-networkType-auto-CDMA-EVDO',
|
||||
'cdma': 'operator-networkType-CDMA',
|
||||
'evdo': 'operator-networkType-EVDO',
|
||||
'wcdma/gsm/cdma/evdo': 'operator-networkType-auto'
|
||||
},
|
||||
'GSM,CDMA,LTE': {
|
||||
'wcdma/gsm': 'operator-networkType-preferWCDMA',
|
||||
'gsm': 'operator-networkType-GSM',
|
||||
'wcdma': 'operator-networkType-WCDMA',
|
||||
'wcdma/gsm-auto': 'operator-networkType-auto-WCDMA-GSM',
|
||||
'cdma/evdo': 'operator-networkType-auto-CDMA-EVDO',
|
||||
'cdma': 'operator-networkType-CDMA',
|
||||
'evdo': 'operator-networkType-EVDO',
|
||||
'wcdma/gsm/cdma/evdo': 'operator-networkType-auto-WCDMA-GSM-CDMA-EVDO',
|
||||
'lte': 'operator-networkType-LTE',
|
||||
'lte/wcdma/gsm': 'operator-networkType-auto-LTE-WCDMA-GSM',
|
||||
'lte/wcdma': 'operator-networkType-auto-LTE-WCDMA',
|
||||
'lte/cdma/evdo': 'operator-networkType-auto-LTE-CDMA-EVDO',
|
||||
'lte/wcdma/gsm/cdma/evdo': 'operator-networkType-auto'
|
||||
},
|
||||
// Default value, the same as 'GSM,CDMA'.
|
||||
'': {
|
||||
'wcdma/gsm': 'operator-networkType-preferWCDMA',
|
||||
'gsm': 'operator-networkType-GSM',
|
||||
'wcdma': 'operator-networkType-WCDMA',
|
||||
'wcdma/gsm-auto': 'operator-networkType-auto-WCDMA-GSM',
|
||||
'cdma/evdo': 'operator-networkType-auto-CDMA-EVDO',
|
||||
'cdma': 'operator-networkType-CDMA',
|
||||
'evdo': 'operator-networkType-EVDO',
|
||||
'wcdma/gsm/cdma/evdo': 'operator-networkType-auto'
|
||||
}
|
||||
};
|
||||
|
||||
var NetworkType = {
|
||||
/**
|
||||
* SupportedNetworkTypeHelper helps map the supported network types to user
|
||||
* friendly strings.
|
||||
*
|
||||
* @param {Array} hwSupportedTypes Array of hardware supported types. The
|
||||
* posssible values of the type are 'gsm',
|
||||
* 'cdma', 'wcdma', 'evdo', and 'lte'.
|
||||
*/
|
||||
SupportedNetworkTypeHelper: function ctor_snth(hwSupportedTypes) {
|
||||
var _hwSupportedTypeMap = {
|
||||
gsm: hwSupportedTypes.indexOf('gsm') !== -1,
|
||||
cdma: hwSupportedTypes.indexOf('cdma') !== -1,
|
||||
wcdma: hwSupportedTypes.indexOf('wcdma') !== -1,
|
||||
evdo: hwSupportedTypes.indexOf('evdo') !== -1,
|
||||
lte: hwSupportedTypes.indexOf('lte') !== -1
|
||||
};
|
||||
|
||||
// Get all supported network types based on the hardware supported types.
|
||||
var _networkTypes = NETWORK_TYPES.filter(function(type) {
|
||||
return type.split('/').every(function(subtype) {
|
||||
return _hwSupportedTypeMap[subtype.split('-')[0]];
|
||||
});
|
||||
});
|
||||
|
||||
// Compose the string for NETWORK_TYPE_STRING_MAP table lookup.
|
||||
var _stringMap = NETWORK_TYPE_STRING_MAP[
|
||||
['GSM', 'CDMA', 'LTE'].filter(function(category) {
|
||||
switch (category) {
|
||||
case 'GSM':
|
||||
return _hwSupportedTypeMap.gsm || _hwSupportedTypeMap.wcdma;
|
||||
case 'CDMA':
|
||||
return _hwSupportedTypeMap.cdma || _hwSupportedTypeMap.evdo;
|
||||
case 'LTE':
|
||||
return _hwSupportedTypeMap.lte;
|
||||
}
|
||||
}).toString()];
|
||||
|
||||
return {
|
||||
get gsm() {
|
||||
return _hwSupportedTypeMap.gsm;
|
||||
},
|
||||
get cdma() {
|
||||
return _hwSupportedTypeMap.cdma;
|
||||
},
|
||||
get wcdma() {
|
||||
return _hwSupportedTypeMap.wcdma;
|
||||
},
|
||||
get evdo() {
|
||||
return _hwSupportedTypeMap.evdo;
|
||||
},
|
||||
get lte() {
|
||||
return _hwSupportedTypeMap.lte;
|
||||
},
|
||||
get networkTypes() {
|
||||
return _networkTypes;
|
||||
},
|
||||
l10nIdForType: function(type) {
|
||||
return _stringMap[type];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return NetworkType;
|
||||
});
|
||||
248
gaia/apps/settings/js/modules/mvvm/list_view.js
Executable file
248
gaia/apps/settings/js/modules/mvvm/list_view.js
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
'use strict';
|
||||
|
||||
define(function(require) {
|
||||
var elements = new WeakMap();
|
||||
var ObservableArray = require('modules/mvvm/observable_array');
|
||||
|
||||
/*
|
||||
* A ListView takes an ObservableArray or an ordinary array, and generate/
|
||||
* manipulate the corresponding DOM elements of the content in the array using
|
||||
* the given template function. If the array is an ObservableArray, ListView
|
||||
* updates the DOM elements accordingly when the array is manipulated.
|
||||
*/
|
||||
var ListView = function(root, observableArray, templateFunc) {
|
||||
var _observableArray = null;
|
||||
var _root = root;
|
||||
var _templateFunc = templateFunc;
|
||||
var _enabled = true;
|
||||
|
||||
// keep track of observeables so we can unobserve
|
||||
var observerMap = new WeakMap();
|
||||
|
||||
function releaseObservers(element) {
|
||||
var observable = observerMap.get(element);
|
||||
if (!observable || typeof observable !== 'object') {
|
||||
return;
|
||||
}
|
||||
var observers = observerMap.get(observable);
|
||||
if (observers) {
|
||||
observers.forEach(function eachObserver(observer) {
|
||||
observable.unobserve(observer);
|
||||
});
|
||||
observerMap.delete(observable);
|
||||
observerMap.delete(element);
|
||||
}
|
||||
}
|
||||
|
||||
// helper to observe something and automatically remove old observers
|
||||
function observeAndCall(observable, map) {
|
||||
var observers = observerMap.get(observable) || [];
|
||||
if (observerMap.has(observable)) {
|
||||
observers = observerMap.get(observable);
|
||||
}
|
||||
observers = Object.keys(map).map(function eachProperty(property) {
|
||||
observable.observe(property, map[property]);
|
||||
map[property]();
|
||||
return map[property];
|
||||
});
|
||||
observerMap.set(observable, observers);
|
||||
}
|
||||
|
||||
var _helper = {
|
||||
observeAndCall: observeAndCall
|
||||
};
|
||||
|
||||
var _handleEvent = function(event) {
|
||||
// Ignore the change event when the list view is not enabled.
|
||||
if (!_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = event.detail;
|
||||
switch (event.type) {
|
||||
case 'insert':
|
||||
_insert(data.index, data.items);
|
||||
break;
|
||||
case 'remove':
|
||||
_remove(data.index, data.count);
|
||||
break;
|
||||
case 'replace':
|
||||
_replace(data.index, data.newValue);
|
||||
break;
|
||||
case 'reset':
|
||||
_reset(data.items || []);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
var _insert = function(index, items) {
|
||||
if (items.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add DOM elements
|
||||
var referenceElement = _root.children[index];
|
||||
for (var i = items.length - 1; i >= 0; i--) {
|
||||
var curElement = _templateFunc(items[i], undefined, _helper);
|
||||
observerMap.set(curElement, items[i]);
|
||||
_root.insertBefore(curElement, referenceElement);
|
||||
referenceElement = curElement;
|
||||
}
|
||||
};
|
||||
|
||||
var _remove = function(index, count) {
|
||||
if (count === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove DOM elements
|
||||
if (count >= _root.childElementCount) {
|
||||
// clear all
|
||||
while (_root.firstElementChild) {
|
||||
releaseObservers(_root.firstElementChild);
|
||||
_root.removeChild(_root.firstElementChild);
|
||||
}
|
||||
} else {
|
||||
var nextElement = _root.children[index];
|
||||
for (var i = 0; i < count; i++) {
|
||||
if (nextElement) {
|
||||
var temp = nextElement.nextElementSibling;
|
||||
releaseObservers(nextElement);
|
||||
_root.removeChild(nextElement);
|
||||
nextElement = temp;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var _replace = function(index, value) {
|
||||
var element = _root.children[index];
|
||||
if (element) {
|
||||
releaseObservers(element);
|
||||
var newElement = _templateFunc(value, element, _helper);
|
||||
observerMap.set(element, value);
|
||||
if (newElement !== element) {
|
||||
_root.insertBefore(newElement, element);
|
||||
_root.removeChild(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var _reset = function(items) {
|
||||
var itemCount = items.length;
|
||||
var elementCount = _root.childElementCount;
|
||||
|
||||
if (itemCount === 0) {
|
||||
_remove(0, elementCount);
|
||||
} else if (itemCount <= elementCount) {
|
||||
items.forEach(function(item, index) {
|
||||
_replace(index, item);
|
||||
});
|
||||
// remove extra elements
|
||||
_remove(itemCount, elementCount - itemCount);
|
||||
} else {
|
||||
var slicedItems = items.slice(0, elementCount);
|
||||
var remainingItems = items.slice(elementCount);
|
||||
|
||||
slicedItems.forEach(function(item, index) {
|
||||
_replace(index, item);
|
||||
});
|
||||
// add extra elements
|
||||
_insert(elementCount, remainingItems);
|
||||
}
|
||||
};
|
||||
|
||||
var _enabledChanged = function() {
|
||||
if (_enabled) {
|
||||
_reset(_observableArray.array);
|
||||
}
|
||||
};
|
||||
|
||||
var view = {
|
||||
set: function lv_set(newArray) {
|
||||
if (_observableArray) {
|
||||
_observableArray.removeEventListener('insert', _handleEvent);
|
||||
_observableArray.removeEventListener('remove', _handleEvent);
|
||||
_observableArray.removeEventListener('replace', _handleEvent);
|
||||
_observableArray.removeEventListener('reset', _handleEvent);
|
||||
}
|
||||
|
||||
if (!newArray) {
|
||||
// clear all existing items
|
||||
if (_observableArray) {
|
||||
_remove(0, _observableArray.length);
|
||||
_observableArray = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If new array is an ordinary array, convert it to an ObservableArray.
|
||||
if (Array.isArray(newArray)) {
|
||||
newArray = ObservableArray(newArray);
|
||||
}
|
||||
_observableArray = newArray;
|
||||
|
||||
// An ObservableArray fires the four events for notifying its changes.
|
||||
// Register to these events for updating UI.
|
||||
_observableArray.addEventListener('insert', _handleEvent);
|
||||
_observableArray.addEventListener('remove', _handleEvent);
|
||||
_observableArray.addEventListener('replace', _handleEvent);
|
||||
_observableArray.addEventListener('reset', _handleEvent);
|
||||
|
||||
if (this.enabled) {
|
||||
_reset(_observableArray.array);
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
// unobserve from array and null everything out
|
||||
if (_observableArray) {
|
||||
_observableArray.removeEventListener('insert', _handleEvent);
|
||||
_observableArray.removeEventListener('remove', _handleEvent);
|
||||
_observableArray.removeEventListener('replace', _handleEvent);
|
||||
_observableArray.removeEventListener('reset', _handleEvent);
|
||||
}
|
||||
_remove(0, _observableArray.length);
|
||||
elements.delete(_root);
|
||||
_root = null;
|
||||
_observableArray = null;
|
||||
_templateFunc = null;
|
||||
_enabled = false;
|
||||
},
|
||||
|
||||
get element() {
|
||||
return _root;
|
||||
},
|
||||
|
||||
set enabled(value) {
|
||||
if (_enabled !== value) {
|
||||
_enabled = value;
|
||||
_enabledChanged();
|
||||
}
|
||||
},
|
||||
|
||||
get enabled() {
|
||||
return _enabled;
|
||||
}
|
||||
};
|
||||
|
||||
if (elements.has(_root)) {
|
||||
// destroy old ListView if we setup a second one
|
||||
elements.get(_root).destroy();
|
||||
}
|
||||
|
||||
elements.set(_root, view);
|
||||
|
||||
// emtpy element at creation time
|
||||
_root.innerHTML = '';
|
||||
|
||||
view.set(observableArray);
|
||||
return view;
|
||||
};
|
||||
|
||||
return ListView;
|
||||
});
|
||||
360
gaia/apps/settings/js/modules/mvvm/observable.js
Executable file
360
gaia/apps/settings/js/modules/mvvm/observable.js
Executable file
|
|
@ -0,0 +1,360 @@
|
|||
/**
|
||||
* Observable provides ways of defining properties that the value changes of
|
||||
* them can be observed. In addition to normal properties, it allows to
|
||||
* create read-only properties and dependency properties.
|
||||
*
|
||||
* Observable creation:
|
||||
* There are two ways of defining an Observable: object literal or extending
|
||||
* from Observable. Object literal is useful when defining a singleton or you
|
||||
* want to create an observable easily.
|
||||
*
|
||||
* @example
|
||||
* var observable = Observable({
|
||||
* prop: 10,
|
||||
* func: function() {}
|
||||
* });
|
||||
*
|
||||
* Extending from Observable allow you to define a class of Observable. The
|
||||
* advantage of this compared to object literal is that the accessers are
|
||||
* shared across all instances of the class. The syntax compatible with the
|
||||
* javascript prototype definition.
|
||||
*
|
||||
* @example
|
||||
* var ExtendedObservable = Module.create(function ExtendedObservable() {
|
||||
* this.super(Observable).call(this);
|
||||
* }).extend(Observable);
|
||||
* Observable.defineObservableProperty(ExtendedObservable.prototype, 'prop',
|
||||
* {
|
||||
* value: 10
|
||||
* });
|
||||
* ExtendedObservable.prototype.func = function() {};
|
||||
*
|
||||
* // Extend from NewObservable
|
||||
* var ExtendedObservable2 = Module.create(function() {
|
||||
* // constructor
|
||||
* }).extend(ExtendedObservable);
|
||||
* Observable.defineObservableProperty(
|
||||
* ExtendedObservable2.prototype, 'prop2',
|
||||
* {
|
||||
* value: 20
|
||||
* });
|
||||
*
|
||||
* var observable = ExtendedObservable2();
|
||||
* console.log(observable.prop); // 10
|
||||
* console.log(observable.prop2); // 20
|
||||
*
|
||||
* Defining a read-only property:
|
||||
* This is only supported when extending from Observable. When you define a
|
||||
* read-only property, an internal property with a '_' prefix is defined at
|
||||
* the same time so you can still change the value inside the observable.
|
||||
*
|
||||
* @example
|
||||
* Observable.defineObservableProperty(ExtendedObservable.prototype, 'prop',
|
||||
* {
|
||||
* readonly: true,
|
||||
* value: 10
|
||||
* });
|
||||
* ExtendedObservable.prototype.inc = function() {
|
||||
* this._prop = 100;
|
||||
* };
|
||||
* var observable = new ExtendedObservable();
|
||||
* observable.prop = 100; // throws an exception
|
||||
* observable.inc(); // observers on "prop" are called
|
||||
*
|
||||
* Defining a dependency property:
|
||||
* This is only supported when extending from Observable. You provide a list
|
||||
* of the dependent properties and when each of them changes, the observsers
|
||||
* on the defined property are called. Dependency properties are read-only and
|
||||
* their values are determined by the specified getter.
|
||||
*
|
||||
* @example
|
||||
* Observable.defineObservableProperty(ExtendedObservable.prototype, 'prop',
|
||||
* {
|
||||
* value: 10
|
||||
* });
|
||||
* Observable.defineObservableProperty(
|
||||
* ExtendedObservable.prototype, 'prop2',
|
||||
* {
|
||||
* value: 20
|
||||
* });
|
||||
* Observable.defineObservableProperty(
|
||||
* ExtendedObservable.prototype, 'dependencyProp',
|
||||
* {
|
||||
* dependency: ['prop', 'prop2'],
|
||||
* get: function() {
|
||||
* return this.prop + this.prop2;
|
||||
* }
|
||||
* });
|
||||
* var observable = new ExtendedObservable();
|
||||
* observable.prop = 100; // observers on "dependencyProp" are called
|
||||
* observable.prop2 = 200; // observers on "dependencyProp" are called
|
||||
* // observable.dependencyProp is 300.
|
||||
*
|
||||
* @module modules/mvvm/observable
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var DependencyGraph = require('modules/base/dependency_graph');
|
||||
|
||||
var OP_PREFIX = (name) => { return '$OP_' + name; };
|
||||
|
||||
/**
|
||||
* @class Observable
|
||||
* @requires module:modules/base/module
|
||||
* @requires module:modules/base/dependency_graph
|
||||
* @returns {Observable}
|
||||
*/
|
||||
var Observable = Module.create(function Observable(object) {
|
||||
this._observers = {};
|
||||
if (object) {
|
||||
this._initWithObject(object);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize the observable with a prototype object. This is not required
|
||||
* for objects that are defined using prototype as everything should be
|
||||
* defined via Observable.defineObservableProperty explicitly.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf Observable.prototype
|
||||
* @param {Object} object
|
||||
*/
|
||||
Observable.prototype._initWithObject = function o_init(object) {
|
||||
for (var name in object) {
|
||||
// If name is a function, simply add it to the observable.
|
||||
if (typeof object[name] === 'function') {
|
||||
this[name] = object[name];
|
||||
} else {
|
||||
_defineObservableProperty(this, name, {
|
||||
value: object[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify the value change of a property.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf Observable.prototype
|
||||
* @param {String} name
|
||||
* @param {Object} newValue
|
||||
* @param {Object} oldValue
|
||||
*/
|
||||
Observable.prototype._notify = function o__notify(name, newValue, oldValue) {
|
||||
var observers = this._observers[name];
|
||||
if (observers) {
|
||||
observers.forEach(function(observer) {
|
||||
observer(newValue, oldValue);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an observer from a property.
|
||||
*
|
||||
* @access private
|
||||
* @memberOf Observable.prototype
|
||||
* @param {Function} observer
|
||||
* @param {String} name
|
||||
*/
|
||||
Observable.prototype._removeObserver =
|
||||
function o__removeObserver(observer, name) {
|
||||
// arguments in reverse order to support .bind(observer) for the
|
||||
// unbind from all case
|
||||
var observers = this._observers[name];
|
||||
if (observers) {
|
||||
var index = observers.indexOf(observer);
|
||||
if (index >= 0) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe a property with an observer. The observer is called when the
|
||||
* property changes.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf Observable.prototype
|
||||
* @param {String} name
|
||||
* @param {Function} observer
|
||||
*/
|
||||
Observable.prototype.observe = function o_observe(name, observer) {
|
||||
if (typeof observer !== 'function') {
|
||||
return;
|
||||
}
|
||||
(this._observers[name] = this._observers[name] || []).push(observer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unobserve a property
|
||||
*
|
||||
* @access public
|
||||
* @memberOf Observable.prototype
|
||||
* @param {String} name
|
||||
* @param {Function} observer
|
||||
*/
|
||||
Observable.prototype.unobserve = function o_unobserve(name, observer) {
|
||||
if (typeof name === 'function') {
|
||||
// (observer) -- remove from every key in _observers
|
||||
Object.keys(this._observers).forEach(
|
||||
this._removeObserver.bind(this, name));
|
||||
} else {
|
||||
if (observer) {
|
||||
// (prop, observer) -- remove observer from the specific prop
|
||||
this._removeObserver(observer, name);
|
||||
} else if (name in this._observers) {
|
||||
// (prop) -- otherwise remove all observers for property
|
||||
this._observers[name] = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Static functions
|
||||
var _dependencyGraphs = new Map();
|
||||
/**
|
||||
* Each module should have its own dependency graph that decides what
|
||||
* observers to called when a property changes. The function returns the
|
||||
* dependency graph of a module. It creates one if the map does not exist.
|
||||
*
|
||||
* @param {Object} modulePrototype
|
||||
*/
|
||||
function _getDependencyGraph(modulePrototype) {
|
||||
var dependencyGraph = _dependencyGraphs.get(modulePrototype);
|
||||
if (!dependencyGraph) {
|
||||
// register a new dependency graph of the module based on the existing
|
||||
// dependency graph on the module.
|
||||
dependencyGraph = DependencyGraph(modulePrototype._dependencyGraph);
|
||||
modulePrototype._dependencyGraph = dependencyGraph;
|
||||
_dependencyGraphs.set(modulePrototype, dependencyGraph);
|
||||
}
|
||||
return dependencyGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* The function helps query the values of all dependent properties of a
|
||||
* specified property.
|
||||
*
|
||||
* @param {Observable} observable
|
||||
* @param {String} sourceProperty
|
||||
* The source property name.
|
||||
*/
|
||||
function _getAllDependentValues(observable, sourceProperty) {
|
||||
var dependentList = observable._dependencyGraph &&
|
||||
observable._dependencyGraph.getAllDependent(sourceProperty);
|
||||
if (dependentList && dependentList.length) {
|
||||
return dependentList.map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
value: observable[name]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _getterTemplate(name, defaultValue) {
|
||||
return function() {
|
||||
var value = this[OP_PREFIX(name)];
|
||||
if (typeof value === 'undefined') {
|
||||
value = this[OP_PREFIX(name)] = defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function _setterTemplate(name) {
|
||||
return function(value) {
|
||||
var oldValue = this[name];
|
||||
if (oldValue !== value) {
|
||||
// cache the old values of all dependent
|
||||
var dependentValues = _getAllDependentValues(this, name);
|
||||
// change the value
|
||||
this[OP_PREFIX(name)] = value;
|
||||
// notify the changes
|
||||
this._notify(name, value, oldValue);
|
||||
if (dependentValues) {
|
||||
dependentValues.forEach((obj) => {
|
||||
this._notify(obj.name, this[obj.name], obj.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _defineObservablePropertyCore(object, name, options) {
|
||||
var dependency = options && options.dependency;
|
||||
|
||||
// Update dependency information
|
||||
if (dependency) {
|
||||
var dependencyGraph = _getDependencyGraph(object);
|
||||
dependency.forEach((dependentName) => {
|
||||
// name depends on dependentName
|
||||
dependencyGraph.addDependency(name, dependentName);
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(object, name, {
|
||||
enumerable: true,
|
||||
get: options.get,
|
||||
set: options.set
|
||||
});
|
||||
}
|
||||
|
||||
function _defineObservableProperty(object, name, options) {
|
||||
if (options && options.readonly) {
|
||||
var internalName = '_' + name;
|
||||
_defineObservablePropertyCore(object, name, {
|
||||
dependency: [internalName],
|
||||
get: function() {
|
||||
return this[internalName];
|
||||
}
|
||||
});
|
||||
_defineObservablePropertyCore(object, internalName, {
|
||||
get: _getterTemplate(internalName, options && options.value),
|
||||
set: _setterTemplate(internalName)
|
||||
});
|
||||
} else if (options && options.dependency && options.dependency.length) {
|
||||
if (typeof options.get !== 'function') {
|
||||
throw new Error('Observable: getter of ' + name + ' is invalid');
|
||||
}
|
||||
_defineObservablePropertyCore(object, name, {
|
||||
dependency: options.dependency,
|
||||
get: options.get
|
||||
});
|
||||
} else {
|
||||
_defineObservablePropertyCore(object, name, {
|
||||
get: _getterTemplate(name, options && options.value),
|
||||
set: _setterTemplate(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe a property with an observer. The observer is called when the
|
||||
* property changes.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf Observable
|
||||
* @param {Object} object
|
||||
* @param {String} name
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.readonly
|
||||
* Indicating if the property is read-only.
|
||||
* @param {Array.<String>} options.dependency
|
||||
* List of the dependent properties.
|
||||
* @param {Function} options.get
|
||||
* Getter of the property.
|
||||
*/
|
||||
Object.defineProperty(Observable, 'defineObservableProperty', {
|
||||
get: function() {
|
||||
return _defineObservableProperty;
|
||||
}
|
||||
});
|
||||
return Observable;
|
||||
});
|
||||
228
gaia/apps/settings/js/modules/mvvm/observable_array.js
Executable file
228
gaia/apps/settings/js/modules/mvvm/observable_array.js
Executable file
|
|
@ -0,0 +1,228 @@
|
|||
/**
|
||||
* ObservableArray is able to notify its change through events of four basic
|
||||
* operations including 'insert', 'remove', 'replace', 'reset'.
|
||||
* ObservableArray implements most used methods including 'push', 'pop', and
|
||||
* 'splice'. Due to the syntax limitation it provides 'get' and 'set' for
|
||||
* manuplication values stored in the array. 'reset' is also provided for
|
||||
* replacing te entire array with another one. In addition to these methods,
|
||||
* methods including 'forEach', 'map', 'every', 'some', 'indexOf',
|
||||
* 'lastIndexOf', 'reduce', and 'reduceRight'.
|
||||
*
|
||||
* ObservableArray creation:
|
||||
* It can be initialized by an ordinary array. If the array is not given, an
|
||||
* empty is used by default.
|
||||
*
|
||||
* @example
|
||||
* var observableArray = ObservableArray([1, 2, 3]);
|
||||
*
|
||||
* Events:
|
||||
* Information regarding the operation is provided in the event detail. Please
|
||||
* check the following example:
|
||||
*
|
||||
* @example
|
||||
* var observableArray = ObservableArray([1, 2, 3]);
|
||||
* var handler = {
|
||||
* handleEvent: function(event) { console.log(JSON.stringify(event)); }
|
||||
* }
|
||||
* observableArray.addEventListener('insert', handler);
|
||||
* observableArray.addEventListener('remove', handler);
|
||||
* observableArray.addEventListener('replace', handler);
|
||||
* observableArray.addEventListener('reset', handler);
|
||||
*
|
||||
* observableArray.push(4);
|
||||
* // observableArray is [1, 2, 3, 4]
|
||||
* // event.type is 'insert'
|
||||
* // event.detail is {index: 3, count: 1, items: [4]}
|
||||
*
|
||||
* observableArray.pop();
|
||||
* // observableArray is [1, 2, 3]
|
||||
* // event.type is 'remove'
|
||||
* // event.detail is {index: 3, count: 1, items: [4]}
|
||||
*
|
||||
* observableArray.set(2, 4);
|
||||
* // observableArray is [1, 2, 4]
|
||||
* // event.type is 'replace'
|
||||
* // event.detail is {index: 2, oldValue: 3, newValue: 4}
|
||||
*
|
||||
* observableArray.reset([1, 2, 3]);
|
||||
* // observableArray is [1, 2, 3]
|
||||
* // event.type is 'reset'
|
||||
* // event.detail is {items: [1, 2, 3]}
|
||||
*
|
||||
* observableArray.splice(2, 1, [4, 5, 6]);
|
||||
* // observableArray is [1, 2, 4, 5, 6]
|
||||
* // First event:
|
||||
* // event.type is 'remove'
|
||||
* // event.detail is {index: 2, count: 1, items: [3]}
|
||||
* // Second event:
|
||||
* // event.type is 'insert'
|
||||
* // event.detail is {index: 2, count: 3, items: [4, 5, 6]}
|
||||
*
|
||||
* @module modules/mvvm/observable_array
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var EventEmitter = require('modules/base/event_emitter');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
var Events = ['insert', 'remove', 'replace', 'reset'];
|
||||
var ReadOnlyMethods = ['forEach', 'map', 'every', 'some', 'indexOf',
|
||||
'lastIndexOf', 'reduce', 'reduceRight'];
|
||||
|
||||
/**
|
||||
* @class ObservableArray
|
||||
* @requires module:modules/base/module
|
||||
* @requires module:modules/base/event_emitter
|
||||
* @requires module:modules/mvvm/observable
|
||||
* @returns {ObservableArray}
|
||||
*/
|
||||
var ObservableArray = Module.create(function ObservableArray(array) {
|
||||
this.super(Observable).call(this);
|
||||
this.super(EventEmitter).call(this, Events);
|
||||
|
||||
this._array = array || [];
|
||||
this._length = this._array.length;
|
||||
}).extend(Observable, EventEmitter);
|
||||
|
||||
/**
|
||||
* An observable property indicating the length of the array.
|
||||
*
|
||||
* @access public
|
||||
* @type {Number}
|
||||
*/
|
||||
Observable.defineObservableProperty(ObservableArray.prototype, 'length', {
|
||||
readonly: true
|
||||
});
|
||||
|
||||
/**
|
||||
* An observable property representing the inner array.
|
||||
*
|
||||
* @access public
|
||||
* @type {Array}
|
||||
*/
|
||||
Observable.defineObservableProperty(ObservableArray.prototype, 'array', {
|
||||
readonly: true
|
||||
});
|
||||
|
||||
ReadOnlyMethods.forEach(function(op) {
|
||||
ObservableArray.prototype[op] = function() {
|
||||
return this._array[op].apply(this._array, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
ObservableArray.prototype.push = function(item) {
|
||||
this._array.push(item);
|
||||
this._length = this._array.length;
|
||||
|
||||
this._emitEvent('insert', {
|
||||
index: this._array.length - 1,
|
||||
count: 1,
|
||||
items: [item]
|
||||
});
|
||||
};
|
||||
|
||||
ObservableArray.prototype.pop = function() {
|
||||
if (!this._array.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = this._array.pop();
|
||||
this._length = this._array.length;
|
||||
|
||||
this._emitEvent('remove', {
|
||||
index: this._array.length,
|
||||
count: 1,
|
||||
items: [item]
|
||||
});
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
ObservableArray.prototype.splice = function(index, count) {
|
||||
if (arguments.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fix the arguments based on the behavior of the real spice function.
|
||||
if (index >= this._length) {
|
||||
index = this._length - 1;
|
||||
} else if (index < 0) {
|
||||
index = this._length + index;
|
||||
}
|
||||
if (count < 0) {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
var addedItems = Array.prototype.slice.call(arguments, 2);
|
||||
var removedItems = this._array.splice.apply(this._array, arguments);
|
||||
this._length = this._array.length;
|
||||
|
||||
if (removedItems.length) {
|
||||
this._emitEvent('remove', {
|
||||
index: index,
|
||||
count: count,
|
||||
items: removedItems
|
||||
});
|
||||
}
|
||||
|
||||
if (addedItems.length) {
|
||||
this._emitEvent('insert', {
|
||||
index: index,
|
||||
count: addedItems.length,
|
||||
items: addedItems
|
||||
});
|
||||
}
|
||||
|
||||
return removedItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a value to the field of a specfied index.
|
||||
*
|
||||
* @access public
|
||||
* @param {Number} index
|
||||
* @param {Object} value
|
||||
*/
|
||||
ObservableArray.prototype.set = function(index, value) {
|
||||
if (index < 0 || index >= this._array.length) {
|
||||
this.throw('set: out of range');
|
||||
}
|
||||
|
||||
var oldValue = this._array[index];
|
||||
this._array[index] = value;
|
||||
this._emitEvent('replace', {
|
||||
index: index,
|
||||
oldValue: oldValue,
|
||||
newValue: value
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the value from the field of a specfied index.
|
||||
*
|
||||
* @access public
|
||||
* @param {Number} index
|
||||
* @returns {Object}
|
||||
*/
|
||||
ObservableArray.prototype.get = function(index) {
|
||||
return this._array[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace the entire array with another one.
|
||||
*
|
||||
* @access public
|
||||
* @param {Array} array
|
||||
*/
|
||||
ObservableArray.prototype.reset = function(array) {
|
||||
this._array = array || [];
|
||||
this._length = this._array.length;
|
||||
this._emitEvent('reset', {
|
||||
items: this._array
|
||||
});
|
||||
};
|
||||
|
||||
return ObservableArray;
|
||||
});
|
||||
8
gaia/apps/settings/js/modules/navigator/battery.js
Executable file
8
gaia/apps/settings/js/modules/navigator/battery.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Wraps navigator.battery for replacing it in unit tests more easily.
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
return navigator.battery;
|
||||
});
|
||||
4
gaia/apps/settings/js/modules/navigator/mozApps.js
Executable file
4
gaia/apps/settings/js/modules/navigator/mozApps.js
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
define(function() {
|
||||
'use strict';
|
||||
return window.navigator.mozApps;
|
||||
});
|
||||
12
gaia/apps/settings/js/modules/navigator/mozBluetooth.js
Executable file
12
gaia/apps/settings/js/modules/navigator/mozBluetooth.js
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Wraps navigator.bluetooth for replacing it in unit tests more easily.
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
if (navigator.mozBluetooth) {
|
||||
return navigator.mozBluetooth;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
4
gaia/apps/settings/js/modules/navigator/mozPermissionSettings.js
Executable file
4
gaia/apps/settings/js/modules/navigator/mozPermissionSettings.js
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
define(function() {
|
||||
'use strict';
|
||||
return window.navigator.mozPermissionSettings;
|
||||
});
|
||||
106
gaia/apps/settings/js/modules/page_transitions.js
Executable file
106
gaia/apps/settings/js/modules/page_transitions.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* PageTransitions provides transition functions used when navigating panels.
|
||||
*
|
||||
* @module PageTransitions
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
var _sendPanelReady = function _send_panel_ready(oldPanelHash, newPanelHash) {
|
||||
var detail = {
|
||||
previous: oldPanelHash,
|
||||
current: newPanelHash
|
||||
};
|
||||
var event = new CustomEvent('panelready', {detail: detail});
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Typically used with phone size device layouts.
|
||||
*
|
||||
* @alias module:PageTransitions#oneColumn
|
||||
* @param {String} oldPanel
|
||||
* @param {String} newPanel
|
||||
* @param {Function} callback
|
||||
*/
|
||||
oneColumn: function pt_one_column(oldPanel, newPanel, callback) {
|
||||
if (oldPanel === newPanel) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// switch previous/current classes
|
||||
if (oldPanel) {
|
||||
oldPanel.className = newPanel.className ? '' : 'previous';
|
||||
}
|
||||
if (newPanel.className === 'current') {
|
||||
_sendPanelReady(oldPanel && '#' + oldPanel.id, '#' + newPanel.id);
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
newPanel.className = 'current';
|
||||
|
||||
/**
|
||||
* Most browsers now scroll content into view taking CSS transforms into
|
||||
* account. That's not what we want when moving between <section>s,
|
||||
* because the being-moved-to section is offscreen when we navigate to its
|
||||
* #hash. The transitions assume the viewport is always at document 0,0.
|
||||
* So add a hack here to make that assumption true again.
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=803170
|
||||
*/
|
||||
if ((window.scrollX !== 0) || (window.scrollY !== 0)) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
newPanel.addEventListener('transitionend', function paintWait() {
|
||||
newPanel.removeEventListener('transitionend', paintWait);
|
||||
if (oldPanel) {
|
||||
_sendPanelReady('#' + oldPanel.id, '#' + newPanel.id);
|
||||
|
||||
if (oldPanel.className === 'current') {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_sendPanelReady(null, '#' + newPanel.id);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Typically used with tablet size device layouts.
|
||||
*
|
||||
* @alias module:PageTransitions#twoColumn
|
||||
* @param {String} oldPanel
|
||||
* @param {String} newPanel
|
||||
* @param {Function} callback
|
||||
*/
|
||||
twoColumn: function pt_two_column(oldPanel, newPanel, callback) {
|
||||
if (oldPanel === newPanel) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPanel) {
|
||||
oldPanel.className = newPanel.className ? '' : 'previous';
|
||||
newPanel.className = 'current';
|
||||
_sendPanelReady('#' + oldPanel.id, '#' + newPanel.id);
|
||||
} else {
|
||||
newPanel.className = 'current';
|
||||
_sendPanelReady(null, '#' + newPanel.id);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
125
gaia/apps/settings/js/modules/panel.js
Executable file
125
gaia/apps/settings/js/modules/panel.js
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Panel is the basic element for navigation. Which defines Six basic
|
||||
* functions: show, hide, beforeShow, beforeHide, init, and uninit for
|
||||
* navigation. These functions are called by `SettingsService` during the
|
||||
* navigation.
|
||||
* Internal functions onShow, onHide, onBeforeShow, onBeforeHide, onInit,
|
||||
* and onUninit are called respectively in the basic functions.
|
||||
*
|
||||
* @module Panel
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
var _emptyFunc = function panel_emptyFunc() {};
|
||||
|
||||
/**
|
||||
* @alias module:Panel
|
||||
* @param {Object} options
|
||||
* Options are used to override the internal functions.
|
||||
* @returns {Panel}
|
||||
*/
|
||||
var Panel = function ctor_panel(options) {
|
||||
var _initialized = false;
|
||||
|
||||
options = options || {};
|
||||
options.onInit = options.onInit || _emptyFunc;
|
||||
options.onUninit = options.onUninit || _emptyFunc;
|
||||
options.onShow = options.onShow || _emptyFunc;
|
||||
options.onHide = options.onHide || _emptyFunc;
|
||||
options.onBeforeShow = options.onBeforeShow || _emptyFunc;
|
||||
options.onBeforeHide = options.onBeforeHide || _emptyFunc;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get a value that indicates whether the panel has been initialized.
|
||||
*
|
||||
* @alias module:Panel#initialized
|
||||
* @return {Boolean}
|
||||
*/
|
||||
get initialized() {
|
||||
return _initialized;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called at the first time when the beforeShow function is called.
|
||||
*
|
||||
* @alias module:Panel#init
|
||||
* @param {HTMLElement} panel
|
||||
* @param {Object} initOptions
|
||||
*/
|
||||
init: function(panel, initOptions) {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
|
||||
return options.onInit(panel, initOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when cleanup.
|
||||
*
|
||||
* @alias module:Panel#uninit
|
||||
*/
|
||||
uninit: function() {
|
||||
if (!_initialized) {
|
||||
return;
|
||||
}
|
||||
_initialized = false;
|
||||
|
||||
options.onUninit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the panel is navigated into the viewport.
|
||||
*
|
||||
* @alias module:Panel#show
|
||||
* @param {HTMLElement} panel
|
||||
* @param {Object} showOptions
|
||||
*/
|
||||
show: function(panel, showOptions) {
|
||||
// Initialize at the first call to show if necessary.
|
||||
return Promise.resolve(this.init(panel, showOptions)).then(function() {
|
||||
return options.onShow(panel, showOptions);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the panel is navigated out of the viewport.
|
||||
*
|
||||
* @alias module:Panel#hide
|
||||
*/
|
||||
hide: function() {
|
||||
return options.onHide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the panel is about to be navigated to into the viewport.
|
||||
*
|
||||
* @alias module:Panel#beforeShow
|
||||
* @param {HTMLElement} panel
|
||||
* @param {Object} beforeShowOptions
|
||||
*/
|
||||
beforeShow: function(panel, beforeShowOptions) {
|
||||
// Initialize at the first call to beforeShow.
|
||||
return Promise.resolve(this.init(panel, beforeShowOptions)).then(
|
||||
function() {
|
||||
return options.onBeforeShow(panel, beforeShowOptions);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the panel is about to be navigated out of the viewport.
|
||||
*
|
||||
* @alias module:Panel#beforeHide
|
||||
* @param {HTMLElement} panel
|
||||
* @param {Object} beforeShowOptions
|
||||
*/
|
||||
beforeHide: function() {
|
||||
return options.onBeforeHide();
|
||||
}
|
||||
};
|
||||
};
|
||||
return Panel;
|
||||
});
|
||||
112
gaia/apps/settings/js/modules/panel_cache.js
Executable file
112
gaia/apps/settings/js/modules/panel_cache.js
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* PanelCache is a singleton that loads a panel module based on the panel id
|
||||
* and caches the loaded modules.
|
||||
*
|
||||
* @module PanelCache
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var SettingsPanel = require('modules/settings_panel');
|
||||
var LazyLoader = require('shared/lazy_loader');
|
||||
|
||||
var _panelCache = {};
|
||||
var _panelStylesheetsLoaded = false;
|
||||
|
||||
// experiment result shows
|
||||
// load all related styles once is time saver
|
||||
var _loadPanelStylesheetsIfNeeded = function loadPanelCSS() {
|
||||
if (_panelStylesheetsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
LazyLoader.load(['shared/style/action_menu.css',
|
||||
'shared/style/confirm.css',
|
||||
'shared/style/progress_activity.css',
|
||||
'shared/elements/gaia-icons/bidi-helper.css',
|
||||
'shared/elements/gaia_buttons/script.js',
|
||||
'shared/elements/gaia_confirm/script.js',
|
||||
'style/homescreens.css',
|
||||
// Bug 1210284 - disabling achievements until they are on
|
||||
// the roadmap again or completely removed.
|
||||
// 'style/achievements.css',
|
||||
'style/apps.css',
|
||||
'style/screen_lock.css',
|
||||
'style/simcard.css',
|
||||
'style/updates.css',
|
||||
'style/downloads.css',
|
||||
'style/developer_service_workers.css'],
|
||||
function callback() {
|
||||
_panelStylesheetsLoaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
// load styles in idle time after document loaded
|
||||
navigator.addIdleObserver({
|
||||
time: 3,
|
||||
onidle: _loadPanelStylesheetsIfNeeded
|
||||
});
|
||||
|
||||
return {
|
||||
// this is for unit test
|
||||
reset: function spc_reset() {
|
||||
_panelCache = {};
|
||||
_panelStylesheetsLoaded = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the panel module of a specified id. If there is no corresponding
|
||||
* panel module of the id, it returns SettingsPanel.
|
||||
*
|
||||
* @alias module:PanelCache#get
|
||||
* @param {String} panelId
|
||||
* The id of the to be loaded panel.
|
||||
* @param {Function} callback
|
||||
* The function to be called when the panel is loaded.
|
||||
*/
|
||||
get: function spc_get(panelId, callback) {
|
||||
if (!panelId && !callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panelId !== 'root') {
|
||||
_loadPanelStylesheetsIfNeeded();
|
||||
}
|
||||
|
||||
var cachedPanel = _panelCache[panelId];
|
||||
if (cachedPanel) {
|
||||
if (callback) {
|
||||
callback(cachedPanel);
|
||||
}
|
||||
} else {
|
||||
// Get the path of the panel creation function
|
||||
var panelElement = document.getElementById(panelId);
|
||||
if (panelElement) {
|
||||
var pathElement = panelElement.querySelector('panel');
|
||||
var path = pathElement ? pathElement.dataset.path : null;
|
||||
|
||||
var panelFuncLoaded = function(panelFunc) {
|
||||
var panel = panelFunc();
|
||||
_panelCache[panelId] = panel;
|
||||
if (callback) {
|
||||
callback(panel);
|
||||
}
|
||||
};
|
||||
|
||||
if (path) {
|
||||
require([path], function(panelFunc) {
|
||||
// Create a new panel object for static panels.
|
||||
panelFuncLoaded(panelFunc ? panelFunc : SettingsPanel);
|
||||
});
|
||||
} else {
|
||||
panelFuncLoaded(SettingsPanel);
|
||||
}
|
||||
} else {
|
||||
if (callback) {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
560
gaia/apps/settings/js/modules/panel_utils.js
Executable file
560
gaia/apps/settings/js/modules/panel_utils.js
Executable file
|
|
@ -0,0 +1,560 @@
|
|||
/**
|
||||
* PanelUtils is a singleton that defines panel related utility functions.
|
||||
*
|
||||
* @module PanelUtils
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Settings = require('settings');
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var LazyLoader = require('shared/lazy_loader');
|
||||
|
||||
var _settings = navigator.mozSettings;
|
||||
|
||||
/**
|
||||
* These so-called "dialog boxes" are just standard Settings panels
|
||||
* (<section role="region" />) with reset/submit buttons: these buttons both
|
||||
* return to the previous panel when clicked, and each button has its own
|
||||
* (optional) callback.
|
||||
*/
|
||||
function openDialog(dialogID, onSubmit, onReset) {
|
||||
if ('#' + dialogID === Settings.currentPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = Settings.currentPanel;
|
||||
|
||||
// Load dialog contents and show it.
|
||||
Settings.currentPanel = dialogID;
|
||||
|
||||
var dialog = document.getElementById(dialogID);
|
||||
var submit = dialog.querySelector('[type=submit]');
|
||||
if (submit) {
|
||||
submit.onclick = function onsubmit() {
|
||||
if (typeof onSubmit === 'function') {
|
||||
(onSubmit.bind(dialog))();
|
||||
}
|
||||
Settings.currentPanel = origin; // hide dialog box
|
||||
};
|
||||
}
|
||||
|
||||
var reset = dialog.querySelector('[type=reset]');
|
||||
if (reset) {
|
||||
reset.onclick = function onreset() {
|
||||
if (typeof onReset === 'function') {
|
||||
(onReset.bind(dialog))();
|
||||
}
|
||||
Settings.currentPanel = origin; // hide dialog box
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* Open a link with a web activity */
|
||||
function openLink(url) {
|
||||
if (url.startsWith('tel:')) { // dial a phone number
|
||||
var telActivity = new window.MozActivity({
|
||||
name: 'dial',
|
||||
data: { type: 'webtelephony/number', number: url.substr(4) }
|
||||
});
|
||||
// For workaround jshint.
|
||||
telActivity.onsuccess = function() {};
|
||||
} else if (!url.startsWith('#')) { // browse a URL
|
||||
var linkActivity = new window.MozActivity({
|
||||
name: 'view',
|
||||
data: { type: 'url', url: url }
|
||||
});
|
||||
// For workaround jshint.
|
||||
linkActivity.onsuccess = function() {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog of a specified id.
|
||||
*
|
||||
* @param {String} dialogID
|
||||
* The id of the dialog element.
|
||||
*/
|
||||
var _openDialog = function pu_openDialog(dialogID) {
|
||||
var dialog = document.getElementById(dialogID);
|
||||
var fields = Array.prototype.slice.call(
|
||||
dialog.querySelectorAll('[data-setting]:not([data-ignore])'));
|
||||
|
||||
var updateInput = function(lock, input) {
|
||||
var key = input.dataset.setting;
|
||||
var request = lock.get(key);
|
||||
|
||||
request.onsuccess = function() {
|
||||
switch (input.type) {
|
||||
case 'gaia-radio':
|
||||
input.checked = (input.value == request.result[key]);
|
||||
break;
|
||||
case 'checkbox':
|
||||
case 'gaia-checkbox':
|
||||
input.checked = request.result[key] || false;
|
||||
break;
|
||||
case 'select-one':
|
||||
input.value = request.result[key] || '';
|
||||
break;
|
||||
default:
|
||||
input.value = request.result[key] || '';
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* In Settings dialog boxes, we don't want the input fields to be preset
|
||||
* by Settings.init() and we don't want them to set the related settings
|
||||
* without any user validation.
|
||||
*
|
||||
* So instead of assigning a `name' attribute to these inputs, a
|
||||
* `data-setting' attribute is used and the input values are set
|
||||
* explicitely when the dialog is shown. If the dialog is validated
|
||||
* (submit), their values are stored into B2G settings.
|
||||
*
|
||||
* XXX warning:
|
||||
* this only supports text/password/radio/select/radio input types.
|
||||
*/
|
||||
|
||||
// initialize all setting fields in the dialog box
|
||||
// XXX for fields being added by lazily loaded script,
|
||||
// it would have to initialize the fields again themselves.
|
||||
function reset() {
|
||||
if (_settings) {
|
||||
var lock = _settings.createLock();
|
||||
fields.forEach(updateInput.bind(null, lock));
|
||||
}
|
||||
}
|
||||
|
||||
// validate all settings in the dialog box
|
||||
function submit() {
|
||||
if (_settings) {
|
||||
// Update the fields node list to include dynamically added fields
|
||||
fields = Array.prototype.slice.call(
|
||||
dialog.querySelectorAll('[data-setting]:not([data-ignore])'));
|
||||
var cset = {}, key;
|
||||
var lock = _settings.createLock();
|
||||
|
||||
fields.forEach(function(input) {
|
||||
key = input.dataset.setting;
|
||||
switch (input.type) {
|
||||
case 'gaia-radio':
|
||||
if (input.checked) {
|
||||
cset[key] = input.value;
|
||||
}
|
||||
break;
|
||||
case 'checkbox':
|
||||
case 'gaia-checkbox':
|
||||
cset[key] = input.checked;
|
||||
break;
|
||||
default:
|
||||
cset[key] = input.value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
lock.set(cset);
|
||||
}
|
||||
}
|
||||
|
||||
reset(); // preset all fields before opening the dialog
|
||||
openDialog(dialogID, submit);
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* The function parses all links in the panel and adds corresponding
|
||||
* handlers.
|
||||
* There are three types of links:
|
||||
* - a[href^="http"]: External link
|
||||
* - a[href^="tel"]: External link
|
||||
* - [data-href]: Generic dialog link and settings-specific dialog link
|
||||
*
|
||||
* @alias module:PanelUtils#activate
|
||||
* @param {HTMLElement} panel
|
||||
* The root element of the panel.
|
||||
*/
|
||||
activate: function pu_activate(panel) {
|
||||
// activate all scripts
|
||||
var scripts = panel.getElementsByTagName('script');
|
||||
var scripts_src = Array.prototype.map.call(scripts, function(script) {
|
||||
return script.getAttribute('src');
|
||||
});
|
||||
LazyLoader.load(scripts_src);
|
||||
|
||||
var _onclick = function(evt) {
|
||||
if (!this.dataset.href) {
|
||||
this.dataset.href = this.href;
|
||||
this.href = '#';
|
||||
}
|
||||
var href = this.dataset.href;
|
||||
if (!href.startsWith('#')) { // external link
|
||||
evt.target.blur();
|
||||
openLink(href);
|
||||
} else if (!href.endsWith('Settings')) { // generic dialog
|
||||
openDialog(href.substr(1));
|
||||
} else { // Settings-specific dialog box
|
||||
_openDialog(href.substr(1));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// activate all links
|
||||
var rule = 'a[href^="http"], a[href^="tel"], [data-href]';
|
||||
var links = panel.querySelectorAll(rule);
|
||||
var i, count;
|
||||
|
||||
for (i = 0, count = links.length; i < count; i++) {
|
||||
if (links[i].tagName !== 'GAIA-HEADER') {
|
||||
links[i].addEventListener('click', _onclick);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup back listener
|
||||
var backHeader = panel.querySelector('gaia-header[action="back"]');
|
||||
var href = backHeader && backHeader.dataset.href;
|
||||
if (backHeader && href) {
|
||||
backHeader.addEventListener('action', function() {
|
||||
Settings.currentPanel = this.dataset.href;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The function presets elements with the settings values.
|
||||
* The supported formats are:
|
||||
* - An input element with a "name" attribute and its value is a settings
|
||||
* key.
|
||||
* - A select element with a "name" attribute and its value is a settings
|
||||
* key.
|
||||
* - A span element with a "data-name" attribute and its value is a settings
|
||||
* key.
|
||||
*
|
||||
* @alias module:PanelUtils#preset
|
||||
* @param {HTMLElement} panel
|
||||
* The root element of the panel.
|
||||
*/
|
||||
preset: function pu_preset(panel) {
|
||||
SettingsCache.getSettings(function(result) {
|
||||
panel = panel || document;
|
||||
|
||||
// preset all checkboxes
|
||||
var rule = 'input[type="checkbox"]:not([data-ignore]), gaia-switch, ' +
|
||||
'gaia-checkbox';
|
||||
var checkboxes = panel.querySelectorAll(rule);
|
||||
var i, count, key;
|
||||
for (i = 0, count = checkboxes.length; i < count; i++) {
|
||||
key = checkboxes[i].name;
|
||||
if (key && result[key] !== undefined) {
|
||||
checkboxes[i].checked = !!result[key];
|
||||
}
|
||||
}
|
||||
|
||||
// remove initial class so the swich animation will apply
|
||||
// on these toggles if user interact with it.
|
||||
setTimeout(function() {
|
||||
for (i = 0, count = checkboxes.length; i < count; i++) {
|
||||
if (checkboxes[i].classList.contains('initial')) {
|
||||
checkboxes[i].classList.remove('initial');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// preset all radio buttons
|
||||
rule = 'gaia-radio:not([data-ignore])';
|
||||
var radios = panel.querySelectorAll(rule);
|
||||
for (i = 0, count = radios.length; i < count; i++) {
|
||||
key = radios[i].name;
|
||||
if (key && result[key] !== undefined) {
|
||||
radios[i].checked = (result[key] === radios[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
// preset all text inputs
|
||||
rule = 'input[type="text"]:not([data-ignore])';
|
||||
var texts = panel.querySelectorAll(rule);
|
||||
for (i = 0, count = texts.length; i < count; i++) {
|
||||
key = texts[i].name;
|
||||
if (key && result[key] !== undefined) {
|
||||
texts[i].value = result[key];
|
||||
}
|
||||
}
|
||||
|
||||
// preset all range inputs
|
||||
rule = 'input[type="range"]:not([data-ignore])';
|
||||
var ranges = panel.querySelectorAll(rule);
|
||||
for (i = 0, count = ranges.length; i < count; i++) {
|
||||
key = ranges[i].name;
|
||||
if (key && result[key] !== undefined) {
|
||||
ranges[i].value = parseFloat(result[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// preset all select
|
||||
var selects = panel.querySelectorAll('select');
|
||||
for (i = 0, count = selects.length; i < count; i++) {
|
||||
var select = selects[i];
|
||||
key = select.name;
|
||||
if (key && result[key] !== undefined) {
|
||||
var value = result[key];
|
||||
var option = 'option[value="' + value + '"]';
|
||||
var selectOption = select.querySelector(option);
|
||||
if (selectOption) {
|
||||
selectOption.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preset all span with data-name fields
|
||||
rule = '[data-name]:not([data-ignore])';
|
||||
var spanFields = panel.querySelectorAll(rule);
|
||||
for (i = 0, count = spanFields.length; i < count; i++) {
|
||||
key = spanFields[i].dataset.name;
|
||||
|
||||
// Mulet has no physical firmware metadata. Use the Vulpes labels
|
||||
// instead of exposing the generic 1.0.0 desktop fallback.
|
||||
if (key === 'deviceinfo.os' &&
|
||||
(!result[key] || result[key] === '1.0.0')) {
|
||||
result[key] = 'Firefox OS 2.7';
|
||||
}
|
||||
if (key === 'deviceinfo.software' &&
|
||||
(!result[key] || result[key] === '1.0.0')) {
|
||||
result[key] = 'Project Vulpes 2.7';
|
||||
}
|
||||
|
||||
// XXX intentionally checking for the string 'undefined',
|
||||
// see bug 880617
|
||||
if (key && result[key] && result[key] != 'undefined') {
|
||||
// check whether this setting comes from a select option
|
||||
// (it may be in a different panel, so query the whole document)
|
||||
rule = '[data-setting="' + key + '"] ' +
|
||||
'[value="' + result[key] + '"]';
|
||||
var option_span = document.querySelector(rule);
|
||||
if (option_span) {
|
||||
spanFields[i].setAttribute('data-l10n-id',
|
||||
option_span.getAttribute('data-l10n-id'));
|
||||
} else {
|
||||
spanFields[i].removeAttribute('data-l10n-id');
|
||||
spanFields[i].textContent = result[key];
|
||||
}
|
||||
} else { // result[key] is undefined
|
||||
switch (key) {
|
||||
//XXX bug 816899 will also provide 'deviceinfo.software' from
|
||||
// Gecko which is {os name + os version}
|
||||
case 'deviceinfo.software':
|
||||
document.l10n.setAttributes(spanFields[i],
|
||||
'deviceInfo_software',
|
||||
{ os: result['deviceinfo.os'] });
|
||||
break;
|
||||
|
||||
//XXX workaround request from bug 808892 comment 22
|
||||
// hide this field if it's undefined/empty.
|
||||
case 'deviceinfo.firmware_revision':
|
||||
spanFields[i].parentNode.hidden = true;
|
||||
break;
|
||||
|
||||
case 'deviceinfo.mac':
|
||||
spanFields[i].setAttribute('data-l10n-id', 'macUnavailable');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unhide items according to preferences.
|
||||
rule = '[data-show-name]:not([data-ignore])';
|
||||
var hiddenItems = panel.querySelectorAll(rule);
|
||||
for (i = 0; i < hiddenItems.length; i++) {
|
||||
key = hiddenItems[i].dataset.showName;
|
||||
hiddenItems[i].hidden = !result[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When a link element is clicked, the function navigates the app to the
|
||||
* panel of the id specified by the "href" attribute of the element.
|
||||
*
|
||||
* @alias module:PanelUtils#onLinkClick
|
||||
* @param {Event} event
|
||||
*/
|
||||
onLinkClick: function pu_onLinkClick(event) {
|
||||
var target = event.target;
|
||||
var href;
|
||||
|
||||
if (target.classList.contains('icon-back')) {
|
||||
href = target.parentNode.getAttribute('href');
|
||||
} else {
|
||||
var nodeName = target.nodeName.toLowerCase();
|
||||
if (nodeName != 'a') {
|
||||
return;
|
||||
}
|
||||
href = target.getAttribute('href');
|
||||
}
|
||||
// skips the following case:
|
||||
// 1. no href, which is not panel
|
||||
// 2. href is not a hash which is not a panel
|
||||
// 3. href equals # which is translated with loadPanel function, they are
|
||||
// external links.
|
||||
if (!href || !href.startsWith('#') || href === '#') {
|
||||
return;
|
||||
}
|
||||
|
||||
Settings.currentPanel = href;
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Respond to settings changes.
|
||||
* The supported formats are:
|
||||
* - An input element with a "name" attribute and its value is a settings
|
||||
* key.
|
||||
* - A select element with a "name" attribute and its value is a settings
|
||||
* key.
|
||||
* - A span element with a "data-name" attribute and its value is a settings
|
||||
* key.
|
||||
* - Elements with a "data-show-name" attribute. It hides the element when
|
||||
* the value is false and vice versa.
|
||||
*
|
||||
* @alias module:PanelUtils#onSettingsChange
|
||||
* @param {HTMLElement} panel
|
||||
* @param {Event} event
|
||||
*/
|
||||
onSettingsChange: function pu_onSettingsChange(panel, event) {
|
||||
var key = event.settingName;
|
||||
var value = event.settingValue;
|
||||
var i, count;
|
||||
|
||||
// update <span> values when the corresponding setting is changed
|
||||
var rule = '[data-name="' + key + '"]:not([data-ignore])';
|
||||
var spanField = panel.querySelector(rule);
|
||||
if (spanField) {
|
||||
// check whether this setting comes from a select option
|
||||
var options = panel.querySelector('select[data-setting="' + key + '"]');
|
||||
if (options) {
|
||||
// iterate option matching
|
||||
for (i = 0, count = options.length; i < count; i++) {
|
||||
if (options[i] && options[i].value === value) {
|
||||
spanField.dataset.l10nId = options[i].dataset.l10nId;
|
||||
spanField.textContent = options[i].textContent;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
spanField.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
// hide or unhide items
|
||||
rule = '[data-show-name="' + key + '"]:not([data-ignore])';
|
||||
var items = document.querySelectorAll(rule);
|
||||
for (i = 0; i < items.length; i++) {
|
||||
items[i].hidden = !value;
|
||||
}
|
||||
|
||||
// update <input> values when the corresponding setting is changed
|
||||
var inputs = [].slice.call(panel.querySelectorAll(
|
||||
`input[name="${key}"],
|
||||
gaia-switch[name="${key}"],
|
||||
gaia-checkbox[name="${key}"],
|
||||
gaia-radio[name="${key}"]`));
|
||||
if (!inputs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputs.forEach((input) => {
|
||||
switch (input.type) {
|
||||
case 'gaia-switch':
|
||||
case 'gaia-checkbox':
|
||||
case 'checkbox':
|
||||
case 'switch':
|
||||
value = !!value;
|
||||
if (input.checked === value) {
|
||||
return;
|
||||
}
|
||||
input.checked = value;
|
||||
break;
|
||||
case 'range':
|
||||
if (input.value === value) {
|
||||
return;
|
||||
}
|
||||
input.value = value;
|
||||
break;
|
||||
case 'select':
|
||||
for (i = 0, count = input.options.length; i < count; i++) {
|
||||
if (input.options[i].value === value) {
|
||||
input.options[i].selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'gaia-radio':
|
||||
input.checked = (input.value === value);
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Respond to settings changes.
|
||||
* The supported formats are:
|
||||
* - An input element with a "name" attribute and its value is a settings
|
||||
* key.
|
||||
* - A select element with a "name" attribute and its value is a settings
|
||||
* key.
|
||||
* - A span element with a "data-name" attribute and its value is a settings
|
||||
* key.
|
||||
* - Elements with a "data-show-name" attribute. It hides the element when
|
||||
* the value is false and vice versa.
|
||||
*
|
||||
* @alias module:PanelUtils#onInputChange
|
||||
* @param {HTMLElement} panel
|
||||
* @param {Event} event
|
||||
*/
|
||||
onInputChange: function pu_onInputChange(event) {
|
||||
var input = event.target;
|
||||
var type = input.type || input.nodeName.toLowerCase();
|
||||
var key = input.name;
|
||||
|
||||
//XXX should we check data-ignore here?
|
||||
if (!key || !_settings || event.type != 'change') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Not touching <input> with data-setting attribute here
|
||||
// because they would have to be committed with a explicit "submit"
|
||||
// of their own dialog.
|
||||
if (input.dataset.setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
var value;
|
||||
switch (type) {
|
||||
case 'gaia-switch':
|
||||
case 'gaia-checkbox':
|
||||
case 'checkbox':
|
||||
case 'switch':
|
||||
value = input.checked; // boolean
|
||||
break;
|
||||
case 'range':
|
||||
// Bug 906296:
|
||||
// We parseFloat() once to be able to round to 1 digit, then
|
||||
// we parseFloat() again to make sure to store a Number and
|
||||
// not a String, otherwise this will make Gecko unable to
|
||||
// apply new settings.
|
||||
value = parseFloat(parseFloat(input.value).toFixed(1)); // float
|
||||
break;
|
||||
case 'select-one':
|
||||
case 'gaia-radio':
|
||||
case 'text':
|
||||
case 'password':
|
||||
value = input.value; // default as text
|
||||
if (input.dataset.valueType === 'integer') { // integer
|
||||
value = parseInt(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var cset = {}; cset[key] = value;
|
||||
_settings.createLock().set(cset);
|
||||
}
|
||||
};
|
||||
});
|
||||
147
gaia/apps/settings/js/modules/settings_cache.js
Executable file
147
gaia/apps/settings/js/modules/settings_cache.js
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* SettingsCache is a singleton that caches mozSettings values for fast
|
||||
* access.
|
||||
*
|
||||
* @module SettingsCache
|
||||
*/
|
||||
define(function() {
|
||||
'use strict';
|
||||
var _settings = window.navigator.mozSettings;
|
||||
|
||||
// Cache of all current settings values. There's some large stuff
|
||||
// in here, but not much useful can be done with the settings app
|
||||
// without these, so we keep this around most of the time.
|
||||
var _settingsCache = null;
|
||||
|
||||
// True when a request has already been made to fill the settings
|
||||
// cache. When this is true, no further get("*") requests should be
|
||||
// made; instead, pending callbacks should be added to
|
||||
// _pendingSettingsCallbacks.
|
||||
var _settingsCacheRequestSent = null;
|
||||
|
||||
// There can be race conditions in which we need settings values,
|
||||
// but haven't filled the cache yet. This array tracks those
|
||||
// listeners.
|
||||
var _pendingSettingsCallbacks = [];
|
||||
|
||||
var _callbacks = [];
|
||||
|
||||
var _onSettingsChange = function sc_onSettingsChange(event) {
|
||||
var key = event.settingName;
|
||||
var value = event.settingValue;
|
||||
|
||||
// Always update the cache if it's present, even if the DOM
|
||||
// isn't loaded yet.
|
||||
if (_settingsCache) {
|
||||
_settingsCache[key] = value;
|
||||
}
|
||||
|
||||
_callbacks.forEach(function(callback) {
|
||||
callback(event);
|
||||
});
|
||||
};
|
||||
|
||||
if (_settings) {
|
||||
_settings.onsettingchange = _onSettingsChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event reporting that a setting value is changed.
|
||||
*
|
||||
* @event module:SettingsCache#settingsChange
|
||||
* @property {MozSettingsEvent} event
|
||||
*/
|
||||
var SettingsCache = {
|
||||
// the reset function is for unit tests
|
||||
reset: function sc_reset() {
|
||||
_settings = window.navigator.mozSettings;
|
||||
if (_settings) {
|
||||
_settings.onsettingchange = _onSettingsChange;
|
||||
}
|
||||
_settingsCache = null;
|
||||
_settingsCacheRequestSent = null;
|
||||
_pendingSettingsCallbacks = [];
|
||||
_callbacks = [];
|
||||
},
|
||||
|
||||
get cache() {
|
||||
return _settingsCache;
|
||||
},
|
||||
|
||||
/**
|
||||
* Where callback is a function to be called with a request object for a
|
||||
* successful fetch of settings values, when those values are ready.
|
||||
*
|
||||
* @alias module:SettingsCache#getSettings
|
||||
* @param {Function} callback
|
||||
*/
|
||||
getSettings: function sc_getSettings(callback) {
|
||||
if (!_settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settingsCache && callback) {
|
||||
// Fast-path that we hope to always hit: our settings cache is
|
||||
// already available, so invoke the callback now.
|
||||
callback(_settingsCache);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_settingsCacheRequestSent && !_settingsCache) {
|
||||
_settingsCacheRequestSent = true;
|
||||
var lock = _settings.createLock();
|
||||
var request = lock.get('*');
|
||||
request.onsuccess = function(e) {
|
||||
var result = request.result;
|
||||
var cachedResult = {};
|
||||
for (var attr in result) {
|
||||
cachedResult[attr] = result[attr];
|
||||
}
|
||||
_settingsCache = cachedResult;
|
||||
var cbk;
|
||||
while ((cbk = _pendingSettingsCallbacks.pop())) {
|
||||
cbk(result);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (callback) {
|
||||
_pendingSettingsCallbacks.push(callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @alias module:SettingsCache#addEventListener
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
addEventListener: function sc_addEventListener(eventName, callback) {
|
||||
if (eventName !== 'settingsChange') {
|
||||
return;
|
||||
}
|
||||
var index = _callbacks.indexOf(callback);
|
||||
if (index === -1) {
|
||||
_callbacks.push(callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @alias module:SettingsCache#removeEventListener
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
removeEventListener: function sc_removeEventListsner(eventName, callback) {
|
||||
if (eventName !== 'settingsChange') {
|
||||
return;
|
||||
}
|
||||
var index = _callbacks.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
_callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Make a request for settings to warm the cache, since we need it
|
||||
// very soon in startup after the DOM is available.
|
||||
SettingsCache.getSettings(null);
|
||||
return SettingsCache;
|
||||
});
|
||||
121
gaia/apps/settings/js/modules/settings_panel.js
Executable file
121
gaia/apps/settings/js/modules/settings_panel.js
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* SettingsPanel extends Panel with basic settings services. It presets the UI
|
||||
* elements based on the values in mozSettings and add listeners responding to
|
||||
* mozSettings changes in onReady. In onInit it parses the panel element for
|
||||
* activating links. It also removes listeners in onDone so that we can avoid
|
||||
* unwanted UI updates when the panel is outside of the viewport.
|
||||
*
|
||||
* @module SettingsPanel
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Panel = require('modules/panel');
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var PanelUtils = require('modules/panel_utils');
|
||||
|
||||
var _emptyFunc = function panel_emptyFunc() {};
|
||||
|
||||
/**
|
||||
* @alias module:SettingsPanel
|
||||
* @param {Object} options
|
||||
* Options are used to override the internal functions of
|
||||
* Panel.
|
||||
* @returns {SettingsPanel}
|
||||
*/
|
||||
var SettingsPanel = function ctor_SettingsPanel(options) {
|
||||
/**
|
||||
* The root element of the panel.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
var _panel = null;
|
||||
|
||||
/**
|
||||
* The handler is called when settings change.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
var _settingsChangeHandler = function(event) {
|
||||
PanelUtils.onSettingsChange(_panel, event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add listeners to make the panel be able to respond to setting changes
|
||||
* and user interactions.
|
||||
*
|
||||
* @param {HTMLElement} panel
|
||||
*/
|
||||
var _addListeners = function panel_addListeners(panel) {
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsCache.addEventListener('settingsChange',
|
||||
_settingsChangeHandler);
|
||||
panel.addEventListener('change', PanelUtils.onInputChange);
|
||||
panel.addEventListener('click', PanelUtils.onLinkClick);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all listeners.
|
||||
*
|
||||
* @param {HTMLElement} panel
|
||||
*/
|
||||
var _removeListeners = function panel_removeListeners(panel) {
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsCache.removeEventListener('settingsChange',
|
||||
_settingsChangeHandler);
|
||||
panel.removeEventListener('change', PanelUtils.onInputChange);
|
||||
panel.removeEventListener('click', PanelUtils.onLinkClick);
|
||||
};
|
||||
|
||||
options = options || {};
|
||||
options.onInit = options.onInit || _emptyFunc;
|
||||
options.onUninit = options.onUninit || _emptyFunc;
|
||||
options.onShow = options.onShow || _emptyFunc;
|
||||
options.onHide = options.onHide || _emptyFunc;
|
||||
options.onBeforeShow = options.onBeforeShow || _emptyFunc;
|
||||
options.onBeforeHide = options.onBeforeHide || _emptyFunc;
|
||||
|
||||
return Panel({
|
||||
onInit: function(panel, initOptions) {
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
_panel = panel;
|
||||
PanelUtils.activate(panel);
|
||||
|
||||
return options.onInit(panel, initOptions);
|
||||
},
|
||||
onUninit: function() {
|
||||
_removeListeners(_panel);
|
||||
_panel = null;
|
||||
options.onUninit();
|
||||
},
|
||||
onShow: function(panel, showOptions) {
|
||||
return options.onShow(panel, showOptions);
|
||||
},
|
||||
onHide: function() {
|
||||
// Remove listeners.
|
||||
_removeListeners(_panel);
|
||||
|
||||
return options.onHide();
|
||||
},
|
||||
onBeforeShow: function(panel, beforeShowOptions) {
|
||||
// Preset the panel every time when it is presented.
|
||||
PanelUtils.preset(panel);
|
||||
_addListeners(panel);
|
||||
return options.onBeforeShow(panel, beforeShowOptions);
|
||||
},
|
||||
onBeforeHide: function() {
|
||||
return options.onBeforeHide();
|
||||
}
|
||||
});
|
||||
};
|
||||
return SettingsPanel;
|
||||
});
|
||||
364
gaia/apps/settings/js/modules/settings_service.js
Executable file
364
gaia/apps/settings/js/modules/settings_service.js
Executable file
|
|
@ -0,0 +1,364 @@
|
|||
/**
|
||||
* SettingsService is a singleton that provides the navigation service. It
|
||||
* gets the corresponding panel module from PanelCache and call to its basic
|
||||
* functions when navigating.
|
||||
*
|
||||
* @module SettingsService
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var PageTransitions = require('modules/page_transitions');
|
||||
var PanelCache = require('modules/panel_cache');
|
||||
var ScreenLayout = require('shared/screen_layout');
|
||||
var LazyLoader = require('shared/lazy_loader');
|
||||
var Settings = require('settings');
|
||||
|
||||
var _rootPanelId = null;
|
||||
/**
|
||||
* _currentNavigation caches information of the current panel including id,
|
||||
* element, module, and options.
|
||||
*/
|
||||
var _currentNavigation = null;
|
||||
var _navigating = false;
|
||||
var _pendingNavigationRequest = null;
|
||||
|
||||
var _cachedNavigation = null;
|
||||
var _cachedNavigationOptions = {};
|
||||
|
||||
var _activityHandler = null;
|
||||
|
||||
var _loadModulesForSubPanelsPromise = null;
|
||||
|
||||
var _getAppNameToLink = function ss_get_app_name_to_link(panelId) {
|
||||
var reAppName = /app:(\w+)/;
|
||||
var name = reAppName.exec(panelId);
|
||||
return name && name[1];
|
||||
};
|
||||
|
||||
var _getAppInfo = function ss_get_app_info(appName) {
|
||||
// We can customize the path for specific apps
|
||||
var _supportedAppInFrame = {
|
||||
keyboard: {},
|
||||
bluetooth: {}
|
||||
};
|
||||
|
||||
var appInfo = _supportedAppInFrame[appName];
|
||||
if (!appInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var prefix = 'app://' + appName + '.gaiamobile.org/';
|
||||
var defaultSrc = prefix + 'settings.html';
|
||||
var appMozapp = prefix + 'manifest.webapp';
|
||||
var appSrc = appInfo.src ? prefix + appInfo.src : defaultSrc;
|
||||
|
||||
return {
|
||||
src: appSrc,
|
||||
mozapp: appMozapp
|
||||
};
|
||||
};
|
||||
|
||||
var _isTabletAndLandscape = function ss_is_tablet_and_landscape() {
|
||||
return ScreenLayout.getCurrentLayout('tabletAndLandscaped');
|
||||
};
|
||||
|
||||
var _retriveParentPanelId = function ss_retriveParentPanelId(panelId) {
|
||||
var headerSelector = '#' + panelId + ' > gaia-header';
|
||||
var header = document.querySelector(headerSelector);
|
||||
return (header && header.dataset.href || '').replace('#', '');
|
||||
};
|
||||
|
||||
var _shallCloseActivity = function ss_shallCloseActivity(panelId) {
|
||||
// If we're handling an activity and the 'back' button is hit, close the
|
||||
// activity if the panel id to be navigated equals the parent panel id.
|
||||
|
||||
// This is for the root panel
|
||||
if (panelId === 'close') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_currentNavigation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the parent panel id of the current panel.
|
||||
var parentPanelId = _retriveParentPanelId(_currentNavigation.panelId);
|
||||
|
||||
// Close the activity if the current panel is the original target panel,
|
||||
// and the new panel is the parent panel of the current panel.
|
||||
return _currentNavigation.panelId === _activityHandler.targetPanelId &&
|
||||
panelId === parentPanelId;
|
||||
};
|
||||
|
||||
var _transit = function ss_transit(oldPanel, newPanel, callback) {
|
||||
var promise = new Promise(function(resolve) {
|
||||
var wrappedCallback = function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
if (_isTabletAndLandscape()) {
|
||||
PageTransitions.twoColumn(oldPanel, newPanel, wrappedCallback);
|
||||
} else {
|
||||
PageTransitions.oneColumn(oldPanel, newPanel, wrappedCallback);
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
var _loadPanel = function ss_loadPanel(panelId, callback) {
|
||||
var panelElement = document.getElementById(panelId);
|
||||
if (panelElement.dataset.rendered) { // already initialized
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
panelElement.dataset.rendered = true;
|
||||
|
||||
// XXX remove SubPanel loader once sub panel are modulized
|
||||
if (panelElement.dataset.requireSubPanels) {
|
||||
// load the panel and its sub-panels (dependencies)
|
||||
// (load the main panel last because it contains the scripts)
|
||||
var selector = 'section[id^="' + panelElement.id + '-"]';
|
||||
var subPanels = document.querySelectorAll(selector);
|
||||
for (var i = 0, il = subPanels.length; i < il; i++) {
|
||||
LazyLoader.load([subPanels[i]]);
|
||||
}
|
||||
LazyLoader.load([panelElement], callback);
|
||||
} else {
|
||||
LazyLoader.load([panelElement], callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var _loadModulesForSubPanels = function ss_loadModules(panelId) {
|
||||
if (panelId === _rootPanelId) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
if (!_loadModulesForSubPanelsPromise) {
|
||||
_loadModulesForSubPanelsPromise = new Promise(function(resolve) {
|
||||
require(['shared/async_storage'], resolve);
|
||||
});
|
||||
}
|
||||
return _loadModulesForSubPanelsPromise;
|
||||
}
|
||||
};
|
||||
|
||||
var _onVisibilityChange = function ss_onVisibilityChange() {
|
||||
_handleVisibilityChange(!document.hidden);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the app becomes invisible, we should call to beforeHide and hide
|
||||
* functions of the current panel. When the app becomes visible, we should
|
||||
* call to beforeShow and show functions of the current panel with the
|
||||
* cached options.
|
||||
*/
|
||||
var _handleVisibilityChange = function ss_onVisibilityChange(visible) {
|
||||
if (!_currentNavigation) {
|
||||
return;
|
||||
}
|
||||
|
||||
var panel = _currentNavigation.panel;
|
||||
var element = _currentNavigation.panelElement;
|
||||
var options = _currentNavigation.options;
|
||||
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
panel.beforeShow(element, options);
|
||||
panel.show(element, options);
|
||||
} else {
|
||||
panel.beforeHide();
|
||||
panel.hide();
|
||||
}
|
||||
};
|
||||
|
||||
var _navigate = function ss_navigate(panelId, options, callback) {
|
||||
// Early return if the panel to be navigated is the same as the
|
||||
// current one.
|
||||
if (_currentNavigation && _currentNavigation.panelId === panelId) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
_loadPanel(panelId, function() {
|
||||
PanelCache.get(panelId, function(panel) {
|
||||
var newPanelElement = document.getElementById(panelId);
|
||||
var currentPanelId =
|
||||
_currentNavigation && _currentNavigation.panelId;
|
||||
var currentPanelElement =
|
||||
_currentNavigation && _currentNavigation.panelElement;
|
||||
var currentPanel = _currentNavigation && _currentNavigation.panel;
|
||||
|
||||
// Keep these to make sure we can use when going back
|
||||
_cachedNavigation = _currentNavigation;
|
||||
_cachedNavigationOptions = options;
|
||||
|
||||
// Prepare options and calls to the panel object's before
|
||||
// show function.
|
||||
options = options || {};
|
||||
|
||||
// 0. start the chain
|
||||
_loadModulesForSubPanels(panelId)
|
||||
// 1. beforeHide previous panel
|
||||
.then(function() {
|
||||
// We don't deactivate the root panel.
|
||||
if (currentPanel && currentPanelId !== _rootPanelId) {
|
||||
return currentPanel.beforeHide();
|
||||
}
|
||||
})
|
||||
// 2. beforeShow next panel
|
||||
.then(function() {
|
||||
return panel.beforeShow(newPanelElement, options);
|
||||
})
|
||||
// 3. do the transition
|
||||
.then(function() {
|
||||
return _transit(currentPanelElement, newPanelElement);
|
||||
})
|
||||
// 4. hide previous panel
|
||||
.then(function() {
|
||||
// We don't deactivate the root panel.
|
||||
if (currentPanel && currentPanelId !== _rootPanelId) {
|
||||
return currentPanel.hide();
|
||||
}
|
||||
})
|
||||
// 5. show next panel
|
||||
.then(function() {
|
||||
return panel.show(newPanelElement, options);
|
||||
})
|
||||
// 6. keep information
|
||||
.then(function() {
|
||||
// Update the current navigation object
|
||||
_currentNavigation = {
|
||||
panelId: panelId,
|
||||
panelElement: newPanelElement,
|
||||
panel: panel,
|
||||
options: options
|
||||
};
|
||||
|
||||
// XXX we need to remove this line in the future
|
||||
// to make sure we won't manipulate Settings
|
||||
// directly
|
||||
Settings._currentPanel = '#' + panelId;
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
reset: function ss_reset() {
|
||||
_rootPanelId = null;
|
||||
_currentNavigation = null;
|
||||
_cachedNavigation = null;
|
||||
_cachedNavigationOptions = {};
|
||||
_activityHandler = null;
|
||||
_navigating = false;
|
||||
_pendingNavigationRequest = null;
|
||||
window.removeEventListener('visibilitychange', _onVisibilityChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* Init SettingsService.
|
||||
*
|
||||
* @alias module:SettingsService#init
|
||||
* @param {Object} options
|
||||
* @param {String} options.rootPanelId
|
||||
* Panel with the specified id is assumed to be be kept on
|
||||
* on the screen always. We don't call to its hide and
|
||||
* beforeHide functions.
|
||||
* @param {Object} options.context
|
||||
* The launch context specifying the default panel and the
|
||||
* activity handler if the app is invoked by web
|
||||
* activities.
|
||||
* @param {String} options.context.initialPanelId
|
||||
* @param {ActivityHandler} options.context.activityHandler
|
||||
*/
|
||||
init: function ss_init(options) {
|
||||
if (options) {
|
||||
_rootPanelId = options.rootPanelId || 'root';
|
||||
_activityHandler = options.context && options.context.activityHandler;
|
||||
}
|
||||
|
||||
window.addEventListener('visibilitychange', _onVisibilityChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to a panel with options. The navigation transition is
|
||||
* determined based on the current screen size and orientation.
|
||||
*
|
||||
* @alias module:SettingsService#navigate
|
||||
* @param {String} panelId
|
||||
* @param {Object} options
|
||||
* @param {Function} callback
|
||||
*/
|
||||
navigate: function ss_navigate(panelId, options, callback) {
|
||||
// Check if the app is invoked by web activity and shall post result.
|
||||
if (_activityHandler && _shallCloseActivity(panelId)) {
|
||||
_activityHandler.postResult();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache the navigation request if it is navigating.
|
||||
if (_navigating) {
|
||||
_pendingNavigationRequest = arguments;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we find out the link includes information about app's name,
|
||||
// it means that we are going to embed the app into our app.
|
||||
//
|
||||
// In this way, we have to navigate to `frame` panel and embed it.
|
||||
var appName = _getAppNameToLink(panelId);
|
||||
if (appName) {
|
||||
var appInfo = _getAppInfo(appName);
|
||||
|
||||
if (!appInfo) {
|
||||
console.error('We only embed trust apps.');
|
||||
return;
|
||||
}
|
||||
|
||||
panelId = 'frame';
|
||||
options = options || {};
|
||||
options.mozapp = appInfo.mozapp;
|
||||
options.src = appInfo.src;
|
||||
}
|
||||
|
||||
_navigating = true;
|
||||
_navigate(panelId, options, (function() {
|
||||
_navigating = false;
|
||||
|
||||
// Navigate to the pending navigation if any.
|
||||
if (_pendingNavigationRequest) {
|
||||
var args = _pendingNavigationRequest;
|
||||
_pendingNavigationRequest = null;
|
||||
this.navigate.apply(this, args);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Go back to previous panel
|
||||
*
|
||||
* @alias module:SettingsService#back
|
||||
*/
|
||||
back: function ss_back() {
|
||||
if (_activityHandler) {
|
||||
_activityHandler.postResult();
|
||||
} else if (_cachedNavigation) {
|
||||
this.navigate(_cachedNavigation.panelId, _cachedNavigationOptions);
|
||||
_cachedNavigation = null;
|
||||
_cachedNavigationOptions = {};
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
63
gaia/apps/settings/js/modules/settings_utils.js
Executable file
63
gaia/apps/settings/js/modules/settings_utils.js
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SettingsUtils is a singleton that we will provide all needed methods
|
||||
* when doing panel related actions.
|
||||
*
|
||||
* @module SettingsUtils
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
// TODO
|
||||
// We need to move utils.js to SettingsUtils step by step
|
||||
var LazyLoader = require('shared/lazy_loader');
|
||||
|
||||
var SettingsUtils = {
|
||||
/**
|
||||
* We will use LazyLoader to help us load settings' used templates
|
||||
* and then return the html to callback
|
||||
*
|
||||
* @memberOf SettingsUtils
|
||||
* @param {String} panelId - ID of target panel
|
||||
* @param {Function} callback - with html as its first parameter
|
||||
*/
|
||||
loadTemplate: function su_loadTemplate(panelId, callback) {
|
||||
var templateElement = document.getElementById(panelId);
|
||||
if (!templateElement) {
|
||||
callback(null);
|
||||
} else {
|
||||
LazyLoader.load([templateElement], function() {
|
||||
callback(templateElement.innerHTML);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Re-runs the font-fit title
|
||||
* centering logic.
|
||||
*
|
||||
* The gaia-header has mutation observers
|
||||
* that listen for changes in the header
|
||||
* title and re-run the font-fit logic.
|
||||
*
|
||||
* If buttons around the title are shown/hidden
|
||||
* then these mutation observers won't be
|
||||
* triggered, but we want the font-fit logic
|
||||
* to be re-run.
|
||||
*
|
||||
* This is a deficiency of <gaia-header>. If
|
||||
* anyone knows a way to listen for changes
|
||||
* in visibility, we won't need this anymore.
|
||||
*
|
||||
* @param {GaiaHeader} header
|
||||
* @private
|
||||
*/
|
||||
runHeaderFontFit: function su_runHeaderFontFit(header) {
|
||||
var titles = header.querySelectorAll('h1');
|
||||
[].forEach.call(titles, function(title) {
|
||||
title.textContent = title.textContent;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return SettingsUtils;
|
||||
});
|
||||
108
gaia/apps/settings/js/modules/sim_security.js
Executable file
108
gaia/apps/settings/js/modules/sim_security.js
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* This is a simple model that can help us mimic the status change event
|
||||
* when enabling / disabling simcard.
|
||||
*
|
||||
* @module SimSecurity
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var EventEmitter = require('modules/base/event_emitter');
|
||||
|
||||
var APIS = [
|
||||
'unlockCardLock',
|
||||
'setCardLock',
|
||||
'updateContact',
|
||||
'getCardLock',
|
||||
'getCardLockRetryCount'
|
||||
];
|
||||
|
||||
var EVENT_CHANGE_APIS = [
|
||||
'unlockCardLock',
|
||||
'setCardLock'
|
||||
];
|
||||
|
||||
var _debug = false;
|
||||
var debug = function() {};
|
||||
if (_debug) {
|
||||
debug = function ss_debug(msg) {
|
||||
console.log('--> [SimSecurity]: ' + msg);
|
||||
};
|
||||
}
|
||||
|
||||
var SimSecurity = Module.create(function() {
|
||||
this.super(EventEmitter).call(this, [
|
||||
'pin-enabled',
|
||||
'pin-disabled',
|
||||
'pin2-enabled',
|
||||
'pin2-disabled',
|
||||
'fdn-enabled',
|
||||
'fdn-disabled',
|
||||
'pin-changed',
|
||||
'pin2-changed',
|
||||
'puk2-changed'
|
||||
]);
|
||||
}).extend(EventEmitter);
|
||||
|
||||
SimSecurity.prototype._getIccByCardIndex = function(cardIndex) {
|
||||
if (cardIndex === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var iccObj;
|
||||
if (navigator.mozMobileConnections[cardIndex]) {
|
||||
var iccId = navigator.mozMobileConnections[cardIndex].iccId;
|
||||
if (iccId) {
|
||||
iccObj = navigator.mozIccManager.getIccById(iccId);
|
||||
}
|
||||
}
|
||||
|
||||
return iccObj;
|
||||
};
|
||||
|
||||
APIS.forEach(function(apiName) {
|
||||
SimSecurity.prototype[apiName] = function(cardIndex) {
|
||||
if (arguments.length < 2) {
|
||||
this.throw(
|
||||
'You are using SimSecurity API wrongly and you may forget' +
|
||||
'to put your cardIndex, please check again');
|
||||
}
|
||||
|
||||
var passedArguments = [].slice.call(arguments, 1);
|
||||
var icc = this._getIccByCardIndex(cardIndex);
|
||||
|
||||
if (!icc) {
|
||||
console.error('We can\'t find needed icc object');
|
||||
return Promise.reject();
|
||||
} else {
|
||||
var func = icc[apiName];
|
||||
var promise = func.apply(icc, passedArguments);
|
||||
return promise.then((result) => {
|
||||
if (EVENT_CHANGE_APIS.indexOf(apiName) > -1) {
|
||||
// we don't have to check passing arguments
|
||||
// because we are under Promise.then()
|
||||
var status = '';
|
||||
var lockType = passedArguments[0].lockType;
|
||||
var enabled = passedArguments[0].enabled;
|
||||
var newPin = passedArguments[0].newPin;
|
||||
|
||||
// If we have newPin, it means that we are changing pin or pin2
|
||||
if (newPin) {
|
||||
status = 'changed';
|
||||
} else {
|
||||
status = enabled ? 'enabled' : 'disabled';
|
||||
}
|
||||
debug('_emitEvent: ' + lockType + '-' + status +
|
||||
' through card ' + cardIndex);
|
||||
this._emitEvent(lockType + '-' + status, cardIndex);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
var simSecurity = new SimSecurity();
|
||||
return simSecurity;
|
||||
});
|
||||
198
gaia/apps/settings/js/modules/state_model.js
Executable file
198
gaia/apps/settings/js/modules/state_model.js
Executable file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* StateModel reduces the complexibility of making the UI and a module state
|
||||
* synchronized while keeping the UI responsive at all times. A typical use case
|
||||
* would be bound a toggle to a hardware feature that requires time for turning
|
||||
* on and off.
|
||||
*
|
||||
* StateModel was designed for the following goals:
|
||||
* - Immediate response
|
||||
* The UI is not blocked even the target module can not respond to the user
|
||||
* request immediately.
|
||||
* - Displaying the target state
|
||||
* The final state should always be the last state requested from users unless
|
||||
* the module fails to fufill the request.
|
||||
* - Displaying the real state
|
||||
* If the module fails to be set to the target state or it changes the state
|
||||
* by itself, the final state is the real module state.
|
||||
*
|
||||
* StateModel needs to be created with an object used for performing the
|
||||
* state change operations. The object should define the following three
|
||||
* functinos:
|
||||
* - onInit
|
||||
* It is called only once when the model is being initialized. The function
|
||||
* takes a function as the parameter used for reporting the state changes
|
||||
* from the module. It can be omitted if your module won't change the state
|
||||
* by itself. onInit should also return an initial state immediately.
|
||||
*
|
||||
* - onSetState
|
||||
* It is called with a target state. Do the operation required for switching
|
||||
* to the target state. Always return a promise resolved/rejected with a
|
||||
* state.
|
||||
* If the promise resolves with a state that does not equal to the
|
||||
* latest target state, StateModel will call to this function again with the
|
||||
* target state. If the promise rejects, StateModel simply changes the
|
||||
* target state to the reported state because the module fails to be changed
|
||||
* to the desired state.
|
||||
*
|
||||
* - onGetState
|
||||
* It is called for requesting the current state. Return a promise wrapping
|
||||
* the current state. StateModel fails to be initialized if the returning
|
||||
* promise rejects.
|
||||
*
|
||||
* StateModel provides three observable properties:
|
||||
* - targetState
|
||||
* This is a property representing the user desired state and it gets updated
|
||||
* to the real module state when required. Binding this property to the UI
|
||||
* should be sufficient for most of the use cases.
|
||||
* - currentState
|
||||
* This is a readonly property reporting the latest stable state.
|
||||
* - transitioning
|
||||
* This is a readonly property showing if it is in transition between states.
|
||||
*
|
||||
* @example
|
||||
* var stateModel = StateModel({
|
||||
* _state: null,
|
||||
* onInit: function(stateChange) {
|
||||
* return this._state;
|
||||
* },
|
||||
* onSetState: function(state) {
|
||||
* // A fake time consuming operation
|
||||
* return new Promise((resolve) => {
|
||||
* setTimeout(() => {
|
||||
* this._state = state;
|
||||
* resolve(this._state);
|
||||
* }, 10000);
|
||||
* });
|
||||
* },
|
||||
* onGetState: function() {
|
||||
* return Promise.resolve(this._state);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Do the binding
|
||||
* checkbox.addEventListener('change', function() {
|
||||
* stateModel.targetState = checkbox.checked;
|
||||
* });
|
||||
* stateModel.observe('targetState', function(newState) {
|
||||
* checkbox.checked = newState;
|
||||
* });
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Module = require('modules/base/module');
|
||||
var Observable = require('modules/mvvm/observable');
|
||||
|
||||
/**
|
||||
* @class StateModel
|
||||
* @param {Function} onInit
|
||||
* @param {Function} onSetState
|
||||
* @param {Function} onGetState
|
||||
* @returns {StateModel}
|
||||
*/
|
||||
var StateModel = Module.create(
|
||||
function StateModel({onInit, onGetState, onSetState}) {
|
||||
if (typeof onInit !== 'function' ||
|
||||
typeof onGetState !== 'function' ||
|
||||
typeof onSetState !== 'function') {
|
||||
this.throw('invalid arguments');
|
||||
}
|
||||
this.super(Observable).call(this);
|
||||
|
||||
var argument = arguments[0];
|
||||
this._init(onInit.bind(argument), onGetState.bind(argument),
|
||||
onSetState.bind(argument));
|
||||
}).extend(Observable);
|
||||
|
||||
/**
|
||||
* An observable property indicating the desired state.
|
||||
*
|
||||
* @access public
|
||||
* @memberOf StateModel.prototype
|
||||
* @type {Object}
|
||||
*/
|
||||
Observable.defineObservableProperty(StateModel.prototype, 'targetState', {
|
||||
value: null
|
||||
});
|
||||
|
||||
/**
|
||||
* An observable property indicating the current state.
|
||||
*
|
||||
* @access public
|
||||
* @readonly
|
||||
* @memberOf StateModel.prototype
|
||||
* @type {Object}
|
||||
*/
|
||||
Observable.defineObservableProperty(StateModel.prototype, 'currentState', {
|
||||
readonly: true,
|
||||
value: null
|
||||
});
|
||||
|
||||
/**
|
||||
* An observable property indicating if it is in transition.
|
||||
*
|
||||
* @access public
|
||||
* @readonly
|
||||
* @memberOf StateModel.prototype
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Observable.defineObservableProperty(StateModel.prototype, 'transitioning', {
|
||||
readonly: true,
|
||||
value: false
|
||||
});
|
||||
|
||||
StateModel.prototype._init = function(onInit, onGetState, onSetState) {
|
||||
this._onInit = onInit;
|
||||
this._onGetState = onGetState;
|
||||
this._onSetState = onSetState;
|
||||
|
||||
this.targetState = this._onInit((realState) => {
|
||||
if (!this._transitioning) {
|
||||
this._currentState = realState;
|
||||
this.targetState = realState;
|
||||
}
|
||||
});
|
||||
this._currentState = this.targetState;
|
||||
|
||||
// Do the initialization
|
||||
this._onGetState().then((realState) => {
|
||||
this._currentState = realState;
|
||||
if (this.targetState !== realState) {
|
||||
// If the current target state is not the real state, do set again.
|
||||
this._setState(this.targetState);
|
||||
}
|
||||
|
||||
this.observe('targetState', (newState) => {
|
||||
if (this._transitioning || newState === this._currentState) {
|
||||
return;
|
||||
}
|
||||
this._setState(newState);
|
||||
});
|
||||
}).catch((err) => {
|
||||
this.error('initialization error ' + err);
|
||||
});
|
||||
};
|
||||
|
||||
StateModel.prototype._setState = function(state) {
|
||||
this.debug('set state: ' + state);
|
||||
this._transitioning = true;
|
||||
return this._onSetState(state).then((realState) => {
|
||||
this.debug('set state completed: ' + state +
|
||||
', real state: ' + realState);
|
||||
this._currentState = realState;
|
||||
if (this.targetState !== realState) {
|
||||
// If the current target state is not the real state, do set again.
|
||||
this._setState(this.targetState);
|
||||
} else {
|
||||
this._transitioning = false;
|
||||
}
|
||||
}, (realState) => {
|
||||
this.debug('set state failed: ' + state + ', real state: ' + realState);
|
||||
this._currentState = realState;
|
||||
this.targetState = realState;
|
||||
this._transitioning = false;
|
||||
});
|
||||
};
|
||||
|
||||
return StateModel;
|
||||
});
|
||||
65
gaia/apps/settings/js/modules/storage_helper.js
Executable file
65
gaia/apps/settings/js/modules/storage_helper.js
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
/**
|
||||
* Helper class for getting available/used storage
|
||||
* required by *_storage.js
|
||||
*/
|
||||
var StorageHelper = {
|
||||
/**
|
||||
* formatting file size strings.
|
||||
*
|
||||
* @private
|
||||
* @param {Number} bytes size in Bytes
|
||||
* @param {Number} digits fixed float digits
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getReadableFileSize: function getReadableFileSize(bytes, digits) {
|
||||
if (bytes === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
var size, e;
|
||||
if (bytes) {
|
||||
e = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
size = (bytes / Math.pow(1024, e)).toFixed(digits || 0);
|
||||
} else {
|
||||
e = 0;
|
||||
size = '0';
|
||||
}
|
||||
|
||||
return {
|
||||
size: size,
|
||||
unit: units[e]
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* attach formated size to certain element.
|
||||
*
|
||||
* @public
|
||||
* @param {Object} element html element
|
||||
* @param {String} l10nId l10n string id
|
||||
* @param {Number} sizesize in Bytes
|
||||
*/
|
||||
showFormatedSize: function showFormatedSize(element, l10nId, size) {
|
||||
if (size === undefined || isNaN(size)) {
|
||||
element.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// KB - 3 KB (nearest ones), MB, GB - 1.29 MB (nearest hundredth)
|
||||
var fixedDigits = (size < 1024 * 1024) ? 0 : 2;
|
||||
var sizeInfo = this._getReadableFileSize(size, fixedDigits);
|
||||
|
||||
document.l10n.formatValue('byteUnit-' + sizeInfo.unit).then(unit => {
|
||||
document.l10n.setAttributes(element, l10nId, {
|
||||
size: sizeInfo.size,
|
||||
unit: unit
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return StorageHelper;
|
||||
});
|
||||
83
gaia/apps/settings/js/modules/sync_manager_bridge.js
Executable file
83
gaia/apps/settings/js/modules/sync_manager_bridge.js
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var SyncManagerBridge = {
|
||||
_port: null,
|
||||
_requests: new Map(),
|
||||
|
||||
set onsyncchange(callback) {
|
||||
this._onsyncchange = callback;
|
||||
},
|
||||
|
||||
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('gaia-sync-management').then(ports => {
|
||||
if (!ports || !ports.length) {
|
||||
return reject();
|
||||
}
|
||||
this._port = ports[0];
|
||||
this._port.onmessage = this.onmessage.bind(this);
|
||||
resolve(this._port);
|
||||
}).catch(reject);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
iacRequest: function(request) {
|
||||
return new Promise(resolve => {
|
||||
this.connect().then(port => {
|
||||
if (request.id) {
|
||||
this._requests.set(request.id, resolve);
|
||||
}
|
||||
port.postMessage(request);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onmessage: function(event) {
|
||||
var message = event.data;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.id && this._requests.has(message.id)) {
|
||||
this._requests.get(message.id)(message);
|
||||
this._requests.delete(message.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.name !== 'onsyncchange') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._onsyncchange) {
|
||||
this._onsyncchange(message);
|
||||
}
|
||||
},
|
||||
|
||||
getInfo: function() {
|
||||
return this.iacRequest({
|
||||
name: 'getInfo',
|
||||
id: Date.now()
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
['enable',
|
||||
'disable',
|
||||
'sync'].forEach(requestName => {
|
||||
SyncManagerBridge[requestName] = () => {
|
||||
return SyncManagerBridge.iacRequest({
|
||||
name: requestName
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return SyncManagerBridge;
|
||||
});
|
||||
493
gaia/apps/settings/js/modules/wifi_context.js
Executable file
493
gaia/apps/settings/js/modules/wifi_context.js
Executable file
|
|
@ -0,0 +1,493 @@
|
|||
/**
|
||||
* WifiContext is a singleton that you can easily use it to fetch
|
||||
* some shared data across different panels
|
||||
*
|
||||
* @module WifiContext
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
// modules / helpers
|
||||
var SettingsCache = require('modules/settings_cache');
|
||||
var WifiHelper = require('shared/wifi_helper');
|
||||
var wifiManager = WifiHelper.getWifiManager();
|
||||
var settings = navigator.mozSettings;
|
||||
|
||||
// observed objects
|
||||
var _currentNetwork =
|
||||
wifiManager && wifiManager.connection && wifiManager.connection.network;
|
||||
|
||||
var WifiContext = {
|
||||
/**
|
||||
* These listeners would be called when wifi is enabled
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiEnabledListeners: [],
|
||||
|
||||
/**
|
||||
* These listeners would be called when wifi is disabled
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiDisabledListeners: [],
|
||||
|
||||
/**
|
||||
* These listeners would be called when wifi is changed
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiStatusChangeListeners: [],
|
||||
|
||||
/**
|
||||
* These listeners would be called when
|
||||
* 1. localized
|
||||
* 2. wifi is enabled
|
||||
* 3. wifi is changed
|
||||
* 4. wifi is disabled
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiStatusTextChangeListeners: [],
|
||||
|
||||
/**
|
||||
* These listeners would be called when the current wifi
|
||||
* information is updated
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiConnectionInfoUpdateListeners: [],
|
||||
|
||||
/**
|
||||
* These listeners would be called when the wifi network
|
||||
* is forgotten.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiNetworkForgottenListeners: [],
|
||||
|
||||
/**
|
||||
* These listeners would be called when
|
||||
* 1. wifi connection failed
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {Array}
|
||||
*/
|
||||
_wifiWrongPasswordListeners: [],
|
||||
|
||||
/**
|
||||
* Desc about customized wifi status
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {String}
|
||||
*/
|
||||
_wifiStatusText: { id: null },
|
||||
|
||||
/**
|
||||
* Mac address
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @type {String}
|
||||
*/
|
||||
_macAddress: '',
|
||||
|
||||
/**
|
||||
* Init is used to initialize some basic stuffs
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_init: function() {
|
||||
if (!wifiManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set wifi.enabled so that it mirrors the state of the hardware.
|
||||
// wifi.enabled is not an ordinary user setting because the system
|
||||
// turns it on and off when wifi goes up and down.
|
||||
settings.createLock().set({'wifi.enabled': wifiManager.enabled});
|
||||
|
||||
this._bindEvents();
|
||||
|
||||
// we would call _updateWifi() when init
|
||||
this._updateWifi();
|
||||
},
|
||||
|
||||
/**
|
||||
* We will bind some default listeners here
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_bindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
var _updateWifi = this._updateWifi.bind(this);
|
||||
var _updateNetworkStatus = this._updateNetworkStatus.bind(this);
|
||||
|
||||
// make sure we would update anything when wifi got changed
|
||||
this._wifiEnabledListeners.push(_updateWifi, _updateNetworkStatus);
|
||||
this._wifiDisabledListeners.push(_updateWifi);
|
||||
this._wifiStatusChangeListeners.push(_updateWifi, _updateNetworkStatus);
|
||||
|
||||
// Now register callbacks to track the state of the wifi hardware
|
||||
if (wifiManager) {
|
||||
wifiManager.onenabled = function(event) {
|
||||
self._wifiEnabled(event);
|
||||
self._wifiStatusTextChange();
|
||||
};
|
||||
|
||||
wifiManager.ondisabled = function(event) {
|
||||
self._wifiDisabled(event);
|
||||
self._wifiStatusTextChange();
|
||||
};
|
||||
|
||||
wifiManager.onstatuschange = function(event) {
|
||||
self._wifiStatusChange.call(self, event);
|
||||
self._wifiStatusTextChange();
|
||||
};
|
||||
|
||||
wifiManager.onconnectioninfoupdate =
|
||||
self._wifiConnectionInfoUpdate.bind(self);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* We will update mac address, wifiStatus when wifi is updated
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_updateWifi: function(event) {
|
||||
var self = this;
|
||||
|
||||
if (!wifiManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the MAC address is in the Settings database, it's already
|
||||
// displayed in all `MAC address' fields; if not, it will be set as soon
|
||||
// as the Wi-Fi is enabled (see `storeMacAddress').
|
||||
if (!this._macAddress && settings) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
self._macAddress = results['deviceinfo.mac'];
|
||||
});
|
||||
}
|
||||
|
||||
if (wifiManager.enabled) {
|
||||
this._storeMacAddress();
|
||||
}
|
||||
|
||||
// reflect latest value of wifiStatus
|
||||
this._updateWifiStatusText(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* We will update mac address, wifiStatus when wifi is updated
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_updateWifiStatusText: function(event) {
|
||||
if (wifiManager.enabled) {
|
||||
var network = (event && event.network) ? event.network :
|
||||
wifiManager.connection.network;
|
||||
var status =
|
||||
event && event.status ? event.status : wifiManager.connection.status;
|
||||
|
||||
var networkProp = network ? {ssid: network.ssid} : null;
|
||||
this._wifiStatusText =
|
||||
{ id: 'full-status-' + status,
|
||||
args: networkProp };
|
||||
} else {
|
||||
this._wifiStatusText =
|
||||
{ id: 'disabled' };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* We need to handle network status when wifi is enabled / wifistatus
|
||||
* is changed.
|
||||
*
|
||||
* _currentNetwork: user activated current network
|
||||
* wifiManager.connection.network: system activated current network
|
||||
*
|
||||
* Main difference between |_currentNetwork| and |wifiManager.connection.
|
||||
* network| is that we can't tell if network is known while connecting by
|
||||
* |wifiManager.connectiohn.network.known| because it will always be true.
|
||||
*
|
||||
* Instead, we use |_currentNetwork| to indicate if current connecting
|
||||
* network is new or known network.
|
||||
*
|
||||
* When system connect to network by user's tap on UI, |_currentNetwork|
|
||||
* will point to same network as |wifiManager.connection.network|.
|
||||
*
|
||||
* On the other hand, if system connect to network automatically under
|
||||
* conditions like wifi enable, move into AP's range, or reconnect to
|
||||
* other network after delete current connected network, |_currentNetwork|
|
||||
* will be null, while |wifiManager.connection.network| contains
|
||||
* information about current network.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_updateNetworkStatus: function(event) {
|
||||
if (!_currentNetwork) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event && event.network &&
|
||||
_currentNetwork.ssid != event.network.ssid) {
|
||||
return;
|
||||
}
|
||||
|
||||
var networkStatus = event ? event.status : wifiManager.connection.status;
|
||||
if (networkStatus === 'connectingfailed') {
|
||||
// Since this.forgetNetwork will set _currentNetwork to null, we have
|
||||
// to save the _currentNetwork.known first.
|
||||
var isCurrentNetworkKnown = _currentNetwork.known;
|
||||
delete _currentNetwork.password;
|
||||
this.forgetNetwork(_currentNetwork);
|
||||
if (isCurrentNetworkKnown === false) {
|
||||
// Connection fail on user-activated unknown network, should be wrong
|
||||
// password, delete network and force a new authentication dialog.
|
||||
this._wifiWrongPassword();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When wifi is enabled, we will make sure the mozSettings is sync
|
||||
* with hardward status and call all registered listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiEnabled: function(event) {
|
||||
var self = this;
|
||||
this._syncWifiEnabled(true, function() {
|
||||
self._wifiEnabledListeners.forEach(function(listener) {
|
||||
listener(event);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When wifi is disabled, we will make sure the mozSettings is sync
|
||||
* with hardward status and call all registered listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiDisabled: function(event) {
|
||||
var self = this;
|
||||
this._syncWifiEnabled(false, function() {
|
||||
self._wifiDisabledListeners.forEach(function(listener) {
|
||||
listener(event);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When wifi's status is changed, we will call all registered listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiStatusChange: function(event) {
|
||||
this._wifiStatusChangeListeners.forEach(function(listener) {
|
||||
listener(event);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When text of wifiStatus is changed, we will call all registered
|
||||
* listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiStatusTextChange: function() {
|
||||
this._wifiStatusTextChangeListeners.forEach(function(listener) {
|
||||
listener();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When wifi connection fails, we will call all registered
|
||||
* listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiWrongPassword: function() {
|
||||
this._wifiWrongPasswordListeners.forEach(function(listener) {
|
||||
listener();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When wifi's connection is updated, we will call all registered listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiConnectionInfoUpdate: function(event) {
|
||||
this._wifiConnectionInfoUpdateListeners.forEach(function(listener) {
|
||||
listener(event);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When wifi's network is forgotten, we will call all registered listeners.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_wifiNetworkForgotten: function(event) {
|
||||
this._wifiNetworkForgottenListeners.forEach(function(listener) {
|
||||
listener(event);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Keep mac address in mozSettings
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
*/
|
||||
_storeMacAddress: function() {
|
||||
if (!wifiManager) {
|
||||
return;
|
||||
}
|
||||
// Store the MAC address in the Settings database. Note: the wifiManager
|
||||
// sets macAddress to the string `undefined' when it is not available.
|
||||
if (settings && wifiManager.macAddress &&
|
||||
wifiManager.macAddress !== this._macAddress &&
|
||||
wifiManager.macAddress !== 'undefined') {
|
||||
this._macAddress = wifiManager.macAddress;
|
||||
settings.createLock().set({ 'deviceinfo.mac': this._macAddress });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Keep the setting in sync with the hardware state. We need to do this
|
||||
* because b2g/dom/wifi/WifiWorker.js can turn the hardware on and off.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @param {Boolean} enabled
|
||||
* @param {Function} callback
|
||||
*/
|
||||
_syncWifiEnabled: function(enabled, callback) {
|
||||
SettingsCache.getSettings(function(results) {
|
||||
var wifiEnabled = results['wifi.enabled'];
|
||||
if (wifiEnabled !== enabled) {
|
||||
settings.createLock().set({'wifi.enabled': enabled});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* This is an internal function that can help us find out the matched
|
||||
* callback from catched listeners and remove it
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @param {Array} listeners
|
||||
* @param {Function} callback
|
||||
*/
|
||||
_removeEventListener: function(listeners, callback) {
|
||||
var index = listeners.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This is a wrapper of WifiManger.associate to make sure we can handle
|
||||
* our internal status at the same time.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @param {Object} network
|
||||
* @param {Function} callback
|
||||
*/
|
||||
associateNetwork: function(network, cb) {
|
||||
cb = cb || function() {};
|
||||
var request = wifiManager.associate(network);
|
||||
var done = function() {
|
||||
if (!request.error) {
|
||||
_currentNetwork = network;
|
||||
}
|
||||
cb(request.error);
|
||||
};
|
||||
request.onsuccess = done;
|
||||
request.onerror = done;
|
||||
},
|
||||
/**
|
||||
* This is a wrapper of WifiManger.forget to make sure we can handle
|
||||
* our internal status at the same time.
|
||||
*
|
||||
* @memberOf WifiContext
|
||||
* @param {Object} network
|
||||
* @param {Function} callback
|
||||
*/
|
||||
forgetNetwork: function(network, cb) {
|
||||
cb = cb || function() {};
|
||||
var request = wifiManager.forget(network);
|
||||
var done = function() {
|
||||
if (!request.error) {
|
||||
_currentNetwork = null;
|
||||
WifiContext._wifiNetworkForgotten({ network: network });
|
||||
}
|
||||
cb(request.error);
|
||||
};
|
||||
request.onsuccess = done;
|
||||
request.onerror = done;
|
||||
}
|
||||
};
|
||||
|
||||
WifiContext._init();
|
||||
|
||||
return {
|
||||
addEventListener: function(eventName, callback) {
|
||||
if (eventName === 'wifiEnabled') {
|
||||
WifiContext._wifiEnabledListeners.push(callback);
|
||||
} else if (eventName === 'wifiDisabled') {
|
||||
WifiContext._wifiDisabledListeners.push(callback);
|
||||
} else if (eventName === 'wifiStatusChange') {
|
||||
WifiContext._wifiStatusChangeListeners.push(callback);
|
||||
} else if (eventName === 'wifiStatusTextChange') {
|
||||
WifiContext._wifiStatusTextChangeListeners.push(callback);
|
||||
} else if (eventName === 'wifiWrongPassword') {
|
||||
WifiContext._wifiWrongPasswordListeners.push(callback);
|
||||
} else if (eventName === 'wifiConnectionInfoUpdate') {
|
||||
WifiContext._wifiConnectionInfoUpdateListeners.push(callback);
|
||||
} else if (eventName === 'wifiNetworkForgotten') {
|
||||
WifiContext._wifiNetworkForgottenListeners.push(callback);
|
||||
}
|
||||
},
|
||||
removeEventListener: function(eventName, callback) {
|
||||
if (eventName === 'wifiEnabled') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiEnabledListeners, callback);
|
||||
} else if (eventName === 'wifiDisabled') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiDisabledListeners, callback);
|
||||
} else if (eventName === 'wifiStatusChange') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiStatusChangeListeners, callback);
|
||||
} else if (eventName === 'wifiStatusTextChange') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiStatusTextChangeListeners, callback);
|
||||
} else if (eventName === 'wifiWrongPassword') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiWrongPasswordListeners, callback);
|
||||
} else if (eventName === 'wifiConnectionInfoUpdate') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiConnectionInfoUpdateListeners, callback);
|
||||
} else if (eventName === 'wifiNetworkForgotten') {
|
||||
WifiContext._removeEventListener(
|
||||
WifiContext._wifiNetworkForgottenListeners, callback);
|
||||
}
|
||||
},
|
||||
get wifiStatusText() {
|
||||
return WifiContext._wifiStatusText;
|
||||
},
|
||||
get currentNetwork() {
|
||||
return _currentNetwork;
|
||||
},
|
||||
forgetNetwork: WifiContext.forgetNetwork,
|
||||
associateNetwork: WifiContext.associateNetwork
|
||||
};
|
||||
});
|
||||
345
gaia/apps/settings/js/modules/wifi_utils.js
Executable file
345
gaia/apps/settings/js/modules/wifi_utils.js
Executable file
|
|
@ -0,0 +1,345 @@
|
|||
/**
|
||||
* WifiUtils is a utils-box that keeps wifi-operations needed utils.
|
||||
*
|
||||
* @module WifiUtils
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var WifiHelper = require('shared/wifi_helper');
|
||||
var wifiManager = WifiHelper.getWifiManager();
|
||||
|
||||
var WifiUtils = {
|
||||
/**
|
||||
* Create an explanatory list item
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {String} message
|
||||
* @returns {HTMLLIElement}
|
||||
*/
|
||||
newExplanationItem: function(message) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'explanation';
|
||||
li.setAttribute('data-l10n-id', message);
|
||||
return li;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a network list item
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {Object} options
|
||||
* @returns {HTMLLIElement}
|
||||
*/
|
||||
newListItem: function(options) {
|
||||
/**
|
||||
* A Wi-Fi list item has the following HTML structure:
|
||||
* <li>
|
||||
* <aside class="pack-end wifi-icon level-[?] [secured]"></aside>
|
||||
* <a>
|
||||
* <span> Network SSID </span>
|
||||
* <small> Network Security </small>
|
||||
* </a>
|
||||
* </li>
|
||||
*/
|
||||
var network = options.network;
|
||||
var showNotInRange = options.showNotInRange || false;
|
||||
var onClick = options.onClick || function() {};
|
||||
|
||||
// icon
|
||||
var icon = document.createElement('aside');
|
||||
icon.classList.add('pack-end');
|
||||
icon.classList.add('wifi-icon');
|
||||
var level = Math.min(Math.floor(network.relSignalStrength / 20), 4);
|
||||
icon.classList.add('level-' + level);
|
||||
|
||||
// ssid
|
||||
var ssid = document.createElement('span');
|
||||
ssid.dir = 'auto';
|
||||
ssid.textContent = network.ssid;
|
||||
|
||||
// supported authentication methods
|
||||
var small = document.createElement('small');
|
||||
var keys = WifiHelper.getSecurity(network);
|
||||
var networkNotInRange = (network.known && level === 0);
|
||||
var hasSecurity = (keys && keys.length);
|
||||
|
||||
if (hasSecurity) {
|
||||
if (showNotInRange && networkNotInRange) {
|
||||
small.setAttribute('data-l10n-id', 'notInRange');
|
||||
} else {
|
||||
document.l10n.setAttributes(small, 'securedBy',
|
||||
{ capabilities: keys.join(', ') });
|
||||
}
|
||||
icon.classList.add('secured');
|
||||
} else {
|
||||
if (showNotInRange && networkNotInRange) {
|
||||
small.setAttribute('data-l10n-id', 'notInRange');
|
||||
} else {
|
||||
small.setAttribute('data-l10n-id', 'securityOpen');
|
||||
}
|
||||
}
|
||||
|
||||
var a = document.createElement('a');
|
||||
a.appendChild(ssid);
|
||||
a.appendChild(small);
|
||||
|
||||
// create list item
|
||||
var li = document.createElement('li');
|
||||
li.dataset.ssid = network.ssid;
|
||||
li.appendChild(icon);
|
||||
li.appendChild(a);
|
||||
|
||||
// Show connection status
|
||||
icon.classList.add('wifi-signal');
|
||||
|
||||
var networkStatus = WifiHelper.getNetworkStatus(network);
|
||||
var kActiveStatuses = ['associated', 'connecting', 'connected'];
|
||||
|
||||
if (-1 !== kActiveStatuses.indexOf(networkStatus)) {
|
||||
li.classList.add('active');
|
||||
|
||||
if ('connected' === networkStatus) {
|
||||
icon.classList.add('connected');
|
||||
} else {
|
||||
icon.classList.add('connecting');
|
||||
}
|
||||
|
||||
small.setAttribute('data-l10n-id', 'short-status-' + networkStatus);
|
||||
}
|
||||
|
||||
// bind connection callback
|
||||
li.onclick = function() {
|
||||
onClick(network);
|
||||
};
|
||||
return li;
|
||||
},
|
||||
|
||||
/**
|
||||
* Change dialog layout based on dialogId and security
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {HTMLElement} panel
|
||||
* @param {String} security
|
||||
*/
|
||||
changeDisplay: function(panel, security) {
|
||||
var eap = panel.querySelector('li.eap select');
|
||||
var identity = panel.querySelector('input[name=identity]');
|
||||
var password = panel.querySelector('input[name=password]');
|
||||
var authPhase2 = panel.querySelector('li.auth-phase2 select');
|
||||
var certificate = panel.querySelector('li.server-certificate select');
|
||||
var description =
|
||||
panel.querySelector('li.server-certificate-description');
|
||||
|
||||
if (security === 'WEP' || security === 'WPA-PSK') {
|
||||
identity.parentNode.style.display = 'none';
|
||||
password.parentNode.style.display = 'block';
|
||||
authPhase2.parentNode.parentNode.style.display = 'none';
|
||||
certificate.parentNode.parentNode.style.display = 'none';
|
||||
description.style.display = 'none';
|
||||
} else if (security === 'WPA-EAP') {
|
||||
if (eap) {
|
||||
switch (eap.value) {
|
||||
case 'SIM':
|
||||
identity.parentNode.style.display = 'none';
|
||||
password.parentNode.style.display = 'none';
|
||||
authPhase2.parentNode.parentNode.style.display = 'none';
|
||||
certificate.parentNode.parentNode.style.display = 'none';
|
||||
description.style.display = 'none';
|
||||
break;
|
||||
case 'PEAP':
|
||||
case 'TLS':
|
||||
case 'TTLS':
|
||||
identity.parentNode.style.display = 'block';
|
||||
password.parentNode.style.display = 'block';
|
||||
authPhase2.parentNode.parentNode.style.display = 'block';
|
||||
certificate.parentNode.parentNode.style.display = 'block';
|
||||
description.style.display = 'block';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
identity.parentNode.style.display = 'none';
|
||||
password.parentNode.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This is used to help us do some initialization works if the panel
|
||||
* is auth related.
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {String} panel
|
||||
* @param {Object} network
|
||||
*/
|
||||
initializeAuthFields: function(panel, network) {
|
||||
var ssid = panel.querySelector('input[name=ssid]');
|
||||
var identity = panel.querySelector('input[name=identity]');
|
||||
var password = panel.querySelector('input[name=password]');
|
||||
var showPassword = panel.querySelector('gaia-checkbox[name=show-pwd]');
|
||||
var eap = panel.querySelector('li.eap select');
|
||||
var certificate = panel.querySelector('li.server-certificate select');
|
||||
var submitButton = panel.querySelector('button[type=submit]');
|
||||
|
||||
// load needed certificates first
|
||||
this.loadImportedCertificateOptions(certificate);
|
||||
|
||||
identity.value = network.identity || '';
|
||||
|
||||
password.type = 'password';
|
||||
password.value = network.password || '';
|
||||
|
||||
showPassword.checked = false;
|
||||
showPassword.onchange = function() {
|
||||
password.type = this.checked ? 'text' : 'password';
|
||||
};
|
||||
|
||||
var checkPassword = function() {
|
||||
var key = WifiHelper.getKeyManagement(network);
|
||||
var isDisabled = !WifiHelper.isValidInput(
|
||||
key, password.value, identity.value, eap.value);
|
||||
if (ssid) {
|
||||
isDisabled = isDisabled || !WifiHelper.isSSIDValid(ssid.value);
|
||||
}
|
||||
submitButton.disabled = isDisabled;
|
||||
};
|
||||
|
||||
eap.onchange = function() {
|
||||
var key = WifiHelper.getKeyManagement(network);
|
||||
checkPassword();
|
||||
WifiUtils.changeDisplay(panel, key);
|
||||
};
|
||||
|
||||
if (ssid) {
|
||||
ssid.oninput = checkPassword;
|
||||
}
|
||||
|
||||
password.oninput = checkPassword;
|
||||
identity.oninput = checkPassword;
|
||||
checkPassword();
|
||||
},
|
||||
|
||||
/**
|
||||
* This is an inner function that used to inject certificates options
|
||||
* into select element.
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {HTMLSelectElement} select
|
||||
*/
|
||||
loadImportedCertificateOptions: function(select) {
|
||||
if (!wifiManager.getImportedCerts) {
|
||||
return;
|
||||
}
|
||||
|
||||
var certRequest = wifiManager.getImportedCerts();
|
||||
|
||||
certRequest.onsuccess = function() {
|
||||
var i;
|
||||
var certList = certRequest.result;
|
||||
// save the imported server certificates
|
||||
var certificateList = certList.ServerCert;
|
||||
|
||||
// reset the option to be <option value="none">--</option> only
|
||||
var originLengthOfOptions = select.options.length;
|
||||
for (i = 0; i < originLengthOfOptions - 1; i++) {
|
||||
select.remove(1);
|
||||
}
|
||||
|
||||
for (i = 0; i < certificateList.length; i++) {
|
||||
var option = document.createElement('option');
|
||||
option.text = certificateList[i];
|
||||
option.value = certificateList[i];
|
||||
select.add(option, null);
|
||||
}
|
||||
};
|
||||
|
||||
certRequest.onerror = function() {
|
||||
console.warn('getImportedCerts failed');
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the icon of the given network
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {Object} network
|
||||
* @param {Integer} networkSignal
|
||||
*/
|
||||
updateNetworkSignal: function(network, networkSignal) {
|
||||
var li = document.querySelector('li[data-ssid="' + network.ssid + '"]');
|
||||
var icon = li.querySelector('aside');
|
||||
// Clean previous state
|
||||
icon.className = icon.className.replace(/level-\w*/, '');
|
||||
var level = Math.min(Math.floor(networkSignal / 20), 4);
|
||||
icon.classList.add('level-' + level);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get concated networkKey which can be used as identifier
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {Object} network
|
||||
* @return {String} concated network identifier
|
||||
*/
|
||||
getNetworkKey: function(network) {
|
||||
if (!network) {
|
||||
return '';
|
||||
} else {
|
||||
var key =
|
||||
network.ssid + '+' + WifiHelper.getSecurity(network).join('+');
|
||||
return key;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reflect incoming network status on related listItem (show different UI)
|
||||
*
|
||||
* @memberOf WifiUtils
|
||||
* @param {Object} options
|
||||
* @param {Object} options.listItems - listItems with DOM elements
|
||||
* @param {Object} options.activeItemDOM - DOM element for active item
|
||||
* @param {Object} options.network - network object
|
||||
* @param {Object} options.networkStatus - current status for network
|
||||
*/
|
||||
updateListItemStatus: function(options) {
|
||||
options = options || {};
|
||||
var listItems = options.listItems;
|
||||
var activeItemDOM = options.activeItemDOM;
|
||||
var network = options.network;
|
||||
var networkStatus = options.networkStatus;
|
||||
|
||||
if (!network || !networkStatus || !listItems) {
|
||||
console.log('Please check passing options for updateListItemStatus');
|
||||
return;
|
||||
}
|
||||
|
||||
var key = this.getNetworkKey(network);
|
||||
var listItemDOM = listItems[key];
|
||||
|
||||
if (activeItemDOM && activeItemDOM != listItemDOM) {
|
||||
activeItemDOM.classList.remove('active');
|
||||
activeItemDOM.querySelector('small').
|
||||
setAttribute('data-l10n-id', 'short-status-disconnected');
|
||||
activeItemDOM.querySelector('aside').classList.remove('connecting');
|
||||
activeItemDOM.querySelector('aside').classList.remove('connected');
|
||||
}
|
||||
|
||||
if (listItemDOM) {
|
||||
listItemDOM.classList.add('active');
|
||||
listItemDOM.querySelector('small').
|
||||
setAttribute('data-l10n-id', 'short-status-' + networkStatus);
|
||||
if (networkStatus === 'connecting' || networkStatus === 'associated') {
|
||||
listItemDOM.querySelector('aside').classList.remove('connected');
|
||||
listItemDOM.querySelector('aside').classList.add('connecting');
|
||||
} else if (networkStatus === 'connected') {
|
||||
listItemDOM.querySelector('aside').classList.remove('connecting');
|
||||
listItemDOM.querySelector('aside').classList.add('connected');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return WifiUtils;
|
||||
});
|
||||
Loading…
Reference in a new issue