Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

@ -0,0 +1,182 @@
/* Taken from https://github.com/fxos/achievements-service and in particular,
built from https://github.com/fxos/achievements-service/blob/master/
achievements-service.js */
(function(exports) {
'use strict';
var _toArray = function (arr) {
return Array.isArray(arr) ? arr : Array.from(arr);
};
/* global Notification, console */
var DEFAULT_IMAGE_SIZE = 64;
var MOZ_SETTINGS_NOT_AVAILABLE_MSG = 'navigator.mozSettings is not available';
var ImageHelper = (function () {
var ImageHelper = function ImageHelper() {};
ImageHelper.getImage = function (aSrc) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.src = aSrc;
image.onload = function () {
return resolve(image);
};
image.onerror = function (reason) {
return reject(reason);
};
})['catch'](function (reason) {
return console.warn('Could not load an achievement image:', reason);
});
};
ImageHelper.generateImageDataURL = function (aSrc) {
return ImageHelper.getImage(aSrc).then(function (image) {
try {
var canvas = document.createElement('canvas');
canvas.height = canvas.width = DEFAULT_IMAGE_SIZE;
var context = canvas.getContext('2d');
var dataUrl;
context.drawImage(image, 0, 0, DEFAULT_IMAGE_SIZE,
DEFAULT_IMAGE_SIZE);
dataUrl = canvas.toDataURL();
// Clean up.
canvas.width = canvas.height = 0;
canvas = null;
return dataUrl;
} catch (e) {
return Promise.reject('Could not convert image to Data URL.');
}
})['catch'](function (reason) {
return console.warn(reason);
});
};
return ImageHelper;
})();
var SettingsHelper = (function () {
var SettingsHelper = function SettingsHelper() {};
SettingsHelper.get = function (name, defaultValue) {
if (!name) {
return Promise.reject('Setting name is missing');
}
if (!navigator.mozSettings) {
console.warn(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
return Promise.reject(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
}
return new Promise(function (resolve, reject) {
var setting = navigator.mozSettings.createLock().get(name,
defaultValue);
setting.onsuccess = function () {
var settingValue = setting.result[name] || defaultValue;
resolve(settingValue);
};
setting.onerror = function () {
reject(setting.error);
};
});
};
SettingsHelper.set = function (settings) {
if (!settings) {
return Promise.reject('Settings are missing');
}
if (!navigator.mozSettings) {
console.warn(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
return Promise.reject(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
}
return new Promise(function (resolve, reject) {
var result = navigator.mozSettings.createLock().set(settings);
result.onsuccess = function () {
resolve(result.result);
};
result.onerror = function () {
reject(result.error);
};
});
};
return SettingsHelper;
})();
var AchievementsService = (function () {
var AchievementsService = function AchievementsService() {};
AchievementsService.prototype.reward = function (_ref) {
var criteria = _ref.criteria;
var evidence = _ref.evidence;
var name = _ref.name;
var description = _ref.description;
var image = _ref.image;
if (!evidence) {
return Promise.reject('Evidence is not provided.');
}
var issuedOn;
return SettingsHelper.get('achievements', []).then(
function (achievements) {
var achievement = achievements.find(function (achievement) {
return achievement.criteria === criteria;
});
if (!achievement) {
return Promise.reject('Achievement is not registered.');
}
if (achievement.evidence) {
return Promise.reject('Achievement is already awarded.');
}
achievement.evidence = evidence;
achievement.uid = 'achievement' +
Math.round(Math.random() * 100000000);
achievement.issuedOn = issuedOn = Date.now();
achievement.recipient = {}; // TODO
return achievements;
}).then(function (achievements) {
return Promise.all([ImageHelper.generateImageDataURL(image),
SettingsHelper.set({ achievements: achievements })]);
}).then(function (_ref2) {
var _ref3 = _toArray(_ref2);
var image = _ref3[0];
// Send a Notification via WebAPI to be handled by the Gaia::System
var notification = new Notification(name, {
body: description,
icon: image,
tag: issuedOn
});
notification.onclick = function () {
var activity = new window.MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'achievements'
}
});
activity.onsuccess = activity.onerror = function () {
notification.close();
};
};
})['catch'](function (reason) {
return console.warn(reason);
});
};
return AchievementsService;
})();
exports.AchievementsService = AchievementsService;
})(window);

View file

@ -0,0 +1,141 @@
'use strict';
(function(exports) {
/**
* Match pattern is ported from
* https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/addons
* /MatchPattern.jsm
* which in itself implements pattern matching based on Chrome Apps Platform
* API https://developer.chrome.com/extensions/match_patterns
*
* MatchPattern is used by the add-on manager to match add-ons to apps that
* they affect.
*/
const PERMITTED_SCHEMES = ['http', 'https', 'file', 'ftp', 'app'];
// This function converts a glob pattern (containing * and possibly ?
// as wildcards) to a regular expression.
function globToRegexp(pat, allowQuestion) {
// Escape everything except ? and *.
pat = pat.replace(/[.+^${}()|[\]\\]/g, '\\$&');
if (allowQuestion) {
pat = pat.replace(/\?/g, '.');
} else {
pat = pat.replace(/\?/g, '\\?');
}
pat = pat.replace(/\*/g, '.*');
return new RegExp('^' + pat + '$');
}
// These patterns follow the syntax in
// https://developer.chrome.com/extensions/match_patterns
function SingleMatchPattern(pat) {
if (pat == '<all_urls>') {
this.scheme = PERMITTED_SCHEMES;
this.host = '*';
this.path = new RegExp('.*');
} else if (!pat) {
this.scheme = [];
} else {
var re = new RegExp(
'^(http|https|file|ftp|app|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$');
var match = re.exec(pat);
if (!match) {
console.error(`Invalid match pattern: '${pat}'`);
this.scheme = [];
return;
}
if (match[1] == '*') {
this.scheme = ['http', 'https'];
} else {
this.scheme = [match[1]];
}
this.host = match[2];
this.path = globToRegexp(match[3], false);
// We allow the host to be empty for file URLs.
if (this.host === '' && this.scheme[0] !== 'file') {
console.error(`Invalid match pattern: '${pat}'`);
this.scheme = [];
return;
}
}
}
SingleMatchPattern.prototype = {
matches(uri, ignorePath = false) {
if (this.scheme.indexOf(uri.protocol.slice(0, -1)) == -1) {
return false;
}
// This code ignores the port, as Chrome does.
if (this.host == '*') {
// Don't check anything.
} else if (this.host[0] == '*') {
// It must be *.foo. We also need to allow foo by itself.
var suffix = this.host.substr(2);
if (uri.hostname != suffix && !uri.hostname.endsWith('.' + suffix)) {
return false;
}
} else {
if (this.host != uri.hostname) {
return false;
}
}
if (!ignorePath && !this.path.test(uri.pathname)) {
return false;
}
return true;
}
};
/**
* Match Pattern allows to match patterns based on set of URLs
* @class MatchPattern
* @returns {MatchPattern}
*/
function MatchPattern(pat) {
this.pat = pat;
if (!pat) {
this.matchers = [];
} else if (pat instanceof String || typeof(pat) === 'string') {
this.matchers = [new SingleMatchPattern(pat)];
} else {
this.matchers = Array.from(pat, p => new SingleMatchPattern(p));
}
}
MatchPattern.prototype = {
// |uri| should be an nsIURI.
matches(uri) {
for (var matcher of this.matchers) {
if (matcher.matches(uri)) {
return true;
}
}
return false;
},
matchesIgnoringPath(uri) {
for (var matcher of this.matchers) {
if (matcher.matches(uri, true)) {
return true;
}
}
return false;
},
serialize() {
return this.pat;
},
};
exports.MatchPattern = MatchPattern;
})(window);

View file

@ -0,0 +1,182 @@
/*
* This module is a helper for recording telemetry data in Gecko’s telemetry
* system. Currently, only data from certified apps will be recorded.
*
* There are three different types of metrics that can be used here:
* Counter: This is a simple counter that increments by 1 each time it's
* called.
*
* Linear: This type of metric creates a linear distributed set of buckets
* which range between the min and max. You can specify the number
* of buckets as well as the min and max.
*
* Exponential: This type of metric creates an exponential distributed set of
* buckets ranging between min and max. You can specify the min,
* max, and buckets.
*
* App Setup:
* 1) Add the following to the app (e.g. index.html):
* <script defer src="/shared/js/advanced_telemetry_helper.js"></script>
* <script defer src="/shared/js/settings_listener.js"></script>
* 2) Include AdvancedTelemetryHelper in the globals.
*
* Counter Metric Usage Example:
* // This creates the histogram counter on this metric called
* // 'mycount'
* var count = new AdvancedTelemetryHelper(ATH.HISTOGRAM_COUNT, 'mycount');
* count.add(); //Increment the count. The count of the histogram is now 1.
* // DO SOMETHING
* count.add(); //Increment the count. The count of the histogram is now 2.
*
* Linear Metric Usage Example:
* // This creates a linear histogram called 'mylinear' with minimum
* // of 1 (0 not allowed as a minimum), max of 1000, with 10 buckets.
* var linear = new AdvancedTelemetryHelper(ATH.HISTOGRAM_LINEAR, 'mylinear',
* 1, 1000, 10);
* // This adds the value 15 to the histogram 'mylinear'
* linear.add(15);
* // DO SOMETHING
* // This adds to the existing histogram a value of 800.
* linear.add(800); // The histogram now has two values, one in the 15 bucket
* // and one in the bucket that holds the value 800
*
* Exponential Metric Usage Example:
* // This creates an exponential histogram called 'myexp' with minimum
* // of 1 (0 not allowed as a minimum), max of 1000, with 10 buckets.
* var exp = new AdvancedTelemetryHelper(ATH.HISTOGRAM_EXPONENTIAL, 'myexp',
* 1, 1000, 10);
* // This adds the value 15 to the histogram 'myexp'
* exp.add(15);
* // DO SOMETHING
* // This adds to the existing histogram a value of 800.
* exp.add(800); // The histogram now has two values, one in the 15 bucket
* // and one in the bucket that holds the value 800
*
* Caveats:
* Note that after a histogram is created, it's not possible to change the type
* of the histogram from one type to the other. It's necessary to create a new
* Histogram.
*
* Each histogram should have a unique name.
*
*/
/* globals SettingsListener */
(function(exports) {
'use strict';
const APP_TELEMETRY_LOG_PREFIX = 'telemetry';
const ATH = AdvancedTelemetryHelper;
// Export Histogram types so they can be used by apps
ATH.HISTOGRAM_COUNT = 4;
ATH.HISTOGRAM_EXP = 0;
ATH.HISTOGRAM_LINEAR = 1;
// This is exposed for unit testing purposes
ATH.TELEMETRY_LEVEL_KEY = 'metrics.selectedMetrics.level';
ATH.SETTINGS_INITIALIZED = false;
// API for an exponential histogram entry. This function creates the
// histogram in the system and prepares it subsequent .log(value) calls
// against it.
// <type>: One of ATH.HISTOGRAM_COUNT, ATH.HISTOGRAM_EXP,
// or ATH.HISTOGRAM_LINEAR.
// <name>: Unique name of the metric
// <min>: [Required for linear and exponential only]The minimum (non-zero)
// numeric value for this histogram.
// <max>: [Required for linear and exponential only]The maximum numeric
// value for this histogram.
// <buckets>: [Required for linear and exponential only]The total number of
// buckets for this histogram.
function AdvancedTelemetryHelper(type, name, min, max, buckets) {
if (!ATH.SETTINGS_INITIALIZED) {
ATH.SETTINGS_INITIALIZED = true;
ATH.init();
}
if (typeof type !== 'undefined' && typeof name !== 'undefined') {
if (type === ATH.HISTOGRAM_COUNT) {
this.createHistogram(name, ATH.HISTOGRAM_COUNT, 0, 0, 0);
} else if (type === ATH.HISTOGRAM_EXP || type === ATH.HISTOGRAM_LINEAR) {
this.createHistogram(name, type, min, max, buckets);
} else {
console.warn('Must pass a Histogram type and a unique Histogram name.');
}
} else {
console.warn('Must pass a Histogram type and a unique Histogram name.');
}
}
AdvancedTelemetryHelper.init = function init() {
ATH.telemetryEnabledListener = function telemetryEnabledListener(enabled) {
if (enabled === 'Enhanced') {
ATH.TELEMETRY_LEVEL = true;
} else {
ATH.TELEMETRY_LEVEL = false;
}
}.bind(this);
SettingsListener.observe(ATH.TELEMETRY_LEVEL_KEY,
false, ATH.telemetryEnabledListener);
};
AdvancedTelemetryHelper.prototype.createHistogram =
function(name, type, min, max, buckets) {
if (typeof ATH.TELEMETRY_LEVEL === 'undefined' || ATH.TELEMETRY_LEVEL) {
if (typeof this.metricType !== 'undefined') {
console.warn('Cannot redefine histogram. Must create a new object');
return;
}
this.metricType = type;
if (typeof name !== 'undefined') {
var varCount = 2;
var re = /_/g;
var message = APP_TELEMETRY_LOG_PREFIX;
// Disallow underscores so telemetry system can parse it.
this.metricName = name.replace(re, '-');
message += '|' + this.metricName;
message += '|' + type + '|';
if (typeof min !== 'undefined') { varCount++; }
if (typeof max !== 'undefined') { varCount++; }
if (typeof buckets !== 'undefined') { varCount++; }
if (varCount != 5) {
console.warn('createHistogram must have 5 params.');
return;
}
if (min < 1) {
min = 1;
}
message += min + '|' + max + '|' + buckets;
console.info(message);
} else {
console.warn('A telemetry log entry requires a name.');
}
}
};
// API for logging against a pre-existing histogram. This records the value
// for this particular histogram in the histogram system.
// <value>: The value to record against this histogram.
// For a counter histogram, this can be empty or 1 can be passed.
// For an exponential or linear histogram a valid value > 0 must be passed.
AdvancedTelemetryHelper.prototype.add = function(value) {
if (typeof ATH.TELEMETRY_LEVEL === 'undefined' || ATH.TELEMETRY_LEVEL) {
if (this.metricType == ATH.HISTOGRAM_COUNT) {
value = 1;
}
if (typeof value === 'undefined' || !value || value < 1) {
console.warn('A valid value is required to log a histogram.');
return;
}
var message = APP_TELEMETRY_LOG_PREFIX + '|' + this.metricName +
'|' + value;
console.info(message);
}
};
exports.AdvancedTelemetryHelper = AdvancedTelemetryHelper;
}(window));

View file

@ -0,0 +1,56 @@
/* exported advanced_timer */
'use strict';
var advanced_timer = {
/**
* Maps between user Ids and navigator ones
*/
timers: {},
/**
* Register a new timer with the user's timerId
*/
start: function(timerId, timeout, callback) {
if (typeof(callback) != 'function') {
callback = function() {};
}
var self = this;
var _id = setTimeout(function advTimer() {
var realUsedTime = new Date().getTime() - self.timers[timerId].timestamp;
delete(self.timers[timerId]);
callback(realUsedTime);
}, timeout);
this.timers[timerId] = {
'timeout': timeout,
'internalTimerId': _id,
'timestamp': new Date().getTime()
};
},
/**
* Stops timer and returns the pending time
*/
stop: function(timerId) {
var timer = this.timers[timerId];
if (!timer) {
return 0;
}
clearTimeout(timer.internalTimerId);
var pendingTime = this.queryPendingTime();
delete(this.timers[timerId]);
return pendingTime;
},
/**
* Returns the pending time to timeout the timer
*/
queryPendingTime: function(timerId) {
var timer = this.timers[timerId];
if (!timer) {
return 0;
}
return timer.timeout - (new Date().getTime() - timer.timestamp);
}
};

View file

@ -0,0 +1,84 @@
/**
* @fileoverview This file provides common helper functions used to manipulate
* access point names (APNs)
*/
/* exported ApnHelper */
(function(exports) {
'use strict';
/**
* List of data network types, see source/dom/system/gonk/ril_consts.js in
* mozilla-central. Those match the equivalent Android integer types.
*/
var NETWORK_TYPES = [
null,
'gprs',
'edge',
'umts',
'is95a',
'is95b',
'1xrtt',
'evdo0',
'evdoa',
'hsdpa',
'hsupa',
'hspa',
'evdob',
'ehrpd',
'lte',
'hspa+',
'gsm'
];
/**
* Filter a list of APNs by network type. This involves comparing the bearer
* field if present to the data network type and checking if they match. If
* the bearer value of an APN is not defined or is set to 0 then we consider
* the APN compatible with any network.
*
* @param {Array} apns An array of APNs
* @param {String} type The network type which the APNs must be compatible
* with
*/
function ah_filterByBearer(apns, type) {
var typeIdx = NETWORK_TYPES.indexOf(type);
for (var i = 0; i < apns.length; i++) {
var bearer = apns[i].bearer ? +apns[i].bearer : 0;
if (bearer && (bearer !== typeIdx)) {
// This APN is incompatible, remove it from the array
apns.splice(i, 1);
}
}
}
/**
* Get an APN from the specified list which is compatible with the specified
* MCC, MNC and connection type
*
* @param {Object} list The global APN list, the format should be the same as
* the one used in apn.json
* @param {Integer} mcc The mobile country code
* @param {Integer} mnc The mobile network code
* @param {String} type The network type which the filtered APNs must be
* compatible with
*
* @return {Array} A list of compatible APNs
*/
function ah_getCompatible(list, mcc, mnc, type) {
var apns = list[mcc] ? (list[mcc][mnc] || []) : [];
ah_filterByBearer(apns, type);
return apns;
}
var ApnHelper = {
getCompatible: ah_getCompatible
};
exports.ApnHelper = ApnHelper;
})(this);

View file

@ -0,0 +1,61 @@
'use strict';
(function(exports) {
var AsyncSemaphore = function as_contructor() {
if (!(this instanceof AsyncSemaphore)) {
return new AsyncSemaphore();
}
this._init();
};
// Do not call '_init' and '_execute' directly from outside of AsyncSemaphore
AsyncSemaphore.prototype = {
_init: function as_init() {
// Please don't modify these value directly from outside of AsyncSemaphore
this.pendingTasks = [];
this.semaphore = 0;
},
_execute: function as_execute() {
var task,
context;
if (this.semaphore <= 0) {
while (this.pendingTasks.length > 0) {
task = this.pendingTasks.shift();
context = task.context || this;
if (task && task.callback && typeof task.callback === 'function') {
task.callback.call(context, task.args);
}
}
}
},
v: function as_v(num) {
if (!num) {
num = 1;
}
this.semaphore += num;
},
p: function as_p() {
this.semaphore -= 1;
if (this.semaphore < 0) {
this.semaphore = 0;
}
this._execute();
},
wait: function as_wait(callback, context, args) {
this.pendingTasks.push({
callback: callback,
context: context,
args: args
});
this._execute();
},
getValue: function as_getValue() {
return this.semaphore;
},
getTasksLength: function as_getTasksLength() {
return this.pendingTasks.length;
}
};
exports.AsyncSemaphore = AsyncSemaphore;
}(window));

View file

@ -0,0 +1,180 @@
/* globals indexedDB */
/**
* This file defines an asynchronous version of the localStorage API, backed by
* an IndexedDB database. It creates a global asyncStorage object that has
* methods like the localStorage object.
*
* To store a value use setItem:
*
* asyncStorage.setItem('key', 'value');
*
* If you want confirmation that the value has been stored, pass a callback
* function as the third argument:
*
* asyncStorage.setItem('key', 'newvalue', function() {
* console.log('new value stored');
* });
*
* To read a value, call getItem(), but note that you must supply a callback
* function that the value will be passed to asynchronously:
*
* asyncStorage.getItem('key', function(value) {
* console.log('The value of key is:', value);
* });
*
* Note that unlike localStorage, asyncStorage does not allow you to store and
* retrieve values by setting and querying properties directly. You cannot just
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
*
* removeItem(), clear(), length(), and key() are like the same-named methods of
* localStorage, but, like getItem() and setItem() they take a callback
* argument.
*
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
* values. But unlike localStorage, asyncStorage does not require the values you
* store to be strings. So if you need to save multiple values and want to
* retrieve them together, in a single asynchronous operation, just group the
* values into a single object. The properties of this object may not include
* DOM elements, but they may include things like Blobs and typed arrays.
*
* Unit tests are in apps/gallery/test/unit/asyncStorage_test.js
*/
this.asyncStorage = (function() {
'use strict';
var DBNAME = 'asyncStorage';
var DBVERSION = 1;
var STORENAME = 'keyvaluepairs';
var db = null;
function withDatabase(f) {
if (db) {
f();
} else {
var openreq = indexedDB.open(DBNAME, DBVERSION);
openreq.onerror = function withStoreOnError() {
console.error('asyncStorage: can\'t open database:',
openreq.error.name);
};
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
// First time setup: create an empty object store
openreq.result.createObjectStore(STORENAME);
};
openreq.onsuccess = function withStoreOnSuccess() {
db = openreq.result;
f();
};
}
}
function withStore(type, callback, oncomplete) {
withDatabase(function() {
var transaction = db.transaction(STORENAME, type);
if (oncomplete) {
transaction.oncomplete = oncomplete;
}
callback(transaction.objectStore(STORENAME));
});
}
function getItem(key, callback) {
var req;
withStore('readonly', function getItemBody(store) {
req = store.get(key);
req.onerror = function getItemOnError() {
console.error('Error in asyncStorage.getItem(): ', req.error.name);
};
}, function onComplete() {
var value = req.result;
if (value === undefined) {
value = null;
}
callback(value);
});
}
function setItem(key, value, callback) {
withStore('readwrite', function setItemBody(store) {
var req = store.put(value, key);
req.onerror = function setItemOnError() {
console.error('Error in asyncStorage.setItem(): ', req.error.name);
};
}, callback);
}
function removeItem(key, callback) {
withStore('readwrite', function removeItemBody(store) {
var req = store.delete(key);
req.onerror = function removeItemOnError() {
console.error('Error in asyncStorage.removeItem(): ', req.error.name);
};
}, callback);
}
function clear(callback) {
withStore('readwrite', function clearBody(store) {
var req = store.clear();
req.onerror = function clearOnError() {
console.error('Error in asyncStorage.clear(): ', req.error.name);
};
}, callback);
}
function length(callback) {
var req;
withStore('readonly', function lengthBody(store) {
req = store.count();
req.onerror = function lengthOnError() {
console.error('Error in asyncStorage.length(): ', req.error.name);
};
}, function onComplete() {
callback(req.result);
});
}
function key(n, callback) {
if (n < 0) {
callback(null);
return;
}
var req;
withStore('readonly', function keyBody(store) {
var advanced = false;
req = store.openCursor();
req.onsuccess = function keyOnSuccess() {
var cursor = req.result;
if (!cursor) {
// this means there weren't enough keys
return;
}
if (n === 0 || advanced) {
// Either 1) we have the first key, return it if that's what they
// wanted, or 2) we've got the nth key.
return;
}
// Otherwise, ask the cursor to skip ahead n records
advanced = true;
cursor.advance(n);
};
req.onerror = function keyOnError() {
console.error('Error in asyncStorage.key(): ', req.error.name);
};
}, function onComplete() {
var cursor = req.result;
callback(cursor ? cursor.key : null);
});
}
return {
getItem: getItem,
setItem: setItem,
removeItem: removeItem,
clear: clear,
length: length,
key: key
};
}());

View file

@ -0,0 +1,137 @@
/* global Promise */
/* exported Basket */
'use strict';
// Simple Javascript client for Mozilla Basket newsletters
var Basket = {
DATASTORE_NAME: 'Basket_Newsletter',
basketUrl: 'https://basket.mozilla.org/news/subscribe/',
newsletterId: 'firefox-os',
callback: null,
xhr: null,
// https://github.com/mozilla/basket-client/blob/master/basket/errors.py
errors: {
NETWORK_FAILURE: 1,
INVALID_EMAIL: 2,
UNKNOWN_EMAIL: 3,
UNKNOWN_TOKEN: 4,
USAGE_ERROR: 5,
EMAIL_PROVIDER_AUTH_FAILURE: 6,
AUTH_ERROR: 7,
SSL_REQUIRED: 8,
INVALID_NEWSLETTER: 9,
INVALID_LANGUAGE: 10,
EMAIL_NOT_CHANGED: 11,
CHANGE_REQUEST_NOT_FOUND: 12,
UNKNOWN_ERROR: 99
},
responseHandler: function b_responseHandler() {
if (this.xhr.readyState === 4) {
if (this.xhr.status === 200) {
if (typeof this.callback === 'function') {
this.callback(null, JSON.parse(this.xhr.responseText));
}
} else {
if (typeof this.callback === 'function') {
if (this.xhr.responseText) {
this.callback(JSON.parse(this.xhr.responseText));
} else {
this.callback('Unknown error');
}
}
}
}
},
timeoutHandler: function b_timeoutHandler() {
this.callback('Timeout');
},
/**
* Send data to Mozilla Basket.
*
* @param {String} [email] email we want to add to the newsletter.
*
* @param {Function} [callback] first argument is error, second
* is result of operation or null
* in the error case.
*/
send: function b_send(email, callback) {
var self = this;
var params = 'email=' + encodeURIComponent(email) +
'&newsletters=' + self.newsletterId;
self.callback = callback;
self.xhr = new XMLHttpRequest({mozSystem: true});
self.xhr.onreadystatechange = self.responseHandler.bind(self);
self.xhr.open('POST', self.basketUrl, true);
self.xhr.setRequestHeader('Content-type',
'application/x-www-form-urlencoded');
self.xhr.setRequestHeader('Connection', 'close');
self.xhr.timeout = 5000; // 5 seconds
self.xhr.ontimeout = self.timeoutHandler.bind(self);
self.getLanguage(function do_send(language) {
if (language) {
params += '&lang=' + language;
}
self.xhr.setRequestHeader('Content-length', params.length);
self.xhr.send(params);
});
},
getDataStore: function() {
var self = this;
return new Promise(function (resolve, reject) {
navigator.getDataStores(self.DATASTORE_NAME).then(function(stores) {
// check it's not empty
if (!stores.length) {
reject(new Error('DataStore not loaded'));
} else {
var dataStore = stores[0];
// check ownership of the DS
if (dataStore.owner === 'app://ftu.gaiamobile.org/manifest.webapp') {
resolve(dataStore);
} else {
reject(new Error('Wrong DataStore'));
}
}
});
});
},
store: function b_store(email, callback) {
this.getDataStore().then(function loaded(store) {
// Store the email address
store.add({ 'newsletter_email': email }).then(function(id) {
if (callback) {
callback(false, id);
}
});
}).catch(function error(err) {
console.error('Something went wrong: ' + err);
callback(err);
});
},
getLanguage: function b_getLanguage(callback) {
var settings = window.navigator.mozSettings;
if (!settings || !settings.createLock) {
callback();
return;
}
var s_name = 'language.current';
var req = settings.createLock().get(s_name);
req.onsuccess = function _onsuccess() {
callback(req.result[s_name]);
};
req.onerror = function _onerror() {
console.error('Error getting setting: ' + s_name);
callback();
};
}
};

View file

@ -0,0 +1,287 @@
'use strict';
/* global Promise */
(function(exports) {
var datastore;
// Datastore name declared on the manifest.webapp
var DATASTORE_NAME = 'bookmarks_store';
// Indicates the initialization state
var readyState;
// Event listeners
var listeners = Object.create(null);
function init() {
return new Promise(function doInit(resolve, reject) {
if (readyState === 'initialized') {
resolve();
return;
}
if (readyState === 'initializing') {
document.addEventListener('ds-initialized', function oninitalized() {
document.removeEventListener('ds-initialized', oninitalized);
resolve();
});
return;
}
readyState = 'initializing';
if (!navigator.getDataStores) {
console.error('Bookmark store: DataStore API is not working');
reject({ name: 'NO_DATASTORE' });
readyState = 'failed';
return;
}
navigator.getDataStores(DATASTORE_NAME).then(function(ds) {
if (ds.length < 1) {
console.error('Bookmark store: Cannot get access to the Store');
reject({ name: 'NO_ACCESS_TO_DATASTORE' });
readyState = 'failed';
return;
}
datastore = ds[0];
datastore.addEventListener('change', onchangeHandler);
readyState = 'initialized';
document.dispatchEvent(new CustomEvent('ds-initialized'));
resolve();
}, reject);
});
}
function doGetAll(resolve, reject) {
var result = Object.create(null);
var cursor = datastore.sync();
function cursorResolve(task) {
switch (task.operation) {
case 'update':
case 'add':
result[task.data.id] = task.data;
break;
case 'remove':
delete result[task.data.id];
break;
case 'clear':
result = Object.create(null);
break;
case 'done':
resolve(result);
return;
}
cursor.next().then(cursorResolve, reject);
}
cursor.next().then(cursorResolve, reject);
}
function get(id) {
return new Promise(function doGet(resolve, reject) {
init().then(function onInitialized() {
datastore.get(id).then(resolve, reject);
}, reject);
});
}
function getAll() {
return new Promise(function doGet(resolve, reject) {
init().then(doGetAll.bind(null, resolve, reject), reject);
});
}
function onchangeHandler(event) {
var operation = event.operation;
var callbacks = listeners[operation];
callbacks && callbacks.forEach(function iterCallback(callback) {
datastore.get(event.id).then(function got(result) {
callback.method.call(callback.context || this, {
type: operation,
target: result || event
});
});
});
}
function addEventListener(type, callback) {
var context;
if (!(type in listeners)) {
listeners[type] = [];
}
var cb = callback;
if (typeof cb === 'object') {
context = cb;
cb = cb.handleEvent;
}
if (cb) {
listeners[type].push({
method: cb,
context: context
});
init();
}
}
function removeEventListener(type, callback) {
if (!(type in listeners)) {
return false;
}
var callbacks = listeners[type];
var length = callbacks.length;
for (var i = 0; i < length; i++) {
var thisCallback = callback;
if (typeof thisCallback === 'object') {
thisCallback = callback.handleEvent;
}
if (callbacks[i] && callbacks[i].method === thisCallback) {
callbacks.splice(i, 1);
return true;
}
}
return false;
}
function add(data) {
return new Promise(function doAdd(resolve, reject) {
init().then(function onInitialized() {
var id = data.url;
Object.defineProperty(data, 'id', {
enumerable: true,
configurable: false,
writable: false,
value: id
});
datastore.add(data, id).then(function add_success() {
resolve(true); // Bookmark was added
}, function add_error() {
datastore.put(data, id).then(function put_success() {
resolve(); // Bookmark was updated
}, reject);
});
}, reject);
});
}
function getRevisionId() {
return new Promise(function doGet(resolve, reject) {
init().then(function onInitialized() {
resolve(datastore.revisionId);
}, reject);
});
}
function put(data) {
return new Promise(function doAdd(resolve, reject) {
init().then(function onInitialized() {
datastore.put(data, data.id).then(function success() {
resolve(); // Bookmark was updated
}, reject);
}, reject);
});
}
function remove(id) {
return new Promise(function doRemove(resolve, reject) {
init().then(function onInitialized() {
datastore.remove(id).then(resolve, reject);
}, reject);
});
}
function clear() {
return new Promise(function doClear(resolve, reject) {
init().then(function onInitialized() {
datastore.clear().then(resolve, reject);
}, reject);
});
}
exports.BookmarksDatabase = {
/*
* This method returns a bookmark object
*
* @param{String} String param that represents an identifier
*/
get: get,
/*
* This method returns an object of bookmarks indexed by id
*/
getAll: getAll,
/*
* Returns the latest revision UUID
*/
getRevisionId: getRevisionId,
/*
* Method registers the specified listener on the API
*
* @param{String} A string representing the event type to listen for
*
* @param{Function} The method that receives a notification when an event of
* the specified type occurs
*
*/
addEventListener: addEventListener,
/*
* Method removes the specified listener on the API
*
* @param{String} A string representing the event type to listen for
*
* @param{Function} The method that received a notification when an event of
* the specified type occurs
*
*/
removeEventListener: removeEventListener,
/*
* This method adds a bookmark in the datastore
*
* @param{Object} The bookmark's data
*/
add: add,
/*
* This method updates a bookmark in the datastore
*
* @param{Object} The bookmark's data
*/
put: put,
/*
* This method removes a bookmark from the datastore
*
* @param{String} The bookmark's id
*/
remove: remove,
/*
*
* This method clears the entire datastore, removing all entries.
*
*/
clear: clear
};
}(window));

View file

@ -0,0 +1,2 @@
;(function(exports){'use strict';exports.ComponentUtils={style:function(baseUrl){if(window.VulpesCompat){window.VulpesCompat.componentStyle(this,baseUrl).catch(console.error);return;}var style=document.createElement('style');var url=baseUrl+'style.css';var self=this;style.setAttribute('scoped','');style.innerHTML='@import url('+url+');';this.appendChild(style);this.style.visibility='hidden';style.addEventListener('load',function(){if(self.shadowRoot){self.shadowRoot.appendChild(style.cloneNode(true));}
self.style.visibility='';});}};}(window));

View file

@ -0,0 +1,195 @@
/* exported CustomDialog */
//XXX: Waiting for the window.showModalDialog support in B2G
'use strict';
var CustomDialog = (function() {
var container = null;
var screen = null;
var dialog = null;
var header = null;
var message = null;
var yes = null;
var no = null;
return {
hide: function dialog_hide() {
if (screen === null) {
return;
}
if (!container) {
container = document.body;
}
container.removeChild(screen);
screen = null;
dialog = null;
header = null;
message = null;
yes = null;
no = null;
},
/**
* Method that shows the dialog
* @param {Object} title the l10n id for the title of the dialog. null or
* empty for no title. or you can give a object with more
* options like {icon: path or empty string,
* id: an l10n-id, args: an object of arguments that
* parameterise the 'id' field}.
* @param {String} msg message for the dialog. give a object like the
* title to enable more options.
* @param {Object} cancel {title, callback} object when confirm.
* @param {Object} confirm {title, callback} object when cancel.
* @param {Element} [containerElement] an optional element for which the
* dialog will be injected
*/
show: function dialog_show(title, msg, cancel, confirm, containerElement) {
container = containerElement || document.body;
if (screen === null) {
screen = document.createElement('form');
screen.setAttribute('role', 'dialog');
screen.setAttribute('data-type', 'confirm');
screen.id = 'dialog-screen';
dialog = document.createElement('section');
screen.appendChild(dialog);
// Create a reusable function to decorate elements with all
// possible options, instead of scattering similar code about
// everywhere.
//
// It's also possible to be extended with more usable decorating
// options and elements.
//
// 'title'|'message' -> Object|String -> the element -> dialog
// -> the decorated element
var decorateWithOptions = function cd_decorateWithOptions(type, options,
elm, dialog) {
if ('string' === typeof options) {
elm.setAttribute('data-l10n-id', options);
return elm;
}
var icon = options.icon;
var textElm = elm;
if (icon && '' !== icon) {
// We can't localize elements with child nodes
// so if there's going to be an icon, we need to create a separate
// node for text. See bug 1053629 for details
textElm = document.createElement('span');
var iconImg = new Image();
iconImg.src = icon;
iconImg.classList.add('custom-dialog-' + type + '-icon');
// Icons usually insert as the first element.
elm.insertBefore(iconImg, elm.firstChild);
elm.appendChild(textElm);
}
// More decorating options goes here.
if (options.id) {
document.l10n.setAttributes(
textElm,
options.id,
options.args
);
} else {
var text = options[type];
textElm.textContent = text;
}
return elm;
};
var setElementText = function (element, options) {
if ('string' === typeof options) {
element.setAttribute('data-l10n-id', options);
}
if(options.id) {
document.l10n.setAttributes(element, options.id, options.args);
}
};
header = document.createElement('h1');
header.id = 'dialog-title';
if (title && title !== '') {
header = decorateWithOptions('title', title, header, dialog);
}
dialog.appendChild(header);
message = document.createElement('p');
message.id = 'dialog-message';
message = decorateWithOptions('message', msg, message, dialog);
dialog.appendChild(message);
var menu = document.createElement('menu');
menu.dataset.items = 1;
no = document.createElement('button');
// The default type of the button element is "Submit",
// and form submit in system app would make system app reload.
no.type = 'button';
setElementText(no, cancel.title);
no.id = 'dialog-no';
no.addEventListener('click', clickHandler);
menu.appendChild(no);
if (confirm) {
menu.dataset.items = 2;
yes = document.createElement('button');
// The default type of button element is "Submit",
// and form submit in system app would make system app reload.
yes.type = 'button';
setElementText(yes, confirm.title);
yes.id = 'dialog-yes';
//confirm can be with class "danger" or "recommend"
//the default is "danger"
yes.className = confirm.recommend ? 'recommend' : 'danger';
yes.addEventListener('click', clickHandler);
menu.appendChild(yes);
}
else { // 1 button, should be full.
no.classList.add('full');
}
screen.appendChild(menu);
container.appendChild(screen);
}
// Make the screen visible
screen.classList.add('visible');
// This is the event listener function for the buttons
function clickHandler(evt) {
// Hide the dialog
screen.classList.remove('visible');
// Call the appropriate callback, if it is defined
if (evt.target === yes && confirm.callback) {
confirm.callback();
} else if (evt.target === no && cancel.callback) {
cancel.callback();
}
}
return screen;
}
};
}());

View file

@ -0,0 +1,49 @@
/**
* Shim for navigator.mozHour12 API.
* Send `localechanged` event if mozHour12 is changed.
*
* App need include following permission in manifest:
*
* "settings":{ "access": "readonly" }
*/
(function(){
'use strict';
// not polyfill if API already exists
if (window.navigator.mozHour12 || window.navigator.hour12) {return;}
// mock mozHour12 onto window.navigator
window.navigator.mozHour12 = undefined;
// set hour12 and emit the locale change event if value changed
var _setMozHour12 = function(result) {
if (result === null) {
// locale.hour12 may be true, false, `undefined` or `null`.
// We can't write to mozSettings with `undefined`, but we want to cast
// null to be undefined because then we can easily assign
// Intl hour12: navigator.mozHour12 which is a paradigm we want to use.
//
// if you set hour12 to undefined it uses automatic value, but it doesn't
// work if you set it to `null`, so we cast here to `undefined`.
result = undefined;
}
if (window.navigator.mozHour12 !== result) {
window.navigator.mozHour12 = result;
// emit the locale change event
window.dispatchEvent(new CustomEvent('timeformatchange'));
}
};
// handler observer event
var _hour12Handler = function(event) {
_setMozHour12(event.settingValue);
};
var _kLocaleTime = 'locale.hour12';
// update mozHour12 to real value
var req = window.navigator.mozSettings.createLock().get(_kLocaleTime);
req.onsuccess = function() {
_setMozHour12(req.result[_kLocaleTime]);
};
// monitor settings changes
window.navigator.mozSettings.addObserver(_kLocaleTime, _hour12Handler);
})();

View file

@ -0,0 +1,191 @@
/* global SettingsHelper */
/* exported DefaultActivityHelper */
(function(exports) {
'use strict';
// To add an element on the list, follow the example:
// {
// name: 'pick',
// type: ['image/jpeg',
// 'image/png',
// 'image/gif',
// 'image/bmp'],
// l10nId: 'default-activity-pickimage',
// settingsId: 'activity.default.pickimage'
// },
//
// NOTE: due to specs (bug1039386), some actions are grouped so when an app
// is set as default for any of the actions, the rest (from the same group)
// will be assigned the same app as default launch too.
// Grouped actions:
//
// new "webcontacts/contact"
// open "webcontacts/contact"
// update "webcontacts/contact"
//
// new "mail"
// view “url” (email)
//
// open "video"
// view “video”
//
var supportedActivities = [
{
name: 'dial',
type: ['webtelephony/number'],
l10nId: 'default-activity-dialnumber',
settingsId: 'activity.default.dialnumber'
},
{
name: 'new',
type: ['webcontacts/contact'],
l10nId: 'default-activity-opencontact',
settingsId: 'activity.default.opencontact'
// same as open contact, see previous note
},
{
name: 'new',
type: ['mail'],
l10nId: 'default-activity-newmail',
settingsId: 'activity.default.newmail'
},
{
name: 'new',
type: ['websms/sms'],
l10nId: 'default-activity-newsms',
settingsId: 'activity.default.newsms'
},
{
name: 'open',
type: ['webcontacts/contact'],
l10nId: 'default-activity-opencontact',
settingsId: 'activity.default.opencontact'
},
{
name: 'open',
type: ['image/jpeg',
'image/png',
'image/gif',
'image/bmp'],
l10nId: 'default-activity-openimage',
settingsId: 'activity.default.openimage'
},
{
name: 'open',
type: ['audio/mpeg',
'audio/ogg',
'audio/mp4'],
l10nId: 'default-activity-openaudio',
settingsId: 'activity.default.openaudio'
},
{
name: 'open',
type: ['video/webm',
'video/mp4',
'video/3gpp',
'video/youtube'],
l10nId: 'default-activity-openvideo',
settingsId: 'activity.default.openvideo'
},
{
name: 'record',
type: ['photos'],
l10nId: 'default-activity-recordphoto',
settingsId: 'activity.default.recordphoto'
},
{
name: 'record',
type: ['videos'],
l10nId: 'default-activity-recordvideo',
settingsId: 'activity.default.recordvideo'
},
{
name: 'view',
type: ['url'],
l10nId: 'default-activity-viewurl',
settingsId: 'activity.default.viewurl'
},
{
name: 'view',
type: ['application/pdf'],
l10nId: 'default-activity-viewpdf',
settingsId: 'activity.default.viewpdf'
},
{
name: 'view',
type: ['video/webm',
'video/mp4',
'video/3gpp',
'video/youtube'],
l10nId: 'default-activity-openvideo',
settingsId: 'activity.default.openvideo'
// same as open video, see previous note
},
{
name: 'update',
type: ['webcontacts/contact'],
l10nId: 'default-activity-opencontact',
settingsId: 'activity.default.opencontact'
// same as open contact, see previous note
}
];
var DefaultActivityHelper = {
_helper: null,
getHelper: function(config) {
this._helper = this._helper || SettingsHelper(config.settingsId, null);
return this._helper;
},
/**
* Looks for an activity on the list given the name and the type
* @parameter {String} name Activity name
* @parameter {String} type Activity type
* @returns {Object} Config object for the activity
*/
getDefaultConfig: function(name, type) {
var config = supportedActivities.find(function(a) {
return a.name === name && a.type.indexOf(type) >= 0;
});
return config;
},
/**
* Returns the current choice as default for a given activity name and type
* @param {String} name Activity name
* @param {String} type Activity type
* @param {Function} cb callback executed when search finished
*/
getDefaultAction: function(name, type) {
var config = this.getDefaultConfig(name, type);
return new Promise((resolve, reject) => {
if (!config) {
resolve(null);
}
this.getHelper(config).get((value) => {
resolve(value);
});
});
},
/**
* Sets a preference with the default choice for a given activity and type
* @param {String} name Activity name
* @param {String} type Activity type
* @param {String} choice App manifest of the chosen app
*/
setDefaultAction: function(name, type, choice) {
var config = this.getDefaultConfig(name, type);
if (!config) {
return;
}
this.getHelper(config).set(choice);
}
};
exports.DefaultActivityHelper = DefaultActivityHelper;
})(window);

View file

@ -0,0 +1,84 @@
/* global mozIntl */
/**
* This lib relies on `l10n.js' to implement localizable date/time strings.
*
* The proposed `DownloadFormatter' object provides features for formatting
* the data retrieved from the new API for Downloads, taking into account
* the structure defined by the API itself.
*/
(function(exports) {
'use strict';
var NUMBER_OF_DECIMALS = 2;
var BYTE_SCALE = ['B', 'KB', 'MB', 'GB', 'TB'];
function _getFormattedSize(bytes) {
if (bytes === undefined || isNaN(bytes)) {
return Promise.resolve(null);
}
var index = 0;
while (bytes >= 1024 && index < BYTE_SCALE.length) {
bytes /= 1024;
++index;
}
return document.l10n.
formatValue('byteUnit-' + BYTE_SCALE[index]).then(unit => {
return document.l10n.formatValue('fileSize', {
size: bytes.toFixed(NUMBER_OF_DECIMALS),
unit: unit
});
});
}
function _calcPercentage(currently, total) {
if (total === 0) {
return 0;
}
return parseInt((100 * currently) / total);
}
var DownloadFormatter = {
getFormattedSize: function(bytes) {
return _getFormattedSize(bytes);
},
getPercentage: function(download) {
return _calcPercentage(download.currentBytes, download.totalBytes);
},
getFileName: function(download) {
return download.path.split('/').pop(); // filename.ext
},
getTotalSize: function(download) {
var bytes = download.totalBytes;
return _getFormattedSize(bytes);
},
getDownloadedSize: function(download) {
var bytes = download.currentBytes;
return _getFormattedSize(bytes);
},
getDate: function(download) {
var date;
try {
date = download.startTime;
} catch (ex) {
date = new Date();
console.error(ex);
}
var formatter = mozIntl._gaia.RelativeDate(navigator.languages);
return formatter.format(date);
},
getUUID: function(download) {
return download.id || this.getFileName(download);
}
};
exports.DownloadFormatter = DownloadFormatter;
}(this));

View file

@ -0,0 +1,685 @@
'use strict';
/* exported DownloadHelper */
/* global DownloadFormatter */
/* global DownloadStore */
/* global DownloadUI */
/* global LazyLoader */
/* global MimeMapper */
/* global MozActivity */
/* global SettingsListener */
/*
* DownloadHelper.js: Perform some utility functions with DOMDownload
* objects.
*
* - You have to include in you HTML:
*
* <!-- <script src="shared/js/mime_mapper.js"></script> -->
* <!-- <script src="shared/js/download/download_store.js"></script> -->
* <script defer src="shared/js/lazy_loader.js"></script>
*
* - How to use this component.
*
* For launching a download
*
* var req = DownloadHelper.open(download);
*
* req.onsuccess = function req_onsuccess() {
* alert('The download was opened so we can remove the notification');
* }
*
* req.onerror = function req_onerror() {
* alert('Something wrong!: ' + req.error.message);
* }
*
*/
var DownloadHelper = (function() {
// Exception code constants
var CODE = {
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
DEVICE_STORAGE: 'DEVICE_STORAGE',
MIME_TYPE_NOT_SUPPORTED: 'MIME_TYPE_NOT_SUPPORTED',
INVALID_STATE: 'INVALID_STATE',
NO_SDCARD: 'NO_SDCARD',
NO_PROVIDER: 'NO_PROVIDER',
UNMOUNTED_SDCARD: 'UNMOUNTED_SDCARD'
};
/*
* Request auxiliary object to support asynchronous calls
*/
var Request = function() {
this.done = function(result) {
if (typeof this.onsuccess === 'function') {
this.result = result;
window.setTimeout(function() {
this.onsuccess({
target: this
});
}.bind(this), 0);
}
};
this.failed = function(error) {
if (typeof this.onerror === 'function') {
this.error = error;
window.setTimeout(function() {
this.onerror({
target: this
});
}.bind(this), 0);
}
};
};
// Storage name by default
var STORAGE_NAME_BY_DEFAULT = 'sdcard';
// Storage name settings key
var STORAGE_NAME_KEY = 'device.storage.writable.name';
// Current storage name
var storageName = STORAGE_NAME_BY_DEFAULT;
LazyLoader.load('shared/js/settings_listener.js', function settingsLoaded() {
SettingsListener.observe(STORAGE_NAME_KEY, STORAGE_NAME_BY_DEFAULT,
function setStorageName(evt) {
var settingValue = evt.settingValue;
if (settingValue) {
storageName = settingValue;
}
}
);
});
/*
* Error auxiliary method
*/
function sendError(req, message, code) {
req.failed({
message: message,
code: code
});
console.error(message);
}
/*
* Returns a blob from the sdcard
*
* @param{Object} The download object
*
*/
function getBlob(download) {
var req = new Request();
var storage = getVolume(download.storageName);
var storeAvailableReq = storage.available();
storeAvailableReq.onsuccess = function available_onsuccess(e) {
var path = download.storagePath;
switch (storeAvailableReq.result) {
case 'unavailable':
sendError(req, ' Could not open the file: ' + path + ' from ' +
download.storageName, CODE.NO_SDCARD);
break;
case 'shared':
sendError(req, ' Could not open the file: ' + path + ' from ' +
download.storageName, CODE.UNMOUNTED_SDCARD);
break;
default:
try {
var storeGetReq = storage.get(path);
storeGetReq.onsuccess = function store_onsuccess() {
req.done(storeGetReq.result);
};
storeGetReq.onerror = function store_onerror() {
sendError(req, storeGetReq.error.name +
' Could not open the file: ' + path + ' from ' +
download.storageName, CODE.FILE_NOT_FOUND);
};
} catch (ex) {
sendError(req, 'Error getting the file ' + path + ' from ' +
download.storageName, CODE.DEVICE_STORAGE);
}
}
};
storeAvailableReq.onerror = function available_onerror() {
sendError(req, 'Error getting storage state ', CODE.DEVICE_STORAGE);
};
return req;
}
/*
* Base action that implements the commom logic for actions based on the
* MozActivity API
*
* @param{Object} Configuration parameters
*/
var Action = function Action(params) {
this.req = params.request;
this.name = params.actionType.activityName;
};
Action.prototype = {
/*
* It performs the action based on MozActivity
*/
run: function a_run() {
var activity = new MozActivity({
name: this.name,
data: this.data
});
activity.onsuccess = this._onsuccess.bind(this);
activity.onerror = this._onerror.bind(this);
},
/*
* This method implements the generic onsuccess callback
*/
_onsuccess: function a_onsuccess() {
this.req.done();
},
/*
* This method implements the generic onerror callback
*/
_onerror: function a_onerror(evt) {
this.req.failed({
message: evt.target.error.name
});
}
};
/*
* This action opens downloads extending the Action Object
*
* @param{Object} Configuration parameters
*/
var OpenAction = function OpenAction(params) {
Action.call(this, params);
this.data = {
url: params.download.path,
filename: DownloadFormatter.getFileName(params.download),
type: params.type,
blob: params.blob
};
};
OpenAction.prototype = {
__proto__: Action.prototype
};
/*
* This action shares downloads extending the Action Object
*
* @param{Object} Configuration parameters
*/
var ShareAction = function ShareAction(params) {
Action.call(this, params);
this.data = {
// 'share' activities do not work with specific mime types
type: params.type.split('/')[0] + '/*',
blobs: [params.blob],
filenames: [DownloadFormatter.getFileName(params.download)]
};
};
ShareAction.prototype = {
__proto__: Action.prototype,
/*
* It overrides the generic onerror callback for another more suitable
*/
_onerror: function sa_onerror(evt) {
if (evt.target.error.name !== 'NO_PROVIDER') {
return;
}
sendError(this.req, 'No provider to share file', CODE.NO_PROVIDER);
}
};
/*
* This action gets the info for a download
*
* @param{Object} Configuration parameters
*/
var InfoAction = function InfoAction(params) {
Action.call(this, params);
this.data = {
name: DownloadFormatter.getFileName(params.download),
type: params.type,
blob: params.blob,
size: params.download.totalBytes,
path: params.download.path
};
};
InfoAction.prototype = {
__proto__: Action.prototype,
/*
* It overrides the generic run method
*/
run: function ia_run() {
this.req.done(this.data);
}
};
// This is a factory that deals with different <Action> objects
var ActionsFactory = {
TYPE: {
OPEN: {
activityName: 'open',
actionClass: OpenAction
},
SHARE: {
activityName: 'share',
actionClass: ShareAction
},
INFO: {
actionClass: InfoAction
},
WALLPAPER: {
activityName: 'setwallpaper',
actionClass: ShareAction
},
RINGTONE: {
activityName: 'setringtone',
actionClass: ShareAction
}
},
create: function af_create(params) {
return new params.actionType.actionClass(params);
}
};
/*
* Returns the mime type
*
* @param{Object} It represents a DOMDownload object
*
* @returns(String) Mime type
*/
function getType(download) {
var fileName = DownloadFormatter.getFileName(download);
var type = MimeMapper.guessTypeFromFileProperties(fileName,
download.contentType);
return type;
}
/*
* This method allows third-parties to open or share downloads
*
* @param{Object} Action types: <ActionsFactory.TYPE.OPEN> or
* <ActionsFactory.TYPE.SHARE>
*
* @param{Object} It represents a DOMDownload object
*/
function runAction(actionType, download) {
var req = new Request();
window.setTimeout(function launching() {
var state = download.state;
if (state === 'succeeded' || state === 'finalized') {
LazyLoader.load(['shared/js/mime_mapper.js',
'shared/js/download/download_formatter.js'],
function loaded() {
var type = getType(download);
//
// The 'open' action will always launch an activity using the original
// content type to allow for third party applications to handle
// arbitrary types of content.
//
// The 'share' action on the other hand only works with known mime
// types at this time.
//
if (type.length === 0) {
type = download.contentType;
if (actionType !== ActionsFactory.TYPE.OPEN &&
actionType !== ActionsFactory.TYPE.INFO) {
sendError(req, 'Mime type not supported: ' + type,
CODE.MIME_TYPE_NOT_SUPPORTED);
return;
}
}
var blobReq = getBlob(download);
blobReq.onsuccess = function() {
ActionsFactory.create({
actionType: actionType,
download: download,
type: type,
blob: blobReq.result,
request: req
}).run();
};
blobReq.onerror = function() {
// Problem getting the blob from the sdcard
req.failed(blobReq.error);
};
});
return;
}
sendError(req, 'Becareful, the download is not finished!',
CODE.INVALID_STATE);
}, 0);
return req;
}
/*
* This method allows clients to remove a download, from the
* list and the phone.
*
* @param{Object} It represents a DOMDownload object
*/
function remove(download) {
var req = new Request();
var incompleteDownload = download.state !== 'succeeded';
// If is not done, use download manager to remove it,
// otherwise, deal with the datastore.
setTimeout(function() {
if (incompleteDownload) {
if (!navigator.mozDownloadManager) {
sendError(req, 'DownloadManager not present', CODE.INVALID_STATE);
} else {
// First we pause the download so that everyone knows it's being
// stopped. The Downloads API itself won't stop the download first,
// it will simply kill it.
// XXXAus: Remove when we fix bug #1090551
download.pause().then(
function() {
navigator.mozDownloadManager.remove(download).then(
function success() {
req.done(download);
},
function error() {
sendError(req,
'DownloadManager doesnt know about this download',
CODE.INVALID_STATE);
}
);
},
function() {
sendError(req,
'Failed to pause download before removal',
CODE.INVALID_STATE);
}
);
}
} else {
req.done(download);
}
}, 0);
return incompleteDownload ? req : doRemoveFromPhone(req, download);
}
/*
* Performs the proper delete of the physical file, also
* from the datastore if the download has finished.
*
* @param{Object} This is the Request object
* @param{Object} It represents a DOMDownload object
*/
function doRemoveFromPhone(deleteRequest, download) {
var req = new Request();
deleteRequest.onsuccess = function() {
var storage = getVolume(download.storageName);
var storeAvailableReq = storage.available();
storeAvailableReq.onsuccess = function available_onsuccess(e) {
var path = download.path;
switch (storeAvailableReq.result) {
case 'unavailable':
sendError(req, ' Could not delete the file: ' + path + ' from ' +
storageName, CODE.NO_SDCARD);
break;
case 'shared':
sendError(req, ' Could not delete the file: ' + path + ' from ' +
storageName, CODE.UNMOUNTED_SDCARD);
break;
default:
var storeDeleteReq = storage.delete(download.storagePath);
storeDeleteReq.onsuccess = function store_onsuccess() {
// Remove from the datastore if status is 'succeeded'
// if we find any problem with the datastore, don't send
// an error, since the physical remove already happened
if (download.state === 'succeeded') {
LazyLoader.load(['/shared/js/download/download_store.js'],
function() {
DownloadStore.remove(download);
}
);
}
req.done(storeDeleteReq.result);
};
storeDeleteReq.onerror = function store_onerror() {
sendError(req, storeDeleteReq.error.name +
' Could not remove the file: ' + download.path + ' from ' +
storageName, CODE.FILE_NOT_FOUND);
};
}
};
storeAvailableReq.onerror = function available_onerror() {
sendError(req, 'Error getting storage state ', CODE.DEVICE_STORAGE);
};
};
deleteRequest.onerror = function(error) {
sendError(req, error.message, error.code);
};
return req;
}
function handlerError(error, download, cb) {
LazyLoader.load('shared/js/download/download_ui.js', (function loaded() {
var req;
var show = DownloadUI.show;
// Canceled activites are normal and shouldn't be interpreted as errors.
// Unfortunately, this isn't reported in a standard way by our
// applications (or third party apps for that matter). This is why we
// have this lazy filter here that may need to be updated in the future
// but hopefully will just get removed.
if (error.message &&
(error.message.endsWith('canceled') ||
error.message.endsWith('cancelled'))) {
// Since this isn't actually an error, we invoke the callback with null.
cb && cb(null);
// and return early.
return;
}
switch (error.code) {
case CODE.NO_SDCARD:
case CODE.UNMOUNTED_SDCARD:
case CODE.FILE_NOT_FOUND:
case CODE.NO_PROVIDER:
req = show(DownloadUI.TYPE[error.code], download, true);
req.onconfirm = cb;
break;
case CODE.MIME_TYPE_NOT_SUPPORTED:
req = show(DownloadUI.TYPE.UNSUPPORTED_FILE_TYPE, download, true);
req.onconfirm = function tuftOnConfirm() {
showRemoveFileUI(download, cb);
};
break;
default:
req = show(DownloadUI.TYPE.FILE_OPEN_ERROR, download, true);
req.onconfirm = function tfoeOnConfirm() {
showRemoveFileUI(download, cb);
};
break;
}
// We have to remove the notification if the user cancels
req.oncancel = cb;
}));
}
function showRemoveFileUI(download, cb) {
var req = DownloadUI.show(DownloadUI.TYPE.DELETE, download, true);
req.oncancel = cb;
req.onconfirm = function doRemove() {
if (typeof cb === 'function') {
cb(download);
} else {
remove(download);
}
};
}
function getFreeSpace(cb) {
var storage = navigator.getDeviceStorage(storageName);
if (!storage) {
console.error('Cannot get free space size in sdcard');
cb(null);
return;
}
var req = storage.freeSpace();
req.onsuccess = function(e) {
cb(e.target.result);
};
req.onerror = function() {
cb(null);
};
}
/**
* Gets the storage for the download based on the volumen
* it was saved (storageName)
*/
function getVolume(volumeName) {
// 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 (!volumeName || volumeName === '') {
return volumes[0];
}
for (var i = 0; i < volumes.length; ++i) {
if (volumes[i].storageName === volumeName) {
return volumes[i];
}
}
return volumes[0];
}
return {
/*
* This method allows clients to open a downlaod
*
* @param{Object} It represents a DOMDownload object
*/
open: function(download) {
return runAction(ActionsFactory.TYPE.OPEN, download);
},
/*
* This method allows clients to share a downlaoded file
*
* @param{Object} It represents a DOMDownload object
*/
share: function(download) {
return runAction(ActionsFactory.TYPE.SHARE, download);
},
/*
* This method allows clients to set as wallaper a downlaoded file
*
* @param{Object} It represents a DOMDownload object
*/
wallpaper: function(download) {
return runAction(ActionsFactory.TYPE.WALLPAPER, download);
},
/*
* This method allows clients to set as ringtone a downlaoded file
*
* @param{Object} It represents a DOMDownload object
*/
ringtone: function(download) {
return runAction(ActionsFactory.TYPE.RINGTONE, download);
},
/*
* This method returns information about a download
*
* @param{Object} It represents a DOMDownload object
*/
info: function(download) {
return runAction(ActionsFactory.TYPE.INFO, download);
},
/*
* Given a download, remove it from the DownloadManager
* list, and the file system.
*
* @param{Object} It represents a DOMDownload object
*/
remove: remove,
/*
* Returns exception code constants
*/
get CODE() {
return CODE;
},
/*
* This method handles different errors when users attemp to open files
*
* @param{Object} Error object
*
* @param{Object} It represents a DOMDownload object
*
* @param{Function} This function is performed when the flow is finished
*/
handlerError: handlerError,
/*
* Returns the free memory size in bytes
*
* @param{Function} This function is performed when the free memory size has
* been calculated
*/
getFreeSpace: getFreeSpace
};
}());

View file

@ -0,0 +1,451 @@
'use strict';
/* jshint moz:true */
/* global Promise */
/* global indexedDB */
/*
* DownloadStore.js: Allows to store download finished ordered by timestamp
*
* - Adding a new download:
*
* var req = DownloadStore.add(download);
*
* req.onsuccess = function() {
* console.log('Complete download stored successfully');
* };
*
* req.onerror = function(e) {
* console.error(e);
* };
*
* - Getting all downloads:
*
* var reqStore = DownloadStore.getAll();
*
* reqStore.onsuccess = function(ev) {
* ev.target.result.forEach(function(download) {
* // Do something cool with the download
* });
* };
*
* reqStore.onerror = function(e) {
* console.error(e);
* };
*
* - Removing a existing download:
*
* var req = DownloadStore.remove(download);
*
* req.onsuccess = function() {
* console.log('Deleted download successfully');
* };
*
* req.onerror = function(e) {
* console.error(e);
* };
*
* WARNING:
*
* The system app should add this to its manifest file
*
* "datastores-owned": {
* "download_store": {
* "description": "Stores download finished"
* }
* }
*
* The settings app should add this to its manifest file
*
* "datastores-access": {
* "download_store": {
* "description": "Stores download finished"
* }
* }
*
*/
/* exported DownloadStore */
var DownloadStore = (function() {
var datastore;
// Datastore name declared on the manifest.webapp
const DATASTORE_NAME = 'download_store';
var downloadListDB;
var downloadList = [];
var lastRevisionId;
const LIST_DB = 'download_list';
const LIST_STORE = 'download_list';
const LIST_KEY = 0;
const REVISION_KEY = 1;
/*
* Request auxiliary object to support asynchronous calls
*/
var Request = function() {
this.done = function(result) {
if (typeof this.onsuccess === 'function') {
this.result = result;
window.setTimeout(function() {
this.onsuccess({
target: this
});
}.bind(this), 0);
}
};
this.failed = function(error) {
if (typeof this.onerror === 'function') {
this.error = error;
window.setTimeout(function() {
this.onerror({
target: this
});
}.bind(this), 0);
}
};
};
function promisify(request) {
return new Promise(function(resolve, reject) {
request.onsuccess = (event) => {resolve(event.target.result);};
request.onerror = (event) => {reject(event.target.errorCode);};
});
}
// Indicates the initialization state
var readyState;
function init(success, fail) {
if (readyState === 'initialized') {
success();
return;
}
if (readyState === 'initializing') {
document.addEventListener('ds-initialized', function oninitalized() {
document.removeEventListener('ds-initialized', oninitalized);
success();
});
return;
}
readyState = 'initializing';
if (!navigator.getDataStores) {
var messageError = 'Data store: DataStore API is not working';
console.error(messageError);
fail({
message: messageError
});
return;
}
navigator.getDataStores(DATASTORE_NAME).then(function(ds) {
var messageError = 'Download Store: Cannot get access to the DataStore';
if (ds.length < 1) {
console.error(messageError);
throw ({
message: messageError
});
}
datastore = ds[0];
// Checking the length as the index should be there
}).then(function() {
return openDownloadListIndexedDB();
}).then(function(db) {
downloadListDB = db;
var listStore = downloadListDB.transaction(LIST_STORE, 'readonly')
.objectStore(LIST_STORE);
return Promise.all([
promisify(listStore.get(LIST_KEY))
.then((value) => {downloadList = value;}),
promisify(listStore.get(REVISION_KEY))
.then((value) => {lastRevisionId = value;})
]);
}).then(function() {
readyState = 'initialized';
// Remove the legacy index element, if it exists. See Bug 1180250.
const LEGACY_INDEX_ID = 1;
if (downloadList && downloadList.includes(LEGACY_INDEX_ID)) {
return promisify(get(LEGACY_INDEX_ID)).then((download) => {
if ('byTimestamp' in download) {
downloadList.splice(downloadList.indexOf(LEGACY_INDEX_ID), 1);
}
});
}
}).then(function() {
notifyOpenSuccess(success);
}).catch(function(e) {
console.error('Error while opening the Download Store: ', e.message);
fail(e);
});
}
function openDownloadListIndexedDB() {
return new Promise(function(resolve, reject) {
var request = indexedDB.open(LIST_DB);
request.onsuccess = function(event) {
var db = event.target.result;
resolve(db);
};
request.onerror = function(event) {
reject(event.target.errorCode);
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
promisify(db.createObjectStore(LIST_STORE)).then(() => {
var listStore = db.transaction(LIST_STORE, 'readwrite')
.objectStore(LIST_STORE);
return Promise.all([
promisify(listStore.add([], LIST_KEY)),
promisify(listStore.add(0, REVISION_KEY))
]);
}).catch(reject);
};
request.onblocked = function(event) {
reject(event.target.errorCode);
};
});
}
function notifyOpenSuccess(cb) {
window.setTimeout(cb, 0);
document.dispatchEvent(new CustomEvent('ds-initialized'));
}
// These fields will be stored in our datastore
var fieldsToPropagate = ['url', 'path', 'totalBytes', 'contentType',
'startTime', 'state', 'storageName', 'storagePath'];
function cookDownload(download) {
var ret = Object.create(null);
fieldsToPropagate.forEach(function(field) {
ret[field] = download[field];
});
// This is the timestamp when the download finished
ret.finalizeTime = new Date();
return ret;
}
function defaultError(req) {
return defaultErrorCb.bind(null, req);
}
function defaultErrorCb(request, error) {
request.failed(error);
}
function defaultSuccess(req) {
return defaultSuccessCb.bind(null, req);
}
function defaultSuccessCb(request, result) {
request.done(result);
}
function updateDownloadList() {
var cursor = datastore.sync(lastRevisionId);
function cursorResolve(task) {
switch (task.operation) {
case 'done':
lastRevisionId = task.revisionId;
var listStore = downloadListDB.transaction(LIST_STORE, 'readwrite')
.objectStore(LIST_STORE);
return Promise.all([
promisify(listStore.put(downloadList, LIST_KEY)),
promisify(listStore.put(lastRevisionId, REVISION_KEY))
]);
case 'clear':
downloadList = [];
break;
case 'add':
downloadList.push(task.id);
break;
case 'update':
// This doesn't matter for us.
break;
case 'remove':
var i = downloadList.indexOf(task.id);
if (i >= 0) {
downloadList.splice(i, 1);
}
break;
}
return cursor.next().then(cursorResolve);
}
return cursor.next().then(cursorResolve);
}
function doAdd(download, req) {
var downloadCooked = cookDownload(download);
// Adding our cooked download
datastore.add(downloadCooked).then(function(id) {
// Enriched object with the id provided by the datastore
downloadCooked.id = id;
datastore.put(downloadCooked, id)
.then(function() { req.done(downloadCooked); },
defaultError(req));
}, defaultError(req));
}
function add(download) {
var req = new Request();
window.setTimeout(function() {
init(doAdd.bind(null, download, req), req.failed.bind(req));
});
return req;
}
function doGet(id, req) {
datastore.get(id).then(function(download) { req.done(download); },
defaultError(req));
}
function get(id) {
var req = new Request();
window.setTimeout(function() {
init(doGet.bind(null, id, req), req.failed.bind(req));
});
return req;
}
function doGetAll(req) {
updateDownloadList().then(function() {
datastore.get.apply(datastore, downloadList).then(defaultSuccess(req),
defaultError(req));
}, defaultError(req));
}
function getAll() {
var req = new Request();
window.setTimeout(function() {
init(doGetAll.bind(null, req), req.failed.bind(req));
});
return req;
}
function doRemove(id, req) {
// Removing the download object from datastore
datastore.remove(id).then(function(success) {
if (success) {
defaultSuccess(req);
} else {
req.failed({
message: 'The download with id: ' + id + 'does not exist'
});
}
}, defaultError(req));
}
function remove(download) {
var req = new Request();
window.setTimeout(function() {
init(doRemove.bind(null, download.id, req), req.failed.bind(req));
});
return req;
}
function doAddListener(listener) {
datastore.addEventListener('change', listener);
}
function addListener(listener) {
var req = new Request();
window.setTimeout(function() {
init(doAddListener.bind(null, listener), req.failed.bind(req));
});
return req;
}
function doRemoveListener(listener) {
datastore.removeEventListener('change', listener);
}
function removeListener(listener) {
var req = new Request();
window.setTimeout(function() {
init(doRemoveListener.bind(null, listener), req.failed.bind(req));
});
return req;
}
return {
/*
* This method returns the download with the specified id.
*/
get: get,
/*
* This method returns an array of complete downloads
*/
getAll: getAll,
/*
* It adds a new download object and returns the new datastore
* download object
*
* @param{Object} Download object provided by the API
*
*/
add: add,
/*
* It removes a download object
*
* @param{Object} The download object to be removed from the datastore
*
*/
remove: remove,
/*
* Add a listener that will receive datastore change events.
*
* @param{Function} The listener to be added.
*/
addListener: addListener,
/*
* Remove a listener previously added via addListener.
*
* @param{Function} The listener to be removed.
*/
removeListener: removeListener
};
}());

View file

@ -0,0 +1,358 @@
'use strict';
/* exported DownloadUI */
/* global DownloadFormatter */
/* global LazyLoader */
/* global MimeMapper */
/**
* This file defines a component to show download confirmations
*
* - Stop download (Are you sure you want to stop the download?)
* - Download stopped (Download was stopped. Try downloading again?)
* - Download failed (xfile failed to download. Try downloading again?)
* - Delete download (Delete xfile?)
* - Unsupported file type
* - File not found
* - File open error
* - No provider to share file
*
* var request = DownloadUI.show(DownloadUI.TYPE.STOP, download);
*
* request.oncancel = function() {
* alert('CANCEL');
* };
*
* request.onconfirm = function() {
* alert('CONFIRM');
* };
*
* WARNING: To use this library you need to include 'shared/js/l10n.js'
*
*/
var DownloadUI = (function() {
/**
* Download type constructor
*
* @param {String} Type name
* @param {Array} CSS classes to confirm button
* @param {Boolean} Message without parameters
*/
var DownloadType = function(name, classes, isPlainMessage) {
this.name = name;
this.classes = classes;
this.isPlainMessage = isPlainMessage;
this.numberOfButtons = classes.indexOf('full') !== -1 ? 1 : 2;
};
/**
* Errors reported by the Downloads API.
*/
var ERRORS = {
NO_MEMORY: 2152857616,
NO_SDCARD: 2152857618,
UNMOUNTED_SDCARD: 2152857621
};
var TYPES = {
STOP: new DownloadType('stop', ['danger'], true),
STOPPED: new DownloadType('stopped', ['recommend'], true),
FAILED: new DownloadType('failed', ['recommend']),
DELETE: new DownloadType('delete', ['danger']),
DELETE_ALL: new DownloadType('delete_all', ['danger']),
UNSUPPORTED_FILE_TYPE: new DownloadType('unsupported_file_type',
['danger']),
FILE_NOT_FOUND: new DownloadType('file_not_found', ['recommend', 'full'],
true),
FILE_OPEN_ERROR: new DownloadType('file_open_error', ['danger']),
NO_SDCARD: new DownloadType('no_sdcard_found_2', ['recommend', 'full'],
true),
UNMOUNTED_SDCARD: new DownloadType('unmounted_sdcard_2', ['recommend',
'full'], true),
NO_PROVIDER: new DownloadType('no_provider', ['recommend', 'full'], true),
NO_MEMORY: new DownloadType('no_memory', ['recommend', 'full'], true)
};
var DownloadAction = function(id, type) {
this.id = id;
this.name = id.toLowerCase();
this.title = this.name + '_downloaded_file';
this.type = type;
};
var ACTIONS = {
OPEN: new DownloadAction('OPEN', 'confirm'),
SHARE: new DownloadAction('SHARE', 'confirm'),
WALLPAPER: new DownloadAction('WALLPAPER', 'confirm'),
RINGTONE: new DownloadAction('RINGTONE', 'confirm')
};
// Confirm dialog containers
var confirm = null;
var actionMenu = null;
/**
* Request auxiliary object to support asynchronous calls
*/
var Request = function() {
this.cancel = function() {
removeContainers();
if (typeof this.oncancel === 'function') {
this.oncancel();
}
};
this.confirm = function(result) {
removeContainers();
if (typeof this.onconfirm === 'function') {
this.result = result;
this.onconfirm({
target: this
});
}
};
};
function removeContainers() {
removeConfirm();
removeActionMenu();
}
function addConfirm() {
if (confirm !== null) {
confirm.parentNode.removeChild(confirm);
}
confirm = document.createElement('form');
confirm.id = 'downloadConfirmUI';
confirm.setAttribute('role', 'dialog');
confirm.setAttribute('data-type', 'confirm');
confirm.setAttribute('data-z-index-level', '100');
var screen = document.getElementById('screen');
var container = screen || document.body;
container.appendChild(confirm);
}
function removeConfirm() {
if (confirm === null) {
return;
}
confirm.innerHTML = '';
confirm.style.display = 'none';
}
// When users click or hold on home button UIs should be removed
window.addEventListener('home', removeContainers);
window.addEventListener('holdhome', removeContainers);
function l10n(element, l10nid, l10nargs) {
// First set our args.
if (l10nargs) {
element.setAttribute('data-l10n-args', JSON.stringify(l10nargs));
}
// Then localize.
element.setAttribute('data-l10n-id', l10nid);
return element;
}
function createConfirm(type, req, downloads) {
addConfirm();
var dialog = document.createElement('section');
// Header
var header = document.createElement('h1');
l10n(header, type.name + '_download_title');
dialog.appendChild(header);
// Message
var message = document.createElement('p');
if (type.isPlainMessage) {
l10n(message, type.name + '_download_message');
} else {
var args = Object.create(null);
if (type === TYPES.DELETE_ALL) {
args.number = downloads.length;
} else {
args.name = DownloadFormatter.getFileName(downloads[0]);
}
l10n(message, type.name + '_download_message', args);
}
dialog.appendChild(message);
var menu = document.createElement('menu');
menu.dataset.items = type.numberOfButtons;
if (type.numberOfButtons === 2) {
// Left button
var lButton = document.createElement('button');
lButton.type = 'button';
lButton.setAttribute('data-l10n-id',
type.name + '_download_left_button');
lButton.onclick = function l_cancel() {
lButton.onclick = null;
req.cancel();
};
menu.appendChild(lButton);
}
// Right button
var rButton = document.createElement('button');
rButton.type = 'button';
type.classes.forEach(function(clazz) {
rButton.classList.add(clazz);
});
rButton.setAttribute('data-l10n-id',
type.name + '_download_right_button');
rButton.onclick = function r_confirm() {
rButton.onclick = null;
req.confirm();
};
menu.appendChild(rButton);
dialog.appendChild(menu);
confirm.appendChild(dialog);
confirm.style.display = 'block';
}
function addActionMenu() {
// If we have an actionMenu, empty it and re-create it.
if (actionMenu !== null) {
document.body.removeChild(actionMenu);
}
actionMenu = document.createElement('gaia-menu');
actionMenu.id = 'downloadActionMenuUI';
actionMenu.className = 'actions';
}
function removeActionMenu() {
if (actionMenu === null) {
return;
}
actionMenu.hide();
document.body.removeChild(actionMenu);
actionMenu = null;
}
function createActionMenu(req, download) {
var actions = [ACTIONS.OPEN, ACTIONS.SHARE];
var fileName = DownloadFormatter.getFileName(download);
var type = MimeMapper.guessTypeFromFileProperties(fileName,
download.contentType);
if (type.length > 0) {
if (type.startsWith('image/')) {
actions.push(ACTIONS.WALLPAPER);
} else if (type.startsWith('audio/')) {
actions.push(ACTIONS.RINGTONE);
}
}
doCreateActionMenu(req, fileName, actions);
}
function doCreateActionMenu(req, fileName, actions) {
addActionMenu();
var header = document.createElement('header');
header.textContent = fileName;
actionMenu.appendChild(header);
actions.forEach(function addActionButton(action) {
var button = document.createElement('button');
button.id = action.id;
l10n(button, action.title);
button.dataset.type = action.type;
actionMenu.appendChild(button);
button.addEventListener('click', function buttonCliked(evt) {
button.removeEventListener('click', buttonCliked);
req[evt.target.dataset.type](ACTIONS[evt.target.id]);
});
});
document.body.appendChild(actionMenu);
actionMenu.removeAttribute('hidden');
}
var styleSheets = [
'shared/style/buttons.css',
'shared/style/headers.css',
'shared/style/confirm.css'
];
/*
* Shows a confirmation depending on type. It returns a request object with
* oncancel and onconfirm callbacks
*
* @param {String} Confirmation type
*
* @param {Array} It represents the download(s) object(s)
*
* @param {Boolean} This optional parameter indicates if the library should
* include BBs
*/
function show(type, downloads, ignoreStyles) {
var req = new Request();
downloads = Array.isArray(downloads) ? downloads : [downloads];
window.setTimeout(function() {
var libs = ['shared/js/download/download_formatter.js'];
if (!ignoreStyles) {
libs.push.apply(libs, styleSheets);
}
// We have to discover the type of UI depending on state when type is null
if (type === null) {
type = TYPES.STOPPED;
var download = downloads[0];
if (download.state === 'finalized' ||
download.state === 'stopped' && download.error !== null) {
type = TYPES.FAILED;
}
}
LazyLoader.load(libs, createConfirm.call(this, type, req, downloads));
}, 0);
return req;
}
function showActions(download) {
var req = new Request();
window.setTimeout(function() {
LazyLoader.load(['shared/js/mime_mapper.js',
'shared/js/download/download_formatter.js',
'shared/js/component_utils.js',
'shared/elements/gaia_menu/script.js'],
createActionMenu.call(this, req, download));
}, 0);
return req;
}
return {
show: show,
showActions: showActions,
hide: removeContainers,
get ERRORS() {
return ERRORS;
},
get TYPE() {
return TYPES;
}
};
}());

View file

@ -0,0 +1,4 @@
;(function(){'use strict';function dump_off(msg,optionalObject){}
function dump_on(msg,optionalObject){var output=msg;if(optionalObject){try{output+=JSON.stringify(optionalObject);}catch(e){}}
if(dump){var appName=document.location.hostname.replace(/\..*$/,'');dump('['+appName+'] '+output+'\n');}else{console.log(output);}}
window.DUMP=dump_off;var settings=window.navigator.mozSettings;var reqGaiaDebug=settings.createLock().get('debug.gaia.enabled');reqGaiaDebug.onsuccess=function gaiaDebug(){window.DUMP=reqGaiaDebug.result['debug.gaia.enabled']?dump_on:dump_off;};settings.addObserver('debug.gaia.enabled',function(event){window.DUMP=event.settingValue?dump_on:dump_off;dump_on(event.settingValue?'Enabling DUMP':'Disabling DUMP');});}());

View file

@ -0,0 +1,60 @@
'use strict';
/* exported eventSafety */
/**
* Listens for an event to fire _once_ or a timeout to expire.
* This is generally used for things which are waiting on transitions
* as it's sometimes possible to miss the transitionend event.
* This should not be necessary once we have web animations as they
* will guarantee that a callback is fired.
*
* Usage:
*
* eventSafety(window, 'transitionend', callback, 400);
*
* eventSafety(window, 'transitionend', 400).then(() => { ... });
*
* @param {Object} obj
* Object to listen to the event on.
* @param {String} event
* The name of the event.
* @param {Function} callback
Optional; called after the event is fired.
* @param {Integer} timeout The duration of the safety timeout.
*
* @return {Promise}
* A promise that will never reject.
*/
function eventSafety(obj, event, callback, timeout) {
if (typeof callback === 'number') {
timeout = callback;
callback = null;
}
if (!timeout) {
throw new Error('You must pass a valid timeout value to `eventSafety`.');
}
return new Promise((resolve) => {
var finishTimeout;
function done(e) {
// Both "transitionend" and "animationend" events bubble by default;
// ignore them here if they're not targeted on the element we care about.
if (e && e.target !== obj &&
(e.type === 'transitionend' || e.type === 'animationend')) {
return;
}
clearTimeout(finishTimeout);
obj.removeEventListener(event, done);
if (callback) {
/*jshint validthis:true */
resolve(callback.apply(this, arguments));
} else {
resolve(e);
}
}
finishTimeout = setTimeout(done, timeout);
obj.addEventListener(event, done);
});
}

View file

@ -0,0 +1,25 @@
/* exported IAC_API_WAKEUP_REASON_ENABLED_CHANGED */
/* exported IAC_API_WAKEUP_REASON_TRY_DISABLE */
/* exported IAC_API_WAKEUP_REASON_LOGIN */
/* exported IAC_API_WAKEUP_REASON_LOGOUT */
/* exported IAC_API_WAKEUP_REASON_STALE_REGISTRATION */
/* exported IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED */
/* exported wakeUpFindMyDevice */
'use strict';
const IAC_API_WAKEUP_REASON_ENABLED_CHANGED = 0;
const IAC_API_WAKEUP_REASON_STALE_REGISTRATION = 1;
const IAC_API_WAKEUP_REASON_LOGIN = 2;
const IAC_API_WAKEUP_REASON_LOGOUT = 3;
const IAC_API_WAKEUP_REASON_TRY_DISABLE = 4;
const IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED = 5;
function wakeUpFindMyDevice(reason) {
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
app.connect('findmydevice-wakeup').then(function(ports) {
ports[0].postMessage(reason);
});
};
}

View file

@ -0,0 +1,39 @@
/* exported Gzip */
'use strict';
var Gzip = {
WORKER_URL: '/shared/js/gzip/gzip_worker.js',
compress: function(payload) {
return new Promise((resolve, reject) => {
var worker = new Worker(this.WORKER_URL);
// Protect against the worker failing for whatever reason.
var workerTimeout = setTimeout(function() {
worker.terminate();
reject('Failed to compress: gzip worker did not respond.');
}, 10000);
worker.onmessage = function(msg) {
clearTimeout(workerTimeout);
worker.terminate();
var data = msg.data;
if (data.returnResult) {
resolve(data.gzipData);
}
else {
reject('Failed to compress: ' + data.error);
}
};
worker.postMessage({
cmd: 'gzip',
payload: payload
});
});
}
};

View file

@ -0,0 +1,36 @@
/* jshint ignore: start */
/*
* Original source: https://github.com/imaya/zlib.js
*/
(function() {'use strict';var p=void 0,v=!0,da=this;function fa(f,e){var c=f.split("."),a=da;!(c[0]in a)&&a.execScript&&a.execScript("var "+c[0]);for(var b;c.length&&(b=c.shift());)!c.length&&e!==p?a[b]=e:a=a[b]?a[b]:a[b]={}};var C="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function F(f,e){this.index="number"===typeof e?e:0;this.f=0;this.buffer=f instanceof(C?Uint8Array:Array)?f:new (C?Uint8Array:Array)(32768);if(2*this.buffer.length<=this.index)throw Error("invalid index");this.buffer.length<=this.index&&ga(this)}function ga(f){var e=f.buffer,c,a=e.length,b=new (C?Uint8Array:Array)(a<<1);if(C)b.set(e);else for(c=0;c<a;++c)b[c]=e[c];return f.buffer=b}
F.prototype.b=function(f,e,c){var a=this.buffer,b=this.index,g=this.f,l=a[b],m;c&&1<e&&(f=8<e?(H[f&255]<<24|H[f>>>8&255]<<16|H[f>>>16&255]<<8|H[f>>>24&255])>>32-e:H[f]>>8-e);if(8>e+g)l=l<<e|f,g+=e;else for(m=0;m<e;++m)l=l<<1|f>>e-m-1&1,8===++g&&(g=0,a[b++]=H[l],l=0,b===a.length&&(a=ga(this)));a[b]=l;this.buffer=a;this.f=g;this.index=b};F.prototype.finish=function(){var f=this.buffer,e=this.index,c;0<this.f&&(f[e]<<=8-this.f,f[e]=H[f[e]],e++);C?c=f.subarray(0,e):(f.length=e,c=f);return c};
var ia=new (C?Uint8Array:Array)(256),M;for(M=0;256>M;++M){for(var N=M,S=N,ja=7,N=N>>>1;N;N>>>=1)S<<=1,S|=N&1,--ja;ia[M]=(S<<ja&255)>>>0}var H=ia;function ka(f,e,c){var a,b="number"===typeof e?e:e=0,g="number"===typeof c?c:f.length;a=-1;for(b=g&7;b--;++e)a=a>>>8^T[(a^f[e])&255];for(b=g>>3;b--;e+=8)a=a>>>8^T[(a^f[e])&255],a=a>>>8^T[(a^f[e+1])&255],a=a>>>8^T[(a^f[e+2])&255],a=a>>>8^T[(a^f[e+3])&255],a=a>>>8^T[(a^f[e+4])&255],a=a>>>8^T[(a^f[e+5])&255],a=a>>>8^T[(a^f[e+6])&255],a=a>>>8^T[(a^f[e+7])&255];return(a^4294967295)>>>0}
var la=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,
2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,
2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,
2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,
3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,
936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],T=C?new Uint32Array(la):la;function U(f){this.buffer=new (C?Uint16Array:Array)(2*f);this.length=0}U.prototype.getParent=function(f){return 2*((f-2)/4|0)};U.prototype.push=function(f,e){var c,a,b=this.buffer,g;c=this.length;b[this.length++]=e;for(b[this.length++]=f;0<c;)if(a=this.getParent(c),b[c]>b[a])g=b[c],b[c]=b[a],b[a]=g,g=b[c+1],b[c+1]=b[a+1],b[a+1]=g,c=a;else break;return this.length};
U.prototype.pop=function(){var f,e,c=this.buffer,a,b,g;e=c[0];f=c[1];this.length-=2;c[0]=c[this.length];c[1]=c[this.length+1];for(g=0;;){b=2*g+2;if(b>=this.length)break;b+2<this.length&&c[b+2]>c[b]&&(b+=2);if(c[b]>c[g])a=c[g],c[g]=c[b],c[b]=a,a=c[g+1],c[g+1]=c[b+1],c[b+1]=a;else break;g=b}return{index:f,value:e,length:this.length}};function ma(f,e){this.h=na;this.j=0;this.input=C&&f instanceof Array?new Uint8Array(f):f;this.c=0;e&&(e.lazy&&(this.j=e.lazy),"number"===typeof e.compressionType&&(this.h=e.compressionType),e.outputBuffer&&(this.a=C&&e.outputBuffer instanceof Array?new Uint8Array(e.outputBuffer):e.outputBuffer),"number"===typeof e.outputIndex&&(this.c=e.outputIndex));this.a||(this.a=new (C?Uint8Array:Array)(32768))}var na=2,V=[],$;
for($=0;288>$;$++)switch(v){case 143>=$:V.push([$+48,8]);break;case 255>=$:V.push([$-144+400,9]);break;case 279>=$:V.push([$-256+0,7]);break;case 287>=$:V.push([$-280+192,8]);break;default:throw"invalid literal: "+$;}
ma.prototype.g=function(){var f,e,c,a,b=this.input;switch(this.h){case 0:c=0;for(a=b.length;c<a;){e=C?b.subarray(c,c+65535):b.slice(c,c+65535);c+=e.length;var g=e,l=c===a,m=p,d=p,h=p,s=p,x=p,n=this.a,k=this.c;if(C){for(n=new Uint8Array(this.a.buffer);n.length<=k+g.length+5;)n=new Uint8Array(n.length<<1);n.set(this.a)}m=l?1:0;n[k++]=m|0;d=g.length;h=~d+65536&65535;n[k++]=d&255;n[k++]=d>>>8&255;n[k++]=h&255;n[k++]=h>>>8&255;if(C)n.set(g,k),k+=g.length,n=n.subarray(0,k);else{s=0;for(x=g.length;s<x;++s)n[k++]=
g[s];n.length=k}this.c=k;this.a=n}break;case 1:var q=new F(C?new Uint8Array(this.a.buffer):this.a,this.c);q.b(1,1,v);q.b(1,2,v);var u=oa(this,b),w,aa,z;w=0;for(aa=u.length;w<aa;w++)if(z=u[w],F.prototype.b.apply(q,V[z]),256<z)q.b(u[++w],u[++w],v),q.b(u[++w],5),q.b(u[++w],u[++w],v);else if(256===z)break;this.a=q.finish();this.c=this.a.length;break;case na:var B=new F(C?new Uint8Array(this.a.buffer):this.a,this.c),ra,L,O,P,Q,Ha=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],W,sa,X,ta,ba,ea=Array(19),
ua,R,ca,y,va;ra=na;B.b(1,1,v);B.b(ra,2,v);L=oa(this,b);W=pa(this.n,15);sa=qa(W);X=pa(this.m,7);ta=qa(X);for(O=286;257<O&&0===W[O-1];O--);for(P=30;1<P&&0===X[P-1];P--);var wa=O,xa=P,G=new (C?Uint32Array:Array)(wa+xa),r,I,t,Y,E=new (C?Uint32Array:Array)(316),D,A,J=new (C?Uint8Array:Array)(19);for(r=I=0;r<wa;r++)G[I++]=W[r];for(r=0;r<xa;r++)G[I++]=X[r];if(!C){r=0;for(Y=J.length;r<Y;++r)J[r]=0}r=D=0;for(Y=G.length;r<Y;r+=I){for(I=1;r+I<Y&&G[r+I]===G[r];++I);t=I;if(0===G[r])if(3>t)for(;0<t--;)E[D++]=0,
J[0]++;else for(;0<t;)A=138>t?t:138,A>t-3&&A<t&&(A=t-3),10>=A?(E[D++]=17,E[D++]=A-3,J[17]++):(E[D++]=18,E[D++]=A-11,J[18]++),t-=A;else if(E[D++]=G[r],J[G[r]]++,t--,3>t)for(;0<t--;)E[D++]=G[r],J[G[r]]++;else for(;0<t;)A=6>t?t:6,A>t-3&&A<t&&(A=t-3),E[D++]=16,E[D++]=A-3,J[16]++,t-=A}f=C?E.subarray(0,D):E.slice(0,D);ba=pa(J,7);for(y=0;19>y;y++)ea[y]=ba[Ha[y]];for(Q=19;4<Q&&0===ea[Q-1];Q--);ua=qa(ba);B.b(O-257,5,v);B.b(P-1,5,v);B.b(Q-4,4,v);for(y=0;y<Q;y++)B.b(ea[y],3,v);y=0;for(va=f.length;y<va;y++)if(R=
f[y],B.b(ua[R],ba[R],v),16<=R){y++;switch(R){case 16:ca=2;break;case 17:ca=3;break;case 18:ca=7;break;default:throw"invalid code: "+R;}B.b(f[y],ca,v)}var ya=[sa,W],za=[ta,X],K,Aa,Z,ha,Ba,Ca,Da,Ea;Ba=ya[0];Ca=ya[1];Da=za[0];Ea=za[1];K=0;for(Aa=L.length;K<Aa;++K)if(Z=L[K],B.b(Ba[Z],Ca[Z],v),256<Z)B.b(L[++K],L[++K],v),ha=L[++K],B.b(Da[ha],Ea[ha],v),B.b(L[++K],L[++K],v);else if(256===Z)break;this.a=B.finish();this.c=this.a.length;break;default:throw"invalid compression type";}return this.a};
function Fa(f,e){this.length=f;this.k=e}
var Ga=function(){function f(b){switch(v){case 3===b:return[257,b-3,0];case 4===b:return[258,b-4,0];case 5===b:return[259,b-5,0];case 6===b:return[260,b-6,0];case 7===b:return[261,b-7,0];case 8===b:return[262,b-8,0];case 9===b:return[263,b-9,0];case 10===b:return[264,b-10,0];case 12>=b:return[265,b-11,1];case 14>=b:return[266,b-13,1];case 16>=b:return[267,b-15,1];case 18>=b:return[268,b-17,1];case 22>=b:return[269,b-19,2];case 26>=b:return[270,b-23,2];case 30>=b:return[271,b-27,2];case 34>=b:return[272,
b-31,2];case 42>=b:return[273,b-35,3];case 50>=b:return[274,b-43,3];case 58>=b:return[275,b-51,3];case 66>=b:return[276,b-59,3];case 82>=b:return[277,b-67,4];case 98>=b:return[278,b-83,4];case 114>=b:return[279,b-99,4];case 130>=b:return[280,b-115,4];case 162>=b:return[281,b-131,5];case 194>=b:return[282,b-163,5];case 226>=b:return[283,b-195,5];case 257>=b:return[284,b-227,5];case 258===b:return[285,b-258,0];default:throw"invalid length: "+b;}}var e=[],c,a;for(c=3;258>=c;c++)a=f(c),e[c]=a[2]<<24|
a[1]<<16|a[0];return e}(),Ia=C?new Uint32Array(Ga):Ga;
function oa(f,e){function c(b,e){var a=b.k,c=[],g=0,f;f=Ia[b.length];c[g++]=f&65535;c[g++]=f>>16&255;c[g++]=f>>24;var d;switch(v){case 1===a:d=[0,a-1,0];break;case 2===a:d=[1,a-2,0];break;case 3===a:d=[2,a-3,0];break;case 4===a:d=[3,a-4,0];break;case 6>=a:d=[4,a-5,1];break;case 8>=a:d=[5,a-7,1];break;case 12>=a:d=[6,a-9,2];break;case 16>=a:d=[7,a-13,2];break;case 24>=a:d=[8,a-17,3];break;case 32>=a:d=[9,a-25,3];break;case 48>=a:d=[10,a-33,4];break;case 64>=a:d=[11,a-49,4];break;case 96>=a:d=[12,a-
65,5];break;case 128>=a:d=[13,a-97,5];break;case 192>=a:d=[14,a-129,6];break;case 256>=a:d=[15,a-193,6];break;case 384>=a:d=[16,a-257,7];break;case 512>=a:d=[17,a-385,7];break;case 768>=a:d=[18,a-513,8];break;case 1024>=a:d=[19,a-769,8];break;case 1536>=a:d=[20,a-1025,9];break;case 2048>=a:d=[21,a-1537,9];break;case 3072>=a:d=[22,a-2049,10];break;case 4096>=a:d=[23,a-3073,10];break;case 6144>=a:d=[24,a-4097,11];break;case 8192>=a:d=[25,a-6145,11];break;case 12288>=a:d=[26,a-8193,12];break;case 16384>=
a:d=[27,a-12289,12];break;case 24576>=a:d=[28,a-16385,13];break;case 32768>=a:d=[29,a-24577,13];break;default:throw"invalid distance";}f=d;c[g++]=f[0];c[g++]=f[1];c[g++]=f[2];var h,l;h=0;for(l=c.length;h<l;++h)n[k++]=c[h];u[c[0]]++;w[c[3]]++;q=b.length+e-1;x=null}var a,b,g,l,m,d={},h,s,x,n=C?new Uint16Array(2*e.length):[],k=0,q=0,u=new (C?Uint32Array:Array)(286),w=new (C?Uint32Array:Array)(30),aa=f.j,z;if(!C){for(g=0;285>=g;)u[g++]=0;for(g=0;29>=g;)w[g++]=0}u[256]=1;a=0;for(b=e.length;a<b;++a){g=
m=0;for(l=3;g<l&&a+g!==b;++g)m=m<<8|e[a+g];d[m]===p&&(d[m]=[]);h=d[m];if(!(0<q--)){for(;0<h.length&&32768<a-h[0];)h.shift();if(a+3>=b){x&&c(x,-1);g=0;for(l=b-a;g<l;++g)z=e[a+g],n[k++]=z,++u[z];break}0<h.length?(s=Ja(e,a,h),x?x.length<s.length?(z=e[a-1],n[k++]=z,++u[z],c(s,0)):c(x,-1):s.length<aa?x=s:c(s,0)):x?c(x,-1):(z=e[a],n[k++]=z,++u[z])}h.push(a)}n[k++]=256;u[256]++;f.n=u;f.m=w;return C?n.subarray(0,k):n}
function Ja(f,e,c){var a,b,g=0,l,m,d,h,s=f.length;m=0;h=c.length;a:for(;m<h;m++){a=c[h-m-1];l=3;if(3<g){for(d=g;3<d;d--)if(f[a+d-1]!==f[e+d-1])continue a;l=g}for(;258>l&&e+l<s&&f[a+l]===f[e+l];)++l;l>g&&(b=a,g=l);if(258===l)break}return new Fa(g,e-b)}
function pa(f,e){var c=f.length,a=new U(572),b=new (C?Uint8Array:Array)(c),g,l,m,d,h;if(!C)for(d=0;d<c;d++)b[d]=0;for(d=0;d<c;++d)0<f[d]&&a.push(d,f[d]);g=Array(a.length/2);l=new (C?Uint32Array:Array)(a.length/2);if(1===g.length)return b[a.pop().index]=1,b;d=0;for(h=a.length/2;d<h;++d)g[d]=a.pop(),l[d]=g[d].value;m=Ka(l,l.length,e);d=0;for(h=g.length;d<h;++d)b[g[d].index]=m[d];return b}
function Ka(f,e,c){function a(b){var c=d[b][h[b]];c===e?(a(b+1),a(b+1)):--l[c];++h[b]}var b=new (C?Uint16Array:Array)(c),g=new (C?Uint8Array:Array)(c),l=new (C?Uint8Array:Array)(e),m=Array(c),d=Array(c),h=Array(c),s=(1<<c)-e,x=1<<c-1,n,k,q,u,w;b[c-1]=e;for(k=0;k<c;++k)s<x?g[k]=0:(g[k]=1,s-=x),s<<=1,b[c-2-k]=(b[c-1-k]/2|0)+e;b[0]=g[0];m[0]=Array(b[0]);d[0]=Array(b[0]);for(k=1;k<c;++k)b[k]>2*b[k-1]+g[k]&&(b[k]=2*b[k-1]+g[k]),m[k]=Array(b[k]),d[k]=Array(b[k]);for(n=0;n<e;++n)l[n]=c;for(q=0;q<b[c-1];++q)m[c-
1][q]=f[q],d[c-1][q]=q;for(n=0;n<c;++n)h[n]=0;1===g[c-1]&&(--l[0],++h[c-1]);for(k=c-2;0<=k;--k){u=n=0;w=h[k+1];for(q=0;q<b[k];q++)u=m[k+1][w]+m[k+1][w+1],u>f[n]?(m[k][q]=u,d[k][q]=e,w+=2):(m[k][q]=f[n],d[k][q]=n,++n);h[k]=0;1===g[k]&&a(k)}return l}
function qa(f){var e=new (C?Uint16Array:Array)(f.length),c=[],a=[],b=0,g,l,m,d;g=0;for(l=f.length;g<l;g++)c[f[g]]=(c[f[g]]|0)+1;g=1;for(l=16;g<=l;g++)a[g]=b,b+=c[g]|0,b<<=1;g=0;for(l=f.length;g<l;g++){b=a[f[g]];a[f[g]]+=1;m=e[g]=0;for(d=f[g];m<d;m++)e[g]=e[g]<<1|b&1,b>>>=1}return e};function La(f,e){this.input=f;this.c=this.i=0;this.d={};e&&(e.flags&&(this.d=e.flags),"string"===typeof e.filename&&(this.filename=e.filename),"string"===typeof e.comment&&(this.l=e.comment),e.deflateOptions&&(this.e=e.deflateOptions));this.e||(this.e={})}
La.prototype.g=function(){var f,e,c,a,b,g,l,m,d=new (C?Uint8Array:Array)(32768),h=0,s=this.input,x=this.i,n=this.filename,k=this.l;d[h++]=31;d[h++]=139;d[h++]=8;f=0;this.d.fname&&(f|=Ma);this.d.fcomment&&(f|=Na);this.d.fhcrc&&(f|=Oa);d[h++]=f;e=(Date.now?Date.now():+new Date)/1E3|0;d[h++]=e&255;d[h++]=e>>>8&255;d[h++]=e>>>16&255;d[h++]=e>>>24&255;d[h++]=0;d[h++]=Pa;if(this.d.fname!==p){l=0;for(m=n.length;l<m;++l)g=n.charCodeAt(l),255<g&&(d[h++]=g>>>8&255),d[h++]=g&255;d[h++]=0}if(this.d.comment){l=
0;for(m=k.length;l<m;++l)g=k.charCodeAt(l),255<g&&(d[h++]=g>>>8&255),d[h++]=g&255;d[h++]=0}this.d.fhcrc&&(c=ka(d,0,h)&65535,d[h++]=c&255,d[h++]=c>>>8&255);this.e.outputBuffer=d;this.e.outputIndex=h;b=new ma(s,this.e);d=b.g();h=b.c;C&&(h+8>d.buffer.byteLength?(this.a=new Uint8Array(h+8),this.a.set(new Uint8Array(d.buffer)),d=this.a):d=new Uint8Array(d.buffer));a=ka(s,p,p);d[h++]=a&255;d[h++]=a>>>8&255;d[h++]=a>>>16&255;d[h++]=a>>>24&255;m=s.length;d[h++]=m&255;d[h++]=m>>>8&255;d[h++]=m>>>16&255;d[h++]=
m>>>24&255;this.i=x;C&&h<d.length&&(this.a=d=d.subarray(0,h));return d};var Pa=255,Oa=2,Ma=8,Na=16;fa("Zlib.Gzip",La);fa("Zlib.Gzip.prototype.compress",La.prototype.g);}).call(this);

View file

@ -0,0 +1,27 @@
/* global Zlib */
'use strict';
importScripts('gzip.min.js');
self.onmessage = function(message) {
var cmd = message.data.cmd;
if (cmd === 'gzip') {
// gzip function requires Uint8Array
var textEncoder = new TextEncoder('utf8');
var payload = message.data.payload;
var uint8Data = textEncoder.encode(payload);
try {
var gzip = new Zlib.Gzip(uint8Data);
var compressed = gzip.compress();
postMessage({returnResult: true, gzipData: compressed});
}
catch(e) {
postMessage({returnResult: false, error: e});
}
} else {
console.warn('GzipWorker, unsupported cmd:', cmd);
}
};

View file

@ -0,0 +1,147 @@
/* global LazyLoader */
(function(exports) {
'use strict';
var resources = ['/shared/js/component_utils.js',
'/shared/elements/gaia_buttons/script.js',
'/shared/elements/gaia_confirm/script.js',
'/shared/elements/gaia_menu/script.js'];
/**
Generic dialog helper this _depends_ on <gaia-confirm> but is not required
to be loaded as part of the main gaia-confirm script.
*/
function ConfirmDialogHelper(config) {
this.config = config;
}
ConfirmDialogHelper.prototype = {
show: function(parent) {
LazyLoader.load(resources, this._show.bind(this, parent));
},
_show: function(parent) {
var config = this.config;
var wrapper = document.createElement('div');
// only include a cancel button if the config was given...
var cancelButton = config.cancel ?
'<button class="cancel" type="button"></button>' :
'';
wrapper.innerHTML =
'<gaia-confirm>' +
'<h1></h1>' +
'<p></p>' +
'<gaia-buttons skin="dark">' +
cancelButton +
'<button class="confirm" type="button"></button>' +
'</gaia-buttons>' +
'</gaia-confirm>';
var element = wrapper.firstElementChild;
this.element = element;
element.dataset.type = config.type;
element.addEventListener('confirm', this);
element.addEventListener('cancel', this);
// XXX: Primarily here for pressing the home screen button.
// The home button triggers a hashchange of the homescreen.
window.addEventListener('hashchange', this);
// TODO: Add visibility change handling...
var title = element.querySelector('h1');
var body = element.querySelector('p');
var cancel = element.querySelector('.cancel');
var confirm = element.querySelector('.confirm');
var setL10nAttributes = function (element, options){
if ('string' === typeof options) {
navigator.mozL10n.setAttributes(element, options);
}
if(options.id) {
navigator.mozL10n.setAttributes(element, options.id, options.args);
}
};
setL10nAttributes(title, config.title);
setL10nAttributes(body, config.body);
setL10nAttributes(confirm, config.confirm.title);
if (config.cancel) {
setL10nAttributes(cancel, config.cancel.title);
}
if (config.confirm.type) {
confirm.classList.add(config.confirm.type);
}
document.activeElement.blur();
element.setAttribute('hidden', '');
parent.appendChild(element);
// This nested requestAnimationFrame is to work around the coalescing
// of the style changes associated with removing of the 'hidden'
// attribute with the creation of the element.
// For whatever reason, flushing the style with the usual trick of
// accessing clientTop doesn't work, and a setTimeout requires an
// unreasonably lengthy timeout (>50ms) to work, and that may not be
// a reliable solution.
// This work-around, though gross, appears to work consistently without
// introducing too much lag or extra work.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
element.removeAttribute('hidden');
window.dispatchEvent(new CustomEvent('gaia-confirm-open'));
});
});
},
destroy: function() {
if (!this.element) {
return;
}
// Ensure cleanup of our hacks!
window.removeEventListener('hashchange', this);
this.element.addEventListener('transitionend',
function removeAfterHide(e) {
if (e.target !== this.element) {
return;
}
this.element.parentNode.removeChild(this.element);
this.element = null;
window.dispatchEvent(new CustomEvent('gaia-confirm-close'));
}.bind(this));
this.element.setAttribute('hidden', '');
},
handleEvent: function(e) {
// Ensure we hide the dialog in the face of other errors.
this.destroy();
switch (e.type) {
case 'hashchange':
// Hashchange is only here to trigger this to call destroy.
break;
case 'confirm':
var confirm = this.config.confirm.cb;
confirm && confirm();
break;
case 'cancel':
var cancel = this.config.cancel.cb;
cancel && cancel();
break;
}
}
};
// This name is intentionally verbose.
window.ConfirmDialogHelper = ConfirmDialogHelper;
}(window));

View file

@ -0,0 +1,5 @@
;'use strict';(function(exports){const FETCH_XHR_TIMEOUT=10000;function fetchBlob(uri){return new Promise(function(accept,reject){var xhr=new XMLHttpRequest({mozAnon:true,mozSystem:true});xhr.open('GET',uri,true);xhr.responseType='blob';xhr.timeout=FETCH_XHR_TIMEOUT;xhr.send();xhr.onload=function(){var status=xhr.status;if(status!==0&&status!==200){reject(new Error('Got HTTP status '+status+' trying to load '+uri));return;}
accept(xhr.response);};xhr.onerror=xhr.ontimeout=function(){reject(new Error('Error while HTTP GET: ',uri));};});}
function Icon(elem,uri){this.elem=elem;this.uri=uri;}
Icon.prototype={get size(){return this._size||40;},set size(size){this._size=size||this.size;var style=this.elem.style;var sizeInRems=(this._size/10)+'rem';style.backgroundSize=style.width=style.height=sizeInRems;},render:function render(options){options=options||{};var uri=this.uri;this.size=options.size;if(!uri){return;}
fetchBlob(uri).then(function(blob){this.renderBlob(blob,options);}.bind(this));},renderBlob:function renderBlob(blob,options){options=options||{};var style=this.elem.style;this.size=options.size;var img=new Image();img.src=URL.createObjectURL(blob);var size=this.size;img.onload=()=>{var renderer=new GridIconRenderer({grid:{layout:{get gridIconSize(){return size;},get gridMaxIconSize(){return size*devicePixelRatio;}}}});var type=options.type||GridIconRenderer.TYPE.FAVICON;renderer[type](img).then((blob)=>{var url=URL.createObjectURL(blob);var imageUrl='url('+url+')';style.backgroundImage=imageUrl;if(options.onLoad){options.onLoad(blob);}});URL.revokeObjectURL(img.src);};img.onerror=()=>{if(options.onerror){options.onerror();}};}};exports.Icon=Icon;}(window));

View file

@ -0,0 +1,49 @@
'use strict';
var IACHandler = {
_eventPrefix: 'iac-',
_ports: {},
init: function onInit() {
var self = this;
window.navigator.mozSetMessageHandler('connection',
function onConnected(request) {
var keyword = request.keyword;
var port = request.port;
// We will save each port by keyword; ports may get overwritten
// sometimes, but that's ok because there should only be one *active*
// port with a given keyword at any time.
self._ports[keyword] = port;
port.onmessage = function onReceivedMessage(evt) {
var message = evt.data;
/*
* System will emit a event when received message
* e.g. iac-ftucomms
* iac-[keywords]
*/
var evtName = self._eventPrefix + keyword;
var iacEvt = document.createEvent('CustomEvent');
/*
* You can get the message by accessing
* `event.detail`
*
* It will store the whole message you passed from postMessage
*/
iacEvt.initCustomEvent(evtName,
/* canBubble: */ true, /* cancelable */ false, message);
window.dispatchEvent(iacEvt);
};
});
},
getPort: function(keyword) {
return this._ports[keyword];
}
};
IACHandler.init();

View file

@ -0,0 +1,186 @@
'use strict';
/**
* IccHelper redirect calls to ICC related APIs to correct object. IccHelper is
* created for backward compatibility of gaia master after the patch of
* bug 814637 landed in m-c. In the patch the interface provided by IccManager
* was moved to a MozIcc object. Details please refer to the URL[1].
* The helper *MUST* be removed once all gaia developement no longer depend
* on b2g26.
* [1]: https://wiki.mozilla.org/WebAPI/WebIccManager/Multi-SIM
*/
(function(exports) {
var _iccManager = navigator.mozIccManager;
var _iccProxy = null;
Object.defineProperty(exports, 'IccHelper', {
configurable: true,
get: function() {
return _iccProxy;
},
// This is for code that sets value to IccHelper. And for unit tests that
// mock IccHelper.
set: function(value) {
_iccProxy = value;
}
});
if (_iccManager && _iccManager.getIccById) {
var activeIccObj = null;
var eventListeners = {};
var cachedEventListeners = {}; // cache on + 'eventName' event handlers
var events = ['cardstatechange', 'iccinfochange',
'stkcommand', 'stksessionend'];
var getters = ['iccInfo', 'cardState'];
var methods = ['sendStkResponse', 'sendStkMenuSelection',
'sendStkTimerExpiration', 'sendStkEventDownload'];
var domRequests = ['getCardLock', 'unlockCardLock', 'setCardLock',
'getCardLockRetryCount', 'readContacts',
'updateContact', 'iccOpenChannel', 'iccExchangeAPDU',
'iccCloseChannel'];
var getterTemplate = function(name) {
return function() {
if (activeIccObj) {
return activeIccObj[name];
} else {
return null;
}
};
};
var methodTemplate = function(name) {
return function() {
if (activeIccObj) {
return activeIccObj[name].apply(activeIccObj, arguments);
}
};
};
// Multi-SIM API available
var createIccProxy = function() {
var iccProxy = {
addEventListener: function(eventName, callback) {
if (typeof callback !== 'function') {
return;
}
if (events.indexOf(eventName) === -1) {
return;
}
var listeners = eventListeners[eventName];
if (listeners == null) {
listeners = eventListeners[eventName] = [];
}
if (listeners.indexOf(callback) === -1) {
listeners.push(callback);
}
},
removeEventListener: function(eventName, callback) {
var listeners = eventListeners[eventName];
if (listeners) {
var index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
}
}
};
getters.forEach(function(getter) {
Object.defineProperty(iccProxy, getter, {
enumerable: true,
get: getterTemplate(getter)
});
});
methods.forEach(function(method) {
iccProxy[method] = methodTemplate(method);
});
domRequests.forEach(function(request) {
iccProxy[request] = methodTemplate(request);
});
events.forEach(function(eventName) {
// Define 'on' + eventName properties
// Assigning an event handler and adding an event listener are
// handled separately. We need to simulate the same behavior in
// IccHelper.
Object.defineProperty(iccProxy, 'on' + eventName, {
enumerable: true,
set: function(newListener) {
var oldListener = cachedEventListeners[eventName];
if (oldListener) {
iccProxy.removeEventListener(eventName, oldListener);
}
cachedEventListeners[eventName] = newListener;
if (newListener) {
iccProxy.addEventListener(eventName, newListener);
}
},
get: function() {
return cachedEventListeners[eventName];
}
});
});
return iccProxy;
};
// Initialize icc proxy
_iccProxy = createIccProxy();
if (_iccManager.iccIds && _iccManager.iccIds.length) {
activeIccObj = _iccManager.getIccById(_iccManager.iccIds[0]);
if (activeIccObj) {
// register to callback
events.forEach(function(eventName) {
activeIccObj.addEventListener(eventName, function(event) {
var listeners = eventListeners[eventName];
if (listeners) {
listeners.forEach(function(listener) {
listener(event);
});
}
});
});
}
}
_iccManager.oniccdetected = function(event) {
if (_iccProxy.cardState == null) {
activeIccObj = _iccManager.getIccById(event.iccId);
if (activeIccObj) {
// register to callback
events.forEach(function(eventName) {
activeIccObj.addEventListener(eventName, function(event) {
var listeners = eventListeners[eventName];
if (listeners) {
listeners.forEach(function(listener) {
listener(event);
});
}
});
});
// trigger cardstatechange and iccinfochange manually when a mozIcc
// object becomes valid (detected).
['cardstatechange', 'iccinfochange'].forEach(function(eventName) {
if (eventListeners[eventName]) {
eventListeners[eventName].forEach(function(listener) {
listener();
});
}
});
}
}
};
} else {
_iccProxy = _iccManager;
}
})(window);

View file

@ -0,0 +1,441 @@
/* globals Icon */
/* exported IconsHelper */
'use strict';
/**
* Utility library that will help us to work with icons coming from
* different sources.
*/
(function IconsHelper(exports) {
const ICON_CACHE_PERIOD = 24 * 60 * 60 * 1000; // 1 day
const FETCH_XHR_TIMEOUT = 10000;
const DEBUG = false;
var dataStore = null;
/**
* Return default size in px based in devicePixelRatio
*
* Sized based on current homescreen selected icons for apps
* in a configuration of 3 icons per row. See:
* https://github.com/mozilla-b2g/gaia/blob/master/
* shared/elements/gaia_grid/js/grid_layout.js#L15
* @returns {Number}
*/
function getDefaultIconSize() {
var dpr = window.devicePixelRatio;
return (dpr && dpr > 1) ? 142 : 84;
}
function sizeIsNearer(size1, size2, targetSize) {
// TODO: weight for a larger vs. smaller icon?
var delta1 = Math.abs(targetSize - size1);
var delta2 = Math.abs(targetSize - size2);
return (delta1 <= delta2);
}
/**
* Return a promise that resolves to the URL of the best icon for a web page
* given its meta data and web manifest.
*
* @param uri {string}
* @param iconTargetSize {number}
* @param placeObj {Object}
* @param siteObj {Object}
* @returns {Promise}
*/
function getIcon(uri, iconTargetSize, placeObj = {}, siteObj = {}) {
var iconUrl = null;
iconTargetSize = iconTargetSize * window.devicePixelRatio;
// First look for an icon in the Webmanifest.
if (siteObj.webManifestUrl && siteObj.webManifest) {
iconUrl = getBestIconFromWebManifest(siteObj.webManifest, iconTargetSize);
if (DEBUG && iconUrl) {
console.log('Icon from Web Manifest');
}
}
// Then look for an icon in the Firefox manifest.
if (!iconUrl && siteObj.manifest && siteObj.manifest.icons) {
iconUrl = getBestIconFromWebManifest({
icons: _convertToWebManifestIcons(siteObj.manifest,
siteObj.origin || siteObj.manifest.origin)
}, iconTargetSize);
if (DEBUG && iconUrl) {
console.log('Icon from Firefox App Manifest');
}
}
// Otherwise, look into the meta tags.
if (!iconUrl && placeObj && placeObj.icons) {
iconUrl = getBestIconFromMetaTags(placeObj.icons, iconTargetSize);
if (DEBUG && iconUrl) {
console.log('Icon from Meta tags');
}
}
// Last resort, we look for a favicon.ico file.
if (!iconUrl) {
var a = document.createElement('a');
a.href = uri;
iconUrl = a.origin + '/favicon.ico';
if (iconTargetSize) {
iconUrl += '#-moz-resolution=' + iconTargetSize + ',' + iconTargetSize;
}
DEBUG && console.log('Icon from favicon.ico');
}
return new Promise(resolve => {
resolve(iconUrl);
});
}
/**
* Same as above except the promise resolves as an object containing the blob
* of the icon and its size in pixels.
*
* @param uri {string}
* @param iconTargetSize {number}
* @param placeObj {Object}
* @param siteObj {Object}
* @returns {Promise}
*/
function getIconBlob(uri, iconTargetSize, placeObj = {}, siteObj = {}) {
return new Promise((resolve, reject) => {
getIcon(uri, iconTargetSize, placeObj, siteObj)
.then(iconUrl => {
// @todo Need a better syntax.
getStore().then(iconStore => {
iconStore.get(iconUrl).then(iconObj => {
if (!iconObj || !iconObj.timestamp ||
Date.now() - iconObj.timestamp >= ICON_CACHE_PERIOD) {
return fetchIconBlob(iconUrl)
.then(iconBlob => {
var img = document.createElement('img');
var icon = new Icon(img, uri);
icon.renderBlob(iconBlob, {
size: iconTargetSize,
onLoad: function(blob) {
var iconObj = {
blob: blob,
originalUrl: iconUrl.toString(),
timestamp: Date.now()
};
// We resolve here to avoid I/O blocking on
// dataStore and quicker display.
// Persisting to the dataStore takes place subsequently.
resolve(iconObj);
iconStore.add(iconObj, iconUrl);
},
onerror: function(e) {
reject(`Failed to fetch icon ${iconUrl}`);
}
});
})
.catch(err => {
reject(`Failed to fetch icon ${iconUrl}: ${err}`);
});
}
return resolve(iconObj);
}).catch(err => {
// We should fetch the icon and resolve the promise here, anyhow.
reject(`Failed to get icon from dataStore: ${err}`);
});
}).catch(err => {
// We should fetch the icon and resolve the promise here, anyhow.
reject(`Error opening the dataStore: ${err}`);
});
});
});
}
/**
* Same as above but set the image as the icon property of a gaia-app-icon
* element.
*
* @param icon {Object?}
* @param targetSize {number}
* @returns {Promise}
*/
function setElementIcon(icon, targetSize) {
return getIconBlob(icon.bookmark.url, targetSize,
icon.bookmark, icon.bookmark)
.then(iconObj => {
if (iconObj.blob) {
icon.icon = iconObj.blob;
return Promise.resolve();
} else if (icon.bookmark.icon) {
// We fallback to the bookmark.icon property if no icons were found.
return fetchIconBlob(icon.bookmark.icon)
.then(iconBlob => {
icon.icon = iconBlob;
return Promise.resolve();
}, Promise.reject.bind(Promise)); // XXXbz Why second arg?
}
return Promise.reject('No icon data found');
}, Promise.reject.bind(Promise)); // XXXbz Why bother with second arg?
}
function getBestIconFromWebManifest(webManifest, iconSize) {
var icons = webManifest.icons;
if (!icons) {
return null;
}
var maxSize = 10000;
var bestSize = maxSize;
var iconURL = null;
iconSize = iconSize || getDefaultIconSize();
icons.forEach((potentialIcon) => {
if (!iconURL) {
iconURL = potentialIcon.src;
}
var sizes = Array.from(potentialIcon.sizes);
var nearestSize = getNearestSize(sizes, iconSize, bestSize);
if (nearestSize !== bestSize &&
sizeIsNearer(nearestSize, bestSize, iconSize)) {
iconURL = potentialIcon.src;
bestSize = nearestSize;
}
});
return iconURL ? iconURL : null;
}
function _convertToWebManifestIcons(manifest, origin) {
return Object.keys(manifest.icons).map(function(size) {
var url = manifest.icons[size];
var sizes = [size + 'x' + size];
url = url.indexOf('http') > -1 ? url : origin + url;
return {
src: new URL(url),
sizes: sizes
};
});
}
// See bug 1041482, we will need to support better
// icons for different part of the system application.
// A web page have different ways to defining icons
// based on size, 'touch' capabilities and so on.
// From gecko we will receive all the rel='icon'
// defined which will contain as well the sizes
// supported in that file.
// This function will help to deliver the best suitable
// icon based on that definition list.
// The expected format is the following one:
//
// {
// '[uri 1]': {
// sizes: ['16x16 32x32 48x48', '60x60']
// },
// '[uri 2]': {
// sizes: ['16x16']
// }
// }
//
// iconSize is an additional parameter to specify a concrete
// size or the closest icon.
function getBestIconFromMetaTags(icons, iconSize) {
if (!icons) {
return null;
}
iconSize = iconSize || getDefaultIconSize();
var iconURL = null;
var bestSize = 10000;
Object.keys(icons).forEach((uri) => {
var potentialIcon = icons[uri];
if (!iconURL) {
// Handle the case of no size info in the whole list
iconURL = uri;
}
var sizes = Array.from(potentialIcon.sizes);
var nearestSize = getNearestSize(sizes, iconSize, bestSize);
if (nearestSize !== bestSize &&
sizeIsNearer(nearestSize, bestSize, iconSize)) {
iconURL = uri;
bestSize = nearestSize;
}
if (potentialIcon.rel === 'apple-touch-icon' ||
potentialIcon.rel === 'apple-touch-icon-precomposed') {
var moreInfoUrl = 'https://developer.mozilla.org/en-US/' +
'Apps/Build/Icon_implementation_for_apps#General_icons_for_web_apps';
console.warn('Warning: The apple-touch icons are being used ' +
'as a fallback only. They will be deprecated in ' +
'the future. See ' + moreInfoUrl);
}
});
return iconURL || null;
}
// Given an array of size strings e.g. (64x64), a target iconSize
// and optionally a best-so-far-size,
// return the nearest match
function getNearestSize(sizes, iconSize, bestSize) {
var bogusSize = 10000;
if (!bestSize) {
bestSize = bogusSize;
}
var nearestSize = sizes.reduce(function(nearestSize, sizeString, idx) {
var size = widthFromSizeString(sizeString);
if (isNaN(size)) {
return nearestSize;
}
if (sizeIsNearer(size, nearestSize, iconSize)) {
return size;
} else {
return nearestSize;
}
}, bestSize);
return nearestSize === bogusSize ? -1 : nearestSize;
}
// Given an icon size by string YYxYY returns the
// width measurement, so will assume this will be
// used by strings that identify a square size.
function widthFromSizeString(size) {
size = size || '';
var xIndex = size.indexOf('x');
if (!xIndex) {
return NaN;
}
return parseInt(size.substr(0, xIndex));
}
/**
* Return a promise that resolves to a dataStore for icons.
*
* @returns {Promise}
*/
function getStore() {
return new Promise(resolve => {
if (dataStore) {
return resolve(dataStore);
}
navigator.getDataStores('icons').then(stores => {
dataStore = stores[0];
return resolve(dataStore);
});
});
}
/**
* Clear all the icons in the store.
*
* @returns {Promise}
*/
function clear() {
return getStore().then(iconStore => {
iconStore.clear();
});
}
/**
* Return a promise that resolves to an object containing the blob url
* in pixels of an icon given its URL `iconUrl`.
*
* @param {string} iconUrl
* @returns {Promise}
*/
function fetchIcon(iconUrl) {
return new Promise((resolve, reject) => {
fetchIconBlob(iconUrl)
.then((iconBlob) => {
var img = document.createElement('img');
img.src = URL.createObjectURL(iconBlob);
img.onload = () => {
var iconSize = Math.max(img.naturalWidth, img.naturalHeight);
resolve({
blob: iconBlob,
url: iconUrl,
size: iconSize,
timestamp: Date.now()
});
};
img.onerror = () => {
reject(new Error(`Error while loading image.`));
};
})
.catch((e) => {
reject(new Error(`Error while loading image: ${e}`));
});
});
}
/**
* Return a promise that resolves to an object containing the blob
* given its URL `iconUrl`.
*
* @param {string} iconUrl
* @returns {Promise}
*/
function fetchIconBlob(iconUrl) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest({
mozAnon: true,
mozSystem: true
});
xhr.open('GET', iconUrl, true);
xhr.responseType = 'blob';
xhr.timeout = FETCH_XHR_TIMEOUT;
// Remember that send() can throw for some non http protocols.
// The promise wrapper here protects us.
xhr.send();
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
var iconBlob = xhr.response;
resolve(iconBlob);
return;
}
reject(new Error(
`Got HTTP status ${xhr.status} trying to load ${iconUrl}.`));
};
xhr.onerror = xhr.ontimeout = () => {
reject(new Error(`Error while getting ${iconUrl}.`));
};
});
}
exports.IconsHelper = {
getIcon: getIcon,
getIconBlob: getIconBlob,
setElementIcon: setElementIcon,
getBestIconFromWebManifest: getBestIconFromWebManifest,
getBestIconFromMetaTags: getBestIconFromMetaTags,
fetchIcon: fetchIcon,
fetchIconBlob: fetchIconBlob,
get defaultIconSize() {
return getDefaultIconSize();
},
clear: clear,
// Make public for unit test purposes.
getNearestSize: getNearestSize,
};
})(window);

View file

@ -0,0 +1,121 @@
/*
This file implements window.setIdleTimeout() and
window.clearIdleTimeout(). They look like setTimeout() except the
setIdleTimeout() function takes two callbacks:
setIdleTimeout(idleCallback, activeCallback, ms):
idleCallback will fire after specific microsecond of idle.
The time it takes will calculated from the time
setIdleTimeout() is called and resets as user interacts.
activeCallback will fire when the first user action *after*
idleCallback fires
returns id.
clearIdleTimeout(id):
takes the id returns from setIdleTimeout() and cancels it.
*/
// Wrap everything into a closure so we will not expose idleTimerRegistry
(function idleTimerAsAIdleObserverWrapper(win) {
'use strict';
// stuff the 0th element so id is always a truey value
var idleTimerRegistry = [undefined];
// setIdleTimeout()
win.setIdleTimeout = function setIdleTimeout(idleCallback,
activeCallback,
ms) {
var idleFired = false;
var idleTimer = {};
var triggerIdleCallback = function() {
// remove the timer
idleTimer.clearTimer();
// set idleFired to true
idleFired = true;
// fire the real idleCallback
idleCallback();
};
idleTimer.timer = setTimeout(triggerIdleCallback, ms);
idleTimer.clearTimer = function clearIdleTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
// Create an idle observer with a very short time as
// we are not interested in when onidle fires (since it's inaccuate),
// instead, we need to know when onactive callback calls.
idleTimer.observer = {
onidle: function observerReportIdle() {
// Once the onidle fires, the next user action will trigger onactive.
// No need to reset the timer if it has been set in the init stage.
if (idleTimer.timer) {
return;
}
// The onidle callback will delay 1 second after user stops actions
// so reduce 1000ms here to counteract it.
var time = ms - 1000;
idleTimer.timer = setTimeout(triggerIdleCallback, time < 0 ? 0 : time);
},
onactive: function observerReportActive() {
// Remove the timer set by onidle
idleTimer.clearTimer();
// If idleCallback is not called yet,
// we should not trigger activeCallback here
if (!idleFired) {
return;
}
// fire the real activeCallback
activeCallback();
// reset the flag
idleFired = false;
// After a short time, onidle will fire again.
// timer will be registered there again.
},
time: 1
};
// Register the idleObserver
navigator.addIdleObserver(idleTimer.observer);
// Push the idleTimer object to the registry
idleTimerRegistry.push(idleTimer);
// return the id so people can do clearIdleTimeout();
return (idleTimerRegistry.length - 1);
};
// clearIdleTimeout()
win.clearIdleTimeout = function clearIdleTimeout(id) {
if (!idleTimerRegistry[id]) {
return;
}
// Get the idleTimer object and remove it from registry
var idleTimer = idleTimerRegistry[id];
idleTimerRegistry[id] = undefined;
// Properly clean it up, make sure we will never heard from
// those callbacks ever again.
navigator.removeIdleObserver(idleTimer.observer);
idleTimer.clearTimer();
};
})(this);

View file

@ -0,0 +1,572 @@
/*
* This shared module defines a single ImageUtils object in the global scope.
*
* ImageUtils.getSizeAndType() takes a Blob as its argument and
* returns a Promise. If the promise resolves, the resolve method is
* passed an object with width, height, and type properties. Width and
* height specify the width and height of the image in pixels. Type
* specifies the MIME type of the image. If the promise is rejected,
* the reject method will be passed a string error message.
*
* Supported image types are JPEG, PNG, GIF and BMP. The MIME type
* strings returned when a promise resolves are "image/jpeg",
* "image/png", "image/gif", and "image/bmp".
*
* If you pass a blob that holds an image of some other type (or a
* blob that is not an image at all) then promise will be rejected.
*
* getSizeAndType() does not decode the image to determine its size
* because can require large amounts of memory. Instead, it parses the
* image file to determine its size and typically only needs to read a
* prefix of the file into memory.
*
* getSizeAndType() differs from the getImageSize() function defined
* in shared/js/media/immage_size.js. That function is similar but
* also parses EXIF metadata for JPEG images and is only suitable if
* you need to access EXIF data.
*/
(function(exports) {
'use strict';
/* global Promise */
var ImageUtils = exports.ImageUtils = {};
// Define constants for the MIME types we use
const JPEG = 'image/jpeg';
const PNG = 'image/png';
const GIF = 'image/gif';
const BMP = 'image/bmp';
// Export them
ImageUtils.JPEG = JPEG;
ImageUtils.PNG = PNG;
ImageUtils.GIF = GIF;
ImageUtils.BMP = BMP;
ImageUtils.getSizeAndType = function getSizeAndType(imageBlob) {
if (!(imageBlob instanceof Blob)) {
console.log('not blob');
return Promise.reject(new TypeError('argument is not a Blob'));
}
return new Promise(function(resolve, reject) {
if (imageBlob.size <= 16) {
reject('corrupt image file');
return;
}
// Start off assuming that we'll read the first 32kb of the file
var bytesToRead = 32 * 1024;
// But if the blob indicates that this is a PNG, GIF, or BMP, then
// we can read less since the size information is always close to the
// start of the file for those formats.
if (imageBlob.type === PNG ||
imageBlob.type === GIF ||
imageBlob.type === BMP) {
bytesToRead = 512;
}
// Try to find the information we want in the first bytes of the file
findSizeAndType(imageBlob, bytesToRead, success, tryagain);
// If we got the size and type data, resolve the promise
function success(data) {
resolve(data);
}
// If we didn't find it, try again for JPEG files
function tryagain(data) {
// If we were able to verify that this blob is a jpeg file
// then we will try again because in JPEG files, the size
// comes after the EXIF data so if there is a large preview image,
// the size might be more than 32kb into the file
if (data.type === JPEG) {
findSizeAndType(imageBlob, imageBlob.size, success, failure);
}
// For all other known image types the type and size data is
// near the start of the file, if we didn't find it on this first
// try we need to give up now.
else {
reject(data.error);
}
}
// If we didn't find the data even after a second attempt then reject.
function failure(data) {
reject(data.error);
}
});
// This is the internal helper function that actually reads the blob
// and finds type and size information.
function findSizeAndType(imageBlob, amountToRead, success, failure) {
var slice = imageBlob.slice(0, Math.min(amountToRead, imageBlob.size));
var reader = new FileReader();
reader.readAsArrayBuffer(slice);
reader.onloadend = function() {
parseImageData(reader.result);
};
function parseImageData(buffer) {
var header = new Uint8Array(buffer, 0, 16);
var view = new DataView(buffer);
if (header[0] === 0x89 &&
header[1] === 0x50 && /* P */
header[2] === 0x4e && /* N */
header[3] === 0x47 && /* G */
header[4] === 0x0d && /* \r */
header[5] === 0x0a && /* \n */
header[6] === 0x1A &&
header[7] === 0x0a &&
header[12] === 0x49 && /* I */
header[13] === 0x48 && /* H */
header[14] === 0x44 && /* D */
header[15] === 0x52) /* R */
{
// This is a PNG file
try {
success({
type: PNG,
width: view.getUint32(16, false), // 32 bit big endian width
height: view.getUint32(20, false) // 32 bit big endian height
});
}
catch (ex) {
failure({ error: ex.toString()});
}
}
else if (header[0] === 0x47 && /* G */
header[1] === 0x49 && /* I */
header[2] === 0x46 && /* F */
header[3] === 0x38 && /* 8 */
(header[4] === 0x37 || /* 7 */
header[4] === 0x39) && /* or 9 */
header[5] === 0x61) /* a */
{
// This is a GIF file
try {
success({
type: GIF,
width: view.getUint16(6, true), // 16-bit little endian width
height: view.getUint16(8, true) // 16-big little endian height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0x42 && /* B */
header[1] === 0x4D && /* M */
view.getUint32(2, true) === imageBlob.size)
{
// This is a BMP file
try {
var width, height;
if (view.getUint16(14, true) === 12) { // check format version
width = view.getUint16(18, true); // 16-bit little endian width
height = view.getUint16(20, true); // 16-bit little endian height
}
else { // newer versions of the format use 32-bit ints
width = view.getUint32(18, true); // 32-bit little endian width
height = view.getUint32(22, true); // 32-bit little endian height
}
success({
type: BMP,
width: width,
height: height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0xFF &&
header[1] === 0xD8) {
var value = {
type: JPEG
};
// This is a JPEG file. To find its width and height we need
// to skip past the EXIF data and find the start of image data.
try {
var offset = 2;
// Loop through the segments of the file until we find an SOF
// segment with image size or until we reach the end of our data.
for (;;) {
// The byte at the current offset should be 0xFF marking
// the start of a new segment.
if (view.getUint8(offset) !== 0xFF) {
failure({ error: 'corrupt JPEG file' });
}
var segmentType = view.getUint8(offset + 1);
var segmentSize = view.getUint16(offset + 2) + 2;
// If we found an SOF "Start of Frame" segment, we can get
// the image size from it.
if ((segmentType >= 0xC0 && segmentType <= 0xC3) ||
(segmentType >= 0xC5 && segmentType <= 0xC7) ||
(segmentType >= 0xC9 && segmentType <= 0xCB) ||
(segmentType >= 0xCD && segmentType <= 0xCF))
{
// 16-bit big-endian dimensions, height first
value.height = view.getUint16(offset + 5, false);
value.width = view.getUint16(offset + 7, false);
success(value);
break;
}
// Otherwise, move on to the next segment
offset += segmentSize;
// We may not have read the entire file into our array buffer
// so we have to make sure we have not gone past the end of
// the buffer before looping again. Note that in this case
// the object we pass to the failure callback includes the
// image type. This is a signal to getSizeAndType that it
// should try again with a larger ArrayBuffer.
if (offset + 9 > view.byteLength) {
value.error = 'corrupt JPEG file';
failure(value);
break;
}
}
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else {
failure({ error: 'unknown image type' });
}
}
}
};
//
// Given a blob containing an image file, this function returns a
// Promise that will resolve to a blob for an image file that has
// the specified width and height. If the blob already has that
// size, it is returned unchanged. If the image is bigger than the
// specified size, then it will be shrunk and cropped as necessary
// to fit the specified dimensions. If the image is smaller than
// the specified size, it will be enlarged and cropped to fit. The
// cropping is done in the same way as with the CSS
// background-size:cover attribute.
//
// If the image is resized and outputType is "image/jpeg" or
// "image/png" then that type will be used for the resized image. If
// a JPEG or PNG image is resized and outputType is not specified,
// then the output image will have the same type as the input image.
// In all other cases, a resized image will be encoded as a PNG image.
// Note that when an image does not need to be resized its type is not
// changed, even if outputType is specified.
// 'encoderOptions' is passed 'as is' to the 'toBlob' method of canvas:
// A Number between 0 and 1 indicating image quality if the requested type
// is image/jpeg or image/webp. If this argument is anything else,
// the default value for image quality is used.
ImageUtils.resizeAndCropToCover = function(inputImageBlob,
outputWidth, outputHeight,
outputType, encoderOptions)
{
if (!outputWidth || !isFinite(outputWidth) || outputWidth <= 0 ||
!outputHeight || !isFinite(outputHeight) || outputHeight <= 0) {
return Promise.reject(new TypeError('invalid output dimensions'));
}
outputWidth = Math.round(outputWidth);
outputHeight = Math.round(outputHeight);
return ImageUtils.getSizeAndType(inputImageBlob).then(
function resolve(data) {
var inputWidth = data.width;
var inputHeight = data.height;
// If the image already has the desired size, just return it unchanged
if (inputWidth === outputWidth && inputHeight === outputHeight) {
return inputImageBlob;
}
// Otherwise, return a promise for the resized image
return resize(data);
},
function reject(error) {
// If we couldn't determine the image size and type for some
// reason we are still going to assume that it is a valid image,
// just one we don't know about. If gecko can decode it we'll
// use it and won't reject unless gecko refuses it.
return resize({});
}
);
// Decode the image in an <img> element, resize and/or crop it into a
// <canvas> and the re-encode it to a blob again. Returns a Promise
// that resolves to the encoded blob.
function resize(data) {
var inputType = data.type;
var inputWidth = data.width;
var inputHeight = data.height;
// Ignore invalid outputType parameters
if (outputType && outputType !== JPEG && outputType !== PNG) {
console.warn('Ignoring unsupported outputType', outputType);
outputType = undefined;
}
// Determine the output type if none was specified.
if (!outputType) {
if (inputType === JPEG || inputType === PNG) {
outputType = inputType;
}
else {
outputType = PNG;
}
}
// Get a URL for the image blob so we can ask gecko to decode it.
var url = URL.createObjectURL(inputImageBlob);
// If we can downsample the image while decoding it, then add a
// #-moz-samplesize media fragment to the URL. For large JPEG inputs
// this can save megabytes of memory.
var mediaFragment;
if (inputType === JPEG &&
inputWidth > outputWidth && inputHeight > outputHeight) {
// How much would we like to scale the image by while decoding?
var reduction = Math.max(outputWidth / inputWidth,
outputHeight / inputHeight);
// Get a media fragment that will downsample the image by up to
// this amount.
mediaFragment = ImageUtils.Downsample.sizeNoMoreThan(reduction);
}
else {
mediaFragment = '';
}
// The next step is to decode (while possibly downsampling) the
// image, but that is asynchronous, so we'll have to return a
// Promise to handle that
return new Promise(function(resolve, reject) {
var offscreenImage = new Image();
offscreenImage.src = url + mediaFragment;
offscreenImage.onerror = function(e) {
cleanupImage();
reject('failed to decode image');
};
offscreenImage.onload = function() {
// The image has been decoded now, so we know its actual size.
// If getSizeAndType failed, or if we used a media fragment then
// this may be different than the inputWidth and inputHeight
// values we used previously.
var actualWidth = offscreenImage.width;
var actualHeight = offscreenImage.height;
// Now figure how much we have to scale the decoded image down
// (or up) so that it covers the specified output dimensions.
var widthScale = outputWidth / actualWidth;
var heightScale = outputHeight / actualHeight;
var scale = Math.max(widthScale, heightScale);
// Scaling the output dimensions by this much tells us the
// dimensions of the crop area on the decoded image
var cropWidth = Math.round(outputWidth / scale);
var cropHeight = Math.round(outputHeight / scale);
// Now center that crop region within the decoded image
var cropLeft = Math.floor((actualWidth - cropWidth) / 2);
var cropTop = Math.floor((actualHeight - cropHeight) / 2);
// Set up the canvas we need to copy the crop region into
var canvas = document.createElement('canvas');
canvas.width = outputWidth;
canvas.height = outputHeight;
var context = canvas.getContext('2d', { willReadFrequently: true });
// Copy the image into the canvas
context.drawImage(offscreenImage,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, outputWidth, outputHeight);
// We're done with the image now: try to release the decoded
// image memory as fast as we can
cleanupImage();
// Now encode the pixels in the canvas as an image blob
canvas.toBlob(function(blob) {
// We've got the encoded image in the blob, so we don't need
// the pixels in the canvas anymore. Try to release that memory
// right away, too.
canvas.width = 0;
resolve(blob);
}, outputType, encoderOptions);
};
function cleanupImage() {
offscreenImage.onerror = offscreenImage.onload = '';
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
URL.revokeObjectURL(url);
}
});
}
};
//
// This module defines a Downsample object with static
// methods that return objects representing media fragments for
// downsampling images while they are decoded. The current implementation
// is based on the #-moz-samplesize media fragment. But because of
// problems with that fragment (see bug 1004908) it seems likely that a
// new syntax or new fragment will be introduced. If that happens, we can
// just change this module and not have to change anything else that
// depends on it.
//
// The method Downsample.areaAtLeast(scale) returns an object
// representing a media fragment to use to decode an image downsampled by
// at least as much as the specified scale. If you are trying to preview
// an 8mp image and don't want to use more than 2mp of image memory, for
// example, you would pass a scale of .25 (2mp/8mp) here, and the
// resulting media fragment could be appended to the url to make the
// image decode at a size equal to or smaller than 2mp.
//
// The method Downsample.sizeNoMoreThan(scale) returns a media fragment
// object that you can use to reduce the dimensions of an image as much
// as possible without exceeding the specified scale. If you have a
// 1600x1200 image and want to decode it to produce an image that is as
// small as possible but at least 160x120, you would pass a scale of 0.1.
//
// The returned objects have a dimensionScale property that specifies how
// they affect the dimensions of the image and an areaScale property that
// specifies how much they affect the area (number of pixels) in an
// image. (The areaScale is just the square of the scale.) To avoid
// floating-point rounding issues, the values of these scale properties
// are rounded to the nearest hundredth.
//
// The returned objects also have a scale() method that scales a
// dimension with proper rounding (it rounds up to the nearest integer
// just as libjpeg does).
//
// Each object also has a toString() method that returns the required
// media fragment (including the hash mark) so you can simply use
// string concatentation to append one of these objects to the URL of
// the image you want to decode.
//
// Downsample.NONE is a no-op media fragment object with scale set to
// 1, and a toString() method that returns the empty string.
//
(function(exports) {
'use strict'; // jshint ignore:line
// Round to the nearest hundredth to combat floating point rounding errors
function round(x) {
return Math.round(x * 100) / 100;
}
//
// A factory method for returning an object that represents a
// #-moz-samplesize media fragment. The use of Math.ceil() in the
// scale method is from jpeg_core_output_dimensions() in
// media/libjpeg/jdmaster.c and jdiv_round_up() in media/libjpeg/jutils.c
//
function MozSampleSize(n, scale) {
return Object.freeze({
dimensionScale: round(scale),
areaScale: round(scale * scale),
toString: function() { return '#-moz-samplesize=' + n; },
scale: function(x) { return Math.ceil(x * scale); }
});
}
// A fragment object that represents no downsampling with no fragment
var NONE = Object.freeze({
dimensionScale: 1,
areaScale: 1,
toString: function() { return ''; },
scale: function(x) { return x; }
});
//
// The five possible #-moz-samplesize values.
// The mapping from sample size to scale comes from:
// the moz-samplesize code in /image/decoders/nsJPEGDecoder.cpp and
// the jpeg_core_output_dimensions() function in media/libjpeg/jdmaster.c
//
var fragments = [
NONE,
// samplesize=2 reduces size by 1/2 and area by 1/4, etc.
MozSampleSize(2, 1 / 2),
MozSampleSize(3, 3 / 8),
MozSampleSize(4, 1 / 4),
MozSampleSize(8, 1 / 8)
];
// Return the fragment object that has the largest scale and downsamples the
// dimensions of an image at least as much as the specified scale.
// If none of the choices scales enough, return the one that comes closest
function sizeAtLeast(scale) {
scale = round(scale);
for (var i = 0; i < fragments.length; i++) {
var f = fragments[i];
if (f.dimensionScale <= scale) {
return f;
}
}
return fragments[fragments.length - 1];
}
// Return the fragment object that downsamples an image as far as possible
// without going beyond the specified scale. This might return NONE.
function sizeNoMoreThan(scale) {
scale = round(scale);
for (var i = fragments.length - 1; i >= 0; i--) {
var f = fragments[i];
if (f.dimensionScale >= scale) {
return f;
}
}
return NONE;
}
// Return the fragment object that has the largest scale and downsamples the
// area of an image at least as much as the specified scale.
// If none of the choices scales enough, return the one that comes closest
function areaAtLeast(scale) {
scale = round(scale);
for (var i = 0; i < fragments.length; i++) {
var f = fragments[i];
if (f.areaScale <= scale) {
return f;
}
}
return fragments[fragments.length - 1];
}
// Return the fragment object that downsamples the area of an image
// as far as possible without going beyond the specified scale. This
// might return NONE.
function areaNoMoreThan(scale) {
scale = round(scale);
for (var i = fragments.length - 1; i >= 0; i--) {
var f = fragments[i];
if (f.areaScale >= scale) {
return f;
}
}
return NONE;
}
exports.Downsample = {
sizeAtLeast: sizeAtLeast,
sizeNoMoreThan: sizeNoMoreThan,
areaAtLeast: areaAtLeast,
areaNoMoreThan: areaNoMoreThan,
NONE: NONE,
MAX_SIZE_REDUCTION: 1 / fragments[fragments.length - 1].dimensionScale,
MAX_AREA_REDUCTION: 1 / fragments[fragments.length - 1].areaScale
};
}(exports.ImageUtils));
})(window);

View file

@ -0,0 +1,385 @@
'use strict';
/**
* InputAppListSettings is reponsible for collecting and keep track of settings
* needed by InputAppList.
*/
(function(exports) {
var InputApp = function(domApp, dynamicInputs) {
this.domApp = domApp;
this.dynamicInputs = dynamicInputs || {};
if (!this.domApp.manifest) {
throw new Error(
'InputApp: should not be constructed before DOMApplication ' +
'has manifest ready.');
}
};
InputApp.prototype.getInputs = function() {
var dict = {};
var inputId;
for (inputId in this.domApp.manifest.inputs) {
dict[inputId] = this.domApp.manifest.inputs[inputId];
delete dict[inputId].nameL10nId;
}
for (inputId in this.dynamicInputs) {
if (inputId in dict) {
// Staticly declaired?
break;
}
dict[inputId] = this.dynamicInputs[inputId];
dict[inputId].isDynamic = true;
}
// Special case: we'll need to localize the one and only layout label
// "Emoji"; all other layouts are named in their native scripts so require
// no localization.
if (this.domApp.manifest.type === 'certified') {
if ('emoji' in dict) {
dict.emoji.nameL10nId = 'keyboard-layout-emoji';
}
}
return dict;
};
var InputAppListSettings = function() {
this._values = {
enable3rdPartyApps: undefined,
dynamicInputs: undefined
};
this._bindCallback = null;
this.ready = false;
this._promise = null;
};
InputAppListSettings.prototype.onchange = null;
InputAppListSettings.prototype.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED =
'keyboard.3rd-party-app.enabled';
InputAppListSettings.prototype.SETTINGS_KEY_DYNAMIC_INPUTS =
'keyboard.dynamic-inputs';
InputAppListSettings.prototype.start = function() {
if (navigator.mozSettings) {
// We need to keep this so we could removeObserver()
this._bindCallback = this.callback.bind(this);
navigator.mozSettings.addObserver(
this.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED, this._bindCallback);
navigator.mozSettings.addObserver(
this.SETTINGS_KEY_DYNAMIC_INPUTS, this._bindCallback);
}
this.getSettings().then(function() {
this.ready = true;
}.bind(this));
};
InputAppListSettings.prototype.getSettings = function() {
if (this.ready) {
return this.getSettingsSync();
}
return this.getSettingsAsync();
};
InputAppListSettings.prototype.getSettingsSync = function() {
if (!this.ready) {
console.warn('getSettingsSync: getSettingsSync called before ready.');
}
return this._values;
};
InputAppListSettings.prototype.getSettingsAsync = function() {
if (this._promise) {
return this._promise;
}
var lock = navigator.mozSettings.createLock();
var p = this._promise = Promise.all([
lock.get(this.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED),
lock.get(this.SETTINGS_KEY_DYNAMIC_INPUTS)])
.then(function(values) {
this._values.enable3rdPartyApps =
values[0][this.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED];
this._values.dynamicInputs =
values[1][this.SETTINGS_KEY_DYNAMIC_INPUTS];
return this._values;
}.bind(this))
.catch(function(e) {
console.error('InputAppListSettings: Fail to get setting.', e);
this._promise = null;
this._values.enable3rdPartyApps = false;
this._values.dynamicInputs = {};
return this._values;
}.bind(this));
return p;
};
// Callback for mozSettings.addObserver()
InputAppListSettings.prototype.callback = function(obj) {
switch (obj.settingName) {
case this.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED:
this._values.enable3rdPartyApps = obj.settingValue;
break;
case this.SETTINGS_KEY_DYNAMIC_INPUTS:
this._values.dynamicInputs = obj.settingValue;
break;
}
if (typeof this.onchange === 'function') {
this.onchange(this._values);
}
};
InputAppListSettings.prototype.stop = function() {
this._values = {
enable3rdPartyApps: undefined,
dynamicInputs: undefined
};
this.ready = false;
this._promise = null;
if (navigator.mozSettings) {
navigator.mozSettings.removeObserver(
this.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED, this._bindCallback);
navigator.mozSettings.removeObserver(
this.SETTINGS_KEY_DYNAMIC_INPUTS, this._bindCallback);
this._bindCallback = null;
}
};
/**
* InputAppList takes app registry and settings to generate a list of available
* input apps.
*/
var InputAppList = function() {
this.ready = false;
this._inputApps = null;
this.settings = null;
this._getListPromise = null;
};
InputAppList.prototype.onready = null;
InputAppList.prototype.onupdate = null;
InputAppList.prototype.SETTINGS_KEY_THIRD_PARTY_APP_ENABLED =
'keyboard.3rd-party-app.enabled';
InputAppList.prototype.start = function() {
if (navigator.mozApps && navigator.mozApps.mgmt) {
navigator.mozApps.mgmt.addEventListener('install', this);
navigator.mozApps.mgmt.addEventListener('uninstall', this);
}
this.settings = new InputAppListSettings();
this.settings.onchange = this._refresh.bind(this);
this.settings.start();
this.getList().then(function() {
this.ready = true;
if (typeof this.onready === 'function') {
return this.onready(this._inputApps);
}
}.bind(this)).catch(function(e) {
console.error(e);
});
};
InputAppList.prototype.getList = function() {
if (this._getListPromise) {
return this._getListPromise;
}
var p = this._getListPromise =
Promise.all([this.settings.getSettings(), this._getAllApps()])
.then(this._setInputApps.bind(this))
.then(function() {
return this._inputApps;
}.bind(this))
.catch(function(e) {
this._getListPromise = null;
console.error(e);
}.bind(this));
return p;
};
InputAppList.prototype.getListSync = function() {
if (!this.ready) {
console.warn('InputAppList: getListSync called when list is not ready.');
}
return this._inputApps;
};
InputAppList.prototype.handleEvent = function(evt) {
var updated = false;
switch (evt.type) {
case 'install':
var app = evt.application;
if (app.downloading) {
// App is currently being downloaded;
// wait for download to complete as manifest is not available now.
app.addEventListener('downloadsuccess', this);
return;
}
updated = this._addInputApp(app);
break;
case 'downloadsuccess':
updated = this._addInputApp(evt.target);
break;
case 'uninstall':
updated = this._removeInputApp(evt.application);
break;
}
if (updated && typeof this.onupdate === 'function') {
this.onupdate(this._inputApps);
}
};
InputAppList.prototype.stop = function() {
this.ready = false;
this._inputApps = null;
this.settings = null;
this._getListPromise = null;
if (navigator.mozApps && navigator.mozApps.mgmt) {
navigator.mozApps.mgmt.removeEventListener('install', this);
navigator.mozApps.mgmt.removeEventListener('uninstall', this);
}
};
InputAppList.prototype._getAllApps = function() {
if (!navigator.mozApps || !navigator.mozApps.mgmt) {
console.error('InputAppList: mozApps.mgmt not available.');
return [];
}
return Promise.resolve(navigator.mozApps.mgmt.getAll())
.catch(function(e) {
console.error('InputAppList: Fail to get app list.', e);
return [];
});
};
InputAppList.prototype._setInputApps = function(values) {
var apps = values[1];
var dynamicInputs = this.settings.getSettingsSync().dynamicInputs || {};
var inputApps = apps.filter(this._isInputApp, this).map(function(domApp) {
var inputApp = new InputApp(domApp, dynamicInputs[domApp.manifestURL]);
return inputApp;
});
if (inputApps.length === 0) {
console.error('InputAppList: No input apps installed?');
}
this._inputApps = inputApps;
};
InputAppList.prototype._addInputApp = function(domApp) {
if (!this._isInputApp(domApp)) {
return false;
}
this._inputApps.push(new InputApp(domApp));
return true;
};
InputAppList.prototype._removeInputApp = function(domApp) {
var index = this._inputApps.findIndex(function(appInList) {
return (appInList.domApp.manifestURL === domApp.manifestURL);
});
if (index === -1) {
return false;
}
this._inputApps.splice(index, 1);
return true;
};
InputAppList.prototype._isInputApp = function(domApp) {
if (!domApp.manifest || 'input' !== domApp.manifest.role) {
return false;
}
// Check app type
if (domApp.manifest.type !== 'certified' &&
domApp.manifest.type !== 'privileged') {
return false;
}
var settingValues = this.settings.getSettingsSync();
if (!settingValues.enable3rdPartyApps &&
domApp.manifest.type !== 'certified') {
console.warn('InputAppList: ' +
'A 3rd-party input app is installed but the feature is not enabled.',
domApp.manifestURL);
return false;
}
// Check permission
// TODO: Maybe we shouldn't be checking permission ourselves?
if (domApp.manifest.permissions &&
(typeof domApp.manifest.permissions.input !== 'object')) {
return false;
}
if (typeof domApp.manifest.inputs !== 'object') {
return false;
}
return true;
};
InputAppList.prototype._refresh = function() {
this._getListPromise = null;
this.getList().then(function() {
if (typeof this.onupdate === 'function') {
return this.onupdate(this._inputApps);
}
}.bind(this)).catch(function(e) {
console.error(e);
});
};
exports.InputAppListSettings = InputAppListSettings;
exports.InputAppList = InputAppList;
exports.InputApp = InputApp;
}(window));

View file

@ -0,0 +1,152 @@
/* exported InputParser */
'use strict';
/**
* Stateless object for input parser functions..
* The intent is the methods here will only relate to the parsing
* of input[type="date|time"]
*/
var InputParser = (function() {
var InputParser = {
_dateParts: ['year', 'month', 'date'],
_timeParts: ['hours', 'minutes', 'seconds'],
/**
* Import HTML5 input[type="time"] string value
*
* @param {String} value 23:20:50.52, 17:39:57.
* @return {Object} { hours: 23, minutes: 20, seconds: 50 }.
*/
importTime: function(value) {
var result = {
hours: 0,
minutes: 0,
seconds: 0
};
if (!value) {
return result;
}
var parts = value.split(':');
var part;
var partName;
var i = 0;
var len = InputParser._timeParts.length;
for (; i < len; i++) {
partName = InputParser._timeParts[i];
part = parts[i];
if (part) {
result[partName] = parseInt(part.slice(0, 2), 10) || 0;
}
}
return result;
},
/**
* Export date to HTML5 input[type="time"]
*
* @param {Date} value export value.
* @return {String} 17:39:57.
*/
exportTime: function(value) {
return value.toLocaleString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
/**
* Import HTML5 input[type="time"] to object.
*
* @param {String} value 1997-12-19.
* @return {Object} { year: 1997, month: 12, date: 19 }.
*/
importDate: function(value) {
var result = {
year: 0,
month: 0,
date: 0
};
if (!value) {
return result;
}
var parts = value.split('-');
var part;
var partName;
var i = 0;
var len = InputParser._dateParts.length;
for (; i < len; i++) {
partName = InputParser._dateParts[i];
part = parts[i];
if (part) {
result[partName] = parseInt(part, 10);
}
}
if (result.month > 0) {
result.month = result.month - 1;
}
result.date = result.date || 1;
return result;
},
/**
* Export js local date to HTML5 input[type="date"].
*
* @param {Date} value export value.
* @return {String} date string (1997-12-19).
*/
exportDate: function(value) {
var year = value.getFullYear();
var month = value.getMonth() + 1;
var day = value.getDate();
if (month < 10) {
month = `0${month}`;
}
if (day < 10) {
day = `0${day}`;
}
return `${year}-${month}-${day}`;
},
/**
* Designed to take a date & time value from
* html5 input types and returns a JS Date.
*
* @param {String} date input date.
* @param {String} time input time.
*
* @return {Date} full date object from date/time.
*/
formatInputDate: function(date, time) {
time = InputParser.importTime(time);
date = InputParser.importDate(date);
return new Date(
date.year,
date.month,
date.date,
time.hours,
time.minutes,
time.seconds
);
},
};
return InputParser;
}());

View file

@ -0,0 +1,822 @@
'use strict';
/* global InputAppList, ManifestHelper */
/**
* Helper object to find all installed keyboard apps and layouts.
*
* (Need mozApps.mgmt and settings permission)
*/
(function(exports) {
/* jshint validthis: true */
/**
* The set of "basic keyboard" types
*/
var BASE_TYPES = new Set([
'text', 'url', 'email', 'password', 'number', 'option'
]);
/**
* The keys stored in the settings database
*/
var SETTINGS_KEYS = {
ENABLED: 'keyboard.enabled-layouts',
DEFAULT: 'keyboard.default-layouts'
};
var DEPRECATE_KEYBOARD_SETTINGS = {
en: 'keyboard.layouts.english',
'en-Dvorak': 'keyboard.layouts.dvorak',
cs: 'keyboard.layouts.czech',
fr: 'keyboard.layouts.french',
de: 'keyboard.layouts.german',
hu: 'keyboard.layouts.hungarian',
nb: 'keyboard.layouts.norwegian',
my: 'keyboard.layouts.myanmar',
sl: 'keyboard.layouts.slovak',
tr: 'keyboard.layouts.turkish',
ro: 'keyboard.layouts.romanian',
ru: 'keyboard.layouts.russian',
ar: 'keyboard.layouts.arabic',
he: 'keyboard.layouts.hebrew',
'zh-Hant-Zhuyin': 'keyboard.layouts.zhuyin',
'zh-Hans-Pinyin': 'keyboard.layouts.pinyin',
el: 'keyboard.layouts.greek',
'jp-kanji': 'keyboard.layouts.japanese',
pl: 'keyboard.layouts.polish',
'pt-BR': 'keyboard.layouts.portuguese',
sr: 'keyboard.layouts.serbian',
es: 'keyboard.layouts.spanish',
ca: 'keyboard.layouts.catalan'
};
var MULTI_LAYOUT_MAP = {
sr: ['sr-Cyrl', 'sr-Latn']
};
// In order to provide default defaults, we need to know the default keyboard
var defaultKeyboardManifestURL =
'app://keyboard.gaiamobile.org/manifest.webapp';
// Stores a local copy of whatever is in the settings database
var currentSettings = {
defaultLayouts: {}
};
// until we read otherwise, asssume the default keyboards are en and number
currentSettings.defaultLayouts[defaultKeyboardManifestURL] = {
en: true,
number: true
};
// and also assume that the defaults are the enabled
currentSettings.enabledLayouts = map2dClone(currentSettings.defaultLayouts);
/**
* helper function for reading a value in one of the currentSettings
*/
function map2dIs(manifestURL, layoutId) {
// force boolean true or false
return !!(this[manifestURL] && this[manifestURL][layoutId]);
}
/**
* helper function for setting a value to true in one of the currentSettings
*/
function map2dSet(manifestURL, layoutId) {
if (!this[manifestURL]) {
this[manifestURL] = {};
}
this[manifestURL][layoutId] = true;
}
/**
* helper function for setting a value to false in one of the currentSettings
* deletes the appropriate keys
*/
function map2dUnset(manifestURL, layoutId) {
if (!this[manifestURL]) {
return;
}
delete this[manifestURL][layoutId];
if (!Object.keys(this[manifestURL]).length) {
delete this[manifestURL];
}
}
/**
* helper function for cloning one of the currentSettings
*/
function map2dClone(obj) {
var result = {};
for (var key1 in obj) {
result[key1] = {};
for (var key2 in obj[key1]) {
result[key1][key2] = obj[key1][key2];
}
}
return result;
}
// callbacks when something changes
var watchQueries = [];
// holds the result of keyboard_layouts.json
var defaultLayoutConfig;
/**
* Sends any watchers the result of their query.
*/
function kh_updateWatchers(reason) {
if (watchQueries.length) {
// we do a getApps in order to make sure that we have the most recent
// app data before calling getLayouts
KeyboardHelper.getApps(function withApps() {
watchQueries.forEach(function eachWatchQuery(watch) {
KeyboardHelper.getLayouts(watch.query, function withLayouts(layouts) {
watch.callback(layouts, reason);
});
});
});
}
}
// callbacks waiting to know when settings are loaded
var waitingForSettings = [];
var loadedSettings = new Set();
/**
* Tracks the number of settings loaded and calls the callbacks
*/
function kh_loadedSetting(setting) {
loadedSettings.add(setting);
if (loadedSettings.size >= Object.keys(SETTINGS_KEYS).length) {
waitingForSettings.forEach(function(callback) {
callback();
});
waitingForSettings = [];
kh_updateWatchers({ settings: true });
}
}
/**
* Call a callback when the settings have loaded, or immediately if already
* loaded.
*/
function kh_withSettings(callback) {
if (loadedSettings.size >= Object.keys(SETTINGS_KEYS).length) {
callback();
} else {
waitingForSettings.push(callback);
}
}
/**
* Internal helper to read the settings again
*/
function kh_getSettings() {
loadedSettings = new Set();
var lock = window.navigator.mozSettings.createLock();
lock.get(SETTINGS_KEYS.DEFAULT).onsuccess = kh_parseDefault;
lock.get(SETTINGS_KEYS.ENABLED).onsuccess = kh_parseEnabled;
}
/**
* Parse the result from the settings query for default layouts
*/
function kh_parseDefault() {
var value = this.result[SETTINGS_KEYS.DEFAULT];
if (value) {
currentSettings.defaultLayouts = value;
}
kh_loadedSetting(SETTINGS_KEYS.DEFAULT);
}
/**
* Parse the result from the settings query for enabled layouts
*/
function kh_parseEnabled() {
var value = this.result[SETTINGS_KEYS.ENABLED];
var needsSave = false;
if (!value) {
currentSettings.enabledLayouts = map2dClone(currentSettings.defaultLayouts);
needsSave = true;
} else {
var type = typeof value;
// deal with the old string format - might be able to be removed once test
// devices have been living with this new format for a bit.
if (type === 'string') {
try {
currentSettings.enabledLayouts = {};
var oldSettings = JSON.parse(value);
oldSettings.forEach(function(layout) {
if (layout.enabled) {
var manifestURL = layout.manifestURL;
if (!manifestURL) {
manifestURL = layout.appOrigin + '/manifest.webapp';
}
map2dSet.call(currentSettings.enabledLayouts, manifestURL,
layout.layoutId);
}
});
} catch (e) {
currentSettings.enabledLayouts =
map2dClone(currentSettings.defaultLayouts);
needsSave = true;
}
} else {
// if it wasn't a string, assume its our object
currentSettings.enabledLayouts = value;
}
}
kh_loadedSetting(SETTINGS_KEYS.ENABLED);
if (needsSave) {
KeyboardHelper.saveToSettings();
}
}
/**
* Parse and migrate the result for the deprecated settings (v1.1) for enabled
* layouts.
*/
function kh_migrateDeprecatedSettings(deprecatedSettings) {
/* jshint loopfunc: true */
var settingEntry = DEPRECATE_KEYBOARD_SETTINGS.en;
// No need to do migration if the deprecated settings are not available
if (deprecatedSettings[settingEntry] === undefined) {
return;
}
// reset the enabled layouts
currentSettings.enabledLayouts[defaultKeyboardManifestURL] = {
number: true
};
var hasEnabledLayout = false;
for (var key in DEPRECATE_KEYBOARD_SETTINGS) {
settingEntry = DEPRECATE_KEYBOARD_SETTINGS[key];
// this layout was set as enabled in the old settings
if (deprecatedSettings[settingEntry]) {
hasEnabledLayout = true;
if (key in MULTI_LAYOUT_MAP) {
MULTI_LAYOUT_MAP[key].forEach(function enableLayout(layoutId) {
map2dSet.call(currentSettings.enabledLayouts,
defaultKeyboardManifestURL, layoutId);
});
} else {
map2dSet.call(currentSettings.enabledLayouts,
defaultKeyboardManifestURL, key);
}
}
}
// None of the layouts has been set enabled, so enable English by default
if (!hasEnabledLayout) {
map2dSet.call(currentSettings.enabledLayouts,
defaultKeyboardManifestURL, 'en');
}
// Clean up all the deprecated settings
var deprecatedSettingsQuery = {};
for (key in DEPRECATE_KEYBOARD_SETTINGS) {
// the deprecated setting entry, e.g. keyboard.layout.english
settingEntry = DEPRECATE_KEYBOARD_SETTINGS[key];
// Set the default value for each entry
deprecatedSettingsQuery[settingEntry] = null;
}
window.navigator.mozSettings.createLock().set(deprecatedSettingsQuery);
KeyboardHelper.saveToSettings();
}
/**
* JSON loader
*/
function kh_loadJSON(href, callback) {
if (!callback) {
return;
}
var xhr = new XMLHttpRequest();
xhr.onerror = function() {
console.error('Failed to fetch file: ' + href, xhr.statusText);
};
xhr.onload = function() {
callback(xhr.response);
};
xhr.open('GET', href, true); // async
xhr.responseType = 'json';
xhr.send();
}
//
// getSettings: Query the value of multiple settings at once.
//
// settings is an object whose property names are the settings to query
// and whose property values are the default values to use if no such
// setting is found. This function will create a setting lock and
// request the value of each of the specified settings. Once it receives
// a response to all of the queries, it passes all the settings values to
// the specified callback function. The argument to the callback function
// is an object just like the settings object, where the property name is
// the setting name and the property value is the setting value (or the
// default value if the setting was not found).
//
function kh_getMultiSettings(settings, callback) {
var results = {};
try {
var lock = navigator.mozSettings.createLock();
}
catch (e) {
// If settings is broken, just return the default values
console.warn('Exception in mozSettings.createLock():', e,
'\nUsing default values');
for (var p in settings) {
results[p] = settings[p];
}
callback(results);
}
var settingNames = Object.keys(settings);
var numSettings = settingNames.length;
var numResults = 0;
for (var i = 0; i < numSettings; i++) {
requestSetting(settingNames[i]);
}
function requestSetting(name) {
var request;
try {
request = lock.get(name);
}
catch (e) {
console.warn('Exception querying setting', name, ':', e,
'\nUsing default value');
recordResult(name, settings[name]);
return;
}
request.onsuccess = function() {
var value = request.result[name];
if (value === undefined) {
value = settings[name]; // Use the default value
}
recordResult(name, value);
};
request.onerror = function(evt) {
console.warn('Error querying setting', name, ':', evt.error);
recordResult(name, settings[name]);
};
}
function recordResult(name, value) {
results[name] = value;
numResults++;
if (numResults === numSettings) {
callback(results);
}
}
}
/**
* Creates an object that describes a keyboard layout. The objects prototype
* will give it accessors for 'default' and 'enabled' that will read or change
* the proper setting from currentSettings.
*
* @constructor
*/
function KeyboardLayout(options) {
for (var key in options) {
this[key] = options[key];
}
}
Object.defineProperties(KeyboardLayout.prototype, {
'default': {
get: function kh_getLayoutIsDefault() {
return map2dIs.call(
currentSettings.defaultLayouts, this.app.manifestURL, this.layoutId
);
}
},
enabled: {
get: function kh_getLayoutIsEnabled() {
return map2dIs.call(
currentSettings.enabledLayouts, this.app.manifestURL, this.layoutId
);
},
set: function kh_setLayoutIsDefault(value) {
var method = value ? map2dSet : map2dUnset;
method.call(currentSettings.enabledLayouts,
this.app.manifestURL, this.layoutId);
}
}
});
/**
* Exposed as KeyboardHelper.settings this gives us a fairly safe way to read
* and write to the settings data structures directly.
* XXX: is this really used anywhere?
*/
var kh_SettingsHelper = {};
Object.defineProperties(kh_SettingsHelper, {
'default': {
get: function() {
return map2dClone(currentSettings.defaultLayouts);
},
set: function(value) {
currentSettings.defaultLayouts = map2dClone(value);
},
enumerable: true
},
'enabled': {
get: function() {
return map2dClone(currentSettings.enabledLayouts);
},
set: function(value) {
currentSettings.enabledLayouts = map2dClone(value);
},
enumerable: true
}
});
var KeyboardHelper = exports.KeyboardHelper = {
settings: kh_SettingsHelper,
// bug 1035117: define the fallback layout for a group if no layout has been
// selected for that group (if it's not enforced in settings)
// please see the bug and its related UX spec for the sense of 'fallback'
fallbackLayoutNames: {
password: 'en'
},
fallbackLayouts: {},
// InputAppList manages the current input apps for us.
inputAppList: null,
/**
* Listen for changes in settings or apps and read the deafault settings
*/
init: function kh_init() {
watchQueries = [];
this.inputAppList = new InputAppList();
this.inputAppList.onupdate =
kh_updateWatchers.bind(undefined, { apps: true });
this.inputAppList.start();
// load the current settings, and watch for changes to settings
var settings = window.navigator.mozSettings;
if (settings) {
kh_getSettings();
settings.addObserver(SETTINGS_KEYS.ENABLED, kh_getSettings);
settings.addObserver(SETTINGS_KEYS.DEFAULT, kh_getSettings);
// read deprecated settings
var deprecatedSettingsQuery = {};
for (var key in DEPRECATE_KEYBOARD_SETTINGS) {
// the deprecated setting entry, e.g. keyboard.layout.english
var settingEntry = DEPRECATE_KEYBOARD_SETTINGS[key];
// Set the default value for each entry
deprecatedSettingsQuery[settingEntry] = undefined;
}
kh_getMultiSettings(deprecatedSettingsQuery,
kh_migrateDeprecatedSettings);
}
window.addEventListener('applicationinstallsuccess', this);
},
/**
* Handles the application changed events. Clears the cache and updates
* any listening watchers.
*/
handleEvent: function(event) {
kh_updateWatchers({ apps: true });
},
/**
* If this is a basic keyboard type
*/
isKeyboardType: function kh_isKeyboardType(type) {
return BASE_TYPES.has(type);
},
/**
* Enables or disables a layout based on manifest URL and layoutId
*/
setLayoutEnabled: function kh_setLayoutEnabled(manifestURL, layoutId,
enabled) {
var method = enabled ? map2dSet : map2dUnset;
method.call(currentSettings.enabledLayouts, manifestURL, layoutId);
},
/**
* Returns true if the layout specified by manifest URL and layoutId is
* enabled.
*/
getLayoutEnabled: function kh_getLayoutEnabled(manifestURL, layoutId) {
return map2dIs.call(currentSettings.enabledLayouts, manifestURL, layoutId);
},
/**
* set/unset a layout as default based on manifest URL and layoutId
*/
setLayoutIsDefault: function kh_setLayoutIsDefault(manifestURL, layoutId,
enabled) {
var method = enabled ? map2dSet : map2dUnset;
method.call(currentSettings.defaultLayouts, manifestURL, layoutId);
},
/**
* Returns true if the layout specified by manifest URL and layoutId
* is default.
*/
getLayoutIsDefault: function kh_getLayoutIsDefault(manifestURL, layoutId) {
return map2dIs.call(currentSettings.defaultLayouts, manifestURL, layoutId);
},
/**
* Checks the currently enabled layouts to ensure there is at least one
* 'text', 'url' and 'number' keyboard enabled. If there is not, it will
* enable the corresponding default keyboard.
*
* Only calls the callback if a default was enabled, and passes an array
* of layouts that were enabled.
*/
checkDefaults: function kh_checkDefaults(callback) {
var layoutsEnabled = [];
var missingTypes = [];
['text', 'url', 'number'].forEach(function eachType(type) {
// getLayouts is sync when we already have data
var enabled;
this.getLayouts({ type: type, enabled: true }, function(layouts) {
enabled = layouts.length;
});
if (!enabled) {
missingTypes.push(type);
this.getLayouts({ type: type, 'default': true }, function(layouts) {
if (layouts[0]) {
layouts[0].enabled = true;
layoutsEnabled.push(layouts[0]);
}
});
}
}, this);
if (layoutsEnabled.length) {
if (callback) {
callback(layoutsEnabled, missingTypes);
}
}
},
/**
* Saves the current state to settings. None of the changes made will be
* saved automatically by KeyboardHelper. This allows someone to batch
* whatever settings changes are nessecary, then call save.
*/
saveToSettings: function ke_saveToSettings() {
var toSet = {};
toSet[SETTINGS_KEYS.ENABLED] = currentSettings.enabledLayouts;
toSet[SETTINGS_KEYS.DEFAULT] = currentSettings.defaultLayouts;
window.navigator.mozSettings.createLock().set(toSet);
},
/**
* Get a list of current keyboard applications.
*/
getApps: function kh_getApps(callback) {
// every time we get a list of apps, clean up the settings
var cleanupSettings = function(inputApps) {
Object.keys(currentSettings.enabledLayouts)
.concat(Object.keys(currentSettings.defaultLayouts))
.forEach(function(manifestURL) {
// if the manifestURL doesn't exist in the list of apps, delete it
// from the settings maps
if (!inputApps.some(function(inputApp) {
return (inputApp.domApp.manifestURL === manifestURL);
})) {
delete currentSettings.enabledLayouts[manifestURL];
delete currentSettings.defaultLayouts[manifestURL];
}
});
};
// XXX We have to preserve the original getApps() behavior here,
// i.e. call the sync callback when we already have the data.
if (this.inputAppList.ready) {
var inputApps = this.inputAppList.getListSync();
cleanupSettings(inputApps);
callback(inputApps);
return;
}
this.inputAppList.getList().then(function(inputApps) {
cleanupSettings(inputApps);
callback(inputApps);
})['catch'](function(e) { // workaround gjslint error
console.error(e);
});
},
/**
* Requests an array of layouts matching an optional set of search params.
* Calls the callback when data is received, or immediately if already
* cached locally.
*
* options.default - When true, only default layouts are returned.
* options.enabled - When true, only enabled layouts are returned.
* options.type - A string, or array of strings. Layouts filtered to type.
*/
getLayouts: function kh_getLayouts(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
function withApps(inputApps) {
/* jshint loopfunc: true */
// we'll delete keys in this active copy (= the purpose of copying)
var fallbackLayoutNames = {};
for (var group in this.fallbackLayoutNames) {
fallbackLayoutNames[group] = this.fallbackLayoutNames[group];
}
this.fallbackLayouts = {};
var layouts = inputApps.reduce(function eachApp(result, inputApp) {
var domApp = inputApp.domApp;
var manifest = new ManifestHelper(domApp.manifest);
var inputs = inputApp.getInputs();
for (var layoutId in inputs) {
var inputManifest = inputs[layoutId];
if (!inputManifest.types) {
console.warn(domApp.manifestURL, layoutId, 'did not declare type.');
continue;
}
var layout = new KeyboardLayout({
app: domApp,
inputApp: inputApp,
manifest: manifest,
inputManifest: inputManifest,
layoutId: layoutId
});
// bug 1035117: insert a fallback layout regardless of its
// and enabledness
// XXX: we only do this for built-in keyboard?
if (domApp.manifestURL === defaultKeyboardManifestURL) {
for (var group in fallbackLayoutNames) {
if (layoutId === fallbackLayoutNames[group]) {
this.fallbackLayouts[group] = layout;
delete fallbackLayoutNames[group];
}
}
}
if (options['default'] && !layout['default']) {
continue;
}
if (options.enabled && !layout.enabled) {
continue;
}
if (options.type) {
options.type = [].concat(options.type);
// check for any type in our options to be any type in the layout
if (!options.type.some(function(type) {
return inputManifest.types.indexOf(type) !== -1;
})) {
continue;
}
}
result.push(layout);
}
// sort the default query by most specific keyboard
if (options['default']) {
result.sort(function(a, b) {
return a.inputManifest.types.length - b.inputManifest.types.length;
});
}
return result;
}.bind(this), []);
kh_withSettings(callback.bind(null, layouts));
}
this.getApps(withApps.bind(this));
},
/**
* Clears all watch queries
*/
stopWatching: function() {
watchQueries = [];
},
/**
* Calls the callback whenever settings or apps are changed. The query is
* passed onto getLayouts. The callback will be called as soon as the current
* settings and apps are loaded as well.
*
* The callback will be passed an array of layouts, and a reason object.
* I.E. function callback(layouts, reason) {}
* reason.apps will be true when apps have changed, and reason.settings
* will be true if settings changed. Both will be true on the first
* callback.
*/
watchLayouts: function(query, callback) {
if (typeof query === 'function') {
callback = query;
query = {};
}
var watch = {
query: query,
callback: callback
};
watchQueries.push(watch);
this.getLayouts(query, function initialCall(layouts) {
watch.layouts = layouts;
callback(layouts, { apps: true, settings: true });
});
},
// Read keyboard_layouts for language -> layouts mapping
getDefaultLayoutConfig: function kh_getDefaultLayoutConfig(callback) {
if (!callback) {
return;
}
if (defaultLayoutConfig) {
callback(defaultLayoutConfig);
} else {
var KEYBOARDS = '/shared/resources/keyboard_layouts.json';
kh_loadJSON(KEYBOARDS, function loadKeyboardLayouts(data) {
if (data) {
defaultLayoutConfig = data;
callback(defaultLayoutConfig);
}
});
}
},
// Change the default layouts set according to the language
// language: the current system language, used to look up the mapping table
// reset: if set as true, will reset the current enabled layouts
changeDefaultLayouts: function kh_changeDefaultLayouts(language, reset) {
this.getDefaultLayoutConfig(function gotDefaultLayouts(keyboards) {
var newKbLayouts = keyboards.layout[language];
// XXX: change this so that it could support multiple built-in
// keyboard apps
var kbManifestURL = defaultKeyboardManifestURL;
// reset the set of default layouts
currentSettings.defaultLayouts = {};
// set the language-independent default layouts
var langIndependentLayouts = keyboards.langIndependentLayouts;
for (var i = langIndependentLayouts.length - 1; i >= 0; i--) {
this.setLayoutIsDefault(kbManifestURL,
langIndependentLayouts[i].layoutId,
true);
}
if (reset) {
// reset the set of default layouts
currentSettings.enabledLayouts =
map2dClone(currentSettings.defaultLayouts);
}
// Enable the language specific keyboard layout group
for (i = newKbLayouts.length - 1; i >= 0; i--) {
this.setLayoutIsDefault(kbManifestURL, newKbLayouts[i].layoutId, true);
this.setLayoutEnabled(kbManifestURL, newKbLayouts[i].layoutId, true);
}
this.saveToSettings(); // save changes to settings
}.bind(this));
}
};
KeyboardHelper.init();
}(window));

View file

@ -0,0 +1,245 @@
;;;;;(function(window,undefined){'use strict';function L10nError(message,id,loc){this.name='L10nError';this.message=message;this.id=id;this.loc=loc;}
L10nError.prototype=Object.create(Error.prototype);L10nError.prototype.constructor=L10nError;var io={_load:function(type,url,callback,sync){var xhr=new XMLHttpRequest();var needParse;if(xhr.overrideMimeType){xhr.overrideMimeType(type);}
xhr.open('GET',url,!sync);if(type==='application/json'){if(sync){needParse=true;}else{xhr.responseType='json';}}
xhr.addEventListener('load',function io_onload(e){if(e.target.status===200||e.target.status===0){var res=e.target.response||e.target.responseText;callback(null,needParse?JSON.parse(res):res);}else{callback(new L10nError('Not found: '+url));}});xhr.addEventListener('error',callback);xhr.addEventListener('timeout',callback);try{xhr.send(null);}catch(e){if(e.name==='NS_ERROR_FILE_NOT_FOUND'){callback(new L10nError('Not found: '+url));}else{throw e;}}},load:function(url,callback,sync){return io._load('text/plain',url,callback,sync);},loadJSON:function(url,callback,sync){return io._load('application/json',url,callback,sync);}};function EventEmitter(){}
EventEmitter.prototype.emit=function ee_emit(){if(!this._listeners){return;}
var args=Array.prototype.slice.call(arguments);var type=args.shift();if(!this._listeners[type]){return;}
var typeListeners=this._listeners[type].slice();for(var i=0;i<typeListeners.length;i++){typeListeners[i].apply(this,args);}};EventEmitter.prototype.addEventListener=function ee_add(type,listener){if(!this._listeners){this._listeners={};}
if(!(type in this._listeners)){this._listeners[type]=[];}
this._listeners[type].push(listener);};EventEmitter.prototype.removeEventListener=function ee_rm(type,listener){if(!this._listeners){return;}
var typeListeners=this._listeners[type];var pos=typeListeners.indexOf(listener);if(pos===-1){return;}
typeListeners.splice(pos,1);};function getPluralRule(lang){var locales2rules={'af':3,'ak':4,'am':4,'ar':1,'asa':3,'az':0,'be':11,'bem':3,'bez':3,'bg':3,'bh':4,'bm':0,'bn':3,'bo':0,'br':20,'brx':3,'bs':11,'ca':3,'cgg':3,'chr':3,'cs':12,'cy':17,'da':3,'de':3,'dv':3,'dz':0,'ee':3,'el':3,'en':3,'eo':3,'es':3,'et':3,'eu':3,'fa':0,'ff':5,'fi':3,'fil':4,'fo':3,'fr':5,'fur':3,'fy':3,'ga':8,'gd':24,'gl':3,'gsw':3,'gu':3,'guw':4,'gv':23,'ha':3,'haw':3,'he':2,'hi':4,'hr':11,'hu':0,'id':0,'ig':0,'ii':0,'is':3,'it':3,'iu':7,'ja':0,'jmc':3,'jv':0,'ka':0,'kab':5,'kaj':3,'kcg':3,'kde':0,'kea':0,'kk':3,'kl':3,'km':0,'kn':0,'ko':0,'ksb':3,'ksh':21,'ku':3,'kw':7,'lag':18,'lb':3,'lg':3,'ln':4,'lo':0,'lt':10,'lv':6,'mas':3,'mg':4,'mk':16,'ml':3,'mn':3,'mo':9,'mr':3,'ms':0,'mt':15,'my':0,'nah':3,'naq':7,'nb':3,'nd':3,'ne':3,'nl':3,'nn':3,'no':3,'nr':3,'nso':4,'ny':3,'nyn':3,'om':3,'or':3,'pa':3,'pap':3,'pl':13,'ps':3,'pt':3,'rm':3,'ro':9,'rof':3,'ru':11,'rwk':3,'sah':0,'saq':3,'se':7,'seh':3,'ses':0,'sg':0,'sh':11,'shi':19,'sk':12,'sl':14,'sma':7,'smi':7,'smj':7,'smn':7,'sms':7,'sn':3,'so':3,'sq':3,'sr':11,'ss':3,'ssy':3,'st':3,'sv':3,'sw':3,'syr':3,'ta':3,'te':3,'teo':3,'th':0,'ti':4,'tig':3,'tk':3,'tl':4,'tn':3,'to':0,'tr':0,'ts':3,'tzm':22,'uk':11,'ur':3,'ve':3,'vi':0,'vun':3,'wa':4,'wae':3,'wo':0,'xh':3,'xog':3,'yo':0,'zh':0,'zu':3};function isIn(n,list){return list.indexOf(n)!==-1;}
function isBetween(n,start,end){return typeof n===typeof start&&start<=n&&n<=end;}
var pluralRules={'0':function(){return'other';},'1':function(n){if((isBetween((n%100),3,10))){return'few';}
if(n===0){return'zero';}
if((isBetween((n%100),11,99))){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'2':function(n){if(n!==0&&(n%10)===0){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'3':function(n){if(n===1){return'one';}
return'other';},'4':function(n){if((isBetween(n,0,1))){return'one';}
return'other';},'5':function(n){if((isBetween(n,0,2))&&n!==2){return'one';}
return'other';},'6':function(n){if(n===0){return'zero';}
if((n%10)===1&&(n%100)!==11){return'one';}
return'other';},'7':function(n){if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'8':function(n){if((isBetween(n,3,6))){return'few';}
if((isBetween(n,7,10))){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'9':function(n){if(n===0||n!==1&&(isBetween((n%100),1,19))){return'few';}
if(n===1){return'one';}
return'other';},'10':function(n){if((isBetween((n%10),2,9))&&!(isBetween((n%100),11,19))){return'few';}
if((n%10)===1&&!(isBetween((n%100),11,19))){return'one';}
return'other';},'11':function(n){if((isBetween((n%10),2,4))&&!(isBetween((n%100),12,14))){return'few';}
if((n%10)===0||(isBetween((n%10),5,9))||(isBetween((n%100),11,14))){return'many';}
if((n%10)===1&&(n%100)!==11){return'one';}
return'other';},'12':function(n){if((isBetween(n,2,4))){return'few';}
if(n===1){return'one';}
return'other';},'13':function(n){if((isBetween((n%10),2,4))&&!(isBetween((n%100),12,14))){return'few';}
if(n!==1&&(isBetween((n%10),0,1))||(isBetween((n%10),5,9))||(isBetween((n%100),12,14))){return'many';}
if(n===1){return'one';}
return'other';},'14':function(n){if((isBetween((n%100),3,4))){return'few';}
if((n%100)===2){return'two';}
if((n%100)===1){return'one';}
return'other';},'15':function(n){if(n===0||(isBetween((n%100),2,10))){return'few';}
if((isBetween((n%100),11,19))){return'many';}
if(n===1){return'one';}
return'other';},'16':function(n){if((n%10)===1&&n!==11){return'one';}
return'other';},'17':function(n){if(n===3){return'few';}
if(n===0){return'zero';}
if(n===6){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'18':function(n){if(n===0){return'zero';}
if((isBetween(n,0,2))&&n!==0&&n!==2){return'one';}
return'other';},'19':function(n){if((isBetween(n,2,10))){return'few';}
if((isBetween(n,0,1))){return'one';}
return'other';},'20':function(n){if((isBetween((n%10),3,4)||((n%10)===9))&&!(isBetween((n%100),10,19)||isBetween((n%100),70,79)||isBetween((n%100),90,99))){return'few';}
if((n%1000000)===0&&n!==0){return'many';}
if((n%10)===2&&!isIn((n%100),[12,72,92])){return'two';}
if((n%10)===1&&!isIn((n%100),[11,71,91])){return'one';}
return'other';},'21':function(n){if(n===0){return'zero';}
if(n===1){return'one';}
return'other';},'22':function(n){if((isBetween(n,0,1))||(isBetween(n,11,99))){return'one';}
return'other';},'23':function(n){if((isBetween((n%10),1,2))||(n%20)===0){return'one';}
return'other';},'24':function(n){if((isBetween(n,3,10)||isBetween(n,13,19))){return'few';}
if(isIn(n,[2,12])){return'two';}
if(isIn(n,[1,11])){return'one';}
return'other';}};var index=locales2rules[lang.replace(/-.*$/,'')];if(!(index in pluralRules)){return function(){return'other';};}
return pluralRules[index];}
var MAX_PLACEABLES=100;var PropertiesParser={patterns:null,entryIds:null,init:function(){this.patterns={comment:/^\s*#|^\s*$/,entity:/^([^=\s]+)\s*=\s*(.*)$/,multiline:/[^\\]\\$/,index:/\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,unicode:/\\u([0-9a-fA-F]{1,4})/g,entries:/[^\r\n]+/g,controlChars:/\\([\\\n\r\t\b\f\{\}\"\'])/g,placeables:/\{\{\s*([^\s]*?)\s*\}\}/,};},parse:function(ctx,source){if(!this.patterns){this.init();}
var ast=[];this.entryIds=Object.create(null);var entries=source.match(this.patterns.entries);if(!entries){return ast;}
for(var i=0;i<entries.length;i++){var line=entries[i];if(this.patterns.comment.test(line)){continue;}
while(this.patterns.multiline.test(line)&&i<entries.length){line=line.slice(0,-1)+entries[++i].trim();}
var entityMatch=line.match(this.patterns.entity);if(entityMatch){try{this.parseEntity(entityMatch[1],entityMatch[2],ast);}catch(e){if(ctx){ctx._emitter.emit('parseerror',e);}else{throw e;}}}}
return ast;},parseEntity:function(id,value,ast){var name,key;var pos=id.indexOf('[');if(pos!==-1){name=id.substr(0,pos);key=id.substring(pos+1,id.length-1);}else{name=id;key=null;}
var nameElements=name.split('.');if(nameElements.length>2){throw new L10nError('Error in ID: "'+name+'".'+' Nested attributes are not supported.');}
var attr;if(nameElements.length>1){name=nameElements[0];attr=nameElements[1];if(attr[0]==='$'){throw new L10nError('Attribute can\'t start with "$"',id);}}else{attr=null;}
this.setEntityValue(name,attr,key,this.unescapeString(value),ast);},setEntityValue:function(id,attr,key,rawValue,ast){var pos,v;var value=rawValue.indexOf('{{')>-1?this.parseString(rawValue):rawValue;if(attr){pos=this.entryIds[id];if(pos===undefined){v={$i:id};if(key){v[attr]={};v[attr][key]=value;}else{v[attr]=value;}
ast.push(v);this.entryIds[id]=ast.length-1;return;}
if(key){if(typeof(ast[pos][attr])==='string'){ast[pos][attr]={$x:this.parseIndex(ast[pos][attr]),$v:{}};}
ast[pos][attr].$v[key]=value;return;}
ast[pos][attr]=value;return;}
if(key){pos=this.entryIds[id];if(pos===undefined){v={};v[key]=value;ast.push({$i:id,$v:v});this.entryIds[id]=ast.length-1;return;}
if(typeof(ast[pos].$v)==='string'){ast[pos].$x=this.parseIndex(ast[pos].$v);ast[pos].$v={};}
ast[pos].$v[key]=value;return;}
ast.push({$i:id,$v:value});this.entryIds[id]=ast.length-1;},parseString:function(str){var chunks=str.split(this.patterns.placeables);var complexStr=[];var len=chunks.length;var placeablesCount=(len-1)/2;if(placeablesCount>=MAX_PLACEABLES){throw new L10nError('Too many placeables ('+placeablesCount+', max allowed is '+MAX_PLACEABLES+')');}
for(var i=0;i<chunks.length;i++){if(chunks[i].length===0){continue;}
if(i%2===1){complexStr.push({t:'idOrVar',v:chunks[i]});}else{complexStr.push(chunks[i]);}}
return complexStr;},unescapeString:function(str){if(str.lastIndexOf('\\')!==-1){str=str.replace(this.patterns.controlChars,'$1');}
return str.replace(this.patterns.unicode,function(match,token){return unescape('%u'+'0000'.slice(token.length)+token);});},parseIndex:function(str){var match=str.match(this.patterns.index);if(!match){throw new L10nError('Malformed index');}
if(match[2]){return[{t:'idOrVar',v:match[1]},match[2]];}else{return[{t:'idOrVar',v:match[1]}];}}};var KNOWN_MACROS=['plural'];var MAX_PLACEABLE_LENGTH=2500;var rePlaceables=/\{\{\s*(.+?)\s*\}\}/g;var nonLatin1=/[^\x01-\xFF]/;var FSI='\u2068';var PDI='\u2069';function createEntry(node,env){var keys=Object.keys(node);if(typeof node.$v==='string'&&keys.length===2){return node.$v;}
var attrs;for(var i=0,key;key=keys[i];i++){if(key[0]==='$'){continue;}
if(!attrs){attrs=Object.create(null);}
attrs[key]=createAttribute(node[key],env,node.$i+'.'+key);}
return{id:node.$i,value:node.$v!==undefined?node.$v:null,index:node.$x||null,attrs:attrs||null,env:env,dirty:false};}
function createAttribute(node,env,id){if(typeof node==='string'){return node;}
return{id:id,value:node.$v||(node!==undefined?node:null),index:node.$x||null,env:env,dirty:false};}
function format(args,entity){if(typeof entity==='string'){return[{},entity];}
if(entity.dirty){throw new L10nError('Cyclic reference detected: '+entity.id);}
entity.dirty=true;var rv;try{rv=resolveValue({},args,entity.env,entity.value,entity.index);}finally{entity.dirty=false;}
return rv;}
function resolveIdentifier(args,env,id){if(KNOWN_MACROS.indexOf(id)>-1){return[{},env['__'+id]];}
if(args&&args.hasOwnProperty(id)){if(typeof args[id]==='string'||(typeof args[id]==='number'&&!isNaN(args[id]))){return[{},args[id]];}else{throw new L10nError('Arg must be a string or a number: '+id);}}
if(id in env&&id!=='__proto__'){return format(args,env[id]);}
throw new L10nError('Unknown reference: '+id);}
function subPlaceable(locals,args,env,id){var res;try{res=resolveIdentifier(args,env,id);}catch(err){return[{error:err},'{{ '+id+' }}'];}
var value=res[1];if(typeof value==='number'){return res;}
if(typeof value==='string'){if(value.length>=MAX_PLACEABLE_LENGTH){throw new L10nError('Too many characters in placeable ('+
value.length+', max allowed is '+
MAX_PLACEABLE_LENGTH+')');}
if(locals.contextIsNonLatin1||value.match(nonLatin1)){res[1]=FSI+value+PDI;}
return res;}
return[{},'{{ '+id+' }}'];}
function interpolate(locals,args,env,arr){return arr.reduce(function(prev,cur){if(typeof cur==='string'){return[prev[0],prev[1]+cur];}else if(cur.t==='idOrVar'){var placeable=subPlaceable(locals,args,env,cur.v);return[prev[0],prev[1]+placeable[1]];}},[locals,'']);}
function resolveSelector(args,env,expr,index){var selectorName=index[0].v;var selector=resolveIdentifier(args,env,selectorName)[1];if(typeof selector!=='function'){return selector;}
var argValue=index[1]?resolveIdentifier(args,env,index[1])[1]:undefined;if(selector===env.__plural){if(argValue===0&&'zero'in expr){return'zero';}
if(argValue===1&&'one'in expr){return'one';}
if(argValue===2&&'two'in expr){return'two';}}
return selector(argValue);}
function resolveValue(locals,args,env,expr,index){if(!expr){return[locals,expr];}
if(typeof expr==='string'||typeof expr==='boolean'||typeof expr==='number'){return[locals,expr];}
if(Array.isArray(expr)){locals.contextIsNonLatin1=expr.some(function($_){return typeof($_)==='string'&&$_.match(nonLatin1);});return interpolate(locals,args,env,expr);}
if(index){var selector=resolveSelector(args,env,expr,index);if(expr.hasOwnProperty(selector)){return resolveValue(locals,args,env,expr[selector]);}}
if('other'in expr){return resolveValue(locals,args,env,expr.other);}
throw new L10nError('Unresolvable value');}
var Resolver={createEntry:createEntry,format:format,rePlaceables:rePlaceables};function walkContent(node,fn){if(typeof node==='string'){return fn(node);}
if(node.t==='idOrVar'){return node;}
var rv=Array.isArray(node)?[]:{};var keys=Object.keys(node);for(var i=0,key;(key=keys[i]);i++){if(key==='$i'||key==='$x'){rv[key]=node[key];}else{rv[key]=walkContent(node[key],fn);}}
return rv;}
var reAlphas=/[a-zA-Z]/g;var reVowels=/[aeiouAEIOU]/g;var ACCENTED_MAP='\u0226\u0181\u0187\u1E12\u1E16\u0191\u0193\u0126\u012A'+'\u0134\u0136\u013F\u1E3E\u0220\u01FE\u01A4\u024A\u0158'+'\u015E\u0166\u016C\u1E7C\u1E86\u1E8A\u1E8E\u1E90'+'[\\]^_`'+'\u0227\u0180\u0188\u1E13\u1E17\u0192\u0260\u0127\u012B'+'\u0135\u0137\u0140\u1E3F\u019E\u01FF\u01A5\u024B\u0159'+'\u015F\u0167\u016D\u1E7D\u1E87\u1E8B\u1E8F\u1E91';var FLIPPED_MAP='\u2200\u0510\u2183p\u018E\u025F\u05E4HI\u017F'+'\u04FC\u02E5WNO\u0500\xD2\u1D1AS\u22A5\u2229\u0245'+'\uFF2DX\u028EZ'+'[\\]\u1D65_,'+'\u0250q\u0254p\u01DD\u025F\u0183\u0265\u0131\u027E'+'\u029E\u0285\u026Fuodb\u0279s\u0287n\u028C\u028Dx\u028Ez';function makeLonger(val){return val.replace(reVowels,function(match){return match+match.toLowerCase();});}
function replaceChars(map,val){return val.replace(reAlphas,function(match){return map.charAt(match.charCodeAt(0)-65);});}
var reWords=/[^\W0-9_]+/g;function makeRTL(val){return val.replace(reWords,function(match){return'\u202e'+match+'\u202c';});}
var reExcluded=/(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;function mapContent(fn,val){if(!val){return val;}
var parts=val.split(reExcluded);var modified=parts.map(function(part){if(reExcluded.test(part)){return part;}
return fn(part);});return modified.join('');}
function Pseudo(id,name,charMap,modFn){this.id=id;this.translate=mapContent.bind(null,function(val){return replaceChars(charMap,modFn(val));});this.name=this.translate(name);}
var PSEUDO={'fr-x-psaccent':new Pseudo('fr-x-psaccent','Runtime Accented',ACCENTED_MAP,makeLonger),'ar-x-psbidi':new Pseudo('ar-x-psbidi','Runtime Bidi',FLIPPED_MAP,makeRTL)};function Locale(id,ctx){this.id=id;this.ctx=ctx;this.isReady=false;this.entries=Object.create(null);this.entries.__plural=getPluralRule(this.isPseudo()?this.ctx.defaultLocale:id);}
Locale.prototype.isPseudo=function(){return this.ctx.qps.indexOf(this.id)!==-1;};var bindingsIO={extra:function(id,ver,path,type,callback,errback){if(type==='properties'){type='text';}
navigator.mozApps.getLocalizationResource(id,ver,path,type).then(callback.bind(null,null),errback);},app:function(id,ver,path,type,callback,errback,sync){switch(type){case'properties':io.load(path,callback,sync);break;case'json':io.loadJSON(path,callback,sync);break;}},};Locale.prototype.build=function L_build(callback){var sync=!callback;var ctx=this.ctx;var self=this;var l10nLoads=ctx.resLinks.length;function onL10nLoaded(err){if(err){ctx._emitter.emit('fetcherror',err);}
if(--l10nLoads<=0){self.isReady=true;if(callback){callback();}}}
if(l10nLoads===0){onL10nLoaded();return;}
function onJSONLoaded(err,json){if(!err&&json){self.addAST(json);}
onL10nLoaded(err);}
function onPropLoaded(err,source){if(!err&&source){var ast=PropertiesParser.parse(ctx,source);self.addAST(ast);}
onL10nLoaded(err);}
var idToFetch=this.isPseudo()?ctx.defaultLocale:this.id;var appVersion=null;var source='app';if(typeof(navigator)!=='undefined'){source=navigator.mozL10n._config.localeSources[this.id]||'app';appVersion=navigator.mozL10n._config.appVersion;}
for(var i=0;i<ctx.resLinks.length;i++){var resLink=decodeURI(ctx.resLinks[i]);var path=resLink.replace('{locale}',idToFetch);var type=path.substr(path.lastIndexOf('.')+1);var cb;switch(type){case'json':cb=onJSONLoaded;break;case'properties':cb=onPropLoaded;break;}
bindingsIO[source](this.id,appVersion,path,type,cb,onL10nLoaded,sync);}};function createPseudoEntry(node,entries){return Resolver.createEntry(walkContent(node,PSEUDO[this.id].translate),entries);}
Locale.prototype.addAST=function(ast){var createEntry=this.isPseudo()?createPseudoEntry.bind(this):Resolver.createEntry;for(var i=0;i<ast.length;i++){this.entries[ast[i].$i]=createEntry(ast[i],this.entries);}};function Context(id){this.id=id;this.isReady=false;this.isLoading=false;this.defaultLocale='en-US';this.availableLocales=[];this.supportedLocales=[];this.qps=[];this.resLinks=[];this.locales={};this._emitter=new EventEmitter();this._ready=new Promise(this.once.bind(this));}
function reportMissing(id,err){this._emitter.emit('notfounderror',err);return id;}
function getWithFallback(id){var cur=0;var loc;var locale;while(loc=this.supportedLocales[cur]){locale=this.getLocale(loc);if(!locale.isReady){locale.build(null);}
var entry=locale.entries[id];if(entry===undefined){cur++;reportMissing.call(this,id,new L10nError('"'+id+'"'+' not found in '+loc+' in '+this.id,id,loc));continue;}
return entry;}
throw new L10nError('"'+id+'"'+' missing from all supported locales in '+this.id,id);}
function formatTuple(args,entity){try{return Resolver.format(args,entity);}catch(err){this._emitter.emit('resolveerror',err);var locals={error:err};return[locals,entity.id];}}
function formatValue(args,entity){if(typeof entity==='string'){return entity;}
return formatTuple.call(this,args,entity)[1];}
function formatEntity(args,entity){var entityTuple=formatTuple.call(this,args,entity);var value=entityTuple[1];var formatted={value:value,attrs:null,};if(entity.attrs){formatted.attrs=Object.create(null);}
for(var key in entity.attrs){var attrTuple=formatTuple.call(this,args,entity.attrs[key]);formatted.attrs[key]=attrTuple[1];}
return formatted;}
function formatAsync(fn,id,args){return this._ready.then(getWithFallback.bind(this,id)).then(fn.bind(this,args),reportMissing.bind(this,id));}
Context.prototype.formatValue=function(id,args){return formatAsync.call(this,formatValue,id,args);};Context.prototype.formatEntity=function(id,args){return formatAsync.call(this,formatEntity,id,args);};function legacyGet(fn,id,args){if(!this.isReady){throw new L10nError('Context not ready');}
var entry;try{entry=getWithFallback.call(this,id);}catch(err){if(err.loc){throw err;}
reportMissing.call(this,id,err);return'';}
return fn.call(this,args,entry);}
Context.prototype.get=function(id,args){return legacyGet.call(this,formatValue,id,args);};Context.prototype.getEntity=function(id,args){return legacyGet.call(this,formatEntity,id,args);};Context.prototype.getLocale=function getLocale(code){var locales=this.locales;if(locales[code]){return locales[code];}
return locales[code]=new Locale(code,this);};function negotiate(available,requested,defaultLocale){var supportedLocale;for(var i=0;i<requested.length;i++){var locale=requested[i];if(available.indexOf(locale)!==-1){supportedLocale=locale;break;}}
if(!supportedLocale||supportedLocale===defaultLocale){return[defaultLocale];}
return[supportedLocale,defaultLocale];}
function freeze(supported){var locale=this.getLocale(supported[0]);if(locale.isReady){setReady.call(this,supported);}else{locale.build(setReady.bind(this,supported));}}
function setReady(supported){this.supportedLocales=supported;this.isReady=true;this._emitter.emit('ready');}
Context.prototype.registerLocales=function(defLocale,available){if(defLocale){this.defaultLocale=defLocale;}
this.availableLocales=[this.defaultLocale];this.qps=Object.keys(PSEUDO);if(available){for(var i=0,loc;loc=available[i];i++){if(this.availableLocales.indexOf(loc)===-1){this.availableLocales.push(loc);var pos=this.qps.indexOf(loc);if(pos!==-1){this.qps.splice(pos,1);}}}}};Context.prototype.requestLocales=function requestLocales(){if(this.isLoading&&!this.isReady){throw new L10nError('Context not ready');}
this.isLoading=true;var requested=Array.prototype.slice.call(arguments);if(requested.length===0){throw new L10nError('No locales requested');}
var supported=negotiate(this.availableLocales.concat(this.qps),requested,this.defaultLocale);if(this.supportedLocales[0]!==supported[0]){freeze.call(this,supported);}};Context.prototype.addEventListener=function(type,listener){this._emitter.addEventListener(type,listener);};Context.prototype.removeEventListener=function(type,listener){this._emitter.removeEventListener(type,listener);};Context.prototype.ready=function(callback){if(this.isReady){setTimeout(callback);}
this.addEventListener('ready',callback);};Context.prototype.once=function(callback){if(this.isReady){setTimeout(callback);return;}
var callAndRemove=(function(){this.removeEventListener('ready',callAndRemove);callback();}).bind(this);this.addEventListener('ready',callAndRemove);};var allowed={elements:['a','em','strong','small','s','cite','q','dfn','abbr','data','time','code','var','samp','kbd','sub','sup','i','b','u','mark','ruby','rt','rp','bdi','bdo','span','br','wbr'],attributes:{global:['title','aria-label','aria-valuetext','aria-moz-hint'],a:['download'],area:['download','alt'],input:['alt','placeholder'],menuitem:['label'],menu:['label'],optgroup:['label'],option:['label'],track:['label'],img:['alt'],textarea:['placeholder'],th:['abbr']}};var rtlList=['ar','he','fa','ps','ar-x-psbidi','ur'];var nodeObserver=null;var pendingElements=null;var moConfig={attributes:true,characterData:false,childList:true,subtree:true,attributeFilter:['data-l10n-id','data-l10n-args']};navigator.mozL10n={ctx:null,get:function get(id,ctxdata){return navigator.mozL10n.ctx.get(id,ctxdata);},formatValue:function(id,ctxdata){return navigator.mozL10n.ctx.formatValue(id,ctxdata);},formatEntity:function(id,ctxdata){return navigator.mozL10n.ctx.formatEntity(id,ctxdata);},translateFragment:function(fragment){return translateFragment.call(navigator.mozL10n,fragment);},setAttributes:setL10nAttributes,getAttributes:getL10nAttributes,ready:function ready(callback){return navigator.mozL10n.ctx.ready(callback);},once:function once(callback){return navigator.mozL10n.ctx.once(callback);},get readyState(){return navigator.mozL10n.ctx.isReady?'complete':'loading';},language:{set code(lang){navigator.mozL10n.ctx.requestLocales(lang);},get code(){return navigator.mozL10n.ctx.supportedLocales[0];},get direction(){return getDirection(navigator.mozL10n.ctx.supportedLocales[0]);}},qps:PSEUDO,_config:{appVersion:null,localeSources:Object.create(null),isPretranslated:false,},_getInternalAPI:function(){return{Error:L10nError,Context:Context,Locale:Locale,Resolver:Resolver,getPluralRule:getPluralRule,rePlaceables:rePlaceables,translateDocument:translateDocument,onMetaInjected:onMetaInjected,PropertiesParser:PropertiesParser,walkContent:walkContent,buildLocaleList:buildLocaleList};}};function getDirection(lang){return(rtlList.indexOf(lang)>=0)?'rtl':'ltr';}
var readyStates={loading:0,interactive:1,complete:2};function whenInteractive(callback){if(readyStates[document.readyState]>=readyStates.interactive){callback();return;}
document.addEventListener('readystatechange',function l10n_onrsc(){if(readyStates[document.readyState]>=readyStates.interactive){document.removeEventListener('readystatechange',l10n_onrsc);callback();}});}
function initObserver(){nodeObserver=new MutationObserver(onMutations.bind(navigator.mozL10n));nodeObserver.observe(document,moConfig);}
function init(pretranslate){if(!pretranslate){initObserver();}
initResources.call(navigator.mozL10n);}
function initResources(){var meta={};var nodes=document.head.querySelectorAll('link[rel="localization"],'+'meta[name="availableLanguages"],'+'meta[name="defaultLanguage"],'+'meta[name="appVersion"],'+'script[type="application/l10n"]');for(var i=0,node;node=nodes[i];i++){var type=node.getAttribute('rel')||node.nodeName.toLowerCase();switch(type){case'localization':this.ctx.resLinks.push(node.getAttribute('href'));break;case'meta':onMetaInjected.call(this,node,meta);break;case'script':onScriptInjected.call(this,node);break;}}
var additionalLanguagesPromise;if(navigator.mozApps&&navigator.mozApps.getAdditionalLanguages){additionalLanguagesPromise=navigator.mozApps.getAdditionalLanguages().catch(function(e){console.error('Error while loading getAdditionalLanguages',e);});document.addEventListener('additionallanguageschange',function(evt){registerLocales.call(this,meta,evt.detail);this.ctx.requestLocales.apply(this.ctx,navigator.languages||[navigator.language]);}.bind(this));}else{additionalLanguagesPromise=Promise.resolve();}
additionalLanguagesPromise.then(function(extraLangs){registerLocales.call(this,meta,extraLangs);initLocale.call(this);}.bind(this));}
function registerLocales(meta,extraLangs){var locales=buildLocaleList.call(this,meta,extraLangs);navigator.mozL10n._config.localeSources=locales[1];this.ctx.registerLocales(locales[0],Object.keys(locales[1]));}
function getMatchingLangpack(appVersion,langpacks){for(var i=0,langpack;(langpack=langpacks[i]);i++){if(langpack.target===appVersion){return langpack;}}
return null;}
function buildLocaleList(meta,extraLangs){var loc,lp;var localeSources=Object.create(null);var defaultLocale=meta.defaultLanguage||this.ctx.defaultLocale;if(meta.availableLanguages){for(loc in meta.availableLanguages){localeSources[loc]='app';}}
if(extraLangs){for(loc in extraLangs){lp=getMatchingLangpack(this._config.appVersion,extraLangs[loc]);if(!lp){continue;}
if(!(loc in localeSources)||!meta.availableLanguages[loc]||parseInt(lp.revision)>meta.availableLanguages[loc]){localeSources[loc]='extra';}}}
if(!(defaultLocale in localeSources)){localeSources[defaultLocale]='app';}
return[defaultLocale,localeSources];}
function splitAvailableLanguagesString(str){var langs={};str.split(',').forEach(function(lang){lang=lang.trim().split(':');langs[lang[0]]=parseInt(lang[1]);});return langs;}
function onMetaInjected(node,meta){switch(node.getAttribute('name')){case'availableLanguages':meta.availableLanguages=splitAvailableLanguagesString(node.getAttribute('content'));break;case'defaultLanguage':meta.defaultLanguage=node.getAttribute('content');break;case'appVersion':navigator.mozL10n._config.appVersion=node.getAttribute('content');break;}}
function onScriptInjected(node){var lang=node.getAttribute('lang');var locale=this.ctx.getLocale(lang);locale.addAST(JSON.parse(node.textContent));}
function initLocale(){this.ctx.requestLocales.apply(this.ctx,navigator.languages||[navigator.language]);window.addEventListener('languagechange',function l10n_langchange(){this.ctx.requestLocales.apply(this.ctx,navigator.languages||[navigator.language]);}.bind(this));}
function localizeMutations(mutations){var mutation;var targets=new Set();for(var i=0;i<mutations.length;i++){mutation=mutations[i];if(mutation.type==='childList'){var addedNode;for(var j=0;j<mutation.addedNodes.length;j++){addedNode=mutation.addedNodes[j];if(addedNode.nodeType!==Node.ELEMENT_NODE){continue;}
targets.add(addedNode);}}
if(mutation.type==='attributes'){targets.add(mutation.target);}}
targets.forEach(function(target){if(target.childElementCount){translateFragment.call(this,target);}else if(target.hasAttribute('data-l10n-id')){translateElement.call(this,target);}},this);}
function onMutations(mutations,self){self.disconnect();localizeMutations.call(this,mutations);self.observe(document,moConfig);}
function onReady(){if(!navigator.mozL10n._config.isPretranslated){translateDocument.call(this);}
navigator.mozL10n._config.isPretranslated=false;if(pendingElements){for(var i=0,element;element=pendingElements[i];i++){translateElement.call(this,element);}
pendingElements=null;}
if(!nodeObserver){initObserver();}
fireLocalizedEvent.call(this);}
function fireLocalizedEvent(){var event=new CustomEvent('localized',{'bubbles':false,'cancelable':false,'detail':{'language':this.ctx.supportedLocales[0]}});window.dispatchEvent(event);}
var reOverlay=/<|&#?\w+;/;var reHtml=/[&<>]/g;var htmlEntities={'&':'&amp;','<':'&lt;','>':'&gt;',};function translateDocument(){document.documentElement.lang=this.language.code;document.documentElement.dir=this.language.direction;translateFragment.call(this,document.documentElement);}
function translateFragment(element){if(typeof element.hasAttribute==='function'&&element.hasAttribute('data-l10n-id')){translateElement.call(this,element);}
var nodes=getTranslatableChildren(element);for(var i=0;i<nodes.length;i++){translateElement.call(this,nodes[i]);}}
function setL10nAttributes(element,id,args){element.setAttribute('data-l10n-id',id);if(args){element.setAttribute('data-l10n-args',JSON.stringify(args));}}
function getL10nAttributes(element){return{id:element.getAttribute('data-l10n-id'),args:JSON.parse(element.getAttribute('data-l10n-args'))};}
function getTranslatableChildren(element){return element?element.querySelectorAll('*[data-l10n-id]'):[];}
function camelCaseToDashed(string){if(string==='ariaValueText'){return'aria-valuetext';}
return string.replace(/[A-Z]/g,function(match){return'-'+match.toLowerCase();}).replace(/^-/,'');}
function escapeL10nArgs(match){return htmlEntities[match];}
function translateElement(element){if(!this.ctx.isReady){if(!pendingElements){pendingElements=[];}
pendingElements.push(element);return;}
var l10nId=element.getAttribute('data-l10n-id');if(!l10nId){return false;}
var l10nArgs=element.getAttribute('data-l10n-args');var entity=this.ctx.getEntity(l10nId,l10nArgs?JSON.parse(l10nArgs.replace(reHtml,escapeL10nArgs)):undefined);var value=entity.value;if(typeof value==='string'){if(!reOverlay.test(value)){element.textContent=value;}else{var translation=element.ownerDocument.createElement('template');translation.innerHTML=value;overlayElement(element,translation.content);}}
for(var key in entity.attrs){var attrName=camelCaseToDashed(key);if(isAttrAllowed({name:attrName},element)){element.setAttribute(attrName,entity.attrs[key]);}}}
function overlayElement(sourceElement,translationElement){var result=translationElement.ownerDocument.createDocumentFragment();var k,attr;var childElement;while((childElement=translationElement.childNodes[0])){translationElement.removeChild(childElement);if(childElement.nodeType===Node.TEXT_NODE){result.appendChild(childElement);continue;}
var index=getIndexOfType(childElement);var sourceChild=getNthElementOfType(sourceElement,childElement,index);if(sourceChild){overlayElement(sourceChild,childElement);result.appendChild(sourceChild);continue;}
if(isElementAllowed(childElement)){var sanitizedChild=childElement.ownerDocument.createElement(childElement.nodeName);overlayElement(sanitizedChild,childElement);result.appendChild(sanitizedChild);continue;}
result.appendChild(document.createTextNode(childElement.textContent));}
sourceElement.textContent='';sourceElement.appendChild(result);if(translationElement.attributes){for(k=0,attr;(attr=translationElement.attributes[k]);k++){if(isAttrAllowed(attr,sourceElement)){sourceElement.setAttribute(attr.name,attr.value);}}}}
function isElementAllowed(element){return allowed.elements.indexOf(element.tagName.toLowerCase())!==-1;}
function isAttrAllowed(attr,element){var attrName=attr.name.toLowerCase();var tagName=element.tagName.toLowerCase();if(allowed.attributes.global.indexOf(attrName)!==-1){return true;}
if(!allowed.attributes[tagName]){return false;}
if(allowed.attributes[tagName].indexOf(attrName)!==-1){return true;}
if(tagName==='input'&&attrName==='value'){var type=element.type.toLowerCase();if(type==='submit'||type==='button'||type==='reset'){return true;}}
return false;}
function getNthElementOfType(context,element,index){var nthOfType=0;for(var i=0,child;child=context.children[i];i++){if(child.nodeType===Node.ELEMENT_NODE&&child.tagName===element.tagName){if(nthOfType===index){return child;}
nthOfType++;}}
return null;}
function getIndexOfType(element){var index=0;var child;while((child=element.previousElementSibling)){if(child.tagName===element.tagName){index++;}}
return index;}
var DEBUG=false;navigator.mozL10n.ctx=new Context(window.document?document.URL:null);navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n));navigator.mozL10n.ctx.addEventListener('notfounderror',function reportMissingEntity(e){if(DEBUG||e.loc==='en-US'){console.warn(e.toString());}});if(DEBUG){navigator.mozL10n.ctx.addEventListener('fetcherror',console.error.bind(console));navigator.mozL10n.ctx.addEventListener('parseerror',console.error.bind(console));navigator.mozL10n.ctx.addEventListener('resolveerror',console.error.bind(console));}
if(window.document){navigator.mozL10n._config.isPretranslated=document.documentElement.lang===navigator.language;var forcePretranslate=!navigator.mozL10n._config.isPretranslated;whenInteractive(init.bind(navigator.mozL10n,forcePretranslate));}
document.l10n={setAttributes:navigator.mozL10n.setAttributes,getAttributes:navigator.mozL10n.getAttributes,formatValue:function(id,args){return navigator.mozL10n.formatValue(id,args);},translateFragment:function(frag){return Promise.resolve(navigator.mozL10n.translateFragment(frag));},ready:new Promise(function(resolve){navigator.mozL10n.once(resolve);}),formatValues:function(){var keys=arguments;var resp=keys.map(function(key){if(Array.isArray(key)){return navigator.mozL10n.formatValue(key[0],key[1]);}
return navigator.mozL10n.formatValue(key);});return Promise.all(resp);},requestLanguages:function(langs){navigator.mozL10n.ctx.requestLocales.apply(navigator.mozL10n.ctx,langs);},pseudo:{'fr-x-psaccent':{getName:function(){return Promise.resolve(navigator.mozL10n.qps['fr-x-psaccent'].name);},processString:function(s){return Promise.resolve(navigator.mozL10n.qps['fr-x-psaccent'].translate(s));}},'ar-x-psbidi':{getName:function(){return Promise.resolve(navigator.mozL10n.qps['ar-x-psbidi'].name);},processString:function(s){return Promise.resolve(navigator.mozL10n.qps['ar-x-psbidi'].translate(s));}}},};navigator.mozL10n.ready(function(){document.documentElement.setAttribute('langs',navigator.mozL10n.ctx.supportedLocales.join(' '));});navigator.mozL10n.once(function(){window.addEventListener('localized',function(){document.dispatchEvent(new CustomEvent('DOMRetranslated',{bubbles:false,cancelable:false}));});});})(this);

View file

@ -0,0 +1,11 @@
;;'use strict';var LazyLoader=(function(){function LazyLoader(){this._loaded={};this._isLoading={};}
LazyLoader.prototype={_js:function(file,callback){var script=document.createElement('script');script.src=file;script.async=false;script.addEventListener('load',callback);document.head.appendChild(script);this._isLoading[file]=script;},_css:function(file,callback){var style=document.createElement('link');style.type='text/css';style.rel='stylesheet';style.href=file;document.head.appendChild(style);callback();},_html:function(domNode,callback){if(domNode.getAttribute('is')){this.load(['/shared/js/html_imports.js'],function(){HtmlImports.populate(callback);}.bind(this));return;}
for(var i=0;i<domNode.childNodes.length;i++){if(domNode.childNodes[i].nodeType==document.COMMENT_NODE){domNode.innerHTML=domNode.childNodes[i].nodeValue;break;}}
window.dispatchEvent(new CustomEvent('lazyload',{detail:domNode}));callback();},getJSON:function(file,mozSystem){return new Promise(function(resolve,reject){var xhr;if(mozSystem){xhr=new XMLHttpRequest({mozSystem:true});}else{xhr=new XMLHttpRequest();}
xhr.open('GET',file,true);xhr.responseType='json';xhr.onerror=function(error){reject(error);};xhr.onload=function(){if(xhr.response!==null){resolve(xhr.response);}else{reject(new Error('No valid JSON object was found ('+
xhr.status+' '+xhr.statusText+')'));}};xhr.send();});},load:function(files,callback){var deferred={};deferred.promise=new Promise(resolve=>{deferred.resolve=resolve;});if(!Array.isArray(files)){files=[files];}
var loadsRemaining=files.length,self=this;function perFileCallback(file){if(self._isLoading[file]){delete self._isLoading[file];}
self._loaded[file]=true;if(--loadsRemaining===0){deferred.resolve();if(callback){callback();}}}
for(var i=0;i<files.length;i++){var file=files[i];if(this._loaded[file.id||file]){perFileCallback(file);}else if(this._isLoading[file]){this._isLoading[file].addEventListener('load',perFileCallback.bind(null,file));}else{var method,idx;if(typeof file==='string'){method=file.match(/\.([^.]+)$/)[1];idx=file;}else{method='html';idx=file.id;}
this['_'+method](file,perFileCallback.bind(null,idx));}}
return deferred.promise;}};return new LazyLoader();}());

View file

@ -0,0 +1,336 @@
/* globals MobileOperator, SettingsListener, SIMSlotManager */
'use strict';
(function(exports) {
var _lockedStateMsgMap = {
'unknown': 'emergencyCallsOnly-unknownSIMState',
'pinRequired': 'emergencyCallsOnly-pinRequired',
'pukRequired': 'emergencyCallsOnly-pukRequired',
'networkLocked': 'emergencyCallsOnly-networkLocked',
'serviceProviderLocked': 'emergencyCallsOnly-serviceProviderLocked',
'corporateLocked': 'emergencyCallsOnly-corporateLocked',
'network1Locked': 'emergencyCallsOnly-network1Locked',
'network2Locked': 'emergencyCallsOnly-network2Locked',
'hrpdNetworkLocked' : 'emergencyCallsOnly-hrpdNetworkLocked',
'ruimCorporateLocked' : 'emergencyCallsOnly-ruimCorporateLocked',
'ruimServiceProviderLocked' : 'emergencyCallsOnly-ruimServiceProviderLocked'
};
/*
* Types of 2G Networks
*/
var _NETWORKS_2G = ['gsm', 'gprs', 'edge'];
/**
* Constructor of LockScreenConnInfoManager. LockScreenConnInfoManager updates
* mobile connection related information on lockscreen.
*
* @param {HTMLElement} root The root element of connection states.
* @constructor LockScreenConnInfoManager
*/
var LockScreenConnInfoManager = function(root) {
if (root) {
this._initialize(root);
}
};
var LockScreenConnInfoManagerPrototype = {
_connStates: null,
_settings: null,
_cellbroadcastLabel: null,
/*
* Telephony default service ID
*/
_telephonyDefaultServiceId: 0,
/*
* Airplane mode
*/
_airplaneMode: false
};
/**
* Initialize connection state elements and event handlers.
*
* @param {HTMLElement} root The root element of connection states.
*/
LockScreenConnInfoManagerPrototype._initialize =
function lscs_initialize(root) {
this._connStates = root;
this._settings = navigator.mozSettings;
this._connStates.hidden = false;
SIMSlotManager.getSlots().forEach(function(simslot, index) {
// connection state
this._connStates.appendChild(this._createConnStateElement());
simslot.conn.addEventListener('voicechange',
(function(index) {
this.updateConnState(simslot);
}).bind(this));
}, this);
// event handlers
this.simSlotCardStateChangeListener = (function(evt) {
this.updateConnState(evt.detail);
}).bind(this);
window.addEventListener('simslot-cardstatechange',
this.simSlotCardStateChangeListener);
this.simSlotIccInfoChangeListener = (function(evt) {
this.updateConnState(evt.detail);
}).bind(this);
window.addEventListener('simslot-iccinfochange',
this.simSlotIccInfoChangeListener);
// Handle incoming CB messages that need to be displayed.
this.cellBroadcastMsgChangedListener = (function(evt) {
this._cellbroadcastLabel = evt.detail;
this.updateConnStates();
}).bind(this);
window.addEventListener('cellbroadcastmsgchanged',
this.cellBroadcastMsgChangedListener);
this._settings.addObserver('ril.radio.disabled', (function(evt) {
this._airplaneMode = evt.settingValue;
this.updateConnStates();
}).bind(this));
this._settings.addObserver('ril.telephony.defaultServiceId',
(function(evt) {
this._telephonyDefaultServiceId = evt.settingValue;
this.updateConnStates();
}).bind(this));
// update UI
var req = SettingsListener.getSettingsLock().get('ril.radio.disabled');
req.onsuccess = (function() {
this._airplaneMode = !!req.result['ril.radio.disabled'];
var req2 =
SettingsListener.getSettingsLock()
.get('ril.telephony.defaultServiceId');
req2.onsuccess = (function() {
this._telephonyDefaultServiceId =
req2.result['ril.telephony.defaultServiceId'] || 0;
this.updateConnStates();
}).bind(this);
}).bind(this);
/* Immediately update the UI, this will display default data which will
* soon be updated once the various event handlers start triggering. */
this.updateConnStates();
};
LockScreenConnInfoManagerPrototype.teardown =
function lscs_teardown() {
window.removeEventListener('simslot-cardstatechange',
this.simSlotCardStateChangeListener);
window.removeEventListener('simslot-iccinfochange',
this.simSlotIccInfoChangeListener);
window.removeEventListener('cellbroadcastmsgchanged',
this.cellBroadcastMsgChangedListener);
};
/**
* Create connection state element.
*
* @this {LockScreenConnInfoManager}
*/
LockScreenConnInfoManagerPrototype._createConnStateElement =
function lscs_createConnStateElement() {
//
// <div>
// <span></span>
// <span class="connstate-line"></span>
// <span class="connstate-line"></span>
// </div>
//
var div = document.createElement('div');
var span = document.createElement('span');
var line1 = document.createElement('span');
var line2 = document.createElement('span');
line1.className = line2.className = 'connstate-line';
div.appendChild(span);
div.appendChild(line1);
div.appendChild(line2);
return div;
};
/**
* Update states of all sim slots.
*
* @this {LockScreenConnInfoManager}
*/
LockScreenConnInfoManagerPrototype.updateConnStates =
function lscs_updateConnStates() {
SIMSlotManager.getSlots().forEach((function(simslot) {
this.updateConnState(simslot);
}).bind(this));
};
/**
* This is a helper function that uses a flag dataset.content
* to determine if the line has content or not.
*
* The content of the connstateLine may come from l10nId or manually
* injected text content.
*/
function lineText(node, l10nId, l10nArgs, text) {
if (!l10nId && !text) {
node.setAttribute('data-content', true);
} else {
node.removeAttribute('data-content');
}
if (l10nId) {
navigator.mozL10n.setAttributes(node, l10nId, l10nArgs);
} else {
node.removeAttribute('data-l10n-id');
if (text) {
node.textContent = text;
} else {
node.textContent = '';
}
}
}
/**
* Update the state of a sim slot.
*
* @param {SIMSlot} simslot
* @this {LockScreenConnInfoManager}
*/
LockScreenConnInfoManagerPrototype.updateConnState =
function lscs_updateConnState(simslot) {
var conn = simslot.conn;
var index = simslot.index;
var connstate = this._connStates.children[index];
var simIDLine = connstate.children[0];
var connstateLines =
Array.prototype.slice.call(
connstate.querySelectorAll('.connstate-line'));
var iccObj = simslot.simCard;
var voice = conn.voice;
connstate.hidden = false;
// Set sim ID line
if (SIMSlotManager.isMultiSIM()) {
simIDLine.hidden = false;
lineText(simIDLine, 'lockscreen-sim-id', {n: (index + 1)});
} else {
simIDLine.hidden = true;
}
// Reset Lines
connstateLines.forEach(function(line) {
lineText(line);
});
var nextLine = function() {
for (var i = 0; i < connstateLines.length; i++) {
var line = connstateLines[i];
if (line.hasAttribute('data-content')) {
return line;
}
}
return connstateLines[connstateLines.length - 1];
};
// Airplane mode
if (this._airplaneMode) {
// Only show one airplane mode status
if (index === 0) {
lineText(nextLine(), 'airplaneMode');
} else {
connstate.hidden = true;
}
simIDLine.hidden = true;
return;
}
// If there is no sim card on the device, we only show one information.
if (SIMSlotManager.noSIMCardOnDevice()) {
if (index === 0) {
if (voice.emergencyCallsOnly) {
lineText(nextLine(), 'emergencyCallsOnly');
lineText(nextLine(), 'emergencyCallsOnly-noSIM');
} else {
lineText(nextLine(), 'emergencyCallsOnly-noSIM');
}
}
simIDLine.hidden = true;
return;
} else if (SIMSlotManager.noSIMCardConnectedToNetwork()) {
if (index === 0) {
lineText(nextLine(), 'emergencyCallsOnly');
}
simIDLine.hidden = true;
return;
}
// If there are multiple sim slots and only one sim card inserted, we
// only show the state of the inserted sim card.
if (SIMSlotManager.isMultiSIM() &&
navigator.mozIccManager.iccIds.length == 1 &&
simslot.isAbsent()) {
connstate.hidden = true;
return;
}
// Possible value of voice.state are:
// 'notSearching', 'searching', 'denied', 'registered',
// where the latter three mean the phone is trying to grab the network.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=777057
if (voice && 'state' in voice && voice.state == 'notSearching') {
lineText(nextLine(), 'noNetwork');
return;
}
if (!voice.connected && !voice.emergencyCallsOnly) {
// "Searching"
// voice.state can be any of the latter three values.
// (it's possible that the phone is briefly 'registered'
// but not yet connected.)
lineText(nextLine(), 'searching');
return;
}
if (voice.emergencyCallsOnly) {
if (this._telephonyDefaultServiceId == index) {
lineText(nextLine(), 'emergencyCallsOnly');
lineText(nextLine(), _lockedStateMsgMap[iccObj.cardState]);
} else {
connstate.hidden = true;
}
return;
}
var operatorInfos = MobileOperator.userFacingInfo(conn);
var operator = operatorInfos.operator;
var is2G = _NETWORKS_2G.some(function checkConnectionType(elem) {
return (conn.voice.type == elem);
});
var l10nArgs;
if (voice.roaming) {
l10nArgs = { operator: operator };
lineText(nextLine(), 'roaming', l10nArgs);
} else {
lineText(nextLine(), null, null, operator);
}
if (this._cellbroadcastLabel && is2G) {
lineText(nextLine(), null, null, this._cellbroadcastLabel);
} else if (operatorInfos.carrier) {
l10nArgs = { carrier: operatorInfos.carrier,
region: operatorInfos.region };
lineText(nextLine(), 'operator-info', l10nArgs);
}
};
LockScreenConnInfoManager.prototype = LockScreenConnInfoManagerPrototype;
exports.LockScreenConnInfoManager = LockScreenConnInfoManager;
})(window);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
// Start Outer IIFE
(function(exports) {
'use strict';
// helper function, bound to manifest and property name in constructor
function ManifestHelper_get(prop) {
var manifest = this;
var value = manifest[prop];
var lang = document.documentElement.lang || '';
if (manifest.locales) {
// try to replace values from the locales object using the best language
// match. stop when a replacement is found
[lang, lang.substr(0, lang.indexOf('-'))].some(function tryLanguage(lang) {
// this === manifest.locales
if (this[lang] && this[lang][prop]) {
value = this[lang][prop];
// aborts [].some
return true;
}
}, manifest.locales);
}
// return a new ManifestHelper for any object children
if (typeof value === 'object' && !(value instanceof Array)) {
value = new ManifestHelper(value);
}
return value;
}
/**
* Helper object to access manifest information with locale support.
*
* @constructor
* @param {Object} manifest The app manifest.
*/
function ManifestHelper(manifest) {
// Bind getters for the localized property values.
for (var prop in manifest) {
Object.defineProperty(this, prop, {
get: ManifestHelper_get.bind(manifest, prop),
enumerable: true
});
}
}
/**
* Getter for display name (short_name if defined, otherwise name).
*/
Object.defineProperty(ManifestHelper.prototype, 'displayName', {
get: function displayName() {
return this.short_name || this.name;
}
});
exports.ManifestHelper = ManifestHelper;
// End outer IIFE
}(window));

View file

@ -0,0 +1,135 @@
/* exported MimeMapper */
'use strict';
/**
* MimeMapper helps gaia apps to decide the mapping of mimetype and extension.
* The use cases often happen when apps need to know about the exact
* mimetypes or extensions, such as to delegate the open web activity, we must
* have suitable mimetypes or extensions to request the right activity
*
* The mapping is basically created according to:
* http://en.wikipedia.org/wiki/Internet_media_type
*
* The supported formats are considered base on the deviceStorage properties:
* http://dxr.mozilla.org/mozilla-central/toolkit/content/
* devicestorage.properties
*
*/
var MimeMapper = {
// This list only contains the extensions we currently supported
// We should make it more complete for further usages
_typeToExtensionMap: {
// Image
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/bmp': 'bmp',
// Audio
'audio/mpeg': 'mp3',
'audio/mp4': 'm4a',
'audio/ogg': 'ogg',
'audio/webm': 'webm',
'audio/3gpp': '3gp',
'audio/amr': 'amr',
// Video
'video/mp4': 'mp4',
'video/mpeg': 'mpg',
'video/ogg': 'ogg',
'video/webm': 'webm',
'video/3gpp': '3gp',
'video/3gpp2': '3g2',
// Application
// If we want to support some types, like pdf, just add
'application/pdf': 'pdf',
'application/vcard': 'vcf',
// Text
'text/vcard': 'vcf',
'text/x-vcard': 'vcf'
},
// This list only contains the mimetypes we currently supported
// We should make it more complete for further usages
_extensionToTypeMap: {
// Image
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpe': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'bmp': 'image/bmp',
// Audio
'mp3': 'audio/mpeg',
'm4a': 'audio/mp4',
'm4b': 'audio/mp4',
'm4p': 'audio/mp4',
'm4r': 'audio/mp4',
'aac': 'audio/aac',
'opus': 'audio/ogg',
'amr': 'audio/amr',
// Video
'mp4': 'video/mp4',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'ogv': 'video/ogg',
'ogx': 'video/ogg',
'webm': 'video/webm',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
'ogg': 'video/ogg',
// Application
// If we want to support some extensions, like pdf, just add
'pdf': 'application/pdf',
// Text
'vcf': 'text/vcard'
},
_parseExtension: function(filename) {
var array = filename.split('.');
return array.length > 1 ? array.pop() : '';
},
isSupportedType: function(mimetype) {
return (mimetype in this._typeToExtensionMap);
},
isSupportedExtension: function(extension) {
return (extension in this._extensionToTypeMap);
},
isFilenameMatchesType: function(filename, mimetype) {
var extension = this._parseExtension(filename);
var guessedType = this.guessTypeFromExtension(extension);
return (guessedType == mimetype);
},
guessExtensionFromType: function(mimetype) {
return this._typeToExtensionMap[mimetype];
},
guessTypeFromExtension: function(extension) {
return this._extensionToTypeMap[extension];
},
// If mimetype is not in the supported list, we will try to
// predict the possible valid mimetype based on extension.
guessTypeFromFileProperties: function(filename, mimetype) {
var extension = this._parseExtension(filename);
var type = this.isSupportedType(mimetype) ?
mimetype : this.guessTypeFromExtension(extension);
return type || '';
},
// if mimetype is not supported, preserve the original extension
// and add the predict result as new extension.
// If both filename and mimetype are not supported, return the original
// filename.
ensureFilenameMatchesType: function(filename, mimetype) {
if (!this.isFilenameMatchesType(filename, mimetype)) {
var guessedExt = this.guessExtensionFromType(mimetype);
if (guessedExt) {
filename += '.' + guessedExt;
}
}
return filename;
}
};

View file

@ -0,0 +1,100 @@
/* exported MobileInfo, MobileOperator */
'use strict';
var MobileInfo = {
brazil: {
carriers: {
'00': 'NEXTEL',
'02': 'TIM', '03': 'TIM', '04': 'TIM',
'05': 'CLARO', '06': 'VIVO', '07': 'CTBC', '08': 'TIM',
'10': 'VIVO', '11': 'VIVO', '15': 'SERCOMTEL',
'16': 'OI', '23': 'VIVO', '24': 'OI', '31': 'OI',
'32': 'CTBC', '33': 'CTBC', '34': 'CTBC', '37': 'AEIOU'
},
regions: {
'11': 'SP', '12': 'SP', '13': 'SP', '14': 'SP', '15': 'SP', '16': 'SP',
'17': 'SP', '18': 'SP', '19': 'SP',
'21': 'RJ', '22': 'RJ', '24': 'RJ',
'27': 'ES', '28': 'ES',
'31': 'MG', '32': 'MG', '33': 'MG', '34': 'MG', '35': 'MG', '37': 'MG',
'38': 'MG',
'41': 'PR', '42': 'PR', '43': 'PR', '44': 'PR', '45': 'PR', '46': 'PR',
'47': 'SC', '48': 'SC', '49': 'SC',
'51': 'RS', '53': 'RS', '54': 'RS', '55': 'RS',
'61': 'DF',
'62': 'GO',
'63': 'TO',
'64': 'GO',
'65': 'MT', '66': 'MT',
'67': 'MS',
'68': 'AC',
'69': 'RO',
'71': 'BA', '73': 'BA', '74': 'BA', '75': 'BA', '77': 'BA',
'79': 'SE',
'81': 'PE',
'82': 'AL',
'83': 'PB',
'84': 'RN',
'85': 'CE',
'86': 'PI',
'87': 'PE',
'88': 'CE',
'89': 'PI',
'91': 'PA',
'92': 'AM',
'93': 'PA', '94': 'PA',
'95': 'RR',
'96': 'AP',
'97': 'AM',
'98': 'MA', '99': 'MA'
}
}
};
var MobileOperator = {
BRAZIL_MCC: '724',
BRAZIL_CELLBROADCAST_CHANNEL: 50,
userFacingInfo: function mo_userFacingInfo(mobileConnection) {
var network = mobileConnection.voice.network;
var iccid = mobileConnection.iccId;
var iccObj = navigator.mozIccManager.getIccById(iccid);
var iccInfo = iccObj ? iccObj.iccInfo : null;
var operator = network ? (network.shortName || network.longName) : null;
if (operator && iccInfo && iccInfo.isDisplaySpnRequired && iccInfo.spn) {
if (iccInfo.isDisplayNetworkNameRequired && operator !== iccInfo.spn) {
operator = operator + ' ' + iccInfo.spn;
} else {
operator = iccInfo.spn;
}
}
var carrier, region;
if (this.isBrazil(mobileConnection)) {
// We are in Brazil, It is legally required to show local info
// about current registered GSM network in a legally specified way.
var lac = mobileConnection.voice.cell.gsmLocationAreaCode % 100;
var carriers = MobileInfo.brazil.carriers;
var regions = MobileInfo.brazil.regions;
carrier = carriers[network.mnc] ||
(this.BRAZIL_MCC + network.mnc);
region = (regions[lac] ? regions[lac] + ' ' + lac : '');
}
return {
'operator': operator,
'carrier': carrier,
'region': region
};
},
isBrazil: function mo_isBrazil(mobileConnection) {
var cell = mobileConnection.voice.cell;
var net = mobileConnection.voice.network;
return net ?
(net.mcc === this.BRAZIL_MCC && cell && cell.gsmLocationAreaCode) :
null;
}
};

View file

@ -0,0 +1,35 @@
;(function(global){'use strict';global.mozIntl={formatList:function(list){return navigator.mozL10n.formatValue('listSeparator_middle').then(sep=>list.join(sep));},DateTimeFormat:function(locales,options){const resolvedOptions=Object.assign({},options);if(resolvedOptions.dayperiod){if(resolvedOptions.hour===undefined){resolvedOptions.hour='numeric';}}
if(resolvedOptions.hour!==undefined&&resolvedOptions.hour12===undefined){resolvedOptions.hour12=navigator.mozHour12;}
if(resolvedOptions.dayperiod===undefined&&resolvedOptions.hour12===true){resolvedOptions.dayperiod=true;}
var intlFormat=new Intl.DateTimeFormat(locales,resolvedOptions);resolvedOptions.locale=intlFormat.resolvedOptions().locale;resolvedOptions.hour12=intlFormat.resolvedOptions().hour12;var hourFormatter;if(resolvedOptions.dayperiod!==undefined&&resolvedOptions.hour12===true){hourFormatter=Intl.DateTimeFormat(locales,{hour:'numeric',hour12:false});}
return{resolvedOptions(){return resolvedOptions;},format:function(date,tokenFormats){var dayPeriod;var string=intlFormat.format(date);if(resolvedOptions.dayperiod===false&&resolvedOptions.hour12===true){dayPeriod=getDayPeriodTokenForDate(date,hourFormatter);string=string.replace(dayPeriod,'').trim();}else if(resolvedOptions.dayperiod===true&&options.hour===undefined){dayPeriod=getDayPeriodTokenForDate(date,hourFormatter);const hour=date.toLocaleString(navigator.languages,{hour12:true,hour:'numeric'}).replace(dayPeriod,'').trim();string=string.replace(hour,'').trim();}
for(var token in tokenFormats){if(token==='dayperiod'&&resolvedOptions.hour12===false){continue;}
const localOptions={[token]:resolvedOptions[token],};var formatter=global.mozIntl.DateTimeFormat(navigator.languages,localOptions);var tokenString=formatter.format(date);string=string.replace(tokenString,tokenFormats[token]);}
return string;},};},calendarInfo:function(token){switch(token){case'firstDayOfTheWeek':return navigator.mozL10n.formatValue('firstDayOfTheWeek').then(firstDayOfTheWeek=>parseInt(firstDayOfTheWeek)%7);default:throw new Error('Unknown token: '+token);}},DurationFormat:function(locales=navigator.languages,options={}){const resolvedOptions=Object.assign({locale:locales[0],maxUnit:'hour',minUnit:'second',},options);const numFormatter=Intl.NumberFormat(locales,{style:'decimal',useGrouping:false,minimumIntegerDigits:2});const maxUnitIdx=getDurationUnitIdx(resolvedOptions.maxUnit,0);const minUnitIdx=getDurationUnitIdx(resolvedOptions.minUnit,durationFormatOrder.length-1);return navigator.mozL10n.formatValue('durationPattern').then(fmt=>({resolvedOptions:function(){return resolvedOptions;},format:function(input){const minValue=durationFormatElements[resolvedOptions.minUnit].value;input=Math.round(input/minValue)*minValue;const duration=splitIntoTimeUnits(input,maxUnitIdx,minUnitIdx);var string=trimDurationPattern(fmt,resolvedOptions.maxUnit,resolvedOptions.minUnit);for(var unit in duration){const token=durationFormatElements[unit].token;string=string.replace(token,numFormatter.format(duration[unit]));}
if(input<0){return'-'+string;}
return string;}}));},RelativeTimeFormat:function(locales,options){return{resolvedOptions:function(){return options;},format:function(x){const{unit,value}=relativeTimeFormatId(x,options);return navigator.mozL10n.formatValue(unit,{value});},};},UnitFormat:function(locales,options){const unitGroup=getUnitFormatGroupName(options.unit);if(unitGroup===undefined){throw new RangeError(`invalid value ${options.unit} for option unit`);}
if(!unitFormatGroups[unitGroup].styles.includes(options.style)){throw new RangeError(`invalid value ${options.style} for option style`);}
const unit=`${unitGroup}-${options.unit}-${options.style}`;return{format:function(x){return document.l10n.formatValue(unit,{value:x});},};},_gaia:{relativePart:function(milliseconds){const units=computeTimeUnits(milliseconds);const unit=getBestMatchUnit(units);return{unit:unit+'s',value:Math.abs(units[unit])};},RelativeDate:function(locales,options){const style=options&&options.style||'long';const maxFormatter=Intl.DateTimeFormat(locales,{year:'numeric',month:'numeric',day:'numeric'});const relativeFmtOptions={unit:'bestFit',style:style,minUnit:'minute',};return{format:function(time,maxDiff){maxDiff=maxDiff||86400*10;const secDiff=(Date.now()-time)/1000;if(isNaN(secDiff)){return navigator.mozL10n.formatValue('incorrectDate');}
if(secDiff>maxDiff){return Promise.resolve(maxFormatter.format(time));}
const{unit,value}=relativeTimeFormatId(time,relativeFmtOptions);return navigator.mozL10n.formatValue(unit,{value});},formatElement:function(element,time,maxDiff){maxDiff=maxDiff||86400*10;const secDiff=(Date.now()-time)/1000;if(isNaN(secDiff)){element.setAttribute('data-l10n-id','incorrectDate');}
element.removeAttribute('data-l10n-id');if(secDiff>maxDiff){element.textContent=maxFormatter.format(time);}
const{unit,value}=relativeTimeFormatId(time,relativeFmtOptions);navigator.mozL10n.setAttributes(element,unit,{value});},};},getFormattedUnit:function(type,style,v){if(!unitFormatData.hasOwnProperty(type)){throw new RangeError(`invalid type ${type}`);}
if(!unitFormatGroups[type].styles.includes(style)){throw new RangeError(`invalid style ${style} for type ${type}`);}
var units=unitFormatData[type];var scale=0;for(let i=1;i<units.length;i++){if(v<units[i].value*unitFormatGroups[type].rounding){scale=i-1;break;}else if(i===units.length-1){scale=i;}}
var value=Math.round(v/units[scale].value*100)/100;return global.mozIntl.UnitFormat(navigator.languages,{unit:units[scale].name,style:style}).format(value);},}};const durationFormatOrder=['hour','minute','second','millisecond'];const durationFormatElements={'hour':{value:3600000,token:'hh'},'minute':{value:60000,token:'mm'},'second':{value:1000,token:'ss'},'millisecond':{value:10,token:'SS'}};const unitFormatData={'duration':[{'name':'second','value':1},{'name':'minute','value':60},{'name':'hour','value':60*60},{'name':'day','value':24*60*60},{'name':'month','value':30*24*60*60},],'digital':[{'name':'byte','value':1},{'name':'kilobyte','value':1024},{'name':'megabyte','value':1024*1024},{'name':'gigabyte','value':1024*1024*1024},{'name':'terabyte','value':1024*1024*1024*1024},],};const unitFormatGroups={'duration':{'units':['second','minute','hour','day','month'],'styles':['narrow'],'rounding':1},'digital':{'units':['byte','kilobyte','megabyte','gigabyte','terabyte'],'styles':['short'],'rounding':0.8}};function getDurationUnitIdx(name,defaultValue){if(!name){return defaultValue;}
const pos=durationFormatOrder.indexOf(name);if(pos===-1){throw new Error('Unknown unit type: '+name);}
return pos;}
function splitIntoTimeUnits(v,maxUnitIdx,minUnitIdx){const units={};var input=Math.abs(v);for(var i=maxUnitIdx;i<=minUnitIdx;i++){const key=durationFormatOrder[i];const{value}=durationFormatElements[key];units[key]=i==minUnitIdx?Math.round(input/value):Math.floor(input/value);input-=units[key]*value;}
return units;}
function trimDurationPattern(string,maxUnit,minUnit){const maxToken=durationFormatElements[maxUnit].token;const minToken=durationFormatElements[minUnit].token;string=string.substring(string.indexOf(maxToken),string.indexOf(minToken)+minToken.length);return string;}
function getDayPeriodTokenForDate(date,hourFormatter){const hourToken=hourFormatter.format(date);const newDate=new Date(date);newDate.setHours(parseInt(hourToken));return new Intl.DateTimeFormat(navigator.language, {hour:'numeric', hour12:true}).formatToParts(newDate).find(part => part.type === 'dayPeriod').value;}
function computeTimeUnits(v){const units={};const millisecond=Math.round(v);const second=Math.round(millisecond/1000);const minute=Math.round(second/60);const hour=Math.round(minute/60);const day=Math.round(hour/24);const rawYear=day*400/146097;units.millisecond=millisecond;units.second=second;units.minute=minute;units.hour=hour;units.day=day;units.week=Math.round(day/7);units.month=Math.round(rawYear*12);units.quarter=Math.round(rawYear*4);units.year=Math.round(rawYear);return units;}
function getBestMatchUnit(units){if(Math.abs(units.minute)<45){return'minute';}
if(Math.abs(units.hour)<22){return'hour';}
if(Math.abs(units.day)<7){return'day';}
if(Math.abs(units.week)<4){return'week';}
if(Math.abs(units.month)<11){return'month';}
return'year';}
function relativeTimeFormatId(x,options){const ms=x-Date.now();const units=computeTimeUnits(ms);const unit=options.unit==='bestFit'?getBestMatchUnit(units):options.unit;const v=units[unit];const tl=v<0?'-ago':'-until';const style=options.style||'long';const entry=unit+'s'+tl+'-'+style;return{unit:entry,value:Math.abs(v)};}
function getUnitFormatGroupName(unitName){for(let groupName in unitFormatGroups){if(unitFormatGroups[groupName].units.includes(unitName)){return groupName;}}
return undefined;}})(this);

View file

@ -0,0 +1,671 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright © 2013, Deutsche Telekom, Inc. */
/* globals MozNDEFRecord, TextEncoder, TextDecoder, StringHelper */
/* exported NDEF, NfcBuffer, NfcUtils */
'use strict';
(function(exports) {
/**
* NDEF (NFC Data Exchange Format) contants and common values.
*/
const NDEF = {
MB: 1 << 7,
ME: 1 << 6,
CF: 1 << 5,
SR: 1 << 4,
IL: 1 << 3,
TNF: 0x07,
TNF_EMPTY: 'empty',
TNF_WELL_KNOWN: 'well-known',
TNF_MIME_MEDIA: 'media-type',
TNF_ABSOLUTE_URI: 'absolute-uri',
TNF_EXTERNAL_TYPE: 'external',
TNF_UNKNOWN: 'unknown',
TNF_UNCHANGED: 'unchanged',
RTD_TEXT: 0,
RTD_URI: 0,
RTD_SMART_POSTER: 0,
RTD_ALTERNATIVE_CARRIER: 0,
RTD_COLLISION_RESOLUTION: 0,
RTD_HANDOVER_CARRIER: 0,
RTD_HANDOVER_REQUEST: 0,
RTD_HANDOVER_SELECT: 0,
RTD_TEXT_IANA_LENGTH: 0x3F,
RTD_TEXT_ENCODING: 0x80,
RTD_TEXT_UTF8: 0,
RTD_TEXT_UTF16: 1,
CPS_INACTIVE: 0,
CPS_ACTIVE: 1,
CPS_ACTIVATING: 2,
CPS_UNKNOWN: 3,
MIME_BLUETOOTH_OOB: 0,
SMARTPOSTER_ACTION: 0,
// Action Record Values:
DO_ACTION: 0x00,
SAVE_FOR_LATER_ACTION: 0x01,
OPEN_FOR_EDITING_ACTION: 0x02,
RFU_ACTION: 0x03, // Reserved from 0x03 to 0xFF
URIS: [],
init: function ndef_init() {
var nfcUtils = new NfcUtils();
this.RTD_TEXT = nfcUtils.fromUTF8('T');
this.RTD_URI = nfcUtils.fromUTF8('U');
this.RTD_SMART_POSTER = nfcUtils.fromUTF8('Sp');
this.RTD_ALTERNATIVE_CARRIER = nfcUtils.fromUTF8('ac');
this.RTD_COLLISION_RESOLUTION = nfcUtils.fromUTF8('cr');
this.RTD_HANDOVER_CARRIER = nfcUtils.fromUTF8('Hc');
this.RTD_HANDOVER_REQUEST = nfcUtils.fromUTF8('Hr');
this.RTD_HANDOVER_SELECT = nfcUtils.fromUTF8('Hs');
this.MIME_BLUETOOTH_OOB =
nfcUtils.fromUTF8('application/vnd.bluetooth.ep.oob');
this.MIME_VCARD_STR_ARR =
['text/vcard', 'text/x-vCard', 'text/x-vcard'];
this.SMARTPOSTER_ACTION = nfcUtils.fromUTF8('act');
this.URIS[0x00] = '';
this.URIS[0x01] = 'http://www.';
this.URIS[0x02] = 'https://www.';
this.URIS[0x03] = 'http://';
this.URIS[0x04] = 'https://';
this.URIS[0x05] = 'tel:';
this.URIS[0x06] = 'mailto:';
this.URIS[0x07] = 'ftp://anonymous:anonymous@';
this.URIS[0x08] = 'ftp://ftp.';
this.URIS[0x09] = 'ftps://';
this.URIS[0x0A] = 'sftp://';
this.URIS[0x0B] = 'smb://';
this.URIS[0x0C] = 'nfs://';
this.URIS[0x0D] = 'ftp://';
this.URIS[0x0E] = 'dav://';
this.URIS[0x0F] = 'news:';
this.URIS[0x10] = 'telnet://';
this.URIS[0x11] = 'imap:';
this.URIS[0x12] = 'rtsp://';
this.URIS[0x13] = 'urn:';
this.URIS[0x14] = 'pop:';
this.URIS[0x15] = 'sip:';
this.URIS[0x16] = 'sips:';
this.URIS[0x17] = 'tftp:';
this.URIS[0x18] = 'btspp://';
this.URIS[0x19] = 'btl2cap://';
this.URIS[0x1A] = 'btgoep://';
this.URIS[0x1B] = 'tcpobex://';
this.URIS[0x1C] = 'irdaobex://';
this.URIS[0x1D] = 'file://';
this.URIS[0x1E] = 'urn:epc:id:';
this.URIS[0x1F] = 'urn:epc:tag:';
this.URIS[0x20] = 'urn:epc:pat:';
this.URIS[0x21] = 'urn:epc:raw:';
this.URIS[0x22] = 'urn:epc:';
this.URIS[0x23] = 'urn:nfc:';
},
payload: {
/**
* Decodes NDEF record payload
* @see NFCForum-TS-NDEF_1.0
* @param String tnf - record TNF
* @param {Uint8Array} type - record type
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decode: function decode(tnf, type, payload) {
var nfcUtils = new NfcUtils();
var decodedPayload = { type: 'empty' };
switch (tnf) {
case NDEF.TNF_WELL_KNOWN:
decodedPayload = this.decodeWellKnown(type, payload);
break;
case NDEF.TNF_MIME_MEDIA:
decodedPayload = this.decodeMIME(type, payload);
break;
case NDEF.TNF_ABSOLUTE_URI:
case NDEF.TNF_EXTERNAL_TYPE:
decodedPayload = { type: nfcUtils.toUTF8(type) };
break;
case NDEF.TNF_UNKNOWN:
decodedPayload = {};
break;
case NDEF.TNF_UNCHANGED:
decodedPayload = null;
break;
}
return decodedPayload;
},
/**
* Decodes TNF Well Know NDEF record payload
* @see NFCForum-TS-NDEF_1.0
* @param {Uint8Array} type - record type
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decodeWellKnown: function decodeWellKnown(type, payload) {
var nfcUtils = new NfcUtils();
if (nfcUtils.equalArrays(type, NDEF.RTD_TEXT)) {
return this.decodeText(payload);
} else if (nfcUtils.equalArrays(type, NDEF.RTD_URI)) {
return this.decodeURI(payload);
} else if (nfcUtils.equalArrays(type, NDEF.RTD_SMART_POSTER)) {
return this.decodeSmartPoster(payload);
}
return null;
},
/**
* Decodes TNF Well Known RTD Text NDEF record payload
* @see NFCForum-TS-RTD_Text_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload
*/
decodeText: function decodeText(payload) {
var nfcUtils = new NfcUtils();
var decoded = { type: 'text' };
var langLen = payload[0] & NDEF.RTD_TEXT_IANA_LENGTH;
decoded.language = nfcUtils.toUTF8(payload.subarray(1, langLen + 1));
var encoding = (payload[0] & NDEF.RTD_TEXT_ENCODING) !== 0 ? 1 : 0;
if (encoding === NDEF.RTD_TEXT_UTF8) {
decoded.text = nfcUtils.toUTF8(payload.subarray(langLen + 1));
decoded.encoding = 'UTF-8';
} else if (encoding === NDEF.RTD_TEXT_UTF16) {
decoded.text = nfcUtils.UTF16BytesToStr(payload.subarray(langLen +
1));
decoded.encoding = 'UTF-16';
}
return decoded;
},
/**
* Decodes TNF Well Known RTD URI NDEF record payload
* @see NFCForum-TS-RTD_URI_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decodeURI: function decodeURI(payload) {
var nfcUtils = new NfcUtils();
var prefix = NDEF.URIS[payload[0]];
if (prefix === undefined) {
return null;
}
var suffix = nfcUtils.toUTF8(payload.subarray(1));
return { type: 'uri', uri: prefix + suffix };
},
/**
* Decodes TNF Well Known RTD Smart Poster NDEF record payload
* @see NFCForum-SmartPoster_RTD_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload
*/
decodeSmartPoster: function decodeSmartPoster(payload) {
var nfcUtils = new NfcUtils();
var buffer = new NfcBuffer(payload);
var records = nfcUtils.parseNDEF(buffer);
// First, decode URI. It's treated specially, because it's the only
// mandatory record in a smart poster.
var URIRecords = records.filter(function(record) {
return nfcUtils.equalArrays(record.type, NDEF.RTD_URI);
});
if (URIRecords.length !== 1) {
return null;
}
var uriPoster = {
type: 'smartposter',
uri: NDEF.payload.decodeURI(URIRecords[0].payload).uri
};
// Now decode all other records and attach their data to poster.
return records.reduce((poster, record) => {
var typeStr = nfcUtils.toUTF8(record.type);
if (nfcUtils.equalArrays(record.type, NDEF.RTD_TEXT)) {
poster.text = poster.text || {};
var textData = NDEF.payload.decodeText(record.payload);
if (poster.text[textData.language]) {
// According to NFCForum-SmartPoster_RTD_1.0 3.3.2,
// there MUST NOT be two or more records with
// the same language identifier.
return null;
}
poster.text[textData.language] = textData.text;
} else if ('act' === typeStr) {
poster.action = record.payload[0];
} else if (NDEF.TNF_MIME_MEDIA === record.tnf) {
poster.icons = poster.icons || [];
poster.icons.push({
type: nfcUtils.toUTF8(record.type),
bytes: record.payload
});
}
return poster;
}, uriPoster);
},
/**
* Decodes TNF MIME Media NDEF Record payload
* @see NFCForum-TS-NDEF_1.0
* @param {Uint8Array} type - record mime-type
* @returns {Object} data - decoded payload
*/
decodeMIME: function docodeMIME(type, payload) {
var nfcUtils = new NfcUtils();
var typeStr = (typeof type === 'string') ? type : nfcUtils.toUTF8(type);
if (NDEF.MIME_VCARD_STR_ARR.indexOf(typeStr) !== -1) {
return {
type: 'text/vcard',
blob: new Blob([nfcUtils.toUTF8(payload)], {type: 'text/vcard'})
};
}
return { type: typeStr };
}
}
};
/**
* NfcBuffer wraps Uint8Array providing helper methods for accessing
* its elements.
*
* @param {Array} array Either a plain Array or an Uint8Array instance.
* @constructor
*/
function NfcBuffer(array) {
this.uint8array = new Uint8Array(array);
this.offset = 0;
}
NfcBuffer.prototype = {
uint8array: null,
offset: 0,
/**
* Returns a single octet from the buffer advancing position
* by one.
*
* @returns {Number} Single element from the buffer.
* @memberof NfcBuffer
*/
getOctet: function getOctet() {
if (this.offset == this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array[this.offset++];
},
/**
* Returns an Uint8Array containing first len elements from the
* buffer.
*
* @param {Number} len Number of elements to return.
* @returns {Uint8Array} Array of len first elements from the buffer.
* @memberof NfcBuffer
*/
getOctetArray: function getOctetArray(len) {
if (typeof len !== 'number' || len < 0 ||
this.offset + len > this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array.subarray(this.offset, this.offset += len);
},
/**
* Discards len elements from the buffer.
*
* @param {Number} len Number of elements to skip over.
* @memberof NfcBuffer
*/
skip: function skip(len) {
if (typeof len !== 'number' || len < 0 ||
this.offset + len > this.uint8array.length) {
throw Error('NfcBuffer too small');
}
this.offset += len;
},
/**
* Returns a single octet from the buffer, but will not
* advance a position.
*
* @returns {Number} Single element from the buffer.
* @memberof NfcBuffer
*/
peek: function peek() {
if (this.offset === this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array[this.offset];
}
};
/**
* NfcUtils offers a set of utility function to handle NDEF messages according
* to NFCForum-TS-NDEF_1.0.
*
* @class NfcUtils
*/
function NfcUtils() { }
NfcUtils.prototype = {
/**
* Returns an Uint8Array representation of a string.
*
* @param {String} str String to convert.
* @return {Uint8Array}
* @memberof NfcUtils
*/
fromUTF8: function fromUTF8(str) {
if (!str) {
return null;
}
var enc = new TextEncoder('utf-8');
return enc.encode(str);
},
/**
* Returns a string representation of an Uint8Array.
*
* @param {Uint8Array} a Uint8Array instance.
* @return {String}
* @memberof NfcUtils
*/
toUTF8: function toUTF8(a) {
if (!a) {
return null;
}
// BOM removal
if (this.equalArrays(a.subarray(0, 3), [0xEF, 0xBB, 0xBF])) {
a = a.subarray(3);
}
var dec = new TextDecoder('utf-8');
return dec.decode(a);
},
/**
* Decodes UTF-16 bytes array into a String
*
* @param {Uint8Array} array containing UTF-16 encoded bytes
* @return {string}
* @memberof NfcUtils
*/
UTF16BytesToStr: function UTF16BytesToStr(array) {
if (!array) {
return null;
}
// if BOM not present Big-endian should be used
// NFCForum-TS-RTD_Text_1.0
var le = false;
var possibleBom = array.subarray(0, 2);
if (this.equalArrays(possibleBom, [0xFF, 0xFE])) {
array = array.subarray(2);
le = true;
} else if (this.equalArrays(possibleBom, [0xFE, 0xFF])) {
array = array.subarray(2);
}
var encoding = (le) ? 'utf-16le' : 'utf-16be';
var dec = new TextDecoder(encoding);
return dec.decode(array);
},
/**
* Ecodes string into Uint8Array conating UTF-16 BE bytes without BOM.
* @param {string}
* @return {Uint8Array}
* @memberof NfcUtils
*/
strToUTF16Bytes: function strToUTF16Bytes(str) {
if (!str) {
return null;
}
var enc = new TextEncoder('utf-16be');
return enc.encode(str);
},
/**
* Compares arrays, array-like objects and typed arrays for equality.
*
* @param {Array} a1 First array
* @param {Array} a2 Second array
* @return {Boolean} True if arrays are equal, false otherwise.
* @memberof NfcUtils
*/
equalArrays: function equalArrays(a1, a2) {
if (!a1 || !a2) {
return false;
}
if (a1.length !== a2.length) {
return false;
}
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) {
return false;
}
}
return true;
},
/**
* Takes an array of NDEFRecords and returns an Array of octets
* representing the binary encoding of the NDEF message according
* to the NFC Forum.
* Note: support for message chunking is not implemented.
*
* @param {Array} ndefRecords Array of NDEF records to encode.
* @return {Array} Byte array containing encoded NDEF message.
* @memberof NfcUtils
*/
encodeNDEF: function encodeNDEF(records) {
var result = [];
function getTnfNum(tnfString) {
var tnf = [
NDEF.TNF_EMPTY, NDEF.TNF_WELL_KNOWN, NDEF.TNF_MIME_MEDIA,
NDEF.TNF_ABSOLUTE_URI, NDEF.TNF_EXTERNAL_TYPE, NDEF.TNF_UNKNOWN,
NDEF.TNF_UNCHANGED, NDEF.TNF_RESERVED];
return tnf.indexOf(tnfString);
}
records.forEach((record, recordIndex) => {
record.payload = record.payload || [];
record.id = record.id || [];
record.type = record.type || [];
var payloadLen = record.payload.length;
var idLen = record.id.length;
var typeLen = record.type.length;
var firstOctet = getTnfNum(record.tnf) & 0x07;
firstOctet |= (recordIndex === 0) ? NDEF.MB : 0;
firstOctet |= (recordIndex === records.length - 1) ? NDEF.ME : 0;
firstOctet |= (idLen > 0) ? NDEF.IL : 0;
firstOctet |= (payloadLen <= 0xFF) ? NDEF.SR : 0;
result.push(firstOctet);
result.push(typeLen);
for (var p = (payloadLen > 0xFF ? 3 : 0); p >= 0; p -= 1) {
result.push((payloadLen >>> (8 * p)) & 0xFF);
}
if (idLen > 0) {
result.push(idLen);
}
result.push.apply(result, record.type);
result.push.apply(result, record.id);
result.push.apply(result, record.payload);
});
return result;
},
/**
* Parses a NDEF message contained in a NfcBuffer instance according to
* NFCForum-TS-NDEF_1.0.
*
* Usage:
* var buf = new NfcBuffer(<Uint8Array that contains the raw NDEF
* message>);
* var ndefMessage = NdefCodec.parse(buf);
*
* @param {NfcBuffer} Buffer containing the NDEF message.
* @return {Array} Array of MozNDEFRecord instances or null if message
* couldn't be parsed.
* @memberof NfcUtils
*/
parseNDEF: function parseNDEF(buffer) {
try {
return this._doParseNDEF(buffer);
} catch (err) {
console.error('[NfcUtils]: ' + err);
return null;
}
},
_doParseNDEF: function doParseNDEF(buffer) {
var records = [];
var isFirstRecord = true;
var firstOctet;
do {
firstOctet = buffer.peek();
if (isFirstRecord && !(firstOctet & NDEF.MB)) {
throw Error('MB bit not set in first NDEF record');
}
if (!isFirstRecord && (firstOctet & NDEF.MB)) {
throw Error('MB can only be set for the first record');
}
if (firstOctet & NDEF.CF) {
throw Error('Chunked payloads are not supported');
}
records.push(this._parseNDEFRecord(buffer));
isFirstRecord = false;
} while (!(firstOctet & NDEF.ME));
if (buffer.offset < buffer.uint8array.length) {
throw Error('ME bit set on non-last record');
}
return records;
},
_parseNDEFRecord: function parseNDEFRecord(buffer) {
var tnfArray = [
NDEF.TNF_EMPTY, NDEF.TNF_WELL_KNOWN, NDEF.TNF_MIME_MEDIA,
NDEF.TNF_ABSOLUTE_URI, NDEF.TNF_EXTERNAL_TYPE, NDEF.TNF_UNKNOWN,
NDEF.TNF_UNCHANGED, NDEF.TNF_RESERVED];
var firstOctet = buffer.getOctet();
var typeLen = buffer.getOctet();
var payloadLen = buffer.getOctet();
if (!(firstOctet & NDEF.SR)) {
for (var i = 0; i < 3; i++) {
payloadLen <<= 8;
payloadLen |= buffer.getOctet();
}
}
var idLen = 0;
if (firstOctet & NDEF.IL) {
idLen = buffer.getOctet();
}
var tnf = firstOctet & NDEF.TNF;
var type = buffer.getOctetArray(typeLen);
var id = buffer.getOctetArray(idLen);
var payload = buffer.getOctetArray(payloadLen);
return new MozNDEFRecord({tnf: tnfArray[tnf],
type: type || undefined,
id: id || undefined,
payload: payload || undefined});
},
/**
* Parses a URL string to MozNDEFRecord format.
*
* @param {String} url url string.
* @return {Array} Array of MozNDEFRecord instances.
* @memberof NfcUtils
*/
parseURIString: function parseURIString(url) {
if (!url) {
return;
}
var content = url;
for (var i = 1; i < NDEF.URIS.length; i++) {
var len = NDEF.URIS[i].length;
if (url.substring(0, len) == NDEF.URIS[i]) {
var uriPayload = url.substring(len);
content = String.fromCharCode(i) + uriPayload;
break;
}
}
var payload = StringHelper.fromUTF8(content);
var record = new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN,
type: NDEF.RTD_URI,
payload: payload});
if (!record) {
return null;
}
return [record];
}
};
NDEF.init();
exports.NfcBuffer = NfcBuffer;
exports.NfcUtils = NfcUtils;
exports.NDEF = NDEF;
})(window);

View file

@ -0,0 +1,65 @@
/* exported NotificationHelper */
(function(window) {
'use strict';
window.NotificationHelper = {
getIconURI: function nh_getIconURI(app, entryPoint) {
var icons = app.manifest.icons;
if (entryPoint) {
icons = app.manifest.entry_points[entryPoint].icons;
}
if (!icons) {
return null;
}
var sizes = Object.keys(icons).map(function parse(str) {
return parseInt(str, 10);
});
sizes.sort(function(x, y) { return y - x; });
var HVGA = document.documentElement.clientWidth < 480;
var index = sizes[HVGA ? sizes.length - 1 : 0];
return app.installOrigin + icons[index];
},
// titleL10n and options.bodyL10n may be:
// a string -> l10nId
// an object -> {id: l10nId, args: l10nArgs}
// an object -> {raw: string}
send: function nh_send(titleL10n, options) {
return Promise.all([titleL10n, options.bodyL10n].map(getL10n)).then(
([title, body]) => {
if (body) {
options.body = body;
}
options.lang = document.documentElement.getAttribute('lang');
var notification = new window.Notification(title, options);
if (options.closeOnClick !== false) {
notification.addEventListener('click', function nh_click() {
notification.removeEventListener('click', nh_click);
notification.close();
});
}
return notification;
});
},
};
function getL10n(l10nAttrs) {
if (!l10nAttrs) {
return;
}
if (typeof l10nAttrs === 'string') {
return document.l10n.formatValue(l10nAttrs);
}
if (typeof l10nAttrs.raw === "string") {
return l10nAttrs.raw;
}
return document.l10n.formatValue(l10nAttrs.id, l10nAttrs.args);
}
})(this);

View file

@ -0,0 +1,401 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/**
* An OperatorVariantHelper object can be used to trigger customizations
* in an application at startup.
*
* The object will call the listener passed to it during construction
* if the MCC/MNC pair present on the SIM card differs from the previously
* seen pair. The listener will also be called during first boot up of the
* device.
*
* Optionally, one can also use an OperatorVariantListener object to listen
* to SIM card changes during runtime, using the 'listen' method. This can
* enable applications to support multi-SIM devices if appropriate for the
* functionality it provides.
*
* @param {String} iccId - ICC id from the card.
* @param {Numeric} iccCardIndex - Index of ICC card.
* @param {Function} listener - A listener function that will be called when
* a check occurs and the SIM information has
* changed. The function signature should match
* the following: function(mcc, mnc).
* @param {String} persistKey - The settings key which will be used to persist
* if the customizations have been applied. We
* recommend using the application package name
* as the root for your key along with
* 'customization'. The persisted setting is a
* boolean value, where true means the
* customizations have been applied.
* @param {Boolean} checkNow - Perform a check for SIM card change immediately.
* Defaults to true.
*
* @constructor
*/
function OperatorVariantHelper(iccId,
iccCardIndex,
listener,
persistKey,
checkNow) {
var errMsg = null;
// mozIccManager must be present.
var iccManager = window.navigator.mozIccManager;
if (!iccManager) {
errMsg = 'Expected mozIccManager to be present.';
console.error(errMsg);
throw new Error(errMsg);
}
// And mozSettings must be present.
var settings = window.navigator.mozSettings;
if (!settings) {
errMsg = 'Expected mozSettings to be present.';
console.error(errMsg);
throw new Error(errMsg);
}
if ((iccId === '0') || (iccCardIndex < 0)) {
throw new Error('iccId and iccCardIndex arguments must have a value!');
}
this._iccId = iccId;
this._iccCardIndex = iccCardIndex;
if (listener === undefined || typeof listener !== 'function') {
throw new Error('listener argument must be a function!');
}
this._listener = listener;
if (persistKey === undefined || typeof persistKey !== 'string') {
throw new Error('persistKey argument must be a string!');
}
this._persistKey = persistKey;
if (!!checkNow) {
this.customize();
}
}
OperatorVariantHelper.prototype = {
// The listener to call when the SIM card is new or has changed.
_listener: null,
// The actual registered listener (since we use bind we need this).
_addedListener: null,
//
_iccCard: null,
// Cached ICC information.
_iccSettings: { mcc: '', mnc: '', iccId: '' },
// Settings persistence key.
_persistKey: null,
// Are operator variant customizations disabled?
_disableAll: false,
// The mozSettings key for the saved ICCIDs.
get ICCID_SETTINGS_KEY() {
return 'operatorvariant.iccId';
},
// The mozSettings key for the saved MCCs.
get MCC_SETTINGS_KEY() {
return 'operatorvariant.mcc';
},
// The mozSettings key for the saved MNCs.
get MNC_SETTINGS_KEY() {
return 'operatorvariant.mnc';
},
// The prefs key for disabling all customizations.
get OPERATOR_VARIANT_DISABLE_ALL_KEY() {
return 'operatorvariant.disableAll';
},
// Getter for global objects we use frequently.
get settings() {
return window.navigator.mozSettings;
},
/**
* Get the saved ICC Settings (MCC/MNC).
*/
getICCSettings: function() {
var transaction = this.settings.createLock();
var mccRequest = transaction.get(this.MCC_SETTINGS_KEY);
mccRequest.onsuccess = (function() {
var mccs = mccRequest.result[this.MCC_SETTINGS_KEY];
if (!mccs) {
this._iccSettings.mcc = '000';
} else if (!Array.isArray(mccs)) {
this._iccSettings.mcc = mccs;
} else if (!mccs[this._iccCardIndex]) {
this._iccSettings.mcc = '000';
} else {
this._iccSettings.mcc = mccs[this._iccCardIndex];
}
var mncRequest = transaction.get(this.MNC_SETTINGS_KEY);
mncRequest.onsuccess = (function() {
var mncs = mncRequest.result[this.MNC_SETTINGS_KEY];
if (!mncs) {
this._iccSettings.mnc = '00';
} else if (!Array.isArray(mncs)) {
this._iccSettings.mnc = mncs;
} else if (!mncs[this._iccCardIndex]) {
this._iccSettings.mnc = '00';
} else {
this._iccSettings.mnc = mncs[this._iccCardIndex];
}
var iccIdsRequest = transaction.get(this.ICCID_SETTINGS_KEY);
iccIdsRequest.onsuccess = (function() {
var iccIds = iccIdsRequest.result[this.ICCID_SETTINGS_KEY];
this._iccSettings.iccId = iccIds && iccIds[this._iccCardIndex];
this.checkICCInfo();
}).bind(this);
}).bind(this);
}).bind(this);
},
/**
* Verify the saved ICC Settings vs what the SIM card is reporting. If they
* differ we will call the listener that was registered during construction
* to enable customization.
*/
checkICCInfo: function() {
var iccManager = window.navigator.mozIccManager;
this._iccCard = iccManager.getIccById(this._iccId);
if (!this._iccCard) {
return;
}
if (!this._iccCard.iccInfo || this._iccCard.cardState !== 'ready') {
return;
}
// XXX sometimes we get 000/00 for mcc/mnc, even when cardState === 'ready'
var mcc = this._iccCard.iccInfo.mcc || '000';
var mnc = this._iccCard.iccInfo.mnc || '00';
var iccId = this._iccId;
var transaction;
if (mcc === '000') {
return;
}
// ensure that the iccSettings have been retrieved
if (this._iccSettings.iccId === '') {
return;
}
if (iccId !== this._iccSettings.iccId) {
if (this._addedListener) {
try {
// apply new settings
this._listener(mcc, mnc);
}
catch (e) {
console.error('Listener threw an error!', e);
}
this.listen(false);
}
} else {
// Check whether we ran customizations already.
transaction = this.settings.createLock();
var persistKeyGetRequest = transaction.get(this._persistKey);
persistKeyGetRequest.onsuccess = (function persistKeyGetRequestCb() {
// Looks like we didn't run customizations, apply settings.
if (!persistKeyGetRequest.result[this._persistKey]) {
if (this._addedListener) {
try {
// apply new APN settings
this._listener(mcc, mnc, true);
}
catch (e) {
console.error('Listener threw an error!', e);
}
}
}
this.listen(false);
}).bind(this);
}
// store current mcc/mnc info in the settings
transaction = this.settings.createLock();
var mccRequest = transaction.get(this.MCC_SETTINGS_KEY);
mccRequest.onsuccess = (function() {
var mccs = mccRequest.result[this.MCC_SETTINGS_KEY];
if (!mccs || !Array.isArray(mccs)) {
mccs = ['000', '000'];
}
mccs[this._iccCardIndex] = mcc;
var mccSettings = {};
mccSettings[this.MCC_SETTINGS_KEY] = mccs;
transaction.set(mccSettings);
var mncRequest = transaction.get(this.MNC_SETTINGS_KEY);
mncRequest.onsuccess = (function() {
var mncs = mncRequest.result[this.MNC_SETTINGS_KEY];
if (!mncs || !Array.isArray(mncs)) {
mncs = ['00', '00'];
}
mncs[this._iccCardIndex] = mnc;
var mncSettings = {};
mncSettings[this.MNC_SETTINGS_KEY] = mncs;
transaction.set(mncSettings);
var iccIdsRequest = transaction.get(this.ICCID_SETTINGS_KEY);
iccIdsRequest.onsuccess = (function() {
var iccIds = iccIdsRequest.result[this.ICCID_SETTINGS_KEY];
if (!iccIds || !Array.isArray(iccIds)) {
iccIds = [null, null];
}
iccIds[this._iccCardIndex] = iccId;
var iccIdSettings = {};
iccIdSettings[this.ICCID_SETTINGS_KEY] = iccIds;
transaction.set(iccIdSettings);
this._iccSettings.mcc = mcc;
this._iccSettings.mnc = mnc;
this._iccSettings.iccId = iccId;
}).bind(this);
}).bind(this);
}).bind(this);
},
/**
* Persist that the customizations have been applied. Uses the 'persistKey'
* passed in during construction of the object.
*/
applied: function() {
var transaction = this.settings.createLock();
// Persist the fact that we ran customizations.
var item = {};
item[this._persistKey] = true;
var opVariantReq = transaction.set(item);
// We're running in test mode, just return.
if (opVariantReq === undefined) {
return;
}
// If we error we could end up running the customizations again! Ensure
// we surface this to developers.
opVariantReq.onerror = (function(event) {
console.error('Failed to set',
this._persistKey,
'to true.',
'Customizations may run again!');
}).bind(this);
},
/**
* Revert persisted key that tracks if customizations have been applied.
*/
revert: function() {
var transaction = this.settings.createLock();
// Persist the fact that we ran customizations.
var item = {};
item[this._persistKey] = false;
var opVariantReq = transaction.set(item);
// We're running in test mode, just return.
if (opVariantReq === undefined) {
return;
}
opVariantReq.onerror = (function(event) {
console.error('Failed to set',
this._persistKey,
'to false.',
'Customizations will not be able to be applied again.');
}).bind(this);
},
/**
* Start the customization process.
*/
customize: function() {
this.getICCSettings();
},
/**
* Listen for further changes in MCC/MNC values, will trigger customization
* even if customizations have already run. Be careful when using this, it's
* meant for settings that *should* change at runtime.
*
* @param {Boolean} listenForChange - Listen for changes in MCC/MNC values
* during runtime. For multi-sim support.
* Defaults to true. Call this method
* with 'false' to stop listening.
*/
listen: function(listenForChange) {
// Defaults to true so we need to make sure it's set.
var iccManager;
if (listenForChange === undefined) {
listenForChange = true;
}
// Register listener if we're listening for changes.
if (listenForChange) {
// We need to keep a reference to the added listener for removal later.
this._addedListener = this.customize.bind(this);
iccManager = window.navigator.mozIccManager;
this._iccCard = iccManager.getIccById(this._iccId);
if (this._iccCard) {
// Add the actual bound listener.
this._iccCard.addEventListener(
'iccinfochange',
this._addedListener
);
this._iccCard.addEventListener(
'cardstatechange',
this._addedListener
);
} else {
iccManager.oniccdetected = (function iccDetectedHandler(evt) {
if (this._iccId !== evt.iccId) {
return;
}
this._iccCard = iccManager.getIccById(this._iccId);
// Add the actual bound listener.
this._iccCard.addEventListener(
'iccinfochange',
this._addedListener
);
this._iccCard.addEventListener(
'cardstatechange',
this._addedListener
);
}).bind(this);
}
return;
}
if (this._addedListener) {
iccManager = window.navigator.mozIccManager;
this._iccCard = iccManager.getIccById(this._iccId);
if (this._iccCard) {
// Otherwise, unregister.
this._iccCard.removeEventListener(
'iccinfochange',
this._addedListener
);
this._iccCard.removeEventListener(
'cardstatechange',
this._addedListener
);
}
// Clear our reference to the bound listener.
this._addedListener = null;
}
}
};

View file

@ -0,0 +1,138 @@
/* exported PasscodeHelper */
/* globals crypto, TextEncoder */
(function(exports) {
'use strict';
const SET_DIGEST_VALUE = 'lockscreen.passcode-lock.digest.value';
const SET_DIGEST_SALT = 'lockscreen.passcode-lock.digest.salt';
const SET_DIGEST_ITERATIONS = 'lockscreen.passcode-lock.digest.iterations';
const SET_DIGEST_ALGORITHM = 'lockscreen.passcode-lock.digest.algorithm';
const DEFAULT_ALGORITHM = 'SHA-1'; //XXX Update to SHA-256 after bug 554827?
const DEFAULT_ITERATIONS = 1000;
const PBKDF2_OUTPUT_LENGTH = 160; //XXX HMAC-SHA-1 = 160. Change for SHA-256!
function _encode(str) {
return new TextEncoder('utf-8').encode(str);
}
function _toTypedArray(obj) {
// SettingsAPI doesnt like arrays and gives us an Object { 1: .., 2: .. }
var a = [];
for (var key of Object.keys(obj)) {
a.push(obj[key]);
}
return new Uint8Array(a);
}
function _deriveBits(pwKey, salt, iterations, algorithm) {
var params = {
name: 'PBKDF2',
hash: algorithm,
salt: salt,
iterations: iterations
};
return crypto.subtle.deriveBits(params, pwKey, PBKDF2_OUTPUT_LENGTH);
}
function _makeDigest(pass, salt, iterations, algorithm) {
var bytes = _encode(pass);
var promise = crypto.subtle.importKey('raw', bytes, 'PBKDF2', false,
['deriveBits']);
promise = promise.then((pwKey) => {
return _deriveBits(pwKey, salt, iterations, algorithm);
});
promise = promise.catch((error) => {
console.error('PasscodeHelper: _derive_bits() failed!', error);
throw error;
});
return promise;
}
function setPasscode(newPass) {
// Always generate a new salt.
var salt = crypto.getRandomValues(new Uint8Array(8));
/* the combined lock.get() makes it quite ugly. we get
an Array of objects, each with just one key/value,
which is the requested setting. let's destruct as follows:
*/
var promise = _makeDigest(newPass, salt, DEFAULT_ITERATIONS,
DEFAULT_ALGORITHM);
promise = promise.then((digest) => {
return _storeNewPasscode(digest, salt);
});
promise = promise.catch((error) => {
console.error('PasscodeHelper: Could not make digest:', error);
throw error;
});
return promise;
}
function _storeNewPasscode(digest, salt) {
// Note: We can now store the salt, since digest was generated!
var digestUint8 = new Uint8Array(digest);
var newSettings = {
[SET_DIGEST_SALT]: salt,
[SET_DIGEST_VALUE]: digestUint8,
[SET_DIGEST_ITERATIONS]: DEFAULT_ITERATIONS,
[SET_DIGEST_ALGORITHM]: DEFAULT_ALGORITHM,
};
var lock = navigator.mozSettings.createLock();
var req = lock.set(newSettings);
return req.then(() => {
return digestUint8;
}, (error) => {
console.error('PasscodeHelper: Couldnt store new digest', error);
throw error;
});
}
function checkPasscode(testPass) {
//get salt & digest out of settings
var lock = navigator.mozSettings.createLock();
var storedParams = Promise.all([
lock.get(SET_DIGEST_SALT),
lock.get(SET_DIGEST_ITERATIONS),
lock.get(SET_DIGEST_ALGORITHM),
lock.get(SET_DIGEST_VALUE)
]);
return storedParams.then((values) => {
return _makeAndCompare(...values, testPass);
})
.catch((error) => {
console.error('PasscodeHelper: Couldnt get digest Settings:', error);
throw error;
});
}
function _makeAndCompare(salt, iterations, algorithm, storedDigest,
testPass) {
/* the combined lock.get() makes it quite ugly. we get
an Array of objects, each with just one key/value,
which is the requested setting. let's destruct as follows:
*/
var _salt = _toTypedArray(salt[SET_DIGEST_SALT]);
var _iterations = iterations[SET_DIGEST_ITERATIONS];
var _algorithm = algorithm[SET_DIGEST_ALGORITHM];
var _storedDigest = _toTypedArray(storedDigest[SET_DIGEST_VALUE]);
var promise = _makeDigest(testPass, _salt, _iterations, _algorithm);
promise = promise.then((digest) => {
var typedDigest = new Uint8Array(digest);
return _compareDigests(_storedDigest, typedDigest);
});
promise = promise.catch((error) => {
console.error('PasscodeHelper: Couldnt create digest', error);
throw error;
});
return promise;
}
function _compareDigests(buf1, buf2) {
if (buf1.byteLength !== buf2.byteLength) {
return false;
}
for (var i = 0; i < buf1.byteLength; i++) {
if (buf1[i] !== buf2[i]) {
return false;
}
}
return true;
}
exports.PasscodeHelper = {
set: setPasscode,
check: checkPasscode
};
})(this);

View file

@ -0,0 +1,87 @@
/* globals define, module */
/**
* A simple library to help you escape HTML using template strings.
*
* It's the counterpart to our eslint "no-unsafe-innerhtml" plugin that helps us
* avoid unsafe coding practices.
* A full write-up of the Hows and Whys are documented
* for developers at
* https://developer.mozilla.org/en-US/Firefox_OS/Security/Security_Automation
* with additional background information and design docs at
* https://wiki.mozilla.org/User:Fbraun/Gaia/SafeinnerHTMLRoadmap
*
*/
(function (root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.Sanitizer = factory();
}
}(this, function () {
'use strict';
var Sanitizer = {
_entity: /[&<>"'/]/g,
_entities: {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&apos;',
'/': '&#x2F;'
},
getEntity: function (s) {
return Sanitizer._entities[s];
},
/**
* Escapes HTML for all values in a tagged template string.
*/
escapeHTML: function (strings, ...values) {
var result = '';
for (var i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
result += String(values[i]).replace(Sanitizer._entity,
Sanitizer.getEntity);
}
}
return result;
},
/**
* Escapes HTML and returns a wrapped object to be used during DOM insertion
*/
createSafeHTML: function (strings, ...values) {
var escaped = Sanitizer.escapeHTML(strings, ...values);
return {
__html: escaped,
toString: function () {
return '[object WrappedHTMLObject]';
},
info: 'This is a wrapped HTML object. See https://developer.mozilla.or'+
'g/en-US/Firefox_OS/Security/Security_Automation for more.'
};
},
/**
* Unwrap safe HTML created by createSafeHTML or a custom replacement that
* underwent security review.
*/
unwrapSafeHTML: function (...htmlObjects) {
var markupList = htmlObjects.map(function(obj) {
return obj.__html;
});
return markupList.join('');
}
};
return Sanitizer;
}));

View file

@ -0,0 +1,124 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/**
* This script can identify the size and watch change of mediaquery.
*
* You can get current layout by calling |gerCurrentLayout|,
* it will only return matching type from defaultQueries.
*
* You can use |watch| and |unwatch| to start/stop watch on specific
* mediaquery string, such as tiny/small/medium/large, you can also
* define your own watcher. After start watcher, this script
* will dispatch 'screenlayoutchange' event and pass name and status.
*/
var ScreenLayout = {
//Refer to famous libraries, like Twitter Bootstrap and Foundation
//we choose 768, 992, 1200 width as our breakpoints
defaultQueries: {
tiny: '(max-width: 767px)',
small: '(min-width: 768px) and (max-width: 991px)',
medium: '(min-width: 992px) and (max-width: 1200px)',
large: '(min-width: 1201px)',
hardwareHomeButton: window.VulpesCompat ? '(min-width: 0px)' : '(-moz-physical-home-button)'
},
init: function sl_init() {
// loop defaultQueries and add window.matchMedia()
// to this.queries object
this.queries = (function(qs) {
var result = {};
for (var key in qs) {
result[key] = window.matchMedia(qs[key]);
}
return result;
})(this.defaultQueries);
},
_isOnRealDevice: undefined,
isOnRealDevice: function sl_isOnRealDevice() {
if (typeof(this._isOnRealDevice) !== 'undefined') {
return this._isOnRealDevice;
}
// XXX: A hack to know we're using real device or not
// is to detect screen size.
// The screen size of b2g running on real device
// is the same as the size of system app.
if (window.innerWidth === screen.availWidth) {
this._isOnRealDevice = true;
} else {
this._isOnRealDevice = false;
}
return this._isOnRealDevice;
},
// name: |String|, ex: 'tiny', 'small', 'medium'
//
// tell user what type it is now
// if type is undeined, it will return matching type from "defaultQueries"
// if type is given, it will return boolean based on all watching queries
getCurrentLayout: function sl_getCurrentLayout(type) {
if (type === undefined) {
for (var name in this.defaultQueries) {
if (this.queries[name] && this.queries[name].matches) {
return name;
}
}
}
if (typeof this.queries[type] !== 'undefined') {
return this.queries[type].matches;
}
return false;
},
// name: |String|, ex: 'tiny', 'small', 'medium', 'large'
// media: |String| optional, ex: '(max-width: 767px)'
//
// Start the |name| watcher
// If |name| is not predefined in defaultQueries,
// then |media| is required.
//
// If overwrite defaultQueries with new |media|,
// |getCurrentLayout| will be also based on new config.
watch: function sl_watch(name, media) {
var mediaString = media || this.queries[name].media;
if (!mediaString) {
return;
}
this.unwatch(name);
this.queries[name] = window.matchMedia(mediaString);
this.boundHandleChange = this.handleChange.bind(this);
this.queries[name].addListener(this.boundHandleChange);
},
unwatch: function sl_unwatch(name) {
if (this.queries[name]) {
this.queries[name].removeListener(this.boundHandleChange);
}
},
// Dispatch screenlayoutchange event, and pass mediaquery name and
// status, which represent name of activating mediaquery and
// activate status(boolean). ex: {name: 'small', status: true}
handleChange: function sl_handleChange(evt) {
for (var key in this.queries) {
if (this.queries[key].media !== evt.media) {
continue;
}
window.dispatchEvent(new CustomEvent('screenlayoutchange', {
detail: {
name: key,
status: evt.matches
}
}));
}
}
};
ScreenLayout.init();

View file

@ -0,0 +1,3 @@
;(function(exports){'use strict';var SettingsHelper=function(key,defaultValue){var SETTINGS_KEY=key;var _settings=navigator.mozSettings;var _value=null;var _defaultValue=defaultValue;var _isReady=false;var _callbacks=[];var _return=function sh_return(callback){if(!callback){return;}
callback.apply(null,Array.prototype.slice.call(arguments,1));};var _ready=function sh_ready(callback){if(!callback){return;}
if(_isReady){callback();}else{_callbacks.push(callback);}};var _getValue=function sh_getValue(callback){var req=_settings.createLock().get(SETTINGS_KEY);req.onsuccess=function(){_return(callback,req.result[SETTINGS_KEY]);};req.onerror=function(){console.error('Error getting '+SETTINGS_KEY+'.');_return(callback,null);};};var _setValue=function sh_setValue(value,callback){var obj={};obj[SETTINGS_KEY]=value;var req=_settings.createLock().set(obj);req.onsuccess=function(){_return(callback);};req.onerror=function(){console.error('Error setting '+SETTINGS_KEY+'.');_return(callback);};};var _valuechanged=function sh_valuehanged(e){_value=e.settingValue;};var _init=function sh_init(callback){_getValue(function(value){_value=value?value:_defaultValue;_return(callback);});_settings.addObserver(SETTINGS_KEY,_valuechanged);};_init(function(){_isReady=true;_callbacks.forEach(function(callback){callback();});});return{get:function(callback){_ready(function(){_return(callback,_value?_value:_defaultValue);});},set:function(value,callback){_ready(function(){_setValue(value,_return.bind(null,callback));});},uninit:function(){_settings.removeObserver(SETTINGS_KEY,_valuechanged);}};};exports.SettingsHelper=SettingsHelper;})(this);

View file

@ -0,0 +1,76 @@
/* exported SettingsListener */
'use strict';
var SettingsListener = {
/* lock stores here */
_lock: null,
/* keep record of observers in order to remove them in the future */
_observers: [],
/**
* getSettingsLock: create a lock or retrieve one that we saved.
* mozSettings.createLock() is expensive and lock should be reused
* whenever possible.
*/
getSettingsLock: function sl_getSettingsLock() {
// If there is a lock present we return that
if (this._lock && !this._lock.closed) {
return this._lock;
}
// If there isn't we return one.
var settings = window.navigator.mozSettings;
return (this._lock = settings.createLock());
},
observe: function sl_observe(name, defaultValue, callback) {
var settings = window.navigator.mozSettings;
if (!settings) {
window.setTimeout(function() { callback(defaultValue); });
return;
}
var req;
try {
req = this.getSettingsLock().get(name);
} catch (e) {
// It is possible (but rare) for getSettingsLock() to return
// a SettingsLock object that is no longer valid.
// Until https://bugzilla.mozilla.org/show_bug.cgi?id=793239
// is fixed, we just catch the resulting exception and try
// again with a fresh lock
console.warn('Stale lock in settings_listener.js.',
'See https://bugzilla.mozilla.org/show_bug.cgi?id=793239');
this._lock = null;
req = this.getSettingsLock().get(name);
}
req.addEventListener('success', (function onsuccess() {
callback(typeof(req.result[name]) != 'undefined' ?
req.result[name] : defaultValue);
}));
var settingChanged = function settingChanged(evt) {
callback(evt.settingValue);
};
settings.addObserver(name, settingChanged);
this._observers.push({
name: name,
callback: callback,
observer: settingChanged
});
},
unobserve: function sl_unobserve(name, callback) {
var settings = window.navigator.mozSettings;
var that = this;
this._observers.forEach(function(value, index) {
if (value.name === name && value.callback === callback) {
settings.removeObserver(name, value.observer);
that._observers.splice(index, 1);
}
});
}
};

View file

@ -0,0 +1,33 @@
/* exported SettingsURL */
'use strict';
// This library helps with managing settings that can either send back
// string uris or blobs that need to be turned into blob: uris.
// Usage is as follows:
// var blobURL = new SettingsURL();
// element.src = blobURL.set(settingValue);
function SettingsURL() {
this._url = null;
this._isBlob = false;
}
SettingsURL.prototype = {
set: function(value) {
if (this._isBlob) {
window.URL.revokeObjectURL(this._url);
}
if (value instanceof Blob) {
this._isBlob = true;
this._url = window.URL.createObjectURL(value);
} else {
this._isBlob = false;
this._url = value;
}
return this._url;
},
get: function() {
return this._url;
}
};

View file

@ -0,0 +1,623 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/**
* Shrinking UI: would make app window tilted and ready to be tapped, dragged
* and sent out and received in. It receives four events to do the UI change:
*
* 'shrinking-start': tilt the window and let user can drag it
* 'shrinking-stop' : tilt the window back immediately and let user can use it
* 'shrinking-receiving' : after the app launched, do the receiving animation
*
* It would send an event out to nofify that the user has dragged the window
* to the top of the frame, which means the content should be sent:
* (@see '_handleSendingOut')
*
* 'shrinking-sent': user has dragged the window to the top of the frame
*
* The caller should notice that after the first time the window got tilted,
* it must be tilted back by the 'shrinking-stop' event, or the app window
* and the whole Gaia may be malfunctioning. Also, when the screen got black
* out, or any other state changes occurs, the whole shrinking process would be
* interrupted, and the 'shrinking-stop' is necessary in such situation.
*
*/
/* globals dump, Promise */
(function(exports) {
var ShrinkingUI = function(foregroundElement, backgroundElement) {
this.elements.foregroundElement = foregroundElement;
this.elements.backgroundElement = backgroundElement;
};
ShrinkingUI.prototype = {
DEBUG: false,
THRESHOLD: 50,
SUSPEND_INTERVAL: 100,
state: {
shrinking: false,
ending: false,
tilting: false,
overThreshold: false,
toward: 'TOP',
touch: {
initY: -1,
prevY: -1
},
slideTransitionCb: null, // Last slide transition
tiltTransitionCb: null // Last tilt transition
},
elements: {},
configs: {
degreeLandscape: '2.7deg',
degreePortrait: '0.65deg',
overDegreeLandscape: '2.9deg',
overDegreePortrait: '0.9deg'
},
tip: null,
cover: null
};
ShrinkingUI.prototype.debug = function su_debug(msg, optObject) {
if (this.DEBUG) {
var output = '[DEBUG] ShrinkingUI: ' + msg;
if (optObject) {
output += JSON.stringify(optObject);
}
dump(output);
}
};
/**
* Bind events and do some necessary initialization.
*
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype.start = function su_start() {
window.addEventListener('shrinking-receiving', this);
this.startTilt();
};
/**
* Unbind events and do some necessary initialization.
*
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype.stop = function su_stop() {
if (this.isActive()) {
this.stopTilt();
}
window.removeEventListener('shrinking-receiving', this);
};
/**
* The event dispatcher.
*
* @param {event} |evt|
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype.handleEvent = function su_handleEvent(evt) {
switch (evt.type) {
case 'shrinking-receiving':
// It should be launched, then received.
// So we'll get a new app.
this._receivingEffects();
break;
case 'touchstart':
this._handleSendingStart(evt);
break;
case 'touchmove':
this._handleSendingSlide(evt);
break;
case 'touchend':
this._handleSendingOut(evt);
break;
}
};
ShrinkingUI.prototype.respondToHierarchyEvent =
function su_respondToHierarchyEvent(evt) {
if (this['_handle_' + evt.type]) {
return this['_handle_' + evt.type](evt);
}
return true;
};
ShrinkingUI.prototype._handle_holdhome = function su__handle_holdhome(evt) {
return this.isActive();
};
ShrinkingUI.prototype._handle_home = function su__handle_home(evt) {
return this.isActive();
};
/**
* Start tilting the app window.
*
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype.startTilt = function su_startTilt() {
// Already shrunk.
if (this.isActive()) {
return;
}
if (!this.elements.backgroundElement || !this.elements.foregroundElement) {
return;
}
this.state.shrinking = true;
this.state.tilting = true;
var afterTilt = () => {
// After it tilted done, turn it to the screenshot mode.
this.elements.foregroundElement.classList.add('hidden');
this.state.tilting = false;
};
this._setTip();
this._setState(true);
// disable rotation to prevent display UI with wrong image
screen.mozLockOrientation(screen.mozOrientation);
this._shrinkingTilt(afterTilt);
};
/**
* Set the tip on the window.
*
* @this {ShrinkingTilt}
*/
ShrinkingUI.prototype._setTip = function su_setTip() {
var backgroundElement = this.elements.backgroundElement;
if (!this.tip) {
this.tip = this._slidingTip();
backgroundElement.appendChild(this.tip);
}
this.tip.classList.remove('hide');
};
/**
* Stop the shrinking effect and tilt it back.
*
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype.stopTilt = function su_stopTilt() {
if (!this.state.shrinking || this.state.ending) {
return;
}
// When shrinking get forcibly stopped, restore the flag.
if (this.state.tilting) {
this.state.tilting = false;
}
this.state.shrinking = false;
this.state.ending = true;
var afterTiltBack = (() => {
// Turn off the screenshot mode.
this.elements.foregroundElement.classList.remove('hidden');
this._cleanEffects().then(() => {
this.state.ending = false;
});
});
this.tip.remove();
this.tip = null;
this._shrinkingTiltBack(true, afterTiltBack);
};
/**
* Indicate shrinking UI is active.
*
* @return {boolean} The frame is shrinked or not.
* @see _setState
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype.isActive = function su_isActive() {
if (this.state.shrinking || this.state.tilting || this.state.ending) {
return true;
}
return false;
};
/**
* Avoid to manipulate strings and DOM attributes everywhere.
* Users should observe the 'data-shrinking-state' to see if now the
* app window is tilted ('true') or not ('false').
*
* @param {boolean} |state| Ccurrently the UI is on or off.
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._setState = function su_shrinking(state) {
// TODO: Call AppWindow.setState and AppWindow.getState instead of
// setting/getting the attribute directly in shrinking UI.
var foregroundElement = this.elements.foregroundElement;
foregroundElement.setAttribute('data-shrinking-state',
state.toString());
this.debug('Setting shrink state to: ' + state);
};
/**
* Update the current 'transitionend' callback for the slide animation.
* @param {cb} transitioned callback
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._updateSlideTransition =
function su_updateSlideTransition(cb) {
if (this.tip && this.state.slideTransitionCb) {
this.elements.foregroundElement.style.transitionDuration = '0s';
this.tip.removeEventListener('transitionend',
this.state.slideTransitionCb);
}
this.state.slideTransitionCb = cb;
};
/**
* Update the current 'transitionend' callback for the tilt animation.
* @param {cb} transitioned callback
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._updateTiltTransition =
function su_updateTiltTransition(cb) {
var foregroundElement = this.elements.foregroundElement;
if (foregroundElement && this.state.tiltTransitionCb) {
foregroundElement.removeEventListener('transitionend',
this.state.tiltTransitionCb);
}
this.state.tiltTransitionCb = cb;
};
/**
* Receiving: make the app itself fly in from the top.
*
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._receivingEffects = function su_receivingEffects() {
// Hide it before we move it.
// Visibility won't work.
var foregroundElement = this.elements.foregroundElement;
foregroundElement.style.opacity = '0';
var afterTop = (function() {
// Restore the display to let it fly in.
foregroundElement.style.opacity = '';
foregroundElement.style.transition = 'transform 0.5s ease';
// 2. Slide to the BOTTOM.
// 3. Tilt back and display it as normal apps.
this._sendingSlideTo('BOTTOM',
(function doTiltBack() {
this._shrinkingTiltBack(false, this._cleanEffects);
}).bind(this)
);
}).bind(this);
var afterTilt = (function() {
// Make it fly to top immediately (can't set to zero or the
// callback won't work).
foregroundElement.style.transition = 'transform 0.05s ease';
this._sendingSlideTo('TOP', afterTop);
}).bind(this);
// 1. Make it on top and tilted.
this._shrinkingTilt(afterTilt);
};
/**
* Slide the frame to the specific position.
*
* @param {number} |y| The Y coordinate the frame should slide to.
* @param {function()} |callback| (Optional) execute after the frame moved
* to the position.
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._sendingSlideTo =
function su_sendingSlideTo(y, callback) {
var foregroundElement = this.elements.foregroundElement;
var cbDone = (function on_cbDone(evt) {
foregroundElement.removeEventListener('transitionend', cbDone);
// Clear animation, so the shrinkingUI will follow the touchmove event.
this.elements.foregroundElement.style.transitionDuration = '0s';
if ('undefined' !== typeof callback) {
callback();
}
}).bind(this);
if ('TOP' === y) {
// We should have animation to finish the rest of transformY.
foregroundElement.style.transitionDuration = '0.3s';
y = foregroundElement.parentElement.clientHeight;
} else if ('BOTTOM' === y) {
// If no rest transformY is needed, we should trigger the callback
// directly.
if (this.state.touch.prevY <= 0) {
cbDone();
} else {
foregroundElement.style.transitionDuration = '0.3s';
}
y = 0;
} else if (y < 0) {
y = 0;
}
foregroundElement.addEventListener('transitionend', cbDone);
this._updateTiltTransition(cbDone);
foregroundElement.style.transform =
'rotateX(' + this._getTiltingDegree() + ') ' +
'translateY(-' + y + 'px)';
};
/**
* Create a new sliding cover and bind all necessary event handlers.
*
* @return {DOMElement}
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._slidingCover =
function slidingCover() {
var cover = document.createElement('div');
cover.id = 'shrinking-cover';
cover.style.width = '100%';
cover.style.height = '100%';
cover.style.position = 'relative';
cover.style.zIndex = '2';
cover.addEventListener('touchstart', this);
cover.addEventListener('touchmove', this);
cover.addEventListener('touchend', this);
return cover;
};
/**
* Create a tip.
*
* @return {DOMElement}
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._slidingTip =
function slidingTip() {
var tip = document.createElement('div');
var tipArrow = document.createElement('div');
var tipText = document.createElement('div');
tip.id = 'shrinking-tip';
tipArrow.id = 'shrinking-tip-arrow';
tipText.id = 'shrinking-text';
tipArrow.textContent = '\u00A0';
tipText.setAttribute('data-l10n-id', 'shrinking-tip');
tip.appendChild(tipText);
tip.appendChild(tipArrow);
return tip;
};
/**
* Will return the disabled element to let caller manupulate it.
*
* @return {DOMElement}
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._disableSlidingCover =
function su_disableSlidingCover() {
this.cover.removeEventListener('touchstart', this);
this.cover.removeEventListener('touchmove', this);
this.cover.removeEventListener('touchend', this);
};
/**
* Make the app frame tilted and overlay it with a cover.
*
* @param {function()} |cb| (Optional) if there're are any movement
* should be performed after tilting back.
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._shrinkingTilt =
function su_shrinkingTilt(cb) {
var foregroundElement = this.elements.foregroundElement;
var backgroundElement = this.elements.backgroundElement;
// If start then do some effect would tilt the window again,
// would generate two cover which should be unique.
if (null === this.cover) {
// TODO: Call AppWindow.inject(DOM) instead of changing the DOM
// directly in Shrinking UI.
this.cover = this._slidingCover();
var anchor = foregroundElement.firstElementChild;
foregroundElement.insertBefore(this.cover,
anchor);
}
backgroundElement.classList.add('shrinking-wrapper');
foregroundElement.style.transition = 'transform 0.5s ease';
// Add a little bouncing back animation.
// Nested callbacks: first animation is for sinking down,
// and the second one is for bouncing back.
var bounceBack = (function on_bounceBack(evt) {
if (evt.target !== foregroundElement) {
return;
}
foregroundElement.removeEventListener('transitionend', bounceBack);
foregroundElement.addEventListener('transitionend', bounceBackEnd);
this._updateTiltTransition(bounceBackEnd);
foregroundElement.style.transition = 'transform ease';
foregroundElement.style.transform =
'rotateX(' + this._getTiltingDegree() + ') ';
}).bind(this);
var bounceBackEnd = (function on_bounceBackEnd(evt) {
if (evt.target !== foregroundElement) {
return;
}
foregroundElement.removeEventListener('transitionend',
bounceBackEnd);
foregroundElement.style.transition = 'transform 0.5s ease 0s';
if (cb) {
cb();
}
}).bind(this);
foregroundElement.addEventListener('transitionend', bounceBack);
this._updateTiltTransition(bounceBack);
// After set up, trigger the transition.
foregroundElement.style.transformOrigin = '50% 100% 0';
foregroundElement.style.transform =
'rotateX(' + this._getOverTiltingDegree() + ')';
};
/**
* Restore the app frame. It depends on another stateful method
* because we must update some states after restoring the frame.
*
* Note this function would not clean the effects, because the
* window may need to be tilted again.
*
* @param {boolean} |instant| If true, tilt it back instantly
* @param {function()} |cb| (Optional) Callback when the tilt was done.
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._shrinkingTiltBack =
function su_shrinkingTiltBack(instant, callback) {
var foregroundElement = this.elements.foregroundElement;
// Setup the rotating animation.
if (!instant) {
var tsEnd = (function _tsEnd(evt) {
foregroundElement.removeEventListener('transitionend', tsEnd);
if (callback) {
callback();
}
}).bind(this);
foregroundElement.style.transition = 'transform 0.3s ease';
foregroundElement.addEventListener('transitionend', tsEnd);
this._updateTiltTransition(tsEnd);
foregroundElement.style.transform = 'rotateX(0.0deg)';
} else {
foregroundElement.style.transition = '';
foregroundElement.style.transform = 'rotateX(0.0deg)';
if (callback) {
callback();
}
}
};
/**
* User start to tap on the tilted frame.
*
* @param {event} |evt|
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._handleSendingStart =
function su_handleSendingStart(evt) {
// We keep the value of pageY when touchstart, so that we can
// use it to calculate the offset of transition of every frame.
this.state.touch.initY = evt.touches[0].pageY;
this.debug('_handleSendingStart(): ', this.isActive());
// Stop the touch event to affect the inner elements.
evt.stopImmediatePropagation();
// Remove the tip above the window.
var tsEnd = (function _tsEnd() {
this.tip.removeEventListener('transitionend', tsEnd);
}).bind(this);
this.tip.classList.add('hide');
this.tip.addEventListener('transitionend', tsEnd);
this._updateSlideTransition(tsEnd);
return false;
};
/**
* Sending is in progress.
*
* @param {event} |evt| The 'touchstart' event.
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._handleSendingSlide =
function su_handleSendingSlide(evt) {
var pgy = evt.touches[0].pageY;
var slideY = this.state.touch.initY - pgy;
if ('undefined' === typeof this.state.touch.initY) {
this.state.touch.initY = slideY;
}
if ('undefined' === typeof this.state.touch.prevY) {
this.state.touch.prevY = slideY;
}
// User is dragging it back or not.
this.state.toward = (this.state.touch.prevY < slideY) ? 'TOP' : 'BOTTOM';
this._sendingSlideTo(slideY);
this.state.touch.prevY = slideY;
this.state.overThreshold = (slideY > this.THRESHOLD) ? true : false;
};
/**
* Sending was finished (user released the finger).
* Would trigger 'shrinking-sent' event if user sent it out.
*
* @param {event} |evt| The 'touchmove' event.
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._handleSendingOut =
function su_handleSendingOut(evt) {
this.debug('_handleSendingOut(): ', this.isActive());
if (this.state.overThreshold && 'TOP' === this.state.toward) {
this._sendingSlideTo('TOP' , (function() {
// When it got sent, freeze it at the top.
this._disableSlidingCover();
window.dispatchEvent(new CustomEvent('shrinking-sent'));
}).bind(this));
} else {
// Fallback to the bottom if user cancel it.
this._sendingSlideTo('BOTTOM',
(function resumeSending() {
this._setTip();
}).bind(this)
);
}
};
/**
* Must clear and restore the app frame after transition end
* to make it come back with animation.
*
* @this {ShrinkingUI}
*/
ShrinkingUI.prototype._cleanEffects = function su_cleanEffects() {
var foregroundElement = this.elements.foregroundElement;
var backgroundElement = this.elements.backgroundElement;
return new Promise((resolve, rejected) => {
this.debug('_cleanEffects(): ', this.isActive());
this._disableSlidingCover();
foregroundElement.style.transition = '';
foregroundElement.style.transform = '';
foregroundElement.style.transformOrigin = '50% 50% 0';
this._setState(false);
// Clear the 'transitionend' animation listener callbacks
this._updateTiltTransition(null);
this._updateSlideTransition(null);
backgroundElement.classList.remove('shrinking-wrapper');
this.cover.remove();
this.cover = null;
resolve();
});
};
ShrinkingUI.prototype._getTiltingDegree = function su_getTiltingDegree() {
return (window.OrientationManager ?
window.OrientationManager.fetchCurrentOrientation()
.indexOf('landscape') !== -1 :
window.innerHeight < window.innerWidth) ?
this.configs.degreeLandscape :
this.configs.degreePortrait;
};
ShrinkingUI.prototype._getOverTiltingDegree =
function su_getOverTiltingDegree() {
return (window.OrientationManager ?
window.OrientationManager.fetchCurrentOrientation()
.indexOf('landscape') !== -1 :
window.innerHeight < window.innerWidth) ?
this.configs.overDegreeLandscape :
this.configs.overDegreePortrait;
};
exports.ShrinkingUI = ShrinkingUI;
})(window);

View file

@ -0,0 +1,182 @@
/* globals SIMSlot */
'use strict';
(function(window) {
var _start = Date.now();
var DEBUG = false;
/**
* SIMSlot is the API wrapper for each mobileConnection,
* and since one mobileConnection matches one SIM slot,
* we call it SIMSlot.
*
* @param {Object} conn mobileConnection
* @param {index} index The slot number of this SIM slot.
* @param {Object} [card] iccObject
*
* @property {Object} simCard Represent the current active iccObj,
* i.e., SIM card.
* @property {Number} index The slot number of this SIM slot.
*/
window.SIMSlot = function SIMSlot(conn, index, card) {
this.index = index;
this.conn = conn;
if (card) {
this.update(card);
}
/**
* TODO: Add event listeners on this.conn
*/
/**
* The event represents the instance is initialized.
* @event SIMSlot#simslot-created
*/
this.publish('created');
};
SIMSlot.EVENTS = ['cardstatechange', 'iccinfochange',
'stkcommand', 'stksessionend'];
SIMSlot.METHODS = ['sendStkResponse', 'sendStkMenuSelection',
'sendStkTimerExpiration', 'sendStkEventDownload'];
SIMSlot.DOM_REQUEST_METHODS = ['getCardLock', 'unlockCardLock', 'setCardLock',
'getCardLockRetryCount', 'readContacts',
'updateContact', 'iccOpenChannel', 'iccExchangeAPDU',
'iccCloseChannel'];
SIMSlot.ABSENT_TYPES = ['permanentBlocked'];
SIMSlot.LOCK_TYPES = ['pinRequired', 'pukRequired', 'networkLocked',
'corporateLocked', 'serviceProviderLocked',
'network1Locked', 'network2Locked', 'hrpdNetworkLocked',
'ruimCorporateLocked', 'ruimServiceProviderLocked'];
/**
* Update the iccObj.
*
* This method is called by SIMSlotManager when the iccObj
* needs to be updated.
* @param {Object} iccObj The iccObj belongs to this slot.
*/
SIMSlot.prototype.update = function ss_update(iccObj) {
this.simCard = iccObj;
this.constructor.EVENTS.forEach(function iterater(evt) {
iccObj.addEventListener(evt, this);
}, this);
this.constructor.DOM_REQUEST_METHODS.forEach(function iterator(domRequest) {
this[domRequest] = function() {
return iccObj[domRequest].apply(iccObj, arguments);
};
}, this);
this.constructor.METHODS.forEach(function iterator(method) {
this[method] = function() {
return iccObj[method].apply(iccObj, arguments);
};
}, this);
this.publish('updated');
};
/**
* The prefix of every event published by the SIMSlot instance.
* @type {String}
*/
SIMSlot.prototype.EVENT_PREFIX = 'simslot-';
SIMSlot.prototype.CLASS_NAME = 'SIMSLOT';
/**
* Publish an event with this instance in the detail.
* @param {String} eventName The event name without prefix
*/
SIMSlot.prototype.publish = function ss_publish(eventName) {
this.debug(' publish: ' + eventName);
window.dispatchEvent(new CustomEvent(this.EVENT_PREFIX + eventName, {
detail: this
}));
};
SIMSlot.prototype.handleEvent = function ss_handleEvent(evt) {
switch (evt.type) {
default:
this.publish(evt.type);
if (this.simCard) {
this.debug(this.simCard.cardState);
}
break;
}
};
SIMSlot.prototype.debug = function() {
if (DEBUG) {
console.log('[' + this.CLASS_NAME + ']' +
'[' + (this.index) + ']' +
'[' + (new Date().getTime() / 1000 - _start).toFixed(3) + ']' +
Array.slice(arguments).concat());
}
};
/**
* Indicate the slot has SIM card or not.
* @return {Boolean} Without SIM card or not.
*/
SIMSlot.prototype.isAbsent = function ss_isAbsent() {
return (!this.simCard ||
this.constructor.ABSENT_TYPES.indexOf(this.simCard.cardState) >= 0 ||
this.simCard && this.simCard.iccInfo &&
this.simCard.iccInfo.iccid === null);
};
/**
* Function to get simcard's smsc number
* @param {Function} cb your callback function to get smsc number
*/
SIMSlot.prototype.getSmsc = function ss_getSmsc(cb) {
var mobileMessage = window.navigator.mozMobileMessage;
if (!mobileMessage) {
console.error('can\'t access mozMobileMessage');
cb(null);
} else {
// The return type of getSmscAddress can be either a DOMRequest which
// resolves to a string for legacy implementations, or
// a Promise which resolves to a SmscAddress object in Bug 1043250.
mobileMessage.getSmscAddress(this.index).then(
(result) => {
var smsc;
// Keep the legacy behavior for backward compatibility. The legacy
// interface resolves to a string, and is always assumed to be in text
// mode.
if (typeof result === 'string' || result instanceof String) {
smsc = result.split(',')[0].replace(/"/g, '');
} else {
smsc = result.address;
}
cb(smsc);
},
(error) => {
console.error(error);
cb(null);
}
);
}
};
SIMSlot.prototype.isUnknownState = function ss_isUnknownState() {
var empty = (this.simCard.cardState === '');
var unknown = (this.simCard.cardState === 'unknown');
return !this.simCard.cardState || unknown || empty;
};
/**
* Indicate SIM card in the slot is locked or not.
* @return {Boolean} SIM card locked or not.
*/
SIMSlot.prototype.isLocked = function ss_isLocked() {
return this.constructor.LOCK_TYPES.indexOf(this.simCard.cardState) >= 0;
};
SIMSlot.prototype.getCardState = function ss_getCardState() {
return this.simCard.cardState;
};
}(window));

View file

@ -0,0 +1,235 @@
/* global SIMSlot, SIMSlotManager */
'use strict';
(function(window) {
var IccManager = navigator.mozIccManager;
/**
* SIMSlotManager creates/manages the current SIM slot on the device.
* @type {Object}
*/
window.SIMSlotManager = {
/**
* The number of SIM slots.
* @type {Number}
*/
length: 0,
_instances: [],
/**
* The timeout to wait for the second SIM
* @type {Number}
*/
TIMEOUT_FOR_SIM2: 2000,
/**
* Timer used to wait for the second SIM
* @type {Number} timeoutId
*/
_timerForSIM2: null,
/**
* This property is used to make sure sim_lock won't get inited
* before we receive iccdetected when bootup.
* @type {Boolean}
*/
ready: false,
init: function ssm_init() {
if (!IccManager) {
return;
}
this._conns = Array.prototype.slice.call(navigator.mozMobileConnections);
this.length = this._conns.length;
if (this._conns.length === 0) {
return;
}
var allSIMCardDetected = true;
this._conns.forEach(function iterator(conn, index) {
var slot = new SIMSlot(conn, index,
IccManager.getIccById(conn.iccId));
this._instances.push(slot);
if (slot.isAbsent()) {
allSIMCardDetected = false;
}
}, this);
if (allSIMCardDetected) {
this.publishSIMSlotIsReady();
} else if (this.isMultiSIM() && this.hasOnlyOneSIMCardDetected()) {
// we are now in DSDS device with one simcard detected.
this.waitForSecondSIM();
}
// 'iccdetected' shall always be listened to even when
// 'allSIMCardDetected' is set to true. In addition to detect UICC after
// device boot up, we also rely on this event to update the new Icc
// Object created from IccManager to corresponding SIMSlot when user
// toggles airplane mode ON/OFF.
IccManager.addEventListener('iccdetected', this);
// to notify we are inited
window.dispatchEvent(new CustomEvent('simslotmanagerstarted'));
},
/**
* We support multiSIM or not.
* @return {Boolean} MultiSIM is available or not.
*/
isMultiSIM: function() {
return (this.length > 1);
},
/**
* Check there is sim card on slot#index or not.
* @param {Number} index The slot number.
* @return {Boolean} sim card is absent or not.
*/
isSIMCardAbsent: function ssm_isSIMCardAbsent(index) {
var slot = this.get(index);
if (slot) {
return slot.isAbsent();
} else {
return true;
}
},
/**
* Make sure we really have one simcard information
* @return {Boolean} we already have one simcard.
*/
hasOnlyOneSIMCardDetected: function() {
var sim0Absent = this.isSIMCardAbsent(0);
var sim1Absent = this.isSIMCardAbsent(1);
var hasOneSim =
(sim0Absent && !sim1Absent) || (!sim0Absent && sim1Absent);
return hasOneSim;
},
/**
* Check there is no any sim card on device or not.
* @return {Boolean} There is no sim card.
*/
noSIMCardOnDevice: function ssm_noSIMCardOnDevice() {
if (!IccManager || !IccManager.iccIds) {
return true;
}
return (IccManager.iccIds.length === 0);
},
noSIMCardConnectedToNetwork: function ssm_noSIMCardConnectedToNetwork() {
if (!IccManager || !IccManager.iccIds) {
return true;
}
return this._instances.every(function iterator(instance) {
return instance.conn.voice && instance.conn.voice.emergencyCallsOnly;
});
},
/**
* Get specific SIM slot instance.
* @param {Number} index The slot number.
* @return {Object} The SIMSlot instance.
*/
get: function ssm_get(index) {
if (index > this.length - 1) {
return null;
}
return this._instances[index];
},
/**
* Get specific mobileConnection object.
* @param {Number} index The slot number.
* @return {Object} The mobile connection object.
*/
getMobileConnection: function ssm_getMobileConnection(index) {
if (index > this.length - 1) {
return null;
}
return this._instances[index].conn;
},
/**
* Get all sim slot instances
* @return {Array} The array of sim slot instances.
*/
getSlots: function ssm_getSlots() {
return this._instances;
},
/**
* Get specified simslot by iccId
* @return {Object} The SIMSlot instance.
*/
getSlotByIccId: function ssm_getSlotByIccId(iccId) {
var found = null;
this._instances.some(function iterator(slot, index) {
if (slot.conn.iccId && slot.conn.iccId === iccId) {
found = slot;
return true;
} else {
return false;
}
}, this);
return found;
},
/**
* This method is used to make sure if we can't receive the 2nd
* `iccdetected` event during the timeout, we would treat this
* situation as DSDS device with only one simcard inserted.
*/
waitForSecondSIM: function() {
var self = this;
this._timerForSIM2 = setTimeout(function() {
clearTimeout(self._timerForSIM2);
self.publishSIMSlotIsReady();
}, this.TIMEOUT_FOR_SIM2);
},
/**
* We have to make sure our simcards are ready and emit
* this event out to notify sim_settings_helper & sim_lock
* do related operations.
*/
publishSIMSlotIsReady: function() {
if (!this.ready) {
this.ready = true;
window.dispatchEvent(new CustomEvent('simslotready'));
}
},
handleEvent: function ssm_handleEvent(evt) {
switch (evt.type) {
case 'iccdetected':
var slot = this.getSlotByIccId(evt.iccId);
if (slot) {
slot.update(IccManager.getIccById(evt.iccId));
// we are now in single sim device
if (!this.isMultiSIM()) {
this.publishSIMSlotIsReady();
} else {
// we are now in DSDS device
// if we have one simcard already
if (this.hasOnlyOneSIMCardDetected()) {
this.waitForSecondSIM();
} else {
// we have two simcards already
clearTimeout(this._timerForSIM2);
this.publishSIMSlotIsReady();
}
}
}
break;
}
}
};
SIMSlotManager.init();
}(window));

View file

@ -0,0 +1,78 @@
/* exported STKHelper */
'use strict';
/**
* stk_helper.js: SIM Toolkit utilities.
*/
var STKHelper = {
getIconCanvas: function(mozStkIcon) {
if (!mozStkIcon || !mozStkIcon.pixels ||
!mozStkIcon.width || !mozStkIcon.height) {
return null;
}
if (mozStkIcon.pixels.length < (mozStkIcon.width * mozStkIcon.height)) {
console.error('Not enough pixels for the required dimension: ' +
mozStkIcon.width + 'x' + mozStkIcon.height);
return null;
}
if (!mozStkIcon.codingScheme) {
mozStkIcon.codingScheme = 'basic';
}
var canvas = document.createElement('canvas');
canvas.setAttribute('width', mozStkIcon.width);
canvas.setAttribute('height', mozStkIcon.height);
var ctx = canvas.getContext('2d', { willReadFrequently: true });
var imageData = ctx.createImageData(mozStkIcon.width, mozStkIcon.height);
var pixel = 0, pos = 0;
var data = imageData.data;
for (var y = 0; y < mozStkIcon.height; y++) {
for (var x = 0; x < mozStkIcon.width; x++) {
data[pos++] = (mozStkIcon.pixels[pixel] & 0xFF000000) >>> 24; // Red
data[pos++] = (mozStkIcon.pixels[pixel] & 0xFF0000) >>> 16; // Green
data[pos++] = (mozStkIcon.pixels[pixel] & 0xFF00) >>> 8; // Blue
data[pos++] = (mozStkIcon.pixels[pixel] & 0xFF); // Alpha
pixel++;
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
},
// Helper to retrieve text from MozStkTextMessage
getMessageText: function(stkMessage, defaultMsgL10nId, defaultMsgL10nArgs) {
if (this.isIconSelfExplanatory(stkMessage)) {
return {raw: ''};
}
// stk Message could be a specific string
var text;
if (stkMessage === 'string' || stkMessage instanceof String) {
text = stkMessage;
} else {
text = stkMessage ? stkMessage.text : '';
}
if (!text && defaultMsgL10nId) {
return {
id: defaultMsgL10nId,
args: defaultMsgL10nArgs
};
}
return {raw: text};
},
isIconSelfExplanatory: function(stkMessage) {
return (stkMessage && stkMessage.icons && stkMessage.iconSelfExplanatory);
},
getFirstIconRawData: function(stkItem) {
return stkItem.icons && stkItem.icons.length > 0 ? stkItem.icons[0] : null;
}
};

View file

@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
/* global ERROR_DIALOG_CLOSED_BY_USER */
/* global ERROR_INVALID_SYNC_ACCOUNT */
/* global ERROR_NO_KEY_FETCH_TOKEN */
/* global ERROR_OFFLINE */
/* global ERROR_SYNC_APP_TRY_LATER */
/* global ERROR_UNKNOWN */
/**
* These errors are shared by all Firefox Sync related modules. The key should
* be used as a constant and the value can be used as a l10n string key if
* needed.
*/
(function(exports) {
// Unrecoverable errors will cause the Sync state machine to transition to
// the 'disabled' state.
const unrecoverableErrors = {
// The user closed the FxA dialog during the sync enabling state.
ERROR_DIALOG_CLOSED_BY_USER: 'fxsync-error-dialog-closed-by-user',
// Cannot get Firefox Accounts assertion.
ERROR_GET_FXA_ASSERTION: 'fxsync-error-get-fxa-assertion',
// The user logged in with an invalid account. This is probably because the
// account has no crypto keys associated and we are unable to generate them
// so far.
ERROR_INVALID_SYNC_ACCOUNT: 'fxsync-error-invalid-account',
// The device is offline.
ERROR_OFFLINE: 'fxsync-error-offline',
// The current login is invalid as it does not contain the token to fetch
// sync keys.
ERROR_NO_KEY_FETCH_TOKEN: 'fxsync-error-no-key-fetch-token',
// Cannot register sync request.
ERROR_REQUEST_SYNC_REGISTRATION: 'fxsync-error-request-fxsync-registration',
// The request options passed to the Sync app failed basic DataType checks.
ERROR_SYNC_INVALID_REQUEST_OPTIONS: 'fxsync-error-invalid-request-options',
// Cannot perform sync request.
ERROR_SYNC_REQUEST: 'fxsync-error-request-failed',
ERROR_UNKNOWN: 'fxsync-error-unknown'
};
const recoverableErrors = {
// The app was killed while performing a sync operation.
ERROR_SYNC_APP_KILLED: 'fxsync-error-app-killed',
// The app is already performing a sync request.
ERROR_SYNC_APP_SYNC_IN_PROGRESS: 'fxsync-error-app-fxsync-in-progress',
// Error while trying to sync.
ERROR_SYNC_APP_GENERIC: 'fxsync-error-app-generic',
// The server cannot be reached.
ERROR_SYNC_APP_TRY_LATER: 'fxsync-error-app-try-later',
// A DataStore was updated by another app at the same time.
ERROR_SYNC_APP_RACE_CONDITION: 'fxsync-error-app-race-condition',
// The user is logged in with an unverified account.
ERROR_UNVERIFIED_ACCOUNT: 'fxsync-error-unverified-account'
};
exports.SyncUnrecoverableErrors = [];
Object.keys(unrecoverableErrors).forEach(key => {
exports[key] = unrecoverableErrors[key];
exports.SyncUnrecoverableErrors.push(unrecoverableErrors[key]);
});
exports.SyncRecoverableErrors = [];
Object.keys(recoverableErrors).forEach(key => {
exports[key] = recoverableErrors[key];
exports.SyncRecoverableErrors.push(recoverableErrors[key]);
});
// Map between external (FxA, Syncto, kinto.js) and Sync errors.
exports.SyncErrors = {
'invalid account': ERROR_INVALID_SYNC_ACCOUNT,
'No keyFetchToken': ERROR_NO_KEY_FETCH_TOKEN,
'OFFLINE': ERROR_OFFLINE,
'try later': ERROR_SYNC_APP_TRY_LATER,
'UI_ERROR': ERROR_DIALOG_CLOSED_BY_USER,
'unauthorized': ERROR_INVALID_SYNC_ACCOUNT,
'UNVERIFIED_ACCOUNT': ERROR_INVALID_SYNC_ACCOUNT,
'unrecoverable': ERROR_UNKNOWN
};
}(window));

View file

@ -0,0 +1,26 @@
'use strict';
/* global AdvancedTelemetryHelper */
/* global LazyLoader */
(function(exports) {
var namespace = 'telemetry_gaia_sync';
var dependencies = LazyLoader.load(
['/shared/js/settings_listener.js',
'/shared/js/advanced_telemetry_helper.js']);
var Telemetry = {
logUserAction: function(name) {
return dependencies.then(() => {
var histogramName = namespace + '_user_action_' + name;
var count = new AdvancedTelemetryHelper(
AdvancedTelemetryHelper.HISTOGRAM_COUNT,
histogramName);
count.add(1);
});
}
};
exports.Telemetry = Telemetry;
})(window);

View file

@ -0,0 +1,217 @@
'use strict';
// Utilities for sending data to the FxOS Telemetry Server
/* global asyncStorage, crypto, TextEncoder */
(function(exports) {
const DEFAULT_URL = 'https://fxos.telemetry.mozilla.org/submit/telemetry';
const DEFAULT_APPNAME = 'FirefoxOS';
const UNKNOWN = 'unknown';
const DOGFOOD_KEY = 'debug.performance_data.dogfooding';
const TR = TelemetryRequest;
// Set to true to enable debugging
TR.DEBUG = false;
// Create an XHR for the FxOS Telemetry Server.
// Arguments:
// args: Telemetry arguments
// - reason: 'reason' URL field (i.e 'ftu' or 'appusage'. required)
// - deviceID: unique ID tied to the device or request (required)
// - url: base URL for the telemetry server
// (default: https://fxos.telemetry.mozilla.org/submit/telemetry)
// - ver: Payload version (default: 1)
// - appName: appName URL field (default: FirefoxOS)
// - appUpdateChannel: update channel URL field (default: 'unknown')
// - appBuildID: buildID URL field (default 'unknown')
// - appVersion: app version URL field (default 'unknown')
// info: Arbitrary JSON serializable data that will be recorded by Telemetry
function TelemetryRequest(args, info) {
if (!args) {
throw new Error('No arguments');
}
if (!args.reason) {
throw new Error('No reason given');
}
if (!args.deviceID) {
throw new Error('No deviceID given');
}
// clone so we don't put data into the object that was given to us
this.info = info ? JSON.parse(JSON.stringify(info)) : {};
this.packet = {
ver: args.ver || 1,
info: this.info
};
this.info.reason = args.reason;
this.info.appName = args.appName || DEFAULT_APPNAME;
this.info.appUpdateChannel = args.appUpdateChannel || UNKNOWN;
this.info.appBuildID = args.appBuildID || UNKNOWN;
this.info.appVersion = args.appVersion || UNKNOWN;
var uriParts = [
args.url || DEFAULT_URL,
encodeURIComponent(args.deviceID),
encodeURIComponent(this.info.reason),
encodeURIComponent(this.info.appName),
encodeURIComponent(this.info.appVersion),
encodeURIComponent(this.info.appUpdateChannel),
encodeURIComponent(this.info.appBuildID)
];
this.url = uriParts.join('/');
}
function debug(...args) {
if (TR.DEBUG) {
args.unshift('[Telemetry]');
console.log.apply(console, args);
}
}
// Open and send the underlying XMLHttpRequest
// xhrAttrs: attributes to set on the XHR
// - timeout: The timeout for the request
// - onload / onerror / onabort / ontimeout: callbacks
TelemetryRequest.prototype.send = function(xhrAttrs) {
var xhr = new XMLHttpRequest({ mozSystem: true, mozAnon: true });
xhr.open('POST', this.url);
debug(this.url);
if (xhrAttrs && xhrAttrs.timeout) {
xhr.timeout = xhrAttrs.timeout;
}
xhr.setRequestHeader('Content-type', 'application/json');
xhr.responseType = 'text';
var data = JSON.stringify(this.packet);
xhr.send(data);
debug(data);
if (xhrAttrs) {
xhr.onload = xhrAttrs.onload;
xhr.onerror = xhrAttrs.onerror;
xhr.onabort = xhrAttrs.onabort;
xhr.ontimeout = xhrAttrs.ontimeout;
}
return xhr;
};
TelemetryRequest.getDeviceID = function(key) {
return new Promise(function(resolve, reject) {
if (!navigator.mozSettings) {
reject('Invalid mozSettings');
return;
}
var dogFood = navigator.mozSettings.createLock().get(DOGFOOD_KEY);
if (!dogFood) {
reject('Couldn\'t query ' + DOGFOOD_KEY);
return;
}
dogFood.onsuccess = function onSuccess() {
var isDogfooder = dogFood.result[DOGFOOD_KEY];
if (!isDogfooder && typeof key !== 'undefined') {
//return the stored uuid from the passed in key here
asyncStorage.getItem(key, function(value) {
if (value) {
resolve(value);
} else {
reject('No deviceID found');
}
});
} else {
var dialPromise = navigator.mozTelephony.dial('*#06#', 0);
dialPromise.then(function(call) {
return call.result;
}).then(function(result) {
// Check to make sure our telephony stack is initialized and
// has returned an imei.
if (result && result.success && (result.serviceCode === 'scImei')) {
return result.statusMessage;
} else {
reject('Unable to get IMEI');
}
}).then(function (imei) {
if (isDogfooder) {
resolve(imei);
} else {
return TelemetryRequest.createHash(imei);
}
}).then(function (hashed) {
resolve(hashed);
}).catch(reject);
}
};
});
};
TelemetryRequest.createHash = function createHash(id) {
return new Promise(function (resolve, reject) {
var salt = new Uint8Array([44, 27, 51, 17, 55, 64, 183, 55, 37, 31, 246,
196, 130, 73, 14, 95, 122, 241, 143, 18, 40, 109,
238, 59, 168, 82, 28, 199, 177, 37, 135, 98]);
var hexDigits = '0123456789ABCDEF';
var ftuID = '';
var macString = navigator.mozWifiManager.macAddress.replace(/:/g, '');
var bytes = new TextEncoder('utf-8').encode(id + macString);
crypto.subtle.importKey('raw', bytes, {name:'PBKDF2'},
false, ['deriveBits']).then(function (key) {
return crypto.subtle.deriveBits({
name: 'PBKDF2',
iterations: 1000,
salt: salt,
hash: 'SHA-1'
}, key, 160);
}).then(function (digest) {
var digestArray = new Uint8Array(digest).slice(0, 4);
digestArray.forEach(function(value) {
ftuID += (hexDigits[value >> 4] + hexDigits[value & 15]);
});
resolve(ftuID);
});
});
};
/*
* A utility function shared between telemetry modules that is used to get
* values for all of the specified settings. settingKeysAndDefaults is an
* object that maps settings keys to default values. We query the value of
* each of those settings and then create an object that maps keys to values
* (or to the default values) and pass that object to the callback function.
*/
TelemetryRequest.getSettings = function getSettings(settingKeysAndDefaults,
callback) {
var pendingQueries = 0;
var results = {};
var lock = window.navigator.mozSettings.createLock();
for (var key in settingKeysAndDefaults) {
var defaultValue = settingKeysAndDefaults[key];
query(key, defaultValue);
pendingQueries++;
}
function query(key, defaultValue) {
var request = lock.get(key);
request.onsuccess = function() {
var value = request.result[key];
if (value === undefined || value === null) {
value = defaultValue;
}
results[key] = value;
pendingQueries--;
if (pendingQueries === 0) {
callback(results);
}
};
}
};
exports.TelemetryRequest = TelemetryRequest;
}(window));

View file

@ -0,0 +1,96 @@
'use strict';
var rscheme = /^(?:[a-z\u00a1-\uffff0-9-+]+)(?::(?:\/\/)?)/i;
var UrlHelper = {
// Placeholder anchor tag to format URLs.
a: null,
getUrlFromInput: function urlHelper_getUrlFromInput(input) {
this.a = this.a || document.createElement('a');
this.a.href = input;
return this.a.href;
},
_getScheme: function(input) {
// This function returns one of followings
// - scheme + ':' (ex. http:)
// - scheme + '://' (ex. http://)
// - null
return (rscheme.exec(input) || [])[0];
},
hasScheme: function(input) {
return !!this._getScheme(input);
},
isURL: function urlHelper_isURL(input) {
return !UrlHelper.isNotURL(input);
},
isNotURL: function urlHelper_isNotURL(input) {
// in bug 904731, we use <input type='url' value=''> to
// validate url. However, there're still some cases
// need extra validation. We'll remove it til bug fixed
// for native form validation.
//
// for cases, ?abc and "a? b" which should searching query
var case1Reg = /^(\?)|(\?.+\s)/;
// for cases, pure string
var case2Reg = /[\?\.\s\:]/;
// for cases, data:uri and view-source:uri
var case3Reg = /^(data|view-source)\:/;
var str = input.trim();
if (case1Reg.test(str) || !case2Reg.test(str) ||
this._getScheme(str) === str) {
return true;
}
if (case3Reg.test(str)) {
return false;
}
// require basic scheme before form validation
if (!this.hasScheme(str)) {
str = 'http://' + str;
}
if (!this.urlValidate) {
this.urlValidate = document.createElement('input');
this.urlValidate.setAttribute('type', 'url');
}
this.urlValidate.setAttribute('value', str);
return !this.urlValidate.validity.valid;
},
/**
* Resolve a URL against a base URL.
*
* @param url String URL to resolve.
* @param baseURL String Base URL to resolve against.
* @returns String resolved URL or null.
*/
resolveUrl: function urlHelper_resolveURL(url, baseUrl) {
if (!url) {
return null;
}
try {
return new URL(url, baseUrl).href;
} catch(e) {
return null;
}
},
/**
* Get the hostname from a URL.
*
* @param String URL to process.
* @returns String hostname of URL.
*/
getHostname: function urlHelper_getHostname(url) {
try {
return new URL(url).hostname;
} catch(e) {
return null;
}
}
};

View file

@ -0,0 +1,7 @@
;;'use strict';var DateHelper={todayStarted:function dh_todayStarted(){var now=(new Date()).valueOf();return this.getMidnight(now);},yesterdayStarted:function dh_yesterdayStarted(){var now=(new Date()).valueOf();var dayAgo=now-86400000;return this.getMidnight(dayAgo);},thisWeekStarted:function dh_thisWeekStarted(){var now=new Date();var dayOfTheWeek=now.getDay();var firstDay=new Date(now.getFullYear(),now.getMonth(),now.getDate()-dayOfTheWeek);return this.getMidnight(firstDay);},thisMonthStarted:function dh_thisMonthStarted(){var now=new Date();var firstDay=(new Date(now.getFullYear(),now.getMonth(),1).valueOf());return firstDay;},lastSixMonthsStarted:function dh_lastSixMonthsStarted(){var now=new Date().valueOf();var sixMonthsAgo=now-2629743830*6;return sixMonthsAgo;},thisYearStarted:function dh_thisYearStarted(){var now=new Date();var firstDay=(new Date(now.getFullYear(),0).valueOf());return firstDay;},getMidnight:function dh_getMidnight(timestamp){var day=new Date(timestamp);var midnight=(new Date(day.getFullYear(),day.getMonth(),day.getDate(),0).valueOf());return midnight;}};var NumberHelper={zfill:function nh_zfill(string,len){var s=string;while(s.length<len){s='0'+s;}
return s;}};var StringHelper={fromUTF8:function ut_fromUTF8(str){var buf=new Uint8Array(str.length);for(var i=0;i<str.length;i++){buf[i]=str.charCodeAt(i);}
return buf;},camelCase:function ut_camelCase(str){var rdashes=/-(.)/g;return str.replace(rdashes,function replacer(str,p1){return p1.toUpperCase();});}};var HtmlHelper={createHighlightHTML:function ut_createHighlightHTML(text,terms){var toHighlight=[];var normalizedText=Normalizer.toAscii(text).toLowerCase();terms.forEach(function(term){term=Normalizer.toAscii(term).toLowerCase();var index=normalizedText.indexOf(term);while(index>=0){for(var i=0,length=term.length;i<length;i++){toHighlight[index+i]=true;}
index=normalizedText.indexOf(term,index+term.length);}});var fragment=document.createDocumentFragment();for(var i=0;i<text.length;i++){if(!toHighlight[i]){fragment.appendChild(document.createTextNode(text[i]));}else{var term='';while(toHighlight[i]){term+=text[i];i++;}
i--;var mark=document.createElement('mark');mark.textContent=term;fragment.appendChild(mark);}}
return fragment;},escapeHTML:function ut_escapeHTML(str,escapeQuotes){var span=document.createElement('span');span.textContent=str;span.innerHTML=span.innerHTML.replace(/\s/g,'&nbsp;');if(escapeQuotes){return span.innerHTML.replace(/"/g,'&quot;').replace(/'/g,'&#x27;');}
return span.innerHTML;},importElements:function importElements(context){var ids=[].slice.call(arguments,1);ids.forEach(function(id){context[StringHelper.camelCase(id)]=document.getElementById(id);});}};

View file

@ -0,0 +1,235 @@
// node-uuid/uuid.js
//
// Copyright (c) 2010 Robert Kieffer
// Dual licensed under the MIT and GPL licenses.
// Documentation and details at https://github.com/broofa/node-uuid
(function() {
var _global = this;
// Unique ID creation requires a high quality random # generator, but
// Math.random() does not guarantee "cryptographic quality". So we feature
// detect for more robust APIs, normalizing each method to return 128-bits
// (16 bytes) of random data.
var mathRNG, nodeRNG, whatwgRNG;
// Math.random()-based RNG. All platforms, very fast, unknown quality
var _rndBytes = new Array(16);
mathRNG = function() {
var r, b = _rndBytes, i = 0;
for (var i = 0, r; i < 16; i++) {
if ((i & 0x03) == 0) r = Math.random() * 0x100000000;
b[i] = r >>> ((i & 0x03) << 3) & 0xff;
}
return b;
}
// Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
// Node.js only, moderately fast, high quality
try {
var _rb = require('crypto').randomBytes;
nodeRNG = _rb && function() {
return _rb(16);
};
} catch (e) {}
// Select RNG with best quality
var _rng = nodeRNG || whatwgRNG || mathRNG;
// Buffer class to use
var BufferClass = typeof(Buffer) == 'function' ? Buffer : Array;
// Maps for number <-> hex string conversion
var _byteToHex = [];
var _hexToByte = {};
for (var i = 0; i < 256; i++) {
_byteToHex[i] = (i + 0x100).toString(16).substr(1);
_hexToByte[_byteToHex[i]] = i;
}
// **`parse()` - Parse a UUID into it's component bytes**
function parse(s, buf, offset) {
var i = (buf && offset) || 0, ii = 0;
buf = buf || [];
s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
if (ii < 16) { // Don't overflow!
buf[i + ii++] = _hexToByte[oct];
}
});
// Zero out remaining bytes if string was short
while (ii < 16) {
buf[i + ii++] = 0;
}
return buf;
}
// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
function unparse(buf, offset) {
var i = offset || 0, bth = _byteToHex;
return bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]];
}
// **`v1()` - Generate time-based UUID**
//
// Inspired by https://github.com/LiosK/UUID.js
// and http://docs.python.org/library/uuid.html
// random #'s we need to init node and clockseq
var _seedBytes = _rng();
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
var _nodeId = [
_seedBytes[0] | 0x01,
_seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
];
// Per 4.2.2, randomize (14 bit) clockseq
var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
// Previous uuid creation time
var _lastMSecs = 0, _lastNSecs = 0;
// See https://github.com/broofa/node-uuid for API details
function v1(options, buf, offset) {
var i = buf && offset || 0;
var b = buf || [];
options = options || {};
var clockseq = options.clockseq != null ? options.clockseq : _clockseq;
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
var msecs = options.msecs != null ? options.msecs : new Date().getTime();
// Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1;
// Time since last uuid creation (in msecs)
var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
// Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0 && options.clockseq == null) {
clockseq = clockseq + 1 & 0x3fff;
}
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
nsecs = 0;
}
// Per 4.2.1.2 Throw error if too many uuids are requested
if (nsecs >= 10000) {
throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
}
_lastMSecs = msecs;
_lastNSecs = nsecs;
_clockseq = clockseq;
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
msecs += 12219292800000;
// `time_low`
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
b[i++] = tl >>> 24 & 0xff;
b[i++] = tl >>> 16 & 0xff;
b[i++] = tl >>> 8 & 0xff;
b[i++] = tl & 0xff;
// `time_mid`
var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
b[i++] = tmh >>> 8 & 0xff;
b[i++] = tmh & 0xff;
// `time_high_and_version`
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
b[i++] = tmh >>> 16 & 0xff;
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
b[i++] = clockseq >>> 8 | 0x80;
// `clock_seq_low`
b[i++] = clockseq & 0xff;
// `node`
var node = options.node || _nodeId;
for (var n = 0; n < 6; n++) {
b[i + n] = node[n];
}
return buf ? buf : unparse(b);
}
// **`v4()` - Generate random UUID**
// See https://github.com/broofa/node-uuid for API details
function v4(options, buf, offset) {
// Deprecated - 'format' argument, as supported in v1.2
var i = buf && offset || 0;
if (typeof(options) == 'string') {
buf = options == 'binary' ? new BufferClass(16) : null;
options = null;
}
options = options || {};
var rnds = options.random || (options.rng || _rng)();
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = (rnds[6] & 0x0f) | 0x40;
rnds[8] = (rnds[8] & 0x3f) | 0x80;
// Copy bytes to buffer, if provided
if (buf) {
for (var ii = 0; ii < 16; ii++) {
buf[i + ii] = rnds[ii];
}
}
return buf || unparse(rnds);
}
// Export public API
var uuid = v4;
uuid.v1 = v1;
uuid.v4 = v4;
uuid.parse = parse;
uuid.unparse = unparse;
uuid.BufferClass = BufferClass;
// Export RNG options
uuid.mathRNG = mathRNG;
uuid.nodeRNG = nodeRNG;
uuid.whatwgRNG = whatwgRNG;
if (typeof(module) != 'undefined') {
// Play nice with node.js
module.exports = uuid;
} else {
// Play nice with browsers
var _previousRoot = _global.uuid;
// **`noConflict()` - (browser only) to reset global 'uuid' var**
uuid.noConflict = function() {
_global.uuid = _previousRoot;
return uuid;
}
_global.uuid = uuid;
}
}());

View file

@ -0,0 +1,136 @@
/* global Promise */
/* exported VersionHelper */
'use strict';
(function(exports) {
// Helper to work with version strings
// e.g. 2.0.0.1-prerelease becomes { major: 2, minor: 0 }
function Version(str) {
str = str || '0.0';
var parts = str.split('.');
this.major = parts[0];
this.minor = parts[1];
this.toString = function() {
return '' + str;
};
}
function InfoResult(current, previous) {
this.current = current;
this.previous = previous;
}
InfoResult.prototype.isUpgrade = function() {
var prev = this.previous,
curr = this.current,
isUpgrade = false;
// dont treat lack of previous version info as an upgrade
if (prev && curr) {
isUpgrade = curr.major > prev.major || curr.minor > prev.minor;
}
return isUpgrade;
};
InfoResult.prototype.delta = function() {
var prev = this.previous,
curr = this.current;
var delta = '';
if (prev) {
delta += prev.major + '.' +prev.minor;
}
delta += '..';
if (curr) {
delta += curr.major + '.' + curr.minor;
}
return delta;
};
// Helper to get settings
function getSetting(setting, cb, errback) {
var req = navigator.mozSettings.createLock().get(setting);
req.onsuccess = function() {
var value = req.result[setting];
if(typeof cb === 'function') {
cb(value);
}
};
req.onerror = function() {
if(typeof errback === 'function') {
errback(req.error);
}
};
}
function getCurrent() {
return new Promise(function(resolve, reject) {
getSetting('deviceinfo.os', function(value) {
resolve(value ? new Version(value) : null);
}, function(err) {
reject(err);
});
});
}
function getPrevious() {
return new Promise(function(resolve, reject) {
// XXX: we return null version when previous_os lookup fails
getSetting('deviceinfo.previous_os', function(value) {
resolve(value ? new Version(value) : null);
}, function(err) {
resolve(null);
});
});
}
function updatePrevious(str) {
var currentPromise = str ? Promise.resolve(new Version(str)) :
getCurrent();
var setPromise = new Promise(function(resolve, reject) {
currentPromise.then(function(version) {
// guard against empty deviceinfo.os
if (version) {
var req = navigator.mozSettings.createLock().set({
'deviceinfo.previous_os': version.toString()
});
req.onsuccess = function() {
resolve(version);
};
req.onerror = function() {
var reason = req.error;
reject(reason);
};
} else {
resolve(null);
}
}, function(err) {
reject('Error getting previous version: ', err);
});
});
return setPromise;
}
function getVersionInfo() {
var info = new InfoResult();
var infoPromise = new Promise(function(resolve, reject) {
Promise.all([
getCurrent().then(function(version) {
info.current = version;
}),
getPrevious().then(function(version) {
info.previous = version;
})
]).then(function() {
resolve(info);
}, reject);
});
return infoPromise;
}
exports.VersionHelper = {
updatePrevious: updatePrevious,
getVersionInfo: getVersionInfo
};
})(window);

View file

@ -0,0 +1,207 @@
/* exported WebManifestHelper */
'use strict';
/**
* Helper to fetch and process a Web Manifest.
*/
(function WebManifestHelper(exports) {
/**
* Get Manifest.
*
* @param {String} url Manifest URL
* @returns {Promise} A promise of a response containing manifest data.
*/
var getManifest = function(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('get', url, true);
xhr.responseType = 'json';
xhr.setRequestHeader('Accept', 'application/manifest+json');
xhr.onload = function() {
var status = xhr.status;
var webManifest;
if (status == 200) {
if (!xhr.response) {
// syntax error in the JSON can cause an empty response
reject(new Error('Empty JSON response to '+ url + ', ' +
'possibly syntax error'));
}
webManifest = processRawManifest(xhr.response || {}, url);
resolve(webManifest);
} else {
reject(status);
}
};
xhr.onerror = function(e) {
console.error('Unable to get web manifest');
reject(e.target.status);
};
xhr.send();
});
};
var processRawManifest = function(webManifest, manifestURL) {
if (!manifestURL) {
throw new Error('WebManifestHelper.processRawManifest:' +
' no manifestURL given');
}
if ('icons' in webManifest) {
// sanitize icons list
// 'src' members are URL instances resolved to manifest URL
webManifest.icons = processIcons(webManifest, manifestURL || '');
}
return webManifest;
};
const onlyDecimals = /^\d+$/,
anyRegEx = new RegExp('any', 'i');
/**
* Process the icons member of a web manifest.
*
* Copied from /dom/manifest/ManifestProcess.jsm in Gecko.
*
* @param {Object} manifest An un-processed manifest object.
* @param {String} baseURL The base URL the manifest was fetched from.
* @returns {Object} A processed manifest object.
*/
var processIcons = function(manifest, baseURL) {
const obj = {
objectName: 'manifest',
object: manifest,
property: 'icons',
expectedType: 'array'
},
icons = [];
var value = extractValue(obj);
if (Array.isArray(value)) {
//filter out icons with no "src" or src is empty string
var processableIcons = value.filter(
icon => icon && Object.prototype.hasOwnProperty.call(icon, 'src') &&
icon.src !== ''
);
for (var potentialIcon of processableIcons) {
var src = processSrcMember(potentialIcon, baseURL);
if(src !== undefined){
var icon = {
src: src,
type: processTypeMember(potentialIcon),
sizes: processSizesMember(potentialIcon),
density: processDensityMember(potentialIcon)
};
icons.push(icon);
}
}
}
return icons;
function processTypeMember(icon) {
const obj = {
objectName: 'icon',
object: icon,
property: 'type',
expectedType: 'string'
};
var value = extractValue(obj),
isParsable = (typeof value === 'string' && value.length > 0);
return (value === '' || !isParsable) ? undefined : value;
}
function processDensityMember(icon) {
const hasDensity = Object.prototype.hasOwnProperty.call(icon, 'density'),
rawValue = (hasDensity) ? icon.density : undefined,
value = parseFloat(rawValue),
result = (Number.isNaN(value) || value === +Infinity ||
value <= 0) ? 1.0 : value;
return result;
}
function processSrcMember(icon, baseURL) {
const obj = {
objectName: 'icon',
object: icon,
property: 'src',
expectedType: 'string'
},
value = extractValue(obj);
var url;
if (typeof value === 'string' && value.trim() !== '') {
try {
url = new URL(value, baseURL).href;
} catch (e) {}
}
return url;
}
function processSizesMember(icon) {
const sizes = [],
obj = {
objectName: 'icon',
object: icon,
property: 'sizes',
expectedType: 'string'
};
var value = extractValue(obj);
value = (value) ? value.trim() : value;
if (value) {
//split on whitespace and filter out invalid values
var validSizes = value.split(/\s+/).filter(isValidSizeValue);
validSizes.forEach((size) => sizes.push(size));
}
return sizes;
/*
* Implementation of HTML's link@size attribute checker
*/
function isValidSizeValue(size) {
if (anyRegEx.test(size)) {
return true;
}
size = size.toLowerCase();
if (!size.includes('x') || size.indexOf('x') !==
size.lastIndexOf('x')) {
return false;
}
//split left of x for width, after x for height
const width = size.substring(0, size.indexOf('x'));
const height = size.substring(size.indexOf('x') + 1, size.length);
const isValid = !(height.startsWith('0') || width.startsWith('0') ||
!onlyDecimals.test(width + height));
return isValid;
}
}
};
/*
* Extract a value from a manifest.
*
* This helper function is used to extract values from manifest members.
* It also reports conformance violations.
*
* Copied from /dom/manifest/ManifestProcess.jsm in Gecko.
*/
function extractValue(obj) {
var value = obj.object[obj.property];
//we need to special-case "array", as it's not a JS primitive
const type = (Array.isArray(value)) ? 'array' : typeof value;
if (type !== obj.expectedType) {
if (type !== 'undefined') {
var msg = 'Expected the ' + obj.objectName + 's ' + obj.property +
' member to be a ' + obj.expectedType;
console.warn(msg);
}
value = undefined;
}
return value;
}
exports.WebManifestHelper = {
getManifest: getManifest,
processRawManifest: processRawManifest
};
})(window);