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

125
gaia/apps/ftu/js/app.js Executable file
View file

@ -0,0 +1,125 @@
/* global DataMobile, Navigation, SimManager, TimeManager,
UIManager, WifiManager, ImportIntegration, Tutorial,
VersionHelper, LanguageManager, eventSafety */
/* exported AppManager */
'use strict';
var AppManager = {
EVENT_PREFIX: 'ftu-',
init: function init(versionInfo) {
window.performance.mark('initialize');
this.isInitialized = true;
LanguageManager.init();
UIManager.init();
Navigation.init();
window.performance.mark('navigationLoaded');
this.publish('setup');
var splashTimeout = 1025;
var splashScreenHidden = this.whenEvent(UIManager.splashScreen,
'transitionend',
splashTimeout).then(() => {
UIManager.container.removeAttribute('aria-hidden');
window.performance.mark('visuallyLoaded');
});
var languageListReady = this.whenEvent(window, 'languagelistready');
Promise.all([splashScreenHidden, languageListReady]).then(() => {
window.performance.mark('fullyLoaded');
});
// if it's an upgrade we can jump to tutorial directly
if (versionInfo && versionInfo.isUpgrade()) {
var stepsKey = versionInfo.delta();
// Play the FTU Tuto steps directly on update
UIManager.changeStatusBarColor(UIManager.DARK_THEME);
UIManager.splashScreen.classList.remove('show');
UIManager.activationScreen.classList.remove('show');
UIManager.updateScreen.classList.add('show');
// Load and play the what's new tutorial
Tutorial.init(stepsKey, function() {
Tutorial.start();
});
return;
}
SimManager.init();
WifiManager.init();
ImportIntegration.init();
TimeManager.init();
DataMobile.init();
Tutorial.init();
var kSplashTimeout = 700;
// Retrieve mobile connection if available
// this is used to keep all tests passing while introducing multi-sim APIs
var conn = window.navigator.mozMobileConnections &&
window.navigator.mozMobileConnections[0];
if (!conn) {
setTimeout(function() {
// For desktop
window.location.hash = '#';
UIManager.activationScreen.classList.add('show');
window.location.hash = '#languages';
UIManager.splashScreen.classList.remove('show');
window.performance.mark('navigationInteractive');
window.performance.mark('contentInteractive');
}, kSplashTimeout);
return;
}
// Do we need pin code after splash screen?
setTimeout(function() {
// TODO Include VIVO SIM Card management
// https://bugzilla.mozilla.org/show_bug.cgi?id=801269#c6
Navigation.manageStep();
UIManager.activationScreen.classList.add('show');
// Remove the splash
UIManager.splashScreen.classList.remove('show');
window.performance.mark('navigationInteractive');
window.performance.mark('contentInteractive');
}, kSplashTimeout);
},
whenEvent: function (target, name, timeoutMs) {
return new Promise((resolve, reject) => {
eventSafety(target, name, resolve, timeoutMs || 1000);
});
},
publish: function(name, detail) {
var evt = new CustomEvent(this.EVENT_PREFIX + name,
{
bubbles: true,
detail: detail || this
});
window.dispatchEvent(evt);
}
};
function init() {
VersionHelper.getVersionInfo().then(function(versionInfo) {
if (!AppManager.isInitialized) {
AppManager.init(versionInfo);
} else {
UIManager.initTZ();
if (!UIManager.mainTitle.hasAttribute('data-l10n-id')) {
UIManager.mainTitle.setAttribute('data-l10n-id', 'language');
}
}
});
}
init();
document.addEventListener('DOMRetranslated', init);
document.l10n.ready.then(() => {
window.performance.mark('l10nready');
});

115
gaia/apps/ftu/js/data_mobile.js Executable file
View file

@ -0,0 +1,115 @@
/* exported DataMobile */
/* global Navigation */
/* global UIManager */
'use strict';
var DataMobile = {
key: 'ril.data.enabled',
keySV: 'ftu.ril.data.enabled',
STEP_DATA_3G: 2,
apnRetrieved: false,
init: function dm_init() {
var settings = navigator.mozSettings;
if (!settings) {
console.log('Settings is not available');
return;
}
this.settings = settings;
},
removeSVStatusObserver: function dm_removeSVStatusObserver() {
this.settings.removeObserver(this.keySV, this.getStatus);
},
getStatus: function dm_getStatus(aCallback) {
var self = this;
var reqSV = this.settings.createLock().get(this.keySV);
reqSV.onsuccess = function gst_svsuccess() {
// We need to be sure that we are on Cellular Data screen
// if the user has passed the screen very fast this configuration will
// have no effect
var svStatus = reqSV.result[self.keySV];
if (svStatus !== undefined && !UIManager.dataConnectionChangedByUsr) {
self.settings.removeObserver(self.keySV, self.getStatus);
if (Navigation.currentStep === self.STEP_DATA_3G) {
self.toggle(svStatus);
if (typeof aCallback === 'function') {
aCallback(svStatus);
}
} else {
self.getRealStatus(aCallback);
}
} else {
self.getRealStatus(aCallback);
}
};
reqSV.onerror = function gst_error() {
console.log('Error retrieving ' + self.keySV);
};
this.settings.addObserver(this.keySV, this.getStatus.bind(this, aCallback));
},
getRealStatus: function dm_getRealStatus(callback) {
var request = this.settings.createLock().get(this.key);
var self = this;
request.onsuccess = function gst_success() {
var currentStatus = request.result[self.key];
self.isDataAvailable = currentStatus;
callback(currentStatus);
};
request.onerror = function gst_error() {
console.log('Error retrieving ' + self.key);
};
},
toggle: function dm_toggle(status, callback) {
var options = {};
options[this.key] = status;
if (!this.apnRetrieved) {
// I need to retrieve APN
var self = this;
this.getAPN(function apn_recovered() {
self.settings.createLock().set(options);
self.apnRetrieved = true;
self.isDataAvailable = status;
if (callback) {
callback();
}
});
return;
}
this.settings.createLock().set(options);
this.apnRetrieved = true;
this.isDataAvailable = status;
if (callback) {
callback();
}
},
getAPN: function dm_getapn(callback) {
// By the time the APN settings are needed in the FTU before enabling data
// calls the system app (through the operator variant logic) might store the
// APN settings into the settings database. If not wait for that before
// enabling data calls.
var _self = this;
function ensureApnSettings() {
var req = _self.settings.createLock().get('ril.data.apnSettings');
req.onsuccess = function loadApn() {
var apnSettings = req.result['ril.data.apnSettings'];
if (apnSettings) {
if (callback) {
callback(req.result);
}
_self.settings.removeObserver('ril.data.apnSettings',
ensureApnSettings);
}
};
}
ensureApnSettings();
this.settings.addObserver('ril.data.apnSettings', ensureApnSettings);
}
};

View file

@ -0,0 +1,70 @@
/* exported FinishScreen */
/* global ScreenLayout, Utils */
(function(exports) {
'use strict';
// Defaut layout is 'tiny', the one for mobile device
var currentLayout = 'tiny';
var elementIDs = [
'tutorialFinished',
'enjoyYourPhone',
'enjoyYourPhoneUpdated'
];
var dom = {};
var initialized = false;
var FinishScreen = {
init: function (isUpgrade) {
if (initialized) {
return;
}
// We consider only 'tablet' & 'mobile' (tiny)
if (ScreenLayout.getCurrentLayout() !== 'tiny') {
currentLayout = 'large';
}
// Get panel selector based on layout
var panelSelector = 'tutorial-finish-' + currentLayout;
// Show finish panel
var finishPanel = document.getElementById(panelSelector);
finishPanel.classList.add('show');
// Cache non-layout-related DOM elements
elementIDs.forEach(function (name) {
dom[Utils.camelCase(name)] = document.getElementById(name);
}, this);
if (isUpgrade) {
dom.enjoyYourPhone.hidden = true;
dom.enjoyYourPhoneUpdated.hidden = false;
}
// Add regular listener if needed
if (currentLayout === 'tiny') {
dom.tutorialFinished.addEventListener('click', function ftuEnd() {
window.close();
});
} else {
// In 'tablet', we have to use IAC to tell system ftu is done
navigator.mozApps.getSelf().onsuccess = function(evt) {
var app = evt.target.result;
app.connect('ftucomms').then(function onConnAccepted(ports) {
ports.forEach(function(port) {
port.postMessage('done');
});
}, function onConnRejected(reason) {
console.warn('FTU is rejected due to ' + reason);
});
};
}
initialized = true;
}
};
exports.FinishScreen = FinishScreen;
}(this));

View file

@ -0,0 +1,48 @@
/* global ServicesLauncher */
/* exported ImportIntegration */
'use strict';
var ImportIntegration = {
get liveImportButton() {
delete this.liveImportButton;
return (this.liveImportButton =
document.getElementById('live-import-button'));
},
get gmailImportButton() {
delete this.gmailImportButton;
return (this.gmailImportButton =
document.getElementById('gmail-import-button'));
},
init: function () {
this.liveImportButton.addEventListener('click', this);
this.gmailImportButton.addEventListener('click', this);
},
handleEvent: function (event) {
switch (event.type) {
case 'click':
if (event.target === this.liveImportButton) {
ServicesLauncher.start('live');
}
else if (event.target === this.gmailImportButton) {
ServicesLauncher.start('gmail');
}
break;
}
},
checkImport: function (nextState) {
if (nextState === 'disabled') {
this.gmailImportButton.setAttribute('disabled', 'disabled');
this.liveImportButton.setAttribute('disabled', 'disabled');
}
else if (nextState === 'enabled') {
this.gmailImportButton.removeAttribute('disabled');
this.liveImportButton.removeAttribute('disabled');
}
}
};

85
gaia/apps/ftu/js/language.js Executable file
View file

@ -0,0 +1,85 @@
/* global KeyboardHelper, LanguageList */
/* exported LanguageManager */
'use strict';
var LanguageManager = {
settings: window.navigator.mozSettings,
init: function init() {
this.buildLanguageList();
document.getElementById('languages').addEventListener('change', this);
var onSettingChange = this._boundHandleSettingChange =
this.handleSettingChange.bind(this);
this.settings.addObserver('language.current', onSettingChange);
this.settings.addObserver('accessibility.screenreader', onSettingChange);
},
uninit: function uninit() {
if (this._boundHandleSettingChange) {
this.settings.removeObserver('language.current',
this._boundHandleSettingChange);
this.settings.removeObserver('accessibility.screenreader',
this._boundHandleSettingChange);
}
},
handleSettingChange: function(evt) {
switch (evt.settingName) {
case 'accessibility.screenreader':
this.buildLanguageList();
break;
case 'language.current':
// the 2nd parameter is to reset the current enabled layouts
KeyboardHelper.changeDefaultLayouts(evt.settingValue, true);
break;
}
},
handleEvent: function handleEvent(evt) {
if (!this.settings || evt.target.name != 'language.current') {
return true;
}
this.setLanguage(evt.target.value);
return false;
},
// update current launguage settings
setLanguage: function settings_setLanguage(language) {
this.settings.createLock().set({
'language.current': language
});
},
buildLanguageList: function settings_buildLanguageList() {
var panel = document.querySelector('#languages');
var container = panel.querySelector('ul');
container.innerHTML = '';
LanguageList.get(function fillLanguageList(allLanguages, currentLanguage) {
for (var lang in allLanguages) {
var label = document.createElement('label');
// wrap the name of the language in Unicode control codes which force
// the proper direction of the text regardless of the direction of
// the whole app
label.textContent = LanguageList.wrapBidi(lang, allLanguages[lang]);
var radio = document.createElement('gaia-radio');
radio.className = 'checkone';
radio.setAttribute('name', 'language.current');
radio.setAttribute('value', lang);
radio.checked = (lang === currentLanguage);
radio.appendChild(label);
// Needed to use a custom class with dynamic insertion.
radio.configureClass();
var li = document.createElement('li');
li.dataset.value = lang;
li.appendChild(radio);
container.appendChild(li);
}
panel.dataset.languagesReady = true;
window.dispatchEvent(new CustomEvent('languagelistready'));
});
}
};

View file

@ -0,0 +1,644 @@
/* global URLSearchParams, Navigation, UIManager,
IconsHelper, MobileOperator, Sanitizer, LazyLoader */
(function(exports) {
'use strict';
const MANIFEST_SETTING_NAME = 'latecustomization.url';
const OPERATOR_INFO_SETTING_NAME = 'latecustomization.operatorInfo';
const COMMS_CHANNEL = 'ftucomms';
const ICON_SIZE = 32;
const IMG_PATH = 'style/images/late_customization';
const ICON_INSTALLING_URL = `${IMG_PATH}/app_installing.svg`;
const ICON_FAILED_URL = `${IMG_PATH}/app_install_unrecoverable.svg`;
const ICON_DEFAULT_URL = `${IMG_PATH}/default_icon.svg`;
function LateCustomizationPanel() {
this.debug('LateCustomizationPanel ctor');
}
LateCustomizationPanel.prototype = {
DEBUG: false,
name: 'LateCustomizationPanel',
constructor: LateCustomizationPanel,
_initialized: false,
_downloadPossible: false,
/* The manifest URL for the customization (comes from mozSettings) */
// e.g. https://marketplace-dev.allizom.org/\
// api/v2/late-customization/?carrier=telenor&region=ca
_customizationManifestUrl: '',
get enabled() {
return Boolean(this._customizationManifestUrl &&
(this._operatorInfo || this._predefinedOperatorInfo));
},
get operatorInfo() {
var info = {};
Object.assign(info,
this._operatorInfo || this.getInfoFromMobileConnection(),
this._predefinedOperatorInfo);
return info;
},
get carrierName() {
var info = this.operatorInfo;
return info.carrier || info.operator;
},
get region() {
return this.operatorInfo.region || '';
},
start: function() {
window.addEventListener('hashchange', this);
window.addEventListener('ftu-done', this);
window.addEventListener('online', this);
window.addEventListener('offline', this);
this._operatorInfo = this.getInfoFromMobileConnection();
this.waitForSettingValues().then((results) => {
var [url, predefinedInfo] = results;
if (url) {
this._customizationManifestUrl = url;
}
if (predefinedInfo) {
// store operator/params separate from
// bona-fide mobileconnection details
this._predefinedOperatorInfo = predefinedInfo;
}
this.debug(
'start, resolved manifest url: ' + this._customizationManifestUrl,
' is enabled? ' + this.enabled,
' operatorInfo: ', this._operatorInfo);
if (this.enabled) {
// bail completely if not enabled: don't register step, don't render
this.init();
} else {
this.debug('Not enabled; not initializing');
if (!this._operatorInfo) {
// keep watching in case we can init later
this.waitForOperatorInfo().then((info) => {
if (!this._initialized && this.enabled) {
this.init();
}
});
}
}
}).catch(e => {
this.debug('start: didnt init, error from waitForSettingValues: ', e);
});
},
init: function() {
// conditions for displaying Late Customization pane
// are embodied in this.enabled. If not enabled, init is skipped
// and the step is not registered at all.
if (this._initialized || !this.enabled) {
return;
}
this._initialized = true;
this.appManifestUrls = [];
this.render();
Navigation.registerStep({
hash: '#late_customization',
afterStep: 'firefox_accounts'
});
this.attemptToPopulateAppList();
},
postConnectMessage: function(message) {
navigator.mozApps.getSelf().onsuccess = function(evt) {
var app = evt.target.result;
if (app.connect) {
app.connect(COMMS_CHANNEL).then(function onConnAccepted(ports) {
ports.forEach(function(port) {
port.postMessage(message);
});
}, function onConnRejected(reason) {
console.error('Cannot connect to comms channel: ', reason);
});
} else {
console.error ('mozApps does not have a connect method. ' +
'Cannot connect to the comms channel.');
}
};
},
attemptToPopulateAppList: function() {
this.debug('attemptToPopulateAppList, waiting to go online');
// start waiting to go online.
// We bail if ftu completes first
var connected = this.whenUsableConnection();
connected.then(() => {
this.debug('attemptToPopulateAppList, came online');
return this.fetchAppsToInstall().then((appsMap) => {
if (!(this._appsToInstall && this._appsToInstall.size)) {
// no apps, bail early
this.debug('No apps in response from ' +
this._customizationManifestUrl + ', bailing');
return;
}
var urls = Array.from(this._appsToInstall.keys());
var postMessageDetail = {
type: 'late-customization-apps',
urls: urls
};
this.postConnectMessage(postMessageDetail);
this.updateAppList(this._appsToInstall);
}).catch(err => {
console.warn('LateCustomizationPanel, error fetching customization ' +
'manifest from: ' + this._customizationManifestUrl,
err.message);
});
}).catch((e) => {
this.debug('No connection available in time to ' +
'fetch late customization manifest:', e);
});
return connected;
},
fetchAppsToInstall: function() {
if (this._appsToInstall) {
return Promise.resolve(this._appsToInstall);
}
return this.fetchManifest().then((manifestResp) => {
var appsMap = this.getAppsFromManifest(manifestResp);
if (!appsMap) {
return Promise.reject('getAppsFromManifest didnt return appsMap');
}
return (this._appsToInstall = appsMap);
}, (err) => {
console.warn('LateCustomization: ', err.message || err);
});
},
waitForSettingValues: function() {
var haveUrl, haveInfo;
var lock = navigator.mozSettings.createLock();
function getSetting(name) {
return new Promise((resolve, reject) => {
lock = (lock && !lock.closed) ?
lock : navigator.mozSettings.createLock();
var get = lock.get(name);
get.addEventListener('success', (function() {
resolve(get.result[name]);
}));
get.addEventListener('error', (function() {
reject();
}));
});
}
if (this._customizationManifestUrl) {
haveUrl = Promise.resolve(this._customizationManifestUrl);
} else {
haveUrl = getSetting(MANIFEST_SETTING_NAME);
}
haveInfo = getSetting(OPERATOR_INFO_SETTING_NAME);
return Promise.all([haveUrl, haveInfo]);
},
waitForOperatorInfo: function() {
var info = this.getInfoFromMobileConnection();
if (info) {
this._operatorInfo = info;
return Promise.resolve(info);
}
return new Promise((resolve, reject) => {
this._operatorInfoRequest = {
resolve: resolve,
reject: reject
};
});
},
getInfoFromMobileConnection: function() {
var connections = navigator.mozMobileConnections;
if (connections) {
for (var i=0, conn; i<connections.length; i++) {
conn = connections[i];
if(conn && conn.voice && conn.voice.connected) {
var operatorInfos = MobileOperator.userFacingInfo(conn);
if (operatorInfos.operator) {
operatorInfos.mcc = conn.voice.network.mcc;
operatorInfos.mnc = conn.voice.network.mnc;
return operatorInfos;
}
}
}
}
return null;
},
getAppsFromManifest: function(data) {
var apps = new Map();
var langs = navigator.languages;
var list = data && (data.apps || data.objects);
if (!(list && list.length)) {
return apps;
}
list.filter((app) => {
return app.is_disabled !== true &&
(new Set(app.device_types)).has('firefoxos');
}).forEach((appData) => {
appData.manifestURL = appData.manifest_url;
// get locale-appropriate name for user display
var appName = '';
langs.some(function tryLanguage(lang) {
if (this[lang]) {
appName = this[lang];
// aborts [].some
return true;
}
}, appData.name);
appData.displayName = appName;
this.debug('app data: ', appData.displayName, appData);
apps.set(appData.manifestURL, appData);
});
return apps;
},
attemptInstalls: function() {
this._installQueue = [];
if (this._appsToInstall && this._appsToInstall.size) {
var attempts = [];
this._appsToInstall.forEach((appData, manifestURL) => {
if (appData.installState == 'installed' ||
appData.installState == 'pending') {
this.debug('attemptInstalls: app install already underway: ',
manifestURL, appData.installState);
} else {
attempts.push(this.enqueAppInstall(appData));
}
});
return Promise.all(attempts);
} else {
return Promise.resolve();
}
},
installApp: function(appData) {
this.debug('installApp: ', appData.manifestURL);
return this.whenUsableConnection().then(() => {
return new Promise((resolve, reject) => {
var req = window.navigator.mozApps.install(appData.manifestURL);
var onOutcome = (arg) => {
var selector = `li[data-manifest-url="${appData.manifestURL}"]`;
var itemNode = this.appListNode.querySelector(selector);
var manifestURL = req.result && req.result.manifestURL;
this.debug('appinstall DOMRequest outcome:', arg, req.result);
if (req.error) {
delete appData.installState;
itemNode && itemNode.classList.add('failed');
console.warn('Install error: ', req.error);
} else if (req.result) {
appData.installState = req.result.installState;
this.debug('Install success, app: ',
manifestURL, appData.installState);
}
itemNode && itemNode.classList.remove('pending');
resolve(req);
};
req.onsuccess = req.onerror = onOutcome;
});
}).catch(() => {
this.debug('Lost connection while attempting install of ' +
appData.manifestURL);
});
},
enqueAppInstall: function(app) {
var installQueue = this._installQueue;
installQueue.push(app);
this.debug('enqueAppInstall, queue: ', app.manifestURL);
var currentInstallAttempt = this._installAttempt || Promise.resolve();
return currentInstallAttempt.then(req => {
if (req && req.error) {
console.warn('install error:', req.error);
}
if (this._installQueue.length) {
this._installAttempt =
this.installApp(this._installQueue.shift());
}
});
},
debug: function() {
if (!this.DEBUG) {
return;
}
var args = Array.from(arguments);
args.unshift('LateCustomizationPanel');
console.log.apply(console, args);
},
buildManifestUrl: function(url, infos={}) {
var apiURL = new URL(url);
var params = new URLSearchParams(apiURL.search.substr(1));
for(var key in infos) {
if (typeof infos[key] !== 'undefined') {
params.set(key, infos[key]);
}
}
apiURL.search = params.toString();
return apiURL.href;
},
fetchManifest: function() {
var url = this.buildManifestUrl(this._customizationManifestUrl,
this.operatorInfo);
if (!url) {
return Promise.reject(new Error('No manifest URL available'));
}
this.debug('fetchManifest at: ' + url);
return new Promise((resolve, reject) => {
LazyLoader.getJSON(url, true).then((resp) => {
if (resp.objects) {
resolve(resp);
} else {
reject('No apps in reponse to: ' + url);
}
}).catch(ex => {
reject(ex);
});
});
},
whenUsableConnection: function() {
if (this._pendingNextOnline) {
return this._pendingNextOnline;
}
var canDownload = navigator.onLine;
if (canDownload) {
return Promise.resolve(true);
}
return new Promise((resolve, reject) => {
this._nextOnlineRequest = {
resolve: resolve,
reject: reject
};
});
},
handleEvent: function(evt) {
switch (evt.type) {
case 'hashchange':
if (this.enabled &&
evt.target.location.hash === '#late_customization') {
if (this._appsToInstall && this._appsToInstall.size) {
this.onPanelShown();
} else {
Navigation.skipStep();
}
}
// TODO: is some event I can listen to to know the sim is ready
// to be asked these questions?
if (this._operatorInfoRequest) {
var info = this.getInfoFromMobileConnection();
if (info) {
this._operatorInfo = info;
this._operatorInfoRequest.resolve(info);
this._operatorInfoRequest = null;
}
}
break;
case 'online':
case 'offline':
// install is underway, try to resume
if (navigator.onLine &&
this._installQueue && this._installQueue.length) {
return this.attemptInstalls();
}
if (navigator.onLine && this._nextOnlineRequest) {
this._nextOnlineRequest.resolve(true);
} else if (this._nextOnlineRequest) {
this._nextOnlineRequest.reject(new Error('offline'));
// re-start the wait/fetch/install process
if (!this._appsToInstall) {
this.attemptToPopulateAppList();
}
}
this._nextOnlineRequest = null;
break;
case 'ftu-done':
// abort anything pending
if (this._operatorInfoRequest) {
this._operatorInfoRequest.reject(
new Error('FTU complete with no viable operator'));
this._operatorInfoRequest = null;
}
if (this._nextOnlineRequest) {
this._nextOnlineRequest.reject(
new Error('done before connection became available'));
this._nextOnlineRequest = null;
}
break;
}
},
onPanelShown: function() {
UIManager.mainTitle.setAttribute('data-l10n-id',
'late-customization');
this.finalizeRender();
this.attemptInstalls();
},
_createElement: function(name, options) {
var elm = document.createElement(name);
if (options.id) {
elm.id = options.id;
}
if (options.className) {
elm.className = options.className;
}
if (options.dataset) {
for(var p in options.dataset) {
elm.dataset[p] = options.dataset[p];
}
}
if (options.attributes) {
for(var a in options.attributes) {
elm.setAttribute(a, options.attributes[a]);
}
}
if ('textContent' in options) {
elm.textContent = options.textContent;
}
return elm;
},
render: function() {
var section = this.element =
document.getElementById('late_customization');
if (!section) {
section = this._createElement('section', {
attributes: { role: 'region' },
id: 'late_customization'
});
this.element = section;
}
section.innerHTML = Sanitizer.escapeHTML `
<section id="late-customization-cando" class="content hidden">
<p id="late_customization-downloading-message"></p>
</section>
<section id="late-customization-nocando" class="content">
<p id="late_customization-download-later-message"></p>
<p id="late_customization-download-later-marketplace"></p>
</section>
<article data-type="list">
<ul id="late_customization-applist" class="hidden"></ul>
</article>`;
function setTranslationById(domId, l10Id, attrs) {
var elem = section.querySelector('#' + domId);
document.l10n.setAttributes(elem, l10Id, attrs);
}
var carrierName = this.carrierName;
if (carrierName) {
setTranslationById(
'late_customization-downloading-message',
'late-customization-downloading-message',
{ carrierName: this.carrierName });
setTranslationById(
'late_customization-download-later-message',
'late-customization-download-later',
{ carrierName: this.carrierName });
} else {
// Use an alternate string if we have no carrier name to display
setTranslationById(
'late_customization-downloading-message',
'late-customization-downloading-message-default');
setTranslationById(
'late_customization-download-later-message',
'late-customization-download-later-default');
}
setTranslationById(
'late_customization-download-later-marketplace',
'late-customization-download-later-marketplace',
{ carrierName: this.carrierName });
if (!section.parentNode) {
document.querySelector('#activation-screen > article')
.appendChild(section);
}
this.appListNode = document.querySelector('#late_customization-applist');
},
renderAppItem: function(appData) {
var itemNode = this._createElement('li', {
dataset: { manifestUrl: appData.manifestURL },
className: 'pending'
});
var listNode = this.element.querySelector('#late_customization-applist');
if (!listNode) {
this.debug('No element found to render app details into');
return;
}
itemNode.innerHTML = Sanitizer.escapeHTML `
<span class="icon"
style="background-image: url(${ICON_INSTALLING_URL})"></span>
<p dir="auto">${appData.displayName}</p>
`;
listNode.appendChild(itemNode);
var iconElem = itemNode.querySelector('.icon');
this.getAppIconUrl(appData).then((iconUrl) => {
this.debug('getAppIconUrl returned:', iconUrl);
var img = new Image();
img.onload = () => {
iconElem.style.backgroundImage = 'url('+img.src+')';
};
img.onerror = () => {
iconElem.style.backgroundImage = 'url('+ICON_DEFAULT_URL+')';
};
img.src = iconUrl.href;
});
return itemNode;
},
getAppIconUrl: function(appData, iconSize = ICON_SIZE) {
var siteObj = {};
var manifest = appData;
var manifestURL = appData.manifest_url;
var origin = manifest.origin = new URL(manifestURL).origin;
var hasIcons = (manifest.icons && Object.keys(manifest.icons).length);
if (!hasIcons) {
return Promise.resolve(new URL(ICON_DEFAULT_URL));
}
//Getting the icons from the FirefoxOS-style manifest
siteObj.manifestURL = manifestURL;
siteObj.manifest = manifest;
var gotIcon = new Promise((resolve, reject) => {
IconsHelper.getIcon(origin, iconSize, null, siteObj)
.then((iconUrl) => {
this.debug('getIcon returned: ', iconUrl);
resolve(iconUrl);
}).catch(err => {
this.debug('Failed to load icon from: ' +
JSON.stringify(appData.icons));
console.warn('getIcon errback, err: ',err);
resolve(new URL(ICON_FAILED_URL));
});
});
return gotIcon;
},
finalizeRender: function() {
var container = this.element;
var appsMap = this._appsToInstall;
var hasApps = (appsMap && appsMap.size);
var canDoElm = container.querySelector('#late-customization-cando');
var noCanDoElm = container.querySelector('#late-customization-nocando');
var appListElm = container.querySelector('#late_customization-applist');
canDoElm.classList.toggle('hidden', !hasApps);
appListElm.classList.toggle('hidden', !hasApps);
noCanDoElm.classList.toggle('hidden', !!hasApps);
},
updateAppList: function(appsMap) {
var list = this.element.querySelector('#late_customization-applist');
this.debug('updateAppList: ', appsMap);
while (list.firstChild) {
list.firstChild.remove();
}
if (appsMap && appsMap.size) {
for (var appData of appsMap.values()) {
this.debug('calling renderAppItem with app: ', appData.manifestURL);
this.renderAppItem(appData);
}
}
this.finalizeRender();
}
};
exports.LateCustomizationPanel = LateCustomizationPanel;
var panel;
document.l10n.ready.then(() => {
panel = exports.lateCustomizationPanel = new LateCustomizationPanel();
});
window.addEventListener('ftu-setup', function onSetup(evt) {
window.removeEventListener('ftu-setup', onSetup);
panel && (panel.manager = evt.detail);
panel && panel.start();
});
})(window);

View file

@ -0,0 +1,164 @@
/* global ConfirmDialog,
UIManager,
utils,
VCFReader */
/* exported SdManager */
'use strict';
var SdManager = {
available: function() {
return utils.sdcard.checkStorageCard();
},
checkSDButton: function sm_checkSDButton() {
var sdOption = UIManager.sdImportButton;
// If there is an unlocked SD we activate import from SD
if (!SdManager.alreadyImported && SdManager.available()) {
sdOption.removeAttribute('disabled');
UIManager.noMemorycard.classList.add('hidden');
} else {
sdOption.setAttribute('disabled', 'disabled');
if (!SdManager.alreadyImported) {
UIManager.noMemorycard.classList.remove('hidden');
}
}
},
importContacts: function sm_importContacts() {
var importedContacts = 0;
UIManager.navBar.setAttribute('aria-disabled', 'true');
var cancelled = false;
var importer = null;
var importButton;
var progress = utils.overlay.show(
'memoryCardContacts-importing',
'activityBar',
'infiniteProgress'
);
utils.overlay.showMenu();
return new Promise((resolve, reject) => {
utils.overlay.oncancel = () => {
cancelled = true;
if (importer) {
importer.finish();
} else {
UIManager.navBar.removeAttribute('aria-disabled');
utils.overlay.hide();
}
resolve();
};
importButton = UIManager.sdImportButton;
utils.sdcard.retrieveFiles([
'text/vcard',
'text/directory;profile=vCard',
'text/directory'
], ['vcf', 'vcard'], (error, fileArray) => {
if (!error && !fileArray.length) {
error = 'No contacts were found';
}
if (error) {
reject(error);
return import_error(error);
}
if (cancelled) {
resolve();
return;
}
var promises = [];
fileArray.forEach(file => {
promises.push(utils.sdcard.getTextFromFile(file, onContacts));
});
Promise.all(promises).then(() => {
utils.misc.setTimestamp('sd');
UIManager.navBar.removeAttribute('aria-disabled');
utils.overlay.hide();
if (cancelled) {
resolve();
return;
}
SdManager.alreadyImported = true;
importButton.setAttribute('disabled', 'disabled');
utils.status.show({
id: 'memoryCardContacts-imported3',
args: { n: importedContacts }
});
resolve();
}).catch(reject);
});
});
function onContacts(text) {
if (cancelled) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
importer = new VCFReader(text);
if (!text || !importer) {
var error = 'No contacts were found';
import_error(error);
reject(error);
return;
}
importer.onimported = imported_contact;
importer.onerror = error => {
import_error(error);
reject(error);
};
importer.process(() => {
if (cancelled) {
reject('Cancelled');
utils.overlay.hide();
return;
}
resolve();
});
});
}
function imported_contact() {
importedContacts++;
progress.update();
}
function import_error(e) {
UIManager.navBar.removeAttribute('aria-disabled');
utils.overlay.hide();
// Just in case the user decides to do so later
importButton.removeAttribute('disabled');
// 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
importButton.click();
}
};
ConfirmDialog.show(null, 'memoryCardContacts-error', cancel, retry);
}
}
};

532
gaia/apps/ftu/js/navigation.js Executable file
View file

@ -0,0 +1,532 @@
/* global DataMobile, SimManager, IccHelper,
SdManager, UIManager, WifiManager, WifiUI,
ImportIntegration,
OperatorVariant */
/* exported Navigation */
(function(exports) {
'use strict';
const DOGFOODSETTING = 'debug.performance_data.dogfooding';
const METRICSSETTING = 'metrics.selectedMetrics.level';
/*
Steps of the First Time Usage App
*/
var steps = [
{
onlyForward: true,
hash: '#languages',
requireSIM: false
},
{
onlyForward: false,
hash: '#data_3g',
requireSIM: true
},
{
onlyForward: false,
hash: '#wifi',
requireSIM: false
},
{
onlyForward: false,
hash: '#date_and_time',
requireSIM: false
},
{
onlyForward: false,
hash: '#geolocation',
requireSIM: false
},
{
onlyForward: false,
hash: '#import_contacts',
requireSIM: false
},
{
onlyForward: false,
hash: '#firefox_accounts',
requireSIM: false
},
{
onlyForward: false,
hash: '#welcome_browser',
requireSIM: false
},
{
onlyForward: false,
hash: '#browser_privacy',
requireSIM: false
}
];
exports.Navigation = {
get stepCount() {
return this._stepOrder ? this._stepOrder.length : 0;
},
currentStepIndex: 0,
previousStepIndex: 0,
simMandatory: false,
skipMobileDataScreen: false,
skipDateTimeScreen: false,
tzInitialized: false,
_stepsById: null,
_stepOrder: null,
init: function n_init() {
this._stepOrder = [];
this._stepsById = {};
var settings = navigator.mozSettings;
var forward = document.getElementById('forward');
var back = document.getElementById('back');
this._onForwardClick = this.forward.bind(this);
this._onBackClick = this.back.bind(this);
forward.addEventListener('click', this._onForwardClick);
back.addEventListener('click', this._onBackClick);
window.addEventListener('hashchange', this);
this._onActivationScreenClick = this.handleExternalLinksClick.bind(this);
UIManager.activationScreen.addEventListener('click',
this._onActivationScreenClick);
// register pre-defined steps in the oder they appear
steps.forEach(step => {
this.registerStep(step);
});
var self = this;
var reqSIM =
settings && settings.createLock().get('ftu.sim.mandatory') || {};
reqSIM.onsuccess = function onSuccess() {
self.simMandatory = reqSIM.result['ftu.sim.mandatory'] || false;
};
navigator.mozApps.getSelf().onsuccess = function(evt) {
var app = evt.target.result;
app.connect('ftucomms').then(function onConnAccepted(ports) {
self.ports = ports;
ports.forEach(port => port.postMessage('started'));
}, function onConnRejected(reason) {
console.warn('FTU navigation cannot use IAC: ' + reason);
});
};
},
uninit: function() {
this._stepOrder = null;
this._stepsById = null;
this.currentStepIndex = 0;
this.previousStepIndex = 0;
this.ports = null;
var forward = document.getElementById('forward');
var back = document.getElementById('back');
var activationScreen = UIManager && UIManager.activationScreen;
if (forward && this._onForwardClick) {
forward.removeEventListener('click', this._onForwardClick);
}
if (back && this._onBackClick) {
back.removeEventListener('click', this._onBackClick);
}
if (activationScreen && this._onActivationScreenClick) {
activationScreen.removeEventListener('click',
this._onActivationScreenClick);
}
window.removeEventListener('hashchange', this);
},
registerStep: function(step) {
if (!step.id) {
step.id = step.hash.substring(1); // strip off '#' to make an id;
}
var id = step.id;
this._stepsById[id] = step;
var beforeStepIdx = step.hasOwnProperty('beforeStep') ?
this._stepOrder.indexOf(step.beforeStep) : -1;
var afterStepIdx = step.hasOwnProperty('afterStep') ?
this._stepOrder.indexOf(step.afterStep) : -1;
if (beforeStepIdx > -1) {
this._stepOrder.splice(beforeStepIdx, 0, id);
} else if (afterStepIdx > -1) {
this._stepOrder.splice(afterStepIdx+1, 0, id);
} else {
this._stepOrder.push(id);
}
},
stepAt: function(idx) {
var stepId = this._stepOrder[idx];
if (stepId) {
return this._stepsById[stepId];
}
},
indexOfStep: function(step) {
var id = (typeof id === 'string') ? step.id : step;
return this._stepOrder.indexOf(id);
},
back: function n_back(event) {
var currentStep = this.stepAt(this.currentStepIndex);
var actualHash = window.location.hash;
if (actualHash != currentStep.hash) {
window.history.back();
} else {
var self = this;
var goToStep = function() {
self.previousStepIndex = self.currentStepIndex;
self.currentStepIndex--;
if (self.currentStepIndex >= 0) {
self.manageStep();
}
};
goToStep();
}
},
forward: function n_forward(event) {
var self = this;
var goToStepForward = function() {
self.previousStepIndex = self.currentStepIndex;
self.currentStepIndex++;
if (self.currentStepIndex >= self.stepCount) {
// Try to send Newsletter here
UIManager.sendNewsletter(function newsletterSent(result) {
if (result) { // sending process ok, we advance
UIManager.activationScreen.classList.remove('show');
UIManager.changeStatusBarColor(UIManager.DARK_THEME);
UIManager.finishScreen.classList.add('show');
UIManager.hideActivationScreenFromScreenReader();
} else { // error on sending, we stay where we are
self.currentStepIndex--;
}
});
return;
}
self.manageStep();
};
goToStepForward();
},
handleExternalLinksClick: function n_handleExternalLinksClick(e) {
var link = e.target;
if (! link.mozMatchesSelector('a.external')) {
return;
}
e.preventDefault();
var href = link.href,
title = link.getAttribute('title') || link.textContent;
if (navigator.onLine) {
this.displayExternalLink(href, title);
} else {
UIManager.displayOfflineDialog(href, title);
}
},
displayExternalLink: function n_displayExternalLink(href, title) {
window.open(href, '', 'dialog');
},
ensureTZInitialized: function () {
if (!this.tzInitialized) {
return UIManager.initTZ().then(() => {
this.skipDateTimeScreen = !UIManager.timeZoneNeedsConfirmation;
this.tzInitialized = true;
});
} else {
return Promise.resolve();
}
},
handleEvent: function n_handleEvent(event) {
var actualHash = window.location.hash;
switch (actualHash) {
case '#languages':
UIManager.mainTitle.setAttribute('data-l10n-id', 'language');
break;
case '#data_3g':
UIManager.mainTitle.setAttribute('data-l10n-id', '3g');
DataMobile.
getStatus(UIManager.updateDataConnectionStatus.bind(UIManager));
break;
case '#wifi':
DataMobile.removeSVStatusObserver();
UIManager.mainTitle.setAttribute('data-l10n-id', 'selectNetwork');
UIManager.activationScreen.classList.remove('no-options');
if (UIManager.navBar.classList.contains('secondary-menu')) {
UIManager.navBar.classList.remove('secondary-menu');
return;
}
// This might seem like an odd place to call UIManager.initTZ, but
// there's a reason for it. initTZ tries to determine the timezone
// using information about the current mobile network connection.
// We want to call initTZ as late as possible to give the mobile
// network time to connect before the function is called.
// But we have to call initTZ *before* the Date & Time page
// appears so that it doesn't delay the appearance of the page.
// This is the last good opportunity to call it.
WifiManager.getNetworks(networks => {
this.ensureTZInitialized().then(() => {
WifiUI.renderNetworks(networks);
});
});
break;
case '#date_and_time':
UIManager.mainTitle.setAttribute('data-l10n-id', 'dateAndTime');
break;
case '#geolocation':
UIManager.mainTitle.setAttribute('data-l10n-id', 'geolocation');
break;
case '#import_contacts':
UIManager.mainTitle.setAttribute('data-l10n-id', 'importContacts3');
// Enabling or disabling SIM import depending on card status
SimManager.checkSIMButton();
// Enabling or disabling SD import depending on card status
SdManager.checkSDButton();
// If we have 3G or Wifi activate online services import
var onlineState;
if (!WifiManager.api) {
// Desktop
ImportIntegration.checkImport('enabled');
break;
}
onlineState = window.navigator.onLine ? 'enabled' : 'disabled';
ImportIntegration.checkImport(onlineState);
break;
case '#firefox_accounts':
UIManager.mainTitle.setAttribute('data-l10n-id', 'firefox-accounts');
break;
case '#welcome_browser':
UIManager.mainTitle.setAttribute('data-l10n-id', 'aboutBrowser');
// Initialize the radio button to preselect the basic metrics.
var settings = navigator.mozSettings;
var basicMetrics = document.getElementById('metrics-basic');
var enhancedMetrics = document.getElementById('metrics-enhanced');
var noneMetrics = document.getElementById('metrics-none');
var defaultLevel = settings &&
settings.createLock().get(METRICSSETTING);
if (defaultLevel) {
defaultLevel.onsuccess = function() {
var result = defaultLevel.result[METRICSSETTING];
if (result === 'Basic') {
basicMetrics.checked = true;
} else if (result === 'Enhanced') {
enhancedMetrics.checked = true;
} else if (result === 'None') {
noneMetrics.checked = true;
}
};
}
// If it's a dogfooder, we don't want them to disable the metrics.
var dogfood = settings && settings.createLock().get(DOGFOODSETTING);
if (dogfood) {
dogfood.onsuccess = function() {
if (dogfood.result[DOGFOODSETTING]) {
basicMetrics.setAttribute('disabled', 'true');
enhancedMetrics.setAttribute('disabled', 'true');
noneMetrics.setAttribute('disabled', 'true');
} else {
basicMetrics.removeAttribute('disabled');
enhancedMetrics.removeAttribute('disabled');
noneMetrics.removeAttribute('disabled');
}
};
}
break;
case '#browser_privacy':
UIManager.mainTitle.setAttribute('data-l10n-id', 'aboutBrowser');
break;
case '#SIM_mandatory':
UIManager.mainTitle.setAttribute('data-l10n-id', 'SIM_mandatory');
break;
case '#about-your-rights':
case '#about-your-privacy':
UIManager.mainTitle.setAttribute('data-l10n-id', 'aboutBrowser');
UIManager.navBar.classList.add('back-only');
break;
case '#sharing-performance-data':
UIManager.mainTitle.setAttribute('data-l10n-id', 'aboutBrowser');
UIManager.navBar.classList.add('back-only');
break;
}
// If SIM card is mandatory, we hide the button skip
if (this.simMandatory) {
UIManager.skipPinButton.classList.add('hidden');
UIManager.backSimButton.classList.remove('hidden');
} else {
UIManager.skipPinButton.classList.remove('hidden');
UIManager.backSimButton.classList.add('hidden');
}
// Managing options button
var lastIndex = this.stepCount - 1;
if (this.currentStepIndex <= lastIndex &&
(this.stepAt(this.currentStepIndex).hash !== '#wifi')) {
UIManager.activationScreen.classList.add('no-options');
}
// Managing nav buttons when coming back from out-of-steps (privacy)
if (this.currentStepIndex <= lastIndex &&
this.stepAt(this.currentStepIndex).hash === actualHash) {
UIManager.navBar.classList.remove('back-only');
}
},
/**
* Posts IAC message about FTU steps passed.
*/
postStepMessage: function n_postStepMessage(stepIndex) {
if (!this.ports) {
return;
}
var step = this.stepAt(stepIndex);
this.ports.forEach(function(port) {
port.postMessage({
type: 'step',
hash: step.hash
});
});
},
skipStep: function n_skipStep() {
var stepIndex = this.currentStepIndex;
stepIndex += stepIndex > this.previousStepIndex ? 1 : -1;
if (stepIndex <= 0) {
this.previousStepIndex = stepIndex = 0;
}
if (stepIndex > this.stepCount) {
this.previousStepIndex = stepIndex = this.stepCount;
}
this.currentStepIndex = stepIndex;
this.manageStep();
},
manageStep: function n_manageStep() {
var self = this;
// If we moved forward in FTU, post iac message about progress.
if (self.currentStepIndex > self.previousStepIndex) {
self.postStepMessage(self.previousStepIndex);
}
//SV - We need remember if phone startup with SIM
if (self.currentStepIndex >= self.stepCount) {
OperatorVariant.setSIMOnFirstBootState();
}
// Reset skip screen flags at beginning of navigation
if (self.currentStepIndex === 0) {
self.skipMobileDataScreen = false;
}
// Retrieve future location
var futureLocation = this.stepAt(self.currentStepIndex);
// If SIMcard is mandatory and no SIM, go to message window
if (this.simMandatory &&
!IccHelper.cardState &&
futureLocation.requireSIM) {
//Send to SIM Mandatory message
futureLocation.hash = '#SIM_mandatory';
futureLocation.requireSIM = false;
futureLocation.onlyBackward = true;
}
// Navigation bar management
if (this.stepAt(this.currentStepIndex).onlyForward) {
UIManager.navBar.classList.add('forward-only');
} else {
UIManager.navBar.classList.remove('forward-only');
}
var nextButton = document.getElementById('forward');
if (this.stepAt(this.currentStepIndex).onlyBackward) {
nextButton.setAttribute('disabled', 'disabled');
} else {
nextButton.removeAttribute('disabled');
}
// Substitute button content on last step
if (this.currentStepIndex === this.stepCount) {
nextButton.setAttribute('data-l10n-id', 'done');
} else {
nextButton.setAttribute('data-l10n-id', 'navbar-next');
}
if (futureLocation.hash === '#firefox_accounts') {
nextButton.setAttribute('data-l10n-id', 'skip');
}
// Change hash to the right location
window.location.hash = futureLocation.hash;
// SIM card management
if (futureLocation.requireSIM) {
var check_cardState = function(response) {
if (!response || (!SimManager.available() &&
// Don't skip it if next step is data 3g
futureLocation.hash !== '#data_3g')) {
self.skipStep();
if (self.currentStepIndex > self.previousStepIndex) {
self.skipMobileDataScreen = true;
}
}
};
// if we are navigating backwards, we do not want to
// show the SIM unlock screens for the data_3g step
var skipUnlockScreens = this.currentStepIndex < this.previousStepIndex;
SimManager.handleCardState(check_cardState, skipUnlockScreens);
}
// Only show the Date & Time screen if we couldn't determine the
// timezone from the network. (We assume that if we can
// determine the timezone, we can determine the time too.)
if (this.stepAt(this.currentStepIndex).hash === '#date_and_time') {
if (!self.tzInitialized) {
self.skipDateTimeScreen = !UIManager.timeZoneNeedsConfirmation;
}
if (self.skipDateTimeScreen) {
self.postStepMessage(self.currentStepIndex);
if(navigator.onLine) {
//if you are online you can get a more accurate guess for the time
//time you just need to trigger it
UIManager.updateSetting(
'time.timezone.automatic-update.enabled',
true
);
UIManager.updateSetting(
'time.clock.automatic-update.enabled',
true
);
}
self.skipStep();
}
}
// if we are not connected we should not try fxa
if ((futureLocation.hash === '#firefox_accounts' &&
!navigator.onLine) ||
(futureLocation.hash === '#firefox_accounts' &&
UIManager.skipFxA)) {
self.postStepMessage(self.currentStepIndex);
self.skipStep();
}
}
};
})(window);

View file

@ -0,0 +1,85 @@
/* global IccHelper */
/* exported OperatorVariant */
'use strict';
var OperatorVariant = (function() {
var NO_SIM = '000-000';
/**
* If ftu.simPresentOnFirstBoot setting has value do nothing otherwise
* set ftu.simPresentOnFirstBoot = true if
* IccHelper.cardState value is:
* 'ready'.
* otherwise set to false.
* 'pinRequired',
* 'pukRequired',
* 'networkLocked',
* 'corporateLocked',
* 'serviceProviderLocked',
* 'network1Locked',
* 'network2Locked',
* 'hrpdNetworkLocked',
* 'ruimCorporateLocked',
* 'ruimServiceProviderLocked'
*/
function setSIMOnFirstBootState() {
var cardValue;
function normalizeCode(aCode) {
var ncode = '' + aCode;
while (ncode.length < 3) {
ncode = '0' + ncode;
}
return ncode;
}
try {
if (!IccHelper || !IccHelper.cardState) {
cardValue = NO_SIM;
} else {
var mcc = IccHelper.iccInfo.mcc;
var mnc = IccHelper.iccInfo.mnc;
if ((mcc !== undefined) && (mcc !== null) &&
(mnc !== undefined) && (mnc !== null)) {
cardValue = normalizeCode(mcc) + '-' + normalizeCode(mnc);
} else {
cardValue = NO_SIM;
}
}
var settings = navigator.mozSettings;
if (!settings) {
console.log('Settings is not available');
return;
}
var req = settings.createLock().get('ftu.simPresentOnFirstBoot');
req.onsuccess = function ov_onsuccess() {
var currentStatus = req.result['ftu.simPresentOnFirstBoot'];
if (currentStatus === undefined || currentStatus === null) {
var result = navigator.mozSettings.createLock().set(
{ 'ftu.simPresentOnFirstBoot' : cardValue });
result.onerror = function ov_error() {
console.error('An error occurre setting ' +
'ftu.simPresentOnFirstBoot: ' + cardValue);
};
}
cardValue = null;
};
req.onerror = function ov_error() {
console.error('Error retrieving ftu.simPresentOnFirstBoot');
};
} catch (e) {
console.error('Error setSIMOnFirstBootState. ' + e);
}
}
return {
setSIMOnFirstBootState: setSIMOnFirstBootState
};
})();

View file

@ -0,0 +1,95 @@
/* global utils */
/* exported ServicesLauncher */
'use strict';
var FTU_APP_ORIGIN = document.location.protocol + '//' +
document.location.host;
var ServicesLauncher = (function(document) {
var extensionFrame = document.querySelector('#iframe_extensions');
var oauthFrame = document.querySelector('#iframe_oauth');
var BASE_IMPORT = '/shared/pages/import/import.html?ftu=1';
function getServiceURI(service) {
return BASE_IMPORT + '&service=' + service;
}
var access_token, currentURI, currentService;
function open() {
extensionFrame.className = 'opening';
}
function load(targetService) {
currentService = targetService;
currentURI = getServiceURI(targetService);
window.addEventListener('message', messageHandler);
oauthFrame.contentWindow.postMessage({
type: 'start',
data: {
from: 'friends',
service: targetService
}
}, FTU_APP_ORIGIN);
}
function unload() {
window.removeEventListener('message', messageHandler);
extensionFrame.src = null;
}
function close(messageId) {
extensionFrame.addEventListener('transitionend', function tclose() {
extensionFrame.removeEventListener('transitionend', tclose);
extensionFrame.src = null;
if (messageId) {
utils.status.show(messageId);
}
});
extensionFrame.className = 'closing';
window.removeEventListener('message', messageHandler);
}
// This function can also be executed when other messages arrive
// That's why we cannot call notifySettings outside the switch block
function messageHandler(e) {
if (e.origin !== FTU_APP_ORIGIN) {
return;
}
var data = e.data;
switch (data.type) {
case 'ready':
open();
break;
case 'abort':
unload();
break;
case 'window_close':
close(data.messageId);
break;
case 'authenticated':
extensionFrame.src = currentURI;
access_token = data.data;
break;
case 'messaging_ready':
extensionFrame.contentWindow.postMessage({
type: 'token',
data: access_token
}, FTU_APP_ORIGIN);
break;
}
}
return {
start: load
};
})(document);

840
gaia/apps/ftu/js/sim_manager.js Executable file
View file

@ -0,0 +1,840 @@
/* global ConfirmDialog,
MobileOperator,
Navigation,
SimContactsImporter,
UIManager,
utils */
'use strict';
var SimManager = (function() {
/*
* Icc helper object to track FTE related SIM state
*/
function Icc(mozIcc) {
this.mozIcc = mozIcc;
}
Icc.prototype = {
skipped: false,
unlocked: false,
alreadyImported: false,
mozIcc: null,
isLocked: function() {
return !this.unlocked && lockStates.indexOf(this.mozIcc.cardState) !== -1;
}
};
/*
* Update ui element with a retry count message for the given sim
*/
function showRetryCount(icc, lockType, uiElement) {
var request = icc.mozIcc.getCardLockRetryCount(lockType);
request.onsuccess = function() {
var retryCount = request.result.retryCount;
if (retryCount) {
var l10nArgs = {n: retryCount};
document.l10n.setAttributes(uiElement,'inputCodeRetriesLeft',
l10nArgs);
uiElement.classList.remove('hidden');
}
};
request.onerror = function() {
console.error('Could not fetch CardLockRetryCount', request.error.name);
};
}
// mozIcc.cardState values for a locked SIM
var lockStates = ['pinRequired', 'pukRequired', 'networkLocked',
'corporateLocked', 'serviceProviderLocked', 'network1Locked',
'network2Locked', 'hrpdNetworkLocked', 'ruimCorporateLocked',
'ruimServiceProviderLocked'];
return {
icc0: null,
icc1: null,
simSlots: window.navigator.mozMobileConnections ?
window.navigator.mozMobileConnections.length : 0,
// track the SIM that is currently being unlocked
_unlockingIcc: null,
init: function sm_init() {
this.mobConn = window.navigator.mozMobileConnections;
if (!this.mobConn) {
return;
}
this.iccManager = window.navigator.mozIccManager;
if (!this.iccManager) {
return;
}
// keep track of sim card info in slot 0 and 1
this.icc0 = null;
this.icc1 = null;
if (this.iccManager.iccIds[0]) {
this.updateIccState(this.iccManager.iccIds[0]);
}
if (this.iccManager.iccIds[1]) {
this.updateIccState(this.iccManager.iccIds[1]);
}
this.iccManager.addEventListener('iccdetected',
this.handleIccState.bind(this));
this.alreadyImported = false;
},
handleUnlockError: function sm_handleUnlockError(lockType, retryCount) {
var l10nArgs = {n: retryCount};
switch (lockType) {
case 'pin':
if (retryCount === 0) {
this.showPukScreen(this._unlockingIcc);
break;
}
UIManager.pinInput.value = '';
UIManager.pinInput.classList.add('onerror');
UIManager.pinError.classList.remove('hidden');
document.l10n.setAttributes(UIManager.pinRetriesLeft,
'inputCodeRetriesLeft', l10nArgs);
UIManager.pinRetriesLeft.classList.remove('hidden');
if (retryCount === 1) {
UIManager.pinError.
querySelector('.lastchance').classList.remove('hidden');
} else {
document.l10n.setAttributes(
UIManager.pinError.querySelector('.main'),
'pinAttemptMsg2',
l10nArgs
);
UIManager.pinError.querySelector('.main').classList.remove('hidden');
}
break;
case 'puk':
UIManager.pukInput.value = '';
UIManager.pukInput.classList.add('onerror');
UIManager.pukError.classList.remove('hidden');
UIManager.pukInfo.classList.add('hidden');
document.l10n.setAttributes(UIManager.pukRetriesLeft,
'inputCodeRetriesLeft', l10nArgs);
UIManager.pukRetriesLeft.classList.remove('hidden');
if (retryCount === 1) {
UIManager.pukError.
querySelector('.lastchance').classList.remove('hidden');
} else {
document.l10n.setAttributes(
UIManager.pukError.querySelector('.main'),
'pukAttemptMsg2',
l10nArgs
);
UIManager.pukError.querySelector('.main').classList.remove('hidden');
}
// TODO what if counter gets to 0 ??
break;
case 'nck':
case 'cck':
case 'spck':
UIManager.xckInput.value = '';
UIManager.xckInput.classList.add('onerror');
UIManager.xckError.classList.remove('hidden');
document.l10n.setAttributes(UIManager.xckRetriesLeft,
'inputCodeRetriesLeft', l10nArgs);
UIManager.xckRetriesLeft.classList.remove('hidden');
if (retryCount == 1) {
UIManager.xckError.
querySelector('.lastchance').classList.remove('hidden');
} else {
document.l10n.setAttributes(
UIManager.xckError.querySelector('.main'),
'nckAttemptMsg2',
l10nArgs
);
UIManager.xckError.querySelector('.main').classList.remove('hidden');
}
break;
}
},
available: function sm_available() {
var icc = this.guessIcc();
return (icc && icc.cardState === 'ready');
},
handleIccState: function sm_handleIccState(event) {
this.updateIccState(event.iccId);
},
updateIccState: function(iccId) {
var iccInfo = this.iccManager.getIccById(iccId);
if (!iccInfo) {
throw new Error('Unrecognized iccID: ' + iccId);
}
// determine SIM slot number
if (this.mobConn[0] && iccId === this.mobConn[0].iccId) {
this.icc0 = new Icc(iccInfo);
} else if (this.mobConn[1] && iccId === this.mobConn[1].iccId) {
this.icc1 = new Icc(iccInfo);
} else {
console.warn('ICC detected in unsupported slot', iccId);
}
},
/*
* Possible values:
* null,
* 'unknown',
* 'pinRequired',
* 'pukRequired',
* 'networkLocked',
* 'corporateLocked',
* 'serviceProviderLocked',
* 'network1Locked',
* 'network2Locked',
* 'hrpdNetworkLocked',
* 'ruimCorporateLocked',
* 'ruimServiceProviderLocked'
* 'ready'.
*/
handleCardState: function sm_handleCardState(callback, skipUnlockScreen) {
// used to track which SIM's PIN unlock
// screen we are currently displaying
this._unlockingIcc = null;
SimManager.checkSIMButton();
if (typeof callback === 'function') {
this.finishCallback = callback;
}
if (this.shouldShowUnlockScreen(this.icc0) && !skipUnlockScreen) {
this.showUnlockScreen(this.icc0);
} else if (this.shouldShowUnlockScreen(this.icc1) && !skipUnlockScreen) {
this.showUnlockScreen(this.icc1);
} else if (this.shouldShowSIMInfoScreen()) {
// reset skipped states so if we navigate back
// we will redisplay the SIM unlock screens
this.resetSkipped();
this.showSIMInfoScreen();
} else {
this.resetSkipped();
this.finish();
}
},
finish: function() {
this.hideScreen();
this.hideSIMInfoScreen();
// card state has been handled, so return
// to normal navigation, and only show cell
// data step if we have an unlocked sim
var showCellData = (this.icc0 && !this.icc0.isLocked()) ||
(this.icc1 && !this.icc1.isLocked());
this.finishCallback && this.finishCallback(showCellData);
},
shouldShowUnlockScreen: function sm_shouldShowLockScreen(icc) {
return icc && !icc.skipped && icc.isLocked();
},
// only show sim info screen if we have two SIMs inserted
shouldShowSIMInfoScreen: function sm_shouldShowSIMInfoScreen() {
return (this.icc0 && this.icc1);
},
showUnlockScreen: function sm_showUnlockScreen(icc) {
if (icc.unlocked) {
return;
}
this._unlockingIcc = icc;
switch (icc.mozIcc.cardState) {
case 'pinRequired':
this.showPinScreen(icc);
break;
case 'pukRequired':
this.showPukScreen(icc);
break;
case 'networkLocked':
case 'corporateLocked':
case 'serviceProviderLocked':
case 'network1Locked':
case 'network2Locked':
case 'hrpdNetworkLocked':
case 'ruimCorporateLocked':
case 'ruimServiceProviderLocked':
this.showXckScreen(icc);
break;
default:
throw new Error('Cannot show SIM unlock screen, unknown cardState ' +
icc.mozIcc.cardState);
}
},
/**
* A container of event listeners to avoid listening several times to the same
* event/object pair.
*/
voiceChangeListeners: [],
updateSIMInfoText: function sm_updateSIMInfoText(icc) {
var iccNumber = (icc === this.icc0) ? 1 : 2;
if (icc && icc.isLocked()) {
UIManager['simInfo' + iccNumber].classList.add('locked');
UIManager['simCarrier' + iccNumber].setAttribute('data-l10n-id',
'simPinLocked');
UIManager['simNumber' + iccNumber].removeAttribute('data-l10n-id');
UIManager['simNumber' + iccNumber].textContent = '';
} else {
UIManager['simInfo' + iccNumber].classList.remove('locked');
var mobConn = this.mobConn[iccNumber - 1];
var operator = MobileOperator
.userFacingInfo(mobConn)
.operator;
if (!operator && !this.voiceChangeListeners[iccNumber - 1]) {
// The operator is not yet populated, let's listen to the `voicechange`
// event once and update the UI later.
this.voiceChangeListeners[iccNumber - 1] = function(evt) {
var operator = MobileOperator
.userFacingInfo(mobConn)
.operator;
if (operator) {
// If we have an operator, we update the UI and stop listening.
UIManager['simCarrier' + iccNumber].removeAttribute('data-l10n-id');
UIManager['simCarrier' + iccNumber].textContent = operator;
mobConn.removeEventListener(
'voicechange', this.voiceChangeListeners[iccNumber - 1]);
this.voiceChangeListeners[iccNumber - 1] = null;
}
}.bind(this);
mobConn.addEventListener(
'voicechange', this.voiceChangeListeners[iccNumber - 1]);
}
if (operator) {
UIManager['simCarrier' + iccNumber].removeAttribute('data-l10n-id');
UIManager['simCarrier' + iccNumber].textContent = operator;
} else {
UIManager['simCarrier' + iccNumber].setAttribute(
'data-l10n-id', 'searchingOperator'
);
}
var number = icc.mozIcc.iccInfo.msisdn ||
icc.mozIcc.iccInfo.mdn || '';
if (number) {
UIManager['simNumber' + iccNumber].textContent = number;
UIManager['simInfo' + iccNumber].classList.remove('no-number');
} else {
UIManager['simInfo' + iccNumber].classList.add('no-number');
}
}
},
showSIMInfoScreen: function sm_showSIMInfoScreen() {
this.updateSIMInfoText(this.icc0);
this.updateSIMInfoText(this.icc1);
UIManager.activationScreen.classList.remove('show');
UIManager.simInfoScreen.classList.add('show');
},
hideSIMInfoScreen: function sm_hidescreen() {
UIManager.simInfoScreen.classList.remove('show');
UIManager.activationScreen.classList.add('show');
},
checkSIMButton: function sm_checkSIMButton() {
if (!this.mobConn) {
UIManager.simImport.classList.add('hidden');
return;
}
var simOption = UIManager.simImportButton;
// If there is an unlocked SIM we activate import from SIM
if (!SimManager.alreadyImported && SimManager.available()) {
simOption.removeAttribute('disabled');
UIManager.noSim.classList.add('hidden');
} else {
simOption.setAttribute('disabled', 'disabled');
if (!SimManager.alreadyImported) {
UIManager.noSim.classList.remove('hidden');
}
}
},
showPinScreen: function sm_showPinScreen(icc) {
showRetryCount(icc, 'pin', UIManager.pinRetriesLeft);
// Button management
UIManager.unlockSimButton.disabled = true;
UIManager.pinInput.addEventListener('input', function sm_checkInput(event) {
UIManager.unlockSimButton.disabled = (event.target.value.length < 4);
});
// Screen management
UIManager.activationScreen.classList.remove('show');
UIManager.unlockSimScreen.classList.add('show');
UIManager.pincodeScreen.classList.add('show');
UIManager.xckcodeScreen.classList.remove('show');
UIManager.unlockSimHeader.setAttribute('data-l10n-id', 'pincode2');
if (this.simSlots > 1) {
var simNumber = icc === this.icc0 ? 1 : 2;
document.l10n.setAttributes(UIManager.pinLabel,
'pincodeLabel', {n: simNumber});
} else {
UIManager.pinLabel.setAttribute('data-l10n-id', 'type_pin');
}
UIManager.pinInput.focus();
},
showPukScreen: function sm_showPukScreen(icc) {
showRetryCount(icc, 'puk', UIManager.pukRetriesLeft);
UIManager.unlockSimScreen.classList.add('show');
UIManager.activationScreen.classList.remove('show');
UIManager.pincodeScreen.classList.remove('show');
UIManager.pukcodeScreen.classList.add('show');
UIManager.xckcodeScreen.classList.remove('show');
UIManager.unlockSimHeader.setAttribute('data-l10n-id', 'pukcode');
if (this.simSlots > 1) {
var simNumber = icc === this.icc0 ? 1 : 2;
document.l10n.setAttributes(UIManager.pukLabel,
'pukcodeLabel', {n: simNumber});
} else {
UIManager.pukLabel.setAttribute('data-l10n-id', 'type_puk');
}
UIManager.pukInput.focus();
},
showXckScreen: function sm_showXckScreen(icc) {
var lockType;
switch (icc.mozIcc.cardState) {
case 'networkLocked':
lockType = 'nck';
break;
case 'corporateLocked':
lockType = 'cck';
break;
case 'serviceProviderLocked':
lockType = 'spck';
break;
case 'network1Locked':
lockType = 'nck1';
break;
case 'network2Locked':
lockType = 'nck2';
break;
case 'hrpdNetworkLocked':
lockType = 'hnck';
break;
case 'ruimCorporateLocked':
lockType = 'rcck';
break;
case 'ruimServiceProviderLocked':
lockType = 'rspck';
break;
default:
return; // We shouldn't be here.
}
showRetryCount(icc, lockType, UIManager.xckRetriesLeft);
UIManager.unlockSimScreen.classList.add('show');
UIManager.activationScreen.classList.remove('show');
UIManager.pincodeScreen.classList.remove('show');
UIManager.pukcodeScreen.classList.remove('show');
UIManager.xckcodeScreen.classList.add('show');
var simNumber = icc === this.icc0 ? 1 : 2;
switch (icc.mozIcc.cardState) {
case 'networkLocked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'nckcodeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'nckcodeLabel',
{n: simNumber});
break;
case 'corporateLocked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'cckcodeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'cckcodeLabel',
{n: simNumber});
break;
case 'serviceProviderLocked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'spckcodeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'spckcodeLabel',
{n: simNumber});
break;
case 'network1Locked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'nck1codeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'nck1codeLabel',
{n: simNumber});
break;
case 'network2Locked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'nck2codeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'nck2codeLabel',
{n: simNumber});
break;
case 'hrpdNetworkLocked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'hnckcodeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'hnckcodeLabel',
{n: simNumber});
break;
case 'ruimCorporateLocked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'rcckcodeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'rcckcodeLabel',
{n: simNumber});
break;
case 'ruimServiceProviderLocked':
document.l10n.setAttributes(UIManager.unlockSimHeader,
'rspckcodeTitle', {n: simNumber});
document.l10n.setAttributes(UIManager.xckLabel, 'rspckcodeLabel',
{n: simNumber});
break;
}
UIManager.xckInput.focus();
},
resetForm: function sm_clearInputs() {
this.clearFields();
UIManager.newpinInput.value = '';
UIManager.confirmNewpinInput.value = '';
UIManager.pinInput.value = '';
UIManager.pukInput.value = '';
UIManager.pukInfo.classList.remove('hidden');
UIManager.xckInput.value = '';
UIManager.unlockSimButton.disabled = false;
},
hideScreen: function sm_hideScreen() {
UIManager.unlockSimScreen.classList.remove('show');
UIManager.pincodeScreen.classList.remove('show');
UIManager.pukcodeScreen.classList.remove('show');
UIManager.xckcodeScreen.classList.remove('show');
UIManager.activationScreen.classList.add('show');
},
skip: function sm_skip() {
if (this._unlockingIcc) {
this._unlockingIcc.skipped = true;
}
this.resetForm();
this.hideScreen();
this.handleCardState();
},
resetSkipped: function sm_resetSkipped() {
if (this.icc0) {
this.icc0.skipped = false;
}
if (this.icc1) {
this.icc1.skipped = false;
}
},
back: function sm_back() {
this.resetForm();
this.resetSkipped();
this.hideScreen();
this.hideSIMInfoScreen();
Navigation.back();
},
simUnlockBack: function sm_simUnlockBack() {
if (this.icc0 && this.icc0.skipped) {
this.icc0.skipped = false;
this.hideScreen();
this.handleCardState();
} else {
this.back();
}
},
unlock: function sm_unlock() {
var icc = this._unlockingIcc;
if (!icc) {
throw new Error('Cannot unlock SIM, no current ICC');
}
this.clearFields();
switch (icc.mozIcc.cardState) {
case 'pinRequired':
this.unlockPin(icc);
break;
case 'pukRequired':
this.unlockPuk(icc);
break;
case 'networkLocked':
case 'corporateLocked':
case 'serviceProviderLocked':
case 'network1Locked':
case 'network2Locked':
case 'hrpdNetworkLocked':
case 'ruimCorporateLocked':
case 'ruimServiceProviderLocked':
this.unlockXck(icc);
break;
}
},
unlockPin: function sm_unlockPin(icc) {
var pin = UIManager.pinInput.value;
if (pin.length < 4 || pin.length > 8) {
UIManager.pinError.
querySelector('.main').setAttribute('data-l10n-id', 'pinValidation');
UIManager.pinInput.classList.add('onerror');
UIManager.pinError.classList.remove('hidden');
UIManager.pinInput.focus();
return;
} else {
UIManager.pinInput.classList.remove('onerror');
UIManager.pinError.classList.add('hidden');
}
// Unlock SIM
this.attemptUnlock(icc, {lockType: 'pin', pin: pin });
},
clearFields: function sm_clearFields() {
UIManager.pukInput.classList.remove('onerror');
UIManager.pukError.classList.add('hidden');
UIManager.newpinInput.classList.remove('onerror');
UIManager.newpinError.classList.add('hidden');
UIManager.confirmNewpinInput.classList.remove('onerror');
UIManager.confirmNewpinError.classList.add('hidden');
UIManager.pinError.classList.add('hidden');
UIManager.pinError.querySelector('.main').classList.add('hidden');
UIManager.pinError.querySelector('.lastchance').classList.add('hidden');
UIManager.pinRetriesLeft.classList.add('hidden');
UIManager.pukError.classList.add('hidden');
UIManager.pukError.querySelector('.main').classList.add('hidden');
UIManager.pukError.querySelector('.lastchance').classList.add('hidden');
UIManager.pukRetriesLeft.classList.add('hidden');
UIManager.xckError.classList.add('hidden');
UIManager.xckError.querySelector('.main').classList.add('hidden');
UIManager.xckError.querySelector('.lastchance').classList.add('hidden');
UIManager.xckRetriesLeft.classList.add('hidden');
},
unlockPuk: function sm_unlockPuk(icc) {
var pukCode = UIManager.pukInput.value;
if (pukCode.length !== 8) {
UIManager.pukError.querySelector('.main')
.setAttribute('data-l10n-id', 'pukValidation');
UIManager.pukError.classList.remove('hidden');
UIManager.pukInfo.classList.add('hidden');
UIManager.pukInput.classList.add('onerror');
UIManager.pukError.focus();
return;
}
var newpinCode = UIManager.newpinInput.value;
var confirmNewpin = UIManager.confirmNewpinInput.value;
if (newpinCode.length < 4 || newpinCode.length > 8) {
UIManager.newpinError.setAttribute('data-l10n-id', 'pinValidation');
UIManager.newpinError.classList.remove('hidden');
UIManager.newpinInput.classList.add('onerror');
UIManager.newpinError.focus();
return;
}
if (newpinCode != confirmNewpin) {
UIManager.confirmNewpinError.setAttribute('data-l10n-id',
'newpinConfirmation');
UIManager.confirmNewpinError.classList.remove('hidden');
UIManager.newpinInput.classList.add('onerror');
UIManager.confirmNewpinInput.classList.add('onerror');
UIManager.confirmNewpinError.focus();
return;
}
// Unlock SIM with PUK and new PIN
var options = {lockType: 'puk', puk: pukCode, newPin: newpinCode };
this.attemptUnlock(icc, options);
},
unlockXck: function sm_unlockXck(icc) {
var xck = UIManager.xckInput.value;
var lockType;
switch (icc.mozIcc.cardState) {
case 'networkLocked':
lockType = 'nck';
break;
case 'corporateLocked':
lockType = 'cck';
break;
case 'serviceProviderLocked':
lockType = 'spck';
break;
case 'network1Locked':
lockType = 'nck1';
break;
case 'network2Locked':
lockType = 'nck2';
break;
case 'hrpdNetworkLocked':
lockType = 'hnck';
break;
case 'ruimCorporateLocked':
lockType = 'rcck';
break;
case 'ruimServiceProviderLocked':
lockType = 'rspck';
break;
}
if (xck.length < 8 || xck.length > 16) {
UIManager.xckInput.classList.add('onerror');
UIManager.xckError.classList.remove('hidden');
UIManager.xckError.querySelector('.main').
setAttribute('data-l10n-id', lockType + 'Validation');
UIManager.xckInput.focus();
return;
} else {
UIManager.pinInput.classList.remove('onerror');
UIManager.pinError.classList.add('hidden');
}
// Unlock SIM
var options = {lockType: lockType, pin: xck };
this.attemptUnlock(icc, options);
},
attemptUnlock: function sm_attemptUnlock(icc, options) {
var req = icc.mozIcc.unlockCardLock(options);
req.onsuccess = (function sm_unlockSuccess() {
icc.unlocked = true;
this.resetForm();
this.hideScreen();
this.handleCardState();
}).bind(this);
req.onerror = (function sm_unlockError() {
this.handleUnlockError(options.lockType, req.error.retryCount);
}).bind(this);
},
// Try to infer whats the default SIM
guessIcc: function guessIcc() {
var tempIcc = null;
if (navigator.mozMobileConnections) {
// New multi-sim api, use mozMobileConnections to guess
// the first inserted sim
for (var i = 0;
i < navigator.mozMobileConnections.length && tempIcc === null; i++) {
if (navigator.mozMobileConnections[i] !== null &&
navigator.mozMobileConnections[i].iccId !== null) {
tempIcc = navigator.mozIccManager.getIccById(
navigator.mozMobileConnections[i].iccId);
}
}
} else {
tempIcc = navigator.mozIccManager;
}
return tempIcc;
},
importContacts: function sm_importContacts() {
// Delay for showing feedback to the user after importing
var DELAY_FEEDBACK = 300;
UIManager.navBar.setAttribute('aria-disabled', 'true');
var progress = utils.overlay.show('simContacts-reading',
'activityBar');
var importButton = UIManager.simImportButton;
var cancelled = false,
contactsRead = false;
var importer = new SimContactsImporter(SimManager.guessIcc());
utils.overlay.showMenu();
utils.overlay.oncancel = function oncancel() {
cancelled = true;
importer.finish();
if (contactsRead) {
// A message about canceling will be displayed while the current chunk
// is being cooked
progress.setClass('activityBar');
utils.overlay.hideMenu();
progress.setHeaderMsg('messageCanceling');
} else {
importer.onfinish(); // Early return while reading contacts
}
};
var importedContacts = 0;
importer.onread = function sim_import_read(n) {
contactsRead = true;
if (n > 0) {
progress.setClass('progressBar');
progress.setHeaderMsg('simContacts-importing');
progress.setTotal(n);
}
};
importer.onimported = function imported_contact() {
importedContacts++;
if (!cancelled) {
progress.update();
}
};
importer.onfinish = function sim_import_finish(numDupsMerged, iccId) {
window.setTimeout(function do_sim_import_finish() {
UIManager.navBar.removeAttribute('aria-disabled');
utils.overlay.hide();
if (importedContacts > 0) {
utils.misc.setTimestamp('sim-' + iccId);
}
if (!cancelled) {
SimManager.alreadyImported = true;
importButton.setAttribute('disabled', 'disabled');
utils.status.show({
id: 'simContacts-imported3',
args: {n: importedContacts}
});
}
}, DELAY_FEEDBACK);
importer.onfinish = null;
};
importer.onerror = function sim_import_error() {
UIManager.navBar.removeAttribute('aria-disabled');
utils.overlay.hide();
// Just in case the user decides to do so later
importButton.removeAttribute('disabled');
// 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
importButton.click();
}
};
ConfirmDialog.show(null, 'simContacts-error', cancel, retry);
}; // importer.onerror
importer.start();
}
};
})();

35
gaia/apps/ftu/js/time.js Executable file
View file

@ -0,0 +1,35 @@
/* exported TimeManager */
'use strict';
var TimeManager = {
elementIDs: [
'tz-region',
'tz-city',
'date-configuration',
'time-configuration'
],
init: function tm_init() {
if (window.navigator.mozTime) {
this.time = window.navigator.mozTime;
} else {
console.log('There is no mozTime available in window');
}
this.elementIDs.forEach(function(elementID) {
document.getElementById(elementID + '-button').addEventListener('click',
function(evt) {
evt.preventDefault();
var input = document.getElementById(elementID);
// Inspired by app/clock/js/form_button.js
// It seems like setting focus instantly does not trigger the input/
// select dialog.
setTimeout(input.focus.bind(input), 10);
});
});
},
set: function tm_set(date) {
this.time.set(date);
}
};

383
gaia/apps/ftu/js/tutorial.js Executable file
View file

@ -0,0 +1,383 @@
/* global ScreenLayout, Promise,
Utils, FinishScreen, LazyLoader, TutorialUtils */
/* exported Tutorial */
(function(exports) {
'use strict';
// default layout
var currentLayout = 'tiny';
// Most used DOM elements
var dom = {};
/**
* Manages and controls the configuration, content and state of the tutorial
* @module Tutorial
*/
var elementIDs = [
'tutorial',
'tutorial-step-title',
'tutorial-step-media',
'tutorial-step-image',
'tutorial-step-video',
'forward-tutorial',
'back-tutorial'
];
var Tutorial = {
// A configuration object.
config: null,
// Config for the current tutorial steps
_stepsConfig: {},
// Which tutorial steps to use
_stepsKey: '',
// Keeps track of the current step
_currentStep: 1,
// Track initialized state of tutorial
_initialized: false,
/**
* Initialize the tutorial. This is async as a config file must be loaded.
* A Sequence (array of sync or async functions) is used to manage the init
* tasks.
* When complete, the tutorial is ready to be shown via the 'start' method
*
* @param {String} stepsKey a key into the tutorial config object, i
* i.e. a version or version delta like 1.3..2.0
* @param {Function} onLoaded optional callback for when init is complete
* @memberof Tutorial
*/
init: function(stepsKey, onLoaded) {
// init is async
// need to load config, then load the first step and its assets.
if (this._initialized || this._initialization) {
// init already underway
return;
}
var initTasks = this._initialization = new TutorialUtils.Sequence(
// config should load or already be loaded.
// failure should abort
this.loadConfig.bind(this),
this._initWithConfig.bind(this, stepsKey)
);
initTasks.onabort = this._onAbortInitialization.bind(this);
initTasks.oncomplete =
this._onCompleteInitialization.bind(this, onLoaded);
// first time here: cache DOM elements
elementIDs.forEach(function(name) {
dom[Utils.camelCase(name)] = document.getElementById(name);
if (!dom[Utils.camelCase(name)]) {
console.error('Cache DOM elements: couldnt cache: ' + name);
}
}, this);
initTasks.next();
// watch a ton of video events as context for the tutorial
// media loading/playing
var mediaEvents = this._mediaEvents = ['abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'ended',
'error',
'interruptbegin',
'interruptend',
'loadeddata',
'loadedmetadata',
'loadstart',
'mozaudioavailable',
'pause',
'play',
'playing',
'stalled',
'suspend',
'waiting'];
this._debugEventHandler = {
handleEvent: function(evt) {
var errCodes = {
'1': 'MEDIA_ERR_ABORTED',
'2': 'MEDIA_ERR_NETWORK',
'3': 'MEDIA_ERR_DECODE',
'4': 'MEDIA_ERR_SRC_NOT_SUPPORTED'
};
console.log('TUTORIAL: media-event: '+ evt.type);
if (evt.type == 'error') {
var errtype = errCodes[evt.target.error.code] || 'unknown';
console.log('TUTORIAL: media-event, ' + errtype + ' error');
}
}
};
mediaEvents.forEach(name => {
dom.tutorialStepVideo.addEventListener(name, this._debugEventHandler);
});
},
/**
* Show the tutorial and play the first step
* May be called during or after the init process
* We defer rasing the 'tutorialinitialized' event until this point
* as it signals the tutorial step content is loaded and displayed
*
* @param {Function} onReady optional callback for when start is complete
* @memberof Tutorial
*/
start: function(onReady) {
var sequence;
var initInProgress = false;
if (this._initialization) {
// init still underway, tack steps onto existing sequence
sequence = this._initialization;
initInProgress = true;
} else {
// init already complete, create new sequence
this._initialization = sequence = new TutorialUtils.Sequence();
sequence.onabort = this._onAbortInitialization.bind(this);
sequence.oncomplete =
this._onCompleteInitialization.bind(this);
}
sequence.push(function setInitialStep() {
// setStep should return promise given by _loadMedia
console.log('TUTORIAL: setInitialStep');
return this._setStep();
}.bind(this));
sequence.push(function showTutorialAndFinishInit() {
// Show the panel
dom.tutorial.classList.add('show');
// Custom event that can be used to apply (screen reader) visibility
// changes.
console.log('TUTORIAL: dispatching tutorialinitialized');
window.dispatchEvent(new CustomEvent('tutorialinitialized'));
});
if(typeof onReady === 'function') {
sequence.push(onReady);
}
if (!initInProgress) {
// init done, starting start sequence
sequence.next();
}
},
/**
* Continue initialization once config data is loaded
* Called as a part of the init sequence
*
* @param {String} stepsKey a key into the tutorial config object, i
* i.e. a version or version delta like 1.3..2.0
* @memberof Tutorial
*/
_initWithConfig: function(stepsKey) {
stepsKey = stepsKey || 'default';
this._stepsKey = stepsKey;
this._stepsConfig = this.config[stepsKey] || this.config['default'];
// Add event listeners
dom.forwardTutorial.addEventListener('click', this);
dom.backTutorial.addEventListener('click', this);
// Set the first step
this._currentStep = 1;
},
_onAbortInitialization: function() {
this._initialization = null;
},
_onCompleteInitialization: function(onReady) {
this._initialization = null;
this._initialized = true;
if(typeof onReady === 'function') {
onReady();
}
},
/**
* Advance the tutorial to the given step (first step if no value given)
*
* @param {Number} value number of the step to show (1-based)
* @memberof Tutorial
*/
_setStep: function (value) {
// If value is bigger than the max, show finish screen
value = (typeof value === 'number') ? value : this._currentStep;
var stepIndex = value - 1;
if (stepIndex >= this._stepsConfig.steps.length) {
return Promise.resolve().then(function() {
Tutorial.done();
});
}
var stepData = this._stepsConfig.steps[stepIndex];
if (!stepData) {
return Promise.reject('No data for step: ' + value);
}
// Set the step
dom.tutorial.dataset.step = this._currentStep;
// Internationalize
document.l10n.setAttributes(
dom.tutorialStepTitle,
stepData.l10nKey
);
// Update the image/video
var imgElement = dom.tutorialStepImage,
videoElement = dom.tutorialStepVideo;
var stepPromise;
if (stepData.video) {
stepPromise = TutorialUtils.getBestAssetForDirection(stepData.video)
.then((bestSrc) => {
return TutorialUtils.loadAndPlayMedia(videoElement, bestSrc);
});
videoElement.hidden = false;
imgElement.hidden = true;
} else {
imgElement.hidden = false;
stepPromise = TutorialUtils.loadMedia(imgElement, stepData.image);
imgElement.hidden = false;
videoElement.hidden = true;
}
return stepPromise;
},
/**
* DOM Event handler
*
* @param {DOMEvent} evt Event object
* @memberof Tutorial
*/
handleEvent: function(evt) {
if (evt.type === 'click') {
switch(evt.target) {
case dom.forwardTutorial:
this.next(evt);
break;
case dom.backTutorial:
this.back(evt);
break;
}
}
},
/**
* Advance to the next step in the tutorial
* @memberof Tutorial
*/
next: function() {
return this._setStep(++this._currentStep);
},
/**
* Go back to the previous step in the tutorial
* @memberof Tutorial
*/
back: function() {
return this._setStep(--this._currentStep);
},
/**
* Tutorial complete
* @memberof Tutorial
*/
done: function() {
var isUpgrade = this._stepsKey && this._stepsKey !== 'default';
FinishScreen.init(isUpgrade);
dom.tutorial.classList.remove('show');
dom.tutorialStepVideo.removeAttribute('src');
dom.tutorialStepImage.removeAttribute('src');
},
/**
* Load the config with steps and associated resources for the tutorial
* We have different config files for each screen category:
* phone is 'tiny', tablet is 'large'
* and corresponding tiny.json, large.json
*
* @returns {Promise}
* @memberof Tutorial
*/
loadConfig: function() {
if (!this._configPromise) {
// Update the value of the layout if needed
// 'ScreenLayout' give us 4 different values
// tiny: '(max-width: 767px)',
// small: '(min-width: 768px) and (max-width: 991px)',
// medium: '(min-width: 992px) and (max-width: 1200px)',
// large: '(min-width: 1201px)',
//
// Currently we are taking into account only 'tiny', and we are
// going to consider 'tablet' as 'large'. If we want to add more
// or specific features for 'small' & 'medium', we should add more
// logic here.
currentLayout = ScreenLayout.getCurrentLayout() === 'tiny' ?
'tiny' : 'large';
var configUrl = '/config/' + currentLayout + '.json';
this._configPromise = LazyLoader.getJSON(configUrl)
.then(function(json) {
Tutorial.config = json;
return Tutorial.config;
}, function() {
return new Error('Tutorial config failed to load from: ' + configUrl);
});
}
return this._configPromise;
},
/**
* Test helper to reset the tutorial to its pre-initialized state
* to allow init to be called again
* @memberof Tutorial
*/
reset: function() {
var resetPromise = Promise.resolve();
if (dom.tutorialStepVideo) {
this._mediaEvents.forEach(name => {
dom.tutorialStepVideo.removeEventListener(
name,
this._debugEventHandler
);
});
dom.tutorialStepVideo.hidden = true;
if (dom.tutorialStepVideo.src) {
resetPromise = new Promise((resolve, reject) => {
dom.tutorialStepVideo.addEventListener('emptied', () => {
resolve();
});
dom.tutorialStepVideo.removeAttribute('src');
dom.tutorialStepVideo.load();
});
}
}
if (this._initialization) {
this._initialization.abort();
this._initialization = null;
}
this._configPromise = null;
this._currentStep = 1;
this._stepsConfig = this.config = null;
if (this._initialized) {
dom.tutorial.classList.remove('show');
this._initialized = false;
}
document.getElementById('tutorial').classList.remove('show');
return resetPromise;
}
};
exports.Tutorial = Tutorial;
})(this);

View file

@ -0,0 +1,179 @@
(function(exports) {
'use strict';
var TutorialUtils = {
/**
* Helper function to load images and video
* @param {DOMNode} mediaElement video or image to assign new src to
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
loadMedia: function (mediaElement, src) {
console.log('loadMedia, got mediaElement: ', mediaElement);
var isVideo = (mediaElement.nodeName === 'VIDEO');
console.log('TutorialUtils.loadMedia, isVideo: ',
isVideo, mediaElement.nodeName);
return new Promise(function(resolve, reject) {
function onMediaLoadOrError(evt) {
console.log('TutorialUtils.loadMedia, onMediaLoadOrError for ' +
'evt.type: ' + (evt && evt.type));
evt.target.removeEventListener('error', onMediaLoadOrError);
if (isVideo) {
evt.target.removeEventListener('canplay', onMediaLoadOrError);
evt.target.removeEventListener('abort', onMediaLoadOrError);
} else {
evt.target.removeEventListener('load', onMediaLoadOrError);
}
// Dont block progress on failure to load media
if (evt.type === 'error') {
console.error('Failed to load tutorial media: ' + src);
} else if (evt.type === 'abort') {
console.error('Loading of tutorial media aborted: ' + src);
}
resolve(evt);
}
function onVideoUnloaded(evt) {
console.log('TutorialUtils.loadMedia, onVideoUnloaded for evt.type: ' +
(evt && evt.type));
mediaElement.removeEventListener('emptied', onVideoUnloaded);
mediaElement.addEventListener('canplay', onMediaLoadOrError);
mediaElement.addEventListener('abort', onMediaLoadOrError);
mediaElement.addEventListener('error', onMediaLoadOrError);
console.log('TutorialUtils.loadMedia, onVideoUnloaded, ' +
'assigning src: ' + src);
mediaElement.src = src;
mediaElement.load();
}
if (isVideo) {
// must unload video and force load before switching to new source
if (mediaElement.src) {
console.log('TutorialUtils.loadMedia, removing src attribute');
mediaElement.removeAttribute('src');
mediaElement.addEventListener('emptied', onVideoUnloaded, false);
console.log('TutorialUtils.loadMedia, calling load');
mediaElement.load();
} else {
console.log('TutorialUtils.loadMedia, no src, ' +
'just calling onVideoUnloaded');
onVideoUnloaded();
}
} else {
console.log('TutorialUtils.loadMedia, not a video, ' +
'listen for load/error only');
mediaElement.addEventListener('load', onMediaLoadOrError, false);
mediaElement.addEventListener('error', onMediaLoadOrError, false);
mediaElement.src = src;
}
});
},
/**
* Convenience to load and then play video
* @param {DOMNode} mediaElement video or image to assign new src to
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
loadAndPlayMedia: function(mediaElement, src) {
return this.loadMedia(mediaElement, src).then((evt) => {
if (evt.type !== 'error' && mediaElement.nodeName === 'VIDEO') {
mediaElement.play();
}
});
},
/**
* Helper to check (via HEAD request) a given resource exists
* The returned promise resolves with a true|false value
*
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
fileExists: function (src) {
return new Promise((resolve) => {
window.fetch(src, { method: 'HEAD' }).then((response) => {
resolve(response.ok);
}).catch(() => {
resolve(false);
});
});
},
/**
* Helper to get best-match resource url for the current document.dir
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
getBestAssetForDirection: function(src) {
var dir = document.dir || 'ltr';
var re =
/(-rtl|ltr)?\.(png|gif|jpg|webm|mp4|m4v|ogg|ogv)$/;
var directionSrc = src.replace(re, '-'+dir+'.$2');
return this.fileExists(directionSrc).then((result) => {
return result ? directionSrc : src;
}).catch((e) => {
console.log('Exception from getBestAssetForDirection: ', e);
});
},
/**
* Private helper class to manage a series of sync or async functions
*
* The array may be manipulated using standard array methods while the
* sequence runs. The sequence completes when there are no more functions or
* an exception is raised.
* At the end of the sequence, any 'oncomplete' assigned will be called with
* the return value from the last function
* Functions may return a 'thenable' to indicate async return
* Exceptions will be passed into the oncomplete function
* A Sequence may be cleanly aborted by calling abort() - no callbacks will
* be fired
* @class Sequence
*/
Sequence: function() {
var sequence = Array.slice(arguments);
var aborted = false;
sequence.abort = function() {
aborted = true;
this.length = 0;
if (typeof this.onabort === 'function') {
this.onabort();
}
};
sequence.complete = function(result) {
if(!aborted && typeof this.oncomplete === 'function') {
this.oncomplete(result);
}
};
sequence.fail = function(reason) {
this.complete(reason);
};
sequence.next = function(previousTaskResult) {
var result, exception;
if (aborted) {
return;
}
var task = this.shift();
if (task) {
try {
result = task.apply(null, arguments);
} catch(e) {
exception = e;
}
if (exception) {
this.fail(exception);
} else if (result && typeof result.then === 'function') {
result.then(this.next.bind(this), this.fail.bind(this));
} else {
this.next(result);
}
} else {
this.complete(previousTaskResult);
}
};
return sequence;
}
};
exports.TutorialUtils = TutorialUtils;
})(window);

650
gaia/apps/ftu/js/ui.js Executable file
View file

@ -0,0 +1,650 @@
/* global utils, tzSelect, FinishScreen,
Basket, ConfirmDialog, ScreenLayout,
DataMobile, SimManager, SdManager,
Tutorial, TimeManager, WifiManager,
WifiUI, WifiHelper, FxAccountsIACHelper, SettingsListener */
/* exported UIManager */
'use strict';
var UIManager = {
// As in other Gaia apps, we store all the dom selectors in one
// place and then camelCase them and attach to the main object,
// eg. instead of calling document.getElementById('splash-screen')
// we can access this.splashScreen in our code.
domSelectors: [
'container',
'splash-screen',
'activation-screen',
'finish-screen',
'update-screen',
'nav-bar',
'main-title',
// Unlock SIM Screen
'unlock-sim-screen',
'unlock-sim-header',
'unlock-sim-action',
// PIN Screen
'pincode-screen',
'pin-label',
'pin-retries-left',
'pin-input',
'back-sim-button',
'pin-error',
'skip-pin-button',
'unlock-sim-button',
// PUK Screen
'pukcode-screen',
'puk-label',
'puk-retries-left',
'puk-input',
'puk-info',
'puk-error',
'newpin-input',
'newpin-error',
'confirm-newpin-input',
'confirm-newpin-error',
// XCK Screen
'xckcode-screen',
'xck-label',
'xck-retries-left',
'xck-input',
'xck-error',
// SIM info
'sim-info-screen',
'sim-info-back',
'sim-info-forward',
'sim-info-1',
'sim-info-2',
'sim-number-1',
'sim-number-2',
'sim-carrier-1',
'sim-carrier-2',
// Import contacts
'sim-import',
'sim-import-button',
'no-sim',
'sd-import-button',
'no-memorycard',
// Fxa Intro
'fxa-create-account',
'fxa-intro',
// Wifi
'networks',
'wifi-refresh-button',
'wifi-join-button',
'join-hidden-button',
// Hidden Wifi
'hidden-wifi-authentication',
'hidden-wifi-ssid',
'hidden-wifi-security',
'hidden-wifi-password',
'hidden-wifi-password-box',
'hidden-wifi-identity',
'hidden-wifi-identity-box',
'hidden-wifi-show-password',
//Date & Time
'date-configuration',
'time-configuration',
'date-configuration-label',
'time-configuration-label',
'time-form',
// 3G
'data-connection-switch',
// Geolocation
'geolocation-switch',
// Tutorial
'lets-go-button',
'update-lets-go-button',
'skip-tutorial-button',
'update-skip-tutorial-button',
// Privacy Settings
'metrics-basic',
'metrics-enhanced',
'metrics-none',
'offline-error-dialog',
// Browser privacy newsletter subscription
'newsletter-form',
'newsletter-input',
'newsletter-success-screen',
'offline-newsletter-error-dialog',
'invalid-email-error-dialog'
],
dataConnectionChangedByUsr: false,
timeZoneNeedsConfirmation: true,
DARK_THEME: '#242d33',
LIGHT_THEME: '#eeeeee',
init: function ui_init() {
// Initialization of the DOM selectors
this.domSelectors.forEach(function createElementRef(name) {
this[toCamelCase(name)] = document.getElementById(name);
}.bind(this));
// Setup settings observers
this._settingsObserveHandler = {
'geolocation.enabled': {
// the "checked" attribute in the DOM is currently the source of truth
// for default value, when the setting is not initially defined
defaultValue: this.geolocationSwitch.checked,
callback: function(value) {
var isEnabled = !!value;
if (this.geolocationSwitch.checked !== isEnabled) {
this.geolocationSwitch.checked = isEnabled;
}
}.bind(this)
}
};
for (var name in this._settingsObserveHandler) {
SettingsListener.observe(
name,
this._settingsObserveHandler[name].defaultValue,
this._settingsObserveHandler[name].callback
);
}
var currentDate = new Date();
this.timeConfigurationLabel.textContent = currentDate.toLocaleString(
navigator.languages,
{
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
);
this.dateConfigurationLabel.textContent = currentDate.toLocaleString(
navigator.languages,
{
year: 'numeric',
month: 'numeric',
day: 'numeric'
}
);
// Add events to DOM
this.simImportButton.addEventListener('click', this);
this.sdImportButton.addEventListener('click', this);
this.skipPinButton.addEventListener('click', this);
this.backSimButton.addEventListener('click', this);
this.unlockSimButton.addEventListener('click', this);
this.unlockSimAction.addEventListener('action', this);
this.simInfoBack.addEventListener('click', this);
this.simInfoForward.addEventListener('click', this);
this.dataConnectionSwitch.addEventListener('change', this);
this.wifiRefreshButton.addEventListener('click', this);
this.wifiJoinButton.addEventListener('click', this);
this.networks.addEventListener('click', this);
this.joinHiddenButton.addEventListener('click', this);
this.hiddenWifiSecurity.addEventListener('change', this);
this.wifiJoinButton.disabled = true;
var checkHiddenWifiJoin = function() {
this.wifiJoinButton.disabled = this.hiddenWifiSsid.value === '' ||
!WifiHelper.isValidInput(this.hiddenWifiSecurity.value,
this.hiddenWifiPassword.value,
this.hiddenWifiIdentity.value
);
}.bind(this);
this.hiddenWifiSsid.addEventListener('keyup', checkHiddenWifiJoin);
this.hiddenWifiIdentity.addEventListener('keyup', checkHiddenWifiJoin);
this.hiddenWifiPassword.addEventListener('keyup', checkHiddenWifiJoin);
this.hiddenWifiSecurity.addEventListener('change', checkHiddenWifiJoin);
this.hiddenWifiShowPassword.onchange = function togglePasswordVisibility() {
UIManager.hiddenWifiPassword.type = this.checked ? 'text' : 'password';
};
this.timeConfiguration.addEventListener('input', this);
this.dateConfiguration.addEventListener('input', this);
this.geolocationSwitch.addEventListener('change', this);
this.fxaCreateAccount.addEventListener('click', this);
// Prevent form submit in case something tries to send it
this.timeForm.addEventListener('submit', function(event) {
event.preventDefault();
});
// Input scroll workaround
this.newsletterInput.addEventListener('focus', function() {
window.addEventListener('resize', function resize() {
window.removeEventListener('resize', resize);
// Need to wait till resize is done
setTimeout(function() {
var page = document.getElementById('browser_privacy');
UIManager.scrollToElement(page, UIManager.newsletterInput);
}, 30);
});
});
this.offlineNewsletterErrorDialog
.querySelector('button')
.addEventListener('click',
function offlineDialogClick() {
this.offlineNewsletterErrorDialog.classList.remove('visible');
}.bind(this));
this.invalidEmailErrorDialog
.querySelector('button')
.addEventListener('click',
function invalidEmailDialogClick() {
this.invalidEmailErrorDialog.classList.remove('visible');
}.bind(this));
var skipTutorialAction = function() {
// Stop Wifi Manager
WifiManager.finish();
// For tiny devices
if (ScreenLayout.getCurrentLayout() === 'tiny') {
window.close();
} else {
// for large devices
FinishScreen.init();
}
};
var startTutorialAction = function(evt) {
// Stop Wifi Manager
WifiManager.finish();
// Play the tutorial steps as soon as config is done loading
Tutorial.start(function onTutorialLoaded() {
UIManager.activationScreen.classList.remove('show');
UIManager.updateScreen.classList.remove('show');
UIManager.finishScreen.classList.remove('show');
});
};
this.skipTutorialButton.addEventListener('click', skipTutorialAction);
this.updateSkipTutorialButton.addEventListener('click', skipTutorialAction);
this.letsGoButton.addEventListener('click', startTutorialAction);
this.updateLetsGoButton.addEventListener('click', startTutorialAction);
// Enable saving metrics level choice to settings.
this.metricsBasic.addEventListener('change', this);
this.metricsEnhanced.addEventListener('change', this);
this.metricsNone.addEventListener('change', this);
var button = this.offlineErrorDialog.querySelector('button');
button.addEventListener('click',
this.onOfflineDialogButtonClick.bind(this));
// Handle activation screen visibility.
['confirmdialogshowing',
'loadingoverlayshowing',
'tutorialinitialized'].forEach(function(event) {
window.addEventListener(event,
this.hideActivationScreenFromScreenReader.bind(this));
}, this);
['confirmdialoghiding',
'loadingoverlayhiding'].forEach(function(event) {
window.addEventListener(event,
this.showActivationScreenToScreenReader.bind(this));
}, this);
this.checkInitialFxAStatus();
},
scrollToElement: function ui_scrollToElement(container, element) {
container.scrollTop = element.offsetTop;
},
sendNewsletter: function ui_sendNewsletter(callback) {
var self = this;
var emailValue = self.newsletterInput.value;
if (emailValue === '') {
return callback(true);
} else {
utils.overlay.show('email-loading', 'spinner');
if (self.newsletterInput.checkValidity()) {
if (window.navigator.onLine) {
Basket.send(emailValue, function emailSent(err, data) {
if (err) {
if (err.code && err.code === Basket.errors.INVALID_EMAIL) {
ConfirmDialog.show('invalid-email-dialog-title',
'invalid-email-dialog-text',
{
title: 'cancel',
callback: function ok() {
ConfirmDialog.hide();
}
});
utils.overlay.hide();
return callback(false);
} else {
Basket.store(emailValue);
}
}
utils.overlay.hide();
return callback(true);
});
} else {
Basket.store(emailValue, function stored(errorStoring) {
utils.overlay.hide();
return callback(!errorStoring);
});
}
} else {
utils.overlay.hide();
ConfirmDialog.show('invalid-email-dialog-title',
'invalid-email-dialog-text',
{
title: 'cancel',
callback: function ok() {
ConfirmDialog.hide();
}
});
return callback(false);
}
}
},
initTZ: function ui_initTZ() {
// Initialize the timezone selector, see /shared/js/tz_select.js
var self = this;
var tzRegion = document.getElementById('tz-region');
var tzCity = document.getElementById('tz-city');
return new Promise(function(resolve) {
var onchange = self.setTimeZone.bind(self);
var onload = function() {
self.setTimeZone.apply(self, arguments);
resolve();
};
tzSelect(tzRegion, tzCity, onchange, onload);
});
},
handleEvent: function ui_handleEvent(event) {
switch (event.target.id) {
// SIM
case 'skip-pin-button':
SimManager.skip();
break;
case 'back-sim-button':
case 'sim-info-back':
SimManager.back();
break;
case 'unlock-sim-button':
SimManager.unlock();
break;
case 'unlock-sim-action':
SimManager.simUnlockBack();
break;
case 'sim-info-forward':
SimManager.finish();
break;
case 'sim-import-button':
// Needed to give the browser the opportunity to properly refresh the UI
// Particularly the button toggling cycle (from inactive to active)
window.setTimeout(SimManager.importContacts, 0);
break;
case 'sd-import-button':
// Needed to give the browser the opportunity to properly refresh the UI
// Particularly the button toggling cycle (from inactive to active)
window.setTimeout(SdManager.importContacts, 0);
break;
// 3G
case 'data-connection-switch':
this.dataConnectionChangedByUsr = true;
var status = event.target.checked;
DataMobile.toggle(status);
break;
// WIFI
case 'wifi-refresh-button':
WifiManager.scan(WifiUI.renderNetworks);
break;
case 'wifi-join-button':
if (window.location.hash === '#hidden-wifi-authentication') {
WifiUI.joinHiddenNetwork();
} else {
WifiUI.joinNetwork();
}
break;
case 'join-hidden-button':
WifiUI.addHiddenNetwork();
break;
case 'hidden-wifi-security':
// Assuming that [0] is None, we prefer '' for collision on translations
var securityType = event.target.selectedIndex ? event.target.value : '';
WifiUI.handleHiddenWifiSecurity(securityType);
break;
// Date & Time
case 'time-configuration':
this.setTime();
break;
case 'date-configuration':
this.setDate();
break;
// Geolocation
case 'geolocation-switch':
this.updateSetting(event.target.name, event.target.checked);
break;
// Privacy
case 'metrics-enhanced':
this.updateSetting('hud.hide', (event.target.value === 'Enhanced'));
this.updateSetting('devtools.overlay',
(event.target.value === 'Enhanced'));
/* falls through */
case 'metrics-basic':
case 'metrics-none':
this.updateSetting('metrics.selectedMetrics.level', event.target.value);
break;
case 'fxa-create-account':
this.createFirefoxAccount();
break;
default:
// wifi selection
if (event.target.parentNode.id === 'networks-list') {
WifiUI.chooseNetwork(event);
}
break;
}
},
updateSetting: function ui_updateSetting(name, value) {
if (!name) {
return;
}
var cset = {}; cset[name] = value;
return SettingsListener.getSettingsLock().set(cset);
},
setForwardButtonLabel: function ui_setForwardButtonLabel(label) {
var nextButton = document.getElementById('forward');
nextButton.setAttribute('data-l10n-id', label);
},
checkInitialFxAStatus: function ui_checkInitialFxAStatus() {
// It is possible that we enter the FTU after the user aborted a FTU
// session where she logged into her FxA. In that case, we show the
// information of her account if possible. If there is any reason why we
// can't get the account information, we try to log her out.
// It is quite unlikely that logging out fails, but in that case, we simply
// hide the FxA panel to avoid potential errors such as the one reported on
// bug 1113551. In any case, the user should be able to manage her account
// from the Settings app afterwards.
this.skipFxA = true;
FxAccountsIACHelper.getAccount((account) => {
this.skipFxA = false;
this.onFxALogin(account);
}, () => {
FxAccountsIACHelper.logout(() => {
this.skipFxA = false;
});
});
},
createFirefoxAccount: function ui_createFirefoxAccount() {
FxAccountsIACHelper.openFlow(UIManager.onFxAFlowDone,
UIManager.onFxAError);
},
onFxAFlowDone: function ui_onFxAFlowDone() {
FxAccountsIACHelper.getAccount((account) => {
if (!account) {
return;
}
UIManager.onFxALogin(account);
UIManager.setForwardButtonLabel('navbar-next');
}, UIManager.onFxAError);
},
onFxALogin: function ui_onFxALogin(account) {
if (!account) {
return;
}
// Update the email
UIManager.newsletterInput.value = account.email;
// Update the string
UIManager.fxaIntro.innerHTML = '';
document.l10n.setAttributes(
UIManager.fxaIntro,
account.verified ? 'fxa-signed-in' : 'fxa-email-sent',
{
email: account.email
}
);
// Disable the button
UIManager.fxaCreateAccount.disabled = true;
},
onFxAError: function ui_onFxAError(response) {
console.error('Create FxA Error: ' + JSON.stringify(response));
// Clean fields
UIManager.newsletterInput.value = '';
// Reset the field
document.l10n.setAttributes(
UIManager.fxaIntro,
'fxa-upsell'
);
// Enable the button
UIManager.fxaCreateAccount.disabled = false;
// Change the forward button label
UIManager.setForwardButtonLabel('skip');
},
displayOfflineDialog: function ui_displayOfflineDialog(href, title) {
var dialog = this.offlineErrorDialog;
document.l10n.setAttributes(dialog.querySelector('small'),
'offline-dialog-text', { url: href });
dialog.classList.add('visible');
this.hideActivationScreenFromScreenReader();
},
onOfflineDialogButtonClick: function ui_onOfflineDialogButtonClick(e) {
this.offlineErrorDialog.classList.remove('visible');
this.showActivationScreenToScreenReader();
},
hideActivationScreenFromScreenReader:
function ui_hideActivationScreenFromScreenReader() {
this.activationScreen.setAttribute('aria-hidden', true);
},
showActivationScreenToScreenReader:
function ui_showActivationScreenToScreenReader() {
this.activationScreen.setAttribute('aria-hidden', false);
},
setDate: function ui_sd() {
if (!!this.lock) {
return;
}
// Current time
var now = new Date();
// Format: 2012-09-01
var currentDate = this.dateConfiguration.value;
var currentTime = now.toLocaleFormat('%H:%M');
var timeToSet = new Date(currentDate + 'T' + currentTime);
TimeManager.set(timeToSet);
this.dateConfigurationLabel.textContent = timeToSet.toLocaleString(
navigator.languages,
{
year: 'numeric',
month: 'numeric',
day: 'numeric'
}
);
},
setTime: function ui_st() {
if (!!this.lock) {
return;
}
var timeLabel = document.getElementById('time-configuration-label');
// Current time
var now = new Date();
// Format: 2012-09-01
var currentTime = document.getElementById('time-configuration').value;
if (currentTime.indexOf(':') === 1) { // Format: 8:05 --> 08:05
currentTime = '0' + currentTime;
}
var currentDate = now.toLocaleFormat('%Y-%m-%d');
var timeToSet = new Date(currentDate + 'T' + currentTime);
// Set date through API
TimeManager.set(timeToSet);
// Set DATE properly
timeLabel.textContent = timeToSet.toLocaleString(
navigator.languages,
{
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
);
},
setTimeZone: function ui_stz(timezone, needsConfirmation) {
this.timeZoneNeedsConfirmation = !!needsConfirmation;
var utcOffset = timezone.utcOffset;
document.getElementById('time_zone_overlay').className =
'UTC' + utcOffset.replace(/[+:]/g, '');
var timezoneTitle = document.getElementById('time-zone-title');
document.l10n.setAttributes(timezoneTitle, 'timezoneTitle', {
utcOffset: utcOffset,
region: timezone.region,
city: timezone.city
});
document.getElementById('tz-region-label').textContent = timezone.region;
document.getElementById('tz-city-label').textContent = timezone.city;
var now = new Date();
var timeLabel = document.getElementById('time-configuration-label');
timeLabel.textContent = now.toLocaleString(
navigator.languages,
{
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
);
},
updateDataConnectionStatus: function ui_udcs(status) {
this.dataConnectionSwitch.checked = status;
},
changeStatusBarColor: function ui_csbc(color) {
var themeMeta = document.head.querySelector('meta[name="theme-color"]');
themeMeta.setAttribute('content', color);
}
};
function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
}

16
gaia/apps/ftu/js/utils.js Executable file
View file

@ -0,0 +1,16 @@
(function(exports) {
'use strict';
var rdashes = /-(.)/g;
var Utils = {
camelCase: function ut_camelCase(str) {
return str.replace(rdashes, function(str, p1) {
return p1.toUpperCase();
});
}
};
exports.Utils = Utils;
}(this));

485
gaia/apps/ftu/js/wifi.js Executable file
View file

@ -0,0 +1,485 @@
/* global utils, UIManager, WifiHelper, WifiUI:true */
/* exported WifiManager, WifiUI */
(function(exports) {
'use strict';
const WIFI_STATUS = {
connecting: 'connecting',
associated: 'associated',
connected: 'connected',
connectingfailed: 'connectingfailed',
disconnected: 'disconnected'
};
var WifiManager = {
STATUS: Object.freeze(WIFI_STATUS),
init: function wn_init() {
this.api = WifiHelper.getWifiManager();
this.changeStatus();
// Ensure that wifi is on.
if (navigator.mozSettings) {
var lock = window.navigator.mozSettings.createLock();
this.enable(lock);
this.enableDebugging(lock);
}
this.gCurrentNetwork = this.api ? this.api.connection.network : null;
if (this.gCurrentNetwork !== null) {
this.api.forget(this.gCurrentNetwork);
this.gCurrentNetwork = null;
}
},
getNetworks: function wn_getNetworks(callback) {
this.networks ? callback(this.networks) : this.scan(callback);
},
scan: function wn_scan(callback) {
if (this._scanning) {
return;
}
this._scanning = true;
utils.overlay.show('scanningNetworks', 'spinner');
var SCAN_TIMEOUT = 10000;
var self = this;
self.onScan = callback;
var req = this.api ? this.api.getNetworks() : null;
if (!req) {
// When no wifi API is available (ie. shimless B2G-desktop),
// we will yield to the event loop before calling the callback
// to prevent a race condition with the wifi overlay.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=957769#c5
setTimeout(function() {
self._scanning = false;
callback();
});
return;
}
var handleRequest = function handleRequest() {
self._scanning = false;
clearTimeout(self.scanTimeout);
self.scanTimeout = null;
self.onScan(self.networks);
self.onScan = null;
};
req.onsuccess = function onScanSuccess() {
self.networks = req.result;
handleRequest();
};
req.onerror = function onScanError() {
console.error('Error reading networks: ' + req.error.name);
self.networks = [];
handleRequest();
};
// Timeout in case of scanning errors not thrown by the API
// We can't block the user in the screen (bug 889623)
if (!self.scanTimeout) {
self.scanTimeout = setTimeout(function() {
self._scanning = false;
console.warn('Timeout while reading networks');
self.onScan();
}, SCAN_TIMEOUT);
}
},
enable: function wn_enable(lock) {
lock.set({'wifi.enabled': true});
},
enableDebugging: function wn_enableDebugging(lock) {
// For bug 819947: turn on wifi debugging output to help track down a bug
// in wifi. We turn on wifi output only while the FTU app is active.
this._prevDebuggingValue = false;
var req = lock.get('wifi.debugging.enabled');
req.onsuccess = function wn_getDebuggingSuccess() {
this._prevDebuggingValue = req.result['wifi.debugging.enabled'];
};
lock.set({ 'wifi.debugging.enabled': true });
},
finish: function wn_finish() {
if (!this._prevDebuggingValue && navigator.mozSettings) {
var resetLock = window.navigator.mozSettings.createLock();
resetLock.set({'wifi.debugging.enabled': false});
}
},
getNetwork: function wm_gn(ssid) {
var list = this.networks;
for (var i = 0; i < list.length; i++) {
if (list[i].ssid === ssid) {
return list[i];
}
}
return null;
},
connect: function wn_connect(ssid, password, user) {
var network = this.getNetwork(ssid);
if (!network) {
console.error('Network not found');
return;
}
this.ssid = ssid;
// TODO: Hardcoded for resolving bug 1019146, replace hardcoded eap
// method with user selected eap method after bug 1036829.
WifiHelper.setPassword(network, password, user, 'PEAP');
this.gCurrentNetwork = network;
this.api.associate(network);
},
changeStatus: function wn_cs(callback) {
/**
* mozWifiManager status
* see dom/wifi/nsIWifi.idl -- the 4 possible statuses are:
* - connecting:
* fires when we start the process of connecting to a network.
* - associated:
* fires when we have connected to an access point but do not yet
* have an IP address.
* - connected:
* fires once we are fully connected to an access point.
* - connectingfailed:
* fires when we fail to connect to an access point.
* - disconnected:
* fires when we were connected to a network but have been
* disconnected.
*/
var self = this;
if (WifiManager.api) {
WifiManager.api.onstatuschange = function(event) {
if (event.status === WifiManager.STATUS.disconnected && self.onScan) {
self.scan(self.onScan);
} else {
WifiUI.updateNetworkStatus(event.network.ssid, event.status);
}
};
}
}
};
var WifiUI = {
joinNetwork: function wui_jn() {
var password = document.getElementById('wifi_password').value;
var user = document.getElementById('wifi_user').value;
var ssid = document.getElementById('wifi_ssid').value;
WifiUI.connect(ssid, password, user);
window.history.back();
},
joinHiddenNetwork: function wui_jhn() {
var password = UIManager.hiddenWifiPassword.value;
var user = UIManager.hiddenWifiIdentity.value;
var ssid = UIManager.hiddenWifiSsid.value;
var security = UIManager.hiddenWifiSecurity.value;
var network;
if (ssid.length) {
if (!Array.isArray(WifiManager.networks)) {
WifiManager.networks = [];
}
network = new window.MozWifiNetwork({
ssid: ssid,
capabilities: [],
security: [security],
relSignalStrength: 0,
hidden: true
});
WifiManager.networks.push(network);
this.renderNetworks(WifiManager.networks);
WifiUI.connect(ssid, password, user);
}
// like in Settings: if we don't provide correct
// network data it just get back to the wifi screen
window.history.back();
},
_selectNetworkItem: function(ssid, isSelected=true) {
var elem = document.getElementById(ssid);
if (!elem) {
return;
}
if (isSelected) {
// We should update the state to 'connecting' in here to show the change
// to the user, instead of waiting for the api (takes longer).
// But currently
// there's no feedback from the api when connecting to a hidden wifi fails
// so we skip the visual changes
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1107431#c25
elem.setAttribute('data-wifi-selected', true);
} else {
var icon = elem.querySelector('aside');
elem.removeAttribute('data-wifi-selected');
elem.classList.remove('connected');
icon.classList.remove('connecting');
icon.classList.remove('connected');
var security = elem.dataset.security;
var securityLevelDOM = elem.querySelector('p[data-security-level]');
securityLevelDOM.setAttribute('data-l10n-id',
security ? security : 'securityOpen');
}
},
connect: function wui_connect(ssid, password, user) {
// First we check if there is a previous selected network
// and we remove their status
// And then end we update the selected network
var newWifi = document.getElementById(ssid);
Array.from(document.querySelectorAll('li[data-wifi-selected]'))
.forEach((networkSelected) => {
this._selectNetworkItem(networkSelected,
networkSelected === newWifi);
});
// Finally we try to connect to the network
WifiManager.connect(ssid, password, user);
},
chooseNetwork: function wui_cn(event) {
// Retrieve SSID from dataset
var ssid = event.target.dataset.ssid;
var selectedNetwork = WifiManager.getNetwork(ssid);
if (!selectedNetwork) {
console.error('Network not found');
return;
}
// Do we need to type password?
if (WifiHelper.isOpen(selectedNetwork)) {
WifiUI.connect(ssid);
return;
}
// Remove refresh option
UIManager.activationScreen.classList.add('no-options');
// Update title
UIManager.mainTitle.textContent = ssid;
// Update network values
var ssidHeader = document.getElementById('wifi_ssid');
var userInput = document.getElementById('wifi_user');
var passwordInput = document.getElementById('wifi_password');
var showPassword = document.querySelector(
'gaia-checkbox[name=show_password]');
var joinButton = UIManager.wifiJoinButton;
joinButton.disabled = true;
passwordInput.addEventListener('input', function validatePassword() {
// disable the "Join" button if the password is on wrong format
joinButton.disabled =
!WifiHelper.isValidInput(WifiHelper.getKeyManagement(selectedNetwork),
passwordInput.value, userInput.value);
});
// Show / Hide password
passwordInput.type = 'password';
passwordInput.value = '';
showPassword.checked = false;
showPassword.addEventListener('change', e => {
passwordInput.type = e.target.checked ? 'text' : 'password';
});
// Update form
passwordInput.value = '';
ssidHeader.value = ssid;
// Activate secondary menu
UIManager.navBar.classList.add('secondary-menu');
// Update changes in form
if (WifiHelper.isEap(selectedNetwork)) {
userInput.parentNode.classList.remove('hidden');
} else {
userInput.parentNode.classList.add('hidden');
}
// Change hash
window.location.hash = '#configure_network';
},
addHiddenNetwork: function wui_addHiddenNetwork() {
// Remove refresh option
UIManager.activationScreen.classList.add('no-options');
// Update title
UIManager.mainTitle.setAttribute('data-l10n-id', 'authentication');
UIManager.navBar.classList.add('secondary-menu');
// Clean input contents
UIManager.hiddenWifiSsid.value = '';
UIManager.hiddenWifiPassword.value = '';
UIManager.hiddenWifiIdentity.value = '';
// Reset join button state
UIManager.wifiJoinButton.disabled = true;
window.location.hash = '#hidden-wifi-authentication';
},
handleHiddenWifiSecurity: function wui_handleSecurity(securityType) {
// no need for password if network is open
if (securityType === '') {
UIManager.hiddenWifiPasswordBox.classList.add('hidden');
} else {
UIManager.hiddenWifiPasswordBox.classList.remove('hidden');
}
// need of username is security is WPA-EAP
if (securityType.indexOf('EAP') !== -1) {
UIManager.hiddenWifiIdentityBox.classList.remove('hidden');
} else {
UIManager.hiddenWifiIdentityBox.classList.add('hidden');
}
},
renderNetworks: function wui_rn(networks) {
var networksDOM = document.getElementById('networks');
networksDOM.innerHTML = '';
var networksList;
if (!networks || networks.length === 0) {
var noResult = '<div id="no-result-container">' +
' <div id="no-result-message">' +
' <p data-l10n-id="noWifiFound3"></p>' +
' </div>' +
'</div>';
networksDOM.innerHTML = noResult;
} else {
networksList = document.createElement('ul');
networksList.id = 'networks-list';
networksList.setAttribute('role', 'listbox');
networksList.setAttribute('data-l10n-id', 'networksList');
var networksShown = [];
networks.sort(function(a, b) {
return b.relSignalStrength - a.relSignalStrength;
});
// Add detected networks
for (var i = 0, max = networks.length; i < max; i++) {
// Retrieve the network
var network = networks[i];
// Check if is shown
if (networksShown.indexOf(network.ssid) === -1) {
// Create dom elements
var li = document.createElement('li');
var icon = document.createElement('aside');
var ssidp = document.createElement('p');
ssidp.setAttribute('dir', 'auto');
var small = document.createElement('p');
small.dataset.securityLevel = true;
// Set Icon
icon.classList.add('pack-end');
icon.classList.add('wifi-icon');
var level = Math.min(Math.floor(network.relSignalStrength / 20), 4);
icon.classList.add('level-' + level);
document.l10n.setAttributes(icon, 'wifiLevel', {level: level});
icon.setAttribute('role', 'presentation');
// Set SSID
ssidp.textContent = network.ssid;
li.dataset.ssid = network.ssid;
// Show authentication method
var keys = WifiHelper.getSecurity(network);
li.dataset.security = keys;
if (!WifiHelper.isOpen(network)) {
small.textContent = keys.join(', ');
icon.classList.add('secured');
} else {
small.setAttribute('data-l10n-id', 'securityOpen');
}
// Show connection status
icon.classList.add('wifi-signal');
var connectionStatus = WifiHelper.getNetworkStatus(network) ||
WifiManager.STATUS.disconnected;
li.dataset.status = connectionStatus;
if (connectionStatus === WifiManager.STATUS.connected) {
small.setAttribute('data-l10n-id', 'shortStatus-connected');
small.removeAttribute('aria-label');
icon.classList.add('connected');
li.classList.add('connected');
li.dataset.wifiSelected = true;
}
// Update list of shown networks
networksShown.push(network.ssid);
// Append the elements to li
li.setAttribute('id', network.ssid);
li.setAttribute('role', 'option');
li.setAttribute('aria-live', true);
li.setAttribute('aria-relevant', 'text');
li.appendChild(ssidp);
li.appendChild(small);
li.appendChild(icon);
// Append to DOM
if (WifiHelper.isConnected(network)) {
networksList.insertBefore(li, networksList.firstChild);
} else {
networksList.appendChild(li);
}
}
}
networksList.dataset.type = 'list';
networksDOM.appendChild(networksList);
}
utils.overlay.hide();
},
updateNetworkStatus: function wui_uns(ssid, status) {
var element = document.getElementById(ssid);
// Check if element exists and it's the selected network
if (!element) {
return;
}
if (status === WifiManager.STATUS.connecting && ssid !== WifiManager.ssid) {
// bogus event, see bug 1179703/1216221
return;
}
// Update the element
element.dataset.status = status;
if (status !== WifiManager.STATUS.disconnected) {
element.querySelector('p[data-security-level]').setAttribute(
'data-l10n-id', 'shortStatus-' + status);
} else {
var security = element.dataset.security || 'Open';
element.querySelector('p[data-security-level]').setAttribute(
'data-l10n-id', 'security' + security);
element.classList.remove('connected');
}
// Animate icon if connecting, stop animation if
// failed/connected/disconnected
var icon = element.querySelector('aside');
switch (status) {
case WifiManager.STATUS.connecting:
case WifiManager.STATUS.associated:
icon.classList.add('connecting');
break;
case WifiManager.STATUS.connected:
icon.classList.remove('connecting');
var networksList = document.getElementById('networks-list');
icon.classList.add('connected');
element.classList.add('connected');
networksList.removeChild(element);
networksList.insertBefore(element, networksList.firstChild);
break;
default:
icon.classList.remove('connecting');
icon.classList.remove('connected');
break;
}
}
};
exports.WifiManager = WifiManager;
exports.WifiUI = WifiUI;
})(window);