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,23 @@
'use strict';
(function(exports) {
var AccessibilityHelper = {
/**
* For a set of tab elements, set aria-selected attribute in accordance with
* the current selection.
* @param {Object} selectedTab a tab to select object.
* @param {Array} tabs an array of tabs.
*/
setAriaSelected: function ah_setAriaSelected(selectedTab, tabs) {
// In case tabs is a NodeList, that does not have forEach.
Array.prototype.forEach.call(tabs, function setAriaSelectedAttr(tab) {
tab.setAttribute('aria-selected',
tab === selectedTab ? 'true' : 'false');
});
}
};
exports.AccessibilityHelper = AccessibilityHelper;
})(window);

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,112 @@
/* exported AirplaneModeHelper */
/*
* AirplaneModeHelper is a helper that makes apps enable / disable
* airplane mode easily. It will expose few methods for you :
*
* 1. AirplaneModeHelper.ready();
* 2. AirplaneModeHelper.setEnabled();
* 3. AirplaneModeHelper.getStatus();
* 4. AirplaneModeHelper.addEventListener('statechange', callback)
* 5. AirplaneModeHelper.removeEventListener('statechange', callback)
*
* If you want to call getStatus(), please make sure to put them inside
* an anonymous function in AirplaneModeHelper.ready() because this is
* an async call.
*
* Like this:
*
* AirplaneModeHelper.ready(function() {
* var status = AirplaneModeHelper.getStatus();
* });
*/
(function(exports) {
'use strict';
// constants
const kEventName = 'statechange';
const kCommunicationKey = 'airplaneMode.enabled';
const kStatusKey = 'airplaneMode.status';
// main
var AirplaneModeHelper = {
_mozSettings: window.navigator.mozSettings,
_callbacks: [],
_cachedStatus: '',
ready: function(cb) {
if (this._cachedStatus === '') {
var self = this;
this.addEventListener(kEventName, function onChangeEvent() {
// make sure _cachedStatus is definitely not ''
if (self._cachedStatus !== '') {
self.removeEventListener(kEventName, onChangeEvent);
cb();
}
});
} else {
cb();
}
},
getStatus: function() {
return this._cachedStatus;
},
addEventListener: function(eventName, callback) {
if (eventName === kEventName) {
this._callbacks.push(callback);
}
},
removeEventListener: function(eventName, callback) {
if (eventName === kEventName) {
var index = this._callbacks.indexOf(callback);
if (index >= 0) {
this._callbacks.splice(index, 1);
}
}
},
setEnabled: function(enabled) {
this.ready(function() {
var status = this.getStatus();
if (status === 'enabling' || status === 'disabling') {
// do nothing when transition
} else {
if (enabled && status === 'enabled' ||
!enabled && status === 'disabled') {
return;
}
var setObj = {};
setObj[kCommunicationKey] = enabled;
this._mozSettings.createLock().set(setObj);
}
}.bind(this));
},
init: function() {
var self = this;
// init _cachedStatus
var lock = window.navigator.mozSettings.createLock();
var req = lock.get(kStatusKey);
req.onsuccess = function() {
self._cachedStatus = req.result[kStatusKey];
self._callbacks.forEach(function(callback) {
callback(self._cachedStatus);
});
};
this._mozSettings.addObserver(kStatusKey, function(evt) {
var currentStatus = evt.settingValue;
self._cachedStatus = currentStatus;
self._callbacks.forEach(function(callback) {
callback(currentStatus);
});
});
}
};
exports.AirplaneModeHelper = AirplaneModeHelper;
// by default, we will add observer immediately for you right after
// you include this helper
AirplaneModeHelper.init();
})(this);

84
gaia/shared/js/apn_helper.js Executable file
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));

180
gaia/shared/js/async_storage.js Executable file
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
};
}());

137
gaia/shared/js/basket_client.js Executable file
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();
};
}
};

124
gaia/shared/js/binary_search.js Executable file
View file

@ -0,0 +1,124 @@
'use strict';
/* exported utils */
var utils = window.utils || {};
/**
* This function performs a binary search over an already sorted array
* target is the target item to search for
* array is the sorted array
* options is an optional object which may contain
* the start and end position (from, to)
* an optional arrayField which indicates the object property that contains the
* comparable item and transform and compare functions
*
* Returns an array with the positions on which the target item was found
*
*/
utils.binarySearch = function(target, array, options) {
var arrayField = options.arrayField,
transformFunction = options.transformFunction,
compareFunction = options.compareFunction;
// Obtains the comparable item by transforming if necessary
function getItem(array, index) {
var item = array[index];
if (arrayField) {
item = item[arrayField];
if (typeof transformFunction === 'function') {
item = transformFunction(item);
}
}
return item;
}
// Compares the target with an array item
function compare(target, item) {
var out;
if (typeof compareFunction === 'function') {
out = compareFunction(target, item);
}
else {
if (typeof target === 'string') {
out = target.localeCompare(item);
}
else {
out = target.toString().localeCompare(item);
}
}
return out;
}
var from = options.from;
if (typeof from === 'undefined') {
from = 0;
}
var to = options.to;
if (typeof to === 'undefined') {
to = array.length - 1;
}
if (to < from) {
// Not found
return [];
}
var middleIndex = Math.floor((to - from) / 2);
var item = getItem(array, from + middleIndex);
var compareResult = compare(target, item);
if (compareResult === 0) {
// Once a result is found let's iterate in both directions to get the rest
// Just in case there are more than one result
var results = [from + middleIndex];
var next = from + middleIndex + 1;
var finish = false;
while (next <= (array.length - 1) && !finish) {
item = getItem(array, next);
if (compare(target, item) === 0) {
results.push(next);
}
else {
finish = true;
}
next++;
}
finish = false;
next = from + middleIndex - 1;
while (next >= 0 && !finish) {
item = getItem(array, next);
if (compare(target, item) === 0) {
results.push(next);
}
else {
finish = true;
}
next--;
}
return results;
}
else if (compareResult < 0) {
return utils.binarySearch(target, array, {
from: from,
to: to - middleIndex - 1,
arrayField: arrayField,
transformFunction: transformFunction,
compareFunction: compareFunction
});
}
else {
return utils.binarySearch(target, array, {
from: from + middleIndex + 1,
to: to,
arrayField: arrayField,
transformFunction: transformFunction,
compareFunction: compareFunction
});
}
};

430
gaia/shared/js/blobview.js Executable file
View file

@ -0,0 +1,430 @@
/* exported BlobView */
'use strict';
var BlobView = (function() {
function fail(msg) {
throw Error(msg);
}
var decoderCache = {};
function getDecoder(encoding) {
if (encoding in decoderCache) {
return decoderCache[encoding];
}
var decoder = decoderCache[encoding] = new TextDecoder(encoding);
return decoder;
}
// This constructor is for internal use only.
// Use the BlobView.get() factory function or the getMore instance method
// to obtain a BlobView object.
function BlobView(blob, sliceOffset, sliceLength, slice,
viewOffset, viewLength, littleEndian)
{
this.blob = blob; // The parent blob that the data is from
this.sliceOffset = sliceOffset; // The start address within the blob
this.sliceLength = sliceLength; // How long the slice is
this.slice = slice; // The ArrayBuffer of slice data
this.viewOffset = viewOffset; // The start of the view within the slice
this.viewLength = viewLength; // The length of the view
this.littleEndian = littleEndian; // Read little endian by default?
// DataView wrapper around the ArrayBuffer
this.view = new DataView(slice, viewOffset, viewLength);
// These fields mirror those of DataView
this.buffer = slice;
this.byteLength = viewLength;
this.byteOffset = viewOffset;
this.index = 0; // The read methods keep track of the read position
}
// Async factory function
BlobView.get = function(blob, offset, length, callback, littleEndian) {
if (offset < 0) {
fail('negative offset');
}
if (length < 0) {
fail('negative length');
}
if (offset > blob.size) {
fail('offset larger than blob size');
}
// Don't fail if the length is too big; just reduce the length
if (offset + length > blob.size) {
length = blob.size - offset;
}
var slice = blob.slice(offset, offset + length);
var reader = new FileReader();
reader.readAsArrayBuffer(slice);
reader.onloadend = function() {
var result = null;
if (reader.result) {
result = new BlobView(blob, offset, length, reader.result,
0, length, littleEndian || false);
}
callback(result, reader.error);
};
};
// Synchronous factory function for use when you have an array buffer and want
// to treat it as a BlobView (e.g. to use the readXYZ functions). We need
// this for the music app, which uses an array buffer to hold
// de-unsynchronized ID3 frames.
BlobView.getFromArrayBuffer = function(buffer, offset, length, littleEndian) {
return new BlobView(null, offset, length, buffer, offset, length,
littleEndian);
};
BlobView.prototype = {
constructor: BlobView,
// This instance method is like the BlobView.get() factory method,
// but it is here because if the current buffer includes the requested
// range of bytes, they can be passed directly to the callback without
// going back to the blob to read them
getMore: function(offset, length, callback) {
// If we made this BlobView from an array buffer, there's no blob backing
// it, and so it's impossible to get more data.
if (!this.blob) {
fail('no blob backing this BlobView');
}
if (offset >= this.sliceOffset &&
offset + length <= this.sliceOffset + this.sliceLength) {
// The quick case: we already have that region of the blob
callback(new BlobView(this.blob,
this.sliceOffset, this.sliceLength, this.slice,
offset - this.sliceOffset, length,
this.littleEndian));
}
else {
// Otherwise, we have to do an async read to get more bytes
BlobView.get(this.blob, offset, length, callback, this.littleEndian);
}
},
// Set the default endianness for the other methods
littleEndian: function() {
this.littleEndian = true;
},
bigEndian: function() {
this.littleEndian = false;
},
// These "get" methods are just copies of the DataView methods, except
// that they honor the default endianness
getUint8: function(offset) {
return this.view.getUint8(offset);
},
getInt8: function(offset) {
return this.view.getInt8(offset);
},
getUint16: function(offset, le) {
return this.view.getUint16(offset,
le !== undefined ? le : this.littleEndian);
},
getInt16: function(offset, le) {
return this.view.getInt16(offset,
le !== undefined ? le : this.littleEndian);
},
getUint32: function(offset, le) {
return this.view.getUint32(offset,
le !== undefined ? le : this.littleEndian);
},
getInt32: function(offset, le) {
return this.view.getInt32(offset,
le !== undefined ? le : this.littleEndian);
},
getFloat32: function(offset, le) {
return this.view.getFloat32(offset,
le !== undefined ? le : this.littleEndian);
},
getFloat64: function(offset, le) {
return this.view.getFloat64(offset,
le !== undefined ? le : this.littleEndian);
},
// These "read" methods read from the current position in the view and
// update that position accordingly
readByte: function() {
return this.view.getInt8(this.index++);
},
readUnsignedByte: function() {
return this.view.getUint8(this.index++);
},
readShort: function(le) {
var val = this.view.getInt16(this.index,
le !== undefined ? le : this.littleEndian);
this.index += 2;
return val;
},
readUnsignedShort: function(le) {
var val = this.view.getUint16(this.index,
le !== undefined ? le : this.littleEndian);
this.index += 2;
return val;
},
readInt: function(le) {
var val = this.view.getInt32(this.index,
le !== undefined ? le : this.littleEndian);
this.index += 4;
return val;
},
readUnsignedInt: function(le) {
var val = this.view.getUint32(this.index,
le !== undefined ? le : this.littleEndian);
this.index += 4;
return val;
},
readFloat: function(le) {
var val = this.view.getFloat32(this.index,
le !== undefined ? le : this.littleEndian);
this.index += 4;
return val;
},
readDouble: function(le) {
var val = this.view.getFloat64(this.index,
le !== undefined ? le : this.littleEndian);
this.index += 8;
return val;
},
// Methods to get and set the current position
tell: function() {
return this.index;
},
remaining: function() {
return this.byteLength - this.index;
},
seek: function(index) {
if (index < 0) {
fail('negative index');
}
if (index > this.byteLength) {
fail('index greater than buffer size');
}
this.index = index;
},
advance: function(n) {
var index = this.index + n;
if (index < 0) {
fail('advance past beginning of buffer');
}
// It's usual that when we finished reading one target view,
// the index is advanced to the start(previous end + 1) of next view,
// and the new index will be equal to byte length(the last index + 1),
// we will not fail on it because it means the reading is finished,
// or do we have to warn here?
if (index > this.byteLength) {
fail('advance past end of buffer');
}
this.index = index;
},
// Additional methods to read other useful things
getUnsignedByteArray: function(offset, n) {
return new Uint8Array(this.buffer, offset + this.viewOffset, n);
},
// Additional methods to read other useful things
readUnsignedByteArray: function(n) {
var val = new Uint8Array(this.buffer, this.index + this.viewOffset, n);
this.index += n;
return val;
},
getBit: function(offset, bit) {
var byte = this.view.getUint8(offset);
return (byte & (1 << bit)) !== 0;
},
getUint24: function(offset, le) {
var b1, b2, b3;
if (le !== undefined ? le : this.littleEndian) {
b1 = this.view.getUint8(offset);
b2 = this.view.getUint8(offset + 1);
b3 = this.view.getUint8(offset + 2);
}
else { // big end first
b3 = this.view.getUint8(offset);
b2 = this.view.getUint8(offset + 1);
b1 = this.view.getUint8(offset + 2);
}
return (b3 << 16) + (b2 << 8) + b1;
},
readUint24: function(le) {
var value = this.getUint24(this.index, le);
this.index += 3;
return value;
},
// There are lots of ways to read strings. We support binary (raw 8-bit
// codepoints), Latin-1, UTF-8, and UTF-16. For the latter three, we also
// support null-terminated versions.
getBinaryText: function(offset, len) {
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
return String.fromCharCode.apply(String, bytes);
},
readBinaryText: function(len) {
var s = this.getBinaryText(this.index, len);
this.index += len;
return s;
},
getLatin1Text: function(offset, len) {
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
return getDecoder('latin1').decode(bytes);
},
readLatin1Text: function(len) {
var s = this.getLatin1Text(this.index, len);
this.index += len;
return s;
},
getUTF8Text: function(offset, len) {
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
return getDecoder('utf-8').decode(bytes);
},
readUTF8Text: function(len) {
var s = this.getUTF8Text(this.index, len);
this.index += len;
return s;
},
// Get UTF16 text. If le is not specified, expect a BOM to define
// endianness. If le is true, read UTF16LE, if false, UTF16BE.
getUTF16Text: function(offset, len, le) {
if (len % 2) {
fail('len must be a multiple of two');
}
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
if (le === null || le === undefined) {
var BOM = (bytes[0] << 8) + bytes[1];
if (BOM === 0xFEFF) {
bytes = bytes.subarray(2);
le = false;
} else if (BOM === 0xFFFE) {
bytes = bytes.subarray(2);
le = true;
} else {
le = true;
}
}
var encoding = le ? 'utf-16le' : 'utf-16be';
return getDecoder(encoding).decode(bytes);
},
readUTF16Text: function(len, le) {
var s = this.getUTF16Text(this.index, len, le);
this.index += len;
return s;
},
// Read 4 bytes, ignore the high bit and combine them into a 28-bit
// big-endian unsigned integer.
// This format is used by the ID3v2 metadata.
getID3Uint28BE: function(offset) {
var b1 = this.view.getUint8(offset) & 0x7f;
var b2 = this.view.getUint8(offset + 1) & 0x7f;
var b3 = this.view.getUint8(offset + 2) & 0x7f;
var b4 = this.view.getUint8(offset + 3) & 0x7f;
return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
},
readID3Uint28BE: function() {
var value = this.getID3Uint28BE(this.index);
this.index += 4;
return value;
},
// Read bytes up to and including a null terminator, but never
// more than size bytes. And return as a Latin1 string. If advance_by_size
// is true, this will always seek ahead by `size` bytes, even if a null
// character was found earlier.
readNullTerminatedLatin1Text: function(size, advance_by_size = false) {
var bytes = new Uint8Array(this.buffer, this.viewOffset + this.index,
size);
var nil = bytes.indexOf(0);
if (nil !== -1) {
bytes = bytes.subarray(0, nil);
}
var s = getDecoder('latin1').decode(bytes);
if (nil === -1 || advance_by_size) {
this.index += size;
} else {
this.index += nil + 1;
}
return s;
},
// Read bytes up to and including a null terminator, but never
// more than size bytes. And return as a UTF8 string. If advance_by_size
// is true, this will always seek ahead by `size` bytes, even if a null
// character was found earlier.
readNullTerminatedUTF8Text: function(size, advance_by_size = false) {
var bytes = new Uint8Array(this.buffer, this.viewOffset + this.index,
size);
var nil = bytes.indexOf(0);
if (nil !== -1) {
bytes = bytes.subarray(0, nil);
}
var s = getDecoder('utf-8').decode(bytes);
if (nil === -1 || advance_by_size) {
this.index += size;
} else {
this.index += nil + 1;
}
return s;
},
// Read UTF16 text. If le is not specified, expect a BOM to define
// endianness. If le is true, read UTF16LE, if false, UTF16BE
// Read until we find a null-terminator, but never more than size bytes.
// If advance_by_size is true, this will always seek ahead by `size` bytes,
// even if a null character was found earlier.
readNullTerminatedUTF16Text: function(size, le, advance_by_size = false) {
if (size % 2) {
fail('size must be a multiple of two');
}
for (var len = 0; len < size; len += 2) {
if (this.getUint16(this.index + len, le) === 0) {
break;
}
}
var s = this.getUTF16Text(this.index, len, le);
if (len === size || advance_by_size) {
this.index += size;
} else {
this.index += len + 2;
}
return s;
}
};
// We don't want users of this library to accidentally call the constructor
// instead of using one of the factory functions, so we return a dummy object
// instead of the real constructor. If someone really needs to get at the
// real constructor, the contructor property of the prototype refers to it.
return {
get: BlobView.get,
getFromArrayBuffer: BlobView.getFromArrayBuffer
};
}());

View file

@ -0,0 +1,322 @@
/* exported BluetoothHelper */
'use strict';
var BluetoothHelper = function() {
var profiles = {
'HFP': 0x111E,
'A2DP': 0x110D
};
var _bluetooth = window.navigator.mozBluetooth;
var _isReady = false;
var _callbacks = [];
var _adapter = null;
var _oldAdapter = null; // The varaible is accessed for v1 only.
var _v2 = true;
var _ready = function(callback) {
if (!callback || !_bluetooth) {
return;
}
if (_isReady) {
callback();
} else {
_callbacks.push(callback);
}
};
var _handleRequest = function(request, callback, errorcb) {
request.onsuccess = function() {
if (callback) {
callback(request.result);
}
};
request.onerror = function() {
console.log('Error handling bluetooth request');
if (errorcb) {
errorcb();
}
};
};
// run callbacks when adapter is ready
var _processCallbacks = function() {
if (_adapter) {
_isReady = true;
_callbacks.forEach(function(callback) {
callback();
});
// clean up the _callback queue
_callbacks = [];
} else {
// We can do nothing without default adapter.
console.log('BluetoothHelper(): connot get default adapter yet');
}
};
// API v2 get adapter via bluetooth
var _fetchAdapterV2 = function() {
// need time to get bluetooth adapter at first run
_bluetooth.onattributechanged = function onManagerAttributeChanged(evt) {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
case 'defaultAdapter':
console.log('defaultAdapter changed. address:',
_bluetooth.defaultAdapter.address);
_adapter = _bluetooth.defaultAdapter;
_processCallbacks();
break;
default:
break;
}
}
};
_adapter = _bluetooth.defaultAdapter;
if (_adapter) {
_processCallbacks();
}
};
// API v1 get adapter via bluetooth
var _fetchAdapter = function() {
var req = _bluetooth.getDefaultAdapter();
if (req) {
req.onsuccess = function() {
if (_adapter) {
_oldAdapter = _adapter;
}
_isReady = true;
_adapter = req.result;
// Put the callback function of onpairedstatuschanged to the new adapter
// because the new adapter won't remember those callback function which
// is registered before. In other word, we get a new adpater after
// turned on/off Bluetooth. The new adapter have no registered callback.
if (_oldAdapter && _oldAdapter.onpairedstatuschanged) {
_adapter.onpairedstatuschanged = _oldAdapter.onpairedstatuschanged;
}
_callbacks.forEach(function(callback) {
callback();
});
// clean up the _callback queue
_callbacks = [];
};
req.onerror = function() {
// We can do nothing without default adapter.
console.log('BluetoothHelper(): connot get default adapter!!!');
};
}
};
var _getAdapter = function() {
if (_v2) {
_fetchAdapterV2();
} else {
_fetchAdapter();
}
};
var _resetAdapter = function() {
if (_adapter) {
_oldAdapter = _adapter;
}
// clean up state and adapter
_isReady = false;
_adapter = null;
};
// init
if (_bluetooth) {
// detect API version
if (typeof(_bluetooth.onattributechanged) === 'undefined') {
_v2 = false;
}
if (_v2) {
_bluetooth.onadapteradded = function onAdapterAdded(evt) {
_getAdapter();
};
} else {
_bluetooth.addEventListener('enabled', _getAdapter);
_bluetooth.addEventListener('adapteradded', _getAdapter);
_bluetooth.addEventListener('disabled', _resetAdapter);
}
_getAdapter();
}
return {
profiles: profiles,
answerWaitingCall: function() {
_ready(function() {
_adapter.answerWaitingCall();
});
},
ignoreWaitingCall: function() {
_ready(function() {
_adapter.ignoreWaitingCall();
});
},
toggleCalls: function() {
_ready(function() {
_adapter.toggleCalls();
});
},
getConnectedDevicesByProfile: function(profileID, cb) {
_ready(function() {
_handleRequest(_adapter.getConnectedDevices(profileID), cb);
});
},
connectSco: function(cb) {
_ready(function() {
_handleRequest(_adapter.connectSco(), cb);
});
},
disconnectSco: function(cb) {
_ready(function() {
_handleRequest(_adapter.disconnectSco(), cb);
});
},
getPairedDevices: function(cb) {
_ready(function() {
_handleRequest(_adapter.getPairedDevices(), cb);
});
},
getAddress: function(cb) {
if (_v2) {
console.log('getAddress function is deprecated');
return;
}
_ready(function() {
var address = _adapter.address;
cb(address);
});
},
setPairingConfirmation: function(address, confirmed) {
if (_v2) {
console.log('setPairingConfirmation API is deprecated');
return;
}
_ready(function() {
_adapter.setPairingConfirmation(address, confirmed);
});
},
setPinCode: function(address, pincode) {
if (_v2) {
console.log('setPairingConfirmation API is deprecated');
return;
}
_ready(function() {
_adapter.setPinCode(address, pincode);
});
},
setPasskey: function(address, key) {
if (_v2) {
console.log('setPairingConfirmation API is deprecated');
return;
}
_ready(function() {
_adapter.setPasskey(address, key);
});
},
isScoConnected: function(cb, errorcb) {
_ready(function() {
_handleRequest(_adapter.isScoConnected(), cb, errorcb);
});
},
sendMediaMetaData: function(metadata, cb, errorcb) {
_ready(function() {
_handleRequest(_adapter.sendMediaMetaData(metadata), cb, errorcb);
});
},
sendMediaPlayStatus: function(metadata, cb, errorcb) {
_ready(function() {
_handleRequest(_adapter.sendMediaPlayStatus(metadata), cb, errorcb);
});
},
set onhfpstatuschanged(callback) {
_ready(function() {
_adapter.onhfpstatuschanged = callback;
});
},
set onscostatuschanged(callback) {
_ready(function() {
_adapter.onscostatuschanged = callback;
});
},
set ona2dpstatuschanged(callback) {
_ready(function() {
_adapter.ona2dpstatuschanged = callback;
});
},
set onrequestmediaplaystatus(callback) {
_ready(function() {
_adapter.onrequestmediaplaystatus = callback;
});
},
set onpairedstatuschanged(callback) {
if (_v2) {
console.log('onpairedstatuschanged API is deprecated');
return;
}
_ready(function() {
_adapter.onpairedstatuschanged = callback;
});
},
v2: _v2, // expose API version for app reference
// bypass the enable/disable state if works in APIv1
enable: function() {
if (_v2) {
_ready(function() {
_adapter.enable();
});
} else {
console.log('enable is not support in v1 API!');
}
},
disable: function() {
if (_v2) {
_ready(function() {
_adapter.disable();
});
} else {
console.log('disable is not support in v1 API!');
}
}
};
};

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,45 @@
(function(exports) {
'use strict';
/**
* ComponentUtils is a utility which allows us to use web components earlier
* than we should be able to by polyfilling and fixing platform deficiencies.
*/
exports.ComponentUtils = {
/**
* Injects a style.css into both the shadow root and outside the shadow
* root so we can style projected content. Bug 992249.
*/
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';
// Wait for the stylesheet to load before injecting
// it into the shadow-dom. This is to work around
// bug 1003294, let's review once landed.
style.addEventListener('load', function() {
// Put a clone of the stylesheet into the shadow-dom.
// We have to use two <style> nodes, to work around
// the lack of `:host` (bug 992245) and `:content`
// (bug 992249) selectors. Once we have those we
// can do all our styling from a single style-sheet
// within the shadow-dom.
if (self.shadowRoot) {
self.shadowRoot.appendChild(style.cloneNode(true));
}
self.style.visibility = '';
});
}
};
}(window));

200
gaia/shared/js/confirm.js Executable file
View file

@ -0,0 +1,200 @@
/* exported ConfirmDialog */
'use strict';
var ConfirmDialog = (function() {
var isAnimationPlaying = false;
var isShown = false;
var doc = document; try { if (!document.getElementById("confirmation-message")) doc = parent.document; } catch (_) {}
var screen = doc.getElementById('confirmation-message');
var titleNode = screen.querySelector('h1');
var messageNode = screen.querySelector('p');
var action1Node = screen.querySelector('button:nth-of-type(1)');
var action1Callback;
var action2Node = screen.querySelector('button:nth-of-type(2)');
var action2Callback;
var oldzIndex;
var callBackAndPreventDefault = function callBackAndPreventDefault(ev) {
if (ev.target === action1Node) {
action1Callback();
ev.preventDefault();
return false;
} else if (ev.target === action2Node) {
action2Callback();
ev.preventDefault();
return false;
}
};
var _hide = function _hide() {
// Reset the 'confirmation-message' element and its children to their
// initial values and hide it.
if (!isShown) {
return;
}
titleNode.removeAttribute('data-l10n-id');
titleNode.className = '';
messageNode.removeAttribute('data-l10n-id');
messageNode.className = '';
action1Node.removeAttribute('data-l10n-id');
action1Node.onclick = null;
action2Node.removeAttribute('data-l10n-id');
action2Node.onclick = null;
screen.classList.remove('fade-in');
screen.classList.add('fade-out');
isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('confirmdialoghiding'));
screen.addEventListener('animationend', function cd_fadeOut(ev) {
action1Node.className = '';
action2Node.className = '';
isAnimationPlaying = false;
screen.removeEventListener('animationend', cd_fadeOut);
screen.classList.add('no-opacity');
screen.classList.add('hide');
isShown = false;
});
};
/**
* Method that shows the dialog
* @param {String} title the title of the dialog. null or empty for
* no title.
* @param {String} msg message for the dialog.
* @param {Object} action1 {title, isDanger, callback} object.
* @param {Object} action2 {title, isDanger, callback} object.
*/
var _show = function _show(title, msg, action1, action2, options) {
var setL10nAttributes = function (element, options){
if ('string' === typeof options) {
document.l10n.setAttributes(element, options);
}
if(options.id) {
document.l10n.setAttributes(element, options.id, options.args);
}
};
if (options && options.zIndex) {
oldzIndex = screen.style.zIndex;
screen.style.zIndex = options.zIndex;
}
else {
oldzIndex = null;
}
if (title) {
setL10nAttributes(titleNode, title);
} else {
titleNode.classList.add('hide');
}
if (msg) {
setL10nAttributes(messageNode, msg);
} else {
messageNode.classList.add('hide');
}
if (action1) {
if (action1.title) {
setL10nAttributes(action1Node, action1.title);
}
if (action1.isDanger) {
action1Node.classList.add('danger');
}
if (action1.isRecommend) {
action1Node.classList.add('recommend');
}
if (action1.callback) {
action1Callback = action1.callback;
action1Node.onclick = callBackAndPreventDefault;
}
if (!action2) {
action2Node.classList.add('hide');
action1Node.classList.add('full');
}
}
if (action2) {
if (action2.title) {
setL10nAttributes(action2Node, action2.title);
}
if (action2.isDanger) {
action2Node.classList.add('danger');
}
if (action2.isRecommend) {
action2Node.classList.add('recommend');
}
if (action2.callback) {
action2Callback = action2.callback;
action2Node.onclick = callBackAndPreventDefault;
}
if (!action1) {
action1Node.classList.add('hide');
action2Node.classList.add('full');
}
}
if (isShown) {
return;
}
screen.classList.remove('hide');
screen.classList.remove('fade-out');
screen.classList.add('fade-in');
isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('confirmdialogshowing'));
screen.addEventListener('animationend', function cd_fadeIn(ev) {
isAnimationPlaying = false;
screen.removeEventListener('animationend', cd_fadeIn);
screen.classList.remove('no-opacity');
isShown = true;
});
};
return {
hide: function dialog_hide() {
// Reset the 'confirmation-message' element and its children to their
// initial values and hide it.
if (oldzIndex) {
screen.style.zIndex = oldzIndex;
oldzIndex = null;
}
if (!isAnimationPlaying) {
_hide();
return;
}
screen.addEventListener('animationend',
function cd_hideWhenFinished(ev) {
screen.removeEventListener('animationend', cd_hideWhenFinished);
_hide();
}
);
},
/**
* Method that shows the dialog
* @param {String} title the title of the dialog. null or empty for
* no title.
* @param {String} msg message for the dialog.
* @param {Object} action1 {title, isDanger, callback} object.
* @param {Object} action2 {title, isDanger, callback} object.
*/
show: function dialog_show(title, msg, action1, action2, options) {
if (!isAnimationPlaying) {
_show(title, msg, action1, action2, options);
return;
}
screen.addEventListener('animationend',
function cd_showWhenFinished(ev) {
screen.removeEventListener('animationend', cd_showWhenFinished);
_show(title, msg, action1, action2, options);
}
);
}
};
}());

386
gaia/shared/js/contact2vcard.js Executable file
View file

@ -0,0 +1,386 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* globals setImmediate, Normalizer */
/**
* ContactToVcard provides the functionality necessary to export from
* MozContacts to vCard 3.0 (https://www.ietf.org/rfc/rfc2426.txt). The reason
* to choose the 3.0 standard instead of the 4.0 one is that some systems
* most notoriously Android 4.x don't seem to be able to import vCard 4.0.
*/
(function(exports) {
'use strict';
/** Mapping between contact fields and equivalent vCard fields */
var VCARD_MAP = {
'fax' : 'FAX',
'faxoffice' : 'FAX,WORK',
'faxhome' : 'FAX,HOME',
'faxother' : 'FAX',
'home' : 'HOME',
'mobile' : 'CELL',
'pager' : 'PAGER',
'personal' : 'HOME',
'pref' : 'PREF',
'text' : 'TEXT',
'textphone' : 'TEXTPHONE',
'voice' : 'VOICE',
'work' : 'WORK'
};
var CRLF = '\r\n';
/** Field list to be skipped when converting to vCard */
var VCARD_SKIP_FIELD = ['fb_profile_photo'];
var VCARD_VERSION = '3.0';
var HEADER = 'BEGIN:VCARD' + CRLF + 'VERSION:' + VCARD_VERSION + CRLF;
var FOOTER = 'END:VCARD' + CRLF;
function blobToBase64(blob, cb) {
var reader = new FileReader();
reader.onload = function() {
var dataUrl = reader.result;
var base64 = dataUrl.split(',')[1];
cb(base64);
};
reader.readAsDataURL(blob);
}
function ISODateString(d) {
if (typeof d === 'string') {
d = new Date(d);
}
var str = d.toISOString();
// Remove the milliseconds field
return str.slice(0, str.indexOf('.')) + 'Z';
}
/**
* Given an array with contact fields (usually containing only one field),
* returns the equivalent vcard field
*
* @param {Array} sourceField source field from a MozContact
* @param {String} vcardField vCard field name
* @return {Array} Array of vCard string entries
*/
function fromContactField(sourceField, vcardField) {
if (!sourceField || !sourceField.length) {
return [];
}
// Goes to the entries in the given field (usually only one but potentially
// more) and transforms them into string-based, vCard ones.
return sourceField.map(function(field) {
var str = vcardField;
/**
* If the field doesn't have an equivalent in vcard standard.
* Incompatible fields are stored in `VCARD_SKIP_FIELD`.
*
* @type {boolean}
*/
var skipField = false;
var types = [];
// Checks existing types and converts them to vcard types if necessary
// and fill `types` array with the final types.
if (Array.isArray(field.type)) {
var fieldType = field.type.map(function(aType) {
var out = '';
if (aType) {
aType = aType.trim().toLowerCase();
if (VCARD_SKIP_FIELD.indexOf(aType) !== -1) {
skipField = true;
}
out = VCARD_MAP[aType] || aType;
}
return out;
});
types = types.concat(fieldType);
}
if (skipField) {
return;
}
if (field.pref && field.pref === true) {
types.push('PREF');
}
if (types.length) {
str += ';TYPE=' + types.join(',');
}
return str + ':' + (field.value || '');
});
}
function fromStringArray(sourceField, vcardField) {
if (!sourceField) {
return '';
}
return vcardField + ':' + sourceField.join(',');
}
function joinFields(fields) {
return fields.filter(function(f) { return !!f; }).join(CRLF);
}
function toBlob(vcard, type) {
return new Blob([vcard], {'type': type});
}
/**
* Convenience function that converts an array of contacts into a text/vcard
* blob. The blob is passed to the callback once the conversion is done.
*
* @param {Array} contacts An array of mozContact objects.
* @param {Function} callback A function invoked with the generated blob.
*/
function ContactToVcardBlob(contacts, callback, options) {
var targetType = options && options.type || 'text/vcard';
if(targetType.indexOf('charset') === -1) {
targetType += '; charset=utf-8';
}
if (typeof callback !== 'function') {
throw Error('callback() is undefined or not a function');
}
var str = '';
ContactToVcard(contacts, function append(vcards, nCards) {
str += vcards;
}, function success() {
str = str ? toBlob(str) : null;
callback(toBlob(str, targetType));
});
}
/**
* Converts an array of contacts to a string of vCards. The conversion is
* done in batches. For every batch the append callback is invoked with a
* string of vCards and the number of contacts in the batch. Once all
* contacts have been processed the success callback is invoked.
*
* @param {Array} contacts An array of mozContact objects.
* @param {Function} append A function taking two parameters, the first one
* will be passed a string of vCards and the second an integer
* representing the number of contacts in the string.
* @param {Function} success A function with no parameters that will be
* invoked once all the contacts have been processed.
* @param {Number} batchSize An optional parameter specifying the maximum
* number of characters that should be added to the output string
* before invoking the append callback. If this paramter is not
* provided a default value of 1MiB will be used instead.
*/
function ContactToVcard(contacts, append, success, batchSize, skipPhoto) {
var vCardsString = '';
var nextIndex = 0;
var cardsInBatch = 0;
batchSize = batchSize || (1024 * 1024);
if (typeof append !== 'function') {
throw Error('append() is undefined or not a function');
}
if (typeof success !== 'function') {
throw Error('append() is undefined or not a function');
}
/**
* Append the vCard obtained by converting the contact to the string of
* vCards and if necessary pass the string to the user-specified callback
* function. If we're not done processing all the contacts start processing
* the following one.
*
* @param {String} vcard The string obtained from the previously processed
* contact.
*/
function appendVCard(vcard) {
if (vcard.length > 0) {
vCardsString += HEADER + vcard + CRLF + FOOTER;
}
nextIndex++;
cardsInBatch++;
/* Invoke the user-provided callback if we've filled the current batch or
* if we don't have more contacts to process. */
if ((vCardsString.length > batchSize) ||
(nextIndex === contacts.length)) {
append(vCardsString, cardsInBatch);
cardsInBatch = 0;
vCardsString = '';
}
if (nextIndex < contacts.length) {
processContact(contacts[nextIndex]);
} else {
success();
}
}
/**
* Process a contact and invokes appendVCard with the resulting vCard
* string.
*
* @param {Object} contacts A mozContact object.
*/
function processContact(ct) {
if (navigator.mozContact && !(ct instanceof navigator.mozContact)) {
console.error('An instance of mozContact was expected');
setImmediate(function() { appendVCard(''); });
return;
}
/*
* N TYPE
* The structured type value corresponds, in
* sequence, to the Family Name, Given Name, Additional Names, Honorific
* Prefixes, and Honorific Suffixes. The text components are separated
* by the SEMI-COLON character (ASCII decimal 59). Individual text
* components can include multiple text values (e.g., multiple
* Additional Names) separated by the COMMA character (ASCII decimal
* 44). This type is based on the semantics of the X.520 individual name
* attributes. The property MUST be present in the vCard object.
**/
var n = 'n:' + ([
ct.familyName,
ct.givenName,
ct.additionalName,
ct.honorificPrefix,
ct.honorificSuffix
].map(function(f) {
f = f || [''];
return f.join(',') + ';';
}).join(''));
// vCard standard does not accept contacts without 'n' or 'fn' fields.
if (n === 'n:;;;;;' || !ct.name) {
setImmediate(function() { appendVCard(''); });
return;
}
var allFields = [
n,
fromStringArray(ct.name, 'FN'),
fromStringArray(ct.nickname, 'NICKNAME'),
fromStringArray(ct.category, 'CATEGORY'),
fromStringArray(ct.org, 'ORG'),
fromStringArray(ct.jobTitle, 'TITLE'),
fromStringArray(ct.note, 'NOTE'),
fromStringArray(ct.key, 'KEY')
];
if (ct.bday) {
allFields.push('BDAY:' + ISODateString(ct.bday));
}
if (ct.anniversary) {
allFields.push('ANNIVERSARY:' + ISODateString(ct.anniversary));
}
allFields.push.apply(allFields, fromContactField(ct.email, 'EMAIL'));
allFields.push.apply(allFields, fromContactField(ct.url, 'URL'));
allFields.push.apply(allFields, fromContactField(ct.tel, 'TEL'));
var adrs = fromContactField(ct.adr, 'ADR');
allFields.push.apply(allFields, adrs.map(function(adrStr, i) {
var orig = ct.adr[i];
return adrStr + ([
'',
'',
orig.streetAddress || '', orig.locality || '', orig.region || '',
orig.postalCode || '', orig.countryName || ''].join(';'));
}));
/**
* PHOTO TYPE
* The encoding MUST be reset to "b" using the ENCODING
* parameter in order to specify inline, encoded binary data. If the
* value is referenced by a URI value, then the default encoding of 8bit
* is used and no explicit ENCODING parameter is needed.
* Type value: A single value. The default is binary value. It can also
* be reset to uri value. The uri value can be used to specify a value
* outside of this MIME entity.
* Type special notes: The type can include the type parameter "TYPE" to
* specify the graphic image format type. The TYPE parameter values MUST
* be one of the IANA registered image formats or a non-standard image
* format.
*/
if (
(
typeof skipPhoto == 'undefined' ||
skipPhoto === false
) &&
ct.photo &&
ct.photo.length
) {
var photoMeta = ['PHOTO', 'ENCODING=b'];
var blob = ct.photo[0];
blobToBase64(blob, function(b64) {
if (blob.type) {
photoMeta.push('TYPE=' + blob.type);
}
allFields.push(photoMeta.join(';') + ':' + b64);
appendVCard(joinFields(allFields));
});
} else {
setImmediate(function() { appendVCard(joinFields(allFields)); });
}
}
processContact(contacts[0]);
}
// Generates a name for the contact returned as a vcard
function getVcardFilename(theContact) {
var out = '';
var givenName = Array.isArray(theContact.givenName) &&
theContact.givenName[0];
var familyName = Array.isArray(theContact.familyName) &&
theContact.familyName[0];
if (givenName) {
out = givenName;
}
if (familyName) {
if (out) {
out += '_';
}
out += familyName;
}
out = out || (Array.isArray(theContact.org) && theContact.org[0]);
out = out || (Array.isArray(theContact.tel) && theContact.tel[0] &&
( 'c' + '_' + theContact.tel[0].value) );
out = out || (Array.isArray(theContact.email) && theContact.email[0] &&
theContact.email[0].value);
out = Normalizer.toAscii(out).replace(/[\s+@#&?\+\$]/g, '');
if (out) {
return Promise.resolve(out + '.vcf');
}
return navigator.mozL10n.formatValue('noName').then(name => name + '.vcf');
}
exports.ContactToVcard = ContactToVcard;
exports.ContactToVcardBlob = ContactToVcardBlob;
exports.VcardFilename = getVcardFilename;
})(this);

View file

@ -0,0 +1,71 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
(function(exports) {
'use strict';
function getThumbnail(contact) {
return getOnePhoto(contact, 'begin');
}
function getFullResolution(contact) {
return getOnePhoto(contact, 'end');
}
function getOnePhoto(contact, position) {
if (!contact || !contact.photo || !contact.photo.length) {
return null;
}
if (contact.photo.length === 1) {
return contact.photo[0];
}
// For FB Linked contacts we need to give preference to the local photo
// [0] is the full resolution photo and [1] is the thumbnail
var photos = contact.photo;
var category = contact.category;
if (Array.isArray(category) && category.indexOf('fb_linked') !== -1) {
// Check whether we have a new linked contact or a legacy contact
if (photos.length >= 4) {
return photos[(position == 'begin') ? 1 : 0];
}
// In the case of a legacy contact we always return full resolution
// in order to ensure we are always giving preference to local photo
return photos[0];
}
photos = photosBySize(contact);
var index = (position == 'begin') ? 0 : photos.length - 1;
return photos[index];
}
function photosBySize(contact) {
var photos = contact.photo.slice(0);
photos.sort(function(p1, p2) {
if (size(p1) < size(p2)) {
return -1;
}
if (size(p1) > size(p2)) {
return 1;
}
return 0;
});
return photos;
}
// For legacy purpose we support data URLs and blobs
function size(photo) {
if (typeof photo == 'string') {
return photo.length; // length of the data URL
}
return photo.size; // size of the blob in bytes
}
exports.ContactPhotoHelper = {
getThumbnail: getThumbnail,
getFullResolution: getFullResolution
};
})(window);

View file

@ -0,0 +1,222 @@
'use strict';
/* exported ContactsButtons */
/* globals LazyLoader, MozActivity, MultiSimActionButton, Normalizer,
SmsIntegration, TelephonyHelper, utils */
var ContactsButtons = {
DEFAULT_TEL_TYPE: 'other',
DEFAULT_EMAIL_TYPE: 'other',
_listContainer: null,
_contactDetails: null,
_activityHandler: null,
init: function init(listContainer, contactDetails, activityHandler) {
this._listContainer = listContainer;
this._contactDetails = contactDetails;
this._activityHandler = activityHandler;
},
_enableCalls: function enableCalls() {
this._contactDetails.classList.remove('calls-disabled');
},
_disableCalls: function disableCalls() {
this._contactDetails.classList.add('calls-disabled');
},
_call: function call(phoneNumber, cardIndex) {
this._disableCalls();
var enableCalls = this._enableCalls.bind(this);
LazyLoader.load('/dialer/js/telephony_helper.js', () => {
TelephonyHelper.call(phoneNumber, cardIndex, enableCalls, enableCalls,
enableCalls, enableCalls);
});
},
/* For security reasons we cannot directly call MmiManager.send() thus we
* need to special-case MMI numbers. This function detects MMI numbers in
* their most common form; other forms (such as short-string MMI codes)
* don't make sense for the contacts app and thus aren't considered. */
_isMMI: function _isMMI(number) {
return number.charAt(number.length - 1) === '#';
},
// Check current situation and setup different listener for the button
_setupPhoneButtonListener: function setupPhoneButtonListener(button, number) {
var self = this;
if (this._activityHandler &&
this._activityHandler.currentActivityIsNot(['open'])) {
button.addEventListener('click', this._onPickNumber.bind(this));
} else if (this._isMMI(number)) {
button.addEventListener('click', this._onMMICode.bind(this));
} else if (navigator.mozTelephony) {
LazyLoader.load(['/shared/js/multi_sim_action_button.js'], function() {
/* jshint nonew: false */
new MultiSimActionButton(button, self._call.bind(self),
'ril.telephony.defaultServiceId',
() => number);
});
}
},
// If we are currently handing an activity, send the phone
// number as result of clicking in the phone button.
_onPickNumber: function onPickNumber(evt) {
var number = evt.target.dataset.tel;
if (this._activityHandler) {
this._activityHandler.postPickSuccess({ number: number });
}
},
// If the phone number stored in a contact is a MMI code,
// launch the dialer with that specific code.
_onMMICode: function onMMICode(evt) {
var number = evt.target.dataset.tel;
// For security reasons we cannot directly call MmiManager.send(). We
// need to show the MMI number in the dialer instead.
/* jshint nonew: false */
new MozActivity({
name: 'dial',
data: {
type: 'webtelephony/number',
number: number
}
});
},
_onSendSmsClicked: function onSendSmsClicked(evt) {
var tel = evt.target.dataset.tel;
LazyLoader.load('/shared/js/contacts/sms_integration.js', () => {
SmsIntegration.sendSms(tel);
});
},
_onEmailOrPickClick: function onEmailOrPickClick(evt) {
evt.preventDefault();
var email = evt.target.dataset.email;
this.sendEmailOrPick(email);
return false;
},
renderPhones: function cd_renderPhones(contact) {
if (!contact.tel) {
return;
}
var telLength = this._getLength(contact.tel);
for (var tel = 0; tel < telLength; tel++) {
var currentTel = contact.tel[tel];
var typeKey = currentTel.type;
var escapedType = Normalizer.escapeHTML(currentTel.type, true).trim();
var carrier = Normalizer.escapeHTML(currentTel.carrier || '', true) || '';
if (!escapedType) {
typeKey = this.DEFAULT_TEL_TYPE;
}
var telField = {
value: Normalizer.escapeHTML(currentTel.value, true) || '',
type: escapedType,
'type_l10n_id': typeKey,
carrier: carrier,
i: tel
};
var phonesTemplate =
document.querySelector('#phone-details-template-\\#i\\#');
var template = utils.templates.render(phonesTemplate, telField);
// Add event listeners to the phone template components
var sendSmsButton = template.querySelector('#send-sms-button-' + tel);
sendSmsButton.dataset.tel = telField.value;
sendSmsButton.addEventListener('click',
this._onSendSmsClicked.bind(this));
var callOrPickButton = template.querySelector('#call-or-pick-' + tel);
callOrPickButton.dataset.tel = telField.value;
this._setupPhoneButtonListener(callOrPickButton, telField.value);
if (carrier) {
var carrierWrapperElt = template.querySelector('.carrier-wrapper');
carrierWrapperElt.hidden = false;
}
this._listContainer.appendChild(template);
}
},
renderEmails: function cd_renderEmails(contact) {
if (!contact.email) {
return;
}
var emailLength = this._getLength(contact.email);
for (var email = 0; email < emailLength; email++) {
var currentEmail = contact.email[email];
var emailField = {
value: Normalizer.escapeHTML(currentEmail.value, true) || '',
'type_l10n_id': currentEmail.type,
i: email
};
var emailsTemplate =
document.querySelector('#email-details-template-\\#i\\#');
var template = utils.templates.render(emailsTemplate, emailField);
// Send MMS to the email address
var sendSmsButton =
template.querySelector('#send-sms-to-email-button-' + email);
sendSmsButton.dataset.tel = emailField.value;
sendSmsButton.addEventListener('click',
this._onSendSmsClicked.bind(this));
// Add event listeners to the phone template components
var emailButton = template.querySelector('#email-or-pick-' + email);
emailButton.dataset.email = emailField.value;
emailButton.addEventListener('click',
this._onEmailOrPickClick.bind(this));
this._listContainer.appendChild(template);
}
},
reMark: function(field, value, remarkClass) {
var selector = '[data-' + field + '="' + value + '"]';
var elements = this._listContainer.querySelectorAll(selector);
if (typeof remarkClass === 'undefined') {
remarkClass = 'remark';
}
for (var i = 0; i < elements.length; i++) {
elements[i].classList.add(remarkClass);
}
},
_getLength: function(prop) {
if (!prop || !prop.length) {
return 0;
}
return prop.length;
},
sendEmailOrPick: function(address) {
try {
// We don't check the email format, lets the email
// app do that
/* jshint nonew: false */
new MozActivity({
name: 'new',
data: {
type: 'mail',
URI: 'mailto:' + address
}
});
} catch (e) {
console.error('WebActivities unavailable? : ' + e);
}
}
};

View file

@ -0,0 +1,113 @@
'use strict';
/* global utils */
window.ContactsCleaner = function(contacts) {
this.lcontacts = contacts;
var total = contacts.length;
var next = 0;
var self = this;
var CHUNK_SIZE = 5;
var numResponses = 0;
var notifyClean = false;
var mustHold = false;
var holded = false;
var mustFinish = false;
this.start = function() {
mustHold = holded = mustFinish = false;
if (total > 0) {
cleanContacts(0);
}
else if (typeof self.onsuccess === 'function') {
window.setTimeout(self.onsuccess, 0);
}
};
this.hold = function() {
mustHold = true;
};
this.finish = function() {
mustFinish = true;
if (holded) {
notifySuccess();
}
};
this.resume = function() {
mustHold = holded = mustFinish = false;
window.setTimeout(function resume_clean() {
cleanContacts(next);
});
};
this.performClean = function(contact, number, cbs) {
var req = navigator.mozContacts.remove(utils.misc.toMozContact(contact));
req.number = number;
req.onsuccess = cbs.success;
req.onerror = function(e) {
errorHandler(contact.id, e.target.error);
};
};
function successHandler(e) {
if (notifyClean || typeof self.oncleaned === 'function') {
notifyClean = true;
// Avoiding race condition so the cleaned element is cached
var cleaned = e.target.number;
window.setTimeout(function() {
self.oncleaned(cleaned);
},0);
}
continueCb();
}
function errorHandler(contactid, error) {
if (typeof self.onerror === 'function') {
self.onerror(contactid, error);
}
continueCb();
}
function cleanContacts(from) {
for (var idx = from; idx < (from + CHUNK_SIZE) && idx < total; idx++) {
var contact = contacts[idx];
self.performClean(contact, idx, {
success: successHandler
});
}
}
function notifySuccess() {
if (typeof self.onsuccess === 'function') {
window.setTimeout(self.onsuccess);
}
}
function continueCb() {
next++;
numResponses++;
if (next < total && numResponses === CHUNK_SIZE) {
numResponses = 0;
if (!mustHold && !mustFinish) {
cleanContacts(next);
}
else if (mustFinish && !holded) {
notifySuccess();
}
if (mustHold) {
holded = true;
}
}
else if (next >= total) {
// End has been reached
notifySuccess();
}
} // function
}; // ContactsCleaner

View file

@ -0,0 +1,83 @@
'use strict';
/* exported ContactsDatastore */
/**
* This object will be useful for contact providers. It provides convenient
* access to a contacts datastore
*
*/
var ContactsDatastore = (function() {
function ContactsDatastore(provider) {
this.provider = provider;
this._datastoreLoaded = false;
this._datastoreLoading = false;
}
function promiseResolution (resolve, reject) {
/*jshint validthis:true */
this._datastoreLoading = false;
this._datastoreLoaded = (this.datastore !== null);
if (!this.datastore) {
reject({
name: 'DatastoreNotFound'
});
}
else {
resolve(this.datastore);
}
}
ContactsDatastore.prototype = {
_DS_LOADED_EVENT: 'datastore_loaded',
_DS_NAME: 'contacts',
getStore: function() {
var self = this;
return new Promise(function(resolve, reject) {
if (!navigator.getDataStores) {
reject({
name: 'DatastoreNotEnabled'
});
return;
}
if (self._datastoreLoaded) {
resolve(self.datastore);
return;
}
if (self._datastoreLoading) {
document.addEventListener(self._DS_LOADED_EVENT, function loaded() {
document.removeEventListener(self._DS_LOADED_EVENT, loaded);
promiseResolution.bind(self)(resolve, reject);
});
return;
}
self._datastoreLoading = true;
navigator.getDataStores(self._DS_NAME).then(function(stores) {
var targetOwner = self.provider;
for (var j = 0; j < stores.length; j++) {
if (stores[j].owner === targetOwner) {
self.datastore = stores[j];
break;
}
}
document.dispatchEvent(new CustomEvent(self._DS_LOADED_EVENT));
promiseResolution.bind(self)(resolve, reject);
}, reject);
});
}
};
return ContactsDatastore;
}());

View file

@ -0,0 +1,717 @@
/* global SimplePhoneMatcher */
/* global Normalizer */
/* exported Matcher */
(function(exports) {
'use strict';
function MatcherObj(pdataProvider) {
var blankRegExp = /\s+/g;
var FB_CATEGORY = 'facebook';
var FB_LINKED = 'fb_linked';
var dataProvider = pdataProvider;
// Multiple matcher Object. It tries to find a set of Contacts that match at
// least one of the targets passed as parameters
// ptargets: They are the targets (telephone numbers, emails) we want to
// find in the Contacts DB
// mmatchingOptions: are the matching options to be passed to the
// mozContacts API. Particularly filterBy and filterOp
// MultipleMatcher.onmatch is called when there is at least one matching
// MultipleMatcher.onmismatch is called when there are no matches
function MultipleMatcher(ptargets, pmatchingOptions) {
var next = 0;
var self = this;
var targets = ptargets;
var matchingOptions = pmatchingOptions;
var finalMatchings = {};
function doMatchBy(target, callbacks) {
var options = {
filterValue: target,
filterBy: matchingOptions.filterBy,
filterOp: matchingOptions.filterOp
};
var req = dataProvider.find(options);
req.onsuccess = function() {
var matchings = req.result.filter(function(aResult) {
return filterFacebook(aResult, matchingOptions.linkParams);
});
var filterBy = options.filterBy;
var sanitizedTarget = SimplePhoneMatcher.sanitizedNumber(target);
var targetVariants = SimplePhoneMatcher.generateVariants(target);
matchings.forEach(function(aMatching) {
if (matchingOptions.selfContactId === aMatching.id) {
return;
}
var field = options.filterBy[0];
var values = aMatching[field];
values.forEach(function(aValue) {
var value = aValue.value;
var sanitizedValue = SimplePhoneMatcher.sanitizedNumber(value);
var valueMatched = false;
if (targetVariants.length > 1) {
if (targetVariants.indexOf(sanitizedValue) !== -1) {
valueMatched = true;
}
} else if (SimplePhoneMatcher.generateVariants(sanitizedValue).
indexOf(sanitizedTarget) !== -1) {
valueMatched = true;
}
if (valueMatched) {
var matchings, matchingObj;
if (!finalMatchings[aMatching.id]) {
matchingObj = {
matchings: {},
matchingContact: aMatching
};
finalMatchings[aMatching.id] = matchingObj;
matchingObj.matchings[filterBy[0]] = [];
}
else {
matchingObj = finalMatchings[aMatching.id];
}
matchings = matchingObj.matchings[filterBy[0]];
// Avoinding to report multiple matchings due to variants
var sameValueMatchings = matchings.filter(function(matching) {
return (matching.matchedValue === value);
});
if (sameValueMatchings.length === 0) {
matchings.push({
'target': target,
'matchedValue': value
});
}
}
});
}); // matchings.forEach
carryOn();
}; // onsuccess
req.onerror = function(e) {
window.console.error('Error while trying to do the matching',
e.target.error.name);
notifyMismatch(self);
};
}
function carryOn() {
next++;
if (next < targets.length) {
doMatchBy(targets[next], callbacks);
}
else if (Object.keys(finalMatchings).length > 0) {
notifyMatch(self, finalMatchings);
}
else {
notifyMismatch(self);
}
}
function matched(contacts) {
carryOn();
}
var callbacks = {
onmatch: matched,
onmismatch: carryOn
};
this.start = function() {
doMatchBy(targets[0], callbacks);
};
}
function notifyMatch(obj, matchings) {
typeof obj.onmatch === 'function' && obj.onmatch(matchings);
}
function notifyMismatch(obj) {
typeof obj.onmismatch === 'function' && obj.onmismatch();
}
// Match a Contact by the field and filter specified as parameters
// Callbacks is an object that declares onmatch and onmismatch callbacks
function matchBy(aContact, field, filterOper, callbacks, poptions) {
var options = poptions || {};
var values = [];
if (Array.isArray(aContact[field])) {
aContact[field].forEach(function(aField) {
if (field === 'tel') {
// Bug 924378 Contacts API does not properly match phone numbers
// and their variants
var variants = SimplePhoneMatcher.generateVariants(aField.value);
variants.forEach(function(aVariant) {
values.push(aVariant);
});
}
else if (typeof aField.value === 'string') {
values.push(aField.value.trim());
}
});
}
if (values.length > 0) {
var matcher = new MultipleMatcher(values, {
filterBy: [field],
filterOp: filterOper,
selfContactId: aContact.id,
linkParams: options
});
matcher.onmatch = callbacks.onmatch;
matcher.onmismatch = callbacks.onmismatch;
matcher.start();
}
else {
notifyMismatch(callbacks);
}
}
function matchByTel(aContact, callbacks, options) {
matchBy(aContact, 'tel', 'match', callbacks, options);
}
function matchByEmail(aContact, callbacks, options) {
matchBy(aContact, 'email', 'startsWith', callbacks, options);
}
// Performs a matching for an incoming contact 'aContact' and the mode
// ['active', passive'] specified as parameter. Callbacks is an object
// with the onmatch and onmismatch functions that will be called after
// the matching process has finished
function doMatch(aContact, mode, callbacks) {
if (mode === 'passive') {
doMatchPassive(aContact, callbacks);
}
else if (mode === 'active') {
doMatchActive(aContact, callbacks);
}
}
// Implements the active mode 'matching'
function doMatchTelAndEmail(aContact, callbacks, options) {
var localCbs = {
onmatch: function(telMatches) {
var matchCbs = {
onmatch: function(mailMatches) {
// Have a unique set of matches
var allMatches = telMatches;
Object.keys(mailMatches).forEach(function(aMatch) {
if (!allMatches[aMatch]) {
allMatches[aMatch] = mailMatches[aMatch];
}
else {
allMatches[aMatch].matchings.email =
mailMatches[aMatch].matchings.email;
}
});
notifyMatch(callbacks, allMatches);
},
onmismatch: function() {
notifyMatch(callbacks, telMatches);
}
};
matchByEmail(aContact, matchCbs, options);
},
onmismatch: function() {
matchByEmail(aContact, callbacks, options);
}
};
matchByTel(aContact, localCbs, options);
}
function getSourceSimUrl(aContact) {
var out;
if (Array.isArray(aContact.category) &&
aContact.category.indexOf('sim') !== -1 &&
Array.isArray(aContact.url)) {
for (var j = 0; j < aContact.url.length; j++) {
var aUrl = aContact.url[j];
if (aUrl.type.indexOf('source') !== -1 &&
aUrl.type.indexOf('sim') !== -1) {
out = aUrl.value;
}
}
}
return out;
}
// Implements the silent mode matching
function doMatchPassive(aContact, callbacks) {
if (!hasName(aContact)) {
notifyMismatch(callbacks);
return;
}
// SIM Contacts are detected and matched accordingly
// We try to match by name and then try to see if they correspond to
// the same SIM contact. In that case matching is reported
var simUrl = getSourceSimUrl(aContact);
if (simUrl) {
performPassiveMatchForSimContact(simUrl, aContact, callbacks);
}
else {
performPassiveMatch(aContact, callbacks);
}
}
function performPassiveMatchForSimContact(simUrl, aContact, callbacks) {
var localCbs = {
onmatch: function(results) {
var matchingFound = false;
Object.keys(results).forEach(function(aResultId) {
var matchingContact = results[aResultId].matchingContact;
var matchingUrl = getSourceSimUrl(matchingContact);
if (matchingUrl && matchingUrl === simUrl) {
matchingFound = true;
var matchings = {};
matchings[aResultId] = {
matchingContact: matchingContact
};
callbacks.onmatch(matchings);
return;
}
});
if (!matchingFound) {
performPassiveMatch(aContact, callbacks);
}
},
onmismatch: function() {
performPassiveMatch(aContact, callbacks);
}
};
// Matching by name and then see if they have the same SIM contact source
matchByName(aContact, localCbs);
}
// Implements the silent mode matching
function performPassiveMatch(aContact, callbacks) {
var matchingsFound = {};
var localCbs = {
onmatch: function(results) {
// Results will contain contacts that match by tel or email
// Normalizing the strings
var names = [];
Object.keys(results).forEach(function(aResultId) {
var mContact = results[aResultId].matchingContact;
if (!hasName(mContact)) {
return;
}
// As the number of candidates here will be short a normal search
// will be conducted
var targetFN = null;
if (!isEmptyStr(aContact.familyName)) {
targetFN = Normalizer.toAscii(
aContact.familyName[0].trim().toLowerCase()).
replace(blankRegExp, '');
}
var targetGN = null;
if (!isEmptyStr(aContact.givenName)) {
targetGN = Normalizer.toAscii(
aContact.givenName[0].trim().toLowerCase()).
replace(blankRegExp, '');
}
// To support seamless matching of SIM contacts
var targetName = (targetGN || '') + (targetFN || '');
var mFamilyName = null;
var mGivenName = null;
if (!isEmptyStr(mContact.familyName)) {
mFamilyName = Normalizer.toAscii(
mContact.familyName[0].trim().toLowerCase()
).replace(blankRegExp, '');
}
if (!isEmptyStr(mContact.givenName)) {
mGivenName = Normalizer.toAscii(
mContact.givenName[0].trim().toLowerCase()
).replace(blankRegExp, '');
}
// To support seamless matching of SIM contacts
var mName = (mGivenName || '') + (mFamilyName || '');
names.push({
contact: mContact,
familyName: mFamilyName,
givenName: mGivenName,
name: mName
});
var matchingList = names.filter(function(obj) {
return ((obj.familyName === targetFN &&
obj.givenName === targetGN) ||
(obj.name && obj.name === targetName) && (
!Array.isArray(obj.contact.category) ||
obj.contact.category.indexOf(FB_CATEGORY) === -1));
});
matchingList.forEach(function(aMatching) {
var contact = aMatching.contact;
matchingsFound[contact.id] = {
matchingContact: contact
};
});
});
reconcileResults(aContact, matchingsFound, results, callbacks);
},
onmismatch: function() {
notifyMismatch(callbacks);
}
};
// Matching by email and phone number, then match by names
doMatchTelAndEmail(aContact, localCbs);
}
function doMatchActive(aContact, callbacks) {
var options = {
linkedMatched: {},
linkedTo: getLinkedTo(aContact)
};
var localCbs = {
onmatch: function(results) {
var cbsName = {
onmatch: function(nameResults) {
Object.keys(nameResults).forEach(function(aId) {
if (!results[aId]) {
results[aId] = nameResults[aId];
}
else {
results[aId].matchings.name =
nameResults[aId].matchings.name;
}
});
notifyMatch(callbacks, results);
},
onmismatch: function() {
notifyMatch(callbacks, results);
}
};
matchByName(aContact, cbsName, options);
},
onmismatch: function() {
matchByName(aContact, callbacks, options);
}
};
// Matching by email and phone number, then match by names
doMatchTelAndEmail(aContact, localCbs, options);
}
function notifyFindNameReady() {
document.dispatchEvent(new CustomEvent('by_name_ready'));
}
// Used for the active mode. Performs matching by familyName and givenName
function matchByName(aContact, callbacks, options) {
// First we try to find by familyName
// Afterwards we search by givenName
var isSimContact = (Array.isArray(aContact.category) &&
aContact.category.indexOf('sim') !== -1);
if ((isEmptyStr(aContact.familyName) || isEmptyStr(aContact.givenName)) &&
!isSimContact) {
notifyMismatch(callbacks);
return;
}
var finalResult = {};
var resultsByName = [];
if (!isEmptyStr(aContact.name)) {
var targetName = aContact.name[0].trim();
// Filter by familyName using startsWith. Gecko 'startsWith' operation
// acts as 'equal' but does not match case.
var reqName = dataProvider.find({
filterValue: targetName,
filterBy: ['name'],
filterOp: 'startsWith'
});
reqName.onsuccess = function() {
resultsByName = reqName.result.filter(function(aResult) {
return filterFacebook(aResult, options);
});
notifyFindNameReady();
};
reqName.onerror = function(e) {
window.console.warn('Error while trying to find by name: ',
e.target.error.name);
notifyFindNameReady();
};
}
else {
notifyFindNameReady();
}
if (isEmptyStr(aContact.familyName)) {
endOfMatchByName(finalResult, aContact, resultsByName, callbacks);
}
else {
var targetFamilyName = aContact.familyName[0].trim();
// Filter by familyName using startsWith. Gecko 'startsWith' operation
// acts as 'equal' but does not match case.
var reqFamilyName = dataProvider.find({
filterValue: targetFamilyName,
filterBy: ['familyName'],
filterOp: 'startsWith'
});
reqFamilyName.onsuccess = function() {
var results = reqFamilyName.result;
var givenNames = [];
var targetGN = null;
if (!isEmptyStr(aContact.givenName)) {
targetGN = Normalizer.toAscii(
aContact.givenName[0].trim().toLowerCase()).
replace(blankRegExp, '');
}
results.forEach(function(mContact) {
if (mContact.id === aContact.id || isEmptyStr(mContact.givenName)) {
return;
}
givenNames.push({
contact: mContact,
givenName: Normalizer.toAscii(
mContact.givenName[0].trim().toLowerCase()).
replace(blankRegExp, '')
});
});
var finalMatchings = givenNames.filter(function(obj) {
var gn = obj.givenName;
return ((gn === targetGN || targetGN.startsWith(gn) ||
gn.startsWith(targetGN)) &&
filterFacebook(obj.contact, options));
});
finalMatchings.forEach(function(aMatching) {
finalResult[aMatching.contact.id] = {
matchings: {
'name': [{
target: targetFamilyName,
matchedValue: Array.isArray(aMatching.contact.name) ?
aMatching.contact.name[0] :
getCompleteName(aMatching.contact)
}]
},
matchingContact: aMatching.contact
};
});
if (resultsByName) {
endOfMatchByName(finalResult, aContact, resultsByName, callbacks);
}
else {
document.addEventListener('by_name_ready', function nameReady() {
document.removeEventListener('by_name_ready', nameReady);
endOfMatchByName(finalResult, aContact, resultsByName, callbacks);
});
}
};
reqFamilyName.onerror = function(e) {
window.console.error('Error while trying to find by familyName: ',
e.target.error.name);
notifyMismatch(callbacks);
};
}
}
function getLinkedTo(contact) {
var out = null;
if (Array.isArray(contact.category)) {
var idx = contact.category.indexOf(FB_LINKED);
if (idx !== -1) {
out = contact.category[idx + 1];
}
}
return out;
}
function isFbLinked(contact) {
return (Array.isArray(contact.category) &&
contact.category.indexOf(FB_LINKED) !== -1);
}
function isFbContact(contact) {
return (Array.isArray(contact.category) &&
contact.category.indexOf(FB_CATEGORY) !== -1);
}
function filterFacebook(contact, linkParams) {
var out = false;
if (!isFbContact(contact)) {
out = true;
}
else if (isFbLinked(contact)) {
var linkedTo = getLinkedTo(contact);
var targetUid = linkParams.linkedTo || '';
var linkedMatched = linkParams.linkedMatched || {};
if (targetUid === linkedTo) {
out = true;
}
// It is only allowed to match one linked contact or various but all of
// them linked to the same FB friend
else if ((Object.keys(linkedMatched).length === 0 ||
linkedMatched[linkedTo]) && !targetUid) {
linkedMatched[linkedTo] = linkedTo;
out = true;
}
}
return out;
}
function endOfMatchByName(finalResult, aContact, resultsByName, callbacks) {
resultsByName.forEach(function(aResult) {
if (aResult.id === aContact.id) {
return;
}
var matchingObj = {
matchings: {
'name': [{
target: (Array.isArray(aContact.name) &&
(typeof aContact.name[0] === 'string')) ?
aContact.name[0].trim() : getCompleteName(aContact),
matchedValue: Array.isArray(aResult.name) ?
aResult.name[0] :
getCompleteName(aResult)
}]
},
matchingContact: aResult
};
if (!finalResult[aResult.id]) {
finalResult[aResult.id] = matchingObj;
}
else {
finalResult[aResult.id].matchings.name =
matchingObj.matchings.name;
}
});
if (Object.keys(finalResult).length > 0) {
notifyMatch(callbacks, finalResult);
}
else {
notifyMismatch(callbacks);
}
}
function getCompleteName(contact) {
var givenName = Array.isArray(contact.givenName) ?
contact.givenName[0] : '';
var familyName = Array.isArray(contact.familyName) ?
contact.familyName[0] : '';
var completeName = givenName && familyName ?
givenName + ' ' + familyName :
givenName || familyName;
return completeName;
}
function isEmpty(collection) {
return ((!Array.isArray(collection) ||
!collection[0]) ||
(collection[0] &&
collection[0].value &&
!collection[0].value.trim()));
}
function isEmptyStr(collection) {
return (!Array.isArray(collection) ||
(typeof collection[0] !== 'string') || !(collection[0].trim()));
}
function hasName(aContact) {
return (!isEmptyStr(aContact.givenName) ||
!isEmptyStr(aContact.familyName));
}
function reconcileResults(incomingContact, nameMatches, phoneMailMatches,
callbacks) {
var finalMatchings = {};
// Name matches drive all the process
Object.keys(nameMatches).forEach(function(aNameMatching) {
var matchingContact = nameMatches[aNameMatching].matchingContact;
var isPhoneMatching = Array.isArray(phoneMailMatches[aNameMatching].
matchings.tel);
var isMailMatching = Array.isArray(phoneMailMatches[aNameMatching].
matchings.email);
// Three cases under which a matching is considered
if (isPhoneMatching && isMailMatching) {
finalMatchings[aNameMatching] = phoneMailMatches[aNameMatching];
}
else if (isPhoneMatching && (isEmpty(incomingContact.email) ||
isEmpty(matchingContact.email))) {
finalMatchings[aNameMatching] = phoneMailMatches[aNameMatching];
}
else if (isMailMatching && (isEmpty(incomingContact.tel) ||
isEmpty(matchingContact.tel))) {
finalMatchings[aNameMatching] = phoneMailMatches[aNameMatching];
}
});
if (Object.keys(finalMatchings).length > 0) {
notifyMatch(callbacks, finalMatchings);
}
else {
notifyMismatch(callbacks);
}
}
// Exported method
this.match = doMatch;
Object.defineProperty(this, 'dataProvider', {
set: function(theProvider) {
dataProvider = theProvider;
}
});
}
exports.Matcher = new MatcherObj(navigator.mozContacts);
}(window));

View file

@ -0,0 +1,378 @@
/* globals Promise, SimplePhoneMatcher, utils, ContactPhotoHelper */
'use strict';
var contacts = window.contacts || {};
contacts.Merger = (function() {
var DEFAULT_ADR_TYPE = 'home';
var DEFAULT_TEL_TYPE = 'other';
var DEFAULT_EMAIL_TYPE = 'other';
// Performs the merge passing the master contact and matching contacts
// The master contact is the one that contains the info with more priority
// The matchingContacts are the contacts which information will merged with
// the master. It is an Array in which the elements should be ordered
// by priority i.e. the data from the first elements might take precedence
// over the data of the last elements according to the merging rules.
//
// Each element in the array is an object with the following keys:
// * matchingContact: the Contact object matching one or more targets.
// * matchings: optional, an object which entries are arrays of
// field-matching objects indexed by the field name.
//
// Each field-matching object has two fields:
// * target: the value that field matched with.
// * matchedValue: the value from the matchingContact field matching the
// target.
//
// Here is an example:
// {
// matchingContact: { ... },
// matchings: {
// 'tel': [{
// target: '600000000',
// matchedValue: '+34600000000'
// }]
// }
// }
//
// The `matchings` field can be not present if the matching contact comes
// from an external source not related with the matching algorithm.
function doMerge(pmasterContact, pmatchingContacts, callbacks) {
window.setTimeout(function contactsMerge() {
mergeAll(pmasterContact, pmatchingContacts, filled, callbacks);
}, 0);
}
function doInMemoryMerge(pmasterContact, pmatchingContacts) {
return new Promise(function (resolve, reject) {
mergeAll(pmasterContact, pmatchingContacts, inMemoryMergeDone, {
success: function(masterContact) {
resolve(masterContact);
},
error: function(err) {
reject(err);
}
});
});
}
function inMemoryMergeDone(callbacks, matchingContacts, masterContact) {
callbacks.success(masterContact);
}
function isSimContact(contact) {
return Array.isArray(contact.category) &&
contact.category.indexOf('sim') !== -1;
}
function mergeAll(masterContact, matchingContacts, onFilled, callbacks) {
var emailsHash;
var categoriesHash;
var telsHash;
var mergedContact = {};
var mergedPhoto;
mergedContact.givenName = [];
copyStringArray(masterContact.givenName, mergedContact.givenName);
mergedContact.familyName = [];
copyStringArray(masterContact.familyName, mergedContact.familyName);
mergedContact.photo = masterContact.photo || [];
mergedPhoto = mergedContact.photo;
mergedContact.bday = masterContact.bday;
mergedContact.anniversary = masterContact.anniversary;
mergedContact.adr = masterContact.adr || [];
telsHash = {};
mergedContact.tel = [];
if (Array.isArray(masterContact.tel)) {
masterContact.tel.forEach(function(aTel) {
if (!telsHash[aTel.value]) {
aTel.type = (Array.isArray(aTel.type) ? aTel.type : [aTel.type]);
aTel.type[0] = aTel.type[0] || DEFAULT_TEL_TYPE;
mergedContact.tel.push(aTel);
}
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
variants.forEach(function(aVariant) {
telsHash[aVariant] = true;
});
});
}
mergedContact.email = [];
emailsHash = {};
populateEmails(masterContact.email, emailsHash, mergedContact.email);
mergedContact.org = masterContact.org || [];
mergedContact.category = [];
categoriesHash = {};
populateNoDuplicates(masterContact.category, categoriesHash,
mergedContact.category);
mergedContact.url = masterContact.url || [];
mergedContact.note = masterContact.note || [];
matchingContacts.forEach(function(aResult) {
var theMatchingContact = aResult.matchingContact;
var givenName = theMatchingContact.givenName;
if (Array.isArray(givenName) && givenName[0]) {
if (mergedContact.givenName.indexOf(givenName[0]) === -1) {
if (mergedContact.givenName[0] &&
mergedContact.givenName[0].trim()) {
mergedContact.givenName.push(givenName[0]);
}
else {
mergedContact.givenName[0] = givenName[0];
}
}
}
var familyName = theMatchingContact.familyName;
if (Array.isArray(familyName) && familyName[0]) {
if (mergedContact.familyName.indexOf(familyName[0]) === -1) {
if (mergedContact.familyName[0] &&
mergedContact.familyName[0].trim()) {
mergedContact.familyName.push(familyName[0]);
}
else {
mergedContact.familyName[0] = familyName[0];
}
}
}
// Avoiding duplicating givenName - familyName for SIM Contacts
if ((!isDefined(masterContact.givenName) ||
!isDefined(masterContact.familyName)) &&
isSimContact(theMatchingContact)) {
if (isDefined(mergedContact.givenName) &&
isDefined(mergedContact.familyName) &&
mergedContact.familyName[0] === mergedContact.givenName[0]) {
if (!isDefined(masterContact.givenName)) {
mergedContact.givenName = [];
mergedContact.name = mergedContact.familyName;
}
if (!isDefined(masterContact.familyName)) {
mergedContact.familyName = [];
mergedContact.name = mergedContact.givenName;
}
}
}
if (!mergedContact.bday && theMatchingContact.bday) {
mergedContact.bday = theMatchingContact.bday;
}
if (!mergedContact.anniversary && theMatchingContact.anniversary) {
mergedContact.anniversary = theMatchingContact.anniversary;
}
if (isDefined(theMatchingContact.org) && mergedContact.org.length === 0) {
mergedContact.org = theMatchingContact.org;
}
if (isDefined(theMatchingContact.category)) {
populateNoDuplicates(theMatchingContact.category, categoriesHash,
mergedContact.category);
}
populateEmails(theMatchingContact.email, emailsHash, mergedContact.email);
if (Array.isArray(theMatchingContact.tel)) {
var theMatchings = aResult.matchings || {};
var telMatchings = theMatchings.tel;
theMatchingContact.tel.forEach(function(aTel) {
var theValue = aTel.value;
var target = theValue, matchedValue = '';
if (telMatchings) {
var matchedFrom = telMatchings.filter(function(x) {
return (x.target === theValue || x.matchedValue === theValue);
});
if (matchedFrom[0]) {
target = matchedFrom[0].target;
matchedValue = matchedFrom[0].matchedValue;
}
}
var variants = SimplePhoneMatcher.generateVariants(theValue);
var alreadyPresent = false;
for (var j = 0; j < variants.length; j++) {
if (telsHash[variants[j]]) {
alreadyPresent = true;
}
}
if (!alreadyPresent) {
theValue = target.length > matchedValue.length ?
target : matchedValue;
mergedContact.tel.push({
type: aTel.type || [DEFAULT_TEL_TYPE],
value: theValue,
carrier: aTel.carrier,
pref: aTel.pref
});
variants.forEach(function(aVariant) {
telsHash[aVariant] = true;
});
}
});
}
if (!Array.isArray(mergedPhoto) || mergedPhoto.length === 0 &&
isDefined(theMatchingContact.photo)) {
var photo = ContactPhotoHelper.getFullResolution(theMatchingContact);
mergedPhoto = [photo];
}
populateField(
theMatchingContact.adr,
mergedContact.adr,
DEFAULT_ADR_TYPE
);
populateField(theMatchingContact.url, mergedContact.url);
populateField(theMatchingContact.note, mergedContact.note);
}); // matchingResults
mergedContact.name = [((mergedContact.givenName[0] ?
mergedContact.givenName[0] : '') + ' ' +
(mergedContact.familyName[0] ?
mergedContact.familyName[0] : '')).trim()];
fillMasterContact(masterContact, mergedContact, mergedPhoto,
onFilled.bind(null, callbacks, matchingContacts));
}
function filled(callbacks, matchingContacts, masterContact) {
// Updating the master contact
var req = navigator.mozContacts.save(
utils.misc.toMozContact(masterContact));
req.onsuccess = function() {
// Now for all the matchingContacts they have to be removed
matchingContacts.forEach(function(aMatchingContact) {
// Only remove those contacts which are already in the DB
if (aMatchingContact.matchingContact.id) {
var contact = aMatchingContact.matchingContact;
navigator.mozContacts.remove(utils.misc.toMozContact(contact));
}
});
if (typeof callbacks.success === 'function') {
callbacks.success(masterContact);
}
};
req.onerror = function() {
window.console.error('Error while saving merged Contact: ',
req.error.name);
typeof callbacks.error === 'function' && callbacks.error(req.error);
};
}
function isDefined(field) {
return (Array.isArray(field) && field[0] &&
((typeof field[0] === 'string' && field[0].trim().length > 0) ||
typeof field[0] === 'object'));
}
function copyStringArray(source, dest) {
if (Array.isArray(source)) {
source.forEach(function(aVal) {
if (aVal && aVal.trim()) {
dest.push(aVal);
}
});
}
}
function populateEmails(sourceEmails, hash, out) {
if (Array.isArray(sourceEmails)) {
sourceEmails.forEach(function(aEmail) {
aEmail.type = Array.isArray(aEmail.type) ? aEmail.type : [aEmail.type];
aEmail.type[0] = aEmail.type[0] || DEFAULT_EMAIL_TYPE;
var value = aEmail.value;
if (value && value.trim()) {
value = value.toLowerCase();
if (!hash[value]) {
aEmail.value = value;
out.push(aEmail);
hash[value] = true;
}
}
});
}
}
function populateNoDuplicates(source, hash, out) {
if (Array.isArray(source)) {
source.forEach(function(aCat) {
if (!hash[aCat]) {
out.push(aCat);
hash[aCat] = true;
}
});
}
}
function populateField(source, destination, defaultType) {
if (Array.isArray(source)) {
// The easiest way to compare two objects is to compare their's
// stringified JSON representations as strings. So we create
// temporary Array with JSONs of the objects to compare.
var stringifiedDestination = destination.map(function(element){
return JSON.stringify(element);
});
source.forEach(function(as, index) {
// If the source value and destination value is the same we
// don't want to merge and will leave contact as it is. This
// prevents duplication of the data like in Bug 935636
if (stringifiedDestination.indexOf(JSON.stringify(as)) === -1) {
if (defaultType && (!as.type || !as.type[0])) {
as.type = [defaultType];
}
destination.push(as);
}
});
}
}
function fillMasterContact(masterContact, mergedContact, mergedPhotos, done) {
var fields = ['familyName', 'givenName', 'name', 'org', 'email', 'tel',
'bday', 'anniversary', 'adr', 'category',
'url', 'note', 'photo'];
fields.forEach(function(aField) {
masterContact[aField] = mergedContact[aField];
});
if (!Array.isArray(mergedPhotos) || !mergedPhotos[0] ||
mergedPhotos.length >= 2) {
done(masterContact);
return;
}
var mergedPhoto = mergedPhotos[0];
utils.thumbnailImage(mergedPhoto, function gotTumbnail(thumbnail) {
if (mergedPhoto !== thumbnail) {
masterContact.photo = [mergedPhoto, thumbnail];
} else {
masterContact.photo = [mergedPhoto];
}
done(masterContact);
});
}
return {
merge: doMerge,
inMemoryMerge: doInMemoryMerge
};
})();

View file

@ -0,0 +1,231 @@
'use strict';
var utils = window.utils || {};
if (!utils.alphaScroll) {
(function initScrollbar(doc) {
var alphaScroll = utils.alphaScroll = {};
var scrollToCallback, jumper, overlay,
overlayStyle, groupSelector, liSearch, liFavorites;
var isScrolling = false;
var alreadyRendered = false;
var isDesktop = false;
// Callback invoked when scrolling is neded
var P_SCROLLTO_CB = 'scrollToCb';
// Element that represents the alpha scroll bar
var P_JUMPER = 'jumper';
// Element that shows the current letter
var P_OVERLAY = 'overlay';
// Selector that will allow to get the group that should be scrolled to
// Group will be identified by this selector plus the corresponding letter
var P_GROUP_SELECTOR = 'groupSelector';
var TRANSITION_DELAY = '0.3s';
var TRANSITION_DURATION = '0.2s';
var RESET_TRANSITION = '0s';
var offset = 0, lastY = 0;
var isTouch = 'ontouchstart' in window;
var touchstart = isTouch ? 'touchstart' : 'mousedown';
var touchmove = isTouch ? 'touchmove' : 'mousemove';
var touchend = isTouch ? 'touchend' : 'mouseup';
var getY = (function getYWrapper() {
return isTouch ? function(e) { return e.touches[0].pageY; } :
function(e) { return e.pageY; };
})();
var getTarget = (function getTargetWrapper() {
if (isTouch) {
return function(e) {
var touch = e.touches[0];
return document.elementFromPoint(touch.pageX, touch.pageY);
};
} else {
return function(e) {
return e.target;
};
}
})();
alphaScroll.init = function(params, desktop) {
isDesktop = params && params.desktop;
if (alreadyRendered && !isDesktop) {
return;
}
scrollToCallback = params[P_SCROLLTO_CB];
jumper = params[P_JUMPER];
overlay = params[P_OVERLAY];
groupSelector = params[P_GROUP_SELECTOR];
overlay.textContent = '';
overlayStyle = overlay.style;
jumper.addEventListener(touchstart, scrollStart);
jumper.addEventListener(touchmove, scrollTo);
jumper.addEventListener(touchend, scrollEnd);
// Determine if the content for this scrollbar is statically defined
// in the index.html. If there are no templates, then do not attempt
// to render it.
if (typeof jumper === 'string') {
jumper = document.querySelector(jumper);
}
var template = jumper.querySelector('*[data-template]');
if (!template) {
alreadyRendered = true;
return;
}
// Otherwise render the content via templating.
var frag = document.createDocumentFragment();
var alphabet = [];
for (var i = 65; i <= 90; i++) {
alphabet.push({ anchor: String.fromCharCode(i) });
}
alphabet.push({
anchor: '#'
});
utils.templates.append(jumper, alphabet, frag);
jumper.appendChild(frag);
alreadyRendered = true;
};
// Provide a mechanism to hide/show groups at will
alphaScroll.showGroup = function(name) {
var selector = '[data-anchor="group-'+ name + '"]';
var group = jumper.querySelector(selector);
if (group) {
group.classList.remove('hide');
}
};
alphaScroll.hideGroup = function(name) {
var selector = '[data-anchor="group-'+ name + '"]';
var group = jumper.querySelector(selector);
if (group) {
group.classList.add('hide');
}
};
function hideExtraItems(value) {
if (!liSearch) {
liSearch = jumper.querySelector('li[data-anchor="search-container"]');
}
if (!liFavorites) {
liFavorites = jumper.querySelector('li[data-anchor="group-favorites"]');
}
liSearch.hidden = value;
liFavorites.hidden = value;
}
alphaScroll.toggleFormat = function(type) {
hideExtraItems(type === 'short');
};
function scrollTo(evt) {
evt.preventDefault();
evt.stopPropagation();
if (!isScrolling) {
return;
}
var currentY = getY(evt);
// We set the threshold of updating the shortcut to half of the offset
// to avoid when touch already moved to center of the certain letter but
// shows another letter.
if (Math.abs(lastY - currentY) < offset / 2 && !isDesktop) {
return;
}
lastY = currentY;
var elem = getTarget(evt);
if (!elem) {
return;
}
var dataset = elem.dataset;
// Render
if (dataset.letter) {
overlay.textContent = dataset.letter;
} else if (dataset.img) {
overlay.textContent = '';
var img = new Image();
img.src = 'style/images/' + dataset.img;
overlay.appendChild(img);
}
// The headers are sticky, and can have wrong offsetTop,
// so scroll to the top of each section on jump click.
var anch = dataset.anchor;
var selector = anch === 'group-#' ? 'group-und' : anch;
var domTarget = doc.querySelector('#section-' + selector +
', #' + selector);
if (!domTarget) {
return;
}
scrollToCallback(domTarget, selector.replace('group-', ''));
}
function scrollStart(evt) {
var dataset = getTarget(evt).dataset;
evt.preventDefault();
evt.stopPropagation();
// There is no need to show overlay if the target doesn't contain
// any valid data for overlay block.
if (!dataset.letter && !dataset.img) {
return;
}
offset = offset || jumper.querySelector('[data-anchor]').offsetHeight;
overlayStyle.MozTransitionDelay = RESET_TRANSITION;
overlayStyle.MozTransitionDuration = RESET_TRANSITION;
overlayStyle.opacity = '1';
isScrolling = true;
scrollTo(evt);
}
function scrollEnd(evt) {
evt.preventDefault();
evt.stopPropagation();
var transitionDelay = TRANSITION_DELAY;
var transitionDuration = TRANSITION_DURATION;
// In the case of the scroll ending when we are at the
// bottom or the top, remove the transition inmediately
if (overlay.textContent === '#' || overlay.textContent === '') {
transitionDelay = RESET_TRANSITION;
transitionDuration = RESET_TRANSITION;
}
overlayStyle.MozTransitionDelay = transitionDelay;
overlayStyle.MozTransitionDuration = transitionDuration;
overlayStyle.opacity = '0';
overlay.textContent = null;
isScrolling = false;
lastY = 0;
}
// Cache images refered in 'data-img'es
(function(doc) {
var images = doc.querySelectorAll('li[data-img]');
Object.keys(images).forEach(function(value) {
var img = new Image();
img.src = 'contacts/style/images/' + images[value].dataset.img;
});
}(doc));
})(document);
}

View file

@ -0,0 +1,214 @@
'use strict';
/* global contacts */
/* global utils */
/* global Matcher */
(function() {
var CHUNK_SIZE = 5;
window.ContactsImporter = function(pContacts, pAccessToken, pConnector) {
/* jshint validthis:true */
this.contacts = Object.keys(pContacts);
var contactsHash = pContacts;
var access_token = pAccessToken;
var total = this.contacts.length;
var self = this;
var serviceConnector = pConnector;
var numImported = 0;
var mustHold = false;
var holded = false;
var mustFinish = false;
var isOnLine = navigator.onLine;
// To count the number of merged duplicate contacts
var numMergedDuplicated = 0;
window.addEventListener('online', onLineChanged);
window.addEventListener('offline', onLineChanged);
function onLineChanged() {
isOnLine = navigator.onLine;
}
function contactSaved(e) {
var cfdata = this;
if (typeof self.oncontactimported === 'function') {
window.setTimeout(function() {
self.oncontactimported(cfdata);
}, 0);
}
continueCb();
}
function contactSaveError(err) {
window.console.error('Error while importing contact: ', err.name);
if (typeof self.onerror === 'function') {
window.setTimeout(self.onerror.bind(null, err), 0);
}
continueCb();
}
function saveMozContact(deviceContact, successCb, errorCb) {
var req = navigator.mozContacts.save(
utils.misc.toMozContact(deviceContact));
req.onsuccess = successCb;
req.onerror = errorCb;
}
function pictureReady(blobPicture) {
var serviceContact = this;
var done = function() {
var deviceContact = self.adapt(serviceContact);
self.persist(deviceContact, contactSaved.bind(serviceContact),
contactSaveError);
};
// Photo is assigned to the service contact as it is needed by the
// Fb Connector
if (!blobPicture) {
done();
return;
}
utils.thumbnailImage(blobPicture, function gotThumbnail(thumbnail) {
if (blobPicture !== thumbnail) {
serviceContact.photo = [blobPicture, thumbnail];
} else {
serviceContact.photo = [blobPicture];
}
done();
});
}
function pictureError() {
window.console.error('Error while getting picture for contact: ',
this.user_id);
self.persist(self.adapt(this), contactSaved.bind(this), contactSaveError);
}
function pictureTimeout() {
window.console.warn('Timeout while getting picture for contact: ',
this.user_id);
self.persist(self.adapt(this), contactSaved.bind(this),
contactSaveError);
}
this.start = function() {
numMergedDuplicated = 0;
mustHold = false;
holded = false;
mustFinish = false;
importContacts(numImported);
};
this.hold = function() {
mustHold = true;
};
this.finish = function() {
mustFinish = true;
if (holded) {
notifySuccess();
}
};
this.resume = function() {
mustHold = false;
holded = false;
mustFinish = false;
window.setTimeout(function resume_import() {
importContacts(numImported);
}, 0);
};
// This method might be overritten
this.persist = function(contactData, successCb, errorCb) {
var cbs = {
onmatch: function(matches) {
numMergedDuplicated++;
contacts.adaptAndMerge(this, matches, {
success: successCb,
error: errorCb
});
}.bind(utils.misc.toMozContact(contactData)),
onmismatch: function() {
saveMozContact(this, successCb, function onMismatchError(evt) {
errorCb(evt.target.error);
});
}.bind(contactData)
};
// Try to match and if so merge is performed
Matcher.match(contactData, 'passive', cbs);
};
// This method might be overwritten
this.adapt = function(serviceContact) {
return serviceConnector.adaptDataForSaving(serviceContact);
};
function importContacts(from) {
for (var i = from; i < from + CHUNK_SIZE && i < total; i++) {
importContact(i);
}
}
function importContact(index) {
var serviceContact = contactsHash[self.contacts[index]];
// We need to get the picture
var callbacks = {
success: pictureReady.bind(serviceContact),
error: pictureError.bind(serviceContact),
timeout: pictureTimeout.bind(serviceContact)
};
if (isOnLine === true) {
serviceConnector.downloadContactPicture(serviceContact,
access_token, callbacks);
}
else {
callbacks.success(null);
}
}
function notifySuccess() {
if (typeof self.onsuccess === 'function') {
window.setTimeout(function do_success() {
self.onsuccess(numImported, numMergedDuplicated);
}, 0);
}
}
function continueCb() {
numImported++;
var next = numImported + CHUNK_SIZE - 1;
if (next < total) {
if (!mustHold && !mustFinish) {
importContact(next);
}
else if (mustFinish && !holded) {
notifySuccess();
}
if (mustHold) {
holded = true;
}
}
else if (numImported >= total) {
// End has been reached
notifySuccess();
}
}
};
})();

View file

@ -0,0 +1,383 @@
'use strict';
/* global fb, utils */
/* global Matcher */
if (!window.FacebookConnector) {
window.FacebookConnector = (function() {
var nextUpdateTime;
window.fb = window.fb || {};
fb.operationsTimeout = parent.config ?
parent.config.operationsTimeout : null;
// Only one instance in order to reuse it for saving friends in the
// database. If the instance is not released between transactions, new
// objects will be created.
var reusedFbContact = new fb.Contact();
reusedFbContact.ready = true;
function createFbImporter(clist, access_token) {
/* jshint validthis:true */
var out = new window.ContactsImporter(clist, access_token, this);
out.persist = persistFbData;
return out;
}
function persistFbData(data, successCb, errorCb, options) {
var saveCbs = {
success: function(e) {
if (options === 'not_match') {
successCb();
return;
}
var cbs = {
onmatch: function(matches) {
// For each of the matching contacts link with the just imported
Object.keys(matches).forEach(function(aMatchId) {
var fbContact = new
fb.Contact(matches[aMatchId].matchingContact);
var mozContReq = fb.utils.getMozContact(data.uid);
mozContReq.onsuccess = function() {
var linkParams = {
uid: data.uid,
mozContact: mozContReq.result
};
if (Array.isArray(data.photo) && data.photo[0]) {
linkParams.photoUrl = data.pic_big;
}
var req = fbContact.linkTo(linkParams);
req.onsuccess = successCb;
req.onerror = function error(e) {
window.console.error('Error while automatically linking : ',
aMatchId, data.uid, e.target.name);
successCb();
};
};
mozContReq.onerror = function(e) {
window.console.error(
'Error getting mozContact data in automatic linking: ',
e.target.error.name);
successCb();
};
});
},
onmismatch: successCb
};
// Try to match and if so merge is performed
Matcher.match(data, 'passive', cbs);
},
error: errorCb
};
saveFbContact(data, saveCbs.success, saveCbs.error);
}
function saveFbContact(data, successCb, errorCb) {
var fbContact, successWrapperCb;
if (reusedFbContact.ready) {
reusedFbContact.ready = null;
fbContact = reusedFbContact;
successWrapperCb = function onsuccess(e) {
reusedFbContact.ready = true;
successCb(e);
};
} else {
fbContact = new fb.Contact();
successWrapperCb = successCb;
}
fbContact.setData(data);
var req = fbContact.save();
req.onsuccess = successWrapperCb;
req.onerror = errorCb;
}
function setInfraForSync(existingContacts, friendsImported, callback) {
// From the FTU the infra for sync will not be loaded
if (!fb.sync) {
(typeof callback === 'function') && callback();
return;
}
// Check wether we need to set the update alarm
window.asyncStorage.getItem(fb.utils.ALARM_ID_KEY, function(data) {
if (!data || (existingContacts && existingContacts.length === 0) &&
!friendsImported) {
// This is the first contact imported
fb.utils.setLastUpdate(nextUpdateTime, function() {
var req = fb.sync.scheduleNextSync();
if (typeof callback === 'function') {
req.onsuccess = callback;
}
});
}
});
}
var UID_FILTER_IDX = 5;
var acc_tk;
function buildFriendQuery(uid) {
var aquery1 = [].concat(FRIENDS_QUERY);
aquery1[UID_FILTER_IDX] = '= ' + uid;
return aquery1.join('');
}
// Query that retrieves the information about friends
var FRIENDS_QUERY = [
'SELECT uid, name, first_name, last_name, pic_big, current_location, ' ,
'middle_name, birthday_date, email, profile_update_time, ' ,
' work, phones, hometown_location' ,
' FROM user' ,
' WHERE uid ',
'IN (SELECT uid1 FROM friend WHERE uid2=me())' ,
' ORDER BY ',
'first_name'
];
var friendsQueryStr = FRIENDS_QUERY.join('');
function contactDataLoaded(options, response) {
if (!response.error) {
// Just in case this is the first contact imported
nextUpdateTime = Date.now();
/* jshint validthis:true */
var successCb = this.success;
var friend = response.data[0];
if (friend) {
var out1 = self.adaptDataForShowing(friend);
var photoCbs = {};
photoCbs.success = function(blobPicture) {
if (!blobPicture) {
pictureLoaded(out1, options, successCb);
return;
}
utils.thumbnailImage(blobPicture,
function gotThumbnail(thumbnail) {
if (blobPicture !== thumbnail) {
out1.photo = [blobPicture, thumbnail];
} else {
out1.photo = [blobPicture];
}
pictureLoaded(out1, options, successCb);
});
}; // successCb
self.downloadContactPicture(friend, acc_tk, photoCbs);
} // if friend
else {
window.console.error('FB: No Friend data found');
/* jshint validthis:true */
this.error('No friend data found');
}
}
else {
/* jshint validthis:true */
this.error(response.error);
}
}
function pictureLoaded(friend, options, success) {
var data = self.adaptDataForSaving(friend);
/* jshint validthis:true */
persistFbData(data, function() {
success({
uid: friend.uid,
url: friend.pic_big
});
}, function error(err) {
console.error('Error while persisting: ', err);
}, options);
// If there is no an alarm set it has to be set
window.asyncStorage.getItem(fb.utils.ALARM_ID_KEY, function(data) {
if (!data) {
fb.utils.setLastUpdate(nextUpdateTime,
fb.sync.scheduleNextSync);
}
});
}
function FacebookConnector() { }
FacebookConnector.prototype = {
listAllContacts: function(access_token, callbacks, options) {
if (options && options.orderBy === 'lastName') {
FRIENDS_QUERY[FRIENDS_QUERY.length] = 'last_name';
friendsQueryStr = FRIENDS_QUERY.join('');
}
return fb.utils.runQuery(friendsQueryStr, {
success: callbacks.success,
error: callbacks.error,
timeout: callbacks.timeout
},access_token);
},
listDeviceContacts: function(callbacks) {
// Dummy implementation for the time being
var req = fb.utils.getAllFbContacts();
req.onsuccess = function successHandler() {
callbacks.success(req.result);
};
req.onerror = callbacks.error;
},
getImporter: function(contactsList, access_token) {
return createFbImporter.bind(this)(contactsList, access_token);
},
// Imports a Contact to FB indexedDB private database
importContact: function(uid, access_token, callbacks, options) {
acc_tk = access_token;
var oneFriendQuery = buildFriendQuery(uid);
var auxCallbacks = {
success: contactDataLoaded.bind(callbacks, options),
error: callbacks.error,
timeout: callbacks.timeout
};
fb.utils.runQuery(oneFriendQuery, auxCallbacks, access_token);
},
cleanContacts: function(contactsList, mode, cb) {
if (mode === 'update') {
var cleaner = new fb.utils.FbContactsCleaner(contactsList, mode);
window.setTimeout(cleaner.start, 0);
cb(cleaner);
}
else {
// Mode === clear
var req = fb.utils.clearFbData();
req.onsuccess = function() {
cb(req.result);
};
req.onerror = function(e) {
window.console.error('Error while starting cleaning: ',
e.target.error.name);
// Notifying the UI importer to show an error to the user
// As we cannot get a cleaner the param is set to null
cb(null);
};
}
},
adaptDataForShowing: function(source) {
var box = utils.misc.getPreferredPictureBox();
var picWidth = box.width;
var picHeight = box.height;
var out = fb.friend2mozContact(source);
out.contactPictureUri = 'https://graph.facebook.com/' +
source.uid + '/picture?type=square' +
'&width=' + picWidth +
'&height=' + picHeight +
'&t=' + Date.now();
return out;
},
adaptDataForSaving: function fbAdapt(cfdata) {
var worksAt = fb.getWorksAt(cfdata);
var address = fb.getAddresses(cfdata);
var birthDate = null;
if (cfdata.birthday_date && cfdata.birthday_date.length > 0) {
birthDate = fb.getBirthDate(cfdata.birthday_date);
}
var fbInfo = {
bday: birthDate,
org: [worksAt]
};
if (address) {
fbInfo.adr = address;
}
// This is the short telephone number to enable indexing
if (cfdata.shortTelephone) {
fbInfo.shortTelephone = cfdata.shortTelephone;
delete cfdata.shortTelephone;
}
// Check whether we were able to get the photo or not
fbInfo.url = [];
if (cfdata.photo) {
fbInfo.photo = cfdata.photo;
if (cfdata.pic_big) {
// The URL is stored for synchronization purposes
fb.setFriendPictureUrl(fbInfo, cfdata.pic_big);
}
delete cfdata.photo;
}
// Facebook info is set and then contact is saved
cfdata.fbInfo = fbInfo;
return cfdata;
},
// It would allow to know the UID of a service contact already imported
// on the device. That is needed by the generic importer, for live
// a dummy implementation as we are currently not supporting updates
getContactUid: function(deviceContact) {
return fb.getFriendUid(deviceContact);
},
get name() {
return 'facebook';
},
get automaticLogout() {
return false;
},
downloadContactPicture: function(contact, access_token, callbacks) {
return fb.utils.getFriendPicture(contact.uid, callbacks.success,
access_token, utils.misc.getPreferredPictureDetail());
},
oncontactsloaded: function(lfriends) {
// This is the timestamp for later syncing as it set at the time
// when data was ready
nextUpdateTime = Date.now();
// Now caching the number
fb.utils.setCachedNumFriends(lfriends.length);
},
oncontactsimported: function(existingContacts, friendsImported, cb) {
setInfraForSync(existingContacts, friendsImported, cb);
fb.contacts.flush();
},
startSync: function(existingContacts, myFriendsByUid, syncFinished) {
var callbacks = {
success: syncFinished
};
fb.sync.startWithData(existingContacts, myFriendsByUid, callbacks);
},
scheduleNextSync: function() {
fb.sync.scheduleNextSync();
}
};
var self = new FacebookConnector();
return self;
})();
}

View file

@ -0,0 +1,659 @@
'use strict';
/* global utils */
var fb = window.fb || {};
// Encapsulates all the logic to obtain the data for a FB contact
fb.Contact = function(deviceContact, cid) {
var contactData;
var devContact = deviceContact;
var contactid = cid;
function doGetFacebookUid(data) {
return fb.getFriendUid(data);
}
function getFacebookUid() {
return doGetFacebookUid(deviceContact);
}
function setFacebookUid(value) {
doSetFacebookUid(deviceContact, value);
}
function doSetFacebookUid(dcontact, value) {
if (!dcontact.category) {
dcontact.category = [];
}
if (dcontact.category.indexOf(fb.CATEGORY) === -1) {
markAsFb(dcontact);
}
var idx = dcontact.category.indexOf(fb.CATEGORY);
dcontact.category[idx + 2] = value;
}
function markAsFb(dcontact) {
if (!dcontact.category) {
dcontact.category = [];
}
if (dcontact.category.indexOf(fb.CATEGORY) === -1) {
dcontact.category.push(fb.CATEGORY);
dcontact.category.push(fb.NOT_LINKED);
}
return dcontact;
}
// Mark a mozContact (deviceContact) as linked to a FB contact (uid)
function markAsLinked(dcontact, uid) {
if (!dcontact.category) {
dcontact.category = [];
}
if (dcontact.category.indexOf(fb.LINKED) === -1) {
dcontact.category.push(fb.CATEGORY);
dcontact.category.push(fb.LINKED);
dcontact.category.push(uid);
}
return dcontact;
}
function promoteToLinked(dcontact) {
var idx = dcontact.category.indexOf(fb.NOT_LINKED);
if (idx != -1) {
dcontact.category[idx] = fb.LINKED;
}
}
// Sets the data for an imported FB Contact
this.setData = function(data) {
contactData = data;
};
Object.defineProperty(this, 'uid', {
get: getFacebookUid,
set: setFacebookUid,
enumerable: true,
});
Object.defineProperty(this, 'mozContact', {
get: getDevContact
});
function getDevContact() {
return devContact;
}
// For saving an imported FB contact
this.save = function() {
var outReq = new fb.utils.Request();
if (contactData && navigator.mozContacts) {
window.setTimeout(function save_do() {
var photoList = contactData.fbInfo.photo;
if (photoList && photoList.length > 0 && photoList[0]) {
utils.squareImage(photoList[0], function squared(squared_image) {
photoList[0] = squared_image;
doSave(outReq);
});
}
else {
doSave(outReq);
}
},0);
}
else {
throw 'Data or mozContacts not available';
}
return outReq;
};
function doSave(outReq) {
// Info to be saved on mozContacts
var contactInfo = {};
// Copying names to the mozContact
copyNames(contactData, contactInfo);
// URL (photo, etc) is stored also with the Device contact
if (contactData.fbInfo.url) {
contactInfo.url = contactData.fbInfo.url;
}
doSetFacebookUid(contactInfo, contactData.uid);
var fbReq = persistToFbCache(contactData);
fbReq.onsuccess = function() {
var mozContactsReq = navigator.mozContacts.save(
utils.misc.toMozContact(contactInfo));
mozContactsReq.onsuccess = function(e) {
outReq.done(fbReq.result);
};
mozContactsReq.onerror = function(e) {
window.console.error('FB: Error while saving on mozContacts',
e.target.error);
outReq.failed(e.target.error);
}; // fbReq.onsuccess
};
fbReq.onerror = function() {
window.console.error('FB: Error while saving on datastore',
fbReq.error && fbReq.error.name);
outReq.failed(fbReq.error);
};
}
// Persists FB Friend Data to the FB cache
function persistToFbCache(contactData, mUpdate) {
var isUpdate = (mUpdate === true ? mUpdate : false);
var outReq = new fb.utils.Request();
window.setTimeout(function persist_fb_do() {
// now saving the FB-originated data to the "private cache area"
var data = Object.create(contactData.fbInfo);
data.tel = contactData.tel || [];
data.email = contactData.email || [];
data.uid = contactData.uid;
Object.keys(contactData.fbInfo).forEach(function(prop) {
data[prop] = contactData.fbInfo[prop];
});
// Names are also stored on datastore
// thus restoring the contact (if unlinked) will be trivial
copyNames(contactData, data);
var updaterFn = (isUpdate === true ?
fb.contacts.update : fb.contacts.save);
var fbReq = updaterFn(data);
fbReq.onsuccess = function() {
outReq.done(fbReq.result);
};
fbReq.onerror = function() {
window.console.error('FB: Error while saving Fb data on the Datastore',
fbReq.error && fbReq.error.name);
outReq.failed(fbReq.error);
};
},0);
return outReq;
}
// Updates a FB Contact
this.update = function(contactData) {
// Auxiliary function to do all the work for saving to the FB cache
function auxDoUpdate(contactData, outReq) {
// If the photo was not updated it is needed to save it
if (!contactData.fbInfo.photo) {
var dataReq = fb.contacts.get(contactData.uid);
dataReq.onsuccess = function() {
contactData.fbInfo.photo = dataReq.result.photo;
auxCachePersist(contactData, outReq, true);
};
dataReq.onerror = function() {
window.console.error('Error while retrieving existing photo for ',
contactData.uid, dataReq.error);
outReq.failed(dataReq.error);
};
}
else {
utils.squareImage(contactData.fbInfo.photo[0],
function sq_img(squaredImg) {
contactData.fbInfo.photo[0] = squaredImg;
auxCachePersist(contactData, outReq, true);
}
);
}
}
// Persist the data to the FB Cache
function auxCachePersist(contactData, outReq, isUpdate) {
var fbReq = persistToFbCache(contactData, isUpdate);
fbReq.onsuccess = function() {
outReq.done(fbReq.result);
};
fbReq.onerror = function() {
window.console.error('FB: Error while saving to FB cache: ',
contactData.uid, fbReq.error);
outReq.failed(fbReq.error);
};
}
// Code starts here
var outReq = new fb.utils.Request();
window.setTimeout(function update_do() {
if (!fb.isFbLinked(devContact)) {
copyNames(contactData, devContact);
} else {
// We are going to update names if they are propagated
revisitPropagatedNames(contactData, devContact);
}
// Check whether the photo has changed
if (contactData.fbInfo.photo) {
devContact.url = contactData.fbInfo.url;
}
var auxReq = new fb.utils.Request();
auxReq.onsuccess = function() {
var mozContactsReq = navigator.mozContacts.save(
utils.misc.toMozContact(devContact));
mozContactsReq.onsuccess = function(e) {
outReq.done();
};
mozContactsReq.onerror = function(e) {
window.console.error('FB: Error while saving mozContact: ',
devContact.id, e.target.error);
outReq.failed(e.target.error);
};
}; // auxReq.onsuccess
auxReq.onerror = function(e) {
outReq.failed(e.target.error);
}; // auxReq.onerror
// And now doing the update
auxDoUpdate(contactData, auxReq);
},0);
return outReq;
};
function asArrayOfValues(value) {
return Array.isArray(value) ? value : [value];
}
function copyNames(source, destination) {
destination.name = asArrayOfValues(source.name);
destination.givenName = asArrayOfValues(source.givenName);
destination.familyName = asArrayOfValues(source.familyName);
destination.additionalName = asArrayOfValues(source.additionalName);
}
// Merges mozContact data with Facebook data
this.merge = function(fbdata) {
return fb.mergeContact(devContact, fbdata);
};
// Gets the data
this.getData = function() {
return fb.getData(devContact);
};
this.getDataAndValues = function() {
var outReq = new fb.utils.Request();
window.setTimeout(function do_getData() {
var uid = doGetFacebookUid(devContact);
if (uid) {
var fbreq = fb.contacts.get(uid);
fbreq.onsuccess = (function() {
var fbdata = fbreq.result;
var out1 = this.merge(fbdata);
var out2 = {};
var duplicates = {};
Object.keys(fbdata).forEach(function(key) {
var dataElement = fbdata[key];
if (dataElement && Array.isArray(dataElement) && key !== 'photo') {
dataElement.forEach(function(item) {
if (item.value && item.value.length > 0) {
// Check for duplicates. Those duplicates are annotated to
// be later removed from the out2 array
var dupList = fb.checkDuplicates(key, item, devContact[key],
fbdata.shortTelephone);
dupList.forEach(function(aDup) {
duplicates[aDup] = true;
});
out2[item.value] = true;
}
else if (typeof item === 'string' && item.length > 0) {
out2[item] = true;
}
else if (key === 'adr') {
if (item.locality) {
out2[item.locality] = true;
}
if (item.countryName) {
out2[item.countryName] = true;
}
}
});
}
else if (key === 'photo') {
out2.hasPhoto = true;
}
else if (dataElement) {
out2[dataElement] = true;
}
});
Object.keys(duplicates).forEach(function(aDup) {
delete out2[aDup];
});
outReq.done([out1, out2, fbdata]);
}).bind(this);
fbreq.onerror = function() {
outReq.failed(fbreq.error);
};
}
else {
outReq.done([devContact, {}]);
}
}.bind(this), 0);
return outReq;
};
this.promoteToLinked = function() {
promoteToLinked(devContact);
};
this.linkTo = function(fbFriend) {
var out = new fb.utils.Request();
window.setTimeout(function do_linkTo() {
if (!devContact) {
// We need to get the Contact data
var req = fb.utils.getContactData(contactid);
req.onsuccess = function() {
devContact = req.result;
doLink(devContact, fbFriend, out);
}; // req.onsuccess
req.onerror = function() {
throw 'FB: Error while retrieving contact data';
};
} // devContact
else {
doLink(devContact, fbFriend, out);
}
},0);
return out;
};
function propagateField(field, from, to) {
var copied = false;
// The field is copied when it is undefined in the target object
if (!Array.isArray(to[field]) || !to[field][0] || !to[field][0].trim()) {
to[field] = from[field];
copied = true;
}
return copied;
}
function createName(contact) {
contact.name = [];
if (Array.isArray(contact.givenName)) {
contact.name[0] = contact.givenName[0] + ' ';
}
if (Array.isArray(contact.familyName)) {
contact.name[0] += contact.familyName[0];
}
}
function propagateNames(from, to) {
var isGivenNamePropagated = propagateField('givenName', from, to);
var isFamilyNamePropagated = propagateField('familyName', from, to);
if (isGivenNamePropagated || isFamilyNamePropagated) {
// We are going to mark the propagation in the category field
if (isGivenNamePropagated) {
fb.setPropagatedFlag('givenName', to);
}
if (isFamilyNamePropagated) {
fb.setPropagatedFlag('familyName', to);
}
createName(to);
}
}
// This method copies names to a contact when they are propagated from fb
function revisitPropagatedNames(from, to) {
if (fb.isPropagated('givenName', to)) {
to.givenName = from.givenName;
}
if (fb.isPropagated('familyName', to)) {
to.familyName = from.familyName;
}
createName(to);
}
function doLink(contactdata, fbFriend, out) {
if (contactdata) {
if (fbFriend.uid) {
// When marking as linked is needed to store a reference to the profile
// picture URL
markAsLinked(contactdata, fbFriend.uid);
if (fbFriend.photoUrl) {
fb.setFriendPictureUrl(contactdata, fbFriend.photoUrl);
}
else if (fbFriend.mozContact) {
contactdata.url = fbFriend.mozContact.url;
}
}
else if (fbFriend.mozContact) {
markAsLinked(contactdata, doGetFacebookUid(fbFriend.mozContact));
contactdata.url = fbFriend.mozContact.url;
}
propagateNames(fbFriend.mozContact, contactdata);
var mozContactsReq = navigator.mozContacts.save(
utils.misc.toMozContact(contactdata));
mozContactsReq.onsuccess = function(e) {
// The FB contact on mozContacts needs to be removed
if (fbFriend.mozContact && !fb.isFbLinked(fbFriend.mozContact)) {
var deleteReq = navigator.mozContacts.remove(
utils.misc.toMozContact(fbFriend.mozContact));
deleteReq.onsuccess = function(e) {
out.done(e.target.result);
};
deleteReq.onerror = function(e) {
window.console.error('FB: Error while linking');
out.failed(e.target.error);
};
}
else {
out.done(e.target.result);
}
}; // mozContactsReq.onsuccess
mozContactsReq.onerror = function(e) {
out.failed(e.target.error);
}; // mozContactsReq.onerror
} // if(dev.contact)
else {
throw 'FB: Contact data not defined';
}
}
// if type === 'hard' the FB Friend is removed from the cache
this.unlink = function(type) {
var out = new fb.utils.Request();
window.setTimeout(function do_unlink() {
if (!devContact) {
// We need to get the Contact data
var req = fb.utils.getContactData(contactid);
req.onsuccess = function() {
devContact = req.result;
doUnlink(devContact, out, type);
}; // req.onsuccess
req.onerror = function() {
throw 'FB: Error while retrieving contact data';
};
} // devContact
else {
doUnlink(devContact, out, type);
}
}, 0);
return out;
};
// Reset givenName and familyName if it is needed after unlinking
function resetNames(dContact) {
fb.resetNames(dContact);
}
function doUnlink(dContact, out, type) {
var theType = type || 'soft';
var uid = doGetFacebookUid(dContact);
resetNames(dContact);
fb.markAsUnlinked(dContact);
var req = navigator.mozContacts.save(utils.misc.toMozContact(dContact));
req.onsuccess = function(e) {
if (theType !== 'hard') {
// Then the original FB imported contact is restored
var fbNumReq = fb.utils.getNumberMozContacts(uid);
fbNumReq.onsuccess = function() {
// Because we have already unlinked one
if (fbNumReq.result >= 1) {
out.done();
} else {
var fbDataReq = fb.contacts.get(uid);
fbDataReq.onsuccess = function() {
var imported = fbDataReq.result;
var data = {};
copyNames(imported, data);
data.url = imported.url;
doSetFacebookUid(data, uid);
// The FB contact is restored
var aMozContact = utils.misc.toMozContact(data);
var reqRestore = navigator.mozContacts.save(aMozContact);
reqRestore.onsuccess = function(e) {
out.done(aMozContact.id);
};
reqRestore.onerror = function(e) {
out.failed(e.target.error);
};
};
fbDataReq.onerror = function() {
window.console.error('FB: Error while unlinking contact data');
out.failed(fbDataReq.error);
};
}
};
fbNumReq.onerror = function() {
window.console.error('FB: Error while unlinking contact data');
out.failed(fbNumReq.error);
};
}
else {
// FB Data is removed from the cache
var removeReq = fb.contacts.remove(uid, true);
removeReq.onsuccess = function() {
out.done(removeReq.result);
};
removeReq.onerror = function() {
out.failed(removeReq.error);
};
}
};
req.onerror = function(e) {
out.failed(e.target.error);
};
}
this.remove = function(pforceFlush) {
var forceFlush = (pforceFlush === true) ? pforceFlush : false;
var out = new fb.utils.Request();
window.setTimeout(function do_remove() {
var uid = doGetFacebookUid(devContact);
var fbNumReq = fb.utils.getNumberMozContacts(uid);
fbNumReq.onsuccess = function num_onsuccess() {
var removeReq;
// If there is only one Device Contact associated
// Then corresponding FB Data is removed otherwise only
// the device contact is removed
if (fbNumReq.result === 1) {
removeReq = navigator.mozContacts.remove(
utils.misc.toMozContact(devContact));
removeReq.onsuccess = function(e) {
var fbReq = fb.contacts.remove(uid, forceFlush);
fbReq.onsuccess = function() {
out.done(fbReq.result);
};
fbReq.onerror = function() {
out.failed(fbReq.error);
};
};
removeReq.onerror = function(e) {
out.failed(e.target.error);
};
}
else {
removeReq = navigator.mozContacts.remove(
utils.misc.toMozContact(devContact));
removeReq.onsuccess = function(e) {
out.done();
};
removeReq.onerror = function(e) {
out.failed(e.target.error);
};
}
};
}, 0);
return out;
};
}; // fb.Contact

View file

@ -0,0 +1,260 @@
'use strict';
/* global fb, utils */
/* jshint ignore:start */
var fb = this.fb || {};
/* jshint ignore:end */
fb.PROPAGATED_PREFIX = 'fb_propagated_';
// Types of URLs for FB Information
fb.PROFILE_PHOTO_URI = 'fb_profile_photo';
fb.FRIEND_URI = 'fb_friend';
fb.DEFAULT_PHONE_TYPE = 'other';
fb.DEFAULT_EMAIL_TYPE = 'other';
// This year indicates that the year can be ignored (yearless date)
fb.FLAG_YEAR_IGNORED = 9996;
fb.CONTACTS_APP_ORIGIN = location.origin;
fb.isPropagated = function fcu_isPropagated(field, devContact) {
return (devContact.category && devContact.category.
indexOf(fb.PROPAGATED_PREFIX + field) !== -1);
};
fb.removePropagatedFlag = function fcu_removePropagatedFlag(field, devContact) {
var idx = devContact.category.indexOf(fb.PROPAGATED_PREFIX + field);
if (idx !== -1) {
devContact.category.splice(idx, 1);
}
};
fb.setPropagatedFlag = function fcu_setPropagatedFlag(field, devContact) {
var idx = devContact.category.indexOf(fb.PROPAGATED_PREFIX + field);
if (idx === -1) {
devContact.category.push(fb.PROPAGATED_PREFIX + field);
}
};
fb.getFriendPictureUrl = function(devContact) {
var out;
var urls = devContact.url;
if (urls) {
for (var c = 0; c < urls.length; c++) {
var aurl = urls[c];
if (aurl.type.indexOf(fb.PROFILE_PHOTO_URI) !== -1) {
out = aurl.value;
break;
}
}
}
return out;
};
fb.setFriendPictureUrl = function(devContact, url) {
var urls = devContact.url || [];
urls.push({
type: [fb.PROFILE_PHOTO_URI],
value: url
});
devContact.url = urls;
};
// Adapts data to the mozContact format names
fb.friend2mozContact = function(f) {
function normalizeFbPhoneNumber(phone) {
var out = phone.number;
if (phone.country_code && out.indexOf('+') !== 0) {
out = '+' + phone.country_code + out;
}
return out;
}
// Check whether this has been already normalized to mozContact
if (Array.isArray(f.familyName)) {
return f;
}
// givenName is put as name but it should be f.first_name
f.familyName = [f.last_name ? f.last_name.trim() : (f.last_name || '')];
var middleName = f.middle_name ? f.middle_name.trim() : (f.middle_name || '');
f.additionalName = middleName;
var firstName = f.first_name ? f.first_name.trim() : (f.first_name || '');
f.givenName = [(firstName + ' ' + middleName).trim()];
delete f.last_name;
delete f.middle_name;
delete f.first_name;
if (f.email) {
f.email1 = f.email;
f.email = [{
type: [fb.DEFAULT_EMAIL_TYPE],
value: f.email
}];
}
else {
f.email1 = '';
}
if (Array.isArray(f.phones) && f.phones.length > 0) {
f.tel = [];
f.shortTelephone = [];
f.phones.forEach(function(aphone) {
f.tel.push({
type: [fb.DEFAULT_PHONE_TYPE],
value: normalizeFbPhoneNumber(aphone)
});
// Enabling to find FB phones by short number
f.shortTelephone.push(aphone.number);
});
}
delete f.phones;
f.uid = f.uid.toString();
return f;
};
/**
* Auxiliary function to know where a contact works
*
*/
fb.getWorksAt = function(fbdata) {
var out = '';
if (fbdata.work && fbdata.work.length > 0) {
// It is assumed that first is the latest
out = fbdata.work[0].employer.name;
}
return out;
};
/**
* Facebook dates are MM/DD/YYYY
*
* Returns the birth date
*
*/
fb.getBirthDate = function getBirthDate(sbday) {
var out = new Date(0);
var imonth = sbday.indexOf('/');
var smonth = sbday.substring(0, imonth);
var iyear = sbday.lastIndexOf('/');
if (iyear === imonth) {
iyear = sbday.length;
}
var sday = sbday.substring(imonth + 1, iyear);
var syear = sbday.substring(iyear + 1, sbday.length);
out.setUTCDate(parseInt(sday, 10));
out.setUTCMonth(parseInt(smonth, 10) - 1, parseInt(sday, 10));
if (syear && syear.length > 0) {
out.setUTCFullYear(parseInt(syear, 10));
}
else {
// 9996 is the year that flags a not known year
out.setUTCFullYear(fb.FLAG_YEAR_IGNORED);
}
out.setUTCHours(0);
out.setUTCMinutes(0);
out.setUTCSeconds(0);
out.setUTCMilliseconds(0);
return out;
};
fb.getAddresses = function(fbdata) {
function fillAddress(fbAddress, type) {
var outAddr = {};
outAddr.type = [type];
outAddr.locality = fbAddress.city || '';
outAddr.region = fbAddress.state || '';
outAddr.countryName = fbAddress.country || '';
return outAddr;
}
var out = [];
var addrTypes = {
'home': 'hometown_location',
'current': 'current_location'
};
Object.keys(addrTypes).forEach(function onAddressType(type) {
var addrObj = fbdata[addrTypes[type]];
if (addrObj) {
out.push(fillAddress(addrObj, type));
}
});
return out;
};
// The contact is now totally unlinked
// [...,facebook, fb_not_linked, 123456,....]
fb.markAsUnlinked = function(devContact) {
var category = devContact.category;
var updatedCategory = [];
if (category) {
var idx = category.indexOf(fb.CATEGORY);
if (idx !== -1) {
for (var c = 0; c < idx; c++) {
updatedCategory.push(category[c]);
}
// The facebook category, the linked mark and the UID are skipped
for (c = idx + 3; c < category.length; c++) {
updatedCategory.push(category[c]);
}
}
}
devContact.category = updatedCategory;
return devContact;
};
// Unlinks a FB Contact. This must only be called from the clean all process
fb.unlinkClearAll = function(devContact) {
fb.resetNames(devContact);
fb.markAsUnlinked(devContact);
};
// Reset givenName and familyName if it is needed after unlinking
fb.resetNames = function resetNames(dContact) {
if (fb.isPropagated('givenName', dContact)) {
dContact.givenName = [''];
fb.removePropagatedFlag('givenName', dContact);
}
if (fb.isPropagated('familyName', dContact)) {
dContact.familyName = [''];
fb.removePropagatedFlag('familyName', dContact);
}
dContact.name = [dContact.givenName[0] + ' ' + dContact.familyName[0]];
};
// Marks that FB Cleaning is in progress or not
fb.markFbCleaningInProgress = function(value) {
utils.cookie.update({
fbCleaningInProgress: value
});
};

View file

@ -0,0 +1,380 @@
'use strict';
/* global LazyLoader, TelIndexer, SimplePhoneMatcher */
// WARNING: This file lazy loads:
// 'shared/js/fb/fb_data_reader.js
// 'shared/js/fb/fb_tel_index.js'
// 'shared/js/binary_search.js'
// 'shared/js/simple_phone_matcher.js' (will be removed once bug 938265 lands)
var fb = window.fb || {};
(function() {
var contacts = fb.contacts || {};
fb.contacts = contacts;
var Reader;
var readerLoaded = false;
// Record Id for the index
var INDEX_ID = 1;
var isIndexDirty = false;
var READER_LOADED_EV = 'reader_loaded';
var TEL_INDEXER_JS = '/shared/js/fb/fb_tel_index.js';
var PHONE_MATCHER_JS = '/shared/js/simple_phone_matcher.js';
var FB_READER_JS = '/shared/js/fb/fb_data_reader.js';
var BINARY_SEARCH_JS = '/shared/js/binary_search.js';
contacts.UID_NOT_FOUND = 'UIDNotFound';
contacts.ALREADY_EXISTS = 'AlreadyExists';
// This is needed for having proxy methods setted and ready before
// the real reader methods (in fb_data_reader) are loaded
if (!contacts.init) {
var proxyMethods = ['get', 'getLength', 'getByPhone', 'search',
'refresh', 'init', 'restart'];
proxyMethods.forEach(function(aMethod) {
contacts[aMethod] = defaultFunction.bind(null, aMethod);
});
LazyLoader.load(FB_READER_JS, onreaderLoaded);
}
else {
onreaderLoaded();
}
function onreaderLoaded() {
readerLoaded = true;
Reader = fb.contacts;
document.dispatchEvent(new CustomEvent(READER_LOADED_EV));
}
function setIndex(index) {
Reader.dsIndex = index;
isIndexDirty = false;
}
function datastore() {
return Reader.datastore;
}
function index() {
return Reader.dsIndex;
}
function defaultFunction(target) {
var args = [];
for (var j = 1; j < arguments.length; j++) {
args.push(arguments[j]);
}
if (!readerLoaded) {
document.addEventListener(READER_LOADED_EV, function rd_loaded() {
document.removeEventListener(READER_LOADED_EV, rd_loaded);
Reader[target].apply(this, args);
});
}
else {
// As the reader load will overwrite those functions probably this
// will never be called
/*jshint validthis:true */
Reader[target].apply(this, args);
}
}
function initError(outRequest, error) {
outRequest.failed(error);
}
// Ensures a proper error object is returned
function safeError(err) {
if (err && err.name) {
return err;
}
return {
name: 'UnknownError'
};
}
// Creates a default handler for errors
function defaultError(request) {
return defaultErrorCb.bind(null, request);
}
// Creates a default handler for success
function defaultSuccess(request) {
return defaultSuccessCb.bind(null, request);
}
function defaultErrorCb(request, error) {
request.failed(safeError(error));
}
function defaultSuccessCb(request, result) {
request.done(result);
}
function doSave(obj, outRequest) {
LazyLoader.load([TEL_INDEXER_JS, PHONE_MATCHER_JS,
BINARY_SEARCH_JS] , function() {
var uid = obj.uid;
datastore().add(obj, uid).then(function success() {
indexByPhone(obj, uid);
isIndexDirty = true;
outRequest.done();
}, function error(err) {
if (err.name === 'ConstraintError') {
err = {
name: contacts.ALREADY_EXISTS
};
}
outRequest.failed(err);
});
});
}
function indexByPhone(obj, newId) {
// Update index by tel
// As this is populated by FB importer we don't need to have
// extra checks
if (Array.isArray(obj.tel)) {
obj.tel.forEach(function(aTel) {
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
variants.forEach(function(aVariant) {
index().byTel[aVariant] = newId;
});
// To avoid the '+' char
TelIndexer.index(index().treeTel, aTel.value.substring(1), newId);
});
}
}
function reIndexByPhone(oldObj, newObj, dsId) {
removePhoneIndex(oldObj);
indexByPhone(newObj, dsId);
TelIndexer.orderTree(index().treeTel);
}
function removePhoneIndex(deletedFriend) {
// Need to update the tel indexes
if (Array.isArray(deletedFriend.tel)) {
deletedFriend.tel.forEach(function(aTel) {
TelIndexer.remove(index().treeTel, aTel.value.substring(1));
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
variants.forEach(function(aVariant) {
delete index().byTel[aVariant];
});
});
}
}
/**
* Allows to save FB Friend Information
*
*/
contacts.save = function(obj) {
var retRequest = new fb.utils.Request();
window.setTimeout(function save() {
contacts.init(function() {
doSave(obj, retRequest);
},
function(err) {
initError(retRequest, err);
});
}, 0);
return retRequest;
};
/**
* Allows to update FB Friend Information
*
*
*/
contacts.update = function(obj) {
var retRequest = new fb.utils.Request();
window.setTimeout(function save() {
contacts.init(function() {
doUpdate(obj, retRequest);
},
function(err) {
initError(retRequest, err);
});
}, 0);
return retRequest;
};
function doUpdate(obj, outRequest) {
LazyLoader.load([TEL_INDEXER_JS, PHONE_MATCHER_JS,
BINARY_SEARCH_JS], function() {
var uid = obj.uid;
var successCb = successUpdate.bind(null, outRequest);
var errorCb = errorUpdate.bind(null, outRequest, uid);
// It is necessary to get the old object and delete old indexes
datastore().get(uid).then(function success(oldObj) {
if (!oldObj) {
errorCb({
name: contacts.UID_NOT_FOUND
});
return;
}
reIndexByPhone(oldObj, obj, uid);
datastore().put(obj, uid).then(function success() {
return datastore().put(index(), INDEX_ID);
}, errorCb).then(successCb, errorCb);
}, errorCb); // datastore.get
});
}
function successUpdate(outRequest) {
outRequest.done();
}
function errorUpdate(outRequest, uid, error) {
window.console.error('Error while updating datastore for: ', uid);
outRequest.failed(error);
}
function doRemove(uid, outRequest, forceFlush) {
LazyLoader.load([TEL_INDEXER_JS, PHONE_MATCHER_JS,
BINARY_SEARCH_JS], function() {
var errorCb = errorRemove.bind(null, outRequest, uid);
var objToDelete;
datastore().get(uid).then(function success_get_remove(object) {
objToDelete = object;
if (!objToDelete) {
errorRemove(outRequest, uid, {
name: contacts.UID_NOT_FOUND
});
return;
}
datastore().remove(uid).then(function success_rm(removed) {
successRemove(outRequest, objToDelete, forceFlush, removed);
}, errorCb);
}, errorCb);
}); // LazyLoader.load
}
// Needs to update the index data conveniently
function successRemove(outRequest, deletedFriend, forceFlush, removed) {
if (removed === true) {
isIndexDirty = true;
removePhoneIndex(deletedFriend);
if (forceFlush) {
var flushReq = fb.contacts.flush();
flushReq.onsuccess = function() {
isIndexDirty = false;
outRequest.done(true);
};
flushReq.onerror = function() {
outRequest.failed(flushReq.error);
};
}
else {
outRequest.done(true);
}
}
else {
outRequest.done(false);
}
}
function errorRemove(outRequest, uid, error) {
error = safeError(error);
window.console.error('FB Data: Error while removing ', uid, ': ',
error.name);
outRequest.failed(error);
}
/**
* Allows to remove FB contact from the DB
*
*/
contacts.remove = function(uid, flush) {
var hasToFlush = (flush === true ? flush : false);
var retRequest = new fb.utils.Request();
window.setTimeout(function remove() {
contacts.init(function() {
doRemove(uid, retRequest, hasToFlush);
},
function(err) {
initError(retRequest, err);
});
}, 0);
return retRequest;
};
/**
* Removes all the FB Friends and the index
*
* The index is restored as empty
*
*/
contacts.clear = function() {
var outRequest = new fb.utils.Request();
window.setTimeout(function clear() {
contacts.init(function() {
doClear(outRequest);
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doClear(outRequest) {
datastore().clear().then(function success() {
setIndex(null);
// TODO:
// This is working but there are open questions on the mailing list
datastore().put(index(), INDEX_ID).then(defaultSuccess(outRequest),
function error(err) {
err = safeError(err);
window.console.error('Error while re-creating the index: ',
err.name);
outRequest.failed(err);
}
);
}, defaultError(outRequest));
}
/**
* Persists the index on the datastore
*
*/
contacts.flush = function() {
var outRequest = new fb.utils.Request();
window.setTimeout(function do_Flush() {
if (!(datastore()) || !isIndexDirty) {
window.console.warn(
'The datastore has not been initialized or is not dirty');
outRequest.done();
return;
}
TelIndexer.orderTree(index().treeTel);
datastore().put(index(), INDEX_ID).then(
defaultSuccess(outRequest),
defaultError(outRequest));
}, 0);
return outRequest;
};
})();

View file

@ -0,0 +1,163 @@
'use strict';
/* global fb */
/* jshint ignore:start */
var fb = this.fb || {};
// In an only worker execution context this would not be necessary
var self = this;
fb.utils = this.fb.utils || {};
/* jshint ignore:end */
fb.DEFAULT_TIMEOUT = 30000;
// Runs a query against Facebook FQL. Callback is a string!!
fb.utils.runQuery = function(query, callback, access_token) {
// Auxiliary function for canceling a request
function QueryRequest() {
this.cancel = function() {
if (typeof this.oncancel === 'function') {
window.setTimeout(function() {
this.oncancel();
}.bind(this), 0);
}
};
}
var outReq = new QueryRequest();
var queryService = 'https://graph.facebook.com/fql?q=';
queryService += encodeURIComponent(query);
var params = ['access_token' + '=' + access_token,
'format=json'];
var queryParams = params.join('&');
var remote = queryService + '&' + queryParams;
var xhr = new XMLHttpRequest({
mozSystem: true
});
// To enable xhr.abort if user cancels
outReq.xhr = xhr;
outReq.oncancel = function() {
this.xhr.abort();
};
xhr.open('GET', remote, true);
xhr.responseType = 'json';
xhr.timeout = fb.operationsTimeout || fb.DEFAULT_TIMEOUT;
xhr.onload = function(e) {
if (xhr.status === 200 || xhr.status === 400 || xhr.status === 0) {
if (callback && typeof callback.success === 'function') {
self.setTimeout(function() {
callback.success(xhr.response);
},0);
}
}
else {
self.console.error('FB: HTTP error executing query. ',
query, ' Status: ', xhr.status);
if (callback && typeof callback.error === 'function') {
self.setTimeout(callback.error, 0);
}
}
}; // onload
xhr.ontimeout = function(e) {
self.console.error('FB: Timeout!!! while executing query', query);
if (callback && typeof callback.timeout === 'function') {
self.setTimeout(callback.timeout, 0);
}
}; // ontimeout
xhr.onerror = function(e) {
self.console.error('FB: Error while executing query: ', query,
': ', e);
if (callback && typeof callback.error === 'function') {
self.setTimeout(function() {
callback.error(e);
},0);
}
}; // onerror
xhr.send();
return outReq;
};
/**
* Obtains a img DOM Element with the Contact's img
*
*/
fb.utils.getFriendPicture = function(uid, callback, access_token,
targetPictureSize) {
// Access token is necessary just in case the image is not public
// When passing an access token to FB https must be used
var imgSrc = 'https://graph.facebook.com/' + uid + '/picture?';
var params = [
'width=' + targetPictureSize,
'access_token=' + access_token,
't=' + Date.now()
];
var imgService = imgSrc + params.join('&');
var xhr = new XMLHttpRequest({
mozSystem: true
});
xhr.open('GET', imgService, true);
xhr.responseType = 'blob';
xhr.timeout = fb.operationsTimeout || fb.DEFAULT_TIMEOUT;
xhr.onload = function(e) {
if (xhr.status === 200 || xhr.status === 0) {
var mblob = e.target.response;
if (typeof callback === 'function') {
self.setTimeout(function() {
callback(mblob);
},0);
}
}
else {
self.console.error('FB: HTTP error retrieving img for uid: ',
uid, ' Status: ', xhr.status);
if (typeof callback === 'function') {
self.setTimeout(function() {
callback(null);
},0);
}
}
}; // onload
xhr.ontimeout = function(e) {
self.console.error('FB: Timeout!!! while retrieving img for uid: ', uid);
if (typeof callback === 'function') {
self.setTimeout(function() {
callback(null);
},0);
}
}; // ontimeout
xhr.onerror = function(e) {
self.console.error('FB: Error while retrieving img for uid: ', uid,
'Error: ', e);
if (typeof callback === 'function') {
self.setTimeout(function() {
callback(null);
},0);
}
}; // onerror
xhr.send();
};

View file

@ -0,0 +1,526 @@
'use strict';
/* global ImportStatusData, oauthflow, utils */
/* Ignoring function declaration in loops */
/* jshint -W083 */
var fb = window.fb || {};
window.fb = fb;
(function(document) {
var Utils = fb.utils || {};
fb.utils = Utils;
var FRIEND_COUNT_QUERY = 'select friend_count from user where uid=me()';
var CACHE_FRIENDS_KEY = Utils.CACHE_FRIENDS_KEY = 'numFacebookFriends';
var LAST_UPDATED_KEY = Utils.LAST_UPDATED_KEY = 'lastUpdatedTime';
Utils.ALARM_ID_KEY = 'nextAlarmId';
Utils.SCHEDULE_SYNC_KEY = 'facebookShouldHaveScheduledAt';
var REDIRECT_LOGOUT_URI = window.oauthflow ?
oauthflow.params.facebook.redirectLogout : '';
var STORAGE_KEY = Utils.TOKEN_DATA_KEY = 'tokenData';
// For controlling data synchronization
Utils.setLastUpdate = function(value, cb) {
window.asyncStorage.setItem(LAST_UPDATED_KEY, {
data: value
}, cb);
};
Utils.getLastUpdate = function(callback) {
window.asyncStorage.getItem(LAST_UPDATED_KEY, function(obj) {
var out = 0;
if (obj) {
out = obj.data || out;
}
if (typeof callback === 'function') {
callback(out);
}
});
};
Utils.getNonCacheableUrl = function(url) {
if (url.indexOf('?') === -1) {
url += '?';
}
return url + '&burst_cache=' + Date.now();
};
Utils.getContactData = function(cid) {
var outReq = new Utils.Request();
var req = navigator.mozContacts.find({
filterBy: ['id'],
filterValue: cid,
filterOp: 'equals'
});
req.onsuccess = function(e) {
if (e.target.result && e.target.result.length > 0) {
outReq.done(e.target.result[0]);
}
else {
outReq.done(null);
}
};
req.onerror = function(e) {
outReq.failed(e.target.error);
};
return outReq;
};
// Returns the mozContact associated to a UID in FB
Utils.getMozContact = function(uid) {
var outReq = new Utils.Request();
window.setTimeout(function get_mozContact_ByUid() {
fb.getMozContactByUid(uid,
function onsuccess(result) {
if (Array.isArray(result) && result.length > 0) {
outReq.done(result[0]);
} else {
outReq.done(null);
}
},
function onerror(e) {
outReq.failed(e.target.error);
}
);
}, 0);
return outReq;
};
// Returns the number of mozContacts associated to a UID in FB
Utils.getNumberMozContacts = function(uid) {
var outReq = new Utils.Request();
window.setTimeout(function get_mozContact_ByUid() {
fb.getMozContactByUid(uid,
function onsuccess(result) {
if (Array.isArray(result)) {
outReq.done(result.length);
} else {
outReq.done(0);
}
},
function onerror(error) {
outReq.failed(error);
}
);
},0);
return outReq;
};
Utils.getAllFbContacts = function() {
var outReq = new Utils.Request();
window.setTimeout(function get_all_fb_contacts() {
var filter = {
filterValue: fb.CATEGORY,
filterOp: 'contains',
filterBy: ['category']
};
var req = navigator.mozContacts.find(filter);
req.onsuccess = function(e) {
outReq.done(e.target.result);
};
req.onerror = function(e) {
outReq.failed(e.target.error);
};
}, 0);
return outReq;
};
// On the device
Utils.getNumFbContacts = function() {
var outReq = new Utils.Request();
window.setTimeout(function get_num_fb_contacts() {
var req = fb.contacts.getLength();
req.onsuccess = function() {
outReq.done(req.result);
};
req.onerror = function() {
outReq.failed(req.error);
};
}, 0);
return outReq;
};
// Requests the number remotely
Utils.getNumFbFriends = function(callback, access_token) {
fb.utils.runQuery(FRIEND_COUNT_QUERY, callback, access_token);
};
Utils.getCachedAccessToken = function(callback) {
ImportStatusData.get(STORAGE_KEY).then(function(data) {
var out = null;
if (data) {
out = data.access_token || null;
}
if (typeof callback === 'function') {
callback(out);
}
});
};
Utils.setCachedAccessToken = function(data, callback) {
ImportStatusData.put(STORAGE_KEY, data).then(function done() {
if (typeof callback === 'function') {
callback();
}
}, function error() {
console.error('Error while saving access token');
if (typeof callback === 'function') {
callback();
}
});
};
// Obtains the number locally
Utils.getCachedNumFbFriends = function(callback) {
ImportStatusData.get(CACHE_FRIENDS_KEY).then(function(data) {
if (typeof callback === 'function' && typeof data === 'number') {
callback(data);
}
});
};
Utils.setCachedNumFriends = function(value, cb) {
ImportStatusData.put(CACHE_FRIENDS_KEY, value).then(cb);
};
// Removes cached number of friends from datastore to keep state of sync.
Utils.removeCachedNumFriends = function(callback) {
ImportStatusData.remove(CACHE_FRIENDS_KEY).then(function() {
typeof callback === 'function' && callback();
});
};
Utils.getImportChecked = function(callback) {
// If we have an access token Import should be checked
Utils.getCachedAccessToken(function(access_token) {
var out = 'logged-out';
if (access_token) {
out = 'logged-in';
}
else {
// In this case is needed to know whether the access_token has
// been invalidated
Utils.getCachedNumFbFriends(function(value) {
if (value) {
out = 'renew-pwd';
if (typeof callback === 'function') {
callback(out);
}
}
});
}
if (typeof callback === 'function') {
callback(out);
}
});
};
// Obtains the number locally (cached) and tries to get them remotely
Utils.numFbFriendsData = function(callback) {
var localCb = callback.local;
var remoteCb = callback.remote;
Utils.getCachedNumFbFriends(localCb);
function auxCallback(response) {
if (response.data && response.data[0] &&
response.data[0].friend_count) {
remoteCb(response.data[0].friend_count);
}
}
if (typeof remoteCb === 'function' && navigator.onLine === true) {
var remoteCallbacks = {
success: auxCallback,
error: null,
timeout: null
};
Utils.getCachedAccessToken(function(access_token) {
if (access_token) {
Utils.getNumFbFriends(remoteCallbacks, access_token);
}
});
}
};
// Clears all Fb data (use with caution!!)
Utils.clearFbData = function() {
// First step all Contacts which are FB Friends are obtained
// then those not linked are directly removed
// Those linked are unlinked
var outReq = new Utils.Request();
window.setTimeout(function do_clearFbData() {
// First a clear request is issued
var ireq = fb.contacts.clear();
ireq.onsuccess = function() {
var req = Utils.getAllFbContacts();
req.onsuccess = function() {
var cleaner = new Utils.FbContactsCleaner(req.result, 'clear');
// And now success notification is sent
outReq.done(cleaner);
// The cleaning activity should be starting immediately
window.setTimeout(cleaner.start, 0);
};
req.onerror = function() {
window.console.error('FB Clean. Error retrieving FB Contacts');
outReq.failed(req.error);
};
};
ireq.onerror = function(e) {
window.console.error('Error while clearing the FB Cache');
outReq.failed(ireq.error);
};
},0);
return outReq;
};
Utils.logout = function() {
var outReq = new Utils.Request();
// Duration of the timer that checks window close by the user (1/2 sec)
var WINDOW_TIMER_INTERVAL = 500;
// Maximum time we are going to wait for a logout operation (5 secs)
var LOGOUT_TIMEOUT = 5000;
var MAX_TIMER_TICKS = LOGOUT_TIMEOUT / WINDOW_TIMER_INTERVAL;
window.setTimeout(function do_logout() {
Utils.getCachedAccessToken(function getAccessToken(access_token) {
if (access_token) {
var logoutService = 'https://www.facebook.com/logout.php?';
var params = [
'next' + '=' + encodeURIComponent(REDIRECT_LOGOUT_URI),
'access_token' + '=' + access_token
];
var logoutParams = params.join('&');
var logoutUrl = logoutService + logoutParams;
var loggedOut = false;
var m_listen = function(e) {
if (e.origin !== fb.CONTACTS_APP_ORIGIN) {
return;
}
if (e.data === 'closed') {
loggedOut = true;
ImportStatusData.remove(STORAGE_KEY);
outReq.done();
}
e.stopImmediatePropagation();
window.removeEventListener('message', m_listen);
};
window.addEventListener('message', m_listen);
var logoutWindow = window.open(logoutUrl, '', 'dialog');
// This timer is run in order to monitor whether the window is
// closed by the user during the logout process (bug 1084224).
// This will have to be removed once bug 966216 is fixed
var timerTicks = 0;
var timerId = window.setInterval(function() {
timerTicks++;
var closed = (logoutWindow.closed === true);
var timedOut = (timerTicks >= MAX_TIMER_TICKS);
if (loggedOut || closed || timedOut) {
window.clearInterval(timerId);
window.removeEventListener('message', m_listen);
}
if (closed && !loggedOut) {
outReq.failed('UserCancelled');
}
else if (timedOut && !loggedOut) {
outReq.failed('Timeout');
}
}, WINDOW_TIMER_INTERVAL);
} // if
else {
outReq.done();
}
}); // cachedToken
}, 0); // setTimeout
return outReq;
}; // logout
// FbContactsCleaner Object
// Mode can be 'update' or 'cleanAll'
Utils.FbContactsCleaner = function(contacts, pmode) {
this.lcontacts = contacts;
var total = contacts.length;
var next = 0;
var self = this;
var CHUNK_SIZE = 5;
var numResponses = 0;
var mustUpdate = (pmode === 'update');
var notifyClean = false;
var mustHold = false;
var holded = false;
var mustFinish = false;
this.start = function() {
mustHold = holded = mustFinish = false;
if (total > 0) {
cleanContacts(0);
}
else if (typeof self.onsuccess === 'function') {
window.setTimeout(self.onsuccess);
}
};
this.hold = function() {
mustHold = true;
};
this.finish = function() {
mustFinish = true;
if (holded) {
notifySuccess();
}
};
this.resume = function() {
mustHold = holded = mustFinish = false;
window.setTimeout(function resume_clean() {
cleanContacts(next);
});
};
function successHandler(e) {
if (notifyClean || typeof self.oncleaned === 'function') {
notifyClean = true;
// Avoiding race condition so the cleaned element is cached
var cleaned = e.target.number;
window.setTimeout(function() {
self.oncleaned(cleaned);
},0);
}
continueCb();
}
function errorHandler(contactid, error) {
if (typeof self.onerror === 'function') {
self.onerror(contactid, error);
}
continueCb();
}
function cleanContacts(from) {
for (var idx = from; idx < (from + CHUNK_SIZE) && idx < total; idx++) {
var contact = contacts[idx];
var number = idx;
var req, fbContact;
if (fb.isFbLinked(contact)) {
if (mustUpdate) {
fbContact = new fb.Contact(contact);
req = fbContact.unlink('hard');
}
else {
// Here we only need to mark this contact as unlinked
// as the FB Data would have been already removed
fb.unlinkClearAll(contact);
req = navigator.mozContacts.save(
utils.misc.toMozContact(contact));
}
}
else {
if (mustUpdate) {
fbContact = new fb.Contact(contact);
req = fbContact.remove();
}
else {
req = navigator.mozContacts.remove(
utils.misc.toMozContact(contact));
}
}
req.number = number;
req.onsuccess = successHandler;
req.onerror = function(e) {
errorHandler(contact.id, e.target.error);
};
}
}
function notifySuccess() {
if (typeof self.onsuccess === 'function') {
window.setTimeout(self.onsuccess);
}
}
function finishHandler() {
// It is needed to flush in order to properly update the index
var req = fb.contacts.flush();
req.onsuccess = notifySuccess;
req.onerror = function cleaner_flushError() {
errorHandler(null, req.error);
};
}
function continueCb() {
next++;
numResponses++;
if (next < total && numResponses === CHUNK_SIZE) {
numResponses = 0;
if (!mustHold && !mustFinish) {
cleanContacts(next);
}
else if (mustFinish && !holded) {
finishHandler();
}
if (mustHold) {
holded = true;
}
}
else if (next >= total) {
// End has been reached
finishHandler();
}
} // function
}; // FbContactsCleaner
})(document);

View file

@ -0,0 +1,223 @@
'use strict';
/* global Normalizer, utils */
/* exported FriendListRenderer */
var FriendListRenderer = (function() {
// Order criteria
var orderCriteria = {
firstName: ['givenName', 'familyName', 'email1'],
lastName: ['familyName', 'givenName', 'email1']
};
var HEADER_LETTERS = [
'ABCDEFGHIJKLMNOPQRSTUVWXYZ', // Roman
'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ' , // Greek
'АБВГДЂЕЁЖЗИЙЈКЛЉМНЊОПРСТЋУФХЦЧЏШЩЭЮЯ' // Cyrillic (Russian + Serbian)
].join('');
// Options by default
var defaults = {
container: '#groups-list',
orderBy: 'lastName'
};
// Callback to notify when finish
var finishCb;
var totalContacts;
var lock;
var CHUNK_SIZE = 10;
var render = function render(contacts, cb, options) {
// Populate defaults
for (var key in defaults) {
if (typeof options[key] === 'undefined') {
options[key] = defaults[key];
}
}
finishCb = cb;
totalContacts = contacts.length;
lock = navigator.requestWakeLock('cpu');
doRender(contacts, cb, options);
};
var doRender = function doRender(contacts, cb, options) {
var groupsList = document.querySelector(options.container);
var orderBy = options.orderBy;
var order = orderCriteria[orderBy];
// Sorting friend list
contacts.sort(function(a, b) {
return getStringToBeOrdered(a, order).localeCompare(
getStringToBeOrdered(b, order));
});
// Hash containing each group
var groups = {};
contacts.forEach(function(contact) {
// Contacts are ordered so it is pretty easy to group them
var groupName = getGroupName(contact, order);
if (!groups[groupName]) {
groups[groupName] = [];
}
// An array per group
groups[groupName].push(contact);
});
// We are going to delete the paragraph that paints the other order for the
// contact's name from template...
var notRenderedParagraph = groupsList.querySelector('[data-order-by="' +
(orderBy === 'firstName' ? 'lastName' : 'firstName') + '"]');
if (notRenderedParagraph) {
notRenderedParagraph.parentNode.removeChild(notRenderedParagraph);
}
// Start first element of HEADER_LETTERS (A)
var letterStart = 0;
doRenderGroupChunk(letterStart, HEADER_LETTERS[letterStart],
groupsList, groups);
};
// Controls group rendering one by one
function doRenderGroupChunk(index, group, groupsList, groups) {
renderGroup(groupsList, group, groups[group], function(fragment) {
if (fragment) {
groupsList.appendChild(fragment);
}
fragment = null;
var headerLettersLength = HEADER_LETTERS.length;
if (index + 1 <= headerLettersLength) {
window.setTimeout(function renderNextGroup() {
doRenderGroupChunk(index + 1, HEADER_LETTERS[index + 1],
groupsList, groups);
});
}
else if (group != '#') {
window.setTimeout(function renderNextGroup() {
doRenderGroupChunk(index + 1, '#', groupsList, groups);
});
}
else {
// Deleting template
groupsList.removeChild(groupsList.firstElementChild);
if (typeof finishCb === 'function') {
// We wait a delay depending on number of nodes
// Afterwards the curtain will be displayed
window.setTimeout(finishCb, totalContacts * 2);
}
if (lock) {
lock.unlock();
}
}
});
}
// Renders the items in a group in chunks
function doRenderGroupItems(from, groupsList, group, friends, element, cb) {
var end = from + CHUNK_SIZE;
// This is the <ol> and <header> is children[0]
var list = element.children[1];
for (var i = from; i < end && i < friends.length; i++) {
var friend = friends[i];
if (friend.search && friend.search.length > 0) {
// Set the picture size
var box = utils.misc.getPreferredPictureBox();
friend.picwidth = box.width;
friend.picheight = box.height;
friend.search = Normalizer.toAscii(friend.search);
// New friend appended
utils.templates.append(list, friend);
}
}
if (i < friends.length) {
window.setTimeout(function renderNextChunk() {
doRenderGroupItems(end, groupsList, group, friends, element, cb);
});
}
else {
list.removeChild(list.firstElementChild);
cb();
}
}
// Renders a group
function renderGroup(groupsList, group, friends, cb) {
if (!friends || friends.length === 0) {
window.setTimeout(cb);
return;
}
// Document fragment that will hold the group nodes
var fragment = document.createDocumentFragment();
// New element appended
var element = utils.templates.append(groupsList, {
group: group
}, fragment);
// For each friend in the group
doRenderGroupItems(0, groupsList, group, friends, element, function() {
cb(fragment);
});
}
function getStringToBeOrdered(contact, order) {
var ret = contact.search;
if (!ret) {
ret = [];
order.forEach(function(field) {
ret.push(getValue(contact, field));
});
ret = contact.search = ret.join(' ');
}
return ret;
}
function getValue(contact, field) {
var out = contact[field];
if (out) {
out = Array.isArray(out) ? out[0] : out;
} else {
out = '';
}
return out;
}
function getGroupName(contact, order) {
var ret = getStringToBeOrdered(contact, order);
ret = ret.charAt(0).toUpperCase();
ret = ret.replace(/[ÁÀ]/ig, 'A');
ret = ret.replace(/[ÉÈ]/ig, 'E');
ret = ret.replace(/[ÍÌ]/ig, 'I');
ret = ret.replace(/[ÓÒ]/ig, 'O');
ret = ret.replace(/[ÚÙ]/ig, 'U');
if (HEADER_LETTERS.indexOf(ret) < 0) {
ret = '#';
}
return ret;
}
return {
'render': render
};
})();

View file

@ -0,0 +1,475 @@
/* globals Rest */
/* exported GmailConnector */
'use strict';
/*
Gmail Contacts connector
Provides the capabilities to connect to the Gmail Contacts service
and return the contacts on a readable and importable format.
*/
var GmailConnector = (function GmailConnector() {
// Google contacts service end point,
// force a huge number of contacts to not paginate :S
var END_POINT =
'https://www.google.com/m8/feeds/contacts/default/full/?max-results=10000';
var GROUPS_END_POINT =
'https://www.google.com/m8/feeds/groups/default/full/';
var EXTRA_HEADERS = {
'GData-Version': '3.0'
};
var GD_NAMESPACE = 'http://schemas.google.com/g/2005';
var CATEGORY = 'gmail';
var URN_IDENTIFIER = 'urn:service:gmail:uid:';
// Will be used as a cache for the thumbnail url for each contact
var photoUrls = {};
// In some cases we will need the access token, cache a copy
var accessToken = null;
// We have a xml response from Google, all the entries in an array,
// no matter if they are xml entries.
// Despite of being a node, inject a 'uid' as this will be necessary
// for the selection
var nodeListToArray = function nodeListToArray(response) {
var entries = response.getElementsByTagName('entry');
var contacts = [];
var numContacts = entries.length;
for (var i = 0; i < numContacts; i++) {
contacts.push(gContactToJson(entries[i]));
}
return contacts;
};
// Returns the object used to build the headers necesary by the service
var buildRequestHeaders = function buildRequestHeaders(access_token) {
var requestHeaders = EXTRA_HEADERS;
requestHeaders.Authorization = 'OAuth ' + access_token;
return requestHeaders;
};
// Gets a list of all contacts giving a valid access token
var listAllContacts = function listAllContacts(access_token,
callbacks) {
// Copy the access_token
accessToken = access_token;
photoUrls = {};
getContactsGroup(access_token, callbacks);
};
var getContactsGroup = function getContactsGroup(access_token,
callbacks) {
var groupCallbacks = {
success: function onSuccess(response) {
// Locate the entry witch systemGroup id is 'Contacts'
var feed = response.querySelector('feed');
if (feed === null) {
callbacks.error();
return;
}
var sgc = feed.querySelector('systemGroup[id="Contacts"]');
if (sgc !== null) {
var id = sgc.parentNode.querySelector('id').textContent;
getContactsByGroup(id, access_token, callbacks);
} else {
callbacks.error();
}
},
error: function(e) {
if (e && e.status === 401) {
// This is a token expired / invalid token problem
window.console.warn('GMail Access token expired or invalid. ',
'restarting flow!');
callbacks.success({
error: {
code: 190
}
});
}
else {
callbacks.error();
}
},
timeout: callbacks.timeout
};
return performAPIRequest(GROUPS_END_POINT, groupCallbacks, access_token);
};
// Retrieve all the contacts for the specific groupId
var getContactsByGroup = function getContactsByGroup(groupId, access_token,
callbacks) {
var listingCallbacks = {
success: function onSuccess(response) {
callbacks.success({
'data': nodeListToArray(response)
});
},
error: callbacks.error,
timeout: callbacks.timeout
};
var groupUrl = END_POINT + '&group=' + groupId;
return performAPIRequest(groupUrl, listingCallbacks, access_token);
};
// Given a Google contacts api url add the authentication and
// extra headers to perform the correct request
var performAPIRequest = function performAPIRequest(url, callbacks,
access_token) {
return Rest.get(url, callbacks, {
'requestHeaders': buildRequestHeaders(access_token),
'responseType': 'xml'
});
};
// Return the list of contacts on the device imported using this connector
var listDeviceContacts = function listDeviceContacts(callbacks) {
var filterOptions = {
filterValue: CATEGORY,
filterOp: 'contains',
filterBy: ['category']
};
var req = navigator.mozContacts.find(filterOptions);
req.onsuccess = function() {
callbacks.success(req.result);
};
req.onerror = function onError() {
callbacks.success([]);
};
};
var getImporter = function getImporter(contactsList, access_token) {
return new window.ContactsImporter(contactsList, access_token, this);
};
var cleanContacts = function cleanContacts(contactsList, mode, cb) {
var cleaner = new window.ContactsCleaner(contactsList);
window.setTimeout(cleaner.start, 0);
if (cb) {
cb(cleaner);
}
};
var getValueForNode = function getValueForNode(doc, name, def) {
var defaultValue = def || '';
if (doc == null || name == null) {
return defaultValue;
}
var node = doc.querySelector(name);
if (node && node.textContent) {
return node.textContent;
}
return defaultValue;
};
/*
Given an xml contact entry from gmail contacts we will need to output
a json object that contains the minimun viable information to display
that contact into a list.
The fields for that minimun visualization object are:
uid
givenName
familyName
email1
*/
var adaptDataForShowing = function adaptDataForShowing(contact) {
var output = {
'uid': '-1',
'givenName': '',
'familyName': '',
'email1': '',
'contactPictureUri': ''
};
output.uid = contact.uid;
if (contact.familyName) {
output.familyName = contact.familyName;
}
if (contact.email && contact.email.length > 0) {
output.email1 = contact.email[0].value;
}
var tel;
if (contact.tel && contact.tel.length > 0) {
tel = contact.tel[0].value;
}
if(contact.givenName) {
output.givenName = contact.givenName;
} else if(!contact.familyName) {
output.givenName = output.email1 || contact.org || tel;
} else if(contact.familyName) {
output.givenName = '' ;
}
var photoUrl = buildContactPhotoURL(contact, accessToken);
if (photoUrl) {
output.contactPictureUri = photoUrl;
}
return output;
};
var adaptDataForSaving = function adaptDataForSaving(contact) {
return contact;
};
// Transform a Google contact entry into json format.
// The json format is the same used in Contacts api ;P
var gContactToJson = function gContactToJson(googleContact) {
var output = {};
// This field will be needed for indexing within the
// import process, not for the api
output.uid = getUid(googleContact);
output.name = [getValueForNode(googleContact, 'title')];
// Store the photo url, not in the contact itself
var photoUrl = googleContact.querySelector('link[type="image/*"]');
if (photoUrl) {
photoUrl = photoUrl.getAttribute('href');
} else {
// No image link
photoUrl = '';
}
photoUrls[output.uid] = photoUrl;
var name = googleContact.querySelector('name');
if (name) {
var contactName = getValueForNode(name, 'givenName');
if (contactName) {
output.givenName = [contactName];
}
var contactFamilyName = getValueForNode(name, 'familyName');
if (contactFamilyName) {
output.familyName = [contactFamilyName];
}
var contactSuffix = getValueForNode(name, 'additionalName');
if (contactSuffix) {
output.additionalName = [contactSuffix];
}
}
output.email = parseEmails(googleContact);
output.adr = parseAddress(googleContact);
output.tel = parsePhones(googleContact);
var org = googleContact.querySelector('organization');
if (org) {
output.org = [getValueForNode(org, 'orgName')];
output.jobTitle = [getValueForNode(org, 'orgTitle')];
}
var bday = googleContact.querySelector('birthday');
if (bday) {
var bdayMS = Date.parse(bday.getAttribute('when'));
if (!isNaN(bdayMS)) {
output.bday = new Date(bdayMS);
}
}
var content = googleContact.querySelector('content');
if (content) {
output.note = [content.textContent];
}
output.category = [CATEGORY];
output.url = [{
type: ['source'],
value: getContactURI(output)
}];
return output;
};
var getContactURI = function getContactURI(contact) {
return URN_IDENTIFIER + contact.uid;
};
// This will be a full url like:
// http://www.google.com/m8/feeds/contacts/<email>/base/<contact_id>
// for a specific contact node
var getUid = function getUid(contact) {
return contact.querySelector('id').textContent;
};
// Returns an array with the possible emails found in a contact
// as a ContactField format
var parseEmails = function parseEmails(googleContact) {
var DEFAULT_EMAIL_TYPE = 'other';
var emails = [];
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
'email');
if (fields && fields.length > 0) {
for (var i = 0; i < fields.length; i++) {
var emailField = fields.item(i);
// Type format: rel="http://schemas.google.com/g/2005#home"
var type = emailField.getAttribute('rel') || DEFAULT_EMAIL_TYPE;
if (type.indexOf('#') > -1) {
type = type.substr(type.indexOf('#') + 1);
}
emails.push({
'type': [type],
'value': emailField.getAttribute('address')
});
}
}
return emails;
};
// Given a google contact returns an array of ContactAddress
var parseAddress = function parseAddress(googleContact) {
var addresses = [];
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
'structuredPostalAddress');
if (fields && fields.length > 0) {
for (var i = 0; i < fields.length; i++) {
var field = fields.item(i);
var address = {};
address.streetAddress = getValueForNode(field, 'street');
address.locality = getValueForNode(field, 'city');
address.region = getValueForNode(field, 'region');
address.postalCode = getValueForNode(field, 'postcode');
address.countryName = getValueForNode(field, 'country');
addresses.push(address);
}
}
return addresses;
};
// Given a google contact this function returns an array of
// ContactField with the pones stored for that contact
var parsePhones = function parsePhones(googleContact) {
var DEFAULT_PHONE_TYPE = 'other';
var GMAIL_MAP = {
'work_fax' : 'faxOffice',
'home_fax' : 'faxHome',
'pager' : 'other',
'main' : 'other'
};
var phones = [];
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
'phoneNumber');
if (fields && fields.length > 0) {
for (var i = 0; i < fields.length; i++) {
var field = fields.item(i);
// Type format: rel="http://schemas.google.com/g/2005#home"
var type = field.getAttribute('rel') || DEFAULT_PHONE_TYPE;
if (type.indexOf('#') > -1) {
type = type.substr(type.indexOf('#') + 1);
}
phones.push({
'type': [GMAIL_MAP[type] || type],
'value': field.textContent
});
}
}
return phones;
};
// Given a contact from the mozcontact api, fetch the Google Contacts
// identifier
var getContactUid = function getContactUid(deviceContact) {
var out = -1;
var url = deviceContact.url;
if (Array.isArray(url)) {
var targetUrls = url.filter(function(aUrl) {
return Array.isArray(aUrl.type) &&
aUrl.type.indexOf('source') !== -1 &&
aUrl.value;
});
if (targetUrls[0]) {
out = resolveURI(targetUrls[0].value);
}
}
return out;
};
// From a contact URL, that we expect to be a URI
// return the google contact id if we find it on
// the uri, -1 otherwise
var resolveURI = function resolveURI(uri) {
if (uri && uri.indexOf(URN_IDENTIFIER) === 0) {
var output = uri.substr(URN_IDENTIFIER.length);
if (output && output.length > 0) {
return output;
}
}
return -1;
};
var downloadContactPicture = function downloadContactPicture(googleContact,
access_token, callbacks) {
var url = buildContactPhotoURL(googleContact, access_token);
return Rest.get(url, callbacks, {
'responseType': 'blob'
});
};
// Build the url of the photo with the access token
var buildContactPhotoURL = function contactPhotoURL(contact, access_token) {
if (photoUrls && photoUrls[contact.uid]) {
return photoUrls[contact.uid] + '?access_token=' + access_token;
}
return null;
};
var startSync = function startSync() {
//We don't sync Google contacts (yet)
};
var getServiceName = (function getServiceName() {
return 'gmail';
})();
var getAutomaticLogout = (function getAutomaticLogout() {
return true;
})();
return {
'listAllContacts': listAllContacts,
'listDeviceContacts': listDeviceContacts,
'getImporter': getImporter,
'cleanContacts': cleanContacts,
'adaptDataForShowing': adaptDataForShowing,
'adaptDataForSaving': adaptDataForSaving,
'getContactUid': getContactUid,
'downloadContactPicture': downloadContactPicture,
'startSync': startSync,
'name': getServiceName,
'automaticLogout': getAutomaticLogout,
'gContactToJson': gContactToJson
};
})();

View file

@ -0,0 +1,132 @@
/* globals LazyLoader, LiveConnector, GmailConnector, FacebookConnector,
utils, importer */
'use strict';
(function(document) {
var params = parseParams(window.location.search.substring(1));
var serviceName = getServiceName();
var allowedOrigin = location.origin;
function parseParams(paramsStr) {
var out = {};
var paramsList;
if (paramsStr.indexOf('&') != -1) {
paramsList = paramsStr.split('&');
}
else {
paramsList = [paramsStr];
}
paramsList.forEach(function(param) {
var paramsValues = param.split('=');
out[paramsValues[0]] = paramsValues[1];
});
return out;
}
function getServiceName() {
return params.service;
}
var servicesConnectors = {
'live': function(cb) {
LazyLoader.load('/shared/js/contacts/import/live/live_connector.js',
function() {
cb(LiveConnector);
});
},
'gmail': function gmailLoader(cb) {
LazyLoader.load('/shared/js/contacts/import/gmail/gmail_connector.js',
function onLoad() {
cb(GmailConnector);
});
},
'facebook': function(cb) {
var files = [
'/shared/js/fb/fb_request.js',
'/shared/js/contacts/import/facebook/fb_data.js',
'/shared/js/contacts/import/facebook/fb_utils.js',
'/shared/js/fb/fb_reader_utils.js',
'/shared/js/contacts/import/facebook/fb_contact_utils.js',
'/shared/js/contacts/import/facebook/fb_query.js',
'/shared/js/contacts/import/facebook/fb_contact.js',
'/shared/js/contacts/import/facebook/facebook_connector.js',
'/shared/pages/import/style/facebook.css'
];
if (!params.ftu) {
files.push('/facebook/js/fb_sync.js');
}
LazyLoader.load(files, function() {
cb(FacebookConnector);
});
}
};
function getServiceConnector(cb) {
if (serviceName) {
servicesConnectors[serviceName](cb);
}
else {
throw new Error('Service Connector not found !!!');
}
}
function tokenReady(access_token) {
if (document.readyState === 'complete') {
onLoad(access_token);
}
else {
window.addEventListener('load', function do_load() {
onLoad(access_token);
window.removeEventListener('load', do_load);
});
}
}
function onLoad(access_token) {
utils.listeners.add({
'#import-action': importer.ui.importAll,
'#done-search': window.Search.exitSearchMode,
'#groups-list': importer.ui.selection,
'#header': [
{
event: 'action',
handler: importer.ui.end
}
],
'#search-start': [
{
event: 'click',
handler: window.Search.enterSearchMode
}
]
});
// This is done through onclick as it is going to be changed it dynamically
document.querySelector('#select-all').onclick = importer.ui.selectAll;
document.querySelector('#deselect-all').onclick =
importer.ui.unSelectAll;
importer.ui.init();
getServiceConnector(function(connector) {
importer.start(access_token, connector, allowedOrigin);
});
}
window.addEventListener('message', function getAccessToken(e) {
if (e.origin !== allowedOrigin) {
return;
}
window.removeEventListener('message', getAccessToken);
if (e.data.type === 'token') {
tokenReady(e.data.data);
}
});
parent.postMessage({
type: 'messaging_ready',
data: ''
}, allowedOrigin);
})(document);

View file

@ -0,0 +1,95 @@
'use strict';
/* globals Promise */
/* exported ImportStatusData */
/*
* Module for communication of import status data between FTU and Comms Apps via
* Datastore.
*
*/
if (!window.ImportStatusData) {
var ImportStatusData = (function() {
var store = null;
var DS_NAME = 'Import_Status_Data';
var datastoreLoading = false;
var datastoreLoaded = false;
var DS_LOADED_EVENT = 'datastore_loaded';
var getDatastore = function getDatastore() {
return new Promise(function(resolve, reject) {
if (!navigator.getDataStores) {
reject({
name: 'DatastoreNotEnabled'
});
return;
}
if (datastoreLoaded) {
resolve(store);
return;
}
if (datastoreLoading) {
document.addEventListener(DS_LOADED_EVENT, function loadedHandler() {
document.removeEventListener(DS_LOADED_EVENT, loadedHandler);
resolve(store);
});
}
datastoreLoading = true;
navigator.getDataStores(DS_NAME).then(function(stores) {
store = stores[0];
}, reject).then(function() {
datastoreLoading = false;
datastoreLoaded = true;
document.dispatchEvent(new CustomEvent(DS_LOADED_EVENT));
resolve(store);
}, reject);
});
};
var put = function put(key, obj) {
return new Promise(function(resolve, reject) {
getDatastore().then(function success(store) {
store.put(obj, key).then(resolve, reject);
}, reject);
});
};
var remove = function remove(key) {
return new Promise(function(resolve, reject) {
getDatastore().then(function success(store) {
store.remove(key).then(resolve, reject);
}, reject);
});
};
var get = function get(key) {
return new Promise(function(resolve, reject) {
getDatastore().then(function success(store) {
store.get(key).then(resolve, reject);
}, reject);
});
};
var clear = function clear() {
return new Promise(function(resolve, reject) {
getDatastore().then(function success(store) {
store.clear().then(resolve, reject);
}, reject);
});
};
return {
put: put,
remove: remove,
get: get,
clear: clear
};
})();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,210 @@
'use strict';
/* global Rest */
/* global utils */
if (!window.LiveConnector) {
window.LiveConnector = (function() {
var LIVE_ENDPOINT = 'https://apis.live.net/v5.0/';
var CONTACTS_RESOURCE = 'me/contacts';
var PICTURE_RESOURCE = '/picture';
var LIVE_CATEGORY = 'live';
var itemsTypeMap = {
'personal': 'personal',
'mobile': 'mobile',
'business': 'work',
'other': 'other',
'preferred': 'personal'
};
function fillAddress(addrType, addrValue) {
var out = {
type: [addrType],
streetAddress: addrValue.street || '',
locality: addrValue.city || '',
region: addrValue.state || '',
countryName: addrValue.region || '',
postalCode: addrValue.postal_code || ''
};
return out;
}
function getURI(liveContact) {
return 'urn:uuid:' + (liveContact.user_id || liveContact.id);
}
function resolveURI(uri) {
var components = uri.split(':');
// The third element is the user id of the live contact
return components[2];
}
function LiveConnector() {
}
LiveConnector.prototype = {
listAllContacts: function(access_token, callbacks) {
var uriElements = [LIVE_ENDPOINT, CONTACTS_RESOURCE, '?',
'access_token', '=', access_token];
return Rest.get(uriElements.join(''), callbacks);
},
listDeviceContacts: function(callbacks) {
var filterOptions = {
filterValue: LIVE_CATEGORY,
filterOp: 'contains',
filterBy: ['category']
};
var req = navigator.mozContacts.find(filterOptions);
req.onsuccess = function() {
callbacks.success(req.result);
};
req.onerror = function() {
callbacks.error(req.error);
};
},
getImporter: function(contactsList, access_token) {
return new window.ContactsImporter(contactsList, access_token, this);
},
cleanContacts: function(contactsList, mode, cb) {
// Just a placeholder for the moment
var cleaner = new window.ContactsCleaner(contactsList);
window.setTimeout(cleaner.start, 0);
cb(cleaner);
},
adaptDataForShowing: function(source) {
var out = source;
out.uid = source.user_id || source.id;
out.givenName = [source.first_name || ''];
out.familyName = [source.last_name || ''];
out.email1 = source.emails.account ||
source.emails.preferred ||
source.emails.personal ||
'';
out.contactPictureUri = [LIVE_ENDPOINT, out.uid,
PICTURE_RESOURCE, '?type=medium'].join('');
return out;
},
adaptDataForSaving: function live2MozContact(liveContact) {
var out = {
givenName: [liveContact.first_name || ''],
familyName: [liveContact.last_name || ''],
name: [liveContact.name || ''],
tel: [],
email: [],
adr: [],
photo: liveContact.photo,
category: [LIVE_CATEGORY],
url: [{
type: ['source'],
value: getURI(liveContact)
}]
};
// Months in JS goes from 0 to 11 :(
var bmonth = liveContact.birth_month - 1;
var bday = liveContact.birth_day;
if (bmonth > -1 && bday) {
var birthdate = out.bday = new Date();
birthdate.setUTCDate(bday);
birthdate.setUTCMonth(bmonth, bday);
birthdate.setUTCFullYear(utils.misc.FLAG_YEAR_IGNORED);
}
var liveEmails = liveContact.emails || {};
var alreadyAddedEmails = {};
Object.keys(liveEmails).forEach(function(emailType) {
var emailValue = liveEmails[emailType];
var present = (typeof alreadyAddedEmails[emailValue] === 'boolean');
if (emailValue && !present) {
out.email.push({
type: [itemsTypeMap[emailType]],
value: emailValue
});
alreadyAddedEmails[emailValue] = true;
}
});
var livePhones = liveContact.phones || {};
Object.keys(livePhones).forEach(function(phoneType) {
var phoneValue = livePhones[phoneType];
if (phoneValue) {
out.tel.push({
type: [itemsTypeMap[phoneType]],
value: phoneValue
});
}
});
var liveAddresses = liveContact.addresses || {};
Object.keys(liveAddresses).forEach(function(addrType) {
var addrValue = liveAddresses[addrType];
if (addrValue) {
out.adr.push(fillAddress(itemsTypeMap[addrType], addrValue));
}
});
return out;
},
// It would allow to know the UID of a service contact already imported
// on the device. That is needed by the generic importer, for live
// a dummy implementation as we are currently not supporting updates
getContactUid: function(deviceContact) {
var out = '-1';
var url = deviceContact.url;
if (Array.isArray(url)) {
var targetUrls = url.filter(function(aUrl) {
return Array.isArray(aUrl.type) &&
aUrl.type.indexOf('source') !== -1;
});
if (targetUrls[0]) {
out = resolveURI(targetUrls[0].value);
}
}
return out;
},
get name() {
return 'live';
},
get automaticLogout() {
return true;
},
downloadContactPicture: function(contact, access_token, callbacks) {
if (contact.user_id) {
var uriElements = [LIVE_ENDPOINT, contact.user_id, PICTURE_RESOURCE,
'?', 'access_token', '=', access_token];
return Rest.get(uriElements.join(''), callbacks, {
responseType: 'blob'
});
}
else {
callbacks.success(null);
}
},
startSync: function() {
// Sync not supported
}
};
return new LiveConnector();
})();
}

View file

@ -0,0 +1,102 @@
'use strict';
/* global Promise */
var utils = window.utils || {};
if (typeof utils.config === 'undefined') {
(function() {
var configs = {};
var initializing = {};
var initialized = {};
var EVENT_INITIALIZED = 'config_initialized';
utils.config = utils.config || {};
utils.config.reset = function() {
configs = {};
initializing = {};
initialized = {};
};
function onLoad(xhr, file, resolveCb) {
configs[file] = xhr.response;
delete initializing[file];
initialized[file] = true;
document.dispatchEvent(new CustomEvent(EVENT_INITIALIZED, {
detail: {
file: file,
success: true
}
}));
resolveCb(configs[file]);
}
utils.config.load = function(file) {
return new Promise(function(resolve, reject) {
var isInitialized = initialized[file];
if (isInitialized === true) {
resolve(configs[file]);
return;
}
var isInitializing = initializing[file];
if (isInitializing) {
var handler = function(expectedFile, e) {
document.removeEventListener(EVENT_INITIALIZED, handler);
var file = e.detail.file;
if (expectedFile !== file) {
return;
}
if (e.detail.success) {
resolve(configs[file]);
}
else {
reject();
}
}.bind(null, file);
document.addEventListener(EVENT_INITIALIZED, handler);
return;
}
initializing[file] = true;
var xhr = new XMLHttpRequest();
xhr.open('GET', file, true);
xhr.responseType = 'json';
xhr.onload = function() {
var resolvedCb = resolve;
if (xhr.status !== 200) {
resolvedCb = reject;
}
onLoad(xhr, file, resolvedCb);
};
xhr.onerror = function() {
document.dispatchEvent(new CustomEvent(EVENT_INITIALIZED, {
detail: {
file: file,
success: false
}
}));
delete initializing[file];
reject(xhr.error);
};
// If there is any exception we consider the file does not exist
try {
xhr.send(null);
}
catch(e) {
console.error('Error while loading config file: ', e.message);
onLoad({ status: 404 }, file, reject);
}
});
};
})();
}

View file

@ -0,0 +1,188 @@
/* global VCardReader, ConfirmDialog, contacts, LazyLoader, Loader */
/* global Matcher */
'use strict';
var utils = window.utils || {};
/**
* Loads the file passed on the activity, reads the text from it, and
* import the info
* Passes the id of last imported contact in the callback
*/
utils.importFromVcard = function(file, callback) {
var MERGE_DEPENDENCIES = [
'/shared/js/simple_phone_matcher.js',
'/shared/js/contacts/merger_adapter.js',
'/shared/js/contacts/contacts_merger.js',
'/shared/js/contacts/contacts_matcher.js'
];
Loader.utility('Overlay', function() {
LazyLoader.load(MERGE_DEPENDENCIES, _loaded);
});
function _loaded() {
var importedContacts = 0;
var cancelled = false;
var importer = null;
var text = null;
var progress = utils.overlay.show(
'memoryCardContacts-reading',
'activityBar'
);
utils.overlay.showMenu();
utils.overlay.oncancel = function oncancel() {
cancelled = true;
if (importer) {
importer.finish();
} else {
utils.overlay.hide();
}
};
readVCard(function(vcardText) {
if (vcardText) {
processTextFromFile(vcardText);
} else {
callback();
}
});
function readVCard(cb) {
var reader;
if (typeof file === 'string') {
cb(file);
} else {
reader = new FileReader();
reader.onloadend = function() {
text = reader.result;
if (text) {
cb(text);
}
};
try {
reader.readAsText(file);
}
catch (ex) {
console.error('Error reading the file ' + ex.message);
import_error();
cb();
}
}
}
function processTextFromFile(textFromFile) {
if (cancelled) {
return;
}
var reader = new VCardReader(textFromFile);
var match = textFromFile && textFromFile.match(/END:VCARD/g);
var cursor = reader.getAll();
var numContacts = match ? match.length : 0;
var numDupsMerged = 0;
var firstContact = null;
var _doContinue = function(savedContact) {
if (savedContact) {
if (!firstContact) {
firstContact = savedContact;
}
imported_contact();
}
if (!cancelled) {
cursor.continue();
}
};
cursor.onsuccess = function(evt) {
if (evt.target.result) {
var contact = evt.target.result;
Matcher.match(contact, 'passive', {
onmatch: function(matches) {
var callbacks = {
success: function(mergedContact) {
numDupsMerged++;
_doContinue(mergedContact);
},
error: _doContinue
};
contacts.adaptAndMerge(contact, matches, callbacks);
},
onmismatch: function() {
var req = navigator.mozContacts.save(contact);
req.onsuccess = function() {
_doContinue(contact);
};
}
});
// No more contacts
} else {
utils.overlay.hide();
if (!cancelled) {
var msgImported = {
id: 'memoryCardContacts-imported3',
args: {n: importedContacts}
};
var msgDupsMerged = numDupsMerged ? {
id: 'contactsMerged',
args: {numDups: numDupsMerged}
} : null;
utils.status.show(msgImported, msgDupsMerged);
}
if (importedContacts > 0) {
callback(importedContacts, firstContact.id);
} else {
callback(importedContacts);
}
}
};
import_read(numContacts);
}
function import_read(n) {
progress.setClass('progressBar');
progress.setHeaderMsg('memoryCardContacts-importing');
progress.setTotal(n);
}
function imported_contact() {
importedContacts++;
if (!cancelled) {
progress.update();
}
}
function import_error(e) {
console.error('Error importing from vcard: ' + e.message);
// Showing error message allowing user to retry
var cancel = {
title: 'cancel',
callback: function() {
ConfirmDialog.hide();
}
};
var retry = {
title: 'retry',
isRecommend: true,
callback: function() {
ConfirmDialog.hide();
// And now the action is reproduced one more time
processTextFromFile(text);
}
};
ConfirmDialog.show(null, 'memoryCardContacts-error',
cancel, retry);
utils.overlay.hide();
}
}
};

View file

@ -0,0 +1,237 @@
/*jshint loopfunc: true */
/* global mozContact, LazyLoader, utils, contacts */
/* global Matcher */
/* exported SimContactsImporter */
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/**
* Imports contacts stored in the SIM card and saves them into
* navigator.mozContacts. Three steps => three callback as arguments:
* - onread: SIM card has been read properly;
* - onimport: A Contact has been imported
* - onfinish: contacts have been saved into navigator.mozContacts;
* - onerror: SIM card is empty or could not be read.
*/
function SimContactsImporter(targetIcc) {
if (targetIcc === null) {
throw new Error('We need an icc to continue with this operation');
}
var pointer = 0;
var CHUNK_SIZE = 5;
var numResponses = 0;
var self = this;
var mustFinish = false;
var loadedMatch = false;
var DEFAULT_TEL_TYPE = 'other';
var icc = targetIcc;
var iccId = icc.iccInfo && icc.iccInfo.iccid;
var numDupsMerged = 0;
function generateIccContactUrl(contactid) {
var urlValue = 'urn:' + 'uuid:' + (iccId || 'iccId') + '-' + contactid;
return [{
type: ['source', 'sim'],
value: urlValue
}];
}
function notifyFinish() {
if (typeof self.onfinish === 'function') {
window.setTimeout(self.onfinish, 0, numDupsMerged, iccId);
}
}
function notifyImported() {
if (typeof self.onimported === 'function') {
window.setTimeout(self.onimported, 0);
}
}
function continueCb() {
numResponses++;
pointer++;
notifyImported();
if (pointer < self.items.length && numResponses === CHUNK_SIZE) {
numResponses = 0;
mustFinish ? notifyFinish() : importSlice(pointer);
}
else if (pointer >= self.items.length) {
notifyFinish();
}
}
function startMigration() {
if (!mustFinish && Array.isArray(self.items) && self.items.length > 0) {
importSlice(0);
}
else {
notifyFinish();
}
}
function onContactsReadyForImport() {
if (typeof self.onread === 'function') {
// This way the total number can be known by the caller
self.onread(self.items.length);
}
if (loadedMatch) {
startMigration();
}
else {
document.addEventListener('matchLoaded', function mloaded() {
document.removeEventListener('matchLoaded', mloaded);
startMigration();
});
}
}
this.start = function() {
numDupsMerged = 0;
if (mustFinish) {
notifyFinish();
return;
}
LazyLoader.load([
'/shared/js/simple_phone_matcher.js',
'/shared/js/contacts/contacts_matcher.js',
'/shared/js/contacts/contacts_merger.js',
'/shared/js/contacts/utilities/image_thumbnail.js',
'/shared/js/contacts/merger_adapter.js'
], function loaded() {
loadedMatch = true;
document.dispatchEvent(new CustomEvent('matchLoaded'));
});
var requestAdn, requestSdn;
// request contacts with readContacts() -- valid types are:
// 'adn': Abbreviated Dialing Numbers
// 'fdn': Fixed Dialing Numbers
// 'sdn': Service Dialing Numbers
if (icc && icc.readContacts) {
requestAdn = icc.readContacts('adn');
} else {
throw new Error('Not able to obtain a SIM import function from platform');
}
requestAdn.onsuccess = function onsuccess() {
if (mustFinish) {
notifyFinish();
return;
}
self.items = requestAdn.result || [];
requestSdn = icc.readContacts('sdn');
requestSdn.onsuccess = function onsuccess() {
if (mustFinish) {
notifyFinish();
return;
}
if (Array.isArray(requestSdn.result)) {
self.items = self.items.concat(requestSdn.result);
}
onContactsReadyForImport();
};
requestSdn.onerror = function error() {
if (mustFinish) {
notifyFinish();
return;
}
console.warn('Could not read SDN Contacts from SIM Card', error.name);
onContactsReadyForImport();
};
};
requestAdn.onerror = function error() {
if (typeof self.onerror === 'function') {
self.onerror(requestAdn.error);
}
};
};
this.finish = function() {
mustFinish = true;
};
/**
* store mozContact elements -- each returned mozContact has two properties:
* .name : [ string ]
* .tel : [{ number: string, type: string }]
* The 'name' property is only related to the mozContact element itself --
* let's use it as the default 'givenName' value.
*/
function importSlice(from) {
for (var i = from; i < from + CHUNK_SIZE && i < self.items.length; i++) {
var item = self.items[i];
var parsedName = utils.misc.parseName(item.name[0]);
item.givenName = [parsedName.givenName];
item.familyName = [parsedName.familyName];
if (Array.isArray(item.tel)) {
var telItems = [];
for (var j = 0; j < item.tel.length; j++) {
var aTel = item.tel[j];
// Filtering out empty values
if (aTel.value && aTel.value.trim()) {
aTel.type = [DEFAULT_TEL_TYPE];
telItems.push(aTel);
}
}
item.tel = telItems;
}
item.category = ['sim'];
item.url = generateIccContactUrl(item.id);
// Item is presumably a mozContact but for some reason if
// we don't create a new mozContact sometimes the save call fails
var contact = item;
var cbs = {
onmatch: function(results) {
var mergeCbs = {
success: function() {
numDupsMerged++;
continueCb();
},
error: function(e) {
window.console.error('Error while merging: ', e.name);
continueCb();
}
};
contacts.adaptAndMerge(this, results, mergeCbs);
}.bind(contact),
onmismatch: function() {
saveContact(this);
}.bind(contact)
};
Matcher.match(item, 'passive', cbs);
}
} // importSlice
function saveContact(contact) {
var req = window.navigator.mozContacts.save(new mozContact(contact));
req.onsuccess = function saveSuccess() {
continueCb();
};
req.onerror = function saveError() {
console.error('SIM Import: Error importing ', contact.id,
req.error.name);
continueCb();
};
}
}

View file

@ -0,0 +1,191 @@
/* global mozContact, oauthflow, ImportStatusData */
'use strict';
var utils = window.utils || {};
// Scale ratio for different devices
var SCALE_RATIO = window.devicePixelRatio || 1;
var LAST_IMPORT_TIMESTAMP_SUFFIX = '_last_import_timestamp';
function scale(size) {
return Math.round(SCALE_RATIO * size);
}
if (!utils.misc) {
utils.misc = {};
utils.misc.toMozContact = function(contact) {
var outContact = contact;
if (!(contact instanceof mozContact)) {
outContact = new mozContact(contact);
outContact.id = contact.id || outContact.id;
}
return outContact;
};
// This year indicates that the year can be ignored
const FLAG_YEAR_IGNORED = 9996;
utils.misc.FLAG_YEAR_IGNORED = FLAG_YEAR_IGNORED;
utils.misc.formatDate = function(date, customFormat) {
var dateString = null;
try {
var format = customFormat || {
month: 'short',
day: 'numeric'
};
var offset = date.getTimezoneOffset() * 60 * 1000;
var normalizedDate = new Date(date.getTime() + offset);
var year = normalizedDate.getFullYear();
if (year !== FLAG_YEAR_IGNORED) {
format.year = 'numeric';
}
var f = new Intl.DateTimeFormat(navigator.languages, format);
dateString = f.format(normalizedDate);
} catch (err) {
console.error('Error parsing date: ', err);
throw err;
}
return dateString;
};
// Parses a name to obtain a givenName and a familyName
// This is useful while importing contacts from SIM card
utils.misc.parseName = function(nameString) {
// Minimum length to consider a token as significative
var MIN_LENGHT_SIGNIFICATIVE = 3;
// Minimum length of a token to consider it a familyName
var MIN_LENGHT_FN = 2;
var out = {
givenName: '',
familyName: ''
};
if (!nameString) {
return out;
}
var str = nameString.trim();
if (!str) {
return out;
}
function startsWithUpper(str) {
var firstLetter = str.charAt(0);
var capFirstLetter = firstLetter.toLocaleUpperCase();
return (capFirstLetter === firstLetter);
}
// First step of the algorithm is to split the name in its tokens
var tokens = nameString.split(/\s+/);
var significativeTokens = {};
for (var j = 0; j < tokens.length; j++) {
var token = tokens[j];
// A significative token os one with that min length and which starts with
// uppercase letter
if (token.length > MIN_LENGHT_SIGNIFICATIVE || startsWithUpper(token)) {
significativeTokens[token] = true;
}
}
var totalTokens = tokens.length;
var lastToken = totalTokens - 1;
var rolePrevToken;
var remainingTokens = Object.keys(significativeTokens).length;
var outGivenNames = [], outFamilyNames = [];
// We need to keep the totals in a different variable as a familyName
// can be composed by more than one token
var numFns = 0;
while (lastToken >= 0) {
if (significativeTokens[tokens[lastToken]]) {
remainingTokens--;
}
var currToken = tokens[lastToken],
nextToken = tokens[lastToken + 1],
prevToken = tokens[lastToken - 1];
// The last element is taken as part of the family name
if (!nextToken && prevToken) {
outFamilyNames.push(currToken);
numFns++;
rolePrevToken = 'FN';
}
else if (!prevToken) {
outGivenNames.push(currToken);
rolePrevToken = 'GN';
}
else if (nextToken) {
if (!significativeTokens[currToken]) {
if (rolePrevToken === 'FN') {
// Here the number of fns is not incremented as this is a non
// significative token
outFamilyNames.push(currToken);
}
else {
outGivenNames.push(currToken);
}
}
else if (currToken.length < MIN_LENGHT_FN) {
outGivenNames.push(currToken);
rolePrevToken = 'GN';
}
else {
// Number of familyNames will be two at most
// Preference is given to the givenName over familyName
// We prefer two givenNames instead of two family Names
if (remainingTokens >= 2 && numFns < 2) {
outFamilyNames.push(currToken);
numFns++;
rolePrevToken = 'FN';
}
else {
outGivenNames.push(currToken);
rolePrevToken = 'GN';
}
}
}
lastToken--;
}
out.givenName = outGivenNames.reverse().join(' ').trim();
out.familyName = outFamilyNames.reverse().join(' ').trim();
return out;
};
utils.misc.getPreferredPictureBox = function() {
var imgThumbSize = oauthflow.params.facebook.imgThumbSize;
var out = {
width: scale(imgThumbSize)
};
out.height = out.width;
return out;
};
utils.misc.getPreferredPictureDetail = function() {
var imgDetailWidth = oauthflow.params.facebook.imgDetailWidth;
return scale(imgDetailWidth);
};
utils.misc.setTimestamp = function(type, callback) {
ImportStatusData.put(type + LAST_IMPORT_TIMESTAMP_SUFFIX, Date.now())
.then(callback);
};
utils.misc.getTimestamp = function(type, callback) {
ImportStatusData.get(type + LAST_IMPORT_TIMESTAMP_SUFFIX)
.then(callback);
};
}

View file

@ -0,0 +1,196 @@
'use strict';
var utils = window.utils || {};
(function() {
utils.overlay = {};
var overlay = document.querySelector('#loading-overlay'),
statusContainer = overlay.querySelector('p[role="status"]'),
progressActivity = document.querySelector('#progress-activity'),
progressTitle = document.querySelector('#progress-title'),
progressElement = document.querySelector('#progress-element'),
progressMsg = document.querySelector('#progress-msg'),
menu = document.querySelector('#loading-overlay menu'),
cancelButton = document.querySelector('#cancel-overlay');
// Constructor for the progress element
function ProgressBar(pMsgId, pClass) {
var counter = 0;
var total = 0;
var progressTextId = pMsgId || 'genericProgress';
var clazz = pClass;
progressElement.setAttribute('value', 0);
function showMessage() {
var attributes = progressTextId == 'infiniteProgress' ?
{ n: counter } :
{ current: counter, total: total };
document.l10n.setAttributes(progressMsg,
progressTextId,
attributes);
}
/**
* Updates progress bar and message. It takes an optional `value` parameter
* to overwrite the internal counter with this value.
* @param {Number} value Overrides internal counter.
*/
this.update = function(value) {
if (value && value <= total && value >= counter) {
counter = value;
} else {
counter++;
}
progressElement.setAttribute('value',
((counter * 100) / total).toFixed()); // Percent fraction is not needed.
showMessage();
};
this.setTotal = function(ptotal) {
total = ptotal;
};
this.setClass = function(clazzName) {
setClass(clazzName);
clazz = clazzName;
// To refresh the message according to the new clazzName
if (clazzName === 'activityBar' || clazzName === 'spinner') {
progressMsg.removeAttribute('data-l10n-id');
}
};
this.setHeaderMsg = function(headerMsgId) {
progressTitle.setAttribute('data-l10n-id', headerMsgId);
};
} // ProgressBar
utils.overlay.isAnimationPlaying = false;
utils.overlay.isShown = false;
utils.overlay._show = function _show(messageId, progressClass, textId) {
progressActivity.classList.remove('hide');
if (typeof messageId === 'string') {
progressTitle.setAttribute('data-l10n-id', messageId);
} else {
document.l10n.setAttributes(progressTitle,
messageId.id,
messageId.args);
}
progressMsg.textContent = null;
progressMsg.removeAttribute('data-l10n-id');
if (utils.overlay.isShown) {
return;
}
overlay.classList.remove('hide');
overlay.classList.remove('fade-out');
overlay.classList.add('fade-in');
utils.overlay.isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('loadingoverlayshowing'));
overlay.addEventListener('animationend', function ov_onFadeIn(ev) {
utils.overlay.isAnimationPlaying = false;
overlay.removeEventListener('animationend', ov_onFadeIn);
overlay.classList.remove('no-opacity');
utils.overlay.isShown = true;
});
};
utils.overlay.show = function show(messageId, progressClass, textId) {
var out;
// In the case of an spinner this object will not be really used
out = new ProgressBar(textId, progressClass);
setClass(progressClass);
utils.overlay.hideMenu(); // By default
if (!utils.overlay.isAnimationPlaying) {
utils.overlay._show(messageId, progressClass, textId);
return out;
}
overlay.addEventListener('animationend',
function ov_showWhenFinished(ev) {
overlay.removeEventListener('animationend', ov_showWhenFinished);
utils.overlay._show(messageId, progressClass, textId);
}
);
return out;
};
utils.overlay.showMenu = function showMenu() {
menu.classList.add('showed');
};
utils.overlay.hideMenu = function hideMenu() {
menu.classList.remove('showed');
};
Object.defineProperty(utils.overlay, 'oncancel', {
set: function(cancelCb) {
if (typeof cancelCb === 'function') {
cancelButton.disabled = false;
cancelButton.onclick = function on_cancel(e) {
delete cancelButton.onclick;
cancelCb();
return false;
};
}
}
});
function setAsProgress() {
statusContainer.classList.remove('loading-icon');
progressElement.setAttribute('max', '100');
progressElement.setAttribute('value', '0');
}
function setClass(clazzName) {
switch (clazzName) {
case 'spinner':
progressElement.classList.remove('pack-activity');
statusContainer.classList.add('loading-icon');
progressElement.removeAttribute('max');
progressElement.removeAttribute('value');
break;
case 'activityBar':
case 'progressActivity':
progressElement.classList.add('pack-activity');
setAsProgress();
break;
case 'progressBar':
progressElement.classList.remove('pack-activity');
setAsProgress();
break;
}
}
utils.overlay._hide = function ov__hide() {
if (!utils.overlay.isShown) {
return;
}
overlay.classList.remove('fade-in');
overlay.classList.add('fade-out');
utils.overlay.isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('loadingoverlayhiding'));
overlay.addEventListener('animationend', function ov_onFadeOut(ev) {
utils.overlay.isAnimationPlaying = false;
overlay.removeEventListener('animationend', ov_onFadeOut);
progressActivity.classList.add('hide');
overlay.classList.add('no-opacity');
overlay.classList.add('hide');
utils.overlay.isShown = false;
});
};
utils.overlay.hide = function ov_hide() {
if (!utils.overlay.isAnimationPlaying) {
utils.overlay._hide();
return;
}
overlay.addEventListener('animationend',
function ov_hideWhenFinished(ev) {
overlay.removeEventListener('animationend', ov_hideWhenFinished);
utils.overlay._hide();
}
);
};
})();

View file

@ -0,0 +1,217 @@
'use strict';
var utils = window.utils || {};
if (!utils.sdcard) {
var SdCard = utils.sdcard = {
NOT_INITIALIZED: 0,
NOT_AVAILABLE: 1,
AVAILABLE: 2,
SHARED: 3,
observers: {}
};
SdCard.status = SdCard.NOT_INITIALIZED;
SdCard.deviceStorages = navigator.getDeviceStorages('sdcard');
SdCard.deviceStorage = Array.isArray(SdCard.deviceStorages) &&
SdCard.deviceStorages[0];
SdCard._toStatus = function toStatus(state) {
switch (state) {
case 'available':
SdCard.status = SdCard.AVAILABLE;
break;
// NOTE: The 'SHARED' state is equivalent to NOT_AVAILABLE, since we
// could have inconsistencies if we allow changing the sdcard content
// meanwhile exporting/importing.
// It's needed to have it's own state solely for showing the proper
// message when needed
case 'shared':
SdCard.status = SdCard.SHARED;
break;
case 'unavailable':
case 'deleted':
SdCard.status = SdCard.NOT_AVAILABLE;
break;
}
};
SdCard.updateStorageState = function sd_updateStorageState(state) {
SdCard._toStatus(state);
Object.keys(this.observers).forEach(function onObserver(name) {
if (typeof(SdCard.observers[name]) === 'function') {
SdCard.observers[name].call(null, state);
}
});
};
if (SdCard.deviceStorage) {
SdCard.deviceStorage
.addEventListener('change', function sd_deviceStorageChangeHandler(e) {
SdCard.updateStorageState(e.reason);
});
if (SdCard.status === SdCard.NOT_INITIALIZED) {
SdCard.deviceStorage.available().onsuccess = (function(e) {
SdCard.updateStorageState(e.target.result);
});
}
}
/**
* Check whether there is a SD card inserted in the device.
* @return {Boolean} true if sdcard available, false otherwise.
*/
SdCard.checkStorageCard = function sd_checkStorageCard() {
return SdCard.status === SdCard.AVAILABLE;
};
SdCard.getStatus = function(cb) {
if (!SdCard.deviceStorage) {
SdCard._toStatus('unavailable');
cb(SdCard.status);
return;
}
var req = SdCard.deviceStorage.available();
req.onsuccess = function() {
SdCard._toStatus(req.result);
cb(SdCard.status);
};
req.onerror = function() {
console.error('Error while determining SD Status', req.error.name);
cb(SdCard.status);
};
};
/**
* Retrieves files that adjust to the kind specified in the
* parameters from the SD card inserted in the device.
*
* @param {String[]} mimes Possible mime types of the files to be found.
* @param {String[]} exts Possible file extensions of the files to be found.
* @param {Function} cb Callback.
*/
SdCard.retrieveFiles = function retrieveFilesContent(mimes, exts, cb) {
var fileArray = [];
var forbiddenDirectories = ['.Trash'];
// getDeviceStorages('sdcard') can return more than one storage unit
// in different order, depends on the default media storage setting,
// so we need to iterate though them to be sure we checked the proper
// SD Card
var storages = navigator.getDeviceStorages('sdcard');
var numberOfStorages = storages.length;
var currentStorage = 0;
var cursorOnError = function() {
cb(this.error.name);
};
// RegExp that checks if a string contains any of the extensions.
var reExt = new RegExp('.(' + exts.join('|') + ')$');
var cursorOnSuccess = function(e) {
var file = e.target.result;
// If we don't have any more files on the storage...
if (!file) {
// ... but we have more storages...
if (++currentStorage < numberOfStorages) {
// ... check the next one
cursor = storages[currentStorage].enumerate();
cursor.onsuccess = cursorOnSuccess;
cursor.onerror = cursorOnError;
} else {
cb(null, fileArray);
}
} else {
if ((mimes.indexOf(file.type) === -1) && !reExt.test(file.name)) {
cursor.continue();
} else {
if (forbiddenDirectories.indexOf(file.name) === -1) {
fileArray.push(file);
cursor.continue();
}
}
}
};
var cursor = storages[currentStorage].enumerate();
cursor.onsuccess = cursorOnSuccess;
cursor.onerror = cursorOnError;
};
/**
* Incrementally extracts vcard text from a file.
*
* @param {Object} file File Object.
* @param {Function} cb Function to call for each chunk of vcards retrieved
* from the file.
*/
SdCard.getTextFromFile = function(file, cb) {
if (!file) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
const END_VCARD = 'end:vcard';
const CHUNK_SIZE = 1 * 1024 * 1024; // 1Mb
var fileReader = new FileReader();
var result = 0;
(function readChunk(offset, buffer) {
if (offset >= file.size) {
resolve(result);
return;
}
var chunk = file.slice(offset, offset + CHUNK_SIZE);
offset += CHUNK_SIZE;
fileReader.readAsText(chunk);
fileReader.onload = function() {
buffer += fileReader.result;
if (!buffer.match(/end:vcard/gi)) {
readChunk(offset, buffer);
return;
}
var lastContactInChunk =
buffer.toLowerCase().lastIndexOf(END_VCARD) + END_VCARD.length;
cb(buffer.substr(0, lastContactInChunk)).then(partialResult => {
result += partialResult;
readChunk(offset, buffer.substr(lastContactInChunk + 1));
}).catch(error => {
console.warn(error);
});
};
})(0 /* initial offset */, '' /* initial empty buffer */);
});
};
// Subscribe a callback for sdcard changes. A name is needed
// to refer to this observer.
// @param {String} identifier for the observer
// @param {Function} the callback to be invoqued on a change
// @param {Boolean} if the callback exists, override it
SdCard.subscribeToChanges = function(name, func, force) {
if (this.observers[name] !== undefined && !force) {
return false;
}
this.observers[name] = func;
return true;
};
// Remove the subscription to listen to changes for the specific
// identifier
// @param {String} the identifier
SdCard.unsubscribeToChanges = function(name) {
if (this.observers[name]) {
delete this.observers[name];
return true;
} else {
return false;
}
};
}

View file

@ -0,0 +1,95 @@
/* global LazyLoader */
'use strict';
var utils = window.utils || {};
utils.status = (function() {
var STATUS_TIME = 2000;
var statusMsg = document.querySelector('#statusMsg');
var hidingTimeout;
var transitionEndTimeout;
// Additional paragraph just in case the status msg has more than one line
var additionalLine;
var hideAnimationDone = function() {
statusMsg.removeEventListener('transitionend', hideAnimationDone);
statusMsg.classList.add('hidden');
};
var showAnimationDone = function() {
if (transitionEndTimeout) {
clearTimeout(transitionEndTimeout);
}
statusMsg.removeEventListener('transitionend', showAnimationDone);
hidingTimeout = setTimeout(utils.status.hide, STATUS_TIME);
};
var hideStatus = function() {
statusMsg.addEventListener('transitionend', hideAnimationDone);
statusMsg.classList.remove('opening');
statusMsg.classList.remove('bannerStart');
statusMsg.querySelector('p').removeAttribute('data-l10n-id');
if (additionalLine) {
statusMsg.removeChild(additionalLine);
additionalLine = null;
}
};
var setL10nAttributes = function(node, l10n) {
if (l10n && l10n.id) {
navigator.mozL10n.setAttributes(node, l10n.id, l10n.args);
return true;
} else {
console.error('Status arguments must be objects');
return false;
}
};
/**
* Fills the DOM with the proper content and makes it visible
* As parameters, it consums objects of the form
* {
* id: id,
* args: args
* }
* @param mainMessage: the message to display
* @param extra: an optional extra line for the message
*/
var showStatus = function(mainMessage, extra) {
// clean listeners in case of previous race conditions
statusMsg.removeEventListener('transitionend', showAnimationDone);
statusMsg.removeEventListener('transitionend', hideAnimationDone);
LazyLoader.load([statusMsg], function _loaded() {
// if parameters correct keep going
if (!setL10nAttributes(statusMsg.querySelector('p'), mainMessage)) {
return;
}
// check for additional messages
if (extra) {
additionalLine = document.createElement('p');
statusMsg.appendChild(additionalLine);
setL10nAttributes(additionalLine, extra);
}
// If showing already, we increase the time after the change
if (statusMsg.classList.contains('opening')) {
clearTimeout(hidingTimeout);
hidingTimeout = setTimeout(hideStatus, STATUS_TIME);
return;
}
statusMsg.classList.remove('hidden');
statusMsg.addEventListener('transitionend', showAnimationDone);
setTimeout(function displaying() {
statusMsg.classList.add('opening');
statusMsg.classList.add('bannerStart');
transitionEndTimeout = setTimeout(showAnimationDone, STATUS_TIME);
}, 10); // Give the opportunity to paint the UI component
});
};
return {
show: showStatus,
hide: hideStatus
};
})();

View file

@ -0,0 +1,885 @@
/* global contacts, LazyLoader, utils, Rest, MimeMapper */
/* global Matcher */
/* exported VCFReader */
'use strict';
function parseDataUri(str) {
var re = /^data:(.+);(charset=([^;]+))?;?base64,(.*)$/;
var matches = re.exec(str);
if (matches) {
return {
mime: matches[1],
charset: matches[3],
value: matches[4]
};
}
return null;
}
function ensureMimeType(type) {
type = type.toLowerCase();
if (!MimeMapper.isSupportedType(type)) {
type = MimeMapper.guessTypeFromExtension(type) || type;
}
return type || '';
}
/**
* Given a string, expected to be in base64 format, an a
* content type, returns the corresponding Blob.
* If cannot be parsed, will return undefined.
*
* @param {string} data as a string.
* @param {string} content type of the expected blob. Can be null.
* @param {int} size of the slice to be parsed.
*/
function b64toBlob(b64Data, contentType, sliceSize) {
if (!b64Data) {
console.error('No b64 data provided to convert to blob.');
return;
}
contentType = contentType || '';
sliceSize = sliceSize || 1024;
function charCodeFromCharacter(c) {
return c.charCodeAt(0);
}
var blob;
try {
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = [];
for (var i = 0, l = slice.length; i < l; i++) {
byteNumbers.push(charCodeFromCharacter(slice[i]));
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
blob = new Blob(byteArrays, {type: contentType});
} catch (e) {
console.error('Error parsing base64 data');
}
return blob;
}
var VCFReader = (function _VCFReader() {
var ReBasic = /^([^:]+):(.+)$/;
var ReTuple = /([a-zA-Z]+)=(.+)/;
var WHITE_SPACE = ' ';
// Default tel type class
var DEFAULT_PHONE_TYPE = 'other';
// basic type vcard to mobile type
var VCARD_SIMPLE_TYPES = {
'fax' : 'faxOther',
'faxother' : 'faxOther',
'home' : 'home',
'internet' : 'internet',
'cell' : 'mobile',
'pager' : 'pager',
'personal' : 'home',
'pref' : 'pref',
'text' : 'text',
'textphone' : 'textphone',
'voice' : 'voice',
'work' : 'work'
};
// complex type vcard to mobile type
var VCARD_COMPLEX_TYPES = {
'fax,work' : 'faxOffice',
'fax,home' : 'faxHome',
'voice,work' : 'work',
'voice,home' : 'home'
};
var REST_TIMEOUT = 5000;
function _parseTuple(p) {
var match = p.match(ReTuple);
return match ? [match[1].toLowerCase(), match[2]] : ['type', p];
}
/**
* Parses a line and creates an object with the line type (name), its meta
* properties and its actual value.
*
* @param {string} line Line to be parsed from a VCF.
* @return {{key: string, data: {meta, value}}}
* @private
*/
function _parseLine(line) {
var parsed = ReBasic.exec(line);
if (!parsed) {
return null;
}
// `tuples` is an array containing all the extra properties
var tuples = parsed[1].split(/[;,]/);
var fieldName = tuples.shift().toLowerCase();
// Value will be an empty array if there is only whitespace and
// semi-colons.
var fieldValue = /[^\s;]/.test(parsed[2]) ? parsed[2].split(';') : [];
var meta = {
type: []
};
var len = tuples.length;
for (var i = 0; i < len; i++) {
var tuple = _parseTuple(tuples[i]);
if (tuple[0] === 'type') {
meta.type.push(tuple[1]);
} else {
meta[tuple[0]] = tuple[1];
}
}
// If this is a photo, we correct the value and type
if (fieldName === 'photo') {
if (meta.type.length) {
meta.type = ensureMimeType(meta.type[0]);
}
fieldValue = parsed[2];
// vCard 3.0 expects a mandatory encoding = 'b' parameter for base64
// encoded images, and vCard 2.1 expects a 'BASE64' one.
if (meta.encoding && (meta.encoding.toLowerCase() === 'b' ||
meta.encoding.toLowerCase() === 'base64')) {
meta.encoding = 'base64';
} else {
var parsedUri = parseDataUri(fieldValue);
if (parsedUri) {
meta.type = ensureMimeType(parsedUri.mime);
meta.encoding = 'base64';
fieldValue = parsedUri.value;
}
}
}
return {
key: fieldName,
data: {
meta: meta,
value: fieldValue
}
};
}
/**
* Parse vCard entries split by lines and pass the converted object back to
* the main thread.
*
* @param {string[][]} cardArray Array of array of strings representing vcard.
* @param {function} cb Callback to call on finishe.
*/
var _parseEntries = function(cardArray, cb) {
var parsedCards = [];
function sendIfFinished(contactObj) {
parsedCards.push(contactObj);
if (parsedCards.length === cardArray.length) {
cb(parsedCards);
}
}
for (var i = 0; i < cardArray.length; i++) {
var lines = cardArray[i];
// If there is no array of strings at cardArray[i] (representing a single
// vcard), push a null value to account for a processed card.
if (!lines) {
sendIfFinished(null);
continue;
}
var fields = {};
var len = lines.length;
for (var j = 0; j < len; j++) {
var line = lines[j];
var parsedLine = _parseLine(line);
if (!parsedLine) {
continue;
}
if (!fields[parsedLine.key]) {
fields[parsedLine.key] = [];
}
fields[parsedLine.key].push(parsedLine.data);
}
// If there isn't either a First Name (fn) or a Name (n) this card is not
// a valid one, ignore.
if (!fields.fn && !fields.n) {
sendIfFinished(null);
continue;
}
vcardToContact(fields, sendIfFinished);
}
};
/**
* Matches Quoted-Printable characters in a string
* @type {RegExp}
*/
var qpRegexp = /=([a-zA-Z0-9]{2})/g;
/**
* Decodes a string encoded in Quoted-Printable format.
* @param {string} str String to be decoded.
* @return {string}
*/
var _decodeQuoted = function(str) {
return decodeURIComponent(
str.replace(qpRegexp, '%$1'));
};
/**
* Decodes Quoted-Printable encoding into UTF-8
* http://en.wikipedia.org/wiki/Quoted-printable
*
* @param {object} metaObj Checks for 'encoding' key to be quoted printable.
* @param {string} value String to be decoded.
* @return {string}
*/
function _decodeQP(metaObj, value) {
var isQP = metaObj && metaObj.encoding &&
(/quoted-printable/i).test(metaObj.encoding);
if (isQP) {
value = _decodeQuoted(value);
}
return value;
}
var NAME_PARTS = [
'familyName',
'givenName',
'additionalName',
'honorificPrefix',
'honorificSuffix'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed name fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processName(vcardObj, contactObj) {
// Set First Name right away as the 'name' property
if (vcardObj.fn && vcardObj.fn.length) {
var fnMeta = vcardObj.fn[0].meta;
var fnValue = vcardObj.fn[0].value[0];
// For the name field, we want the data as it is
contactObj.name = [_decodeQP(fnMeta, fnValue)];
}
if (vcardObj.n && vcardObj.n.length) {
var values = vcardObj.n[0].value;
var meta = vcardObj.n[0].meta;
for (var i = 0; i < values.length; i++) {
var namePart = values[i];
if (namePart && NAME_PARTS[i]) {
// For the rest of the fields, we want to keep the merged data
// separated the same way, in different positions of the array.
// see bug 981674
contactObj[NAME_PARTS[i]] = _decodeQP(meta, namePart).split(',');
}
}
// If we don't have a contact name at this point, make `name` be the
// unification of all the name parts.
if (!contactObj.name) {
contactObj.name = [_decodeQP(meta, values.join(' ').trim())];
}
}
contactObj.givenName = contactObj.givenName || contactObj.name;
return contactObj;
}
var ADDR_PARTS = [null, null, 'streetAddress', 'locality', 'region',
'postalCode', 'countryName'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed address fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processAddr(vcardObj, contactObj) {
if (!vcardObj.adr) {
return contactObj;
}
contactObj.adr = [];
for (var i = 0; i < vcardObj.adr.length; i++) {
var cur = {};
var adr = vcardObj.adr[i];
if (adr.meta && adr.meta.type) {
cur.type = adr.meta.type;
}
for (var j = 2; j < adr.value.length; j++) {
var decoded = _decodeQP(adr.meta, adr.value[j]);
// Because of adding empty fields while parsing the vCard
// merging contacts sometimes doesn't work as expected
// Check Bug 935636 for reference
if (decoded !== '') {
cur[ADDR_PARTS[j]] = decoded;
}
}
contactObj.adr.push(cur);
}
return contactObj;
}
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed phone, email and url fields properly filled,
* inferred from `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processComm(vcardObj, contactObj) {
contactObj.tel = [];
['tel', 'email', 'url'].forEach(function field2field(field) {
if (!vcardObj[field]) {
return;
}
var len = vcardObj[field].length;
var hasTypeMapper = function(x) {
return x.trim().toLowerCase();
};
var notTypeMapper = function(v, key) {
return v.meta[key].trim().toLowerCase();
};
var noType = function(field) {
return field !== 'type';
};
var noPref = function(field) {
return field !== 'pref';
};
var typeFilter = function(metaValue) {
return !!VCARD_SIMPLE_TYPES[metaValue];
};
for (var i = 0; i < len; i++) {
var v = vcardObj[field][i];
var metaValues;
var cur = {};
if (v.meta) {
if (v.value) {
cur.value = _decodeQP(v.meta, v.value[0]);
cur.value = cur.value.replace(/^tel:/i, '');
}
if (v.meta.type) {
metaValues = ([].slice.call(v.meta.type)).map(hasTypeMapper);
} else {
metaValues = Object.keys(v.meta).filter(noType).map(
notTypeMapper.bind(null, v));
}
if (metaValues.indexOf('pref') !== -1) {
cur.pref = true;
metaValues = metaValues.filter(noPref);
}
/*
* metaValues is an array of types. If metaValues length is:
* 0: it returns the default type (No type was defined),
* 1: If the element matches a simple type, returns the simple type,
* if not, it returns the default type
* 2: If the elements match a complex type, it returns the complex
* type. If not, then try to match the elements with a simple type,
* in order. If one of the elements matches a simple type, it
* returns the first element that matches a simple type or if there
* exists no matching, it returns the default type value.
* otherwise --> Returns the first element that matches a simple type
* or if there exists no matching, it returns the default type value
* */
switch (metaValues.length) {
case 0:
cur.type = [DEFAULT_PHONE_TYPE];
break;
case 1:
cur.type = [VCARD_SIMPLE_TYPES[metaValues[0]] ||
DEFAULT_PHONE_TYPE];
break;
case 2:
var complexType1 = metaValues[0] + ',' + metaValues[1];
var complexType2 = metaValues[1] + ',' + metaValues[0];
cur.type = [VCARD_COMPLEX_TYPES[complexType1] ||
VCARD_COMPLEX_TYPES[complexType2] ||
VCARD_SIMPLE_TYPES[metaValues[0]] ||
VCARD_SIMPLE_TYPES[metaValues[1]] ||
DEFAULT_PHONE_TYPE];
break;
default:
cur.type = [
VCARD_SIMPLE_TYPES[metaValues.filter(typeFilter).shift()] ||
DEFAULT_PHONE_TYPE
];
}
}
if (!contactObj[field]) {
contactObj[field] = [];
}
contactObj[field].push(cur);
}
});
return contactObj;
}
function _isValidDate(dateValue) {
return !isNaN(Date.parse(dateValue));
}
function _processFields(vcardObj, contactObj) {
['org', 'title', 'bday', 'anniversary'].forEach(function(field) {
if (!vcardObj[field]) {
return;
}
var v = vcardObj[field][0];
if (!v || !Array.isArray(v.value) || v.value.length === 0) {
return;
}
var dateValue;
if (field === 'bday') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.bday = new Date(dateValue);
}
return;
}
if (field === 'anniversary') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.anniversary = new Date(dateValue);
}
return;
}
if (!v) {
return;
}
if (field === 'title') {
field = 'jobTitle';
}
switch (typeof v) {
case 'object':
contactObj[field] = [_decodeQP(v.meta, v.value[0])];
break;
case 'string':
contactObj[field] = [v];
break;
}
});
return contactObj;
}
function _processPhoto(vcardObj, contactObj, cb) {
if (!Array.isArray(vcardObj.photo) ||
vcardObj.photo.length === 0 ||
!vcardObj.photo[0] ||
!vcardObj.photo[0].value) {
return cb(contactObj);
}
var photo = vcardObj.photo[0];
var photoContents = photo.value.trim();
if (!photoContents) {
return cb(contactObj);
}
if (photoContents && photo.meta && photo.meta.encoding === 'base64') {
var blob = b64toBlob(photoContents, photo.meta.type);
if (blob) {
utils.thumbnailImage(blob, function gotThumbnail(thumbnail) {
if (blob !== thumbnail) {
contactObj.photo = [blob, thumbnail];
} else {
contactObj.photo = [blob];
}
cb(contactObj);
});
} else {
cb(contactObj);
}
}
// Else we assume it is a http url
else {
var callbacks = {
success: function(blobPic) {
if (blobPic) {
contactObj.photo = [blobPic];
}
cb(contactObj);
},
error: function() {
console.error('Error getting photo for contact');
cb(contactObj);
},
timeout: function() {
console.error('Timeout getting photo for contact');
cb(contactObj);
}
};
Rest.get(photoContents, callbacks, {
responseType: 'blob',
operationsTimeout: REST_TIMEOUT
});
}
}
/**
* Converts a parsed vCard to a mozContact.
*
* @param {Object} vcard JSON representation of an vCard.
* @param {Function} cb Function to call with the resulting moxContact object
* @return {Object, null} An object implementing mozContact interface.
*/
var vcardToContact = function(vcard, cb) {
if (!vcard) {
return null;
}
var obj = {};
_processName(vcard, obj);
_processAddr(vcard, obj);
_processComm(vcard, obj);
_processFields(vcard, obj);
_processPhoto(vcard, obj, function(contactObj) {
cb(utils.misc.toMozContact(contactObj));
});
};
/**
* Class used to parse vCard files (http://tools.ietf.org/html/rfc6350).
*
* @param {String} contents vCard formatted text.
* @constructor
*/
var VCFReader = function(contents) {
this.contents = contents;
this.processed = 0;
this.finished = false;
this.currentChar = 0;
this.numDupsMerged = 0;
};
// Number of contacts processed at a given time.
VCFReader.CONCURRENCY = 5;
/**
* Used to stop contact processing.
*/
VCFReader.prototype.finish = function() {
this.finished = true;
};
/**
* Starting point of vcard processing.
* @param {function} cb Function to call after the process is finished.
*/
VCFReader.prototype.process = function(cb) {
/**
* Calculate the total amount of contacts to be imported. This number could
* change in case there are vcards with syntax errors or that our processor
* can't parse.
*/
var self = this;
var match = this.contents.match(/end:vcard/gi);
// If there are no matches, then this probably isn't a vcard and we should
// stop processing.
if (!match) {
if (cb) {
cb();
}
return;
}
this.importedContacts = [];
this.total = match.length;
this.onread && this.onread(this.total);
this.ondone = function(numImported) {
cb(numImported, self.numDupsMerged);
};
LazyLoader.load(['/shared/js/simple_phone_matcher.js',
'/shared/js/mime_mapper.js',
'/shared/js/contact_photo_helper.js',
'/shared/js/contacts/import/utilities/misc.js',
'/shared/js/contacts/contacts_matcher.js',
'/shared/js/contacts/contacts_merger.js',
'/shared/js/contacts/utilities/image_thumbnail.js',
'/shared/js/contacts/merger_adapter.js',
'/shared/js/contacts/utilities/http_rest.js'
], function() {
// Start processing the text
// The data pump flows as follows:
// 1) splitlines() reads char-by-char to split the text into lines
// 2) each line is parsed to parseEntries() when we have accumulated
// enough lines to represent CONCURRENCY cards.
// 3) call post() for each contact found by parseEntries()
// 4) perform matching for each contact
// 5) save the contact if appropriate
// 6) call onParsed() which either goes back to (4) for the next contact
// or goes back to (1) to get the next set of lines
//
// Note: The async callbacks in the matching and mozContacts.save() code
// prevent this code from being purely recursive.
this.splitLines();
}.bind(this));
};
/**
* Called when every contact is effectively saved.
*
* @param {Error} err Error object in case there was one.
* @param {mozContact} ct Contact that has been just saved.
*/
VCFReader.prototype.onParsed = function(err, ct) {
this.processed += 1;
this.importedContacts.push(ct);
this.onimported && this.onimported(ct && ct.name);
if (this.finished || this.processed === this.total) {
this.ondone(this.importedContacts);
return;
}
var processed = this.processed;
if (processed < this.total && processed % VCFReader.CONCURRENCY === 0) {
this.splitLines();
}
};
/**
* This will be called every time we manage to process a contact
* @param {object[]} contactObjects Objects with contact structure.
*/
VCFReader.prototype.post = function(contactObjects) {
var _onParsed = this.onParsed.bind(this);
var cursor = 0;
var self = this;
function afterSave(ct, e) {
_onParsed(e, ct);
cursor += 1;
if (cursor < contactObjects.length) {
saveContact(contactObjects[cursor]);
}
}
function saveContact(ct) {
if (!ct) {
afterSave(null, null);
return;
}
var contact = utils.misc.toMozContact(ct);
var afterSaveFn = afterSave.bind(null, contact);
var matchCbs = {
onmatch: function(matches) {
var callbacks = {
success: function(mergedContact) {
self.numDupsMerged++;
afterSave(mergedContact, null);
},
error: afterSaveFn
};
contacts.adaptAndMerge(contact, matches, callbacks);
},
onmismatch: function() {
VCFReader._save(contact, afterSaveFn);
}
};
Matcher.match(contact, 'passive', matchCbs);
}
saveContact(contactObjects[cursor]);
};
/**
* Saves a single raw entry into the phone contacts
*
* @param {Object} item represents a single vCard entry.
* @param {Function} cb Callback.
*/
VCFReader._save = function(item, cb) {
var req = navigator.mozContacts.save(utils.misc.toMozContact(item));
req.onsuccess = cb;
req.onerror = cb;
};
var reBeginCard = /begin:vcard$/i;
var reEndCard = /end:vcard$/i;
var reVersion = /^VERSION:([\d\.]*)/i;
/**
* Splits vcard text into arrays of lines (one for each vcard field) and
* sends an array of arrays of lines over to process.
*/
VCFReader.prototype.splitLines = function() {
var currentLine = '';
var currentVersion = 0;
var inLabel = false;
var multiline = false;
var cardArray = [
[]
];
/**
* Number of cards processed. Quite faster than looking at `cardArray`
* length.
* @type {number}
*/
var cardsProcessed = 0;
// We start at the last cursor position
var i = this.currentChar;
var callPost = this.post.bind(this);
for (var l = this.contents.length; i < l; i++) {
this.currentChar = i;
var ch = this.contents[i];
if (currentVersion >= 4 && ch === '"') {
inLabel = !inLabel;
currentLine += ch;
continue;
}
// Ignore beginning whitespace that indicates multiline field.
if (multiline === true) {
if ((/[\r\t\s\n]/).test(ch)) {
continue;
} else {
//currentLine += '\n'
multiline = false;
}
}
var next = this.contents[i + 1];
if (inLabel || (ch !== '\n' && ch !== '\r')) {
// If we have a quoted-printable sign for multiline (/=\n/) AND
// we are not in a data URI field, ignore the character.
if (!(/^PHOTO/i).test(currentLine) && ch === '=' && next &&
next.search(/(\r|\n)/) !== -1) {
multiline = true;
continue;
}
currentLine += ch;
// Continue only if this is not the last char in the string
if (i !== l - 1) {
continue;
}
}
// If the field is a photo, the field could be multiline, to know if the
// following line is part of the photo, we must check if the first char
// of the following line is a space.
var firstCharNextLine = this.contents[i + 2];
if ((firstCharNextLine === WHITE_SPACE) && (/[\r\t\s\n]/).test(next) &&
(/^PHOTO/i).test(currentLine)) {
multiline = true;
continue;
}
// At this point, we know that ch is a newline, and in the vcard format,
// if we have a space after a newline, it indicates multiline field.
if (next && (next === WHITE_SPACE || next === '\t')) {
multiline = true;
continue;
}
if (reBeginCard.test(currentLine)) {
currentLine = '';
currentVersion = 0;
continue;
}
// If the current line indicates the end of a card,
if (reEndCard.test(currentLine)) {
cardsProcessed += 1;
if (cardsProcessed === VCFReader.CONCURRENCY ||
(cardsProcessed + this.processed) === this.total) {
_parseEntries(cardArray, callPost);
break;
}
currentLine = '';
cardArray.push([]);
continue;
}
if (currentLine) {
var matches = reVersion.exec(currentLine);
if (matches === null) {
cardArray[cardArray.length - 1].push(currentLine);
} else {
currentVersion = parseFloat(matches[1] || '0');
}
}
currentLine = '';
}
};
VCFReader._decodeQuoted = _decodeQuoted;
VCFReader.processAddr = _processAddr;
VCFReader.processName = _processName;
VCFReader.vcardToContact = vcardToContact;
//Expose some utility methods
VCFReader.utils = {
parseDataUri: parseDataUri,
b64toBlob: b64toBlob
};
return VCFReader;
})();

View file

@ -0,0 +1,758 @@
/* global LazyLoader, utils, Rest, MimeMapper */
/* exported VCardReader */
(function() {
'use strict';
/* WARNING this module LazyLoads these modules
* '/shared/js/mime_mapper.js',
* '/shared/js/contacts/utilities/image_thumbnail.js',
* '/shared/js/contacts/import/utilities/misc.js',
* '/shared/js/contacts/utilities/http_rest.js'
*/
var dependencies = [
'/shared/js/mime_mapper.js',
'/shared/js/contacts/utilities/image_thumbnail.js',
'/shared/js/contacts/import/utilities/misc.js',
'/shared/js/contacts/utilities/http_rest.js'
];
var ReBasic = /^([^:]+):(.+)$/;
var ReTuple = /([a-zA-Z]+)=(.+)/;
var WHITE_SPACE = ' ';
var reBeginCard = /begin:vcard$/i;
var reEndCard = /end:vcard$/i;
var reVersion = /^VERSION:/i;
// Default tel type class
var DEFAULT_PHONE_TYPE = 'other';
// basic type vcard to mobile type
var VCARD_SIMPLE_TYPES = {
'fax' : 'faxOther',
'faxother' : 'faxOther',
'home' : 'home',
'internet' : 'internet',
'cell' : 'mobile',
'pager' : 'pager',
'personal' : 'home',
'pref' : 'pref',
'text' : 'text',
'textphone' : 'textphone',
'voice' : 'voice',
'work' : 'work'
};
// complex type vcard to mobile type
var VCARD_COMPLEX_TYPES = {
'fax,work' : 'faxOffice',
'fax,home' : 'faxHome',
'voice,work' : 'work',
'voice,home' : 'home'
};
var REST_TIMEOUT = 5000;
/**
* VCardReader object. It allows to read sequentially a set of
* vCard objects present on a vCard File.
*
* Example of use:
*
* var reader = new VCardReader(vcardData);
* var cursor = reader.getAll();
*
* cursor.onsuccess = function(evt) {
* var contact = evt.target.result;
* if(contact) {
* // do something
* cursor.continue();
* }
* else {
* // We've just finished
* }
* }
*
*/
window.VCardReader = function VCardReader(data) {
var content = data.replace(/\r?\n/g, '\n');
var currentIndex = 0;
this.getAll = function() {
return new Cursor();
};
function Cursor() {
var self = this,
depsLoaded = false;
this._loadDependenciesAndRead = function() {
if (depsLoaded) {
self._doRead();
} else {
LazyLoader.load(dependencies, function() {
depsLoaded = true;
self._doRead();
});
}
};
this._doRead = function() {
_readNext(function(contact) {
self.result = contact;
if (self.callback) {
self.callback({
target: self
});
}
});
};
Object.defineProperty(this, 'onsuccess', {
set: function(cb) {
self.callback = cb;
self._loadDependenciesAndRead();
}
});
this.continue = function() {
// If there was no previous result. do nothing
if (this.result) {
this._loadDependenciesAndRead();
}
};
}
function _getCardLines() {
var currentLine = '';
var inLabel = false;
var multiline = false;
var lines = [];
// We start at the last cursor position
var i = currentIndex;
for (var l = content.length; i < l; i++) {
currentIndex = i;
var ch = content[i];
if (ch === '"') {
inLabel = !inLabel;
currentLine += ch;
continue;
}
// Ignore beginning whitespace that indicates multiline field.
if (multiline === true) {
if ((/[\r\t\s\n]/).test(ch)) {
continue;
} else {
multiline = false;
}
}
var next = content[i + 1];
if (inLabel || (ch !== '\n' && ch !== '\r')) {
// If we have a quoted-printable sign for multiline (/=\n/) AND
// we are not in a data URI field, ignore the character.
if (!(/^PHOTO/i).test(currentLine) && ch === '=' && next &&
next.search(/(\r|\n)/) !== -1) {
multiline = true;
continue;
}
currentLine += ch;
// Continue only if this is not the last char in the string
if (i !== l - 1) {
continue;
}
}
// If the field is a photo, the field could be multiline, to know if the
// following line is part of the photo, we must check if the first char
// of the following line is a space.
var firstCharNextLine = content[i + 2];
if ((firstCharNextLine === WHITE_SPACE) && (/[\r\t\s\n]/).test(next) &&
(/^PHOTO/i).test(currentLine)) {
multiline = true;
continue;
}
// At this point, we know that ch is a newline, and in the vcard format,
// if we have a space after a newline, it indicates multiline field.
if (next && (next === WHITE_SPACE || next === '\t')) {
multiline = true;
continue;
}
if (reBeginCard.test(currentLine)) {
currentLine = '';
continue;
}
// If the current line indicates the end of a card,
if (reEndCard.test(currentLine)) {
break;
}
if (currentLine && !reVersion.test(currentLine)) {
lines.push(currentLine);
}
currentLine = '';
}
currentIndex++;
return lines;
}
// look if there is more cards to read
function _checkFinished() {
return !content.substr(currentIndex).match(/end:vcard/gi);
}
function _readNext(cb) {
var fields = {};
var lines;
var line;
var parsedLine;
if (_checkFinished()) {
cb();
return;
}
lines = _getCardLines();
for (var i = 0; i < lines.length; i++) {
line = lines[i];
parsedLine = _parseLine(line);
if (parsedLine) {
if (!fields[parsedLine.key]) {
fields[parsedLine.key] = [];
}
fields[parsedLine.key].push(parsedLine.data);
}
}
// If there isn't either a First Name (fn) or a Name (n) this card is not
// a valid one, ignore and try to parse the next one.
if (!fields.fn && !fields.n) {
console.warn('Invalid vCard found. Skipping');
if (_checkFinished()) {
cb();
} else {
_readNext(function(contact) {
cb(contact);
});
}
} else {
vcardToContact(fields, cb);
}
}
};
function parseDataUri(str) {
var re = /^data:(.+);(charset=([^;]+))?;?base64,(.*)$/;
var matches = re.exec(str);
if (matches) {
return {
mime: matches[1],
charset: matches[3],
value: matches[4]
};
}
return null;
}
function ensureMimeType(type) {
type = type.toLowerCase();
if (!MimeMapper.isSupportedType(type)) {
type = MimeMapper.guessTypeFromExtension(type) || type;
}
return type || '';
}
/**
* Given a string, expected to be in base64 format, an a
* content type, returns the corresponding Blob.
* If cannot be parsed, will return undefined.
*
* @param {string} data as a string.
* @param {string} content type of the expected blob. Can be null.
* @param {int} size of the slice to be parsed.
*/
function b64toBlob(b64Data, contentType, sliceSize) {
if (!b64Data) {
console.error('No b64 data provided to convert to blob.');
return;
}
contentType = contentType || '';
sliceSize = sliceSize || 1024;
function charCodeFromCharacter(c) {
return c.charCodeAt(0);
}
var blob;
try {
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length;
offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = [];
for (var i = 0, l = slice.length; i < l; i++) {
byteNumbers.push(charCodeFromCharacter(slice[i]));
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
blob = new Blob(byteArrays, {type: contentType});
} catch (e) {
console.error('Error parsing base64 data');
}
return blob;
}
/**
* Parses a line and creates an object with the line type (name), its meta
* properties and its actual value.
*
* @param {string} line Line to be parsed from a VCF.
* @return {{key: string, data: {meta, value}}}
* @private
*/
function _parseLine(line) {
var parsed = ReBasic.exec(line);
if (!parsed) {
return null;
}
// `tuples` is an array containing all the extra properties
var tuples = parsed[1].split(/[;,]/);
var fieldName = tuples.shift().toLowerCase();
fieldName = fieldName.split('.').pop(); // If grouped element, get real name
// Value will be an empty array if there is only whitespace and
// semi-colons.
var fieldValue = /[^\s;]/.test(parsed[2]) ? parsed[2].split(';') : [];
var meta = {
type: []
};
var len = tuples.length;
for (var i = 0; i < len; i++) {
var tuple = _parseTuple(tuples[i]);
if (tuple[0] === 'type') {
meta.type.push(tuple[1]);
} else {
meta[tuple[0]] = tuple[1];
}
}
// If this is a photo, we correct the value and type
if (fieldName === 'photo') {
if (meta.type.length) {
meta.type = ensureMimeType(meta.type[0]);
}
fieldValue = parsed[2];
// vCard 3.0 expects a mandatory encoding = 'b' parameter for base64
// encoded images, and vCard 2.1 expects a 'BASE64' one.
if (meta.encoding && (meta.encoding.toLowerCase() === 'b' ||
meta.encoding.toLowerCase() === 'base64')) {
meta.encoding = 'base64';
} else {
var parsedUri = parseDataUri(fieldValue);
if (parsedUri) {
meta.type = ensureMimeType(parsedUri.mime);
meta.encoding = 'base64';
fieldValue = parsedUri.value;
}
}
}
return {
key: fieldName,
data: {
meta: meta,
value: fieldValue
}
};
}
function _parseTuple(p) {
var match = p.match(ReTuple);
return match ? [match[1].toLowerCase(), match[2]] : ['type', p];
}
/**
* Converts a parsed vCard to a mozContact.
*
* @param {Object} vcard JSON representation of an vCard.
* @param {Function} cb Function to call with the resulting moxContact object
* @return {Object, null} An object implementing mozContact interface.
*/
function vcardToContact(vcard, cb) {
if (!vcard) {
return null;
}
var obj = {};
_processName(vcard, obj);
_processAddr(vcard, obj);
_processComm(vcard, obj);
_processFields(vcard, obj);
_processPhoto(vcard, obj, function(contactObj) {
cb(utils.misc.toMozContact(contactObj));
});
return utils.misc.toMozContact(obj);
}
/**
* Matches Quoted-Printable characters in a string
* @type {RegExp}
*/
var qpRegexp = /=([a-zA-Z0-9]{2})/g;
/**
* Decodes a string encoded in Quoted-Printable format.
* @param {string} str String to be decoded.
* @return {string}
*/
var _decodeQuoted = function(str) {
return decodeURIComponent(
str.replace(qpRegexp, '%$1'));
};
/**
* Decodes Quoted-Printable encoding into UTF-8
* http://en.wikipedia.org/wiki/Quoted-printable
*
* @param {object} metaObj Checks for 'encoding' key to be quoted printable.
* @param {string} value String to be decoded.
* @return {string}
*/
function _decodeQP(metaObj, value) {
var isQP = metaObj && metaObj.encoding &&
(/quoted-printable/i).test(metaObj.encoding);
if (isQP) {
value = _decodeQuoted(value);
}
return value;
}
var NAME_PARTS = [
'familyName',
'givenName',
'additionalName',
'honorificPrefix',
'honorificSuffix'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed name fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processName(vcardObj, contactObj) {
// Set First Name right away as the 'name' property
if (vcardObj.fn && vcardObj.fn.length) {
var fnMeta = vcardObj.fn[0].meta;
var fnValue = vcardObj.fn[0].value[0];
// For the name field, we want the data as it is
contactObj.name = [_decodeQP(fnMeta, fnValue)];
}
if (vcardObj.n && vcardObj.n.length) {
var values = vcardObj.n[0].value;
var meta = vcardObj.n[0].meta;
for (var i = 0; i < values.length; i++) {
var namePart = values[i];
if (namePart && NAME_PARTS[i]) {
// For the rest of the fields, we want to keep the merged data
// separated the same way, in different positions of the array.
// see bug 981674
contactObj[NAME_PARTS[i]] = _decodeQP(meta, namePart).split(',');
}
}
// If we don't have a contact name at this point, make `name` be the
// unification of all the name parts.
if (!contactObj.name) {
contactObj.name = [_decodeQP(meta, values.join(' ').trim())];
}
}
return contactObj;
}
var ADDR_PARTS = [null, null, 'streetAddress', 'locality', 'region',
'postalCode', 'countryName'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed address fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processAddr(vcardObj, contactObj) {
if (!vcardObj.adr) {
return contactObj;
}
contactObj.adr = [];
for (var i = 0; i < vcardObj.adr.length; i++) {
var cur = {};
var adr = vcardObj.adr[i];
if (adr.meta && adr.meta.type) {
cur.type = adr.meta.type;
}
for (var j = 2; j < adr.value.length; j++) {
var decoded = _decodeQP(adr.meta, adr.value[j]);
// Because of adding empty fields while parsing the vCard
// merging contacts sometimes doesn't work as expected
// Check Bug 935636 for reference
if (decoded !== '') {
cur[ADDR_PARTS[j]] = decoded;
}
}
contactObj.adr.push(cur);
}
return contactObj;
}
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed phone, email and url fields properly filled,
* inferred from `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processComm(vcardObj, contactObj) {
contactObj.tel = [];
['tel', 'email', 'url'].forEach(function field2field(field) {
if (!vcardObj[field]) {
return;
}
var len = vcardObj[field].length;
var hasTypeMapper = function(x) {
return x.trim().toLowerCase();
};
var notTypeMapper = function(v, key) {
return v.meta[key].trim().toLowerCase();
};
var noType = function(field) {
return field !== 'type';
};
var noPref = function(field) {
return field !== 'pref';
};
var typeFilter = function(metaValue) {
return !!VCARD_SIMPLE_TYPES[metaValue];
};
for (var i = 0; i < len; i++) {
var v = vcardObj[field][i];
var metaValues;
var cur = {};
if (v.meta) {
if (v.value) {
cur.value = _decodeQP(v.meta, v.value[0]);
cur.value = cur.value.replace(/^tel:/i, '');
}
if (v.meta.type) {
metaValues = ([].slice.call(v.meta.type)).map(hasTypeMapper);
} else {
metaValues = Object.keys(v.meta).filter(noType).map(
notTypeMapper.bind(null, v));
}
if (metaValues.indexOf('pref') !== -1) {
cur.pref = true;
metaValues = metaValues.filter(noPref);
}
/*
* metaValues is an array of types. If metaValues length is:
* 0: it returns the default type (No type was defined),
* 1: If the element matches a simple type, returns the simple type,
* if not, it returns the default type
* 2: If the elements match a complex type, it returns the complex
* type. If not, then try to match the elements with a simple type,
* in order. If one of the elements matches a simple type, it
* returns the first element that matches a simple type or if there
* exists no matching, it returns the default type value.
* otherwise --> Returns the first element that matches a simple type
* or if there exists no matching, it returns the default type value
* */
switch (metaValues.length) {
case 0:
cur.type = [DEFAULT_PHONE_TYPE];
break;
case 1:
cur.type = [VCARD_SIMPLE_TYPES[metaValues[0]] ||
DEFAULT_PHONE_TYPE];
break;
case 2:
var complexType1 = metaValues[0] + ',' + metaValues[1];
var complexType2 = metaValues[1] + ',' + metaValues[0];
cur.type = [VCARD_COMPLEX_TYPES[complexType1] ||
VCARD_COMPLEX_TYPES[complexType2] ||
VCARD_SIMPLE_TYPES[metaValues[0]] ||
VCARD_SIMPLE_TYPES[metaValues[1]] ||
DEFAULT_PHONE_TYPE];
break;
default:
cur.type = [
VCARD_SIMPLE_TYPES[metaValues.filter(typeFilter).shift()] ||
DEFAULT_PHONE_TYPE
];
}
}
if (!contactObj[field]) {
contactObj[field] = [];
}
contactObj[field].push(cur);
}
});
return contactObj;
}
function _isValidDate(dateValue) {
return !isNaN(Date.parse(dateValue));
}
function _processFields(vcardObj, contactObj) {
['org', 'title', 'bday', 'anniversary'].forEach(function(field) {
if (!vcardObj[field]) {
return;
}
var v = vcardObj[field][0];
var dateValue;
if (field === 'bday') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.bday = new Date(dateValue);
}
return;
}
if (field === 'anniversary') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.anniversary = new Date(dateValue);
}
return;
}
if (!v) {
return;
}
if (field === 'title') {
field = 'jobTitle';
}
switch (typeof v) {
case 'object':
contactObj[field] = [_decodeQP(v.meta, v.value[0])];
break;
case 'string':
contactObj[field] = [v];
break;
}
});
return contactObj;
}
function _processPhoto(vcardObj, contactObj, cb) {
if (!Array.isArray(vcardObj.photo) ||
vcardObj.photo.length === 0 ||
!vcardObj.photo[0] ||
!vcardObj.photo[0].value) {
return cb(contactObj);
}
var photo = vcardObj.photo[0];
var photoContents = photo.value.trim();
if (!photoContents) {
return cb(contactObj);
}
if (photoContents && photo.meta && photo.meta.encoding === 'base64') {
var blob = b64toBlob(photoContents, photo.meta.type);
if (blob) {
utils.thumbnailImage(blob, function gotThumbnail(thumbnail) {
if (blob !== thumbnail) {
contactObj.photo = [blob, thumbnail];
} else {
contactObj.photo = [blob];
}
cb(contactObj);
});
} else {
cb(contactObj);
}
}
// Else we assume it is a http url
else {
var callbacks = {
success: function(blobPic) {
if (blobPic) {
contactObj.photo = [blobPic];
}
cb(contactObj);
},
error: function() {
console.error('Error getting photo for contact');
cb(contactObj);
},
timeout: function() {
console.error('Timeout getting photo for contact');
cb(contactObj);
}
};
Rest.get(photoContents, callbacks, {
responseType: 'blob',
operationsTimeout: REST_TIMEOUT
});
}
}
window.VCardReader.b64toBlob = b64toBlob;
window.VCardReader.parseDataUri = parseDataUri;
})();

View file

@ -0,0 +1,24 @@
'use strict';
var contacts = window.contacts || {};
contacts.adaptAndMerge = function(incomingContact, matches, callbacks) {
var listIds = Object.keys(matches);
var totalMatches = listIds.length;
// First contact here we take as the master
var masterContact = matches[listIds[0]].matchingContact;
var matchingContacts = [];
for (var j = 1; j < totalMatches; j++) {
matchingContacts.push(matches[listIds[j]]);
}
// Finally the last matching is the incoming itself
// XXX: it has no `matchings` entry as it does not come from the matching
// algorithm.
// Now only used here but consider to provide a constructor in the future.
matchingContacts.push({
matchingContact: incomingContact
});
contacts.Merger.merge(masterContact, matchingContacts, callbacks);
};

View file

@ -0,0 +1,182 @@
'use strict';
/* exported MultiContact */
/* globals Promise, LazyLoader, contacts */
// ATTENTION: This library lazy loads contacts_merger.js
var MultiContact = (function() {
var datastores = Object.create(null);
var datastoresLoading = false;
var DS_READY_EVENT = 'ds_ready';
var MOZ_CONTACTS_OWNER = 'app://communications.gaiamobile.org';
function getDatastore(owner) {
if (datastores[owner]) {
return Promise.resolve(datastores[owner]);
}
return new Promise(function(resolve, reject) {
if (datastoresLoading === true) {
document.addEventListener(DS_READY_EVENT, function handler() {
document.removeEventListener(DS_READY_EVENT, handler);
resolve(datastores[owner]);
});
}
else {
datastoresLoading = true;
navigator.getDataStores('contacts').then(function success(dsList) {
dsList.forEach(function(aDs) {
datastores[aDs.owner] = aDs;
});
// This is needed because mozContact DB is not exposed as a Datastore
// Once bug 1016838 lands this will not be needed
datastores[MOZ_CONTACTS_OWNER] = new MozContactsDatastore();
resolve(datastores[owner]);
datastoresLoading = false;
document.dispatchEvent(new CustomEvent(DS_READY_EVENT));
}, function err(error) {
console.error('Error while obtaining datastores: ', error.name);
});
}
});
}
// Adapter object to obtain data from the mozContacts as if it were a DS
// Once bug 1016838 lands this will not be needed
function MozContactsDatastore() {
}
MozContactsDatastore.prototype = {
get: function(id) {
return new Promise(function(resolve, reject) {
var options = {
filterBy: ['id'],
filterOp: 'equals',
filterValue: id
};
var req = navigator.mozContacts.find(options);
req.onsuccess = function() {
resolve(JSON.parse(JSON.stringify(req.result[0])));
};
req.onerror = function() {
reject(req.error);
};
});
},
get name() {
return 'mozContacts';
}
};
function getData(entry) {
if (!entry || !entry.id || !Array.isArray(entry.entryData)) {
return Promise.reject({
name: 'InvalidEntry'
});
}
return new Promise(function(resolve, reject) {
var operations = [];
var entryData = entry.entryData;
var mozContactId;
entryData.forEach(function fetchEntry(aEntry) {
var owner = aEntry.origin;
if (owner === MOZ_CONTACTS_OWNER) {
mozContactId = aEntry.uid;
}
getDatastore(owner).then(function success(datastore) {
operations.push(datastore.get(aEntry.uid));
// It is needed to wait to have all operations ready
if (operations.length === entryData.length) {
execute(operations, resolve, reject, {
targetId: entry.id,
mozContactId: mozContactId
});
}
}, function error(err) {
console.error('Error while obtaining datastore: ', err.name);
reject(err);
});
});
});
}
function execute(operations, resolve, reject, options) {
Promise.all(operations).then(function success(results) {
if (results.length === 1) {
results[0].id = options.targetId;
resolve(results[0]);
return;
}
if (options.mozContactId) {
results = reorderResults(results, options.mozContactId);
}
LazyLoader.load('/shared/js/contacts/contacts_merger.js',
function() {
var matchings = createMatchingContacts(results);
contacts.Merger.inMemoryMerge(results[0], matchings).then(
function success(mergedResult) {
mergedResult.id = options.targetId;
resolve(mergedResult);
}, function error(err) {
console.log('Error while merging: ', err);
reject(err);
});
});
}, function error(err) {
console.error('Error while getting data: ', err.name);
reject(err);
});
}
// This function reorders the contact data results array in order to
// put the mozContact in the first position, as mozContact data will be taken
// precedence over other datastore data
function reorderResults(results, mozContactId) {
var out = [];
for (var j = 0; j < results.length; j++) {
if (results[j].id === mozContactId) {
out.unshift(results[j]);
}
else {
out.push(results[j]);
}
}
return out;
}
// This function adapts the result array to the input object expected by
// the contacts_merger module
function createMatchingContacts(results) {
var out = [];
for (var j = 1; j < results.length; j++) {
out.push({
matchingContact: results[j]
});
}
return out;
}
return {
'getData': getData
};
})();

696
gaia/shared/js/contacts/search.js Executable file
View file

@ -0,0 +1,696 @@
'use strict';
/* global Normalizer */
/* global utils */
/* global HtmlHelper */
/* global LazyLoader */
/* global ImageLoader */
/* global fb */
/* exported Search */
(function(exports) {
var inSearchMode = false,
searchView,
searchBox,
searchList,
searchNoResult,
searchProgress,
searchTimer = null,
contactNodes = null,
selectableForm = null,
// On the steady state holds the list result of the current search
searchableNodes = null,
currentTextToSearch = '',
currentSearchTerms = [],
prevTextToSearch = '',
// Pointer to the nodes which are currently on the result list
currentSet = {},
searchTextCache = {},
canReuseSearchables = false,
blurList = false,
theClones = {},
CHUNK_SIZE = 10,
// Default to invalid page size and recalculate when first row added
searchPageSize = -1,
// Limit search result to hardLimit contacts, recalc with page size
hardLimit = 25,
emptySearch = true,
remainingPending = true,
imgLoader,
searchEnabled = false,
source = null,
navigationController = null;
// The _source argument should be an adapter object that provides access
// to the contact nodes in the app. This is done by defining the following
// functions on the adapter object:
// getNodes() An Array of all contact DOM nodes
// getFirstNode() First contact DOM node
// getNextNode(node) Given a node, find the next node
// expectMoreNodes() True if nodes will be added via appendNodes()
// clone(node) Clone the given contact node
// getNodeById(id) Get the node matching the given ID, or null
// getSearchText(node) Get the search text from the given node
// click(event) Click event handler to use
var init = function load(_source, defaultEnabled, navigation) {
searchView = document.getElementById('search-view');
searchList = document.getElementById('search-list');
selectableForm = document.getElementById('selectable-form');
if (!_source) {
throw new Error('Search requires a contact source!');
}
source = _source;
if (typeof source.click === 'function') {
searchList.addEventListener('click', source.click);
}
searchEnabled = !!defaultEnabled;
navigationController = navigation || window.MainNavigation;
};
var initialized = false;
var ignoreReturnKey = function ignoreReturnKey(evt) {
if (evt.keyCode == 13) { // VK_Return
evt.preventDefault();
}
};
var doInit = function doInit() {
if (initialized) {
return;
}
utils.listeners.add({
'#cancel-search': exitSearchMode,
'#search-contact': [
{
event: 'keypress',
handler: ignoreReturnKey
}
]
});
initialized = true;
searchBox = document.getElementById('search-contact');
var resetButton = searchBox.nextElementSibling;
resetButton.addEventListener('ontouchstart' in window ? 'touchstart' :
'mousedown', function() {
searchBox.value = '';
searchBox.focus();
resetState();
window.setTimeout(fillInitialSearchPage, 0);
});
searchList.parentNode.addEventListener('touchstart', function() {
if (searchableNodes && remainingPending) {
addRemainingResults(searchableNodes, searchPageSize);
}
blurList = true;
});
searchNoResult = document.getElementById('no-result');
searchProgress = document.getElementById('search-progress');
searchBox.addEventListener('blur', function() {
window.setTimeout(onSearchBlur, 0);
});
searchBox.addEventListener('focus', function() {
blurList = false;
});
LazyLoader.load([
'/contacts/js/fb_resolver.js',
'/shared/js/contacts/utilities/image_loader.js'
], function() {
imgLoader = new ImageLoader('#groups-list-search', 'li');
imgLoader.setResolver(fb.resolver);
});
};
var clearHighlights = function(node) {
// We traverse the DOM tree and remove highlighting marks.
// getElements instead of querySelector here because of
// performance.
var highlights = node.getElementsByTagName('mark');
while(highlights.length) {
var parent = highlights[0].parentNode;
while(highlights[0].firstChild) {
parent.insertBefore(highlights[0].firstChild, highlights[0]);
}
// This removes the item from 'highlights' HTMLCollection as well
// via live DOM updating.
parent.removeChild(highlights[0]);
}
};
/**
* Given a contact DOM node, this function highlights the search terms it
* contains. First, it extract the text contents, highlights every chunk and
* then rebuild the content of the node respecting the location of the
* <strong> tag. To know where every chunk starts and ends, we use the
* locateHTMLTag so we know what we have to highlight.
*
* Highlighting is done in the createHighlightHTML() function inside the
* HtmlHelper module.
*/
var highlightNode = function(node) {
function doHighlight(text) {
if (text === '' || text === ' ') {
return document.createTextNode('');
}
return HtmlHelper.createHighlightHTML(text, currentSearchTerms);
}
function locateHTMLTag(text) {
var tagLocation = [];
tagLocation.push(text.indexOf('<'));
tagLocation.push(text.indexOf('>', tagLocation[0]) + 1);
tagLocation.push(text.indexOf('<', tagLocation[1]));
tagLocation.push(text.indexOf('>', tagLocation[2]) + 1);
return tagLocation;
}
var textNode = node.querySelector('.contact-text bdi');
if (textNode === null) {
return;
}
var text = textNode.innerHTML;
var tagPos = locateHTMLTag(text);
var beforeTag = text.substring(0, tagPos[0]);
var inTag = text.substring(tagPos[1], tagPos[2]);
var afterTag = text.substring(tagPos[3]);
var realText = [beforeTag, inTag, afterTag];
if (tagPos[0] == -1) {
var content = doHighlight(Normalizer.unescapeHTML(text));
// clear the previous content (if any)
textNode.textContent = '';
textNode.appendChild(content);
} else {
for (var i = 0, len = realText.length; i < len; i++) {
realText[i] = doHighlight(Normalizer.unescapeHTML(realText[i]));
}
var fragment = document.createDocumentFragment();
var strongTag = document.createElement('strong');
strongTag.appendChild(realText[1]);
fragment.appendChild(realText[0]);
fragment.appendChild(strongTag);
fragment.appendChild(realText[2]);
// clear the previous content (if any)
textNode.textContent = '';
textNode.appendChild(fragment);
}
};
var updateSearchList = function updateSearchList(cb) {
if (!inSearchMode) {
if (cb) {
cb();
}
return;
}
window.setTimeout(function() {
// Resetting state
resetStateAndCache();
search(cb);
});
};
var resetStateAndCache = function resetStateAndCache() {
contactNodes = null;
searchTextCache = {};
resetState();
};
// Search mode instructions
var exitSearchMode = function exitSearchMode(evt) {
if (evt) {
evt.preventDefault();
}
searchView.classList.remove('insearchmode');
if (selectableForm) {
selectableForm.classList.remove('insearchmode');
}
if (navigationController) {
navigationController.back();
}
window.setTimeout(function exit_search() {
hideProgressResults();
searchBox.value = '';
// Resetting state
contactNodes = null;
searchTextCache = {};
resetState();
inSearchMode = false;
}, 0);
window.removeEventListener('input', onInput);
};
function resetState() {
prevTextToSearch = '';
currentTextToSearch = '';
currentSearchTerms = [];
searchableNodes = null;
canReuseSearchables = false;
currentSet = {};
// We don't know if the user will launch a new search later
theClones = {};
utils.dom.removeChildNodes(searchList);
emptySearch = true;
remainingPending = true;
}
function addRemainingResults(nodes, from) {
if (remainingPending !== true) {
return;
}
var fragment = document.createDocumentFragment();
for (var i = from; i < hardLimit && i < nodes.length; i++) {
var node = nodes[i].node;
var clon = getClone(node);
highlightNode(clon);
fragment.appendChild(clon);
currentSet[node.dataset.uuid] = clon;
}
if (fragment.hasChildNodes()) {
searchList.appendChild(fragment);
imgLoader.reload();
}
remainingPending = false;
}
function onSearchBlur(e) {
if (canReuseSearchables && searchableNodes &&
searchView.classList.contains('insearchmode') && blurList) {
// All the searchable nodes have to be added
addRemainingResults(searchableNodes, searchPageSize);
}
else if (emptySearch === true && remainingPending === true) {
var lastNode = searchList.querySelector('li:last-child');
if (lastNode) {
var lastNodeUid = lastNode.dataset.uuid;
var startNode = source.getNextNode(source.getNodeById(lastNodeUid));
fillIdentityResults(startNode, hardLimit - searchPageSize);
remainingPending = false;
imgLoader.reload();
}
}
}
function fillIdentityResults(startNode, number) {
var fragment = document.createDocumentFragment();
var contact = startNode;
for (var i = 0; i < number && contact; i++) {
var clonedNode = getClone(contact);
fragment.appendChild(clonedNode);
currentSet[contact.dataset.uuid] = clonedNode;
contact = source.getNextNode(contact);
}
if (fragment.hasChildNodes()) {
searchList.appendChild(fragment);
}
}
function getClone(node) {
var id = node.dataset.uuid;
var out = theClones[id];
if (!out) {
out = source.clone(node);
cacheClone(id, out);
}
return out;
}
function cacheClone(id, clone) {
theClones[id] = clone;
}
function onInput(e) {
if (e.target.id === searchBox.id) {
search();
}
}
var enterSearchMode = function searchMode(evt) {
evt.preventDefault();
if (!inSearchMode) {
window.addEventListener('input', onInput);
searchView.classList.add('insearchmode');
if (selectableForm) {
selectableForm.classList.add('insearchmode');
}
doInit();
fillInitialSearchPage();
inSearchMode = true;
emptySearch = true;
if (navigationController) {
navigationController.go('search-view', 'none');
}
setTimeout(function nextTick() {
searchBox.focus();
});
}
};
function fillInitialSearchPage(done) {
hideProgressResults();
var startContact = source.getFirstNode();
var numToFill = searchPageSize;
// Calculate rows visible on a single page the first time we get a row
// that we can measure.
if (startContact && searchPageSize < 1) {
fillIdentityResults(startContact, 1);
var viewHeight = searchList.getBoundingClientRect().height;
var rowHeight = searchList.children[0].getBoundingClientRect().height;
searchPageSize = Math.ceil(viewHeight / rowHeight);
hardLimit = ~~(3.5 * searchPageSize);
startContact = source.getNextNode(startContact);
numToFill = searchPageSize - 1;
}
fillIdentityResults(startContact, numToFill);
if (typeof done === 'function') {
done();
}
if (imgLoader) {
imgLoader.reload();
}
}
function doSearch(contacts, from, searchText, pattern, state) {
// Check whether the user enter a new term or not
if (currentTextToSearch.localeCompare(searchText) !== 0) {
canReuseSearchables = false;
window.console.warn('**** Cancelling current search ****');
return;
}
// Search the next chunk of contacts
var end = from + CHUNK_SIZE;
currentSearchTerms = pattern.source.split(/\s+/);
for (var c = from; c < end && c < contacts.length; c++) {
var contact = contacts[c].node || contacts[c];
var contactText = contacts[c].text || getSearchText(contacts[c]);
contactText = Normalizer.unescapeHTML(contactText);
if (!checkContactMatch(currentSearchTerms, contactText)) {
if (contact.dataset.uuid in currentSet) {
searchList.removeChild(currentSet[contact.dataset.uuid]);
delete currentSet[contact.dataset.uuid];
}
} else {
if (state.count === 0) {
hideProgressResults();
}
// Only an initial page of elements is loaded in the search list
if (Object.keys(currentSet).length < searchPageSize &&
!(contact.dataset.uuid in currentSet)) {
var clonedNode = getClone(contact);
currentSet[contact.dataset.uuid] = clonedNode;
searchList.appendChild(clonedNode);
}
if (currentSet[contact.dataset.uuid]) {
// We clear the highlights here because parts of the node could
// been already highlighted from a previous, more general search.
clearHighlights(currentSet[contact.dataset.uuid]);
highlightNode(currentSet[contact.dataset.uuid]);
}
state.searchables.push({
node: contact,
text: contactText
});
state.count++;
}
}
// If we are still searching through the list, then schedule
// the next batch of search comparisons.
if (c < contacts.length) {
searchTimer = window.setTimeout(function do_search() {
searchTimer = null;
doSearch(contacts, from + CHUNK_SIZE, searchText,
pattern, state);
}, 0);
return;
// If we expect to get more nodes, for example if the source is
// still loading, then delay finalizing the end of the search
} else if (source.expectMoreNodes()) {
// Since we're blocked waiting on more contacts to be provided,
// use some delay here to avoid a tight spin loop.
var delay = 250;
searchTimer = window.setTimeout(function do_search() {
doSearch(contacts, Math.min(end, contacts.length), searchText,
pattern, state);
}, delay);
return;
// Or we are complete with no results found
} else if (state.count === 0) {
showNoResults();
canReuseSearchables = false;
// Or we are complete with results that might have images to render
} else {
imgLoader.reload();
searchableNodes = state.searchables;
canReuseSearchables = true;
// If the user wished to scroll let's add the remaining results
if (blurList === true) {
searchTimer = window.setTimeout(function() {
searchTimer = null;
addRemainingResults(searchableNodes, searchPageSize);
},0);
}
}
if (typeof state.searchDoneCb === 'function') {
state.searchDoneCb();
}
}
var enableSearch = function enableSearch() {
if (searchEnabled) {
return;
}
searchEnabled = true;
// We perform the search when all the info have been loaded and the
// user wrote something in the entry field
invalidateCache();
search();
};
// Allow the main contacts list to asynchronously tell us about additional
// nodes as they are loaded.
var appendNodes = function appendNodes(nodes) {
if (!nodes || !nodes.length || !contactNodes) {
return;
}
contactNodes.push.apply(contactNodes, nodes);
// If there are no searches in progress, then we are done
if (!currentTextToSearch || !canReuseSearchables || !searchableNodes) {
return;
}
// If we have a current search then we need to determine whether the
// new nodes should show up in that search.
for (var i = 0, n = nodes.length; i < n; ++i) {
var node = nodes[i];
var nodeText = Normalizer.unescapeHTML(getSearchText(node));
if (!checkContactMatch(currentSearchTerms, nodeText)) {
searchableNodes.push({
node: node,
text: nodeText
});
}
}
};
var checkContactMatch = function checkContactMatch(searchTerms, text) {
for (var i=0, m=searchTerms.length; i < m; i++){
if (!RegExp(searchTerms[i], 'i').test(text)) {
return false;
}
}
return true;
};
var search = function performSearch(searchDoneCb) {
prevTextToSearch = currentTextToSearch;
currentTextToSearch = Normalizer.toAscii(searchBox.value.trim());
currentTextToSearch = Normalizer.escapeRegExp(currentTextToSearch);
var thisSearchText = String(currentTextToSearch);
if (thisSearchText.length === 0) {
resetState();
window.setTimeout(fillInitialSearchPage, 0, searchDoneCb);
}
else {
showProgress();
if (!searchEnabled) {
resetState();
return;
}
emptySearch = false;
// The remaining results have not been added yet
remainingPending = true;
var pattern = new RegExp(thisSearchText, 'i');
var contactsToSearch = getContactsToSearch(thisSearchText,
prevTextToSearch);
var state = {
count: 0,
searchables: [],
searchDoneCb: searchDoneCb
};
searchTimer = window.setTimeout(function do_search() {
searchTimer = null;
doSearch(contactsToSearch, 0, thisSearchText, pattern, state);
},0);
}
};
function getSearchText(contact) {
var out = '';
var uuid = contact.dataset.uuid;
if (uuid) {
out = searchTextCache[uuid];
if (!out) {
out = source.getSearchText(contact);
searchTextCache[uuid] = out;
}
}
else {
window.console.error('Search: Not uuid found for the provided node');
}
return out;
}
var getContactsDom = function contactsDom() {
if (!contactNodes) {
contactNodes = source.getNodes();
}
return contactNodes;
};
var getContactsToSearch = function getContactsToSearch(newText, prevText) {
var out;
if (canReuseSearchables && newText.length > prevText.length &&
prevText.length > 0 && newText.indexOf(prevText) === 0) {
out = searchableNodes || getContactsDom();
} else {
utils.dom.removeChildNodes(searchList);
currentSet = {};
out = getContactsDom();
canReuseSearchables = false;
}
return out;
};
var isInSearchMode = function isInSearchMode() {
return inSearchMode;
};
var invalidateCache = function s_invalidate() {
if (searchTimer) {
window.clearTimeout(searchTimer);
searchTimer = null;
}
currentTextToSearch = '';
canReuseSearchables = false;
searchableNodes = null;
contactNodes = null;
currentSet = {};
searchTextCache = {};
};
var removeContact = function s_removeContact(id) {
var contact = searchList.querySelector('li[data-uuid=\"' + id + '\"]');
searchList.removeChild(contact);
};
var selectRow = function s_selectRow(id, value) {
var check = searchList.querySelector(
'#search-view input[value="' + id + '"]');
if (check) {
check.checked = value;
}
};
function showProgress() {
searchNoResult.classList.add('hide');
searchProgress.classList.remove('hidden');
}
function showNoResults() {
searchNoResult.classList.remove('hide');
searchProgress.classList.add('hidden');
}
function hideProgressResults() {
searchNoResult.classList.add('hide');
searchProgress.classList.add('hidden');
}
exports.Search = {
'init': init,
'invalidateCache': invalidateCache,
'appendNodes': appendNodes,
'removeContact': removeContact,
'search': search,
'enterSearchMode': enterSearchMode,
'exitSearchMode': exitSearchMode,
'isInSearchMode': isInSearchMode,
'enableSearch': enableSearch,
'selectRow': selectRow,
'updateSearchList': updateSearchList
};
})(window);

View file

@ -0,0 +1,20 @@
'use strict';
/* global MozActivity */
/* exported SmsIntegration */
/* jshint nonew: false */
var SmsIntegration = {
sendSms: function si_sendSms(number) {
try {
new MozActivity({
name: 'new',
data: {
type: 'websms/sms',
target: number
}
});
} catch (e) {
console.log('WebActivities unavailable? : ' + e);
}
}
};

View file

@ -0,0 +1,49 @@
'use strict';
var utils = window.utils || {};
if (!utils.dom) {
(function(document) {
var Dom = utils.dom = {};
// remove all children of a given parent node
Dom.removeChildNodes = function(node) {
while (node.hasChildNodes()) {
node.removeChild(node.firstChild);
}
};
// Given a container, applys the class passed to all the
// nodes matching a selector.
// CAUTION: Please be careful using this function as it could
// be quite expensive depending on the container and selector.
Dom.addClassToNodes = function(container, selector, clazz) {
var nodes = container.querySelectorAll(selector);
for (var i = 0, n = nodes.length; i < n; ++i) {
nodes[i].classList.add(clazz);
}
};
// Given a container, removes the class passed to all the
// nodes matching a selector.
// CAUTION: Please be careful using this function as it could
// be quite expensive depending on the container and selector.
Dom.removeClassFromNodes = function(container, selector, clazz) {
var nodes = container.querySelectorAll(selector);
for (var i = 0, n = nodes.length; i < n; ++i) {
nodes[i].classList.remove(clazz);
}
};
Dom.updatePhoto = function(photo, dest) {
var background = '';
if (photo != null) {
background = 'url(' + URL.createObjectURL(photo) + ')';
}
dest.style.backgroundImage = background;
// Only for testing purposes
dest.dataset.photoReady = 'true';
};
})(document);
}

View file

@ -0,0 +1,37 @@
/*jshint loopfunc: true */
'use strict';
var utils = window.utils || {};
if (!utils.listeners) {
(function(document) {
var Listeners = utils.listeners = {};
Listeners.add = function(config) {
try {
for (var id in config) {
var handler = config[id];
var nodes = document.querySelectorAll(id);
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (Array.isArray(handler)) {
handler.forEach(function handle(item) {
if (!item.hasOwnProperty('event') &&
!item.hasOwnProperty('handler')) {
return;
}
node.addEventListener(item.event, item.handler);
});
} else {
node.addEventListener('click', handler);
}
} // nodes
} // Handlers
}
catch (e) {
window.console.error('Error while registering listener for: ', id, e);
}
}; // Add function
})(document);
}

View file

@ -0,0 +1,92 @@
'use strict';
if (!window.Rest) {
window.Rest = (function() {
function RestRequest(xhr) {
var cancelled = false;
this.cancel = function oncancel() {
cancelled = true;
window.setTimeout(xhr.abort.bind(xhr), 0);
};
this.isCancelled = function isCancelled() {
return cancelled;
};
}
function Rest() { }
Rest.prototype = {
get: function(uri, callback, pOptions) {
var DEFAULT_TIMEOUT = 30000;
var options = pOptions || {};
var xhr = new XMLHttpRequest({
mozSystem: true
});
var outReq = new RestRequest(xhr);
xhr.open('GET', uri, true);
var responseType = options.responseType || 'json';
xhr.responseType = responseType;
var responseProperty = responseType === 'xml' ?
'responseXML' : 'response';
xhr.timeout = options.operationsTimeout || DEFAULT_TIMEOUT;
if (!xhr.timeout || xhr.timeout === DEFAULT_TIMEOUT &&
(parent && parent.config && parent.config.operationsTimeout)) {
xhr.timeout = parent.config.operationsTimeout;
}
if (options.requestHeaders) {
for (var header in options.requestHeaders) {
xhr.setRequestHeader(header, options.requestHeaders[header]);
}
}
xhr.onload = function(e) {
if (xhr.status === 200 || xhr.status === 400 || xhr.status === 0) {
if (callback && typeof callback.success === 'function') {
setTimeout(function() {
callback.success(xhr[responseProperty]);
},0);
}
}
else {
console.error('HTTP error executing GET. ',
uri, ' Status: ', xhr.status);
if (callback && typeof callback.error === 'function') {
setTimeout(function errorHandler() {
callback.error({ status: xhr.status });
}, 0);
}
}
}; // onload
xhr.ontimeout = function(e) {
console.error('Timeout!!! while HTTP GET: ', uri);
if (callback && typeof callback.timeout === 'function') {
setTimeout(callback.timeout, 0);
}
}; // ontimeout
xhr.onerror = function(e) {
console.error('Error while executing HTTP GET: ', uri,
': ', e);
if (callback && typeof callback.error === 'function' &&
!outReq.isCancelled()) {
setTimeout(function() {
callback.error(e);
},0);
}
}; // onerror
xhr.send();
return outReq;
} // get
}; // prototype
return new Rest();
})();
}

View file

@ -0,0 +1,103 @@
/* global Promise, Event */
'use strict';
/**
* Helper library to deal with in case of emergency (ICE)
* Contacts.
* Those contacts are suppose to be first responders to
* an emergency. More info:
* https://wiki.mozilla.org/Gaia/Comms/2.1/Ice_Contacts
*
* The information will be stored in a DS for sharing
* between contacts and dialer.
* The content will be an array of two contacts ids.
* This helper will be used for fetching:
* ICEStore.getContacts().then(function(iceContacts) {
* ...
* });
* and setting the data:
* ICEStore.setContacts(ids).then(function() {
* ...
* });
*/
(function(exports) {
var STORE_NAME = 'ice_contacts';
var store = null;
var status = 'not_initialized';
var FIELD = 'ICE_CONTACTS';
// Get a reference to the store, or returns it if
// we already have.
function init() {
if (store !== null) {
return Promise.resolve(store);
}
if (!navigator.getDataStores) {
return Promise.reject('No DS supported');
}
if (status === 'initialising') {
document.addEventListener('ice_ready', function ready() {
document.removeEventListener('ice_ready', ready);
if (store === null) {
return Promise.reject('Error getting store');
} else {
return Promise.resolve(store);
}
});
}
status = 'initialising';
return new Promise(function(resolve, reject) {
navigator.getDataStores(STORE_NAME).then(function(stores) {
if (!stores || stores.length < 0) {
document.dispatchEvent(new Event('on_ice_ready'));
return reject('No DS found');
} else {
store = stores[0];
status = 'initialised';
document.dispatchEvent(new Event('ice_ready'));
return resolve(store);
}
});
});
}
var ICEStore = {
/**
* Set the in case of emergency contacts.
* @param {Array} iceContacts array of contacts id.
* @returns {Promise}
*/
setContacts: function(iceContacts) {
return init().then(function() {
return store.put(iceContacts, FIELD).then(function() {
return Promise.resolve(iceContacts);
});
});
},
/**
* Retrieves the in case of emergency contacts
* @returns {Promise}
*/
getContacts: function() {
return init().then(function() {
return store.get(FIELD);
});
},
/**
* Subscribe to changes in the store
* @param {Function} Callback when a change happens
*/
onChange: function(cb) {
return init().then(function() {
store.addEventListener('change', function() {
store.get(FIELD).then(cb);
});
});
}
};
exports.ICEStore = ICEStore;
})(window);

View file

@ -0,0 +1,207 @@
/* exported ImageLoader */
'use strict';
if (!window.ImageLoader) {
var ImageLoader = function ImageLoader(pContainer, pItems) {
var container, items, itemsSelector, lastScrollTime, scrollLatency = 100,
scrollTimer, itemHeight, total, imgsLoading = 0,
loadImage = defaultLoadImage, self = this;
init(pContainer, pItems);
/**
* Initializer
*
*/
function init(pContainer, pItems) {
itemsSelector = pItems;
container = document.querySelector(pContainer);
attachHandlers();
window.addEventListener('image-loader-resume', resuming);
window.addEventListener('image-loader-pause', unload);
load();
}
function resuming() {
window.clearTimeout(scrollTimer);
attachHandlers();
update();
}
function onUpdate(evt) {
evt.stopPropagation();
onScroll();
}
function load() {
window.clearTimeout(scrollTimer);
scrollTimer = null;
items = container.querySelectorAll(itemsSelector);
// All items have the same height
itemHeight = items[0] ? items[0].offsetHeight : 1;
total = items.length;
// Initial check if items should appear
window.setTimeout(update, 0);
}
function attachHandlers() {
container.addEventListener('scroll', onScroll);
document.addEventListener('onupdate', onUpdate);
}
function unload() {
container.removeEventListener('scroll', onScroll);
document.removeEventListener('onupdate', onUpdate);
window.clearTimeout(scrollTimer);
scrollTimer = null;
}
function setResolver(pResolver) {
loadImage = pResolver;
}
function onScroll() {
if (imgsLoading > 0) {
// Stop the pending images load
window.stop();
imgsLoading = 0;
}
// Clearing and setting a timer on every scroll event is too slow on
// some mobile devices. Therefore, set a timer once here and then
// check how long it has been since the last scroll event in the
// timer handler to determine what to do.
lastScrollTime = Date.now();
if (!scrollTimer) {
scrollTimer = window.setTimeout(updateFromScroll, scrollLatency);
}
}
function updateFromScroll() {
scrollTimer = null;
// If we scrolled more since the timer was set, then we need to
// delay again. Otherwise, go ahead and update now.
var deltaLatency = lastScrollTime - Date.now() + scrollLatency;
if (deltaLatency > 0) {
scrollTimer = window.setTimeout(updateFromScroll, deltaLatency);
} else {
update();
}
}
/**
* Loads the image contained in a DOM Element.
*/
function defaultLoadImage(item) {
var image = item.querySelector('span[data-type=img][data-src]');
if (!image) {
image = item.querySelector('img[data-src]');
if (!image) {
// Image by default
image = item.querySelector('span[data-type=img][data-group]');
if (image) {
item.dataset.visited = 'true';
}
return;
}
}
++imgsLoading;
var tmp = new Image();
var src = tmp.src = image.dataset.src;
tmp.onload = function onload() {
--imgsLoading;
image.style.backgroundImage = 'url(' + src + ')';
if (tmp.complete) {
item.dataset.visited = 'true';
}
tmp = null;
};
tmp.onabort = tmp.onerror = function onerror() {
--imgsLoading;
item.dataset.visited = 'false';
tmp = null;
};
}
/**
* Calculates the set of items are in the current viewport
*
*/
function update() {
if (total === 0) {
return;
}
var viewTop = container.scrollTop;
// Index is always inside or below viewport
var index = Math.floor(viewTop / itemHeight);
var containerHeight = container.offsetHeight;
// Goes backward
for (var i = index; i >= 0; i--) {
var item = items[i];
if (item) {
if (item.offsetTop + itemHeight < viewTop) {
break; // Over
}
if (item.dataset.visited !== 'true' &&
item.offsetTop <= viewTop + containerHeight) {
loadImage(item, self); // Inside
}
}
}
// Goes forward
for (var j = index + 1; j < total; j++) {
var theItem = items[j];
if (!theItem) {
// Returning because of index out of bound
return;
}
if (theItem.offsetTop > viewTop + containerHeight) {
return; // Below
}
if (theItem.dataset.visited !== 'true') {
loadImage(theItem, self);
}
}
} // update
function releaseImage(item) {
var image = item.querySelector('span[data-type=img][data-src]');
if (!image) {
// Image by default
image = item.querySelector('span[data-type=img][data-group]');
if (image) {
item.dataset.visited = 'false';
}
return null;
}
image.style.backgroundImage = 'none';
item.dataset.visited = 'false';
return image;
}
// It's necessary to prevent unit test errors
function destroy() {
unload();
window.removeEventListener('image-loader-pause', unload);
window.removeEventListener('image-loader-resume', resuming);
container = items = itemsSelector = lastScrollTime = scrollLatency = null;
scrollTimer = itemHeight = total = imgsLoading = loadImage = null;
}
this.reload = load;
this.unload = unload;
this.setResolver = setResolver;
this.defaultLoad = defaultLoadImage;
this.releaseImage = releaseImage;
this.destroy = destroy;
};
}

View file

@ -0,0 +1,41 @@
'use strict';
var utils = window.utils || {};
if (typeof utils.squareImage === 'undefined') {
utils.squareImage = function(blob, callback) {
var img = document.createElement('img');
var url = URL.createObjectURL(blob);
img.src = url;
function cleanupImg() {
img.src = '';
URL.revokeObjectURL(url);
}
img.onerror = cleanupImg;
img.onload = function onBlobLoad() {
var width = img.width;
var height = img.height;
if (width === height) {
cleanupImg();
callback(blob);
} else {
var canvas = document.createElement('canvas');
var min = canvas.width = canvas.height = Math.min(width, height);
var context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(img, (width - min) / 2, (height - min) / 2, min, min,
0, 0, min, min);
cleanupImg();
canvas.toBlob(function onCanvasToBlob(canvasBlob) {
context = null;
canvas.width = canvas.height = 0;
canvas = null;
callback(canvasBlob);
}, 'image/jpeg', 0.95);
}
};
}; // utils.squareImage
} // if

View file

@ -0,0 +1,77 @@
'use strict';
/* global ImageUtils, LazyLoader */
/*
* WARNING: This module LazyLoads
*
* '/shared/js/contacts/import/utilities/config.js',
* '/shared/js/image_utils.js'
*
*/
var utils = window.utils || {};
(function(utils) {
var DEPENDENCIES = [
'/shared/js/contacts/import/utilities/config.js',
'/shared/js/image_utils.js'
];
var dpr = window.devicePixelRatio || 1;
var CONFIG_FILE = '/config-images.json';
// Default data for creating a thumbnail. This will be used if a config file
// is not found
var DEFAULT_CONFIG = {
'thumbnail' : {
'format': 'image/jpeg',
'size': 65,
'quality': 1.0
}
};
var THUMB_CONFIG = DEFAULT_CONFIG.thumbnail;
if (typeof utils.thumbnailImage !== 'undefined') {
return;
}
// Obtains the thumbnail configuration by looking for the config file
// If no config file is found, then default values are taken
function getThumbsConfig() {
return new Promise(function(resolve, reject) {
utils.config.load(CONFIG_FILE).then(function resolved(config) {
resolve(config);
}, function rejected() {
resolve(DEFAULT_CONFIG);
});
});
}
function scaleImage(blob, configData) {
var thumbConfig = configData && configData.thumbnail || {};
var thumbnailEdge = (thumbConfig.size || THUMB_CONFIG.size) * dpr;
var format = thumbConfig.format || THUMB_CONFIG.format;
var encodingQuality = thumbConfig.quality || THUMB_CONFIG.quality;
return ImageUtils.resizeAndCropToCover(blob, thumbnailEdge,
thumbnailEdge, format,
encodingQuality);
}
// We keep the aspect ratio and make the smallest edge be
// |thumbnailEdge| long.
utils.thumbnailImage = function(blob, callback) {
LazyLoader.load(DEPENDENCIES, function() {
getThumbsConfig().then(function(config) {
return scaleImage(blob, config);
}).then(callback).catch(function(err) {
console.error('Error while converting image to thumbnail:', err.name);
callback(blob);
});
});
};
})(utils);

View file

@ -0,0 +1,261 @@
'use strict';
var utils = window.utils || {};
if (!utils.templates) {
(function() {
var Templates = utils.templates = {};
/**
* Returns a target HTMLElement from a selector or HTMLElement itself
*
* @param {HTMLElement or Selector} element target element.
*
* @return {HTMLElement} HTMLElement according to the selector or itself.
*
*
*/
function getTarget(element) {
var target = element;
if (!element.tagName) {
target = document.querySelector(element);
}
return target;
}
/**
* Given a target HTML element which contains a template set
* returns the template that will have to be applied over the data
*
* @param {HTMLElement} target which contains the template.
* @param {Object} data to be used on the template.
*
* @return {HTMLElement} HTMLElement with the template.
*
*/
function getTemplate(target, data) {
// Multi templates temporarily disabled to avoid eval usage
// TODO: Implement an alternative for eval
var templates = target.querySelectorAll('*[data-template]');
if (templates.length === 0) {
throw new Error('No template declared');
}
if (templates.length > 1 && templates.item(0).dataset.condition) {
throw new Error('Only one template supported in this version');
}
return {
template: templates.item(0),
isMulti: false
};
}
/**
* Returns a function used to replace data on a template
*
* @param {Object} data the data to be used on the template.
*
* @return {function} to be used.
*
*/
function templateReplace(data) {
return function(text, property) {
var out;
if (property.indexOf('.') === -1) {
out = data[property];
if (Array.isArray(out) && out.length > 0) {
out = out[0];
}
} else {
throw new Error('Dotted expressions not supported');
}
if (typeof out === 'undefined') {
out = text;
}
return out;
};
}
/**
* Adds (append or prepend) a new instance HTMLElement (or array of)
* of a template
* The template is assumed to be a child of the element
* passed as parameter
* The new element will be appended as a child
*
* @param {HTMLElement} ele container lement that contains the template
* and which will contain the new instance. Can be an HTMLElement
* or a CSS selector.
*
* @param {object or array} data with the data displayed by the template.
*
* @param {String} mode oneOf ('A','P').
*
* @return {HTMLElement} (or last element if data is an array).
*
*
*/
function add(element, data, mode, targetNode) {
// It is supported both the element itself or a selector
var target = getTarget(element);
var newElem;
var theData = [data];
if (data instanceof Array) {
theData = data;
}
// Optimization to avoid trying to find a template when
// only one is needed
var multiTemplate = true;
var template;
var idx = 0;
theData.forEach(function(oneData) {
// Pseudo-field with the index
oneData._idx_ = idx++;
// A suitable template for the data is firstly found
if (multiTemplate === true) {
var tresult = getTemplate(target, oneData);
template = tresult.template;
if (tresult.isMulti === false) {
multiTemplate = false;
}
}
if (template) {
newElem = this.render(template, oneData);
target = targetNode || target;
if (mode === 'A') {
target.appendChild(newElem);
} else if (mode === 'P') { // Append mode
if (target.firstChild) {
target.insertBefore(newElem, element.firstChild);
} else {
target.appendChild(newElem);
}
} // prepend mode
} // if template
}.bind(Templates)); // forEach data
return newElem;
}
/**
* Appends a new instance HTMLElement (or array of) of a template
*
* The template is assumed to be a child of the element passed
* as parameter
* The new element will be appended as a child
*
* @param {HTMLElement or String} ele container element that
* contains the template and which will contain the new instance.
* Can be an HTMLElement or a CSS selector.
*
* @param {object or array} data with the data displayed by the template.
*
* @return {HTMLelement} (or last element if data is an array).
*
*
*/
Templates.append = function(element, data, targetNode) {
var f = add.bind(this);
return f(element, data, 'A', targetNode);
};
/**
* Prepends a new instance (or array of) of a template
*
* The template is assumed to be a child of the element passed
* as parameter
*
* @param {HTMLElement or String} ele container element that
* contains the template and which will contain the new instance.
* Can be an HTMLElement or a CSS selector.
*
* @param {Object or Array} data with the data displayed.
*
* @return {HTMLElement} added.
*
*
*/
Templates.prepend = function(element, data, targetNode) {
var f = add.bind(this);
return f(element, data, 'P', targetNode);
};
/**
* Renders the content specified by a template with object data
*
* @param {HTMLElement} eleTemplate the template itself.
* @param {Object} data the data to be used.
*
* @return {HTMLElement} according to the template and with the data.
*
*
*/
Templates.render = function(eleTemplate, data) {
var newElem = eleTemplate.cloneNode(true);
newElem.removeAttribute('data-template');
newElem.removeAttribute('data-condition');
/* var pattern = /#(\w+)#/g; */
var pattern = /#(\w+[\w.]*)#/g;
var inner = newElem.innerHTML;
// Replace function
var replaceFunction = templateReplace(data);
var ninner = inner.replace(pattern, replaceFunction);
newElem.innerHTML = ninner;
var attrs = newElem.attributes;
var total = attrs.length;
for (var c = 0; c < total; c++) {
var val = attrs[c].value;
var nval = val.replace(pattern, replaceFunction);
newElem.setAttribute(attrs[c].name, nval);
}
if (!newElem.id) {
if (data.id) {
newElem.id = data.id;
}
}
return newElem;
};
/**
* Clears a container element
*
* @param {HTMLElement or String} element (selector or HTML element).
*
*
*/
Templates.clear = function(element) {
var target = getTarget(element);
var templates = target.querySelectorAll('*[data-template]');
target.innerHTML = '';
var total = templates.length;
for (var c = 0; c < total; c++) {
target.appendChild(templates.item(c));
}
};
}) ();
} // window.templates

195
gaia/shared/js/custom_dialog.js Executable file
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);
})();

138
gaia/shared/js/dedupe.js Executable file
View file

@ -0,0 +1,138 @@
'use strict';
(function(exports) {
/**
* The minimum length that we would consider de-duping fuzzy domain search
* segments against. e.g., touch.mozilla.com. We would use 'mozilla' as a
* deduplication key, but not 'touch', as that's only 5 characters.
*/
const MIN_SEGMENT_SIGNIFICANT_LENGTH = 6;
/**
* SearchDedupe maintains a list of web search results and uses fuzzy dedupe
* logic to remove what we would consider to be duplicates.
* This is used in the search app for example to deduplicate locally
* installed applications from web results.
*/
function SearchDedupe() {}
SearchDedupe.prototype = {
/**
* A mapping of search results to be de-duplicated via manifesURL.
*/
exactResults: {},
/**
* A mapping of search results to be de-duplicated by other than
* manifestURL. This is our strategy of de-duplicating results from
* Everything.me and locally installed apps.
*/
fuzzyResults: {},
/**
* A list of common words that we ignore when de-duping
*/
dedupeNullList: [
'mobile', 'touch'
],
/**
* The main logic which allows us to reduce the current result set.
* @param {Array} results Array of results.
* @param {String} strategy Dedupe strategy to use. Either fuzzy or exact.
*/
reduce: function(results, strategy) {
var validResults = [];
// Cache the matched dedupe IDs.
// Providers should not attempt to deduplicate against themselves.
// This should perform better and lead to less misses.
var exactDedupeIdCache = [];
var fuzzyDedupeIdCache = [];
results.forEach(function eachResult(result) {
var found = false;
var dedupeId = result.dedupeId.toLowerCase();
// Get the host of the dedupeId for the fuzzy result case
var host;
try {
host = new URL(dedupeId).host;
} catch (e) {
host = dedupeId;
}
var fuzzyDedupeIds = [host, dedupeId];
// Try to use some simple domain heuristics to find duplicates
// E.g, we would want to de-dupe between:
// m.site.org and touch.site.org, sub.m.site.org and m.site.org
// We also try to avoid deduping on second level domains by
// checking the length of the segment.
// For each part of the host, we add it to the fuzzy lookup table
// if we consider it to be significant. This algorithm is far
// from perfect, but it will likely catch 99% of our usecases.
var hostParts = host.split('.');
for (var i in hostParts) {
var part = hostParts[i];
if (part.length > MIN_SEGMENT_SIGNIFICANT_LENGTH &&
this.dedupeNullList.indexOf(part) === -1) {
fuzzyDedupeIds.push(part);
}
}
// Check if we have already rendered the result
if (strategy == 'exact') {
// Strip any querystrings from the URL
dedupeId = dedupeId.replace(/(\?.*)/g, '');
if (this.exactResults[dedupeId]) {
found = true;
}
} else {
// Handle the fuzzy matching case
// Try to match against either host or subdomain
fuzzyDedupeIds.forEach(function eachFuzzy(eachId) {
for (var i in this.fuzzyResults) {
if (i.indexOf(eachId) !== -1) {
found = true;
}
}
}, this);
}
// At the end of each iteration, cache the dedupe keys.
exactDedupeIdCache.push(dedupeId);
fuzzyDedupeIdCache = fuzzyDedupeIdCache.concat(fuzzyDedupeIds);
if (!found) {
validResults.push(result);
}
}, this);
exactDedupeIdCache.forEach(function eachFuzzy(eachId) {
this.exactResults[eachId] = true;
}, this);
fuzzyDedupeIdCache.forEach(function eachFuzzy(eachId) {
this.fuzzyResults[eachId] = true;
}, this);
return validResults;
},
/**
* Resets our current result set. No reductions will take place
* on the next reduce call.
*/
reset: function() {
this.exactResults = {};
this.fuzzyResults = {};
}
};
exports.SearchDedupe = SearchDedupe;
}(window));

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,65 @@
'use strict';
/* exported enumerateAll */
// With the removal of composite storage, this function emulates
// the composite storage enumeration (i.e. return files from
// all of the storage areas).
function enumerateAll(storages, dir, options) {
var storageIndex = 0;
var ds_cursor = null;
var cursor = {
continue: function cursor_continue() {
ds_cursor.continue();
}
};
function onsuccess(e) {
cursor.result = e.target.result;
if (!cursor.result) {
storageIndex++;
if (storageIndex < storages.length) {
enumerateNextStorage();
return;
}
// If we've run out of storages, then we fall through and call
// onsuccess with the null result.
}
if (cursor.onsuccess) {
try {
cursor.onsuccess(e);
} catch (err) {
console.warn('enumerateAll onsuccess threw', err);
}
}
}
function onerror(e) {
cursor.error = e.target.error;
if (cursor.error.name === 'NotFoundError' &&
storageIndex !== storages.length - 1) {
storageIndex++;
enumerateNextStorage();
return;
}
if (cursor.onerror) {
try {
cursor.onerror(e);
} catch (err) {
console.warn('enumerateAll onerror threw', err);
}
}
}
function enumerateNextStorage() {
// The || {} on the next line is required to make enumerate work properly
// on v1-train.
ds_cursor = storages[storageIndex].enumerate(dir, options || {});
ds_cursor.onsuccess = onsuccess;
ds_cursor.onerror = onerror;
}
enumerateNextStorage();
return cursor;
}

View file

@ -0,0 +1,33 @@
'use strict';
/* exported getStorageIfAvailable */
// Get a DeviceStorage object for the specified kind of storage and, if it
// is available, and if the specified number of bytes of storage space are
// free then asynchronously pass the DeviceStorage object to the success
// callback. Otherwise, invoke the error callback, if one is specified. If
// the error callback is called because device storage is not available, the
// argument will be a DeviceStorage status string like 'unavailable' or
// 'shared'. If the error callback is called because there is not enough
// storage space, the argument will be the number of bytes that are available.
function getStorageIfAvailable(kind, size, success, error) {
var storage = navigator.getDeviceStorage(kind);
storage.available().onsuccess = function(e) {
if (e.target.result !== 'available') {
if (error) {
error(e.target.result);
}
}
else {
storage.freeSpace().onsuccess = function(e) {
if (e.target.result < size) {
if (error) {
error(e.target.result);
}
}
else {
success(storage);
}
};
}
};
}

View file

@ -0,0 +1,37 @@
'use strict';
/* exported getUnusedFilename */
// This utility function helps avoid overwriting existing files.
// If no file with the specified name exists in the specified storage, pass
// the name to the callback. Otherwise, add a version number to the name
// and try again. Once a name is found that does not already exist, pass it
// to the callback. If the initial name 'base.ext' does not exist, then new
// names of the form 'base_n.ext', where n is a number are tried.
function getUnusedFilename(storage, name, callback) {
var getreq = storage.get(name);
getreq.onerror = function() {
// We get this error if the file does not already exist, and that
// is what we want
callback(name);
};
getreq.onsuccess = function() {
var version = 0;
var p = name.lastIndexOf('/');
var dir = name.substring(0, p + 1);
var file = name.substring(p + 1);
p = file.lastIndexOf('.');
if (p === -1) {
p = file.length;
}
var ext = file.substring(p);
var base = file.substring(0, p);
var parts = base.match(/^(.*)_(\d{1,2})$/);
if (parts) {
base = parts[1];
version = parseInt(parts[2]);
}
var newname = dir + base + '_' + (version + 1) + ext;
getUnusedFilename(storage, newname, callback);
};
}

263
gaia/shared/js/dialer/contacts.js Executable file
View file

@ -0,0 +1,263 @@
'use strict';
/* global
asyncStorage,
fb,
LazyLoader,
MozActivity,
SimplePhoneMatcher
*/
/* exported Contacts */
var Contacts = {
_FB_FILES: [
'/shared/js/fb/fb_request.js',
'/shared/js/fb/fb_data_reader.js',
'/shared/js/fb/fb_reader_utils.js'
],
// The mozContact API stores a revision of its database that allow us to know
// if we have a proper and updated contact cache.
getRevision: function getRevision(callback) {
var mozContacts = navigator.mozContacts;
if (!mozContacts) {
callback(null);
return;
}
var req = mozContacts.getRevision();
req.onsuccess = function onsuccess(event) {
callback(event.target.result);
};
req.onerror = function onerror(event) {
console.log('Error ' + event.target.error);
callback(null);
};
},
findByNumber: function findByNumber(number, callback) {
LazyLoader.load(this._FB_FILES,
this._findByNumber.bind(this, number, callback));
},
_findByNumber: function _findByNumber(number, callback) {
if (!number) {
callback(null);
return;
}
var options;
var variants;
// Based on E.164 (http://en.wikipedia.org/wiki/E.164)
// if length < 7 we're dealing with a short number
// so no need for variants
if (number.length < 7) {
variants = [number];
options = {
filterBy: ['tel'],
filterOp: 'equals',
filterValue: number
};
} else {
// get the phone number variants (for the Facebook lookup)
variants = SimplePhoneMatcher.generateVariants(number);
var sanitizedNumber = SimplePhoneMatcher.sanitizedNumber(number);
options = {
filterBy: ['tel'],
filterOp: 'match',
filterValue: sanitizedNumber
};
}
var mozContacts = navigator.mozContacts;
if (!mozContacts) {
callback(null);
return;
}
var request = mozContacts.find(options);
request.onsuccess = function findCallback() {
var contacts = request.result;
if (contacts.length === 0) {
// It is only necessary to search for one variant as FB takes care
fb.getContactByNumber(number, function fb_ready(finalContact) {
var objMatching = null;
if (finalContact) {
objMatching = {
value: number,
// Facebook telephone are always of type personal
type: 'personal',
// We don't know the carrier from FB phones
carrier: null
};
}
callback(finalContact, objMatching);
}, function fb_err(err) {
callback(null);
});
return;
}
// formatting the matches as an array (contacts) of arrays (phone numbers)
var matches = contacts.map(function getTels(contact) {
return contact.tel.map(function getNumber(tel) {
return tel.value;
});
});
// Finding the best match
var matchResult = SimplePhoneMatcher.bestMatch(variants, matches);
var contact = contacts[matchResult.bestMatchIndex];
var contactsWithSameNumber;
if (contacts.length > 1) {
contactsWithSameNumber = contacts.length - 1;
}
var matchingTel = contact.tel[matchResult.localIndex];
if (fb.isFbLinked(contact)) {
// Merge with the FB data
var req = fb.getData(contact);
req.onsuccess = function() {
callback(req.result, matchingTel, contactsWithSameNumber);
};
req.onerror = function() {
window.console.error('Error while getting FB Data');
callback(contact, matchingTel, contactsWithSameNumber);
};
}
else {
callback(contact, matchingTel, contactsWithSameNumber);
}
};
request.onerror = function findError() {
callback(null);
};
},
_mergeOneFbContact: function _mergeOneFbContact(contacts, index) {
return new Promise(function(resolve, reject) {
var fbReq = fb.getData(contacts[index]);
fbReq.onsuccess = function() {
// We don't have to merge phone numbers from FB in order to avoid
// duplicate contacts in suggesstion bar because it also searches
// matchings in facebook datastore. It means that this method
// returns mozContacts merged but without FB's phone numbers (just
// local ones)
var originalTel = contacts[index].tel;
contacts[index] = fbReq.result;
contacts[index].tel = originalTel;
resolve();
};
fbReq.onerror = function() {
console.error('Could not merge Facebook data for', contacts[index].id);
reject();
};
});
},
_mergeFbContacts: function _mergeFbContacts(contacts, callback) {
if (!callback || !(callback instanceof Function)) {
return;
}
if (!contacts) {
callback(null);
}
LazyLoader.load(this._FB_FILES, function() {
var pending = contacts.length;
var onContactMerged = function() {
if (--pending === 0) {
callback(contacts);
}
};
for (var i = 0, length = contacts.length; i < length; i++) {
if (fb.isFbContact(contacts[i])) {
this._mergeOneFbContact(contacts, i).then(onContactMerged,
onContactMerged);
} else {
onContactMerged();
}
}
}.bind(this));
},
_findContacts: function _findContacts(options, callback) {
if (!callback || !(callback instanceof Function)) {
return;
}
if (!navigator.mozContacts || !options) {
callback(null);
return;
}
var self = this;
var req = navigator.mozContacts.find(options);
req.onsuccess = function onsuccess() {
var contacts = req.result;
if (!contacts.length) {
callback(null);
return;
}
// If we have contacts data from Facebook, we need to merge it with the
// one from the Contacts API db.
self._mergeFbContacts(contacts, callback);
};
req.onerror = function onerror() {
console.error('Contact finding error. Error: ' + req.errorCode);
callback(null);
};
},
findListByNumber: function findListByNumber(number, limit, callback) {
var self = this;
asyncStorage.getItem('order.lastname', function(value) {
var sortKey = value ? 'familyName' : 'givenName';
var options = {
filterBy: ['tel'],
filterOp: 'contains',
filterValue: number,
sortBy: sortKey,
sortOrder: 'ascending',
filterLimit: limit
};
self._findContacts(options, callback);
});
},
getLength: function getLength(prop) {
if (!prop || !prop.length) {
return 0;
}
return prop.length;
},
sendEmailOrPick: function sendEmailOrPick(address) {
/* jshint nonew: false */
try {
// We don't check the email format, lets the email
// app do that
new MozActivity({
name: 'new',
data: {
type: 'mail',
URI: 'mailto:' + address
}
});
} catch (e) {
console.error('WebActivities unavailable? : ' + e);
}
}
};

View file

@ -0,0 +1,52 @@
/* exported DtmfTone */
'use strict';
/**
* DTMF tone constructor, providing the SIM on which this tone will be played
* is mandatory.
*
* @param {String} tone The tone to be played.
* @param {Boolean} short True if this will be a short tone, false otherwise.
* @param {Integer} serviceId The ID of the SIM card on which to play the tone.
*/
function DtmfTone(tone, short, serviceId) {
this.tone = tone;
this.short = short;
this.serviceId = serviceId;
this.timer = 0;
}
/**
* Length of a short DTMF tone, currently 120ms.
*/
DtmfTone.kShortToneLength = 120;
DtmfTone.prototype = {
/**
* Starts playing the tone, if this is a short tone it will stop automatically
* after kShortToneLength milliseconds, otherwise it will play until stopped.
*/
play: function dt_play() {
clearTimeout(this.timer);
// Stop previous tone before dispatching a new one
navigator.mozTelephony.stopTone(this.serviceId);
navigator.mozTelephony.startTone(this.tone, this.serviceId);
if (this.short) {
this.timer = window.setTimeout(function dt_stopTone(serviceId) {
navigator.mozTelephony.stopTone(serviceId);
}, DtmfTone.kShortToneLength, this.serviceId);
}
},
/**
* Stop the DTMF tone, this is safe to call even if the DTMF tone has already
* stopped.
*/
stop: function dt_stop() {
clearTimeout(this.timer);
navigator.mozTelephony.stopTone(this.serviceId);
}
};

View file

@ -0,0 +1,152 @@
/* globals FontSizeUtils */
/* exported FontSizeManager */
'use strict';
var FontSizeManager = (function fontSizeManager() {
/**
* Possible scenarios in which the dialer and call screen can be in.
*/
var DIAL_PAD = 0,
SINGLE_CALL = 1,
CALL_WAITING = 2,
STATUS_BAR = 3,
SECOND_INCOMING_CALL = 4;
/**
* The following font sizes are in rems.
*/
var _sizes = {};
_sizes[DIAL_PAD] = {min: 2.6, max: 4.1};
_sizes[SINGLE_CALL] = {min: 2.3, max: 3.4, line: 3.4};
_sizes[CALL_WAITING] = {min: 2.3, max: 2.5, line: 1.3};
_sizes[STATUS_BAR] = {min: 1.7, max: 1.7};
_sizes[SECOND_INCOMING_CALL] = {min: 2.3, max: 2.5, line: 3.7};
var _defaultFontSize;
function _getRootFontSize() {
_defaultFontSize = _defaultFontSize ||
parseInt(window.getComputedStyle(document.body, null).
getPropertyValue('font-size'));
return _defaultFontSize;
}
function _getMinFontSize(scenario) {
return Math.round(_sizes[scenario].min * _getRootFontSize());
}
function _getMaxFontSize(scenario) {
return Math.round(_sizes[scenario].max * _getRootFontSize());
}
// Returns an array containing each number between scenario.minsize and
// scenario.maxsize in steps of 4,
// e.g.[minsize, minsize + 4, minsize + 8, ..., maxsize]
function _getAllowedFontSizes(scenario) {
var minFontSize = _getMinFontSize(scenario);
var maxFontSize = _getMaxFontSize(scenario);
var allowedSizes = [];
for (var size = minFontSize; size <= maxFontSize; size +=4) {
allowedSizes.push(size);
}
if (allowedSizes.indexOf(maxFontSize) === -1) {
allowedSizes.push(maxFontSize);
}
return allowedSizes;
}
/* The font size of the view will be modified to fit its width. The scenario
provides the sizing contraints.
If the content is still too large within the size contraints, it will add
an ellipsis */
function adaptToSpace(scenario, view, forceMaxFontSize, ellipsisSide) {
// Bug 1082139 - JavascriptException: JavascriptException: TypeError:
// window.getComputedStyle(...) is null at://
// app://callscreen.gaiamobile.org/gaia_build_defer_index.js line: 146
var computedStyle = window.getComputedStyle(view);
if ((!view.value && !view.textContent) || !computedStyle) {
// We don't care about the font size of empty views
return;
}
var viewWidth = view.getBoundingClientRect().width;
var viewFont = computedStyle.fontFamily;
var allowedSizes;
if (forceMaxFontSize) {
allowedSizes = [_getMaxFontSize(scenario)];
} else {
allowedSizes = _getAllowedFontSizes(scenario);
}
var infos = FontSizeUtils.getMaxFontSizeInfo(
view.value || view.textContent, allowedSizes, viewFont, viewWidth);
var newFontSize = infos.fontSize;
if (view.style.fontSize !== newFontSize) {
view.style.fontSize = newFontSize + 'px';
}
if (infos && infos.overflow) {
var overflowCount = FontSizeUtils.getOverflowCount(
view.value || view.textContent, newFontSize, viewFont, viewWidth);
// Add two characters to the overflowcount to account for "…" width
overflowCount += 2;
_useEllipsis(view, overflowCount, ellipsisSide);
view.dataset.ellipsedCharacters = overflowCount;
} else {
view.dataset.ellipsedCharacters = 0;
}
}
function _useEllipsis(view, overflowCount, ellipsisSide) {
var side = ellipsisSide || 'begin';
var localizedSide = (side === 'begin' ? 'left' : 'right');
var value = view.value || view.textContent;
if (localizedSide == 'left') {
value = '\u2026' + value.substr(-value.length + overflowCount);
} else if (localizedSide == 'right') {
value = value.substr(0, value.length - overflowCount) + '\u2026';
}
if (view.value) {
view.value = value;
} else {
var el = view.querySelector('bdi') || view;
el.textContent = value;
}
}
function ensureFixedBaseline(scenario, view) {
var initialLineHeight = _sizes[scenario].line;
if (!initialLineHeight) {
return;
}
var maxFontSize = _getMaxFontSize(scenario);
var fontSize = parseInt(window.getComputedStyle(view).fontSize, 10);
// Taking the line height for the maximum font size case for each
// scenario as the reference, we need to adjust it adding half of
// the decrease to the final calculated font size in pixels.
view.style.lineHeight = initialLineHeight * _getRootFontSize() +
(maxFontSize - fontSize) / 2 + 'px';
}
function resetFixedBaseline(view) {
view.style.removeProperty('line-height');
}
return {
DIAL_PAD: DIAL_PAD,
SINGLE_CALL: SINGLE_CALL,
CALL_WAITING: CALL_WAITING,
STATUS_BAR: STATUS_BAR,
SECOND_INCOMING_CALL: SECOND_INCOMING_CALL,
adaptToSpace: adaptToSpace,
ensureFixedBaseline: ensureFixedBaseline,
resetFixedBaseline: resetFixedBaseline
};
})();

963
gaia/shared/js/dialer/keypad.js Executable file
View file

@ -0,0 +1,963 @@
/* exported KeypadManager */
/* globals AddContactMenu, CallHandler, CallLogDBManager, CallsHandler,
CallScreen, ConfirmDialog, CustomDialog, DtmfTone, FontSizeManager,
LazyLoader, MultiSimActionButton, Promise, SimSettingsHelper,
SettingsListener, TonePlayer */
'use strict';
// Frequencies coming from http://en.wikipedia.org/wiki/Telephone_keypad
var gTonesFrequencies = {
'1': [697, 1209], '2': [697, 1336], '3': [697, 1477],
'4': [770, 1209], '5': [770, 1336], '6': [770, 1477],
'7': [852, 1209], '8': [852, 1336], '9': [852, 1477],
'*': [941, 1209], '0': [941, 1336], '#': [941, 1477]
};
var KeypadManager = {
kMaxDigits: 50,
_phoneNumber: '',
_onCall: false,
_keypadSoundIsEnabled: false,
_shortTone: false,
_vibrationEnabled: false,
// Keep in sync with Lockscreen and keyboard vibration
kVibrationDuration: 50, // ms
// Due to the ellipsis capabilities of the number view, the cursor position
// in the input element, does not match the point where new digits should be
// added to the current number or where the deletion operation should take
// place. This property holds the position of the edition point for the
// current number (_phoneNumber property).
_insertPosition: null,
onValueChanged: null,
get phoneNumberView() {
delete this.phoneNumberView;
this.phoneNumberView = document.getElementById('phone-number-view');
return this.phoneNumberView;
},
get phoneNumberViewContainer() {
delete this.phoneNumberViewContainer;
this.phoneNumberViewContainer =
document.getElementById('phone-number-view-container');
return this.phoneNumberViewContainer;
},
get keypad() {
delete this.keypad;
this.keypad = document.getElementById('keypad');
return this.keypad;
},
get callBar() {
delete this.callBar;
this.callBar =
document.getElementById('keypad-callbar');
return this.callBar;
},
get hideBar() {
delete this.hideBar;
this.hideBar = document.getElementById('keypad-hidebar');
return this.hideBar;
},
get callBarAddContact() {
delete this.callBarAddContact;
this.callBarAddContact =
document.getElementById('keypad-callbar-add-contact');
return this.callBarAddContact;
},
get callBarCallAction() {
delete this.callBarCallAction;
this.callBarCallAction =
document.getElementById('keypad-callbar-call-action');
return this.callBarCallAction;
},
get callBarCancelAction() {
delete this.callBarCancelAction;
this.callBarCancelAction =
document.getElementById('keypad-callbar-cancel');
return this.callBarCancelAction;
},
get deleteButton() {
delete this.deleteButton;
this.deleteButton = document.getElementById('keypad-delete');
return this.deleteButton;
},
get hideBarHangUpAction() {
delete this.hideBarHangUpAction;
this.hideBarHangUpAction =
document.getElementById('keypad-hidebar-hang-up-action-wrapper');
return this.hideBarHangUpAction;
},
get hideBarHideAction() {
delete this.hideBarHideAction;
this.hideBarHideAction =
document.getElementById('keypad-hidebar-hide-keypad-action');
return this.hideBarHideAction;
},
multiSimActionButton: null,
/**
* Initializes the keypad manager, registers all the appropriate event
* handlers and instances the necessary sound infrastructure so that the
* keypad will be fully functional once this method has been called.
*
* @param oncall {Boolean} True if the keypad manager will be used during a
* call and will play sounds on the "telephony" channel. False if it
* will be used outside of a call and should use the "content" channel
* instead. We should be using the "notification" channel but we can't
* due to bug 1092346.
*/
init: function kh_init(oncall) {
this._onCall = !!oncall;
this._lastPressedKey = null;
this.phoneNumberView.value = '';
this.phoneNumberView
.addEventListener('click', this._updateInsertPosition.bind(this));
this._phoneNumber = '';
var keyHandler = this.keyHandler.bind(this);
this.keypad.addEventListener('contextmenu', keyHandler);
this.keypad.addEventListener('touchstart', keyHandler, true);
this.keypad.addEventListener('touchmove', keyHandler, true);
this.keypad.addEventListener('touchend', keyHandler, true);
this.keypad.addEventListener('touchcancel', keyHandler, true);
this.deleteButton.addEventListener('touchstart', keyHandler);
this.deleteButton.addEventListener('touchend', keyHandler);
// The keypad add contact bar is only included in the normal version of
// the keypad.
if (this.callBarAddContact) {
this.callBarAddContact.addEventListener('click',
this.addContact.bind(this));
}
// The keypad call bar is only included in the normal version and
// the emergency call version of the keypad.
if (this.callBarCallAction) {
if (typeof MultiSimActionButton !== 'undefined') {
this.multiSimActionButton =
new MultiSimActionButton(this.callBarCallAction,
CallHandler.call,
'ril.telephony.defaultServiceId',
this.phoneNumber.bind(this));
}
this.callBarCallAction.addEventListener('click',
this.fetchLastCalled.bind(this));
}
// The keypad cancel bar is only the emergency call version of the keypad.
if (this.callBarCancelAction) {
this.callBarCancelAction.addEventListener('click', function() {
window.parent.LockScreen.switchPanel();
});
}
// The keypad hide bar is only included in the on call version of the
// keypad.
if (this.hideBarHideAction) {
this.hideBarHideAction.addEventListener('click',
this.callbarBackAction);
}
if (this.hideBarHangUpAction) {
this.hideBarHangUpAction.addEventListener('click',
this.hangUpCallFromKeypad);
}
// According to the UX sound spec, we should use the "system" type for the
// dialer pad. See the attachment of the bug1068219.
TonePlayer.init('system');
this.render();
LazyLoader.load(['/shared/style/action_menu.css',
'/dialer/js/suggestion_bar.js']);
this._observePreferences();
},
render: function hk_render(layoutType) {
if (layoutType == 'oncall') {
if (CallsHandler.activeCall) {
var activeCall = CallsHandler.activeCall.call;
this._phoneNumber = activeCall.id.number;
}
this._isKeypadClicked = false;
this.phoneNumberViewContainer.classList.add('keypad-visible');
if (this.callBar) {
this.callBar.classList.add('hide');
}
if (this.hideBar) {
this.hideBar.classList.remove('hide');
}
this.deleteButton.classList.add('hide');
} else {
this.phoneNumberViewContainer.classList.remove('keypad-visible');
if (this.callBar) {
this.callBar.classList.remove('hide');
}
if (this.hideBar) {
this.hideBar.classList.add('hide');
}
this.deleteButton.classList.remove('hide');
}
},
phoneNumber: function hk_phoneNumber() {
return this._phoneNumber;
},
fetchLastCalled: function hk_fetchLastCalled() {
if (this._phoneNumber !== '') {
return;
}
var self = this;
CallLogDBManager.getGroupAtPosition(1, 'lastEntryDate', true, 'dialing',
function hk_ggap_callback(result) {
if (result && (typeof result === 'object') && result.number) {
self.updatePhoneNumber(result.number);
}
}
);
},
addContact: function hk_addContact(event) {
var number = this._phoneNumber;
if (!number) {
return;
}
LazyLoader.load(['/dialer/js/add_contact_menu.js'], function() {
AddContactMenu.show(number);
});
},
callbarBackAction: function hk_callbarBackAction(event) {
CallScreen.hideKeypad();
},
hangUpCallFromKeypad: function hk_hangUpCallFromKeypad(event) {
CallScreen.hideKeypad();
CallsHandler.end();
},
_lastPressedKey: null,
_dtmfTone: null,
_playDtmfTone: function kh_playDtmfTone(key) {
var serviceId = 0;
if (!this._onCall) {
return;
}
if (CallsHandler.activeCall) {
// Single call
serviceId = CallsHandler.activeCall.call.serviceId;
} else {
// Conference call
serviceId = navigator.mozTelephony.active.calls[0].serviceId;
}
if (this._dtmfTone) {
this._dtmfTone.stop();
this._dtmfTone = null;
}
this._dtmfTone = new DtmfTone(key, this._shortTone, serviceId);
this._dtmfTone.play();
},
_stopDtmfTone: function kh_stopDtmfTone() {
if (!this._dtmfTone) {
return;
}
this._dtmfTone.stop();
this._dtmfTone = null;
},
/**
* Function used to respond to touchstart events over the keypad. Reacts to
* the first key that has been pressed by playing the appropriate tone and
* sets up the necessary timers to react to long presses.
*
* @param {String} key The key that was hit by this touchstart event.
*/
_touchStart: function kh_touchStart(key) {
this._longPress = false;
this._lastPressedKey = key;
// If user input number more 50 digits, app shouldn't accept.
// The limit only applies while not on a call - there is no
// limit while on a call (bug 917630).
if (key !== 'delete' && !this._onCall &&
(this._phoneNumber.length >= this.kMaxDigits)) {
return;
}
if (key != 'delete') {
if (this._keypadSoundIsEnabled) {
// We do not support long press if not on a call
TonePlayer.start(
gTonesFrequencies[key], !this._onCall || this._shortTone);
}
if (this._vibrationEnabled) {
navigator.vibrate(this.kVibrationDuration);
}
this._playDtmfTone(key);
}
// Manage long press
if (((key == '0' || key == '*') && !this._onCall) || key == 'delete') {
this._holdTimer = setTimeout(function(self) {
self.restoreCaretPosition();
if (key == 'delete') {
self._clearPhoneNumber();
} else if (key === '0') {
self._replaceLastKey('+');
} else if (key === '*') {
var isAtTheEnd = self._insertPosition === null;
var isFirst = self._insertPosition === 1 ||
(isAtTheEnd && self._phoneNumber.length === 1);
// ',' DTMF separator can't be the first
if (!isFirst) {
self._replaceLastKey(',');
} else {
self._deleteAtInsertPosition();
}
}
self._longPress = true;
self._updatePhoneNumberView('begin', false);
}, 400, this);
}
// Voicemail long press (only if first digit pressed)
if (key === '1' && this._phoneNumber === '') {
this._holdTimer = setTimeout(function vm_call(self) {
self._longPress = true;
self._callVoicemail();
self._phoneNumber = '';
self._updatePhoneNumberView('begin', false);
}, 400, this);
}
if (key == 'delete') {
this._deleteAtInsertPosition();
} else if (this.phoneNumberViewContainer.classList.
contains('keypad-visible')) {
if (!this._isKeypadClicked) {
this._isKeypadClicked = true;
this._phoneNumber = key;
this.replaceAdditionalContactInfo('');
} else {
this._insertAtCaret(key);
}
} else {
this._insertAtCaret(key);
}
// Per certification requirements abbreviated dialing codes need to be
// called immediately after the user enters the '#' key. This covers
// retrieving the device's IMEI codes as well as speed dialing.
if (key === '#' && !this._onCall) {
if (this._phoneNumber === '*#06#') {
this.multiSimActionButton.performAction();
return;
} else if (this._isSpeedDialNumber(this._phoneNumber)) {
// Remove the trailing '#' to get the index
this._handleSpeedDial(this._phoneNumber.slice(0, -1));
return;
}
}
setTimeout(function(self) {
self._updatePhoneNumberView('begin', false);
}, 0, this);
},
/**
* Function used to respond to touchmove events over the keypad. Stops playing
* the tone associated with the last pressed key and resets it if the target
* goes outside its area.
*
* @param {Object} touch Touch position object for this move.
*/
_touchMove: function kh_touchMove(touch) {
var target = document.elementFromPoint(touch.pageX, touch.pageY);
var key = (target && target.dataset) ? target.dataset.value : null;
if (key !== this._lastPressedKey || key === 'delete') {
this._stopDtmfTone();
}
},
/**
* Function used to respond to touchend events over the keypad. Stops playing
* tones and resets timers associated with the key press.
*
* @param {String} key The key over which the tap finished.
*/
_touchEnd: function kh_touchEnd(key) {
this._lastPressedKey = null;
if (key !== 'delete') {
this._stopDtmfTone();
}
if (this._keypadSoundIsEnabled) {
TonePlayer.stop();
}
this.restoreCaretPosition();
// If it was a long press our work is already done
if (this._longPress) {
this._longPress = false;
this._holdTimer = null;
return;
}
if (this._holdTimer) {
clearTimeout(this._holdTimer);
}
},
keyHandler: function kh_keyHandler(event) {
// Avoid the keys to get focus.
event.preventDefault();
// When long pressing on the voicemail button, if a menu pops up on top of
// the 1 button, a click will go through and target that button unless we
// preventDefault the contextmenu event.
if (event.type == 'contextmenu') {
return;
}
var key = event.target.dataset.value;
// We could receive this event from an element that
// doesn't have the dataset value. Got the last key
// pressed and assing this value to continue with the
// proccess.
if (!key) {
return;
}
event.stopPropagation();
switch (event.type) {
case 'touchstart':
if (this._lastPressedKey) {
return; // Ignore multi-taps
}
event.target.classList.add('active');
this._touchStart(key);
break;
case 'touchmove':
this._touchMove(event.touches[0]);
break;
case 'touchend':
case 'touchcancel':
if (key !== this._lastPressedKey) {
return; // Ignore multi-taps
}
event.target.classList.remove('active');
this._touchEnd(key);
break;
}
},
/**
* Returns true if the number is a speed dial code as described in
* 3GPP TS 22.030 6.6.4. Speed dial codes are in the N(N)(N)# format.
*/
_isSpeedDialNumber: function(number) {
return !!number.match(/^[0-9][0-9]{0,2}\#$/);
},
/**
* Returns the telephony number corresponding to the specified index. Speed
* dial numbers are retrieved from the SIM contacts list and not from the
* regular contacts.
*
* @param {Integer} index The index of the speed dial number.
* @returns {Promise} A promise that resolves to the corresponding number.
*/
_getSpeedDialNumber: function(index) {
var self = this;
var cardIndex;
index--; // Speed dial indexes are 1-based
return new Promise(function(resolve, reject) {
LazyLoader.load(['/shared/js/sim_settings_helper.js'], function() {
SimSettingsHelper.getCardIndexFrom('outgoingCall',
function(defaultCardIndex) {
if (defaultCardIndex == SimSettingsHelper.ALWAYS_ASK_OPTION_VALUE) {
LazyLoader.load(['/shared/js/component_utils.js',
'/shared/elements/gaia_sim_picker/script.js'],
function() {
var simPicker = document.getElementById('sim-picker');
simPicker.getOrPick(defaultCardIndex, null,
function(pickedCardIndex) {
cardIndex = pickedCardIndex;
resolve();
});
});
} else {
cardIndex = defaultCardIndex;
resolve();
}
});
});
}).then(function() {
return self._getSimContactsList(cardIndex).then(
function(simContactsList) {
if ((index >= 0) && (index < simContactsList.length)) {
return simContactsList[index].number;
} else {
return Promise.reject('noContactsWereFound');
}
});
});
},
/**
* Handles a speed dial number, replaces the phone number with the one
* at the specified index or displays an error message if none was found.
*
* @param {Integer} index The index of the speed dial number.
*/
_handleSpeedDial: function(index) {
var self = this;
this.updatePhoneNumber('', 'begin', false);
this._getSpeedDialNumber(+index).then(
function(number) {
self.updatePhoneNumber(number, 'begin', false);
}, function(error) {
/* Do not display an error message if the user explicitly
* cancelled the speed dial operation. */
if (error) {
ConfirmDialog.show(error, null, {
title: 'noContactsFoundDialogOk',
callback: ConfirmDialog.hide
});
}
});
},
/**
* Creates an array of contacts populated using the ADN contacts retrieved
* from a SIM card. Every contact will contain only the ID and first
* telephone number and the array will be sorted by ID. This array is then
* suitable to be used to pick speed dial numbers.
*
* @param {Array} contacts An array of mozContact elements.
* @returns {Array} An array of telephone numbers / ID couples sorted by ID.
*/
_createSimContactList: function(contacts) {
var numbers = new Array(contacts.length);
for (var i = 0; i < contacts.length; i++) {
numbers[i] = {
id: contacts[i].id,
number: contacts[i].tel[0].value,
};
}
numbers.sort(function(a, b) {
if (a.id.length == b.id.length) {
return (a.id > b.id) ? 1 : 0;
} else {
return (a.id.length > b.id.length) ? 1 : 0;
}
});
return numbers;
},
/**
* Gets the SIM contacts list for the specified SIM card.
*
* @param {Integer} cardIndex The SIM card index.
* @returns {Promise} A promise that is resolved with the contacts list.
*/
_getSimContactsList: function(cardIndex) {
var self = this;
var canceled = false;
return new Promise(function(resolve, reject) {
LazyLoader.load(['/shared/style/confirm.css',
'/shared/js/confirm.js',
document.getElementById('confirmation-message')],
function() {
var iccId = navigator.mozIccManager.iccIds[cardIndex];
var icc = navigator.mozIccManager.getIccById(iccId);
if (!icc) {
reject('noSimCardToLoadContactsFrom');
return;
}
var req = icc.readContacts('adn');
req.onsuccess = function(event) {
var adnContacts = event.target.result;
var contacts = self._createSimContactList(adnContacts);
if (!canceled) {
ConfirmDialog.hide();
}
resolve(contacts);
};
req.onerror = function(error) {
console.error('Could not retrieve the ADN contacts from SIM card ' +
cardIndex + ', got error ' + error.name);
reject('noContactsWereFound');
};
ConfirmDialog.show('loadingSimContacts', null, {
title: 'cancel',
callback: function() {
canceled = true;
ConfirmDialog.hide();
reject();
}
});
}
);
});
},
sanitizePhoneNumber: function(number) {
return number.replace(/\s+/g, '');
},
updatePhoneNumber: function kh_updatePhoneNumber(number, ellipsisSide,
forceMaxFontSize) {
number = this.sanitizePhoneNumber(number);
this._phoneNumber = number;
this._updatePhoneNumberView(ellipsisSide, forceMaxFontSize);
},
press: function(value) {
this._playDtmfTone(value);
TonePlayer.start(gTonesFrequencies[value], true);
setTimeout((function nextTick() {
TonePlayer.stop();
this._stopDtmfTone();
}).bind(this));
},
_updatePhoneNumberView: function kh_updatePhoneNumberview(ellipsisSide,
forceMaxFontSize) {
var phoneNumber = this._phoneNumber;
// If there are digits in the phone number, show the delete button
// and enable the add contact button
if (this._onCall) {
this.renderPhoneNumber(phoneNumber, ellipsisSide);
} else {
var visibility;
if (phoneNumber.length > 0) {
visibility = 'visible';
this.callBarAddContact.removeAttribute('disabled');
} else {
visibility = 'hidden';
this.callBarAddContact.setAttribute('disabled', 'disabled');
}
this.deleteButton.style.visibility = visibility;
this.phoneNumberView.blur(); // update avoiding caret flickering
this.phoneNumberView.value = phoneNumber;
FontSizeManager.adaptToSpace(
FontSizeManager.DIAL_PAD, this.phoneNumberView, forceMaxFontSize,
ellipsisSide);
}
if (this.onValueChanged) {
this.onValueChanged(this._phoneNumber);
}
},
renderPhoneNumber:
function kh_replacePhoneNumber(phoneNumber, ellipsisSide) {
if (this._onCall && CallsHandler.activeCall) {
CallsHandler.activeCall.
renderPhoneNumber({raw: phoneNumber} , ellipsisSide);
}
},
restorePhoneNumber:
function kh_restorePhoneNumber() {
if (this._onCall && CallsHandler.activeCall) {
CallsHandler.activeCall.restorePhoneNumber();
}
},
replaceAdditionalContactInfo:
function kh_replaceAdditionalContactInfo(additionalContactInfo) {
var call = CallsHandler.activeCall;
if (this._onCall && call) {
call.replaceAdditionalContactInfo(additionalContactInfo);
}
},
restoreAdditionalContactInfo: function kh_restoreAdditionalContactInfo() {
if (this._onCall && CallsHandler.activeCall) {
CallsHandler.activeCall.restoreAdditionalContactInfo();
}
},
_callVoicemail: function() {
if (navigator.mozIccManager.iccIds.length <= 1) {
this._callVoicemailForSim(0);
return;
}
var self = this;
var key = 'ril.voicemail.defaultServiceId';
var req = navigator.mozSettings.createLock().get(key);
req.onsuccess = function() {
LazyLoader.load(['/shared/js/component_utils.js',
'/shared/elements/gaia_sim_picker/script.js'],
function() {
var simPicker = document.getElementById('sim-picker');
navigator.mozL10n.formatValue('voiceMail').then(string => {
simPicker.getOrPick(req.result[key], string,
self._callVoicemailForSim.bind(self));
});
});
};
},
_callVoicemailForSim: function(cardIndex) {
var settings = navigator.mozSettings;
if (!settings) {
return;
}
var transaction = settings.createLock();
var request = transaction.get('ril.iccInfo.mbdn');
request.onsuccess = (function() {
var numbers = request.result['ril.iccInfo.mbdn'];
var number;
if (typeof numbers == 'string') {
number = numbers;
} else {
number = numbers && numbers[cardIndex];
}
var voicemail = navigator.mozVoicemail;
if (!number && voicemail) {
number = voicemail.getNumber(cardIndex);
}
if (number) {
CallHandler.call(number, cardIndex);
} else {
this._showNoVoicemailDialog();
}
}).bind(this);
request.onerror = function() {};
},
_showNoVoicemailDialog: function hk_showNoVoicemailDialog() {
var voicemailDialog = {
title: 'voicemailNoNumberTitle',
text: 'voicemailNoNumberText',
confirm: {
title: 'voicemailNoNumberSettings',
recommend: true,
callback: this.showVoicemailSettings
},
cancel: {
title: 'voicemailNoNumberCancel',
callback: this._hideNoVoicemailDialog
}
};
LazyLoader.load(['/shared/js/custom_dialog.js'], function() {
CustomDialog.show(
voicemailDialog.title, voicemailDialog.text,
voicemailDialog.cancel, voicemailDialog.confirm);
});
},
_hideNoVoicemailDialog: function kh_hideNoVoicemailDialog() {
CustomDialog.hide();
},
showVoicemailSettings: function kh_showVoicemailSettings() {
var activity = new window.MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'call'
}
});
activity.onerror = function() {
console.warn('Configure activity error:', activity.error.name);
};
},
_observePreferences: function kh_observePreferences() {
var self = this;
LazyLoader.load('/shared/js/settings_listener.js', function() {
SettingsListener.observe('phone.ring.keypad', false, function(value) {
self._keypadSoundIsEnabled = !!value;
});
SettingsListener.observe('phone.dtmf.type', 'long', function(value) {
self._shortTone = (value === 'short');
});
SettingsListener.observe('keyboard.vibration', false, function(value) {
self._vibrationEnabled = !!value;
});
});
},
_updateInsertPosition: function kh_updateInsertPosition() {
this._insertPosition = this._realStartPosition();
},
_replaceLastKey: function kh_replaceLastKey(newKey) {
this._deleteAtInsertPosition();
this.restoreCaretPosition();
this._insertAtCaret(newKey);
},
_insertAtCaret: function kh_insertAtCaret(key) {
if (this._insertPosition === null) {
this._phoneNumber += key;
} else {
var start = this._realStartPosition();
var end = this._realEndPosition();
this._phoneNumber = this._phoneNumber.substring(0, start) + key +
this._phoneNumber.substring(end);
this._insertPosition = start + 1;
}
},
_deleteAtInsertPosition: function kh_deleteAtInsertPosition() {
if (this._insertPosition === null) {
this._phoneNumber = this._phoneNumber.slice(0, -1);
} else {
var start = this._realStartPosition();
var end = this._realEndPosition();
if (start > 0 && start === end) {
start = end - 1;
}
this._phoneNumber = this._phoneNumber.substring(0, start) +
this._phoneNumber.substring(end);
this._insertPosition = this._phoneNumber ? start : null;
}
},
_clearPhoneNumber: function kh_clearPhoneNumber() {
if (this._insertPosition === null) {
this._phoneNumber = '';
} else {
var start = this._realStartPosition();
this._phoneNumber = this._phoneNumber.substring(start);
this._insertPosition = this._phoneNumber ? 0 : null;
}
},
/**
* Sets the caret position inside the phone number input of the dialer
* according to the insert position (which is the real place where the caret
* should be inside the complete number) taking into account the number of
* ellipsed characters.
*/
restoreCaretPosition: function kh_restoreCaretPosition() {
if (this._insertPosition !== null) {
var caretPosition = this._caretPosition(this._insertPosition);
this.phoneNumberView.selectionStart = caretPosition;
this.phoneNumberView.selectionEnd = caretPosition;
this.phoneNumberView.focus();
} else {
this.phoneNumberView.blur();
}
},
/**
* Gets the real start index for a selection in the phone view considering
* the ellipsed characters.
*/
_realStartPosition: function kh_realStartPosition() {
var start = this.phoneNumberView.selectionStart;
return this._realPosition(this.phoneNumberView, start);
},
/**
* Gets the real end index for a selection in the phone view considering
* the ellipsed characters.
*/
_realEndPosition: function kh_realEndPosition() {
var end = this.phoneNumberView.selectionEnd;
return this._realPosition(this.phoneNumberView, end);
},
/**
* Gets the position where the cursor should be placed inside the phone
* view (considering the ellipsed scenaries) from the real edition point.
*/
_caretPosition: function kh_caretPosition(realPosition) {
return realPosition - this._ellipsisOffset(this.phoneNumberView);
},
/**
* Utility to calculate the real cursor position given an input with
* ellipsed characters and a position inside the current value.
*/
_realPosition: function kh_realPosition(input, position) {
return position + this._ellipsisOffset(this.phoneNumberView);
},
/**
* Calculates the amount of characters that are not visible in the current
* view. This includes all the ellipsed characters and the ellipsis
* character itself.
*/
_ellipsisOffset: function kh_ellipsisOffset(input) {
var ellipsedCharacters = +input.dataset.ellipsedCharacters;
return ellipsedCharacters ? (ellipsedCharacters - 1) : 0;
}
};

View file

@ -0,0 +1,134 @@
'use strict';
/* globals ConfirmDialog, LazyLoader */
/* exported TelephonyMessages */
var TelephonyMessages = {
/**
* Constants for what message to display when we get a bad number error.
* Depends on the situation.
*/
// Display the regular "invalid number" message, as this is a regular call.
REGULAR_CALL: 0,
// We have no network and/or no SIM, so we can only make emergency calls.
// Display the "no network" message.
NO_NETWORK: 1,
// We are making a call from an emergency call view, so calls can only be
// placed to emergency and ICE numbers. Display the "not an emergency number"
// message.
EMERGENCY_ONLY: 2,
displayMessage: function(message, number) {
var dialogTitle, dialogBody;
switch (message) {
case 'BadNumber':
dialogTitle = 'invalidNumberToDialTitle';
dialogBody = 'invalidNumberToDialMessage';
break;
case 'FlightMode':
dialogTitle = 'callAirplaneModeTitle';
dialogBody = 'callAirplaneModeMessage';
break;
case 'NoNetwork':
dialogTitle = 'emergencyDialogTitle';
dialogBody = 'emergencyDialogBodyBadNumber';
break;
case 'DeviceNotAccepted':
dialogTitle = 'emergencyDialogTitle';
dialogBody = 'emergencyDialogBodyDeviceNotAccepted';
break;
case 'UnableToCall':
dialogTitle = 'unableToCallTitle';
dialogBody = 'unableToCallMessage';
break;
case 'NumberIsBusy':
dialogTitle = 'numberIsBusyTitle';
dialogBody = 'numberIsBusyMessage';
break;
case 'FixedDialingNumbers':
dialogTitle = 'fdnIsActiveTitle';
dialogBody = 'fdnIsActiveMessage';
break;
case 'OtherConnectionInUse':
dialogTitle = 'otherConnectionInUseTitle';
dialogBody = 'otherConnectionInUseMessage';
break;
case 'OperatorDeterminedBarring':
dialogTitle = 'unableToCallTitle';
dialogBody = 'callBarredByTheOperatorMessage';
break;
case 'EmergencyCallOnly':
dialogTitle = 'emergency-call-only';
dialogBody = 'emergency-call-error';
break;
default:
console.error('Invalid message argument'); // Should never happen
return;
}
LazyLoader.load(['/shared/style/confirm.css',
'/shared/js/confirm.js',
document.getElementById('confirmation-message')],
function() {
ConfirmDialog.show(
dialogTitle,
{id: dialogBody, args: {number: number}},
{
title: 'emergencyDialogBtnOk', // Just 'ok' would be better.
callback: function() {
ConfirmDialog.hide();
}
}
);
}
);
},
handleDisconnect: function(reason, number, emergencyOptions) {
if (reason === 'NormalCallClearing') {
return;
}
this.handleError(reason + 'Error', number, emergencyOptions);
},
handleError: function(errorName, number, emergencyOptions) {
// We ignore this error because some networks generate this with
// STK CallControl when calling Voicemail pilots (e.g. TMobile)
if (errorName === 'ModifiedDialError') {
console.log('ModifiedDialError');
return;
}
if (errorName === 'BadNumberError') {
if (emergencyOptions === this.REGULAR_CALL) {
this.displayMessage('BadNumber');
} else if (emergencyOptions === this.NO_NETWORK) {
this.displayMessage('NoNetwork');
} else if (emergencyOptions === this.EMERGENCY_ONLY) {
this.displayMessage('EmergencyCallOnly', number);
} else {
console.error('Unexpected emergency options: ' + emergencyOptions);
}
} else if (errorName === 'DeviceNotAcceptedError') {
this.displayMessage('DeviceNotAccepted');
} else if (errorName === 'RadioNotAvailable') {
this.displayMessage('FlightMode');
} else if (errorName === 'BusyError') {
this.displayMessage('NumberIsBusy');
} else if (errorName === 'FDNBlockedError' ||
errorName === 'FdnCheckFailure') {
this.displayMessage('FixedDialingNumbers', number);
} else if (errorName === 'OtherConnectionInUse') {
this.displayMessage('OtherConnectionInUse');
} else if (errorName === 'OperatorDeterminedBarringError') {
this.displayMessage('OperatorDeterminedBarring');
} else {
// If the call failed for some other reason we should still
// display something to the user. See bug 846403.
console.error('Unexpected error: ', errorName);
this.displayMessage('UnableToCall');
}
},
};

View file

@ -0,0 +1,217 @@
/* globals AudioContext */
/* exported TonePlayer */
'use strict';
const kMasterVolume = 0.5;
const kToneVolume = 0.7;
const kShortPressDuration = 0.15;
const kAttackDuration = 0.025;
const kDecayDuration = 0.025;
const kReleaseDuration = 0.05;
var TonePlayer = {
_audioContext: null,
_channel: null,
_gainNode: null,
_playingNodes: [],
_maybeTrashAudio: null,
_initialized: false,
/**
* Initializes the tone player by specifying which channel will be used to
* play sounds. The TonePlayer will lazily create an AudioContext to play
* sounds when needed and automatically dispose of it when the application is
* hidden. However if the 'telephony' channel is used then we'll keep the
* AudioContext around as long as there's an active call.
*
* @param channel {String} The default channel used to play sounds.
*/
init: function tp_init(channel) {
var telephony = navigator.mozTelephony;
this._reset();
this._channel = channel;
this._maybeTrashAudio = (function tp_maybeTrashAudio() {
var callIsActive = telephony && (telephony.calls.length ||
telephony.conferenceGroup.calls.length);
/* If the application is hidden dispose of the audio context unless we're
* in a call and we're using the 'telephony' channel. */
if (document.hidden &&
!((this._channel === 'telephony') && callIsActive)) {
this._trashAudio();
}
}).bind(this);
window.addEventListener('visibilitychange', this._maybeTrashAudio);
telephony && telephony.addEventListener('callschanged',
this._maybeTrashAudio);
this._initialized = true;
},
/**
* Tears down the tone player and removes the registered event listeners,
* mostly used for unit-testing.
*/
teardown: function tp_teardown() {
var telephony = navigator.mozTelephony;
telephony && telephony.removeEventListener('callschanged',
this._maybeTrashAudio);
window.removeEventListener('visibilitychange', this._maybeTrashAudio);
this._reset();
this._initialized = false;
},
/**
* Reset all internal state to its default value.
*/
_reset: function tp_reset() {
this._audioContext = null;
this._channel = null;
this._gainNode = null;
this._playingNodes = [];
this._maybeTrashAudio = null;
},
_ensureAudio: function tp_ensureAudio() {
if (this._audioContext || !this._initialized) {
return;
}
this._audioContext = new AudioContext();
},
_trashAudio: function tp_trashAudio() {
this.stop();
this._audioContext = null;
},
// Pass 0.0 for |when| to play as soon as possible.
// Pass 0.0 for |duration| to make the tone play until stop is called.
_startAt: function tp_startAt(frequencies, when, duration) {
var context = this._audioContext;
var sampleRate = context.sampleRate;
var envelope =
context.createBuffer(1, (duration ? duration : 0.05) * sampleRate,
sampleRate);
// ADSR
for (var i = 0; i < envelope.length; i++) {
var factor = kToneVolume;
var t = i / sampleRate;
if (t <= kAttackDuration) {
factor = t / kAttackDuration;
} else if (t - kAttackDuration <= kDecayDuration) {
factor = 1.0 - (1.0 - kToneVolume) *
(t - kAttackDuration) / kDecayDuration;
}
if (!duration) {
// The envelope buffer contains the difference from the sustain value
factor -= kToneVolume;
} else if (t > duration - kReleaseDuration) {
factor *= (duration - t) / kReleaseDuration;
}
envelope.getChannelData(0)[i] = factor * kMasterVolume;
}
var gainNode = context.createGain();
gainNode.connect(context.destination);
if (!duration) {
// For long presses, the gainNode will be used to release the tone.
this._gainNode = gainNode;
}
var envelopeNode = context.createBufferSource();
envelopeNode.buffer = envelope;
envelopeNode.start(when);
envelopeNode.connect(gainNode.gain);
// Set the gain which will be summed with the envelope buffer values
// and will be the constant gain at the end of the tone envelope. For
// tones with duration, the envelope covers the entire tone, so the gain
// at the end is zero. For tones with no duration, the envelope covers
// only the attack and delay phases after which the gain is the sustain
// value.
gainNode.gain.setValueAtTime(duration ? 0.0 : kToneVolume * kMasterVolume,
0.0);
for (i = 0; i < frequencies.length; ++i) {
var oscNode = this._audioContext.createOscillator();
oscNode.type = 'sine';
oscNode.frequency.value = frequencies[i];
oscNode.start(when);
if (duration) {
// If starting immediately, then add some extra time to allow the tone
// to start, so that the tone doesn't stop short of the end of the
// envelope.
oscNode.stop(Math.max(when, context.currentTime + 0.5) + duration);
} else {
this._playingNodes.push(oscNode);
}
oscNode.connect(gainNode);
}
},
start: function tp_start(frequencies, shortPress) {
this._ensureAudio();
this._startAt(frequencies, 0, shortPress ? kShortPressDuration : 0);
},
stop: function tp_stop() {
if (!this._gainNode) {
return;
}
var context = this._audioContext;
var sampleRate = context.sampleRate;
var gain = this._gainNode.gain;
this._gainNode = null;
var ramp =
context.createBuffer(1, kReleaseDuration * sampleRate, sampleRate);
for (var i = 0; i < ramp.length; i++) {
ramp.getChannelData(0)[i] =
(ramp.length - i - 1) / ramp.length * kToneVolume * kMasterVolume;
}
var rampNode = context.createBufferSource();
rampNode.buffer = ramp;
rampNode.start();
rampNode.connect(gain);
// Change the current base gain from kToneVolume * kMasterVolume to 0,
// cancelling the initial change from adding the ramp.
gain.setValueAtTime(0.0, 0.0);
// Stop the oscillators some time after the release ramp reaches 0.
// Some extra time is included to allow the release to start.
while (this._playingNodes.length) {
this._playingNodes.pop().
stop(context.currentTime + kReleaseDuration + 0.5);
}
},
// Takes an array of steps, a step being:
// - frequency for channel 1
// - frequency for channel 2
// - duration
playSequence: function tp_playSequence(sequence) {
this._ensureAudio();
// AudioContext.currentTime is the last time received on the main thread
// from the audio graph. Trying to start a tone at currentTime will not
// start playing until this main thread returns to the event loop and
// sends a message to the audio graph thread. Allow a little time for
// this. The way Gecko processes audio in chunks of 30ms means this
// should be at least 60 ms. Experiments on Buri indicate that in
// practice this delay is 165 to 190 ms, much of which is the code that
// runs before returning to the event loop.
var when = this._audioContext.currentTime + 0.2;
for (var index = 0; index < sequence.length; ++index) {
var step = sequence[index];
var frequencies = step.slice(0, 2);
var duration = step[2] / 1000;
this._startAt(frequencies, when, duration);
when += duration;
}
}
};

174
gaia/shared/js/dialer/utils.js Executable file
View file

@ -0,0 +1,174 @@
'use strict';
/* exported Utils */
var Utils = {
prettyDate: function ut_prettyDate(time) {
var f = Intl.DateTimeFormat(navigator.languages, {
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric'
});
return f.format(new Date(time));
},
/**
* Renders localized time duration to a given dom node
* @param {HTMLElement} node that will hold the time duration content
* @param {Number} time duration in ms
* @param {String} l10nPrefix prefix used to select the right l10n id.
* Default value 'callDuration'.
*/
prettyDuration: function(node, duration, l10nPrefix) {
var elapsed = new Date(duration);
var h = elapsed.getUTCHours();
var m = elapsed.getUTCMinutes();
var s = elapsed.getUTCSeconds();
var l10nId = l10nPrefix || 'callDuration';
var durationL10n = {
h: h + '',
m: m + '',
s: s + ''
};
if (l10nId === 'callDuration') {
// Pad the args with a leading 0 if we're displaying them in purely
// digital format.
durationL10n = {
h: (h > 9 ? '' : '0') + h,
m: (m > 9 ? '' : '0') + m,
s: (s > 9 ? '' : '0') + s
};
}
if (l10nPrefix === 'callDurationTextFormat' && h === 0 && m === 0) {
// Special case: only display in seconds format (i.e. "5 s") with text
// formatting, as digital formatting doesn't support this.
l10nId += 'Seconds';
} else {
l10nId += h > 0 ? 'Hours' : 'Minutes';
}
navigator.mozL10n.setAttributes(node, l10nId, durationL10n);
},
setHeaderDate: function ut_setHeaderDate(elem, time) {
var diff = (Date.now() - time) / 1000;
var day_diff = Math.floor(diff / 86400);
var formattedTime;
if (isNaN(day_diff)) {
elem.setAttribute('data-l10n-id', 'incorrectDate');
return;
} else if (day_diff === 0) {
elem.setAttribute('data-l10n-id', 'today');
return;
} else if (day_diff === 1) {
elem.setAttribute('data-l10n-id', 'yesterday');
return;
} else if (day_diff < 6) {
formattedTime = (new Date(time)).toLocaleString(navigator.languages, {
weekday: 'long',
});
} else {
formattedTime = (new Date(time)).toLocaleString(navigator.languages, {
year: '2-digit',
month: '2-digit',
day: '2-digit'
});
}
elem.removeAttribute('data-l10n-id');
elem.textContent = formattedTime;
},
getPhoneNumberPrimaryInfo: function ut_getPhoneNumberPrimaryInfo(matchingTel,
contact) {
if (contact) {
if (contact.name && contact.name.length && contact.name[0] !== '') {
return contact.name;
} else if (contact.org && contact.org.length && contact.org[0] !== '') {
return contact.org;
}
}
if (matchingTel) {
return matchingTel.value;
}
return null;
},
toCamelCase: function ut_toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
},
_phoneTypesL10n: {
'mobile': 'phone_type_mobile',
'home': 'phone_type_home',
'work': 'phone_type_work',
'personal': 'phone_type_personal',
'faxHome': 'phone_type_fax_home',
'faxOffice': 'phone_type_fax_office',
'faxOther': 'phone_type_fax_other',
'other': 'phone_type_other'
},
/**
* In case of a call linked to a contact, the additional information of the
* phone number subject of the call consists in the type and carrier
* associated with this phone number.
*
* Each call is associated with an *unique number* and this phone number can
* belong to n specific contact(s). We don't care about the contact having
* more than one phone number, as we are only interested in the additional
* information of the current call that is associated with *one and only one*
* phone number.
*
* The type of the phone number will be localized if we have a matching key.
*/
getPhoneNumberAdditionalInfo:
function ut_getPhoneNumberAdditionalInfo(matchingTel) {
// In case that there is no stored type for this number, we default to
// "Mobile".
var type = matchingTel.type || 'mobile';
var carrier = matchingTel.carrier;
if (Array.isArray(type)) {
type = type[0];
}
var id;
var args = {};
if (this._phoneTypesL10n.hasOwnProperty(type)) {
id = this._phoneTypesL10n[type];
} else {
id = 'phone_type_custom';
args.type = type;
}
if (carrier) {
id += '_and_carrier';
args.carrier = carrier;
}
return {
id: id,
args: Object.keys(args).length ? args : null
};
},
/**
* Checks if the passed string is one of the predefined phone types that we
* localize when displayed.
*
* @param {String} type A string representing the phone type
* @return {Boolean} true if the string is one of the predefined phone types,
* false otherwise.
*/
isPhoneType: function ut_isPhoneType(type) {
return this._phoneTypesL10n.hasOwnProperty(type);
}
};

View file

@ -0,0 +1,73 @@
'use strict';
/* global Promise */
(function(exports) {
/**
* This Voicemail module provides an easy way to identify if any given number
* is voicemail number or not. Voicemail number could be stored in mozSettings
* under key of 'ril.iccInfo.mbdn' or in navigator.mozVoicemail. User of this
* module don't need to know where voicemail number stores, just query
* voicemail number by invoking Voicemail.check()
*
* @example
* Voicemail.check('123', cardIndex).then(function(isVoicemailNumber) {
* // do something based on value of isVoicemailNumber
* });
*
*/
var Voicemail = {
/**
* Query if a number is voicemail number or not
*
* @param {String} number - Number in query
* @param {Number} [cardIndex] - SIM card index in query, 0 if not specified
* @returns {Promise} - A Promise that resolves as true if number is
* voicemail number, false otherwise.
*/
check: function vm_check(number, cardIndex) {
return new Promise(function(resolve, reject) {
if (!number) {
resolve(false);
return;
}
var serviceId = cardIndex;
// check the voicemail number if the number is in the sim card
var voicemail = navigator.mozVoicemail;
if (voicemail) {
var voicemailNumber = voicemail.getNumber(serviceId);
if (voicemailNumber === number) {
resolve(true);
return;
}
}
// check the voicemail number with the mozSetting value
// based on /shared/resources/apn.json
var settings = navigator.mozSettings;
var req = settings.createLock().get('ril.iccInfo.mbdn');
req.onsuccess = function getVoicemailNumber() {
var isVoicemailNumber = false;
var voicemailNumbers = req.result['ril.iccInfo.mbdn'];
var voicemailNumber;
if (typeof voicemailNumbers === 'string') {
voicemailNumber = voicemailNumbers;
} else {
voicemailNumber = voicemailNumbers && voicemailNumbers[serviceId];
}
if (voicemailNumber === number) {
isVoicemailNumber = true;
}
resolve(isVoicemailNumber);
};
req.onerror = function getVoicemailNumberError() {
resolve(false);
};
});
}
};
exports.Voicemail = Voicemail;
}(window));

91
gaia/shared/js/dialogs.js Executable file
View file

@ -0,0 +1,91 @@
/* jshint unused: true */
/* global LazyLoader, $ */
/* exported Dialogs */
'use strict';
var Dialogs = {
// show a confirm dialog
confirm: function(options, onConfirm, onCancel) {
LazyLoader.load('shared/style/confirm.css', function() {
var dialog = $('confirm-dialog');
var msgEle = $('confirm-msg');
var cancelButton = $('confirm-cancel');
var confirmButton = $('confirm-ok');
/*
* this function handles the logic for adding the correct text
* to `element` given option fields prefixed with `prefix`
* failing to find an option, `defaultId` will be added if it exists
*/
var addText = function (element, prefix, defaultId) {
if (options[prefix + 'Id']) {
document.l10n.setAttributes(
element,
options[prefix + 'Id'],
options[prefix + 'Args'] //may be undefined but will still work
);
}
else if (options[prefix] || options[prefix + 'Text']) {
var textOption = options[prefix] || options[prefix + 'Text'];
element.textContent = textOption;
}
else if (defaultId) {
element.setAttribute('data-l10n-id', defaultId);
}
};
// set up the dialog based on the options
addText(msgEle, 'message');
addText(cancelButton, 'cancel', 'cancel');
addText(confirmButton, 'confirm', 'ok');
if (options.danger) {
confirmButton.classList.add('danger');
} else {
confirmButton.classList.remove('danger');
}
// If the caller specified a bodyClass, add that to the classList of the
// body element. This can be used for accessibility, e.g., to hide the
// main document while a dialog is shown
if (options.bodyClass) {
document.body.classList.add(options.bodyClass);
}
// show the confirm dialog
dialog.classList.remove('hidden');
function close(ev) {
if (options.bodyClass) {
document.body.classList.remove(options.bodyClass);
}
dialog.classList.add('hidden');
cancelButton.removeEventListener('click', onCancelClick);
confirmButton.removeEventListener('click', onConfirmClick);
ev.preventDefault();
ev.stopPropagation();
return false;
}
// attach event handlers
var onCancelClick = function(ev) {
close(ev);
if (onCancel) {
onCancel();
}
return false;
};
var onConfirmClick = function(ev) {
close(ev);
if (onConfirm) {
onConfirm();
}
return false;
};
cancelButton.addEventListener('click', onCancelClick);
confirmButton.addEventListener('click', onConfirmClick);
});
}
};

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;
}
};
}());

35
gaia/shared/js/dump.js Executable file
View file

@ -0,0 +1,35 @@
/* globals dump */
(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; // no traces by default
// enable/disable DUMP according to the related setting
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,231 @@
/*global Map, Set */
/* exported EventDispatcher */
/*
* This file provides an helper to add custom events to any object.
*
* In order to use this functionality wrap your object using the
* 'EventDispatcher.mixin' function:
*
* var obj = EventDispatcher.mixin(new SomeObj());
*
* A list of events can be optionally provided and it is recommended to do so.
* If a list is provided then only the events present in the list will be
* allowed. Using events not present in the list will cause other functions to
* throw an error:
*
* var obj = EventDispatcher.mixin(new SomeObj(), [
* 'somethinghappened',
* 'somethingelsehappened'
* ]);
*
* The wrapped object will have five new methods: 'on', 'once', 'off', 'offAll'
* and 'emit'. Use 'on' to register a new event-handler:
*
* obj.on("somethinghappened", function onSomethingHappened() { ... });
*
* If the same event-handler is added multiple times then only one will be
* registered, e.g.:
*
* function onSomethingHappened() { ... }
* obj.on("somethinghappened", onSomethingHappened);
* obj.on("somethinghappened", onSomethingHappened); // Does nothing
*
* Use 'off' to remove a registered listener:
*
* obj.off("somethinghappened", onSomethingHappened);
*
* Use 'once' to register a one-time event-handler: it will be automatically
* unregistered after being called.
*
* obj.once("somethinghappened", function onSomethingHappened() { ... });
*
* And use 'offAll' to remove all registered event listeners for the specified
* event:
*
* obj.offAll("somethinghappened");
*
* When used without parameters 'offAll' removes all registered event handlers,
* this can be useful when writing unit-tests.
*
* Finally use 'emit' to send an event to the registered handlers:
*
* obj.emit("somethinghappened");
*
* An optional parameter can be passed to 'emit' to be passed to the registered
* handlers:
*
* obj.emit("somethinghappened", 123);
*/
(function(exports) {
'use strict';
function ensureValidEventName(eventName) {
if (!eventName || typeof eventName !== 'string') {
throw new Error('Event name should be a valid non-empty string!');
}
}
function ensureValidHandler(handler) {
if (typeof handler !== 'function') {
throw new Error('Handler should be a function!');
}
}
function ensureAllowedEventName(allowedEvents, eventName) {
if (allowedEvents && allowedEvents.indexOf(eventName) < 0) {
throw new Error('Event "' + eventName + '" is not allowed!');
}
}
// Implements publish/subscribe behaviour that can be applied to any object,
// so that object can be listened for custom events. "this" context is the
// object with Map "listeners" property used to store handlers.
var eventDispatcher = {
/**
* Registers listener function to be executed once event occurs.
* @param {string} eventName Name of the event to listen for.
* @param {function} handler Handler to be executed once event occurs.
*/
on: function(eventName, handler) {
ensureValidEventName(eventName);
ensureAllowedEventName(this.allowedEvents, eventName);
ensureValidHandler(handler);
var handlers = this.listeners.get(eventName);
if (!handlers) {
handlers = new Set();
this.listeners.set(eventName, handlers);
}
// Set.add ignores handler if it has been already registered
handlers.add(handler);
},
/**
* Registers listener function to be executed only first time when event
* occurs.
* @param {string} eventName Name of the event to listen for.
* @param {function} handler Handler to be executed once event occurs.
*/
once: function(eventName, handler) {
ensureValidHandler(handler);
var once = (parameters) => {
eventDispatcher.off.call(this, eventName, once);
handler(parameters);
};
eventDispatcher.on.call(this, eventName, once);
},
/**
* Removes registered listener for the specified event.
* @param {string} eventName Name of the event to remove listener for.
* @param {function} handler Handler to remove, so it won't be executed
* next time event occurs.
*/
off: function(eventName, handler) {
ensureValidEventName(eventName);
ensureAllowedEventName(this.allowedEvents, eventName);
ensureValidHandler(handler);
var handlers = this.listeners.get(eventName);
if (!handlers) {
return;
}
handlers.delete(handler);
if (!handlers.size) {
this.listeners.delete(eventName);
}
},
/**
* Removes all registered listeners for the specified event.
* @param {string} eventName Name of the event to remove all listeners for.
*/
offAll: function(eventName) {
if (typeof eventName === 'undefined') {
this.listeners.clear();
return;
}
ensureValidEventName(eventName);
ensureAllowedEventName(this.allowedEvents, eventName);
var handlers = this.listeners.get(eventName);
if (!handlers) {
return;
}
handlers.clear();
this.listeners.delete(eventName);
},
/**
* Emits specified event so that all registered handlers will be called
* with the specified parameters.
* @param {string} eventName Name of the event to call handlers for.
* @param {Object} parameters Optional parameters that will be passed to
* every registered handler.
*/
emit: function(eventName, parameters) {
ensureValidEventName(eventName);
ensureAllowedEventName(this.allowedEvents, eventName);
var handlers = this.listeners.get(eventName);
if (!handlers) {
return;
}
handlers.forEach(function(handler) {
try {
handler(parameters);
} catch (e) {
console.error(e);
}
});
}
};
exports.EventDispatcher = {
/**
* Mixes dispatcher methods into target object.
* @param {Object} target Object to mix dispatcher methods into.
* @param {Array.<string>} allowedEvents Optional list of the allowed event
* names that can be emitted and listened for.
* @returns {Object} Target object with added dispatcher methods.
*/
mixin: function(target, allowedEvents) {
if (!target || typeof target !== 'object') {
throw new Error('Object to mix into should be valid object!');
}
if (typeof allowedEvents !== 'undefined' &&
!Array.isArray(allowedEvents)) {
throw new Error('Allowed events should be a valid array of strings!');
}
Object.keys(eventDispatcher).forEach(function(method) {
if (typeof target[method] !== 'undefined') {
throw new Error(
'Object to mix into already has "' + method + '" property defined!'
);
}
target[method] = eventDispatcher[method].bind(this);
}, { listeners: new Map(), allowedEvents: allowedEvents });
return target;
}
};
})(self);

60
gaia/shared/js/event_safety.js Executable file
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,193 @@
'use strict';
/* global Promise */
(function(eme) {
const NETWORK_ERROR = 'network error';
const API_KEY = '79011a035b40ef3d7baeabc8f85b862f';
const ICON_FORMAT = 20;
var device = eme.device;
function getCtx() {
var w = device.screen.width;
var h = device.screen.height;
// default to undefined's so that JSON.stringify will drop them
return {
lc: device.language || undefined,
tz: device.timezone || undefined,
v: device.osVersion || undefined,
dn: device.deviceName || undefined,
cr: device.carrier || undefined,
ct: device.dataConnectionType || undefined,
mcc: device.mcc || undefined,
mnc: device.mnc || undefined,
sr: (w && h) ? [w, h].join('x') : undefined
};
}
/**
* Make an async httpRequest to resource with given options.
* Returns a promise which will be resolved/reject on success/error
* respectively
*/
function Request(service, method, options) {
if (!eme.config.apiUrl) {
return new Promise(function(resolve, reject) {
reject('eme.config.apiUrl not defined');
});
}
var resource = service + '/' + method;
var url = eme.config.apiUrl.replace('{resource}', resource);
var payload = '';
options = options ? options : {};
options.apiKey = API_KEY;
options.deviceId = eme.device.deviceId;
options.ctx = getCtx();
for (var opt in options) {
var value = options[opt];
if (value !== null && value !== undefined) {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
payload += opt + '=' + encodeURIComponent(value) + '&';
}
}
eme.log('API request:', decodeURIComponent(url + '?' + payload));
var httpRequest;
var promise = new Promise(function done(resolve, reject) {
httpRequest = new XMLHttpRequest({mozSystem: true});
httpRequest.open('POST', url, true);
httpRequest.setRequestHeader(
'Content-Type', 'application/x-www-form-urlencoded');
httpRequest.onload = function onload(e) {
var response = null;
try {
response = JSON.parse(httpRequest.responseText);
} catch (ex) {
reject(ex);
}
if (response && response.errorCode > 0) {
resolve(response);
} else {
reject(response ?
'errorCode: ' + response.errorCode : 'invalid response');
}
};
httpRequest.onerror = function onError(e) {
reject(NETWORK_ERROR);
};
httpRequest.send(payload);
});
promise.abort = function() {
if (httpRequest.abort) {
httpRequest.abort();
}
};
return promise;
}
function SanitizeAppSearch(result) {
return new Promise(function(resolve, reject) {
// Sanitize app URLs returned from e.me
var apps = result.response.apps;
if (apps.length) {
var a = document.createElement('a');
for (var i = 0, iLen = apps.length; i < iLen; i++) {
a.href = apps[i].appUrl;
apps[i].appUrl = a.href;
}
}
resolve(result);
});
}
function PartnersAPI() {
this.Apps = {
MAX_QUERY_LENGTH : 128,
nativeInfo: function nativeInfo(options) {
if (options.guids && options.guids.length) {
// string together ids like so:
// Apps/nativeInfo/?guids=["guid1","guid2","guid3", ...]
options.guids = JSON.stringify(options.guids);
}
return Request('Apps', 'nativeInfo', options);
},
search: function search(options) {
if (!!options.query && options.query.length > this.MAX_QUERY_LENGTH) {
options.query = options.query.substr(0, this.MAX_QUERY_LENGTH);
}
options.iconFormat = ICON_FORMAT;
return Request('Apps', 'search', options).then(SanitizeAppSearch);
}
};
this.Search = {
suggestions: function suggestions(options) {
return Request('Search', 'suggestions', options);
},
bgimage: function bgimage(options) {
// Some devices contain fractions in dimensions
// which the eme api server does not accept
// so Math.ceil (http://bugzil.la/1023312)
options.width = Math.ceil(options.width || eme.device.screen.width);
options.height = Math.ceil(options.height || eme.device.screen.height);
return Request('Search', 'bgimage', options);
}
};
/** Make a Request and
* 1. on success: cache the response
* 2. on error: get response from cache
* 2.1 on error: reject with NETWORK_ERROR
*/
this.Categories = {
list: function list(options) {
var request = Request('Categories', 'list', options);
return request.then(
response => {
eme.Cache.addRequest('Categories', 'list', options, response);
return response;
},
() => {
return eme.Cache.getRequest('Categories', 'list', options)
.then(response => {
eme.log('using cached response (Categories/list)');
return response;
})
.catch(() => Promise.reject(NETWORK_ERROR));
});
}
};
}
eme.api = new PartnersAPI();
})(window.eme);

View file

@ -0,0 +1,44 @@
'use strict';
/* global Promise*/
/* global asyncStorage*/
(function(eme) {
var PREFIX = 'eme-cache';
var slice = Function.call.bind(Array.prototype.slice);
function Cache() { }
Cache.prototype = {
generateKey: function cache_generateKey() {
return slice(arguments).map(JSON.stringify).join('|');
},
add: function cache_add(key, value) {
return new Promise(function ready(resolve, reject) {
asyncStorage.setItem(PREFIX + key, value);
});
},
get: function cache_get(key) {
return new Promise(function ready(resolve, reject) {
asyncStorage.getItem(PREFIX + key, function onValue(value) {
if (value === null) {
reject(null);
} else {
resolve(value);
}
});
});
},
addRequest: function cache_addRequest(service, method, options, response) {
this.add(this.generateKey(service, method, options), response);
},
getRequest: function cache_getRequest(service, method, options) {
return this.get(this.generateKey(service, method, options));
}
};
eme.Cache = new Cache();
})(window.eme);

View file

@ -0,0 +1,112 @@
'use strict';
/* global Promise */
(function(eme) {
const mozSettings = navigator.mozSettings;
const mozWifiManager = navigator.mozWifiManager;
const mozMobileConnections = navigator.mozMobileConnections;
const dataConnectionNames = {
'hspa+': 'H+',
'lte': '4G', 'ehrpd': '4G',
'hsdpa': '3G', 'hsupa': '3G', 'hspa': '3G', 'evdo0': '3G', 'evdoa': '3G',
'evdob': '3G', '1xrtt': '3G', 'umts': '3G',
'edge': '2G', 'is95a': '2G', 'is95b': '2G', 'gprs': '2G'
};
function Device() {
// set async. from settings when calling init()
this.osVersion = null;
this.deviceId = null;
this.deviceName = null;
this.screen = {
width: window.screen.width * window.devicePixelRatio,
height: window.screen.height * window.devicePixelRatio
};
}
Device.prototype = {
init: function init(settings) {
var deviceId = settings['search.deviceId'];
if (!deviceId) {
deviceId = this.generateDeviceId();
mozSettings.createLock().set({
'search.deviceId': deviceId
});
}
this.deviceId = deviceId;
this.deviceName = settings['deviceinfo.product_model'];
this.osVersion = settings['deviceinfo.os'];
// Return a successful promise for back-compat.
return Promise.resolve();
},
// see duplicate in homescreen/everything.me.js
generateDeviceId: function generateDeviceId() {
var url = window.URL.createObjectURL(new Blob());
var id = url.replace('blob:', '');
window.URL.revokeObjectURL(url);
return 'fxos-' + id;
},
get language() {
return navigator.language;
},
get timezone() {
return (new Date().getTimezoneOffset() / -60).toString();
},
// mobile connection
get connection() {
if (!this._connection) {
this._connection = mozMobileConnections &&
mozMobileConnections.length && mozMobileConnections[0];
}
return this._connection;
},
// voice network
get voiceNet() {
return (this.connection && this.connection.voice) ?
this.connection.voice.network : null;
},
// data connection type
get dataConnectionType() {
if (mozWifiManager && mozWifiManager.enabled) {
return 'wifi';
}
if (this.connection && this.connection.data) {
var type = this.connection.data.type;
return dataConnectionNames[type] || type;
}
return null;
},
get carrier() {
return this.voiceNet ?
(this.voiceNet.shortName || this.voiceNet.longName) : null;
},
get mcc() {
return this.voiceNet ? this.voiceNet.mcc : null;
},
get mnc() {
return this.voiceNet ? this.voiceNet.mnc : null;
}
};
eme.device = new Device();
})(window.eme);

View file

@ -0,0 +1,88 @@
'use strict';
/* global eme */
/* global Promise */
(function(exports) {
const mozSettings = navigator.mozSettings;
const DEBUG = true;
var initPromise = null;
var slice = Function.call.bind(Array.prototype.slice);
exports.eme = {
config: {
apiUrl: null
},
init: function init() {
if (initPromise) {
return initPromise;
}
initPromise = this.readSettings()
.then(
function success(settings) {
// config overrides
if (settings['appsearch.url']) {
this.config.apiUrl = settings['appsearch.url'];
}
// wait for device init
return this.device.init(settings);
}.bind(this),
function error(e) {
eme.log('fatal error accessing device settings', e);
}
).catch(function(e) {
eme.log('init failed', e);
});
// avoid multiple init calls
this.init = function noop() {
eme.log('init: noop');
return initPromise;
};
return initPromise;
},
readSettings: function readSettings() {
return new Promise(function ready(resolve, reject) {
var lock = mozSettings.createLock();
var request = lock.get('*');
request.onsuccess = function onsuccess() {
resolve(request.result);
};
request.onerror = function onerror() {
reject(request.error);
};
}.bind(this));
},
log: function log() {
if (DEBUG) {
var args = slice(arguments);
args.unshift('evme');
console.log.apply(console, args);
}
},
warn: function warn() {
var args = slice(arguments);
args.unshift('evme', 'WARNING');
console.warn.apply(console, args);
},
error: function error() {
var args = slice(arguments);
args.unshift('evme', 'ERROR');
console.error.apply(console, args);
}
};
})(window);

View file

@ -0,0 +1,423 @@
'use strict';
/* global LazyLoader, TelIndexer */
// Reader Module for FB Data in a Datastore
// To use this library you need to include 'shared/js/fb/fb_request.js'
// WARNING: This file lazy loads 'shared/js/fb/fb_tel_index.js'
var fb = self.fb || {};
self.fb = fb;
(function() {
var contacts = fb.contacts || {};
fb.contacts = contacts;
var datastore;
// Datastore name declared on the manifest.webapp
var DATASTORE_NAME = 'Gaia_Facebook_Friends';
// Record Id for the index
var INDEX_ID = 1;
// Indicates the initialization state
var readyState = 'notInitialized';
// Custom event for notifying initializations
var INITIALIZE_EVENT = 'fb_ds_init';
// Last known revision Id. Used to determine whether the index has to
// be reloaded or not
var revisionId;
// Creates the internal Object in the datastore that will act as an index
function createIndex() {
return {
// By tel number and all its possible variants
// (We are not supporting dups right now)
byTel: Object.create(null),
// Prefix tree for enabling searching by partial tel numbers
treeTel: []
};
}
// The index we need to keep the correspondance between FB Friends and
// datastore Ids
var index;
// Ensures a proper error object is returned
function safeError(err) {
if (err && err.name) {
return err;
}
return {
name: 'UnknownError'
};
}
function notifyOpenSuccess(cb) {
readyState = 'initialized';
if (typeof cb === 'function') {
window.setTimeout(cb, 0);
}
var ev = new CustomEvent(INITIALIZE_EVENT);
document.dispatchEvent(ev);
}
function initError(outRequest, error) {
outRequest.failed(error);
}
function handleInitError(err, errorCb) {
readyState = 'error';
contacts.error = err;
if (typeof errorCb === 'function') {
window.setTimeout(function() {
errorCb(err);
}, 0);
}
// We resume other callers state
document.dispatchEvent(new CustomEvent(INITIALIZE_EVENT));
}
// Creates a default handler for errors
function defaultError(request) {
return defaultErrorCb.bind(null, request);
}
// Creates a default handler for success
function defaultErrorCb(request, error) {
request.failed(error);
}
function setIndex(obj) {
index = (obj || createIndex());
}
function successGet(outReq, data) {
outReq.done(data);
}
function errorGet(outReq, uid, err) {
window.console.error('Error while getting object for UID: ', uid);
outReq.failed(err.name);
}
Object.defineProperty(contacts, 'datastore', {
get: function getDataStore() { return datastore; },
enumerable: false,
configurable: false
});
Object.defineProperty(contacts, 'dsIndex', {
get: function getIndex() { return index; },
set: setIndex,
enumerable: false,
configurable: false
});
/**
* Allows to obtain the FB friend information by UID
*
*
*/
contacts.get = function(uid) {
var outRequest = new fb.utils.Request();
window.setTimeout(function get() {
contacts.init(function() {
doGet(uid, outRequest);
}, function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doGet(uid, outRequest) {
var successCb = successGet.bind(null, outRequest);
var errorCb = errorGet.bind(null, outRequest, uid);
datastore.get(uid).then(successCb, errorCb);
}
/**
* Allows to get FB information by tel number (short or long)
* The tel number must be complete
*
*/
contacts.getByPhone = function(tel) {
var outRequest = new fb.utils.Request();
window.setTimeout(function get_by_phone() {
contacts.init(function get_by_phone() {
doGetByPhone(tel, outRequest);
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doGetByPhone(tel, outRequest) {
var dsId;
var normalizedNumber = navigator.mozPhoneNumberService.normalize(tel);
if (datastore.revisionId !== revisionId) {
window.console.info('Datastore revision id has changed!');
// Refreshing the index just in case
datastore.get(INDEX_ID).then(function success(obj) {
setIndex(obj);
revisionId = datastore.revisionId;
dsId = index.byTel[normalizedNumber];
if (typeof dsId !== 'undefined') {
datastore.get(dsId).then(function success(friend) {
outRequest.done(friend);
}, defaultError(outRequest));
}
else {
outRequest.done(null);
}
}, function(err) {
err = safeError(err);
window.console.error('The index cannot be refreshed: ', err.name);
outRequest.failed(err);
});
}
else {
dsId = index.byTel[normalizedNumber];
if (typeof dsId !== 'undefined') {
datastore.get(dsId).then(function success(friend) {
outRequest.done(friend);
}, defaultError(outRequest));
}
else {
outRequest.done(null);
}
}
}
/**
* Allows to search for friends by different criteria.
* Numbers can be partial and partial matchings will be found
*
* by === tel : By Tel number
*
*/
contacts.search = function(by, number) {
var outRequest = new fb.utils.Request();
window.setTimeout(function do_search() {
contacts.init(function do_search_init() {
if (by === 'phone') {
doSearchByPhone(number, outRequest);
}
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doSearchByPhone(number, outRequest) {
var normalizedNumber = navigator.mozPhoneNumberService.normalize(number);
LazyLoader.load(['/shared/js/fb/fb_tel_index.js',
'/shared/js/binary_search.js'], function() {
var toSearchNumber = normalizedNumber;
// TODO: Temporal way of searching for international numbers
// A follow-up is needed by using PhoneNumber.js exposed to Gaia
if (number.charAt(0) === '+') {
toSearchNumber = number.substring(1);
}
if (datastore.revisionId !== revisionId) {
window.console.info('Datastore revision id has changed!');
// Refreshing the index
datastore.get(INDEX_ID).then(function success(obj) {
setIndex(obj);
revisionId = datastore.revisionId;
var results = TelIndexer.search(index.treeTel, toSearchNumber);
var out = null;
if (results.length > 0) {
out = datastore.get.apply(datastore, results);
}
return out;
},function(err) {
err = safeError(err);
window.console.error('The index cannot be refreshed: ', err.name);
outRequest.failed(err);
}).then(function success(objList) {
if (objList && !Array.isArray(objList)) {
objList = [objList];
}
else {
objList = [];
}
outRequest.done(objList);
}, function error(err) {
err = safeError(err);
window.console.error('Error while retrieving result data: ',
err.name);
outRequest.failed(err);
});
}
else {
var results = TelIndexer.search(index.treeTel, toSearchNumber);
if (results.length > 0) {
datastore.get.apply(datastore, results).then(
function success(objList) {
if (!Array.isArray(objList)) {
objList = [objList];
}
outRequest.done(objList);
}, function error(err) {
err = safeError(err);
window.console.error('Error while retrieving result data: ',
err.name);
outRequest.failed(err);
});
}
else {
outRequest.done(results);
}
}
});
}
/**
* Refreshes the index data
*
*/
contacts.refresh = function() {
var outRequest = new fb.utils.Request();
window.setTimeout(function clear() {
contacts.init(function() {
doRefresh(outRequest);
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doRefresh(outRequest) {
datastore.get(INDEX_ID).then(function success(obj) {
setIndex(obj);
outRequest.done();
}, defaultError(outRequest));
}
/**
* Allows to obtain how many records contain he FB Database
*
*/
contacts.getLength = function() {
var retRequest = new fb.utils.Request();
window.setTimeout(function() {
contacts.init(function get_all() {
doGetLength(retRequest);
},
function(err) {
initError(retRequest, err);
});
}, 0);
return retRequest;
};
/**
* Allows to return the total number of records in the DataStore (minus 1)
* That's because the index object also counts
*
*/
function doGetLength(outRequest) {
datastore.getLength().then(function success(length) {
outRequest.done(length - 1);
},
function error(err) {
outRequest.failed(safeError(err));
});
}
// Needed only for testing purposes
contacts.restart = function() {
readyState = 'notInitialized';
};
/**
* Initialization function
*
*/
contacts.init = function(cb, errorCb) {
if (!navigator.getDataStores) {
handleInitError({ name: 'DatastoreNotFound' }, errorCb);
return;
}
if (readyState === 'initialized') {
cb();
return;
}
if (readyState === 'initializing') {
document.addEventListener(INITIALIZE_EVENT, function oninitalized() {
cb();
document.removeEventListener(INITIALIZE_EVENT, oninitalized);
});
return;
}
readyState = 'initializing';
navigator.getDataStores(DATASTORE_NAME).then(function success(ds) {
if (ds.length < 1) {
handleInitError({ name: 'DatastoreNotFound' }, errorCb);
return;
}
datastore = ds[0];
// Checking the length as the index should be there
datastore.getLength().then(function(length) {
if (length === 0 && datastore.readOnly === false) {
window.console.info('Adding index as length is 0');
setIndex(createIndex());
return datastore.add(index);
}
else if (length > 0) {
return datastore.get(INDEX_ID);
}
else if (datastore.readOnly === true) {
window.console.warn('The datastore is empty and readonly');
}
return null;
}).then(function add_index_success(v) {
// Index is created in order not to cause errors
if (typeof v !== 'number') {
setIndex(v);
}
revisionId = datastore.revisionId;
notifyOpenSuccess(cb);
}, function add_index_error(err) {
err = safeError(err);
window.console.error('Error while setting the index: ', err.name);
handleInitError(err, errorCb);
});
}, function error(err) {
err = safeError(err);
window.console.error('FB: Error while opening the DataStore: ',
err.name);
handleInitError(err, errorCb);
});
};
})();

View file

@ -0,0 +1,325 @@
'use strict';
// Includes convenience functions for FB Contact Data
// To use this library you need to include:
// 'shared/js/fb/fb_request.js' and 'shared/js/fb/fb_data_reader.js'
var fb = self.fb || {};
self.fb = fb;
if (!self.AuxFB) {
self.AuxFb = (function() {
var CATEGORY = 'facebook';
var NOT_LINKED = 'not_linked';
var LINKED = 'fb_linked';
// Shallow copy from source to target object
function populate(source, target, propertyNames) {
propertyNames.forEach(function(property) {
var propertyValue = source[property];
if (propertyValue) {
if (Array.isArray(propertyValue)) {
target[property] = propertyValue.slice(0, propertyValue.length);
} else {
target[property] = propertyValue;
}
}
});
}
// Auxiliary function for merging
function mergeFbData(dcontact, fbdata) {
var multipleFields = ['email', 'tel', 'photo', 'org', 'adr'];
multipleFields.forEach(function(field) {
if (!dcontact[field]) {
dcontact[field] = [];
}
var items = fbdata[field];
if (items) {
items.forEach(function(item) {
// If there are no duplicates the merge is done
var dupList = checkDuplicates(field, item, dcontact[field],
fbdata.shortTelephone);
if (dupList.length === 0) {
dcontact[field].push(item);
}
});
}
});
var singleFields = ['bday', 'anniversary'];
singleFields.forEach(function(field) {
if (!dcontact[field]) {
dcontact[field] = fbdata[field];
}
});
// To support the case in which the contact does not have a local name
mergeNames(dcontact, fbdata);
}
// Auxiliary function for merging names
function mergeNames(devContact, fbContact) {
var namesChanged = false;
var nameFields = ['givenName', 'familyName'];
nameFields.forEach(function(anameField) {
// If the device contact does not have name fields setted
// we use the Facebook ones
var fieldValue = devContact[anameField];
var fbValue = fbContact[anameField];
if ((!Array.isArray(fieldValue) || fieldValue.length === 0) &&
Array.isArray(fbValue) && fbValue.length > 0) {
namesChanged = true;
devContact[anameField] = (fieldValue && fieldValue[0]) || [];
devContact[anameField].push(fbValue[0]);
}
});
if (namesChanged) {
var givenName = devContact.givenName[0] || '';
var familyName = devContact.familyName[0] || '';
devContact.name = [givenName + ' ' + familyName];
}
}
/**
* Returns truthy if the device contact is a FB Friend
*
*/
function isFbContact(devContact) {
return (devContact && devContact.category &&
devContact.category.indexOf(CATEGORY) !== -1);
}
/**
* Returns truthy if the device contact is linked to a FB Friend
*
*/
function isFbLinked(devContact) {
return (devContact && devContact.category &&
devContact.category.indexOf(LINKED) !== -1);
}
/**
* Gets all the data enriching the local contact with FB information
*
*/
function getData(devContact) {
var outReq = new fb.utils.Request();
window.setTimeout(function do_getData() {
var uid = getFriendUid(devContact);
if (uid) {
var fbreq = fb.contacts.get(uid);
fbreq.onsuccess = function() {
var fbdata = fbreq.result;
var out = mergeContact(devContact, fbdata);
outReq.done(out);
};
fbreq.onerror = function() {
outReq.failed(fbreq.error);
};
}
else {
outReq.done(devContact);
}
}, 0);
return outReq;
}
/**
* Returns the FB Friend UID associated to a device contact
*
*/
function getFriendUid(devContact) {
var out = devContact.uid;
if (!out) {
if (isFbLinked(devContact)) {
out = getLinkedTo(devContact);
}
else if (devContact.category) {
var idx = devContact.category.indexOf(CATEGORY);
if (idx !== -1) {
out = devContact.category[idx + 2];
}
}
}
return out;
}
/**
* Gets the UID of the FB Friend a device contact is linked to
*
*/
function getLinkedTo(devContact) {
var out;
if (devContact.category) {
var idx = devContact.category.indexOf(LINKED);
if (idx !== -1) {
out = devContact.category[idx + 1];
}
}
return out;
}
/**
* Merge a device contact with a FB Friend
*
*/
function mergeContact(devContact, fbContact) {
var out = devContact;
if (fbContact) {
out = Object.create(null);
out.updated = devContact.updated;
out.published = devContact.published;
// The id comes from devContact and the rest of properties like
// familyName, propertyName, etc... are defined in the proto object
populate(devContact, out, Object.getOwnPropertyNames(devContact));
populate(devContact, out,
Object.getOwnPropertyNames(Object.getPrototypeOf(devContact)));
mergeFbData(out, fbContact);
}
return out;
}
/**
* Returns a FB Contact (enriched with FB info) which has the tel number
*
*/
function getContactByNumber(number, onsuccess, onerror) {
var req = fb.contacts.getByPhone(number);
req.onsuccess = function get_by_phone_success(e) {
var fbData = req.result;
if (fbData) {
fb.getMozContactByUid(fbData.uid, function merge(result) {
if (Array.isArray(result) && result[0]) {
var finalContact = fb.mergeContact(result[0], fbData);
onsuccess(finalContact);
}
else {
onsuccess(null);
}
}, function error_get_mozContact(err) {
console.error('Error getting mozContact: ', err.name);
onerror(err);
});
}
else {
onsuccess(null);
}
};
req.onerror = function() {
onerror(req.error);
};
}
// Checks whether there is a duplicate for the field value
// both in FB and in the local device Contact data
// Returns an array with the values which are duplicated or empty if
// no duplicates were found
// Parameters are: field on which to search, the corresponding fbItem
// the local device items, and extra FB Items which corresp to the short
// telephone numbers allowing to filter out dups with intl-ed numbers
function checkDuplicates(field, fbItem, devContactItems, extraFbItems) {
var potentialDuplicatesFields = ['email', 'tel'];
var out = [];
if (devContactItems &&
potentialDuplicatesFields.indexOf(field) !== -1) {
var total = devContactItems.length;
for (var i = 0; i < total; i++) {
var localValue = devContactItems[i].value;
var fbValue = fbItem.value;
// Checking for telephone international number matching
if (localValue) {
var trimedLocal = localValue.trim();
if (trimedLocal === fbValue ||
(field === 'tel' && Array.isArray(extraFbItems) &&
extraFbItems.indexOf(trimedLocal) !== -1)) {
out.push(trimedLocal);
out.push(fbValue);
}
} // if(localValue)
} // for
} // if(devContactItems)
return out;
}
/**
* Returns the mozContact associated to a certain FB UID
*
*/
function getMozContactByUid(uid, onsuccess, onerror) {
var filter = {
filterBy: ['category'],
filterValue: uid,
filterOp: 'contains'
};
var req = navigator.mozContacts.find(filter);
req.onsuccess = function() {
onsuccess(req.result);
};
req.onerror = function() {
onerror(req.error);
};
}
return {
'isFbContact': isFbContact,
'isFbLinked': isFbLinked,
'getData': getData,
'getFriendUid': getFriendUid,
'getLinkedTo': getLinkedTo,
'mergeContact': mergeContact,
'getContactByNumber': getContactByNumber,
'getMozContactByUid': getMozContactByUid,
'checkDuplicates': checkDuplicates,
get CATEGORY() {
return CATEGORY;
},
get LINKED() {
return LINKED;
},
get NOT_LINKED() {
return NOT_LINKED;
}
};
})();
// This boilerplate code sets the AuxFb properties to the fb object
var props = Object.keys(self.AuxFb);
for (var j = 0, end = props.length; j < end; j++) {
/* jshint loopfunc: true */
var prop = props[j];
if (typeof self.fb[prop] === 'function') {
self.fb[prop] = self.AuxFb[prop];
}
else {
Object.defineProperty(self.fb, prop, {
value: (function() {
return self.AuxFb[prop];
})(), writable: false, enumerable: true, configurable: false
});
}
}
}

39
gaia/shared/js/fb/fb_request.js Executable file
View file

@ -0,0 +1,39 @@
'use strict';
var fb = self.fb || {};
self.fb = fb;
// Only will be executed in the case of not loading fb.utils previously
// i.e. dialer and call log FB integration
fb.utils = self.fb.utils || {};
self.fb.utils = fb.utils;
if (typeof fb.utils.Request !== 'function') {
/**
* Request auxiliary object to support asynchronous calls
*
*/
fb.utils.Request = function() {
this.done = function(result) {
this.result = result;
if (typeof this.onsuccess === 'function') {
var ev = {};
ev.target = this;
window.setTimeout(function() {
this.onsuccess(ev);
}.bind(this), 0);
}
};
this.failed = function(error) {
this.error = error;
if (typeof this.onerror === 'function') {
var ev = {};
ev.target = this;
window.setTimeout(function() {
this.onerror(ev);
}.bind(this), 0);
}
};
};
}

279
gaia/shared/js/fb/fb_tel_index.js Executable file
View file

@ -0,0 +1,279 @@
'use strict';
/* globals utils */
/* exported TelIndexer */
// This object implements a prefix tree (aka trie) for FB tel numbers
function TrieNode(number) {
this.keys = Object.create(null);
this.leaves = null;
this.value = number;
}
var TelIndexer = {
_MIN_TEL_LENGTH: 3,
_INSERT_MODE: 'insert',
_indexOf: function(array, value, byPrefix) {
var out = -1;
if (!Array.isArray(array)) {
return out;
}
for (var j = 0, l = array.length; j < l; j++) {
if (array[j].value === value || (byPrefix &&
array[j].value.startsWith(value))) {
out = j;
break;
}
}
return out;
},
_getStringVariants: function(number) {
var out = [];
var limit = number.length - this._MIN_TEL_LENGTH + 1;
for (var j = 0; j < limit; j++) {
out.push(number.substr(j));
}
return out;
},
// Allows to index the number passed as parameter
index: function(tree, number, dsId) {
var self = this;
var variants = this._getStringVariants(number);
variants.forEach(function(aVariant) {
self._insert(tree, aVariant, dsId);
});
},
// Inserts a substring in a node. dsId is the object id (uid) associated
_insertInNode: function(node, str, dsId) {
if (str.length === 0) {
return;
}
var currentStr = str;
var insertPointFound = false;
var insertObj;
var pointerOriginal = str.length;
while (!insertPointFound && currentStr.length > 0) {
var inextObj = this._indexOf(node.leaves, currentStr, 'prefix');
if (inextObj !== -1) {
insertPointFound = true;
insertObj = node.leaves[inextObj];
}
else {
currentStr = currentStr.substring(0, currentStr.length - 1);
}
pointerOriginal--;
}
if (insertPointFound) {
if (insertObj.value === str) {
// Exact insertion point
insertObj.keys[dsId] = true;
}
else {
var keys = Object.keys(insertObj.keys);
// Let's check what's the gap
var newString = insertObj.value.substring(0, currentStr.length);
var restString = insertObj.value.substring(currentStr.length);
insertObj.value = newString;
if (restString.length > 0) {
var newNode = new TrieNode(restString);
for (var j = 0; j < keys.length; j++) {
newNode.keys[keys[j]] = true;
}
if (Array.isArray(insertObj.leaves)) {
newNode.leaves = [];
insertObj.leaves.forEach(function(aLeave) {
newNode.leaves.push(aLeave);
});
}
insertObj.leaves = [];
insertObj.leaves.push(newNode);
}
insertObj.keys[dsId] = true;
var restOriginal = str.substring(pointerOriginal + 1);
// And the rest of the original string is inserted in the insertObj
this._insertInNode(insertObj, restOriginal, dsId);
}
}
else {
var nextObj = new TrieNode(str);
nextObj.keys[dsId] = true;
node.leaves = node.leaves || [];
node.leaves.push(nextObj);
}
},
// Compare function used for binary search
_compare: function(a, b) {
return a.localeCompare(b);
},
// Find the first level node (corresponding to the 3 first numbers)
_getFirstLevelNode: function(tree, str, mode) {
var out = null;
var index;
if (mode === this._INSERT_MODE) {
index = this._indexOf(tree, str);
if (index !== -1) {
out = tree[index];
}
}
else {
index = utils.binarySearch(str, tree, {
compareFunction: this._compare,
arrayField: 'value'
});
if (index.length > 0) {
out = tree[index[0]];
}
}
return out;
},
// Orders the tree enabling binary search on the first levels
orderTree: function(tree) {
tree.sort(function(a, b) {
return a.value.localeCompare(b.value);
});
},
// Inserts a number on the tree
_insert: function(tree, str, dsId) {
var firstThreeStr = str.substring(0, this._MIN_TEL_LENGTH);
var rootObj = this._getFirstLevelNode(tree, firstThreeStr,
this._INSERT_MODE);
if (!rootObj) {
rootObj = new TrieNode(firstThreeStr);
tree.push(rootObj);
}
rootObj.keys[dsId] = true;
this._insertInNode(rootObj, str.substring(this._MIN_TEL_LENGTH), dsId);
},
// Search a substring in a node
_searchInNode: function(node, str) {
var currentStr = str;
var found = false;
var pointerOriginal = str.length;
var searchObj;
while (!found && currentStr.length > 0) {
var idxObj = this._indexOf(node.leaves, currentStr, 'prefix');
if (idxObj !== -1) {
found = true;
searchObj = node.leaves[idxObj];
}
else {
currentStr = currentStr.substring(0, currentStr.length - 1);
}
pointerOriginal--;
}
if (found) {
if (searchObj.value === str) {
return searchObj;
}
// Let's calculate what is the gap
var restString = str.substr(pointerOriginal + 1);
if (restString.length > 0) {
return this._searchInNode(searchObj, restString);
}
else {
return searchObj;
}
}
else {
return null;
}
},
// Search for a number (which can be partial) on the tree
search: function(tree, number) {
var out = [];
var totalLength = number.length;
var MIN_TEL_LENGTH = this._MIN_TEL_LENGTH;
if (totalLength >= MIN_TEL_LENGTH) {
var firstThreeStr = number.substring(0, MIN_TEL_LENGTH);
var rootObj = this._getFirstLevelNode(tree, firstThreeStr);
if (rootObj) {
if (totalLength === MIN_TEL_LENGTH) {
out = Object.keys(rootObj.keys);
}
else {
var node = this._searchInNode(rootObj,
number.substring(MIN_TEL_LENGTH));
if (node) {
out = Object.keys(node.keys);
}
}
}
}
return out;
},
// Removes a number from the tree
remove: function(tree, number, dsId) {
var self = this;
var variants = this._getStringVariants(number);
variants.forEach(function(aVariant) {
self._unIndex(tree, aVariant, dsId);
});
},
// Removes a partial string from a node
_removeInNode: function(node, str, dsId) {
if (str.length === 0) {
return;
}
var currentStr = str;
var found = false;
var pointerOriginal = str.length;
var searchObj;
while (!found && currentStr.length > 0) {
var idxObj = this._indexOf(node.leaves, currentStr, 'prefix');
if (idxObj !== -1) {
found = true;
searchObj = node.leaves[idxObj];
}
else {
currentStr = currentStr.substring(0, currentStr.length - 1);
}
pointerOriginal--;
}
if (found) {
delete searchObj.keys[dsId];
this._removeInNode(searchObj, str.substr(pointerOriginal + 1), dsId);
}
},
// Removes a number from the tree
// TODO: Compact the tree when a number is removed
_unIndex: function(tree, number, dsId) {
var firstThreeStr = number.substring(0, this._MIN_TEL_LENGTH);
var rootObj = this._getFirstLevelNode(tree, firstThreeStr);
if (rootObj) {
delete rootObj.keys[dsId];
this._removeInNode(rootObj, number.substr(this._MIN_TEL_LENGTH), dsId);
}
}
};

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);
});
};
}

198
gaia/shared/js/font_size_utils.js Executable file
View file

@ -0,0 +1,198 @@
/* jshint -W083 */
(function(exports) {
'use strict';
/**
* Utility functions for measuring and manipulating font sizes
*/
var FontSizeUtils = {
/**
* Keep a cache of canvas contexts with a given font.
* We do this because it is faster to create new canvases
* than to re-set the font on existing contexts repeatedly.
*/
_cachedContexts: {},
/**
* Grab or create a cached canvas context for a given fontSize/family pair.
* @todo Add font-weight as a new dimension for caching.
*
* @param {Integer} fontSize The font size of the canvas we want.
* @param {String} fontFamily The font family of the canvas we want.
* @param {String} fontStyle The style of the font (default to italic).
* @return {CanvasRenderingContext2D} A context with the specified font.
*/
_getCachedContext: function(fontSize, fontFamily, fontStyle) {
// Default to italic style since this code is only ever used
// by headers right now and header text is always italic.
fontStyle = fontStyle || 'italic';
var cache = this._cachedContexts;
var ctx = cache[fontSize] && cache[fontSize][fontFamily] ?
cache[fontSize][fontFamily][fontStyle] : null;
if (!ctx) {
var canvas = document.createElement('canvas');
canvas.setAttribute('moz-opaque', 'true');
canvas.setAttribute('width', '1');
canvas.setAttribute('height', '1');
ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.font = fontStyle + ' ' + fontSize + 'px ' + fontFamily;
// Populate the contexts cache.
if (!cache[fontSize]) {
cache[fontSize] = {};
}
if (!cache[fontSize][fontFamily]) {
cache[fontSize][fontFamily] = {};
}
cache[fontSize][fontFamily][fontStyle] = ctx;
}
return ctx;
},
/**
* Clear any current canvas contexts from the cache.
*/
resetCache: function() {
this._cachedContexts = {};
},
/**
* Get the width of a string in pixels, given its fontSize and fontFamily.
*
* @param {String} string The string we are measuring.
* @param {Integer} fontSize The size of the font to measure against.
* @param {String} fontFamily The font family to measure against.
* @param {String} fontStyle The style of the font (default to italic).
* @return {Integer} The pixel width of the string with the given font.
*/
getFontWidth: function(string, fontSize, fontFamily, fontStyle) {
var ctx = this._getCachedContext(fontSize, fontFamily, fontStyle);
return ctx.measureText(string).width;
},
/**
* Get the maximum allowable fontSize for a string such that it will
* not overflow past a maximum width.
*
* @param {String} string The string for which to check max font size.
* @param {Array} allowedSizes A list of fontSizes allowed.
* @param {String} fontFamily The font family of the string we're measuring.
* @param {Integer} maxWidth The maximum number of pixels before overflow.
* @return {Object} Dict containing fontSize, overflow and textWidth.
*/
getMaxFontSizeInfo: function(string, allowedSizes, fontFamily, maxWidth) {
var fontSize;
var resultWidth;
var i = allowedSizes.length - 1;
do {
fontSize = allowedSizes[i];
resultWidth = this.getFontWidth(string, fontSize, fontFamily);
i--;
} while (resultWidth > maxWidth && i >= 0);
return {
fontSize: fontSize,
overflow: resultWidth > maxWidth,
textWidth: resultWidth
};
},
/**
* Last result of the overflow count computation. Every time we recompute
* the overflow count we start from this value; if the string is larger
* than the previous one this will cut down the number of computations, if
* not we restart from 0.
*/
_overflowCountCache: -1,
/**
* Get the amount of characters truncated from overflow ellipses.
*
* @param {String} string The string for which to check max font size.
* @param {Integer} fontSize The font size of the string we are measuring.
* @param {String} fontFamily The font family of the string we're measuring.
* @param {Integer} maxWidth The maximum number of pixels before overflow.
*/
getOverflowCount: function(string, fontSize, fontFamily, maxWidth) {
var substring;
var resultWidth;
var overflowCount = -1;
// If the new string is longer than the cached result of the previous
// call, and if the calculation yields a coherent result then start from
// the cached value to speed up the calculation. */
if (string.length > this._overflowCountCache) {
substring = string.substr(0, string.length - this._overflowCountCache);
resultWidth = this.getFontWidth(substring, fontSize, fontFamily);
if (resultWidth > maxWidth) {
overflowCount = this._overflowCountCache;
}
}
do {
overflowCount++;
substring = string.substr(0, string.length - overflowCount);
resultWidth = this.getFontWidth(substring, fontSize, fontFamily);
} while (substring.length > 0 && resultWidth > maxWidth);
this._overflowCountCache = overflowCount;
return overflowCount;
},
/**
* Get an element's content width disregarding its box model sizing.
*
* @param {HTMLElement|Object} HTML element, or style object.
* @returns {Number} width in pixels of elements content.
*/
getContentWidth: function(style) {
var width = parseInt(style.width, 10);
if (style.boxSizing === 'border-box') {
width -= (parseInt(style.paddingRight, 10) +
parseInt(style.paddingLeft, 10));
}
return width;
},
/**
* Get an element's style properies.
*
* @param {HTMLElement} element The element from which to fetch style.
* @return {Object} A dictionary containing element's style properties.
*/
getStyleProperties: function(element) {
var style = window.getComputedStyle(element);
var contentWidth = this.getContentWidth(style);
if (isNaN(contentWidth)) {
contentWidth = 0;
}
return {
fontFamily: style.fontFamily,
contentWidth: contentWidth,
paddingRight: parseInt(style.paddingRight, 10),
paddingLeft: parseInt(style.paddingLeft, 10),
offsetLeft: element.offsetLeft
};
},
/**
* Cache and return the width of the inner window.
*
* @return {Integer} The width of the inner window in pixels.
*/
getWindowWidth: function() {
return window.innerWidth;
}
};
exports.FontSizeUtils = FontSizeUtils;
}(this));

25
gaia/shared/js/format.js Executable file
View file

@ -0,0 +1,25 @@
/* exported Format */
'use strict';
/**
* format.js: simple formatters and string utilities.
*/
var Format = {
/**
* Pads a string to the number of characters specified.
* @param {String} input value to add padding to.
* @param {Integer} len length to pad to.
* @param {String} padWith char to pad with (defaults to " ").
*/
padLeft: function(input, len, padWith) {
padWith = padWith || ' ';
var pad = len - (input + '').length;
while (--pad > -1) {
input = padWith + input;
}
return input;
}
};

243
gaia/shared/js/fxa_iac_client.js Executable file
View file

@ -0,0 +1,243 @@
/* exported FxAccountsIACHelper */
'use strict';
// Firefox Accounts IAC client
var FxAccountsIACHelper = function FxAccountsIACHelper() {
var DEFAULT_CONNECTION_STRING = 'fxa-mgmt';
var default_rules = {
'manifestURLs': ['app://system.gaiamobile.org/manifest.webapp',
'app://smart-system.gaiamobile.org/manifest.webapp']
};
var CONNECTION_STRING = DEFAULT_CONNECTION_STRING;
var rules = default_rules;
var port;
// callbacks is a set of arrays containing {successCb, errorCb} pairs,
// keyed on the method name, for example, callbacks.getAccount.
var callbacks = {};
var eventListeners = {};
// Overrides the default configuration, if needed
var init = function init(options) {
if (!options) {
return;
}
if (options.keyword &&
typeof(options.keyword) == 'string' &&
options.keyword.length > 0) {
CONNECTION_STRING = options.keyword;
}
if (options.rules && typeof(options.rules) == 'object') {
rules = options.rules;
}
};
var addEventListener = function addEventListener(eventName, listener) {
if (!eventListeners[eventName]) {
eventListeners[eventName] = [];
}
eventListeners[eventName].push(listener);
};
var removeEventListener = function removeEventListener(eventName, listener) {
var listeners = eventListeners[eventName];
if (!listeners) {
return;
}
var index = listeners.indexOf(listener);
if (index === -1) {
return;
}
listeners.splice(index, 1);
};
// Reset to default values, could clean any future option
var reset = function reset() {
CONNECTION_STRING = DEFAULT_CONNECTION_STRING;
rules = default_rules;
eventListeners = {};
callbacks = {};
port = null;
};
var onMessage = function onMessage(evt) {
if (evt && evt.data) {
var message = evt.data;
if (message.eventName) {
var listeners = eventListeners[message.eventName];
if (!listeners) {
return;
}
for (var listener in listeners) {
if (listeners[listener] &&
typeof listeners[listener] === 'function') {
listeners[listener]();
}
}
return;
}
var cbs, cb;
if (message.methodName) {
cbs = callbacks[message.methodName];
if (!cbs || !cbs.length) {
console.warn('No callbacks for method ' + message.methodName);
return;
}
while (cbs.length) {
cb = cbs.shift();
if (!message.error) {
cb.successCb(message.data);
} else {
cb.errorCb(message.error);
}
}
}
} else {
console.error('Unknown');
}
};
// Get a reference to the application object to be able to invoke IAC.
var getSelf = function getSelf(cb, error) {
var request = navigator.mozApps.getSelf();
request.onsuccess = function onSuccess(evt) {
cb(evt.target.result);
};
request.onerror = function onError() {
if (error && typeof(error) === 'function') {
error();
}
};
};
var requestQueue = [];
var isConnecting = false;
var connect = function connect(cb) {
if (isConnecting) {
return requestQueue.push(cb);
}
isConnecting = true;
_connect(function onConnect(err) {
var next;
isConnecting = false;
if (typeof cb === 'function') {
cb(err);
}
while (!!(next = requestQueue.shift())) {
next && next(err);
}
});
};
var _connect = function _connect(callback) {
getSelf(function onApp(app) {
if (!app) {
return;
}
app.connect(CONNECTION_STRING, rules).then(function(ports) {
if (!ports || ports.length !== 1) {
return;
}
port = ports[0];
callback && callback();
}).catch(function(error) { callback && callback(error); });
});
};
var sendMessage = function sendMessage(message, successCb, errorCb) {
if (port) {
_sendMessage(message, successCb, errorCb);
return;
}
connect(function(error) {
if (error) { errorCb && errorCb(error); return; }
_sendMessage(message, successCb, errorCb);
});
};
// Sends the specific message via IAC to the system app.
// Will be always using the default keyword and forcing it
// to a single manifest.
var _sendMessage = function _sendMessage(message, successCb, errorCb) {
var name = message.name;
if (!name) {
return;
}
if (!callbacks[name]) {
callbacks[name] = [];
}
callbacks[name].push({
successCb: successCb,
errorCb: errorCb
});
// We set onmessage here again cause it is the only way that we have to
// trigger it during the tests. It sucks but it is harmless.
port.onmessage = onMessage;
port.postMessage(message);
};
var getAccount = function getAccount(successCb, errorCb) {
sendMessage({
'name': 'getAccount'
}, successCb, errorCb);
};
var openFlow = function openFlow(successCb, errorCb) {
sendMessage({
'name': 'openFlow'
}, successCb, errorCb);
};
var logout = function logout(successCb, errorCb) {
sendMessage({
'name': 'logout'
}, successCb, errorCb);
};
var refreshAuthentication = function refreshAuthentication(email,
successCb,
errorCb) {
sendMessage({
'name': 'refreshAuthentication',
'email': email
}, successCb, errorCb);
};
var resendVerificationEmail = function resendVerificationEmail(email,
successCb,
errorCb) {
sendMessage({
'name': 'resendVerificationEmail',
'email': email
}, successCb, errorCb);
};
// We do an early connection to be able to get the unsolicited events coming
// from the platform (onlogin, onverifiedlogin, onlogout).
connect();
return {
'addEventListener': addEventListener,
'getAccount': getAccount,
'init': init,
'logout': logout,
'openFlow': openFlow,
'refreshAuthentication': refreshAuthentication,
'removeEventListener': removeEventListener,
'resendVerificationEmail': resendVerificationEmail,
'reset': reset
};
}();

View file

@ -0,0 +1,684 @@
/* exported GestureDetector */
'use strict';
/**
* GestureDetector.js: generate events for one and two finger gestures.
*
* A GestureDetector object listens for touch events on a specified
* element and generates higher-level events that describe one and two finger
* gestures on the element.
*
* Supported events:
*
* tap like a click event
* dbltap like dblclick
* pan one finger motion
* swipe when a finger is released following pan events
* holdstart touch and hold. Must set an option to get these.
* holdmove motion after a holdstart event
* holdend when the finger goes up after holdstart/holdmove
* transform 2-finger pinch and twist gestures for scaling and rotation
*
* Each of these events is a bubbling CustomEvent with important details in the
* event.detail field. The event details are not yet stable and are not yet
* documented. See the calls to emitEvent() for details.
*
* To use this library, create a GestureDetector object by passing an element to
* the GestureDetector() constructor and then calling startDetecting() on it.
* The element will be the target of all the emitted gesture events. You can
* also pass an optional object as the second constructor argument. If you're
* interested in holdstart/holdmove/holdend events, pass {holdEvents:true} as
* this second argument. Otherwise they will not be generated.
* If you want to customize the pan threshold, pass {panThreshold:X}
* (X and Y in pixels) in the options argument.
*
* Implementation note: event processing is done with a simple finite-state
* machine. This means that in general, the various kinds of gestures are
* mutually exclusive. You won't get pan events until your finger has
* moved more than a minimum threshold, for example, but it does, the FSM enters
* a new state in which it can emit pan and swipe events and cannot emit hold
* events. Similarly, if you've started a 1 finger pan/swipe gesture and
* accidentally touch with a second finger, you'll continue to get pan events,
* and won't suddenly start getting 2-finger transform events.
*
* This library never calls preventDefault() or stopPropagation on any of the
* events it processes, so the raw touch events should still be
* available for other code to process. It is not clear to me whether this is a
* feature or a bug.
*/
var GestureDetector = (function() {
//
// Constructor
//
function GD(e, options) {
this.element = e;
this.options = options || {};
this.options.panThreshold = this.options.panThreshold || GD.PAN_THRESHOLD;
this.state = initialState;
this.timers = {};
}
//
// Public methods
//
GD.prototype.startDetecting = function() {
var self = this;
eventtypes.forEach(function(t) {
self.element.addEventListener(t, self);
});
};
GD.prototype.stopDetecting = function() {
var self = this;
eventtypes.forEach(function(t) {
self.element.removeEventListener(t, self);
});
};
//
// Internal methods
//
GD.prototype.handleEvent = function(e) {
var handler = this.state[e.type];
if (!handler) {
return;
}
// If this is a touch event handle each changed touch separately
if (e.changedTouches) {
for (var i = 0; i < e.changedTouches.length; i++) {
handler(this, e, e.changedTouches[i]);
// The first changed touch might have changed the state of the FSM.
handler = this.state[e.type];
if (!handler) {
return;
}
}
}
else { // Otherwise, just dispatch the event to the handler
handler(this, e);
}
};
GD.prototype.startTimer = function(type, time) {
this.clearTimer(type);
var self = this;
this.timers[type] = setTimeout(function() {
self.timers[type] = null;
var handler = self.state[type];
if (handler) {
handler(self, type);
}
}, time);
};
GD.prototype.clearTimer = function(type) {
if (this.timers[type]) {
clearTimeout(this.timers[type]);
this.timers[type] = null;
}
};
// Switch to a new FSM state, and call the init() function of that
// state, if it has one. The event and touch arguments are optional
// and are just passed through to the state init function.
GD.prototype.switchTo = function(state, event, touch) {
this.state = state;
if (state.init) {
state.init(this, event, touch);
}
};
GD.prototype.emitEvent = function(type, detail) {
if (!this.target) {
console.error('Attempt to emit event with no target');
return;
}
var event = this.element.ownerDocument.createEvent('CustomEvent');
event.initCustomEvent(type, true, true, detail);
this.target.dispatchEvent(event);
};
//
// Tuneable parameters
//
GD.HOLD_INTERVAL = 1000; // Hold events after 1000 ms
GD.PAN_THRESHOLD = 20; // 20 pixels movement before touch panning
GD.DOUBLE_TAP_DISTANCE = 50;
GD.DOUBLE_TAP_TIME = 500;
GD.VELOCITY_SMOOTHING = 0.5;
// Don't start sending transform events until the gesture exceeds a threshold
GD.SCALE_THRESHOLD = 20; // pixels
GD.ROTATE_THRESHOLD = 22.5; // degrees
// For pans and zooms, we compute new starting coordinates that are part way
// between the initial event and the event that crossed the threshold so that
// the first event we send doesn't cause a big lurch. This constant must be
// between 0 and 1 and says how far along the line between the initial value
// and the new value we pick
GD.THRESHOLD_SMOOTHING = 0.9;
//
// Helpful shortcuts and utility functions
//
var abs = Math.abs, floor = Math.floor, sqrt = Math.sqrt, atan2 = Math.atan2;
var PI = Math.PI;
// The names of events that we need to register handlers for
var eventtypes = [
'touchstart',
'touchmove',
'touchend'
];
// Return the event's timestamp in ms
function eventTime(e) {
// In gecko, synthetic events seem to be in microseconds rather than ms.
// So if the timestamp is much larger than the current time, assue it is
// in microseconds and divide by 1000
var ts = e.timeStamp;
if (ts > 2 * Date.now()) {
return Math.floor(ts / 1000);
} else {
return ts;
}
}
// Return an object containg the space and time coordinates of
// and event and touch. We freeze the object to make it immutable so
// we can pass it in events and not worry about values being changed.
function coordinates(e, t) {
return Object.freeze({
screenX: t.screenX,
screenY: t.screenY,
clientX: t.clientX,
clientY: t.clientY,
timeStamp: eventTime(e)
});
}
// Like coordinates(), but return the midpoint between two touches
function midpoints(e, t1, t2) {
return Object.freeze({
screenX: floor((t1.screenX + t2.screenX) / 2),
screenY: floor((t1.screenY + t2.screenY) / 2),
clientX: floor((t1.clientX + t2.clientX) / 2),
clientY: floor((t1.clientY + t2.clientY) / 2),
timeStamp: eventTime(e)
});
}
// Given coordinates objects c1 and c2, return a new coordinates object
// representing a point and time along the line between those points.
// The position of the point is controlled by the THRESHOLD_SMOOTHING constant
function between(c1, c2) {
var r = GD.THRESHOLD_SMOOTHING;
return Object.freeze({
screenX: floor(c1.screenX + r * (c2.screenX - c1.screenX)),
screenY: floor(c1.screenY + r * (c2.screenY - c1.screenY)),
clientX: floor(c1.clientX + r * (c2.clientX - c1.clientX)),
clientY: floor(c1.clientY + r * (c2.clientY - c1.clientY)),
timeStamp: floor(c1.timeStamp + r * (c2.timeStamp - c1.timeStamp))
});
}
// Compute the distance between two touches
function touchDistance(t1, t2) {
var dx = t2.screenX - t1.screenX;
var dy = t2.screenY - t1.screenY;
return sqrt(dx * dx + dy * dy);
}
// Compute the direction (as an angle) of the line between two touches
// Returns a number d, -180 < d <= 180
function touchDirection(t1, t2) {
return atan2(t2.screenY - t1.screenY,
t2.screenX - t1.screenX) * 180 / PI;
}
// Compute the clockwise angle between direction d1 and direction d2.
// Returns an angle a -180 < a <= 180.
function touchRotation(d1, d2) {
var angle = d2 - d1;
if (angle > 180) {
angle -= 360;
} else if (angle <= -180) {
angle += 360;
}
return angle;
}
// Determine if two taps are close enough in time and space to
// trigger a dbltap event. The arguments are objects returned
// by the coordinates() function.
function isDoubleTap(lastTap, thisTap) {
var dx = abs(thisTap.screenX - lastTap.screenX);
var dy = abs(thisTap.screenY - lastTap.screenY);
var dt = thisTap.timeStamp - lastTap.timeStamp;
return (dx < GD.DOUBLE_TAP_DISTANCE &&
dy < GD.DOUBLE_TAP_DISTANCE &&
dt < GD.DOUBLE_TAP_TIME);
}
//
// The following objects are the states of our Finite State Machine
//
// In this state we're not processing any gestures, just waiting
// for an event to start a gesture and ignoring others
var initialState = {
name: 'initialState',
init: function(d, e, t) {
// When we enter or return to the initial state, clear
// the detector properties that were tracking gestures
// Don't clear d.lastTap here, though. We need it for dbltap events
d.target = null;
d.start = d.last = null;
d.touch1 = d.touch2 = null;
d.vx = d.vy = null;
d.startDistance = d.lastDistance = null;
d.startDirection = d.lastDirection = null;
d.lastMidpoint = null;
d.scaled = d.rotated = null;
// If we were invoked with an event and touch, then process it.
if (e && t && e.type === 'touchstart') {
initialState.touchstart(d, e, t);
}
},
// Switch to the touchstarted state and process the touch event there
touchstart: function(d, e, t) {
d.switchTo(touchStartedState, e, t);
}
};
// One finger is down but we haven't generated any event yet. We're
// waiting to see... If the finger goes up soon, its a tap. If the finger
// stays down and still, its a hold. If the finger moves its a pan/swipe.
// And if a second finger goes down, its a transform
var touchStartedState = {
name: 'touchStartedState',
init: function(d, e, t) {
// Remember the target of the event
d.target = e.target;
// Remember the id of the touch that started
d.touch1 = t.identifier;
// Get the coordinates of the touch
d.start = d.last = coordinates(e, t);
// Start a timer for a hold
// If we're doing hold events, start a timer for them
if (d.options.holdEvents) {
d.startTimer('holdtimeout', GD.HOLD_INTERVAL);
}
},
touchstart: function(d, e, t) {
// If another finger goes down in this state, then
// go to transform state to start 2-finger gestures.
d.clearTimer('holdtimeout');
if (e.touches.length > 1) { // verify that we have 2 fingers
d.switchTo(transformState, e, t);
}
else {
// If Gecko fails to deliver a touchend event to us (bug 1162771, e.g.)
// then we might get two touchstart events in a row and end up here
// when there is actually only one finger currently on the screen.
// In that case, we switch back to the initial state and process
// this touch from that state. (We don't switch directly to
// touchStartedState because we need to reset things in the
// initialState init() function.)
console.warn('Ignoring missing touchend event. See bug 1162771.');
d.switchTo(initialState, e, t);
}
},
touchmove: function(d, e, t) {
// Ignore any touches but the initial one
// This could happen if there was still a finger down after
// the end of a previous 2-finger gesture, e.g.
if (t.identifier !== d.touch1) {
return;
}
if (abs(t.screenX - d.start.screenX) > d.options.panThreshold ||
abs(t.screenY - d.start.screenY) > d.options.panThreshold) {
d.clearTimer('holdtimeout');
d.switchTo(panStartedState, e, t);
}
},
touchend: function(d, e, t) {
// Ignore any touches but the initial one
if (t.identifier !== d.touch1) {
return;
}
// If there was a previous tap that was close enough in time
// and space, then emit a 'dbltap' event
if (d.lastTap && isDoubleTap(d.lastTap, d.start)) {
d.emitEvent('tap', d.start);
d.emitEvent('dbltap', d.start);
// clear the lastTap property, so we don't get another one
d.lastTap = null;
}
else {
// Emit a 'tap' event using the starting coordinates
// as the event details
d.emitEvent('tap', d.start);
// Remember the coordinates of this tap so we can detect double taps
d.lastTap = coordinates(e, t);
}
// In either case clear the timer and go back to the initial state
d.clearTimer('holdtimeout');
d.switchTo(initialState);
},
holdtimeout: function(d) {
d.switchTo(holdState);
}
};
// A single touch has moved enough to exceed the pan threshold and now
// we're going to generate pan events after each move and a swipe event
// when the touch ends. We ignore any other touches that occur while this
// pan/swipe gesture is in progress.
var panStartedState = {
name: 'panStartedState',
init: function(d, e, t) {
// Panning doesn't start until the touch has moved more than a
// certain threshold. But we don't want the pan to have a jerky
// start where the first event is a big distance. So proceed as
// pan actually started at a point along the path between the
// first touch and this current touch.
d.start = d.last = between(d.start, coordinates(e, t));
// If we transition into this state with a touchmove event,
// then process it with that handler. If we don't do this then
// we can end up with swipe events that don't know their velocity
if (e.type === 'touchmove') {
panStartedState.touchmove(d, e, t);
}
},
touchmove: function(d, e, t) {
// Ignore any fingers other than the one we're tracking
if (t.identifier !== d.touch1) {
return;
}
// Each time the touch moves, emit a pan event but stay in this state
var current = coordinates(e, t);
d.emitEvent('pan', {
absolute: {
dx: current.screenX - d.start.screenX,
dy: current.screenY - d.start.screenY
},
relative: {
dx: current.screenX - d.last.screenX,
dy: current.screenY - d.last.screenY
},
position: current
});
// Track the pan velocity so we can report this with the swipe
// Use a exponential moving average for a bit of smoothing
// on the velocity
var dt = current.timeStamp - d.last.timeStamp;
var vx = (current.screenX - d.last.screenX) / dt;
var vy = (current.screenY - d.last.screenY) / dt;
if (d.vx == null) { // first time; no average
d.vx = vx;
d.vy = vy;
}
else {
d.vx = d.vx * GD.VELOCITY_SMOOTHING +
vx * (1 - GD.VELOCITY_SMOOTHING);
d.vy = d.vy * GD.VELOCITY_SMOOTHING +
vy * (1 - GD.VELOCITY_SMOOTHING);
}
d.last = current;
},
touchend: function(d, e, t) {
// Ignore any fingers other than the one we're tracking
if (t.identifier !== d.touch1) {
return;
}
// Emit a swipe event when the finger goes up.
// Report start and end point, dx, dy, dt, velocity and direction
var current = coordinates(e, t);
var dx = current.screenX - d.start.screenX;
var dy = current.screenY - d.start.screenY;
// angle is a positive number of degrees, starting at 0 on the
// positive x axis and increasing clockwise.
var angle = atan2(dy, dx) * 180 / PI;
if (angle < 0) {
angle += 360;
}
// Direction is 'right', 'down', 'left' or 'up'
var direction;
if (angle >= 315 || angle < 45) {
direction = 'right';
} else if (angle >= 45 && angle < 135) {
direction = 'down';
} else if (angle >= 135 && angle < 225) {
direction = 'left';
} else if (angle >= 225 && angle < 315) {
direction = 'up';
}
d.emitEvent('swipe', {
start: d.start,
end: current,
dx: dx,
dy: dy,
dt: e.timeStamp - d.start.timeStamp,
vx: d.vx,
vy: d.vy,
direction: direction,
angle: angle
});
// Go back to the initial state
d.switchTo(initialState);
}
};
// We enter this state if the user touches and holds for long enough
// without moving much. When we enter we emit a holdstart event. Motion
// after the holdstart generates holdmove events. And when the touch ends
// we generate a holdend event. holdmove and holdend events can be used
// kind of like drag and drop events in a mouse-based UI. Currently,
// these events just report the coordinates of the touch. Do we need
// other details?
var holdState = {
name: 'holdState',
init: function(d) {
d.emitEvent('holdstart', d.start);
},
touchmove: function(d, e, t) {
var current = coordinates(e, t);
d.emitEvent('holdmove', {
absolute: {
dx: current.screenX - d.start.screenX,
dy: current.screenY - d.start.screenY
},
relative: {
dx: current.screenX - d.last.screenX,
dy: current.screenY - d.last.screenY
},
position: current
});
d.last = current;
},
touchend: function(d, e, t) {
var current = coordinates(e, t);
d.emitEvent('holdend', {
start: d.start,
end: current,
dx: current.screenX - d.start.screenX,
dy: current.screenY - d.start.screenY
});
d.switchTo(initialState);
}
};
// We enter this state if a second touch starts before we start
// recoginzing any other gesture. As the touches move we track the
// distance and angle between them to report scale and rotation values
// in transform events.
var transformState = {
name: 'transformState',
init: function(d, e, t) {
// Remember the id of the second touch
d.touch2 = t.identifier;
// Get the two Touch objects
var t1 = e.touches.identifiedTouch(d.touch1);
var t2 = e.touches.identifiedTouch(d.touch2);
// Compute and remember the initial distance and angle
d.startDistance = d.lastDistance = touchDistance(t1, t2);
d.startDirection = d.lastDirection = touchDirection(t1, t2);
// Don't start emitting events until we're past a threshold
d.scaled = d.rotated = false;
},
touchmove: function(d, e, t) {
// Ignore touches we're not tracking
if (t.identifier !== d.touch1 && t.identifier !== d.touch2) {
return;
}
// Get the two Touch objects
var t1 = e.touches.identifiedTouch(d.touch1);
var t2 = e.touches.identifiedTouch(d.touch2);
// Compute the new midpoints, distance and direction
var midpoint = midpoints(e, t1, t2);
var distance = touchDistance(t1, t2);
var direction = touchDirection(t1, t2);
var rotation = touchRotation(d.startDirection, direction);
// Check all of these numbers against the thresholds. Otherwise
// the transforms are too jittery even when you try to hold your
// fingers still.
if (!d.scaled) {
if (abs(distance - d.startDistance) > GD.SCALE_THRESHOLD) {
d.scaled = true;
d.startDistance = d.lastDistance =
floor(d.startDistance +
GD.THRESHOLD_SMOOTHING * (distance - d.startDistance));
} else {
distance = d.startDistance;
}
}
if (!d.rotated) {
if (abs(rotation) > GD.ROTATE_THRESHOLD) {
d.rotated = true;
} else {
direction = d.startDirection;
}
}
// If nothing has exceeded the threshold yet, then we
// don't even have to fire an event.
if (d.scaled || d.rotated) {
// The detail field for the transform gesture event includes
// 'absolute' transformations against the initial values and
// 'relative' transformations against the values from the last
// transformgesture event.
d.emitEvent('transform', {
absolute: { // transform details since gesture start
scale: distance / d.startDistance,
rotate: touchRotation(d.startDirection, direction)
},
relative: { // transform since last gesture change
scale: distance / d.lastDistance,
rotate: touchRotation(d.lastDirection, direction)
},
midpoint: midpoint
});
d.lastDistance = distance;
d.lastDirection = direction;
d.lastMidpoint = midpoint;
}
},
touchend: function(d, e, t) {
// If either finger goes up, we're done with the gesture.
// The user might move that finger and put it right back down
// again to begin another 2-finger gesture, so we can't go
// back to the initial state while one of the fingers remains up.
// On the other hand, we can't go back to touchStartedState because
// that would mean that the finger left down could cause a tap or
// pan event. So we need an afterTransform state that waits for
// a finger to come back down or the other finger to go up.
if (t.identifier === d.touch2) {
d.touch2 = null;
} else if (t.identifier === d.touch1) {
d.touch1 = d.touch2;
d.touch2 = null;
} else {
return; // It was a touch we weren't tracking
}
// If we emitted any transform events, now we need to emit
// a transformend event to end the series. The details of this
// event use the values from the last touchmove, and the
// relative amounts will 1 and 0, but they are included for
// completeness even though they are not useful.
if (d.scaled || d.rotated) {
d.emitEvent('transformend', {
absolute: { // transform details since gesture start
scale: d.lastDistance / d.startDistance,
rotate: touchRotation(d.startDirection, d.lastDirection)
},
relative: { // nothing has changed relative to the last touchmove
scale: 1,
rotate: 0
},
midpoint: d.lastMidpoint
});
}
d.switchTo(afterTransformState);
}
};
// We did a tranform and one finger went up. Wait for that finger to
// come back down or the other finger to go up too.
var afterTransformState = {
name: 'afterTransformState',
touchstart: function(d, e, t) {
d.switchTo(transformState, e, t);
},
touchend: function(d, e, t) {
if (t.identifier === d.touch1) {
d.switchTo(initialState);
}
}
};
return GD;
}());

39
gaia/shared/js/gzip/gzip.js Executable file
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
});
});
}
};

36
gaia/shared/js/gzip/gzip.min.js vendored Executable file
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);
}
};

8
gaia/shared/js/gzip/readme.txt Executable file
View file

@ -0,0 +1,8 @@
The files in `shared/gzip` add the ability to perform gzip compression
in a Worker. This provides two benefits: 1) The gzip library module can be
loaded into memory when it is needed and released when it is no no longer
needed. 2) The compression processing effort takes place in the worker, not
in an app.
The gzip compression module, gzip.min.js, is distrubuted under the
MIT License. The source code lives here -- https://github.com/imaya/zlib.js

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,201 @@
'use strict';
/* global Promise */
(function(exports) {
var datastore;
var storeName = 'homescreen_settings';
// 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('vps-initialized', function oninitalized() {
document.removeEventListener('vps-initialized', oninitalized);
resolve();
});
return;
}
readyState = 'initializing';
if (!navigator.getDataStores) {
console.error('Home screen settings store: ' +
'DataStore API is not working');
reject({ name: 'NO_DATASTORE' });
readyState = 'failed';
return;
}
navigator.getDataStores(storeName).then(function(ds) {
if (ds.length < 1) {
console.error('Home screen settings 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('vps-initialized'));
resolve();
}, 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: {
name: event.id,
value: result
}
});
});
});
}
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 get(id) {
return new Promise(function doGet(resolve, reject) {
init().then(function onInitialized() {
datastore.get(id).then(resolve, reject);
}, reject);
});
}
function put(id, value) {
return new Promise(function doAdd(resolve, reject) {
init().then(function onInitialized() {
datastore.put(value, id).then(function success() {
resolve(); // Setting was updated
}, reject);
}, reject);
});
}
function setStoreName(name) {
if (readyState === 'initializing') {
document.addEventListener('vps-initialized',
function deferSetStoreName() {
document.removeEventListener('vps-initialized', deferSetStoreName);
setStoreName(name);
});
return;
}
readyState = 'uninitialized';
storeName = name;
}
exports.homescreenSettings = {
/*
* This method returns a setting value
*
* @param{String} String param that represents an identifier
*/
get: get,
/*
* This method updates a setting in the datastore
*
* @param{String} The setting identifier
*
* @param{Object} The setting value
*/
put: put,
/*
* 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,
/*
* Method allows setting an alternative datastore. Must be called before
* use of any other methods.
*
* @param{String} The name of the alternative datastore to use
*/
setStoreName: setStoreName
};
}(window));

Some files were not shown because too many files have changed in this diff Show more