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

538
gaia/apps/costcontrol/js/app.js Executable file
View file

@ -0,0 +1,538 @@
/* global BalanceTab, ConfigManager, Common, NonReadyScreen, SimManager,
debug, CostControl, TelephonyTab, ViewManager, LazyLoader,
AirplaneModeHelper, setNextReset, asyncStorage */
/* exported CostControlApp */
'use strict';
/*
* The application is in charge of display detailed information about the usage.
* Application has three tabs configured independently plus a settings view.
*
* This module is only in charge of manage the different tabs but the specific
* behaviour is delegated to each tab (in /js/views directory).
*
* It is important to explain how the URL schema works. Cost Control
* uses the concept of card to refer a tab or a view. There are two main layers:
* the tab layer and the overlay layer. The first one is used to display tabs,
* the second one is used to display settings or other overlaying dialogs.
*
* The URL schema is:
* #tab-id[#overlay-id]
*
* So changing the hash to:
* #balance-tab#settings-view
* makes Cost Control to change to the balance view and show the settings.
*
* We can close the overlay layer by setting only the balance layer:
* #datausage-tab
* this makes Cost Control to change to the datausage tab at the same time it
* closes the card in the overlay layer.
*
* If you want to preserve a layer but changing the other, use the empty string
* as the id of the layer you want to preserve.
* For instance, you want to only close the overlay layer but not affecting the
* tab layer:
* #
*
* Or you want to change tab layer but without affecting the overlay, use:
* #telephony-tab#
*
* Remember: you cannot close the tab layer!
*/
var CostControlApp = (function() {
var costcontrol,
initialized = false,
hashFromLastRun,
hashFromNotification;
var vmanager;
// Set the application in waiting for SIM mode. During this mode, the
// application shows a dialog informing about the current situation of the
// SIM. Once ready, callback is executed.
function waitForSIMReady(callback) {
SimManager.requestDataSimIcc(function _onIccId(dataSim) {
var dataSimIcc = dataSim.icc;
var cardState = dataSimIcc.cardState;
// SIM not ready
if (cardState !== 'ready') {
showNonReadyScreen(cardState);
debug('SIM not ready:', cardState);
dataSimIcc.oncardstatechange = function() {
waitForSIMReady(callback);
};
// SIM is ready
} else {
hideNotReadyScreen();
debug('SIM ready. ICCID:', dataSim.iccId);
dataSimIcc.oncardstatechange = undefined;
callback && callback();
}
// In case we can not get a valid ICCID.
}, function _errorNoSim() {
console.warn('Error when trying to get the ICC, SIM not detected.');
LazyLoader.load(['/shared/js/airplane_mode_helper.js'], function() {
AirplaneModeHelper.ready(function() {
var fakeState = null;
if (AirplaneModeHelper.getStatus() === 'enabled') {
console.warn('The airplaneMode is enabled.');
fakeState = 'airplaneMode';
window.addEventListener('airplaneModeDisabled',
function _onAirplanemodeDisabled(evt) {
if (evt.detail && evt.detail.serviceId === 'data') {
waitForSIMReady(startApp);
}
}
);
}
showNonReadyScreen(fakeState);
});
});
});
}
// Displays a faked modal dialog that can be automatically close when the SIM
// is ready. A second call if it is already shown will only update the
// message.
var nonReadyScreen;
function showNonReadyScreen(cardState) {
if (isApplicationLocalized) {
realshowNonReadyScreen(cardState);
} else {
window.addEventListener('localized', function _onlocalized() {
window.removeEventListener('localized', _onlocalized);
realshowNonReadyScreen(cardState);
});
}
function realshowNonReadyScreen(messageId) {
debug('Showing non-ready screen.');
if (!nonReadyScreen) {
nonReadyScreen =
new NonReadyScreen(document.getElementById('non-ready-screen'));
}
nonReadyScreen.updateForState(cardState);
vmanager.changeViewTo(nonReadyScreen.id);
}
}
function hideNotReadyScreen(status) {
debug('Hiding non-ready screen.');
if (vmanager.getCurrentView() === 'non-ready-screen') {
vmanager.closeCurrentView();
}
return;
}
// XXX: See the module documentation for details about URL schema
var tabmanager, settingsVManager;
function _onHashChange(evt) {
var parser = document.createElement('a');
parser.href = evt.oldURL;
var oldHash = parser.hash.split('#');
parser.href = evt.newURL;
var newHash = parser.hash.split('#');
if (newHash.length > 3) {
console.error('Cost Control bad URL schema');
return;
}
debug('URL schema before normalizing:', newHash);
var normalized = false;
if (!newHash[1] && oldHash[1]) {
newHash[1] = oldHash[1];
normalized = true;
}
if (newHash.length === 3 && newHash[2] === '') {
if (oldHash.length === 3) {
newHash[2] = oldHash[2];
} else {
newHash = newHash.slice(0, 2);
}
normalized = true;
}
if (normalized) {
debug('URL schema after normalization:', newHash);
window.location.hash = newHash.join('#');
return;
}
if (newHash[1]) {
tabmanager.changeViewTo(newHash[1]);
}
if (newHash.length < 3) {
vmanager.closeCurrentView();
} else {
vmanager.changeViewTo(newHash[2], '#tabpanel');
}
}
function setupCardHandler() {
// View managers for dialogs and settings
tabmanager = new ViewManager(
['balance-tab', 'telephony-tab', { id: 'datausage-tab', tab: 'right' }]
);
settingsVManager = new ViewManager();
// View handler
window.addEventListener('hashchange', _onHashChange);
}
function loadMessageHandler() {
var messageHandlerFrame = document.getElementById('message-handler');
var thereIsNextReset = ConfigManager.option('nextReset');
var alreadyLoaded = messageHandlerFrame.src
.contains('message_handler.html');
if (alreadyLoaded && thereIsNextReset) {
setNextReset(ConfigManager.option('nextReset'));
return;
}
if (thereIsNextReset) {
window.addEventListener('messagehandlerready', function _setNextReset() {
window.removeEventListener('messagehandlerready', _setNextReset);
setNextReset(ConfigManager.option('nextReset'));
});
}
messageHandlerFrame.src = 'message_handler.html';
}
function _onDataSimChange() {
// Before restarting the app, it's necessary remove the cached values of
// costcontrol and config to force an update.
CostControl.reset();
ConfigManager.setConfig(null);
SimManager.requestDataSimIcc(startApp);
}
function startApp(callback) {
// If customMode is not ready and it was activated on previous execution,
// we have to change the setup plan to no plan.
if (!ConfigManager.supportCustomizeMode) {
SimManager.requestDataSimIcc(function(dataSim) {
ConfigManager.requestSettings(dataSim.iccId, function(settings) {
if (settings.trackingPeriod === 'custom') {
ConfigManager.setOption({ trackingPeriod: 'never' });
// Removing manually if a nextReset alarm exists
// XXX: This is not part of configuration by SIM so we bypass
// ConfigManager
asyncStorage.getItem('nextResetAlarm', function(id) {
// There is already an alarm, remove it
debug('Current nextResetAlarm', id + '.', id ? 'Removing.' : '');
if (id) {
navigator.mozAlarms.remove(id);
}
asyncStorage.setItem('nextResetAlarm', null, function() {
ConfigManager.setOption({ nextReset: null });
});
});
}
});
});
}
if (SimManager.isMultiSim()) {
window.addEventListener('dataSlotChange', _onDataSimChange);
}
CostControl.getInstance(function _onCostControlReady(instance) {
if (ConfigManager.option('fte')) {
startFTE();
return;
}
loadMessageHandler();
costcontrol = instance;
if (!initialized) {
setupApp(callback);
} else {
loadSettings();
updateUI(callback);
}
});
}
var isApplicationLocalized = false;
window.addEventListener('localized', function _onLocalize() {
isApplicationLocalized = true;
if (initialized) {
updateUI();
loadSettings();
}
});
function setupApp(callback) {
setupCardHandler();
// Configure settings buttons
var settingsButtons = document.querySelectorAll('.settings-button');
Array.prototype.forEach.call(settingsButtons,
function _eachSettingsButton(button) {
button.addEventListener('click', function _showSettings() {
window.location.hash = '##settings-view';
});
}
);
// Handle 'open activity' sent by the user via the widget
navigator.mozSetMessageHandler('activity',
function _handleActivity(activityRequest) {
var name = activityRequest.source.name;
switch (name) {
case 'costcontrol/balance':
window.location.hash = '#balance-tab';
break;
case 'costcontrol/telephony':
window.location.hash = '#telephony-tab';
break;
case 'costcontrol/data_usage':
window.location.hash = '#datausage-tab';
break;
}
}
);
// When a notification is received
window.navigator.mozSetMessageHandler('notification',
function _onNotification(notification) {
if (!notification.clicked) {
return;
}
debug('Notification was clicked!');
navigator.mozApps.getSelf().onsuccess = function _onAppReady(evt) {
var app = evt.target.result;
app.launch();
var type = notification.data;
debug('Notification type:', type);
handleNotification(type);
};
}
);
// Check card state when visible
document.addEventListener('visibilitychange',
function _onVisibilityChange(evt) {
if (!document.hidden && initialized) {
waitForSIMReady();
}
}
);
updateUI(callback);
ConfigManager.observe('plantype', updateUI, true);
initialized = true;
loadSettings();
}
// Load settings in background
function loadSettings() {
window.performance.mark('loadSettingsStart');
document.getElementById('settings-view-placeholder').src = 'settings.html';
}
function handleNotification(type) {
switch (type) {
case 'topUpError':
BalanceTab.topUpWithCode(true);
break;
case 'lowBalance':
case 'zeroBalance':
window.location.hash = '#balance-tab';
hashFromNotification = '#balance-tab';
break;
case 'dataUsage':
hashFromNotification = '#datausage-tab';
tabmanager.changeViewTo('datausage-tab');
break;
}
}
var currentMode;
function updateUI(callback) {
SimManager.requestDataSimIcc(function(dataSim) {
ConfigManager.requestSettings(dataSim.iccId,
function _onSettings(settings) {
var mode = ConfigManager.getApplicationMode();
var newHash = window.location.hash;
var tabs = document.getElementById('tabs');
var dataUsageTab = document.getElementById('datausage-tab');
debug('App UI mode: ', mode);
// Layout
if (mode !== currentMode) {
currentMode = mode;
// Stand alone mode when data usage only
if (mode === 'DATA_USAGE_ONLY') {
tabs.hidden = true;
dataUsageTab.classList.add('standalone');
newHash = '#datausage-tab';
// Two tabs mode
} else {
dataUsageTab.classList.remove('standalone');
tabs.hidden = false;
document.getElementById('balance-tab-filter')
.hidden = (mode !== 'PREPAID');
document.getElementById('telephony-tab-filter')
.hidden = (mode !== 'POSTPAID');
// If it was showing the left tab, force changing to the
// proper left view
if (!isDataUsageTabShown()) {
newHash = (mode === 'PREPAID') ? '#balance-tab#' :
'#telephony-tab#';
}
}
window.location.hash = hashFromNotification ||
hashFromLastRun ||
newHash;
hashFromLastRun = hashFromNotification = null;
// XXX: Break initialization to allow Gecko to render the animation on
// time.
requestAnimationFrame(function continueLoading() {
document.getElementById('main').classList.remove('non-ready');
if (mode === 'PREPAID') {
if (typeof TelephonyTab !== 'undefined') {
TelephonyTab.finalize();
}
if (typeof BalanceTab !== 'undefined') {
BalanceTab.initialize();
}
} else if (mode === 'POSTPAID') {
if (typeof BalanceTab !== 'undefined') {
BalanceTab.finalize();
}
if (typeof TelephonyTab !== 'undefined') {
TelephonyTab.initialize();
}
}
});
}
(typeof callback === 'function') && callback();
});
});
}
function isDataUsageTabShown() {
return window.location.hash.split('#')[1] === 'datausage-tab';
}
function onFteFinished(e) {
if (e.origin !== Common.COST_CONTROL_APP) {
return;
}
var type = e.data.type;
if (type === 'fte_finished') {
window.removeEventListener('message', onFteFinished);
document.getElementById('splash_section').hidden = 'true';
// Only hide the FTE view when everything in the UI is ready
ConfigManager.requestAll(function() {
startApp(Common.closeFTE);
});
}
}
function startFTE() {
window.addEventListener('message', onFteFinished);
var mode = ConfigManager.getApplicationMode();
Common.startFTE(mode);
}
function initApp() {
// The location hash keeps its current value even if the app is killed by
// the oom killer.
hashFromLastRun = window.location.hash;
window.location.hash = '';
vmanager = new ViewManager();
waitForSIMReady(startApp);
}
return {
init: function() {
var SCRIPTS_NEEDED = [
'js/utils/debug.js',
'js/common.js',
'js/views/NonReadyScreen.js',
'js/utils/toolkit.js',
'js/view_manager.js'
];
// Check if the mandatory APIs to work exist.
if (!window.navigator.mozMobileConnections ||
!window.navigator.mozIccManager ||
!window.navigator.mozNetworkStats) {
LazyLoader.load(SCRIPTS_NEEDED, function _showError() {
vmanager = new ViewManager();
showNonReadyScreen(null);
});
} else {
SCRIPTS_NEEDED = [
'js/sim_manager.js',
'js/utils/debug.js',
'shared/js/date_time_helper.js',
'js/utils/chart.js',
'js/utils/formatting.js',
'js/utils/toolkit.js',
'js/settings/networkUsageAlarm.js',
'js/common.js',
'js/costcontrol.js',
'js/config/config_manager.js',
'js/views/NonReadyScreen.js',
'js/view_manager.js'
];
LazyLoader.load(SCRIPTS_NEEDED, initApp);
}
// Tell audio channel manager that we want to adjust the notification
// channel if the user press the volumeup/volumedown buttons in Usage.
if (navigator.mozAudioChannelManager) {
navigator.mozAudioChannelManager.volumeControlChannel = 'notification';
}
},
reset: function() {
costcontrol = null;
initialized = false;
vmanager = null;
tabmanager = null;
settingsVManager = null;
currentMode = null;
isApplicationLocalized = false;
window.removeEventListener('dataSlotChange', _onDataSimChange);
window.removeEventListener('hashchange', _onHashChange);
window.removeEventListener('message', onFteFinished);
window.location.hash = '';
nonReadyScreen = null;
},
showBalanceTab: function _showBalanceTab() {
window.location.hash = '#balance-tab';
},
showDataUsageTab: function _showDataUsageTab() {
window.location.hash = '#datausage-tab';
},
showAppDetailView: function _showAppDetailView() {
window.location.hash = '##appdetail-view';
},
_onHashChange: _onHashChange
};
}());

View file

@ -0,0 +1,4 @@
/* global CostControlApp */
'use strict';
CostControlApp.init();

View file

@ -0,0 +1,456 @@
/* global _, debug, ConfigManager, Toolkit, SimManager */
/* exported addAlarmTimeout, setNextReset, addNetworkUsageAlarm,
getTopUpTimeout, Common, sendBalanceThresholdNotification
*/
'use strict';
function addAlarmTimeout(type, delay) {
var handlerContainer = document.getElementById('message-handler');
return handlerContainer.contentWindow.addAlarmTimeout(type, delay);
}
function setNextReset(when, callback) {
var handlerContainer = document.getElementById('message-handler');
return handlerContainer ?
handlerContainer.contentWindow.setNextReset(when, callback) :
window.setNextReset(when, callback);
}
function getTopUpTimeout(callback) {
var handlerContainer = document.getElementById('message-handler');
return handlerContainer ?
handlerContainer.contentWindow.getTopUpTimeout(callback) :
window.getTopUpTimeout(callback);
}
function addNetworkUsageAlarm(dataInterface, dataLimit, onsuccess, onerror) {
var handlerContainer = document.getElementById('message-handler');
if (handlerContainer) {
handlerContainer.contentWindow
.addNetworkUsageAlarm(dataInterface, dataLimit, onsuccess, onerror);
} else {
window.addNetworkUsageAlarm(dataInterface, dataLimit, onsuccess, onerror);
}
}
function sendBalanceThresholdNotification(remaining, settings, callback) {
var handlerContainer = document.getElementById('message-handler');
if (handlerContainer) {
handlerContainer.contentWindow
.sendBalanceThresholdNotification(remaining, settings, callback);
}
}
function resetTelephony(callback) {
ConfigManager.setOption({
lastTelephonyReset: new Date(),
lastTelephonyActivity: {
calltime: 0,
smscount: 0,
timestamp: new Date()
}
}, callback);
}
var Common = {
COST_CONTROL_APP: 'app://costcontrol.gaiamobile.org',
DATA_USAGE_WARNING: 0.8,
allApps: null,
allAppsLoaded: false,
allNetworkInterfaces: null,
allNetworkInterfaceLoaded: false,
//XXX: Group of apps, whose traffic will be added to the system app.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1079609
specialApps: [
'app://search.gaiamobile.org/manifest.webapp'
],
SYSTEM_MANIFEST: 'app://system.gaiamobile.org/manifest.webapp',
BROWSER_APP: {
manifestURL: 'app://browser.gaiamobile.org/manifest.webapp',
origin: '',
manifest: {
icons: {
'84': '/shared/resources/branding/browser_84.png',
'126': '/shared/resources/branding/browser_126.png',
'142': '/shared/resources/branding/browser_142.png',
'189': '/shared/resources/branding/browser_189.png',
'284': '/shared/resources/branding/browser_284.png'
},
name: 'browser'
}
},
startFTE: function(mode) {
var iframe = document.getElementById('fte_view');
window.addEventListener('message', function handler(e) {
if (e.origin !== Common.COST_CONTROL_APP) {
return;
}
if (e.data.type === 'fte_ready') {
window.removeEventListener('message', handler);
iframe.classList.remove('non-ready');
// PERFORMANCE MARKERS
// Designates that the app's *core* chrome or navigation interface
// exists in the DOM and is marked as ready to be displayed.
window.performance.mark('navigationLoaded');
// Designates that the app's *core* chrome or navigation interface
// has its events bound and is ready for user interaction.
window.performance.mark('navigationInteractive');
// Designates that the app is visually loaded (e.g.: all of the
// "above-the-fold" content exists in the DOM and is marked as
// ready to be displayed).
window.performance.mark('visuallyLoaded');
// Designates that the app has its events bound for the minimum
// set of functionality to allow the user to interact with the
// "above-the-fold" content.
window.performance.mark('contentInteractive');
// Start up ended when FTE ready
window.performance.mark('fullyLoaded');
}
});
iframe.src = '/fte.html' + '#' + mode;
},
closeFTE: function() {
var iframe = window.parent.document.getElementById('fte_view');
iframe.classList.add('non-ready');
iframe.src = '';
},
startApp: function() {
parent.postMessage({
type: 'fte_finished',
data: ''
}, Common.COST_CONTROL_APP);
},
closeApplication: function() {
return setTimeout(function _close() {
debug('Closing.');
window.close();
});
},
modalAlert: function(message) {
alert(message);
},
get localize() {
return navigator.mozL10n.setAttributes;
},
// Returns whether exists an nsIDOMNetworkStatsInterfaces object
// that meet the argument function criteria
getInterface: function getInterface(findFunction) {
if (!Common.allNetworkInterfaceLoaded) {
debug('Network interfaces are not ready yet');
var header = _('data-usage');
var msg = _('loading-interface-data');
this.modalAlert(header + '\n' + msg);
return;
}
if (Common.allNetworkInterfaces) {
return Common.allNetworkInterfaces.find(findFunction);
}
},
getDataSIMInterface: function _getDataSIMInterface(iccId) {
if (!iccId) {
console.warn('Undefined icc identifier, unable get data interface');
return;
}
if (iccId) {
var findCurrentInterface = function(networkInterface) {
if (networkInterface.id === iccId) {
return networkInterface;
}
};
return this.getInterface(findCurrentInterface);
}
return undefined;
},
getWifiInterface: function _getWifiInterface() {
var findWifiInterface = function(networkInterface) {
if (networkInterface.type === navigator.mozNetworkStats.WIFI) {
return networkInterface;
}
};
return this.getInterface(findWifiInterface);
},
loadApps: function() {
return new Promise(function(resolve, reject) {
if (Common.allAppsLoaded) {
resolve(Common.allApps);
return;
}
var request = window.navigator.mozApps.mgmt.getAll();
request.onsuccess = function(event) {
var appList = event.target.result;
// XXX : The data traffic of the filtered apps will be automatically
// counted as residual data. This traffic is added to the system app
// on the drawApps method located on "js/views/datausage.js"
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1084010#c0
Common.allApps = appList.filter(function(app) {
return Common.specialApps.indexOf(app.manifestURL) === -1;
});
Common.allApps.push(Common.BROWSER_APP);
Common.allAppsLoaded = true;
resolve(Common.allApps);
};
request.onerror = function() {
reject(new Error('Apps could not be loaded'));
};
});
},
loadNetworkInterfaces: function(onsuccess, onerror) {
var networks = navigator.mozNetworkStats.getAvailableNetworks();
networks.onsuccess = function() {
Common.allNetworkInterfaces = networks.result;
Common.allNetworkInterfaceLoaded = true;
if (onsuccess) {
onsuccess();
}
};
networks.onerror = function() {
console.error('Error when trying to load network interfaces');
if (onerror) {
onerror();
}
};
},
getApp: function(manifestURL) {
return this.allApps.find(function(app) {
return app.manifestURL === manifestURL;
});
},
getAppManifest: function(app) {
return app.manifest || app.updateManifest;
},
getLocalizedAppName: function(app) {
// If is System App returns label others
if (app.manifestURL === Common.SYSTEM_MANIFEST) {
return _('data-usage-other-apps');
}
// Browser app does not exist, we have to provide the localized app name
if (app.manifestURL === Common.BROWSER_APP.manifestURL) {
return _('data-usage-browser-app');
}
var manifest = this.getAppManifest(app);
var userLang = document.documentElement.lang;
var locales = manifest.locales;
var localized = locales && locales[userLang] && locales[userLang].name;
return localized || manifest.name;
},
getAppIcon: function(app) {
var manifest = this.getAppManifest(app);
var icons = manifest.icons;
var defaultImage = '../style/images/app/icons/default.png';
if (!icons || !Object.keys(icons).length) {
return defaultImage;
}
// The preferred size is 30 by the default. If we use HDPI device, we may
// use the image larger than 30 * 1.5 = 45 pixels.
var preferredIconSize = 30 * (window.devicePixelRatio || 1);
var preferredSize = Number.MAX_VALUE;
var max = 0;
for (var size in icons) {
size = parseInt(size, 10);
if (size > max) {
max = size;
}
if (size >= preferredIconSize && size < preferredSize) {
preferredSize = size;
}
}
// If there is an icon matching the preferred size, we return the result,
// if there isn't, we will return the maximum available size.
if (preferredSize === Number.MAX_VALUE) {
preferredSize = max;
}
var url = icons[preferredSize];
if (url) {
return !(/^(http|https|data):/.test(url)) ? app.origin + url : url;
} else {
return defaultImage;
}
},
getDataLimit: function _getDataLimit(settings) {
var multiplier = (settings.dataLimitUnit === 'MB') ?
1000000 : 1000000000;
return settings.dataLimitValue * multiplier;
},
resetData: function _resetData(mode, onsuccess, onerror) {
// Get all availabe Interfaces
var wifiInterface = Common.getWifiInterface();
// Ask reset for all available Interfaces
var wifiClearRequest, mobileClearRequest;
// onerror callback builder
var getOnErrorFor = function(networkInterface) {
return function() {
if (wifiClearRequest) {
wifiClearRequest.onerror = undefined;
}
if (mobileClearRequest) {
mobileClearRequest.onerror = undefined;
}
(typeof onerror === 'function') && onerror(networkInterface);
};
};
if ((mode === 'all' || mode === 'wifi') && wifiInterface) {
wifiClearRequest = navigator.mozNetworkStats.clearStats(wifiInterface);
wifiClearRequest.onerror = getOnErrorFor('wi-Fi');
}
if (mode === 'all' || mode === 'mobile') {
SimManager.requestDataSimIcc(function(dataSim) {
var currentSimcardInterface = Common.getDataSIMInterface(dataSim.iccId);
if (currentSimcardInterface) {
mobileClearRequest = navigator.mozNetworkStats
.clearStats(currentSimcardInterface);
mobileClearRequest.onerror = getOnErrorFor('simcard');
mobileClearRequest.onsuccess = function _restoreDataLimitAlarm() {
ConfigManager.requestSettings(dataSim.iccId,
function _onSettings(settings) {
if (settings.dataLimit) {
// Restore network alarm
addNetworkUsageAlarm(currentSimcardInterface,
Common.getDataLimit(settings),
function _addNetworkUsageAlarmOK() {
ConfigManager.setOption({ 'dataUsageNotified': false });
});
}
});
};
}
});
}
// Set last Reset
if (mode === 'all') {
ConfigManager.setOption({ lastCompleteDataReset: new Date() });
} else {
// Else clausure prevents running the update event twice
ConfigManager.setOption({ lastDataReset: new Date() });
}
// call onsuccess
if (typeof onsuccess === 'function') {
onsuccess();
}
},
resetAll: function _resetAll(callback) {
function logResetDataError(networkInterface) {
console.log('Error when trying to reset ' + networkInterface +
' interface');
}
Common.resetData('all', thenResetTelephony, logResetDataError);
function thenResetTelephony() {
resetTelephony(callback);
}
},
// Next automatic reset date based on user preferences
updateNextReset: function _updateNextReset(trackingPeriod, value, callback) {
if (trackingPeriod === 'never') {
setNextReset(null, callback); // remove any alarm
return;
}
var nextReset, today = new Date();
// Recalculate with month period
if (trackingPeriod === 'monthly') {
var month, year;
var monthday = parseInt(value, 10);
month = today.getMonth();
year = today.getFullYear();
if (today.getDate() >= monthday) {
month = (month + 1) % 12;
if (month === 0) {
year++;
}
}
nextReset = new Date(year, month, monthday);
if (monthday !== nextReset.getDate()) {
var LAST_DAY_OF_PREVIOUS_MONTH = 0;
// If monthday is not equal to nextReset day, it means that the selected
// reset day does not exist (e.g. 30 Feb). In this case, the reset day
// must be the last day of the previous month
nextReset.setDate(LAST_DAY_OF_PREVIOUS_MONTH);
}
// Recalculate with week period
} else if (trackingPeriod === 'weekly') {
var oneDay = 24 * 60 * 60 * 1000;
var weekday = parseInt(value, 10);
var daysToTarget = weekday - today.getDay();
if (daysToTarget <= 0) {
daysToTarget = 7 + daysToTarget;
}
nextReset = new Date();
nextReset.setTime(nextReset.getTime() + oneDay * daysToTarget);
Toolkit.toMidnight(nextReset);
}
// remove oldAlarm and set the new one
setNextReset(nextReset, callback);
},
localizeWeekdaySelector: function _localizeWeekdaySelector(selector) {
var weekStartsOnMonday =
parseInt(navigator.mozL10n.get('firstDayOfTheWeek'), 10) === 1;
debug('Week starts on monday?', weekStartsOnMonday);
var monday = selector.querySelector('.monday');
var sunday = selector.querySelector('.sunday');
var list = monday.parentNode;
if (weekStartsOnMonday) {
debug('Monday, Tuesday...');
list.insertBefore(monday, list.childNodes[0]); // monday is the first
list.appendChild(sunday); // sunday is the last
} else {
debug('Sunday, Monday...');
list.insertBefore(sunday, list.childNodes[0]); // sunday is the first
list.insertBefore(monday, sunday.nextSibling); // monday is the second
}
}
};

View file

@ -0,0 +1,373 @@
/* global debug, SimManager, LazyLoader, asyncStorage, deepCopy */
/* exported ConfigManager */
'use strict';
var ConfigManager = (function() {
var today = new Date();
var DEFAULT_SETTINGS = {
'dataLimit': false,
'dataLimitValue': 1,
'dataLimitUnit': 'GB',
'errors': {
'INCORRECT_TOPUP_CODE': false,
'BALANCE_TIMEOUT': false,
'TOPUP_TIMEOUT': false
},
'fte': true,
'waitingForBalance': null,
'waitingForTopUp': null,
'lastBalance': null,
'lastBalanceRequest': null,
'lastTopUpRequest': null,
'lastDataUsage': {
'timestamp': today,
'start': today,
'end': today,
'today': today,
'wifi': {
'apps': {},
'total': 0
},
'mobile': {
'apps': {},
'total': 0
}
},
'lastTelephonyActivity': {
'timestamp': today,
'calltime': 0,
'smscount': 0
},
'lastTelephonyReset': today,
'lastDataReset': today,
'lastCompleteDataReset': today,
'lowLimit': false,
'lowLimitThreshold': false,
'lowLimitNotified': false,
'zeroBalanceNotified': false,
'dataUsageNotified': false,
'nextReset': null,
'plantype': 'postpaid',
'resetTime': 1,
'trackingPeriod': 'monthly',
'isMobileChartVisible': true,
'isWifiChartVisible': false
};
var CONFIG_CACHE = [];
function getApplicationMode() {
if (noConfigFound) {
return 'DATA_USAGE_ONLY';
}
return settings.plantype.toUpperCase();
}
var configuration, configurationIndex, noConfigFound = false;
function setConfig(newConfiguration) {
configuration = newConfiguration;
debug('Provider configuration done!');
}
// Load the operator configuration according to MCC_MNC pair
function requestConfiguration(currentDataIcc, callback) {
if (!currentDataIcc || !currentDataIcc.iccInfo) {
console.error('No iccInfo available');
return;
}
function returnConfiguration() {
if (typeof callback === 'function') {
callback(configuration);
}
}
if (configuration) {
setTimeout(function() {
returnConfiguration();
});
return;
}
if (configurationIndex) {
loadConfiguration(currentDataIcc, returnConfiguration);
} else {
loadConfigurationIndex(function onIndex() {
loadConfiguration(currentDataIcc, returnConfiguration);
});
}
}
function loadConfiguration(currentDataIcc, callback) {
var configFilePath = getConfigFilePath(currentDataIcc);
if (CONFIG_CACHE[configFilePath]) {
ConfigManager.setConfig(CONFIG_CACHE[configFilePath]);
(typeof callback === 'function') && callback();
} else {
LazyLoader.load(configFilePath, function() {
// configuration variable is set by calling setConfig() inside the
// configuration file.
CONFIG_CACHE[configFilePath] = configuration;
(typeof callback === 'function') && callback();
});
}
}
function getConfigFilePath(currentDataIcc) {
noConfigFound = false;
var mcc = currentDataIcc.iccInfo.mcc;
var mnc = currentDataIcc.iccInfo.mnc;
var key = mcc + '_' + mnc;
var configDir = configurationIndex[key];
if (!configDir) {
configDir = 'default';
noConfigFound = true;
}
return 'js/config/' + configDir + '/config.js';
}
function loadConfigurationIndex(callback) {
LazyLoader.getJSON('/js/config/index.json').then(function(json) {
configurationIndex = json;
if (configurationIndex === null) { // TODO Remove workaround when
// Bug 1069808 is fixed.
console.error('Error loading the configuration index!' +
'Response from LazyLoader was null.');
configurationIndex = {};
}
if (typeof callback === 'function') {
setTimeout(function() {
callback();
});
}
}, function(error) {
console.error('Error loading the configuration index! ' +
'Error code: ' + error);
configurationIndex = {};
});
}
// Let's serialize dates
// XXX: To avoid random error:
// "DataCloneError: The object could not be cloned."
// {file:
// "app://costcontrol.gaiamobile.org/shared/js/async_storage.js" line: 90}]
Date.prototype.toJSON = function() {
return {'__date__': this.toISOString()};
};
function settingsReviver(k, v) {
if (v === null || typeof v !== 'object' || !v.hasOwnProperty('__date__')) {
return v;
}
return new Date(v.__date__);
}
// Load stored settings
var NO_ICCID = 'NOICCID';
var settings;
function requestSettings(iccIdInfo, callback) {
var currentICCID = iccIdInfo || NO_ICCID;
asyncStorage.getItem(currentICCID, function _wrapGetItem(localSettings) {
// No entry: set defaults
try {
settings = JSON.parse(localSettings, settingsReviver);
} catch (e) {
// If we can't parse the settings, use the default ones
}
if (settings === null) {
settings = deepCopy(DEFAULT_SETTINGS);
debug('Storing default settings for ICCID:', currentICCID);
asyncStorage.setItem(currentICCID, JSON.stringify(settings),
callback && callback.bind(null, settings));
} else if (callback) {
callback(settings);
}
});
}
// Provides vendor configuration and settings
function requestAll(callback) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
requestConfiguration(dataSimIcc.icc,
function _afterConfig(configuration) {
requestSettings(dataSimIcc.iccId, function _afterSettings(settings) {
if (callback) {
callback(configuration, settings, dataSimIcc.iccId);
}
});
}
);
});
}
// Produce a optionchange for the given option
function dispatchOptionChange(name, value, oldValue, settings) {
var event = new CustomEvent('optionchange', { detail: {
name: name,
value: value,
oldValue: oldValue,
settings: settings
} });
window.dispatchEvent(event);
debug('Event optionchange dispatched for', name);
}
// Set setting options asynchronously and dispatch an event for every
// affected option.
function setOption(options, callback) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
// If settings is not ready, load and retry
if (!settings) {
requestSettings(dataSimIcc.iccId, function _afterEnsuringSettings() {
setOption(options, callback);
});
return;
}
// Store former values and update with new ones
var formerValue = {};
for (var name in options) {
if (options.hasOwnProperty(name)) {
formerValue[name] = settings[name];
settings[name] = options[name];
}
}
var currentICCID = dataSimIcc.iccId || NO_ICCID;
asyncStorage.setItem(currentICCID, JSON.stringify(settings),
function _onSet() {
requestSettings(dataSimIcc.iccId, function _onSettings(settings) {
for (var name in options) {
if (options.hasOwnProperty(name)) {
dispatchOptionChange(name, settings[name], formerValue[name],
settings);
}
}
if (callback) {
callback();
}
});
}
);
});
}
// Part of the synchronous interface, return or set a setting.
function syncOption(name, value) {
var oldValue = settings[name];
if (value === undefined) {
return oldValue;
}
var update = {};
update[name] = value;
setOption(update);
}
var callbacks;
// Function in charge of dispatch the events to the observers
function callCallbacks(evt) {
debug('Option', evt.detail.name, 'has changed!');
var callbackCollection = callbacks[evt.detail.name] || [];
for (var i = 0; i < callbackCollection.length; i++) {
var callback = callbackCollection[i];
if (callback) {
callback(evt.detail.value, evt.detail.oldValue, evt.detail.name,
evt.detail.settings);
}
}
}
// Installs an observer to call when the setting has changed. It produces
// an initial call to the callback unless you provide true as the last
// parameter.
function syncObserve(name, callback, avoidInitialCall) {
debug('Installing observer for', name);
// XXX: initialize this only if an observer is added
if (callbacks === undefined) {
callbacks = {};
// Keeps the synchronization when the application need to communicate
// instant information. Local storage event is used. The key sync is
// read to determine which option has changed and a settingchange event
// for that option is dispatched.
window.addEventListener('storage', function _onSync(evt) {
if (evt.key === 'sync') {
var name = evt.newValue.split('#')[0];
var oldValue = settings ? settings[name] : undefined;
debug('Synchronization request for', name, 'received!');
SimManager.requestDataSimIcc(function(dataSimIcc) {
requestSettings(dataSimIcc.iccId,
function _onSettings(newSettings) {
settings = newSettings;
dispatchOptionChange(name, settings[name], oldValue, settings);
});
});
}
});
window.addEventListener('optionchange', callCallbacks);
}
if (callbacks[name] === undefined) {
callbacks[name] = [];
}
if (callbacks[name].indexOf(callback) < 0) {
callbacks[name].push(callback);
avoidInitialCall = avoidInitialCall || false;
if (!avoidInitialCall && callback) {
callback(settings[name], null, name, settings);
}
}
}
function syncRemoveObserver(name, callback) {
var callbackCollection = callbacks[name];
if (!callbackCollection) {
return;
}
var index = callbackCollection.indexOf(callback);
if (index > -1) {
callbackCollection.splice(index, 1);
}
}
function defaultValue(name) {
return deepCopy(DEFAULT_SETTINGS[name]);
}
return {
getApplicationMode: getApplicationMode,
setConfig: setConfig,
requestAll: requestAll,
requestSettings: requestSettings,
setOption: setOption,
defaultValue: defaultValue,
// XXX:These methods are the synchronous/cached interface. You need to
// call requestAll() or requestSettings() to force cache renewal.
// If you want to use observe() and option() as a getter, you need to setup
// the cache by calling one of the former methods before. Only once is
// enough.
option: syncOption,
observe: syncObserve,
removeObserver: syncRemoveObserver,
// XXX: Once loaded via requestConfiguration() / requestAll() it is ensured
// it wont change so you can use this to access OEM confguration in a
// synchronous fashion.
get configuration() { return configuration; },
// XXX: Bug 1126178-[CostControl] Remove ConfigManager.supportCustomizeMode
// when the functionality is ready
supportCustomizeMode: false
};
}());

View file

@ -0,0 +1,4 @@
/* global ConfigManager */
'use strict';
ConfigManager.setConfig({});

View file

@ -0,0 +1,9 @@
{
"724_6": "vivo",
"724_10": "vivo",
"724_11": "vivo",
"724_23": "vivo",
"732_123": "movistar_colombia",
"748_07": "movistar_uruguay",
"716_06": "movistar_peru"
}

View file

@ -0,0 +1,22 @@
/* global ConfigManager */
'use strict';
var MovistarColombiaConfig = {
provider: 'Movistar',
is_free: true,
is_roaming_free: true,
credit: { currency: '$' },
balance: {
destination: '611',
text: 'SALDO,A',
senders: ['612', '611'],
regexp: 'Saldo total:\\$([0-9,]+)\\.([0-9]{2})',
minimum_delay: 3 * 60 * 60 * 1000 // 3h
},
topup: {
ussd_destination: '*611#'
},
default_low_limit_threshold: 2000
};
ConfigManager.setConfig(MovistarColombiaConfig);

View file

@ -0,0 +1,19 @@
/* global ConfigManager */
'use strict';
var MovistarPeruConfig = {
provider: 'Movistar',
is_free: true,
credit: { currency: 'S/.' },
balance: {
destination: '600',
text: 'S',
senders: ['515'],
regexp: '([0-9,]+)\\.([0-9]{2})',
zero_regexp: '(Usted no tiene saldo en sus cuentas)',
minimum_delay: 3 * 60 * 60 * 1000 // 3h
},
default_low_limit_threshold: 1
};
ConfigManager.setConfig(MovistarPeruConfig);

View file

@ -0,0 +1,23 @@
/* global ConfigManager */
'use strict';
var MovistarUruguayConfig = {
provider: 'Movistar',
is_free: true,
is_roaming_free: true,
credit: { currency: '$' },
balance: {
destination: '222',
text: 'SALDO',
senders: ['222'],
regexp: 'Recarga \\$([0-9,]+)(?:\\.([0-9]+))?',
zero_regexp: '(Su saldo actual es cero)',
minimum_delay: 3 * 60 * 60 * 1000 // 3h
},
topup: {
ussd_destination: '*222*1*1#'
},
default_low_limit_threshold: 0
};
ConfigManager.setConfig(MovistarUruguayConfig);

View file

@ -0,0 +1,126 @@
/* global Format, ConfigManager */
'use strict';
var VivoConfig = (function() {
var RESERVED = '0';
var OPERATION_TYPE_REQUEST = '01';
var OPERATION_CHECK_BALANCE = '0102';
var MESSAGE_INIT_WIDTH = 32;
var THOUSANDS_PADDING = 4;
var TENS_PADDING = 2;
function getDateTime() {
var now = new Date();
var date = [
Format.padLeft(now.getUTCFullYear(), THOUSANDS_PADDING, '0'),
Format.padLeft(now.getUTCMonth() + 1, TENS_PADDING, '0'),
Format.padLeft(now.getUTCDate(), TENS_PADDING, '0')
].join('');
var time = [
Format.padLeft(now.getUTCHours(), TENS_PADDING, '0'),
Format.padLeft(now.getUTCMinutes(), TENS_PADDING, '0'),
Format.padLeft(now.getUTCSeconds(), TENS_PADDING, '0')
].join('');
return [date, time];
}
/*
* VIVO request messages are formed by the concatenation of fixed length
* fields:
* - 32-length initial padding, all '0' (zero)
* - 3-length system code. Currently only 'OWD' (FirefoOS) is supported
* - 8-length date string in the form of YYYYMMDD
* - 6-length time string in the form of HHMMSS
* - 2-length operation type
* - 1-length total number of SMS for the request
* - 1-length number of the current SMS being transmitted (starting in 1)
* - 1-length reserved character. It is '0'
* - 4-length operation code
*
* Example of balance request (ignore blank spaces):
* 00000000000000000000000000000000 OWD 20130530 132006 01 1 1 0 0102
*
* A request can include parameters in a last field of variable length from
* 0 to 80 characters.
*/
function generateCheckBalanceMessage() {
var messageInit = (new Array(MESSAGE_INIT_WIDTH + 1)).join('0');
var systemCode = 'OWD';
var dateTime = getDateTime();
var date = dateTime[0];
var time = dateTime[1];
var operationType = OPERATION_TYPE_REQUEST;
var totalMessageCount = '1';
var currentMessageCount = '1';
var requestedOperation = OPERATION_CHECK_BALANCE;
return [
messageInit,
systemCode,
date,
time,
operationType,
totalMessageCount,
currentMessageCount,
RESERVED,
requestedOperation
].join('');
}
/*
* TODO:
* A VIVO answer include specific fields from the related request in such
* a way these fields form a unique ID to match each answer with its request.
*
* For example, compare this pair of request, response:
* 00000000000000000000000000000000 OWD 20130530 132006 01 1 1 0 0102
* 20130530 132006 01 1 1 0 0102 60.45
*
* Current value for the regexp used to validate a balance answer is not
* checking the unique ID since this requires to store this ID and make it
* to survive the application closes. Since access to configuration
* properties must be synchronous, we should use some kind of synchronous
* storage. As DOMStorage is discouraged and IndexedDB synchronous API is
* not ready we can not currently implement this constrain.
*/
var balanceRegularExpression = [
'^[0-9]{14}', // we admit any sequence of 14 numbers. See TODO above.
OPERATION_TYPE_REQUEST,
'1', '1',
RESERVED,
OPERATION_CHECK_BALANCE,
'([0-9]+)\\.([0-9]{1,2})' // matches the remaining balance
].join('');
return {
provider: 'Vivo',
is_free: true,
is_roaming_free: true,
credit: { currency: 'R$' },
balance: {
destination: '4850',
get text() {
return generateCheckBalanceMessage();
},
senders: ['4850'],
regexp: balanceRegularExpression,
minimum_delay: 3 * 60 * 60 * 1000 // 3h
},
topup: {
destination: '7000',
ussd_destination: '*321#',
text: '&code',
senders: ['1515', '7000'],
confirmation_regexp:
'Voce recarregou R\\$\\s*([0-9]+)(?:[,\\.]([0-9]+))?',
incorrect_code_regexp:
'(Favor enviar|envie novamente|Verifique) o codigo de recarga'
},
default_low_limit_threshold: 3
};
}());
ConfigManager.setConfig(VivoConfig);

View file

@ -0,0 +1,720 @@
/* global debug, ConfigManager, Toolkit, addAlarmTimeout, Common, LazyLoader,
SimManager, IACManager, DEBUGGING
*/
/* exported CostControl */
'use strict';
/*
* CostControl is the singleton in charge of provide data to the views by using
* asynchronous requests. Views get the object by calling
* CostControl.getInstance() method.
*
* The CostControl instance has allow to perform requests:
* balance, topup, telephony, datausage
*
* And get the application mode:
* DATA_USAGE_ONLY, PREPAID; POSTPAID
*
*/
var CostControl = (function() {
var costcontrol;
function getInstance(onready) {
debug('Initializing Cost Control');
function goOn() {
debug('Cost Control already ready!');
onready(costcontrol);
}
// Force a reload of Costcontrol when the dataSlotChange event is lost
if (costcontrol) {
if (SimManager.isMultiSim()) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
if (costcontrol.iccId === dataSimIcc.iccId) {
goOn();
}
});
} else {
goOn();
}
return;
}
function setupCostControl(configuration, settings, iccId) {
costcontrol = {
iccId: iccId,
request: request,
lastDataResults: {},
lastDataResultsPerApp: {},
isBalanceRequestSMS: isBalanceRequestSMS
};
debug('Cost Control ready!');
onready(costcontrol);
}
loadAPIs();
ConfigManager.requestAll(setupCostControl);
}
var mobileMessageManager, statistics, isSendingBalanceRequest = false;
function loadAPIs() {
if ('mozMobileMessage' in window.navigator) {
mobileMessageManager = window.navigator.mozMobileMessage;
}
if ('mozNetworkStats' in window.navigator) {
statistics = window.navigator.mozNetworkStats;
}
debug('APIs loaded!');
}
// OTHER LOGIC
// Check if a SMS matches the form of a balance request
function isBalanceRequestSMS(message, configuration) {
return message.receiver === configuration.balance.destination;
}
// Perform a request. They must be specified via a request object with:
// type: datausage, balance, topup or telephony
// data: in case of topup, the code for topup
//
// In a nutshell, request() method check if there are some service issues or
// another operation in progress and, if not, dispatch the request to another
// specific handler.
function request(requestObj, callback) {
ConfigManager.requestAll(function _onInfo(configuration, settings) {
debug('Request for:', requestObj.type);
var force = requestObj.force;
var result = {};
// Only type is set here
result.type = requestObj.type;
function _requestDataStatistics() {
SimManager.requestDataSimIcc(function(dataSim) {
requestDataStatistics(configuration, settings, callback, dataSim,
result, requestObj);
});
}
function _requestBalance(connection) {
// Check service
var issues = getServiceIssues(configuration, settings,
connection);
var canBeIgnoredByForcing = (issues === 'minimum_delay' && force);
if (issues && !canBeIgnoredByForcing) {
result.status = 'error';
result.details = issues;
result.data = settings.lastBalance;
if (callback) {
callback(result);
}
return;
}
var costIssues = getCostIssues(configuration, connection);
if (!force && costIssues) {
result.status = 'error';
result.details = costIssues;
result.data = settings.lastBalance;
if (callback) {
callback(result);
}
return;
}
// TODO To avoid concurrence problems, the best solution would be to
// implement a request queue instead of the isSendingBalanceRequest lock
// Check in-progress
var isWaiting = settings.waitingForBalance !== null;
var timeout = Toolkit.checkEnoughDelay(BALANCE_TIMEOUT,
settings.lastBalanceRequest);
if ((isWaiting && !timeout) || isSendingBalanceRequest) {
result.status = 'in_progress';
result.data = settings.lastBalance;
if (callback) {
callback(result);
}
return;
}
isSendingBalanceRequest = true;
// Dispatch
LazyLoader.load('js/iac_manager.js', function() {
requestBalance(configuration, settings, callback, result);
});
}
function _requestTopUp(connection) {
// Check service
var issuesTopUp = getServiceIssues(configuration, settings,
connection);
if (issuesTopUp && issuesTopUp !== 'minimum_delay') {
result.status = 'error';
result.details = issuesTopUp;
result.data = settings.lastDataUsage;
if (callback) {
callback(result);
}
return;
}
var costIssuesTopUp = getCostIssues(configuration, connection);
if (!force && costIssuesTopUp) {
result.status = 'error';
result.details = costIssuesTopUp;
result.data = settings.lastBalance;
if (callback) {
callback(result);
}
return;
}
// Check in-progress
var isWaitingTopUp = settings.waitingForTopUp !== null;
var timeoutTopUp = Toolkit.checkEnoughDelay(BALANCE_TIMEOUT,
settings.lastTopUpRequest);
if (isWaitingTopUp && !timeoutTopUp && !force) {
result.status = 'in_progress';
result.data = settings.lastDataUsage;
if (callback) {
callback(result);
}
return;
}
// Dispatch
var code = requestObj.data;
LazyLoader.load('js/iac_manager.js', function() {
requestTopUp(configuration, settings, code, callback, result);
});
}
switch (requestObj.type) {
case 'balance':
SimManager.requestDataConnection(_requestBalance);
break;
case 'topup':
SimManager.requestDataConnection(_requestTopUp);
break;
case 'datausage':
// Dispatch
if (!Common.allNetworkInterfaceLoaded) {
Common.loadNetworkInterfaces(_requestDataStatistics,
_requestDataStatistics);
} else {
_requestDataStatistics();
}
break;
case 'telephony':
// Can not fail: only dispatch
result.data = settings.lastTelephonyActivity;
result.status = 'success';
if (callback) {
callback(result);
}
break;
}
return;
});
}
// Check service status and return the most representative issue if there is
function getServiceIssues(configuration, settings, connection) {
if (!connection || !connection.data ||
!connection.voice || !connection.voice.connected) {
return 'no_service';
}
var mode = ConfigManager.getApplicationMode();
if (mode !== 'PREPAID') {
return 'no_service';
}
if (connection.voice.relSignalStrength === null) {
return 'no_coverage';
}
if (configuration.balance.minimum_delay) {
var isMinimumDelayHonored = Toolkit.checkEnoughDelay(
configuration.balance.minimum_delay,
settings.lastBalanceRequest
);
if (!isMinimumDelayHonored) {
return 'minimum_delay';
}
}
return '';
}
// Check cost issues and return
function getCostIssues(configuration, connection) {
var inRoaming = connection.voice.roaming;
if (inRoaming && configuration.is_roaming_free !== true) {
return 'non_free_in_roaming';
}
if (!inRoaming && configuration.is_free !== true) {
return 'non_free';
}
return '';
}
// Send a request SMS and set timeouts for interrupting waiting for response
var BALANCE_TIMEOUT = 5 * 60 * 1000; // Should be 5 min
function requestBalance(configuration, settings, callback, result) {
debug('Requesting balance...');
result.data = settings.lastBalance;
function sendSMS() {
debug('After IAC broadcast ask for starting - balance');
// Send request SMS
var newSMS = mobileMessageManager.send(
configuration.balance.destination,
configuration.balance.text
);
newSMS.onsuccess = function _onSuccess() {
debug('Request SMS sent! Waiting for response.');
if (!DEBUGGING) {
mobileMessageManager.delete(newSMS.result.id);
}
// Add the timeout
var newAlarm = addAlarmTimeout('balanceTimeout', BALANCE_TIMEOUT);
newAlarm.onsuccess = function _alarmSet(evt) {
var id = evt.target.result;
debug('Timeout for balance (', id, ') update set to:',
BALANCE_TIMEOUT);
ConfigManager.setOption(
{
'waitingForBalance': id,
'lastBalanceRequest': new Date()
},
function _onSet() {
isSendingBalanceRequest = false;
result.status = 'success';
if (callback) {
callback(result);
}
}
);
};
newAlarm.onerror = function _alarmFailedToSet(evt) {
ConfigManager.setOption(
{
'lastBalanceRequest': new Date()
},
function _onSet() {
isSendingBalanceRequest = false;
debug('Failed to set timeout for balance request!');
result.status = 'error';
result.details = 'timeout_fail';
if (callback) {
callback(result);
}
}
);
};
};
newSMS.onerror = function _onError() {
isSendingBalanceRequest = false;
debug('Request SMS failed! But returning stored balance.');
IACManager.broadcastEndOfSMSQuery('balance').then(function(msg) {
debug('After IAC broadcast ask for ending - balance');
});
result.status = 'error';
result.details = 'request_fail';
if (callback) {
callback(result);
}
};
debug('Balance out of date. Requesting fresh data...');
}
IACManager.init(configuration);
IACManager.broadcastStartOfSMSQuery('balance').then(sendSMS, sendSMS);
}
// Send a top up SMS and set timeouts for interrupting waiting for response
var TOPUP_TIMEOUT = 5 * 60 * 1000; // Should be 5 min
function requestTopUp(configuration, settings, code, callback, result) {
debug('Requesting TopUp with code', code, '...');
function sendSMS() {
debug('After IAC broadcast ask for starting - topup');
// TODO: Ensure is free
var newSMS = mobileMessageManager.send(
configuration.topup.destination,
configuration.topup.text.replace(/\&code/g, code)
);
newSMS.onsuccess = function _onSuccess() {
debug('TopUp SMS sent! Waiting for response.');
if (!DEBUGGING) {
mobileMessageManager.delete(newSMS.result.id);
}
// Add the timeout (if fail, do not inform the callback)
var newAlarm = addAlarmTimeout('topupTimeout', TOPUP_TIMEOUT);
newAlarm.onsuccess = function _alarmSet(evt) {
var id = evt.target.result;
debug('Timeout for TopUp (', id, ') update set to:', TOPUP_TIMEOUT);
// XXX: waitingForTopUp can be null if no waiting or distinct
// than null to indicate the unique id of the timeout waiting
// for the response message
ConfigManager.setOption(
{
'waitingForTopUp': id,
'lastTopUpRequest': new Date()
},
function _onSet() {
result.status = 'success';
if (callback) {
callback(result);
}
}
);
};
newAlarm.onerror = function _alarmFailedToSet(evt) {
debug('Failed to set timeout for TopUp request!');
result.status = 'error';
result.details = 'timeout_fail';
if (callback) {
callback(result);
}
};
};
newSMS.onerror = function _onError() {
debug('TopUp SMS failed!');
IACManager.broadcastEndOfSMSQuery('topup').then(function(msg) {
debug('After IAC broadcast ask for ending - topup');
});
result.status = 'error';
result.details = 'request_fail';
if (callback) {
callback(result);
}
};
}
IACManager.init(configuration);
IACManager.broadcastStartOfSMSQuery('topup').then(sendSMS, sendSMS);
}
// XXX: pending on bug XXX to get statistics by SIM
// Ask statistics API for mobile and wifi data usage
var DAY = 24 * 3600 * 1000; // 1 day
function requestDataStatistics(configuration, settings, callback, dataSimIcc,
result, requestParameters) {
debug('Statistics out of date. Requesting fresh data...');
var apps = requestParameters && requestParameters.apps;
var start = requestParameters && requestParameters.startDate;
var end = requestParameters && requestParameters.endDate;
if (!start) {
var maxAge = 1000 * statistics.maxStorageAge;
var minimumStart = new Date(Date.now() - maxAge);
debug('The max age for samples is ' + minimumStart);
// If settings.lastCompleteDataReset is not set let's use the past week.
// This is only for not breaking dogfooders build and this can be remove
// at some point in the future (and since this sentence has been said
// multiple times this code will probably stay here for a while).
start = new Date(settings.lastCompleteDataReset ||
Date.now() - 7 * DAY);
if (start < minimumStart) {
console.warn('Start date is surpassing the maximum age for the ' +
'samples. Setting to ' + minimumStart);
start = minimumStart;
}
}
start = Toolkit.toMidnight(start);
if (!end) {
var today = Toolkit.toMidnight(new Date());
var tomorrow = new Date();
tomorrow.setTime(today.getTime() + DAY);
end = Toolkit.toMidnight(settings.nextReset ?
new Date(settings.nextReset.getTime() - DAY) :
tomorrow);
}
if (start > end) {
console.error('Start date is higher than end date. This must not ' +
'happen. Changed end date to day after startDate');
end = new Date(start.getTime() + DAY);
}
var wifiInterface = Common.getWifiInterface();
var currentSimcardNetwork = Common.getDataSIMInterface(dataSimIcc.iccId);
var wifiRequests, simRequests;
var pendingRequests = 0;
function checkForCompletion() {
pendingRequests--;
if (pendingRequests === 0) {
updateDataUsage();
}
}
function updateDataUsage() {
var lastDataUsage = {
timestamp: new Date(),
start: start,
end: end,
today: today,
wifi: {
apps: {},
total: 0,
samples: []
},
mobile: {
apps: {},
total: 0,
samples: []
}
};
function saveLastDataUsage(perApp) {
// XXX: raw samples are apparently too much to persist,
// so we only store aggregate totals
var savedUsage = {};
Object.keys(lastDataUsage).forEach(function(key) {
var value = lastDataUsage[key];
if (key === 'wifi' || key === 'mobile') {
// If saving per app totals we should retain the totals for the
// global query, persisted in settings by the time of this call.
if (perApp) {
value = settings.lastDataUsage[key];
}
savedUsage[key] = { apps: {}, total: value.total };
Object.keys(value.apps).forEach(function(manifest) {
savedUsage[key].apps[manifest] = {
total: value.apps[manifest].total
};
});
} else {
savedUsage[key] = value;
}
});
ConfigManager.setOption({ 'lastDataUsage': savedUsage },
function _onSetItem() {
debug('Statistics up to date and stored.');
}
);
}
// Aggregate fine-grained app specific samples into a top level per-day
// and per-network sample list
function aggregateSamples(network) {
if (!network.apps) {
return;
}
var manifestURLs = Object.keys(network.apps);
if (manifestURLs.length === 0) {
return;
}
var samplesByDate = {};
var today = Toolkit.toMidnight(new Date());
// offset in milliseconds
var offset = today.getTimezoneOffset() * 60 * 1000;
manifestURLs.forEach(function(manifestURL) {
var samples = network.apps[manifestURL].samples;
samples.forEach(function(sample) {
if (sample.date && sample.date.__date__) {
sample.date = new Date(sample.date.__date__);
}
var sampleLocalTime = sample.date.getTime() + offset;
var sampleUTCDate = Toolkit.toMidnight(new Date(sampleLocalTime));
var aggregateSample = samplesByDate[sampleUTCDate.getTime()];
if (!aggregateSample) {
aggregateSample = samplesByDate[sampleUTCDate.getTime()] = {
value: 0,
date: sample.date
};
}
aggregateSample.value += sample.value;
});
});
var dates = Object.keys(samplesByDate);
network.samples = dates.map(function(date) {
return samplesByDate[date];
});
network.samples.sort(function(a, b) {
return a.date.getTime() - b.date.getTime();
});
}
// Handle results from requests to the network stats database
// for data usage on a specific network
function handleResult(request, network) {
var result = request.result;
var data = adaptData(result);
var manifestURL = request.result.appManifestURL;
// ignore the 'null' manifestURL
if (manifestURL && manifestURL !== 'null') {
var isBrowser = request.result.browsingTrafficOnly &&
manifestURL === Common.SYSTEM_MANIFEST;
if (isBrowser) {
manifestURL = Common.BROWSER_APP.manifestURL;
}
network.apps[manifestURL] = {
samples: data[0],
total: data[1]
};
} else {
network.samples = network.samples.concat(data[0]);
}
network.total += data[1];
}
if (simRequests) {
simRequests.forEach(function(request) {
handleResult(request, lastDataUsage.mobile);
});
aggregateSamples(lastDataUsage.mobile);
}
if (wifiRequests) {
wifiRequests.forEach(function(request) {
handleResult(request, lastDataUsage.wifi);
});
aggregateSamples(lastDataUsage.wifi);
}
var perApp = apps && apps.length > 0;
saveLastDataUsage(perApp);
result.status = 'success';
result.data = lastDataUsage;
// Once bug 1083680 is solved, both caches should contain the same values
// so we could use only `costcontrol.lastDataResults`.
if (perApp) {
costcontrol.lastDataResultsPerApp = lastDataUsage;
} else {
costcontrol.lastDataResults = lastDataUsage;
}
debug('Returning up to date statistics.');
if (callback) {
callback(result);
}
}
function requestPerAppUsage(networkId) {
function requestSamples(options) {
pendingRequests++;
var req = statistics.getSamples(networkId, start, end, options);
req.onsuccess = checkForCompletion;
return req;
}
var requests;
if (apps && apps.length > 0) {
requests = [];
apps.forEach(function(manifestURL) {
// Ignoring the system app because of the system traffic is going to
// be calculated (Front-end workaround for Bug 1083680)
if (manifestURL !== Common.SYSTEM_MANIFEST) {
var options = { appManifestURL: manifestURL };
// Browser app is included on System app
if (manifestURL === Common.BROWSER_APP.manifestURL) {
options.appManifestURL = Common.SYSTEM_MANIFEST;
options.browsingTrafficOnly = true;
}
requests.push(requestSamples(options));
}
});
} else {
requests = [requestSamples()];
}
return requests;
}
//Recover current Simcard info
if (currentSimcardNetwork) {
simRequests = requestPerAppUsage(currentSimcardNetwork);
}
if (wifiInterface) {
wifiRequests = requestPerAppUsage(wifiInterface);
}
if (pendingRequests === 0) {
updateDataUsage();
}
}
// Transform data usage to the model accepted by the render
function adaptData(networkStatsResult) {
var data = networkStatsResult.data;
var output = [];
var totalData, accum = 0;
for (var i = 0; i < data.length; i++) {
var item = data[i];
if (item.txBytes === undefined) {
output.push({ date: item.date });
continue;
}
totalData = 0;
if (item.rxBytes) {
totalData += item.rxBytes;
}
if (item.txBytes) {
totalData += item.txBytes;
}
accum += totalData;
output.push({
value: totalData,
date: item.date
});
}
return [output, accum];
}
return {
getInstance: getInstance,
reset: function _onReset() {
costcontrol = null;
}
};
}());

View file

@ -0,0 +1,65 @@
(function(exports) {
'use strict';
const store = new Map();
function isParentAsyncStorageAvailable() {
return !!(window.parent &&
window.parent !== window &&
window.parent.asyncStorage);
}
function callCallback(value, callback) {
if (typeof callback === 'function') {
setTimeout(callback.bind(null, value));
}
}
Object.defineProperty(exports, 'asyncStorage', {
value: {
getItem: function asm_getItem(key, callback) {
if (isParentAsyncStorageAvailable()) {
window.parent.asyncStorage.getItem(key, callback);
return;
}
callCallback(store.get(key) || null, callback);
},
setItem: function asm_setItem(key, value, callback) {
if (isParentAsyncStorageAvailable()) {
window.parent.asyncStorage.setItem(key, value, callback);
return;
}
callCallback(store.set(key, value), callback);
},
removeItem: function asm_removeItem(key, callback) {
if (isParentAsyncStorageAvailable()) {
window.parent.asyncStorage.removeItem(key, callback);
return;
}
callCallback(store.delete(key), callback);
},
clear: function asm_clear(callback) {
if (isParentAsyncStorageAvailable()) {
window.parent.asyncStorage.clear(callback);
return;
}
callCallback(store.clear(), callback);
},
length: function(callback) {
if (isParentAsyncStorageAvailable()) {
window.parent.asyncStorage.length(callback);
return;
}
callCallback(store.size, callback);
},
key: function() {
throw new Error('Not Implemented');
}
}
});
})(window);

View file

@ -0,0 +1,24 @@
(function() {
'use strict';
if (navigator.mozAlarms) {
return;
}
navigator.mozAlarms = {
add: function() {
var returnResult = {};
setTimeout(() => {
returnResult.result = {};
if (typeof returnResult.onsuccess === 'function') {
returnResult.onsuccess.call(returnResult);
}
});
return returnResult;
},
remove: function() {}
};
})();

View file

@ -0,0 +1,33 @@
(function() {
'use strict';
if (navigator.mozApps.mgmt) {
return;
}
const Apps = [{
manifestURL: 'app://system.gaiamobile.org/manifest.webapp',
origin: 'app://system.gaiamobile.org',
manifest: {
name: 'Service'
}
}];
navigator.mozApps.mgmt = {
getAll: function() {
var returnResult = {};
setTimeout(() => {
returnResult.result = Apps;
if (typeof returnResult.onsuccess === 'function') {
returnResult.onsuccess.call(returnResult, {
target: returnResult
});
}
});
return returnResult;
}
};
})();

View file

@ -0,0 +1,21 @@
(function() {
'use strict';
if (navigator.mozIccManager) {
return;
}
navigator.mozIccManager = {
iccIds: ['1234575100210522938'],
getIccById: function(id) {
return {
cardState: 'ready',
iccInfo: {
mcc: id,
mnc: id
}
};
}
};
})();

View file

@ -0,0 +1,11 @@
(function() {
'use strict';
if (navigator.mozMobileConnections) {
return;
}
navigator.mozMobileConnections = navigator.mozIccManager.iccIds.map((id) => {
return { iccId: id};
});
})();

View file

@ -0,0 +1,145 @@
(function() {
'use strict';
if (navigator.mozNetworkStats) {
return;
}
const Today = new Date();
const NetworkInterfaceType = {
WIFI: 0,
MOBILE: 1
};
const Networks = new Map([
['0', {
type: NetworkInterfaceType.WIFI,
id: '0'
}]
].concat(navigator.mozIccManager.iccIds.map((id) => [id, {
type: NetworkInterfaceType.MOBILE,
id: id
}])));
// If we run app in Mulet with the direct URL, then we won't have API for
// navigator.mozApps.mgmt available, but if we run entire gaia in Mulet (aka
// from HomeScreen) then this API will be provided, so it's better to not have
// dependency on the mock navigator.mozApps.mgmt here and just hard code known
// app manifest URLs.
const appManifestURLs = ['app://system.gaiamobile.org/manifest.webapp'];
function generateSample(interfaceId, manifestURL, startValueInMb) {
return {
appManifestURL: manifestURL,
network: Networks.get(interfaceId),
start: new Date('2014-08-14T05:00:00.000Z'),
end: new Date('2014-08-31T05:00:00.000Z'),
data: [{
rxBytes: startValueInMb * 100000,
txBytes: (startValueInMb + 100) * 100000,
date: new Date(
Today.getFullYear(),
Today.getMonth(),
Today.getDate() - 15
)
}, {
rxBytes: startValueInMb * 200000,
txBytes: (startValueInMb + 100) * 200000,
date: Today
}]
};
}
const Samples = new Map([
['0', new Map([
[null, generateSample('0', null, 457)],
[appManifestURLs[0], generateSample('0', appManifestURLs[0], 457)]
])],
[navigator.mozIccManager.iccIds[0], new Map([
[null, generateSample(navigator.mozIccManager.iccIds[0], null, 257)],
[
appManifestURLs[0],
generateSample(
navigator.mozIccManager.iccIds[0],
appManifestURLs[0],
257
)
]
])]
]);
const Alarms = new Map();
navigator.mozNetworkStats = {
WIFI: NetworkInterfaceType.WIFI,
getAvailableNetworks: function() {
var returnResult = {};
setTimeout(() => {
returnResult.result = [];
Networks.forEach((network) => {
returnResult.result.push(network);
});
if (typeof returnResult.onsuccess === 'function') {
returnResult.onsuccess.call(returnResult);
}
});
return returnResult;
},
getSamples: function getSamples(networkInterface, start, end, options) {
var networkSamples = Samples.get(networkInterface.id);
var returnResult = {};
setTimeout(() => {
if (options && options.appManifestURL) {
returnResult.result = networkSamples.get(options.appManifestURL);
} else {
returnResult.result = networkSamples.get(null);
}
if (typeof returnResult.onsuccess === 'function') {
returnResult.onsuccess.call(returnResult);
}
});
return returnResult;
},
getAllAlarms: function(interfaceId) {
var returnResult = {};
setTimeout(() => {
returnResult.result = Alarms.get(interfaceId) || [];
if (typeof returnResult.onsuccess === 'function') {
returnResult.onsuccess.call(returnResult);
}
});
return returnResult;
},
addAlarm: function(interfaceId, alarm) {
var returnResult = {};
setTimeout(() => {
var interfaceAlarms = Alarms.get(interfaceId) || [];
interfaceAlarms.push(alarm);
Alarms.set(interfaceId, interfaceAlarms);
if (typeof returnResult.onsuccess === 'function') {
returnResult.onsuccess.call(returnResult);
}
});
return returnResult;
}
};
})();

View file

@ -0,0 +1,35 @@
(function() {
'use strict';
if (navigator.mozSettings) {
return;
}
const Settings = new Map([['locale.hour12', false]]);
navigator.mozSettings = {
createLock: function() {
return {
get: function(key) {
var request = {
addEventListener: function(eventName, callback) {
request[eventName] = callback;
}
};
window.setTimeout(function() {
if (typeof request.onsuccess === 'function') {
request.result = {};
request.result[key] = Settings.get(key);
request.onsuccess.call(request);
}
});
return request;
}
};
},
addObserver: function() {}
};
})();

289
gaia/apps/costcontrol/js/fte.js Executable file
View file

@ -0,0 +1,289 @@
/* global AutoSettings, BalanceLowLimitView, Common, ConfigManager, SimManager,
dataLimitConfigurer, LazyLoader, debug, ViewManager */
/*
* First time experience is in charge of set up the application.
*/
(function() {
'use strict';
// Fallback from some values, just in case they are missed from configuration
var DEFAULT_LOW_LIMIT_THRESHOLD = 3;
var defaultLowLimitThreshold = DEFAULT_LOW_LIMIT_THRESHOLD;
window.addEventListener('DOMContentLoaded', function _onDomReady() {
initLazyFTE();
});
function initLazyFTE() {
var SCRIPTS_NEEDED = [
'js/utils/debug.js',
'js/utils/toolkit.js',
'js/sim_manager.js',
'js/common.js',
'js/config/config_manager.js',
'js/views/BalanceLowLimitView.js',
'js/view_manager.js',
'js/settings/autosettings.js'
];
LazyLoader.load(SCRIPTS_NEEDED, function onScriptsLoaded() {
setupFTE();
parent.postMessage({
type: 'fte_ready',
data: ''
}, Common.COST_CONTROL_APP);
window.addEventListener('localized', _onLocalize);
});
}
var wizard, vmanager;
var toStep2, step = 0;
function setupFTE() {
if (SimManager.isMultiSim()) {
window.addEventListener('dataSlotChange', function _onDataSimChange() {
window.removeEventListener('dataSlotChange', _onDataSimChange);
// Close FTE if change the SimCard for data connections
Common.closeFTE();
});
}
ConfigManager.requestAll(function _onSettings(configuration, settings) {
wizard = document.getElementById('firsttime-view');
vmanager = new ViewManager();
// Getting some values from config
if (configuration && configuration.default_low_limit_threshold) {
defaultLowLimitThreshold = configuration.default_low_limit_threshold;
}
// Initialize resetTime and trackingPeriod to default values
ConfigManager.setOption({resetTime: 1, trackingPeriod: 'monthly' });
var mode = ConfigManager.getApplicationMode();
var SCRIPTS_NEEDED = [
'js/settings/limitdialog.js',
'js/utils/formatting.js'
];
LazyLoader.load(SCRIPTS_NEEDED, function onScriptsLoaded() {
Common.loadNetworkInterfaces(function() {
AutoSettings.addType('data-limit', dataLimitConfigurer);
if (mode === 'DATA_USAGE_ONLY') {
AutoSettings.initialize(ConfigManager, vmanager,
'#non-vivo-step-2');
}
});
});
// Currency is set by config as well
if (configuration && configuration.credit &&
configuration.credit.currency) {
document.getElementById('currency').textContent =
configuration.credit.currency;
}
if (mode === 'DATA_USAGE_ONLY') {
debug('FTE for non supported SIM');
wizard.dataset.steps = '3';
reset(['step-1', 'non-vivo-step-1', 'non-vivo-step-2']);
AutoSettings.initialize(ConfigManager, vmanager, '#non-vivo-step-1');
} else {
wizard.dataset.steps = '4';
reset(['step-1', 'step-2']);
AutoSettings.initialize(ConfigManager, vmanager, '#step-1');
// Plantype selection
toStep2 = document.getElementById('to-step-2');
toStep2.disabled = true;
document.getElementById('prepaid-plan')
.addEventListener('click', selectTrack);
document.getElementById('postpaid-plan')
.addEventListener('click', selectTrack);
addLowLimitStepConstrains();
}
// Navigation
var next = document.querySelectorAll('[data-navigation=next]');
[].forEach.call(next, function cc_eachNext(nextButton) {
nextButton.addEventListener('click', onNext);
});
var prev = document.querySelectorAll('[data-navigation=back]');
[].forEach.call(prev, function cc_eachPrev(prevButton) {
prevButton.addEventListener('click', onBack);
});
var finish = document.querySelectorAll('[data-navigation=finish]');
[].forEach.call(finish, function cc_eachFinish(finishButton) {
finishButton.addEventListener('click', onFinish);
});
// Needed to put the alarms for balance and networkUsage
LazyLoader.load(['js/costcontrol.js'], function() {
var messageHandlerFrame = document.getElementById('message-handler');
messageHandlerFrame.src = 'message_handler.html';
});
});
}
function _onLocalize() {
Common.localizeWeekdaySelector(
document.getElementById('pre3-select-weekday'));
Common.localizeWeekdaySelector(
document.getElementById('post2-select-weekday'));
Common.localizeWeekdaySelector(
document.getElementById('non2-select-weekday'));
function _setResetTimeToDefault(evt) {
var today = new Date();
var defaultResetTime = (evt.target.value === 'weekly') ?
today.getDay() : today.getDate();
ConfigManager.setOption({ resetTime: defaultResetTime });
}
// Localized resetTime on trackingPeriod change
var trackingPeriodSelector = document
.querySelectorAll('[data-option="trackingPeriod"]');
[].forEach.call(trackingPeriodSelector, function _reset(tPeriodSel) {
tPeriodSel.addEventListener('change', _setResetTimeToDefault);
});
}
if (window.location.hash) {
wizard = document.getElementById('firsttime-view');
if (window.location.hash === '#PREPAID' ||
window.location.hash === '#POSTPAID') {
wizard.querySelector('.authed-sim').hidden = false;
} else {
wizard.querySelector('.nonauthed-sim').hidden = false;
}
}
// TRACK SETUP
var currentTrack = ['step-1', 'step-2'];
function selectTrack(evt) {
if (evt.target.value === 'prepaid') {
currentTrack = ['step-1', 'step-2', 'prepaid-step-2', 'prepaid-step-3'];
AutoSettings.initialize(ConfigManager, vmanager, '#prepaid-step-2');
AutoSettings.initialize(ConfigManager, vmanager, '#prepaid-step-3');
balanceLowLimitView.disabled = false;
ConfigManager.setOption({
dataLimitValue: 40,
dataLimitUnit: 'MB',
lowLimit: true,
lowLimitThreshold: defaultLowLimitThreshold
});
} else if (evt.target.value === 'postpaid') {
currentTrack = ['step-1', 'step-2', 'postpaid-step-2', 'postpaid-step-3'];
AutoSettings.initialize(ConfigManager, vmanager, '#postpaid-step-2');
AutoSettings.initialize(ConfigManager, vmanager, '#postpaid-step-3');
balanceLowLimitView.disabled = true;
ConfigManager.setOption({ dataLimitValue: 2, dataLimitUnit: 'GB' });
}
ConfigManager.setOption({ plantype: evt.target.value }, function _onSet() {
toStep2.disabled = false;
});
}
// NAVIGATION
function reset(track) {
// Reposition screens
var currentScreen = document.getElementById(currentTrack[step]);
var newStartScreen = document.getElementById(track[0]);
currentScreen.dataset.viewport = 'left';
newStartScreen.dataset.viewport = 'right';
delete newStartScreen.dataset.viewport;
for (var i = 1; i < track.length; i += 1) {
var id = track[i];
if (id) {
var screen = document.getElementById(id);
screen.dataset.viewport = 'right';
}
}
// Reset state
step = 0;
currentTrack = track;
}
function onNext() {
if (step === currentTrack.length - 1) {
return;
}
var currentId = currentTrack[step];
var currentScreen = document.getElementById(currentId);
var nextScreenId = currentTrack[step + 1];
var nextScreen = document.getElementById(nextScreenId);
// Enter the next screen
delete nextScreen.dataset.viewport;
currentScreen.dataset.viewport = 'left';
step += 1;
// Validate when in step 2 in order to restore buttons and errors
if (step === 2) {
balanceLowLimitView && balanceLowLimitView.validate();
}
}
function onBack() {
if (step === 0) {
return;
}
var currentId = currentTrack[step];
var currentScreen = document.getElementById(currentId);
var prevScreenId = currentTrack[step - 1];
var prevScreen = document.getElementById(prevScreenId);
// Enter the previous screen
delete prevScreen.dataset.viewport;
currentScreen.dataset.viewport = 'right';
step -= 1;
}
function onFinish(evt) {
evt.target.disabled = true;
SimManager.requestDataSimIcc(function(dataSim) {
ConfigManager.requestSettings(dataSim.iccId,
function _onSettings(settings) {
ConfigManager.setOption({ fte: false }, function _returnToApp() {
Common.updateNextReset(settings.trackingPeriod, settings.resetTime,
function _returnToTheApplication() {
Common.startApp();
}
);
});
});
});
}
// Add particular constrains to the page where setting low limit button
var balanceLowLimitView;
function addLowLimitStepConstrains() {
var nextButton = document.getElementById('low-limit-next-button');
balanceLowLimitView = new BalanceLowLimitView(
document.getElementById('low-limit'),
document.getElementById('low-limit-input')
);
balanceLowLimitView.onvalidation = function(evt) {
nextButton.disabled = !evt.isValid;
};
}
}());

View file

@ -0,0 +1,90 @@
/* global debug, Promise */
/* exported IACManager */
'use strict';
var IACManager = (function() {
var TIMEOUT = 60 * 1000; // 1 minute
var configuration;
function init(cfg) {
configuration = cfg;
}
function broadcastStartOfSMSQuery(type) {
return IACManager._sendBroadcastMessage(_generateMsg(type, 'enable'));
}
function broadcastEndOfSMSQuery(type) {
return IACManager._sendBroadcastMessage(_generateMsg(type, 'disable'));
}
function _sendBroadcastMessage(msg) {
return new Promise(function(resolve, reject) {
navigator.mozApps.getSelf().onsuccess = function(evt) {
var app = evt.target.result;
app.connect('costcontrolSmsQuery').then(function onConnAccepted(ports) {
debug('Connection with sms app established...');
ports.forEach(function(port) {
var waitingForAnswerTimeout;
port.onmessage = function() {
clearTimeout(waitingForAnswerTimeout);
resolve();
};
// if no one has replied the message in one minute, the proposal
// will be rejected
waitingForAnswerTimeout = window.setTimeout(function() {
var errorMsg = 'Rejection by Timeout. No received answer to ' +
msg.action + ' ' + msg.type + ' request.';
reject(errorMsg);
}, TIMEOUT);
port.postMessage(msg);
});
}, function onConnRejected(reason) {
console.log('Rejected connection attempt due to', reason);
reject(reason);
});
};
});
}
function _getSmsNumberList(service) {
var silentSmsList = [];
if (configuration && configuration[service]) {
silentSmsList = configuration[service].senders;
if (silentSmsList.indexOf(configuration[service].destination) === -1) {
silentSmsList.push(configuration[service].destination);
}
}
return silentSmsList;
}
function _reset() {
configuration = null;
}
function _generateMsg(service, action) {
var outgoingNumber = configuration[service] &&
configuration[service].destination;
var incomingNumbers = configuration[service] &&
configuration[service].senders;
return {
action: action,
type: service,
smsNumbers: _getSmsNumberList(service),
outgoingNumber: outgoingNumber,
incomingNumbers: incomingNumbers
};
}
return {
init: init,
broadcastStartOfSMSQuery: broadcastStartOfSMSQuery,
broadcastEndOfSMSQuery: broadcastEndOfSMSQuery,
_getSmsNumberList: _getSmsNumberList,
_generateMsg: _generateMsg,
_sendBroadcastMessage: _sendBroadcastMessage,
_reset: _reset
};
}());

View file

@ -0,0 +1,619 @@
/* global ConfigManager, CostControl, debug, Common, asyncStorage, Formatting,
NotificationHelper, _, MozActivity, NetworkUsageAlarm, LazyLoader,
SimManager, IACManager, DEBUGGING
*/
/*jshint -W020 */
/* The previous directive,ignore the "Read only" errors, that are produced when
redirect global objects (Common and Costcontrol) to parent versions to avoid
conflicts.
*/
(function() {
'use strict';
function inStandAloneMode() {
return window.parent.location.pathname === '/message_handler.html';
}
// Redirect global objects to parent versions to avoid conflicts
if (!inStandAloneMode()) {
ConfigManager = window.parent.ConfigManager;
CostControl = window.parent.CostControl;
SimManager = window.parent.SimManager;
}
// XXX: This case implies that message handler triggered by system
// (inStandAlone check) has replaced CC application (history's length check).
//
// This only occurs when the system (window manager) has detected there is
// already an iframe for CC application, it is in background and it is
// not the message handler. So, as the CC index.html uses the this file
// inside an iframe (no standalone mode), all the messages should be attended
// so we can conclude **there is nothing to do**.
if (inStandAloneMode() && window.history.length > 1) {
debug('Nothing to do in message handler, returning to app...');
window.history.back();
}
function inApplicationMode() {
return window.parent.location.pathname === '/index.html';
}
// Close if in standalone mode
var closing;
function closeIfProceeds() {
debug('Checking for closing...');
if (inStandAloneMode()) {
closing = Common.closeApplication();
}
}
// XXX: Remove from here when this is solved
// https://bugzilla.mozilla.org/show_bug.cgi?id=800431
function addAlarmTimeout(type, delay) {
var date = new Date();
date.setTime(date.getTime() + delay);
var request = navigator.mozAlarms.add(date, 'ignoreTimezone', {type: type});
return request;
}
window.addAlarmTimeout = addAlarmTimeout;
// XXX: Remove from here when this is solved
// https://bugzilla.mozilla.org/show_bug.cgi?id=800431
function setNextReset(when, callback) {
// XXX: This is not part of configuration by SIM so we bypass ConfigManager
asyncStorage.getItem('nextResetAlarm', function(id) {
// There is already an alarm, remove it
debug('Current nextResetAlarm', id + '.', id ? 'Removing.' : '');
if (id) {
navigator.mozAlarms.remove(id);
}
// If no when, disable alarms passing null
if (!when) {
debug('Automatic reset disabled');
updateResetAttributes(null, null, callback);
return;
}
// If the alarm is in the past, fake it was launched
if (when.getTime() < Date.now()) {
debug('Faking a past reset alarm');
_onAlarm({ data: { type: 'nextReset' } });
if (callback) {
setTimeout(callback);
}
return;
}
// If when is provided, request an alarm an set the new values
var alarms = navigator.mozAlarms;
var request = alarms.add(when, 'ignoreTimezone', {type: 'nextReset' });
request.onsuccess = function _onSuccess() {
debug('Setting nextResetAlarm', request.result, 'to', when);
updateResetAttributes(request.result, when, callback);
};
});
}
window.setNextReset = setNextReset;
function getTopUpTimeout(callback) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
var request = navigator.mozAlarms.getAll();
request.onsuccess = function(e) {
var alarms = e.target.result;
var length = alarms.length;
if (!length) {
callback(null);
return;
}
var refId = settings.waitingForTopUp;
var index = 0, alarm, found = false;
while (index < length && !found) {
alarm = alarms[index];
found = (alarm.id === refId);
index++;
}
if (found) {
debug('TopUp timeout found:', alarm.date);
callback(alarm.date);
} else {
callback(null);
}
};
});
});
}
window.getTopUpTimeout = getTopUpTimeout;
function addNetworkUsageAlarm(dataInterface, dataLimit, onsuccess, onerror) {
onsuccess = onsuccess || function() {};
onerror = onerror || function() {};
NetworkUsageAlarm.updateAlarm(dataInterface, dataLimit, onsuccess, onerror);
}
window.addNetworkUsageAlarm = addNetworkUsageAlarm;
// Update the nextResetAlarm and nextReset values and request for
// synchronization.
function updateResetAttributes(alarmId, date, callback) {
asyncStorage.setItem('nextResetAlarm', alarmId, function _updateOption() {
ConfigManager.setOption({ nextReset: date }, function _sync() {
localStorage.sync = 'nextReset#' + Math.random();
if (callback) {
callback();
}
});
});
}
function sendIncorrectTopUpNotification(callback) {
var title = 'topup-incorrectcode-title2';
var message = 'topup-incorrectcode-message3';
sendNotification('topUpError', title, message);
if (callback) {
callback();
}
}
function sendBalanceThresholdNotification(remaining, settings, callback) {
debug('Low limit already notified:', settings.lowLimitNotified);
debug('Zero balance already notified:', settings.lowLimitNotified);
// Zero reached notification
var type;
if (remaining.balance === 0 && !settings.zeroBalanceNotified) {
type = 'zeroBalance';
// There is a limit an we are below that limit and we did not notified yet
} else if (settings.lowLimit &&
remaining.balance < settings.lowLimitThreshold &&
!settings.lowLimitNotified) {
type = 'lowBalance';
// No need for notification
} else if (callback) {
setTimeout(callback);
return;
}
debug('Notification type:', type);
// Get l10n for remaining balance
var remainingBalance = _('currency', {
currency: remaining.currency,
value: remaining.balance
});
// Compose notification and send it
var title = 'low-balance-notification-title';
var message = {
id: 'low-balance-notification-text',
args: { remaining: remainingBalance }
};
if (type === 'zeroBalance') {
title = 'usage';
message = 'zero-balance-message';
}
sendNotification(type, title, message);
// Finally mark the notification as sent
var update = {};
var notified = (type === 'lowBalance') ? 'lowLimitNotified' :
'zeroBalanceNotified';
update[notified] = true;
ConfigManager.setOption(update, callback);
}
window.sendBalanceThresholdNotification = sendBalanceThresholdNotification;
// When receiving an alarm, differenciate by type and act
function _onAlarm(alarm) {
clearTimeout(closing);
SimManager.requestDataSimIcc(function(dataSimIcc) {
function _launchNextReset() {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
Common.resetAll(function updateNextResetAndClose() {
Common.updateNextReset(settings.trackingPeriod, settings.resetTime,
closeIfProceeds);
});
});
}
switch (alarm.data.type) {
case 'balanceTimeout':
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
settings.errors.BALANCE_TIMEOUT = true;
ConfigManager.setOption(
{ 'errors': settings.errors, 'waitingForBalance': null },
function _onBalanceTimeout() {
debug('Timeout for balance');
debug('Trying to synchronize!');
localStorage.sync = 'errors#' + Math.random();
closeIfProceeds();
}
);
});
break;
case 'topupTimeout':
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
settings.errors.TOPUP_TIMEOUT = true;
ConfigManager.setOption(
{ 'errors': settings.errors, 'waitingForTopUp': null },
function _onBalanceTimeout() {
debug('Timeout for topup');
debug('Trying to synchronize!');
localStorage.sync = 'errors#' + Math.random();
closeIfProceeds();
}
);
});
break;
case 'nextReset':
if (!Common.allNetworkInterfaceLoaded) {
Common.loadNetworkInterfaces(_launchNextReset);
} else {
_launchNextReset();
}
break;
}
});
}
function getNofificationAction(type, app) {
var activityName, action;
var noop = function() {};
switch (type) {
case 'lowBalance':
case 'zeroBalance':
activityName = 'costcontrol/balance';
action = function() {
if (window.parent.BalanceTab) {
window.parent.CostControlApp.showBalanceTab();
}
};
break;
case 'topUpError' :
activityName = 'costcontrol/balance';
action = function() {
if (window.parent.BalanceTab) {
window.parent.BalanceTab.topUpWithCode(true);
}
};
break;
default:
activityName = 'costcontrol/data_usage';
action = function() {
if (window.parent.CostControlApp) {
window.parent.CostControlApp.showDataUsageTab();
}
};
break;
}
if (!inStandAloneMode()) {
return function() {
if (inApplicationMode()) {
app.launch();
action && action();
} else {
type = new MozActivity({name: activityName});
}
};
}
return noop;
}
function sendNotification(notificationType, title, message) {
navigator.mozApps.getSelf().onsuccess = function _onAppReady(evt) {
var app = evt.target.result;
var iconURL = NotificationHelper.getIconURI(app);
var navigate = getNofificationAction(notificationType, app);
NotificationHelper.send(title, {
'bodyL10n': message,
'icon': iconURL,
'data': notificationType
}).then(function(notification) {
notification.addEventListener('click', function() {
notification.close();
navigate();
});
});
};
}
function _onNetworkAlarm(alarm) {
clearTimeout(closing);
navigator.mozApps.getSelf().onsuccess = function _onAppReady(evt) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
var limit = Common.getDataLimit(settings);
var limitText = Formatting.formatData(Formatting.smartRound(limit));
var title = {
id: 'data-limit-notification-title2',
args: { limit: limitText }
};
var message = 'data-limit-notification-text2';
sendNotification('dataUsage', title, message);
ConfigManager.setOption({ 'dataUsageNotified': true },
closeIfProceeds);
return;
});
});
};
}
function disableSilentModeFor(type, configuration) {
LazyLoader.load('js/iac_manager.js', function() {
IACManager.init(configuration);
IACManager.broadcastEndOfSMSQuery(type).then(function(msg) {
debug('After broadcasting for ' + type + ' (disabling)');
});
});
}
// Register in standalone or for application
var costcontrol;
function _getCCInstance() {
CostControl.getInstance(function _onCostControl(ccontrol) {
costcontrol = ccontrol;
if (inStandAloneMode() || inApplicationMode()) {
debug('Installing handlers');
// When receiving an SMS, recognize and parse
navigator.mozSetMessageHandler('sms-received', function _onSMS(sms) {
clearTimeout(closing);
ConfigManager.requestAll(function _onInfo(configuration, settings) {
var isBalanceResponse =
configuration.balance &&
Array.isArray(configuration.balance.senders) &&
configuration.balance.senders.indexOf(sms.sender) > -1;
var isTopupResponse =
configuration.topup &&
Array.isArray(configuration.topup.senders) &&
configuration.topup.senders.indexOf(sms.sender) > -1;
// Non expected SMS
if (!isBalanceResponse && !isTopupResponse) {
closeIfProceeds();
return;
}
// Parse the message
debug('Parsing received SMS');
var isBalance, isConfirmation, isError;
isBalance = isConfirmation = isError = false;
debug('Trying to recognize balance SMS');
var description = new RegExp(configuration.balance.regexp);
var balanceData = sms.body.match(description);
if (!balanceData) {
debug('Trying to recognize zero balance SMS');
// Some carriers use another response messages format
// for zero balance
var zeroDescription = configuration.balance.zero_regexp ?
new RegExp(configuration.balance.zero_regexp) : null;
if (zeroDescription && zeroDescription.test(sms.body)) {
balanceData = ['0.00', '0', '0'];
}
}
isBalance = !!balanceData;
if (!isBalance || balanceData.length < 2) {
console.warn('Impossible to parse balance message.');
debug('Trying to recognize TopUp confirmation SMS');
description = new RegExp(configuration.topup.confirmation_regexp);
isConfirmation = !!sms.body.match(description);
if (!isConfirmation) {
console.warn('Impossible to parse TopUp confirmation message.');
debug('Trying to recognize TopUp error SMS');
description =
new RegExp(configuration.topup.incorrect_code_regexp);
isError = !!sms.body.match(description);
if (!isError) {
console.warn('Impossible to parse TopUp confirmation msg.');
}
}
}
if (!isBalance && !isConfirmation && !isError) {
closeIfProceeds();
return;
}
if (!DEBUGGING &&
(isBalance && (settings.waitingForBalance !== null)) ||
(isConfirmation && (settings.waitingForTopUp !== null))) {
var mobileMessageManager = window.navigator.mozMobileMessage;
mobileMessageManager.delete(sms.id);
}
if (isBalance) {
// Compose new balance
var integer = balanceData[1].replace(/[^0-9]/g, '');
var decimal = balanceData[2] || '0';
var newBalance = {
balance: parseFloat(integer + '.' + decimal),
currency: configuration.credit.currency,
timestamp: new Date()
};
if (settings.waitingForBalance !== null) {
// Remove the timeout
navigator.mozAlarms.remove(settings.waitingForBalance);
debug('Balance timeout:', settings.waitingForBalance,
'removed');
disableSilentModeFor('balance', configuration);
}
// Store new balance and sync
ConfigManager.setOption(
{ 'lastBalance': newBalance, 'waitingForBalance': null },
function _onSet() {
debug('Balance up to date and stored');
debug('Trying to synchronize!');
localStorage.sync = 'lastBalance#' + Math.random();
sendBalanceThresholdNotification(newBalance, settings,
closeIfProceeds);
}
);
} else if (isConfirmation) {
if (settings.waitingForTopUp !== null) {
// Store SUCCESS for TopIp and sync
navigator.mozAlarms.remove(settings.waitingForTopUp);
debug('TopUp timeout:', settings.waitingForTopUp, 'removed');
disableSilentModeFor('topup', configuration);
}
ConfigManager.setOption(
{
'waitingForTopUp': null,
'lowLimitNotified': false,
'zeroBalanceNotified': false
},
function _onSet() {
debug('TopUp confirmed!');
debug('Trying to synchronize!');
localStorage.sync = 'waitingForTopUp#' + Math.random();
closeIfProceeds();
}
);
} else if (isError) {
// Store ERROR for TopUp and sync
settings.errors.INCORRECT_TOPUP_CODE = true;
navigator.mozAlarms.remove(settings.waitingForTopUp);
debug('TopUp timeout: ', settings.waitingForTopUp, 'removed');
ConfigManager.setOption(
{
'errors': settings.errors,
'waitingForTopUp': null,
'lowLimitNotified': false,
'zeroBalanceNotified': false
},
function _onSet() {
debug('Balance up to date and stored');
debug('Trying to synchronize!');
localStorage.sync = 'errors#' + Math.random();
sendIncorrectTopUpNotification(closeIfProceeds);
}
);
}
});
});
navigator.mozSetMessageHandler('networkstats-alarm', _onNetworkAlarm);
navigator.mozSetMessageHandler('alarm', _onAlarm);
// Count a new SMS
navigator.mozSetMessageHandler('sms-sent', function _onSent(sms) {
clearTimeout(closing);
debug('SMS sent!');
var configuration = ConfigManager.configuration;
var mode = ConfigManager.getApplicationMode();
if (mode === 'PREPAID' &&
!costcontrol.isBalanceRequestSMS(sms, configuration)) {
costcontrol.request({ type: 'balance' });
}
if (mode === 'PREPAID' &&
costcontrol.isBalanceRequestSMS(sms, configuration)) {
closeIfProceeds();
} else {
var mobileMessageManager = window.navigator.mozMobileMessage;
var infoRequest =
mobileMessageManager.getSegmentInfoForText(sms.body);
infoRequest.onsuccess = function onInfo(evt) {
var realCount, smsInfo = evt.target.result;
if (!smsInfo || !smsInfo.segments) {
console.error(
'Invalid getSegmentInfoForText() result. Counting 1 segment');
realCount = 1;
} else {
realCount = smsInfo.segments;
}
updateSMSCount(realCount);
};
infoRequest.onerror = function onError() {
console.error('Can not retrieve segment info for body ' +
sms.body);
updateSMSCount(1);
};
}
function updateSMSCount(count) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
settings.lastTelephonyActivity.timestamp = new Date();
settings.lastTelephonyActivity.smscount += count;
ConfigManager.setOption({
lastTelephonyActivity: settings.lastTelephonyActivity
}, function _sync() {
localStorage.sync = 'lastTelephonyActivity#' + Math.random();
closeIfProceeds();
});
});
});
}
});
// When a call ends
navigator.mozSetMessageHandler('telephony-call-ended',
function _onCall(tcall) {
clearTimeout(closing);
if (tcall.direction !== 'outgoing') {
closeIfProceeds();
return;
}
debug('Outgoing call finished!');
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
var mode = ConfigManager.getApplicationMode();
if (mode === 'PREPAID') {
costcontrol.request({ type: 'balance' });
}
settings.lastTelephonyActivity.timestamp = new Date();
settings.lastTelephonyActivity.calltime += tcall.duration;
ConfigManager.setOption({
lastTelephonyActivity: settings.lastTelephonyActivity
}, function _sync() {
localStorage.sync = 'lastTelephonyActivity#' + Math.random();
closeIfProceeds();
});
});
});
}
);
}
// Notify message handler is ready
var readyEvent = new CustomEvent('messagehandlerready');
window.parent.dispatchEvent(readyEvent);
});
}
_getCCInstance();
}());

View file

@ -0,0 +1,288 @@
/* global debug */
/* exported AutoSettings */
'use strict';
var AutoSettings = (function() {
var OPTION_CONFIGURERS = {
'select': function _selectConfigurer(guiWidget) {
var optionName = guiWidget.dataset.option;
var span, parent = guiWidget.parentElement;
if (parent.classList.contains('fake-select')) {
var firstChild = parent.firstChild;
if (!firstChild || firstChild.tagName !== 'SPAN') {
span = document.createElement('span');
parent.insertBefore(span, parent.firstChild);
}
}
guiWidget.addEventListener('blur', function _onSelectChange() {
debug('Value:', guiWidget.value);
settings.option(optionName, guiWidget.value);
});
settings.observe(optionName, function _onOptionChange(value) {
if (value === undefined) {
value = settings.defaultValue(optionName);
}
guiWidget.value = value;
if (span) {
var selected = guiWidget.options[guiWidget.selectedIndex];
var l10nId = selected.getAttribute('data-l10n-id');
span.textContent = selected.textContent;
if (l10nId) {
span.setAttribute('data-l10n-id', l10nId);
}
}
});
},
// Select is used to simulate custom combo boxes. It displays a selection
// dialog with an ok button and possibly a cancel button. The value is only
// changed if ok button is pressed. If cancel provided and clicked then
// the former value is restored.
'customselect': function _customselectConfigurer(guiWidget) {
var dialog = document.getElementById(guiWidget.dataset.selectdialog);
var optionName = guiWidget.dataset.option;
// Dialog
var okButton = dialog.querySelector('button.recommend');
if (okButton) {
okButton.addEventListener('click', function _onDialogOk() {
var checked = dialog.querySelector('input[type="radio"]:checked');
settings.option(optionName, checked.value);
vmanager.closeCurrentView();
});
}
var cancelButton = dialog.querySelector('button.cancel');
if (cancelButton) {
cancelButton.addEventListener('click',
function _onDialogCancel() {
var currentValue = settings.option(optionName);
settings.option(optionName, currentValue);
vmanager.closeCurrentView();
}
);
}
guiWidget.addEventListener('click', function _onWidgetClick() {
vmanager.changeViewTo(dialog.id);
});
// Keep the widget and the dialog synchronized
settings.observe(optionName,
function _onOptionChange(value) {
// Use default value if no value
if (value === null || value === undefined) {
value = getDefaultValue(optionName);
}
var radio =
dialog.querySelector('input[type="radio"][value="' + value + '"]');
if (!radio) {
value = getDefaultValue(optionName);
radio = dialog.querySelector('input[type="radio"][value="' +
value + '"]');
}
radio.checked = true;
var textSpan = dialog.querySelector('input:checked + span');
var tagSpan = guiWidget.querySelector('.tag');
tagSpan.textContent = textSpan.textContent;
}
);
// Keep the UI localized
window.addEventListener('localized', function _onLocalized() {
var textSpan = dialog.querySelector('input:checked + span');
var tagSpan = guiWidget.querySelector('.tag');
tagSpan.textContent = textSpan.textContent;
});
},
// Switch configurer handles an checkbox. When it changes, the checked
// state is used as the content for the option.
'switch' : function _switchConfigurer(guiWidget) {
var optionName = guiWidget.dataset.option;
// Add an observer to keep synchronization
settings.observe(
optionName,
function _onOptionChange(value) {
if (value === null || value === undefined) {
value = getDefaultValue(optionName);
}
guiWidget.checked = value;
}
);
// Add an event listener to switch the option
guiWidget.addEventListener('change', function ccapp_onSwitchChange() {
settings.option(optionName, guiWidget.checked);
});
},
// Input configurer handles an input box. When the content changes, the
// content is parsed and sent to the proper option.
'input' : function _inputConfigurer(guiWidget) {
var optionName = guiWidget.dataset.option;
// Add an observer to keep synchronization
settings.observe(
optionName,
function _onOptionChange(value) {
if (value === null || value === undefined) {
value = getDefaultValue(optionName);
}
guiWidget.value = value;
}
);
// Add an event listener to switch the option
guiWidget.addEventListener('change', function _onContentChange() {
var value = guiWidget.value;
if (guiWidget.type === 'number') {
value = parseFloat(value);
}
settings.option(optionName, value);
});
}
};
// Add a new type with its configuration function
function addType(newType, configFunction) {
OPTION_CONFIGURERS[newType] = configFunction;
}
var customRecognizer;
// Add a recognizer to detect new types
function setCustomRecognizer(recognizer) {
customRecognizer = recognizer;
}
// Return the <li> wrapping the option
function getEntryParent(item) {
while (item && item.tagName !== 'LI') {
item = item.parentNode;
}
return item;
}
// Return the default value for an option
function getDefaultValue(optionName) {
return settings.defaultValue(optionName);
}
// Configure the web page
var settings, vmanager;
function initialize(settingsInterface, viewManager, root) {
root = root || 'body';
settings = settingsInterface;
vmanager = viewManager;
// Return the type of configurer needed based on the properties of
// the HTML element marked as the option.
function getWidgetType(widget) {
if (widget.dataset.widgetType) {
return widget.dataset.widgetType;
}
var customType;
if (customRecognizer === 'function') {
customType = customRecognizer(widget, settings, vmanager);
}
if (customType) {
return customType;
}
if (widget.tagName === 'SELECT') {
return 'select';
}
if (typeof widget.dataset.selectdialog !== 'undefined') {
return 'customselect';
}
if (widget.type === 'checkbox' ||
widget.nodeName.toLowerCase() === 'gaia-switch') {
return 'switch';
}
if (['text', 'number'].indexOf(widget.type) !== -1) {
return 'input';
}
}
// Install a dependency based on an expression. When expression is true
// call the callback.
function installDependency(expression, callback) {
var equality = false;
var parsed = expression.split('!=');
if (parsed.length === 1) {
parsed = expression.split('=');
equality = true;
}
var dependency = parsed[0];
var referenceValue = parsed[1];
settings.observe(dependency,
function ccapp_dependencyAction(value) {
var test = (('' + value) === referenceValue);
callback(equality === test); // or is an equality test and values are
// equal or is an inequality test and
// values are different.
}
);
}
// Widgets
var allGUIWidgets = document.querySelectorAll(root + ' .settings-option');
[].forEach.call(allGUIWidgets, function _forEachWidget(guiWidget) {
var type = getWidgetType(guiWidget);
var entry = getEntryParent(guiWidget);
var configurer = OPTION_CONFIGURERS[type];
if (configurer) {
configurer(guiWidget, settings, vmanager, root);
}
// Simple dependency resolution:
// enable / disable some options depending on the values of other
var disableOn = guiWidget.dataset.disableOn;
if (disableOn) {
installDependency(disableOn, function _disable(passed) {
guiWidget.disabled = passed;
if (entry) {
entry.setAttribute('aria-disabled', passed + '');
}
});
}
// hide / show some options depending on the values of other
var hideOn = guiWidget.dataset.hideOn;
if (hideOn) {
installDependency(hideOn, function _hide(passed) {
guiWidget.hidden = passed;
if (entry) {
entry.hidden = passed;
}
});
}
});
}
return {
initialize: initialize,
setCustomRecognizer: setCustomRecognizer,
addType: addType
};
}());

View file

@ -0,0 +1,234 @@
/* global _, addNetworkUsageAlarm, Common, Formatting, SimManager,
CostControl */
/* exported dataLimitConfigurer */
'use strict';
function dataLimitConfigurer(guiWidget, settings, viewManager, widgetRoot) {
var REMOVE_CHAR_CODE = 8,
// It Represents any positive real number (up to three digits length),
// with optional decimal point, accepting up to 2 decimal places. It is
// not necessary adding the significant number when it is zero (eg. '.5'
// is a valid entry)
DATA_LIMIT_NUMERIC_FORMAT = new RegExp('^[0-9]{0,3}(\\.[0-9]{1,2})?$');
var dialog = document.getElementById('data-limit-dialog');
var dataLimitInput = dialog.querySelector('input');
var format = function ccal_formatterDataUnit(value) {
var unit = settings.option('dataLimitUnit');
return Formatting.formatData([value, _(unit)]);
};
window.addEventListener('localized', function _onLocalize() {
var tagSpan = guiWidget.querySelector('.tag');
tagSpan.textContent = format(dataLimitInput.value);
});
// Configure dialog
var okButton = dialog.querySelector('button.recommend');
if (okButton) {
okButton.addEventListener('click', function ccld_onDialogOk() {
var value = parseFloat(dataLimitInput.value);
settings.option('dataLimitValue', value);
settings.option('dataLimitUnit', currentUnit);
var dataLimit = Common.getDataLimit({'dataLimitValue': value,
'dataLimitUnit': currentUnit});
SimManager.requestDataSimIcc(function(dataSim) {
var simInterface = Common.getDataSIMInterface(dataSim.iccId);
addNetworkUsageAlarm(simInterface, dataLimit, null,
() => checkDataLimit(dataLimit));
});
viewManager.closeCurrentView();
});
}
var dialogHeader = dialog.querySelector('#limit-dialog-header');
if (dialogHeader) {
dialogHeader.addEventListener('action',
function ccld_onDialogCancel() {
var oldValue = settings.option('dataLimitValue');
var oldUnit = settings.option('dataLimitUnit');
currentUnit = oldUnit;
settings.option('dataLimitValue', oldValue);
settings.option('dataLimitUnit', oldUnit);
dataLimitInput.classList.remove('error');
okButton.disabled = false;
viewManager.closeCurrentView();
}
);
}
function checkDataLimit(dataLimit) {
CostControl.getInstance(function _onCostControl(costcontrol) {
var requestObj = {
type: 'datausage'
};
costcontrol.request(requestObj, function _onDataStats(result) {
var stats = result.data.mobile.total;
if (stats > dataLimit) {
var mobileUsage = {
limit: Formatting.formatData(Formatting.roundData(dataLimit))
};
var message = _('data-limit-notification-title2', mobileUsage) +
'\n' + _('data-limit-notification-text2');
window.alert(message);
}
});
});
}
function isNotRemovingChar(newChar) {
return REMOVE_CHAR_CODE !== newChar;
}
function isAllowedValue(newValue) {
return isValidValue(newValue) || wouldBeValid(newValue);
}
// Checks if value is numeric and it has a valid format
// Not allowed entry characters '-', ',' or more than one '.'
function isValidValue(newValue) {
var containsNonValidCharacters = [',','-']
.some(c => newValue.indexOf(c) > -1);
var containsMoreThanOneDot = (newValue.match(/\./g) || []).length > 1;
return !Number.isNaN(newValue) &&
!containsNonValidCharacters && !containsMoreThanOneDot &&
isValidFormat(newValue);
}
function wouldBeValid(lowLimitValue) {
var allowedEntry = new RegExp('^0?(\\.0?)?$');
return allowedEntry.test(lowLimitValue);
}
function preventBadInput(valid, newChar, currentInput) {
// This method returns the string resultant of adding the character of the
// keypress event
function simulateNewValue(newChar, currentInput) {
var value = currentInput.value,
idx = currentInput.selectionStart,
newCharValue = String.fromCharCode(newChar);
return value.substr(0, idx) + newCharValue + value.substr(idx);
}
var newValue = simulateNewValue(newChar, currentInput);
var isInvalid = !valid(newValue);
return isInvalid;
}
dataLimitInput.addEventListener('keypress',
function cc_onKeypress(event) {
var currentInput = event.target;
var newChar = event.charCode || event.keyCode;
// If the entry is not valid or the result of the input is invalid, we
// have to prevent the event propagation
if (isNotRemovingChar(newChar) &&
preventBadInput(isAllowedValue, newChar, currentInput)) {
event.preventDefault();
event.stopPropagation();
return;
}
}
);
function isValidFormat(lowLimitValue) {
var numberDataLimit = Number(lowLimitValue);
return (numberDataLimit < 1000 &&
numberDataLimit > 0 &&
DATA_LIMIT_NUMERIC_FORMAT.test(numberDataLimit));
}
// Disable OK button when dataLimitInput not matches any positive real number
// (up to three digits length), with optional decimal point, accepting up
// to 2 decimal places.
dataLimitInput.addEventListener('input',
function cc_ondataLimitInputChange(evt) {
var lowLimitValue = evt.target.value.trim();
var isNumericLowLimit = !Number.isNaN(Number(lowLimitValue));
if (isNumericLowLimit &&
lowLimitValue.indexOf('.0') !== lowLimitValue.length - 2 &&
lowLimitValue.indexOf('.') !== lowLimitValue.length - 1) {
dataLimitInput.value = Number(lowLimitValue);
}
var isValidValue = (isNumericLowLimit && isValidFormat(lowLimitValue));
if (isValidValue) {
dataLimitInput.classList.remove('error');
} else {
dataLimitInput.classList.add('error');
}
okButton.disabled = (!isValidValue);
}
);
// Configure the swicth unit button
var currentUnit = settings.option('dataLimitUnit');
var switchUnitButton = dialog.querySelector('.switch-unit-button');
function localizeSwitchUnitButton(unit) {
switchUnitButton.setAttribute('data-l10n-id', 'unit-' + unit);
switchUnitButton.querySelector('span.tag').setAttribute(
'data-l10n-id', unit);
}
localizeSwitchUnitButton(currentUnit);
switchUnitButton.addEventListener('click',
function ccapp_switchUnit() {
currentUnit = (currentUnit === 'MB') ? 'GB' : 'MB';
localizeSwitchUnitButton(currentUnit);
}
);
// Prevent to loose the focus when tapping on switch unit button
// TODO: Replace with 'focusout' on dataLimitInput when fixing bug:
// https://bugzilla.mozilla.org/show_bug.cgi?id=687787
switchUnitButton.addEventListener('mousedown',
function _preventFocusLost(evt) {
evt.preventDefault();
}
);
// Keep the widget and the dialog synchronized
settings.observe('dataLimitValue',
function ccld_onValueChange(value) {
// Use default value if no value
if (value === null || typeof value === 'undefined') {
value = settings.defaultValue('dataLimitValue');
}
// Set dialog
dataLimitInput.value = value;
var tagSpan = guiWidget.querySelector('.tag');
tagSpan.textContent = format(dataLimitInput.value);
}
);
settings.observe('dataLimitUnit',
function ccld_onUnitChange(value) {
// Use default value if no value
if (value === null || typeof value === 'undefined') {
value = settings.defaultValue('dataLimitUnit');
}
// Set dialog
localizeSwitchUnitButton(value);
var tagSpan = guiWidget.querySelector('.tag');
tagSpan.textContent = format(dataLimitInput.value);
}
);
// Show the dialog
var oldUnitValue;
guiWidget.addEventListener('click', function ccld_onWidgetClick() {
viewManager.changeViewTo(dialog.id, widgetRoot);
dataLimitInput.focus();
dataLimitInput.setSelectionRange(dataLimitInput.value.length,
dataLimitInput.value.length);
oldUnitValue = settings.option('dataLimitUnit');
localizeSwitchUnitButton(oldUnitValue);
});
}

View file

@ -0,0 +1,89 @@
/* exported NetworkUsageAlarm */
'use strict';
var NetworkUsageAlarm = (function() {
function _addAlarm(networkInterface, value, onsuccess, onerror) {
if (!networkInterface) {
console.error('Error, the network interface is not defined when trying ' +
'to add an alarm');
(typeof onerror === 'function') && onerror();
return;
}
if (value === null || typeof value === 'undefined') {
console.error('Limit value is undefined, impossible add an alarm');
(typeof onerror === 'function') && onerror();
return;
}
var request = navigator.mozNetworkStats.addAlarm(networkInterface, value);
request.onsuccess = onsuccess;
request.onerror = function error(e) {
console.error('Error, when trying to addAlarm to the interfaceId: ' +
networkInterface.id + ' and limit: ' + value);
(typeof onerror === 'function') && onerror();
};
}
function _clearAlarms(networkInterface, callback) {
if (!networkInterface) {
console.error('Error, the network interface is not defined when trying ' +
'to remove alarms');
return;
}
var request = navigator.mozNetworkStats.getAllAlarms(networkInterface);
request.onsuccess = function success() {
var alarmList = request.result;
var pendingRequest = alarmList.length;
function checkForCompletion() {
pendingRequest--;
if (pendingRequest === 0) {
(typeof callback === 'function') && callback();
}
}
function _onErrorRemoveAlarm() {
console.error('Error when trying to remove one alarm.');
checkForCompletion();
}
for (var i = 0; i < alarmList.length; i++) {
if (alarmList[i] && alarmList[i].alarmId) {
var rA = navigator.mozNetworkStats.removeAlarms(alarmList[i].alarmId);
rA.onsuccess = checkForCompletion;
rA.onerror = _onErrorRemoveAlarm;
}
}
if (pendingRequest === 0) {
(typeof callback === 'function') && callback();
}
};
request.onerror = function error() {
console.error('Error when trying to get alarms from the API to clear it');
};
}
function _updateAlarm(dataInterface, value, onsuccess, onerror) {
if (dataInterface && value) {
var addNewAlarm = _addAlarm.bind(null, dataInterface, value, onsuccess,
onerror);
_clearAlarms(dataInterface, addNewAlarm);
} else {
if (!dataInterface) {
console.error('Error, the network interface is not defined when ' +
'trying to update an alarm');
} else {
console.error('Error, the data limit value is not defined when ' +
'trying to update an alarm');
}
(typeof onerror === 'function') && onerror();
}
}
return {
updateAlarm: _updateAlarm,
clearAlarms: _clearAlarms
};
}());

View file

@ -0,0 +1,367 @@
/* global BalanceView, LazyLoader, AutoSettings, BalanceLowLimitView,
ResetMenuDialog, ConfirmDialog, ViewManager, dataLimitConfigurer,
Formatting
*/
/* exported debug, sendBalanceThresholdNotification */
/*
* Settings is in charge of setup the setting section. It uses an AutoSettings
* object to automatically bind markup with local settings.
*
* Settings have three drawing areas with views for current values of balance,
* data usage and telephony.
*/
'use strict';
// Import global objects from parent window
var ConfigManager = window.parent.ConfigManager;
var CostControl = window.parent.CostControl;
var SimManager = window.parent.SimManager;
var Common = window.parent.Common;
var NetworkUsageAlarm = window.parent.NetworkUsageAlarm;
// Import global functions from parent window
var addNetworkUsageAlarm = window.parent.addNetworkUsageAlarm;
var resetTelephony = window.parent.resetTelephony;
navigator.mozL10n = window.parent.navigator.mozL10n;
// Import debug
var DEBUGGING = window.parent.DEBUGGING;
var debug = window.parent.debug;
var Settings = (function() {
var costcontrol, vmanager, initialized, endLoadSettingsNotified;
var plantypeSelector, phoneActivityTitle, phoneActivitySettings;
var balanceTitle, balanceSettings, reportsTitle;
var balanceView, resetMenuDialog, confirmDialog;
function configureUI() {
CostControl.getInstance(function _onCostControl(instance) {
costcontrol = instance;
// Debug options
if (DEBUGGING) {
loadDeveloperAids();
}
vmanager = new ViewManager();
// HTML entities
plantypeSelector = document.getElementById('plantype-settings');
phoneActivityTitle = document.getElementById('phone-activity-settings');
phoneActivitySettings =
document.querySelector('#phone-activity-settings + .settings');
balanceTitle = document.getElementById('balance-settings');
balanceSettings =
document.querySelector('#balance-settings + .settings');
reportsTitle = document.getElementById('phone-internet-settings');
// Subviews
var balanceConfig = ConfigManager.configuration.balance;
balanceView = new BalanceView(
document.getElementById('balance'),
document.querySelector('#balance + .meta'),
balanceConfig ? balanceConfig.minimum_delay : undefined
);
var resetDialog = document.getElementById('reset-dialog');
var resetDataDialog = document.getElementById('reset-data-dialog');
resetMenuDialog = new ResetMenuDialog(resetDataDialog, vmanager);
confirmDialog = new ConfirmDialog(resetDialog, vmanager);
resetMenuDialog.initializeResetModes(confirmDialog);
// Autosettings
AutoSettings.addType('data-limit', dataLimitConfigurer);
AutoSettings.initialize(ConfigManager, vmanager, '#settings-view');
configureTelephonyReset();
configureDataResets();
addDoneConstrains();
// Add an observer on dataLimit switch to active o deactivate alarms
ConfigManager.observe(
'dataLimit',
function _onDataLimitChange(value, old, key, settings) {
SimManager.requestDataSimIcc(function(dataSim) {
var iccId = dataSim.iccId;
var currentDataInterface = Common.getDataSIMInterface(iccId);
if (!value) {
NetworkUsageAlarm.clearAlarms(currentDataInterface);
} else {
addNetworkUsageAlarm(currentDataInterface,
Common.getDataLimit(settings));
}
});
},
true
);
// Update layout when changing plantype
ConfigManager.observe('plantype', updateUI, true);
// Update views
ConfigManager.observe('lastTelephonyActivity',
function _updateTelephony(activity, old, key, settings) {
updateTelephony(activity, settings.lastTelephonyReset);
},
true
);
ConfigManager.observe('lastDataUsage',
function _updateDataUsage(stats, old, key, settings) {
updateDataUsage(stats, settings.lastCompleteDataReset);
},
true
);
ConfigManager.observe('lastBalance',
function _updateBalance(balance) {
updateBalance(balance, balance.currency);
},
true
);
// Close button needs to acquire a reference to the settings view
// manager to close itself.
var close = document.getElementById('close-settings');
close.addEventListener('click', function() {
closeSettings();
});
function _setResetTimeToDefault(value, old, key, settings) {
var today = new Date();
var defaultResetTime = (settings.trackingPeriod === 'weekly') ?
today.getDay() : today.getDate();
if (settings.resetTime !== defaultResetTime) {
ConfigManager.setOption({ resetTime: defaultResetTime });
} else {
Common.updateNextReset(settings.trackingPeriod, settings.resetTime);
}
}
function _updateNextReset(value, old, key, settings) {
Common.updateNextReset(settings.trackingPeriod, settings.resetTime);
}
ConfigManager.observe('resetTime', _updateNextReset, true);
ConfigManager.observe('trackingPeriod', _setResetTimeToDefault, true);
initialized = true;
Settings.updateUI();
});
}
function closeSettings() {
window.parent.location.hash = '#';
}
// Loads extra HTML useful for debugging
function loadDeveloperAids() {
var xhr = new XMLHttpRequest();
xhr.overrideMimeType('text/plain');
xhr.open('GET', '/debug.html', false);
xhr.send();
if (xhr.status === 200) {
var src = document.createElement('DIV');
// XXX: We use innerHTML precisely because we need parse the content
// without introducing the overhead of DOM methods.
src.innerHTML = xhr.responseText;
var reference = document.getElementById('plantype-settings');
var parent = reference.parentNode;
[].forEach.call(src.childNodes, function(node) {
reference = parent.insertBefore(node, reference.nextSibling);
});
}
}
// Configure reset dialogs for telephony and data usage
function configureTelephonyReset() {
var resetTelephonyButton = document.getElementById('reset-telephony');
resetTelephonyButton.addEventListener('click', function() {
confirmDialog.setMessage('reset-telephony-confirmation-warning', {});
function resetAction() {
resetTelephony();
updateUI();
confirmDialog.closeConfirmDialog();
}
confirmDialog.updateAcceptAction(resetAction);
confirmDialog.showConfirmDialog();
});
}
// Configure reset dialogs for telephony and data usage
function configureDataResets() {
// Button reset Data Usage Settings send to reset data dialog
var resetDataUsage = document.getElementById('reset-data-usage');
resetDataUsage.addEventListener('click', function _onDataReset() {
var currentTotals = {
mobile: costcontrol.lastDataResults.mobile.total,
wifi: costcontrol.lastDataResults.wifi.total
};
resetMenuDialog.showResetMenuDialog(currentTotals);
});
}
// Add particular constrains to the "Done" button
var balanceLowLimitView;
function addDoneConstrains() {
var closeButton = document.getElementById('close-settings');
balanceLowLimitView = new BalanceLowLimitView(
document.getElementById('low-limit'),
document.getElementById('low-limit-input')
);
balanceLowLimitView.onvalidation = function(evt) {
closeButton.disabled = !evt.isValid;
};
}
window.addEventListener('localized', function _onLocalize() {
if (initialized) {
updateUI();
}
});
var currentMode;
function updateUI() {
ConfigManager.requestAll(function _onInfo(configuration, settings) {
// L10n
Common.localizeWeekdaySelector(
document.getElementById('select-weekday'));
// Layout
var mode = ConfigManager.getApplicationMode();
if (currentMode !== mode) {
currentMode = mode;
var hidePlantypeSelector = (mode === 'DATA_USAGE_ONLY');
var hidePhoneActivity = (mode !== 'POSTPAID');
var hideBalance = (mode !== 'PREPAID');
var hideReportsTitle = (mode === 'PREPAID');
var textReportsTitle = (mode === 'POSTPAID') ?
'phone-and-internet-data-report' : 'internet-data-report';
reportsTitle.querySelector('span').setAttribute(
'data-l10n-id', textReportsTitle
);
balanceLowLimitView.disabled = (mode !== 'PREPAID');
plantypeSelector.hidden = hidePlantypeSelector;
phoneActivityTitle.hidden = hidePhoneActivity;
phoneActivitySettings.hidden = hidePhoneActivity;
balanceTitle.hidden = hideBalance;
balanceSettings.hidden = hideBalance;
reportsTitle.hidden = hideReportsTitle;
}
// Views
var requestObj = {
type: 'datausage'
};
costcontrol.request(requestObj, function _onDataStats(result) {
var stats = result.data;
updateDataUsage(stats, settings.lastCompleteDataReset);
});
switch (mode) {
case 'PREPAID':
updateBalance(settings.lastBalance, configuration.credit.currency);
break;
case 'POSTPAID':
updateTelephony(settings.lastTelephonyActivity,
settings.lastTelephonyReset);
break;
}
if (endLoadSettingsNotified) {
window.performance.measure('loadSettings', 'loadSettingsStart');
endLoadSettingsNotified = true;
}
});
}
// Update data usage view on settings
function updateDataUsage(datausage, lastCompleteDataReset) {
var mobileUsage = document.querySelector('#mobile-data-usage > span');
var data = Formatting.roundData(datausage.mobile.total);
mobileUsage.textContent = Formatting.formatData(data);
var wifiUsage = document.querySelector('#wifi-data-usage > span');
data = Formatting.roundData(datausage.wifi.total);
wifiUsage.textContent = Formatting.formatData(data);
updateDataUsageTimestamp(lastCompleteDataReset, datausage.timestamp);
}
var dataUsagePeriod = { begin: null, end: null };
function updateDataUsageTimestamp(begin, end) {
dataUsagePeriod.begin = begin;
dataUsagePeriod.end = end;
var timestamp = document.querySelector('#wifi-data-usage + .meta');
timestamp.innerHTML = '';
timestamp.appendChild(Formatting.formatTimeHTML(begin, end));
}
// Update balance view on settings
function updateBalance(lastBalance, currency) {
var limitCurrency = document.getElementById('settings-low-limit-currency');
limitCurrency.textContent = currency;
balanceView.update(lastBalance);
}
// Update telephony counters on settings
function updateTelephony(activity, lastTelephonyReset) {
var calltimeSpan = document.getElementById('calltime');
var smscountSpan = document.getElementById('smscount');
Common.localize(calltimeSpan, 'magnitude', {
value: Formatting.computeTelephonyMinutes(activity),
unit: 'min.'
});
Common.localize(smscountSpan, 'magnitude', {
value: activity.smscount,
unit: 'SMS'
});
updateTelephonyTimestamp(lastTelephonyReset, activity.timestamp);
}
var telephonyPeriod = { begin: null, end: null };
function updateTelephonyTimestamp(begin, end) {
telephonyPeriod.begin = begin;
telephonyPeriod.end = end;
var timestamp = document.getElementById('telephony-timestamp');
timestamp.innerHTML = '';
timestamp.appendChild(Formatting.formatTimeHTML(begin, end));
}
window.addEventListener('timeformatchange', function () {
updateTelephonyTimestamp(telephonyPeriod.begin, telephonyPeriod.end);
updateDataUsageTimestamp(dataUsagePeriod.begin, dataUsagePeriod.end);
});
return {
initialize: function() {
var SCRIPTS_NEEDED = [
'js/utils/toolkit.js',
'shared/js/date_time_helper.js',
'js/utils/formatting.js',
'js/views/BalanceLowLimitView.js',
'js/settings/limitdialog.js',
'js/settings/autosettings.js',
'js/view_manager.js',
'js/views/BalanceView.js',
'js/views/ResetMenuDialog.js',
'js/views/ConfirmDialog.js'
];
LazyLoader.load(SCRIPTS_NEEDED, function() {
if (!Common.allNetworkInterfaceLoaded) {
Common.loadNetworkInterfaces(configureUI);
} else {
configureUI();
}
});
},
updateUI: updateUI
};
}());
Settings.initialize();

View file

@ -0,0 +1,197 @@
/* global SettingsListener,AirplaneModeHelper, LazyLoader */
/* exported SimManager */
'use strict';
var SimManager = (function() {
/*jshint validthis: true */
var serviceIcc = {};
var serviceSettingMap = {
'data' : 'ril.data.defaultServiceId',
'telephony' : 'ril.telephony.defaultServiceId',
'message' : 'ril.sms.defaultServiceId'
};
function notifyInitializationDone(updatedService) {
var simInitializedEvent = new CustomEvent('simManagerReady', {
detail: updatedService
});
window.dispatchEvent(simInitializedEvent);
}
function _init(serviceId) {
_reset(serviceId);
LazyLoader.load(['/shared/js/airplane_mode_helper.js'], function() {
AirplaneModeHelper.addEventListener('statechange',
function _onAirplaneModeChange(state) {
if (state === 'disabled') {
serviceIcc[serviceId].dirty = true;
var eventDetail = { detail: {serviceId: serviceId}};
var airplanemodeDisabledEvent =
new CustomEvent('airplaneModeDisabled', eventDetail);
window.dispatchEvent(airplanemodeDisabledEvent);
}
});
});
function checkForCompletion() {
serviceIcc[serviceId].initialized = true;
// Putting a listener to mark the dirty value when a change is detected.
SettingsListener.observe(serviceSettingMap[serviceId], 0,
function(value) {
var isDirty = (serviceIcc[serviceId].slotId !== value);
serviceIcc[serviceId].dirty = isDirty;
serviceIcc[serviceId].slotId = value;
if (isDirty) {
var serviceSlotChangeEvent =
new CustomEvent(serviceId + 'SlotChange', { detail: serviceId });
window.dispatchEvent(serviceSlotChangeEvent);
}
}
);
notifyInitializationDone(serviceId);
}
if (SimManager.isMultiSim()) {
var settings = window.navigator.mozSettings;
var req = settings &&
settings.createLock().get(serviceSettingMap[serviceId]);
req.onsuccess = function _onsuccesSlotId() {
if (!req.result[serviceSettingMap[serviceId]]) {
console.warn('The setting ' + serviceSettingMap[serviceId] +
' does not exists, using default Slot (0)');
}
var slotId = req.result[serviceSettingMap[serviceId]] || 0;
serviceIcc[serviceId].slotId = slotId;
checkForCompletion();
};
req.onerror = function _onErrorSlotId() {
console.warn('The setting ' + serviceSettingMap[serviceId] +
' does not exists');
checkForCompletion();
};
} else {
notifyInitializationDone(serviceId);
}
}
function _requestDataSIMIcc(onsuccess, onerror) {
_requestService('data', onsuccess, onerror);
}
function _requestTelephonySIMIcc(onsuccess, onerror) {
_requestService('telephony', onsuccess, onerror);
}
function _requestMessageSIMIcc(onsuccess, onerror) {
_requestService('message', onsuccess, onerror);
}
function _requestService(serviceId, onsuccess, onerror) {
if (serviceIcc[serviceId] && serviceIcc[serviceId].initialized &&
!serviceIcc[serviceId].dirty) {
(typeof onsuccess === 'function') && onsuccess(serviceIcc[serviceId]);
} else if (!serviceIcc[serviceId] || !serviceIcc[serviceId].initialized) {
console.warn('SimManager is not ready, waiting for initialized custom ' +
'event');
window.addEventListener('simManagerReady', function _onSMInit(evt) {
if (evt.detail === 'all' || evt.detail === serviceId) {
window.removeEventListener('simManagerReady', _onSMInit);
_requestServiceSIMIcc(serviceId, onsuccess, onerror);
}
});
_init(serviceId);
} else {
_requestServiceSIMIcc(serviceId, onsuccess, onerror);
}
}
function _requestServiceSIMIcc(serviceId, onsuccess, onerror) {
var iccManager = window.navigator.mozIccManager;
var slotId = serviceIcc[serviceId].slotId;
var mobileConnection = navigator.mozMobileConnections[slotId];
var iccId = mobileConnection.iccId || null;
if (!iccId) {
console.error('The slot ' + slotId + ', configured as the ' + serviceId +
' slot, is empty');
(typeof onerror === 'function') && onerror();
return;
}
serviceIcc[serviceId].iccId = iccId;
serviceIcc[serviceId].icc = iccManager.getIccById(iccId);
// Icc is not detected although iccId exists
if (!serviceIcc[serviceId].icc) {
(typeof onerror === 'function') && onerror();
return;
}
(typeof onsuccess === 'function') && onsuccess(serviceIcc[serviceId]);
}
function _reset(serviceId) {
function _resetService(serviceId) {
// If it is not a DSDS device, the slotId is correct
var _initialized = !SimManager.isMultiSim();
serviceIcc[serviceId] = {
dirty: true,
icc: null,
iccId: null,
// The default value for slotId is 0 on the no DSDS device
slotId: 0,
initialized: _initialized
};
}
if (!serviceId) {
Object.keys(serviceSettingMap).forEach(function _initService(serviceId) {
_resetService(serviceId);
});
} else {
_resetService(serviceId);
}
}
function _requestServiceConnection(serviceId) {
var slotId = serviceIcc[serviceId].slotId;
return navigator.mozMobileConnections[slotId];
}
function _requestDataConnection(callback) {
if (serviceIcc.data && serviceIcc.data.initialized) {
var connection = _requestServiceConnection('data');
(typeof callback === 'function') && callback(connection);
} else {
console.warn('SimManager is not ready, waiting for initialized custom ' +
'event');
window.addEventListener('simManagerReady', function _onSMInit(evt) {
if (evt.detail === 'all' || evt.detail === 'data') {
window.removeEventListener('simManagerReady', _onSMInit);
var connection = _requestServiceConnection('data');
(typeof callback === 'function') && callback(connection);
}
});
_init('data');
}
}
return {
isMultiSim: function() {
// On a multisim device, the mozMobileConnections array always return
// the number of slots although these slots are empty.
return (navigator.mozMobileConnections.length > 1);
},
// data info
requestDataSimIcc: _requestDataSIMIcc,
// telephony info
requestTelephonySimIcc: _requestTelephonySIMIcc,
requestDataConnection: _requestDataConnection,
// message info
requestMessageSimIcc: _requestMessageSIMIcc,
reset: _reset
};
})();

View file

@ -0,0 +1,585 @@
/* global _, debug, Toolkit, Formatting, Common */
/* exported ChartUtils */
'use strict';
var ChartUtils = (function() {
var DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;
var DAY = 24 * 60 * 60 * 1000;
var NEVER_PERIOD = 30 * DAY;
var NEVER_ANCHOR = 21 * DAY;
var CHART_BG_RATIO = 0.87;
var FONT_SIZE = toDevicePixels(13);
var FONT_WEIGHT = '600';
var FONT_WEIGHT_AXIS = '400';
var WIFI_CHART_STROKE = '#8b9052';
var WIFI_CHART_FILL = '#cbd936';
var MOBILE_CHART_STROKE = '#762d4a';
var MOBILE_CHART_FILL = 'rgba(147, 21, 98, 0.7)';
function toDevicePixels(value) {
return value * DEVICE_PIXEL_RATIO;
}
function makeCSSFontString(fontSize, fontWeight) {
return fontWeight + ' ' + fontSize + 'px sans-serif';
}
function formatChartDate(date) {
return _('verbose-chart-date-format', {
'monthday-number': date.getDate(),
'em-month': _('month-' + date.getMonth() + '-short')
});
}
function getLimitInBytes(settings) {
var multiplier = 1000000; // MB
if (settings.dataLimitUnit === 'GB') {
multiplier = 1000000000; // GB
}
var value = settings.dataLimitValue;
return (value && value !== 0) ? value * multiplier : null;
}
function calculateUpperDate(settings) {
var today = new Date();
var trackingPeriod = settings.trackingPeriod;
var nextReset = settings.nextReset;
if (trackingPeriod !== 'never' && nextReset) {
return new Date(nextReset.getTime() - DAY);
}
var lastReset = settings.lastCompleteDataReset || today;
var offset = today.getTime() - lastReset.getTime();
var upperDate = new Date(lastReset.getTime() + NEVER_PERIOD);
if (offset >= NEVER_ANCHOR) {
upperDate = new Date(today.getTime() + (NEVER_PERIOD - NEVER_ANCHOR));
}
debug('Upper date:', upperDate);
return upperDate;
}
function calculateLowerDate(settings) {
var today = new Date();
var lowerDate = Toolkit.toMidnight(new Date());
var nextReset = settings.nextReset || lowerDate;
var trackingPeriod = settings.trackingPeriod;
if (trackingPeriod === 'weekly') {
lowerDate.setTime(nextReset.getTime() - (7 * DAY));
} else if (trackingPeriod === 'monthly') {
var monthDate = settings.resetTime;
var isFirstDayOfPeriod = today.getDate() == monthDate;
var isAfterFirstDayOfPeriod = today.getDate() > monthDate;
if (isAfterFirstDayOfPeriod) {
// lowerDate is in the current month
lowerDate.setDate(monthDate);
} else if (!isFirstDayOfPeriod) {
// lowerDate is on the previous month
var LAST_DAY_OF_PREVIOUS_MONTH = 0;
var newMonth = today.getMonth() - 1;
var newYear = today.getFullYear();
if (newMonth < 0) {
newMonth = 11;
newYear--;
}
lowerDate = Toolkit.toMidnight(new Date(newYear, newMonth, monthDate));
// The day of the month of lowerDate is different to settings.resetTime
// value when the resetTime day doesn't exists this month, (eg. 30th
// February), on this case, the lowerDate must be the last day of the
// previous month
if (lowerDate.getDate() != monthDate) {
lowerDate = Toolkit.toMidnight(new Date());
lowerDate.setDate(LAST_DAY_OF_PREVIOUS_MONTH);
}
}
} else {
var lastReset = settings.lastCompleteDataReset || lowerDate;
lowerDate = lastReset;
var offset = today.getTime() - lastReset.getTime();
if (offset >= NEVER_ANCHOR) {
lowerDate = new Date(today.getTime() - NEVER_ANCHOR);
}
}
debug('Lower date:', lowerDate);
return lowerDate;
}
// Expand the model with some computed values
function expandModel(base) {
// Update today
var today = Toolkit.toMidnight(new Date());
// Graphic settings
base.originY = Math.floor(base.height * CHART_BG_RATIO);
// Today value
base.axis.X.today = today;
// Normalize today
Toolkit.toMidnight(base.axis.X.today);
Toolkit.toMidnight(base.axis.X.lower);
Toolkit.toMidnight(base.axis.X.upper);
// X axis projection function to convert a value into a pixel value
var xLowerBound = base.axis.X.lower.getTime();
var xSize = base.axis.X.upper.getTime() - xLowerBound;
var realWidth = base.endX - base.originX;
base.axis.X.len = realWidth;
base.axis.X.get = function cc_dataToXPx(value) {
var projection = (value.getTime() - xLowerBound) / xSize;
return projection * realWidth + base.originX;
};
// Y max value
var limitEnabled = true; // XXX: model.limits.enabled;
base.axis.Y.maxValue = Math.max(limitEnabled ? base.limits.value : 0,
base.data.mobile.total,
base.data.wifi.total);
// Y axis projection function and automatic values
base.axis.Y.range = base.axis.Y.upper - base.axis.Y.lower;
base.axis.Y.upper = (1 + base.axis.Y.margin) * base.axis.Y.maxValue;
var yLowerBound = base.axis.Y.lower;
var ySize = base.axis.Y.upper - yLowerBound;
base.axis.Y.get = function cc_dataToYPx(value) {
var projection = (value - yLowerBound) / ySize;
return base.originY * (1 - projection);
};
base.axis.Y.step = base.axis.Y.get(base.axis.Y.maxValue);
// Limits
base.limits.warning = Common.DATA_USAGE_WARNING;
base.limits.warningValue = base.limits.value * Common.DATA_USAGE_WARNING;
}
// Check if the segment of the graph is inside chart area. If so, draw it
function clipAndDrawSegment(ctx, model, x0, y0, x1, y1) {
if (x0 >= model.originX && x1 <= model.endX) {
var x0Fixed = Math.floor(x0) - 0.5;
var x1Fixed = Math.floor(x1) + 0.5;
// Fill
ctx.globalCompositeOperation = 'destination-over';
ctx.beginPath();
ctx.lineTo(x1Fixed, y1);
ctx.lineTo(x1Fixed, model.originY);
ctx.lineTo(x0Fixed, model.originY);
ctx.lineTo(x0Fixed, y0);
ctx.moveTo(x1Fixed, y0);
ctx.fill();
// Stroke
ctx.globalCompositeOperation = 'source-over';
ctx.beginPath();
// It's necessary add 1 to the X coordinate to make up for the offset
// produced with the drawing of the previous line.
ctx.moveTo(x0Fixed + 1, y0);
ctx.lineTo(x1Fixed, y1);
ctx.stroke();
}
}
function drawTodayMark(ctx, x, y, color) {
ctx.save();
var radius = toDevicePixels(4);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
function drawBackgroundLayer(canvas, model, showMobile) {
canvas.height = model.height;
var width = canvas.width = model.width;
var ctx = canvas.getContext('2d');
ctx.save();
// White bg
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, model.originY);
// Horizontal lines every step
var step = model.axis.Y.step;
var limitY = model.axis.Y.get(model.limits.value);
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = toDevicePixels(1);
var displayLimit = model.limits.enabled && showMobile;
for (var y = model.originY - step; y > step; y -= step) {
if (displayLimit && same(y, limitY, 0.1)) {
continue;
}
var drawY = Math.floor(y) - 0.5;
ctx.beginPath();
ctx.moveTo(model.originX, drawY);
ctx.lineTo(model.endX, drawY);
ctx.stroke();
}
// Vertical lines every day
var days = (model.axis.X.upper - model.axis.X.lower) / DAY;
step = model.axis.X.len / days;
ctx.strokeStyle = '#eeeeee';
ctx.lineWidth = toDevicePixels(1);
for (var x = model.originX; x <= model.endX; x += step) {
var drawX = Math.floor(x) + 0.5;
ctx.beginPath();
ctx.moveTo(drawX, model.originY);
ctx.lineTo(drawX, 0);
ctx.stroke();
// Ensure draw the last vertical line
if (((x + step) > model.endX) && (x !== model.endX)) {
// the 0.5 offset is needed to avoid drawing a double line.
drawX = model.endX - 0.5;
ctx.beginPath();
ctx.moveTo(drawX, model.originY);
ctx.lineTo(drawX, 0);
ctx.stroke();
}
}
ctx.restore();
// Return true if a and b not differ more than threshold
function same(a, b, threshold) {
threshold = threshold || 0;
return Math.abs(a - b) <= threshold;
}
}
function drawTodayLayer(canvas, model) {
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
// Compute the X offset
var offsetX = model.axis.X.get(model.axis.X.today);
// Configure Centered today text
var marginTop = toDevicePixels(10);
var todayTag = formatChartDate(model.axis.X.today);
// Render the text
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
// Update measues of today label
var metrics = ctx.measureText(todayTag);
model.todayLabel.x0 = offsetX - metrics.width / 2;
model.todayLabel.x1 = offsetX + metrics.width / 2;
if (model.todayLabel.x1 > model.endX) {
ctx.textAlign = 'right';
}
ctx.fillStyle = 'black';
ctx.fillText(todayTag, offsetX, model.originY + marginTop);
setAccessibilityAttributes(canvas, 'today-layer', { today: todayTag });
}
function drawAxisLayer(canvas, model, showMobile) {
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
// Start drawing Y axis
var step = model.axis.Y.step;
var dataStep = model.axis.Y.upper - model.axis.Y.maxValue;
var marginRight = 4;
var offsetX = model.originX - marginRight;
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT_AXIS);
ctx.textAlign = 'right';
var displayLimit = showMobile && model.limits.enabled;
var lastUnit;
for (var y = 0.5 + model.originY, value = 0;
y > step; y -= step, value += dataStep) {
// First X label for 0 is aligned with the bottom
if (value === 0) {
lastUnit = Formatting.smartRound(dataStep, 0)[1];
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#6a6a6a';
ctx.fillText(Formatting.formatData([0, lastUnit]), offsetX, y - 2.5);
continue;
}
// Rest of labels are aligned with the middle
var rounded = Formatting.smartRound(value, -1);
var v = rounded[0];
var u = rounded[1];
var label = v;
if (lastUnit !== u) {
label = Formatting.formatData([v, u]);
}
lastUnit = u;
ctx.textBaseline = 'middle';
ctx.fillStyle = displayLimit &&
(value === model.limits.value) &&
model.limits.enabled ?
'#b50202' : '#6a6a6a';
ctx.fillText(label, offsetX, y);
}
// Now the X axis
ctx.fillStyle = '#6a6a6a';
var marginTop = toDevicePixels(10);
// Left tag
var leftTag = formatChartDate(model.axis.X.lower);
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
var isBelowToday = model.todayLabel.x0 <=
model.originX + ctx.measureText(leftTag).width;
if (!isBelowToday) {
ctx.fillText(leftTag, model.originX, model.originY + marginTop);
}
// Right tag
var rightTag = formatChartDate(model.axis.X.upper);
ctx.textAlign = 'right';
isBelowToday = model.todayLabel.x1 >=
model.endX - ctx.measureText(rightTag).width;
if (!isBelowToday) {
ctx.fillText(rightTag, model.endX, model.originY + marginTop);
}
setAccessibilityAttributes(canvas, 'axis-layer',
{ from: leftTag, to: rightTag });
}
function setAccessibilityAttributes(element, identifier, data) {
if (identifier) {
element.setAttribute('data-l10n-id', identifier);
if (data) {
element.setAttribute('data-l10n-args', JSON.stringify(data));
}
} else {
element.removeAttribute('data-l10n-id');
element.removeAttribute('data-l10n-args');
element.removeAttribute('aria-label');
}
}
function drawLimits(canvas, model, showMobile) {
var set = model.limits.value;
var color = '#b50202';
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
var displayLimit = showMobile && model.limits.enabled;
if (!displayLimit) {
return;
}
ctx.save();
var marginTop = 1;
var offsetY = set ? model.axis.Y.get(model.limits.value) :
FONT_SIZE + 2 * marginTop;
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
// The dashed limit line
var lineLength = toDevicePixels(15);
var gapLength = toDevicePixels(7);
ctx.strokeStyle = color;
ctx.lineWidth = toDevicePixels(1);
ctx.beginPath();
for (var x = model.originX, drawY = Math.floor(offsetY) - 0.5;
x < model.endX; x += gapLength) {
ctx.moveTo(x, drawY);
ctx.lineTo(Math.min(x += lineLength, model.endX), drawY);
}
ctx.stroke();
ctx.restore();
setAccessibilityAttributes(canvas,
'limit-layer-' + model.limits.dataLimitUnit,
{ value: model.limits.dataLimitValue });
}
function drawDataLayer(canvas, model, sampleType, style) {
var samples = (model.data[sampleType] || {}).samples;
if (!samples) {
return;
}
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
// Style
ctx.fillStyle = style.fill;
ctx.strokeStyle = style.stroke;
ctx.lineWidth = toDevicePixels(2);
ctx.lineJoin = 'round';
ctx.moveTo(model.originX, model.originY);
var today = Toolkit.toMidnight(new Date());
// offset in milliseconds
var offset = today.getTimezoneOffset() * 60 * 1000;
var sum = 0; var x, y = model.originY;
var lastX = model.originX, lastY = model.axis.Y.get(sum);
// Only dealing with the use cases of negative UTC offset because, due to
// the database granularity, the returned samples are given in UTC days, and
// we don't know what part of the traffic sample values correspond to the
// current localtime day. The query for the samples is generated on the
// costcontrol module, and the case into which the returned samples only
// have one record with the data for the localtime day of yesterday is not
// possible.
for (var i = 0, len = samples.length; i < len; i++) {
var sample = samples[i];
var sampleLocalTime = sample.date.getTime() + offset;
var sampleUTCDate = Toolkit.toMidnight(new Date(sampleLocalTime));
var isToday = (today.getTime() === sampleUTCDate.getTime());
var isTomorrow = (today.getTime() + DAY === sampleUTCDate.getTime());
var thereIsATomorrowSample = (isToday && (i + 2 === len));
// Depends on the hour of the day and the offset, it is possible the
// networkStats API returns the current data mobile in the tomorrow
// sample, because on the UTC hour is another day.
if (thereIsATomorrowSample) {
// Join the value of the samples for today and tomorrow
var tomorrowSample = samples[i+1];
if (typeof sample.value === 'undefined') {
sample.value = tomorrowSample.value;
} else if (typeof tomorrowSample.value !== 'undefined') {
sample.value += tomorrowSample.value;
}
if (i === 0) {
lastX = model.axis.X.get(sample.date);
}
i++;
}
if (typeof sample.value === 'undefined') {
lastX = x = model.axis.X.get(sample.date);
ctx.moveTo(x, y);
} else {
if (i === 0) {
lastX = model.axis.X.get(sample.date);
}
sum += sample.value;
x = model.axis.X.get(sample.date);
y = model.axis.Y.get(sum);
clipAndDrawSegment(ctx, model, lastX, lastY, x, y);
lastX = x;
lastY = y;
}
var onlyExistTomorrowSample = (i===0 && isTomorrow);
var isXInsideTheGraph = (x >= model.originX);
if ((isToday || onlyExistTomorrowSample) && isXInsideTheGraph) {
drawTodayMark(ctx, x, y, style.stroke);
return;
}
}
if (!style.pattern) {
return;
}
var pattern = ctx.createPattern(style.pattern, 'repeat');
ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, model.width, model.originY);
}
function drawWarningLayer(canvas, model) {
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
if (!model.limits.enabled || model.limits.value === null) {
setAccessibilityAttributes(canvas);
return;
}
// No problem here
var mobileUsage = model.data.mobile.total;
var mobileUsageDifference = mobileUsage - model.limits.value;
if (mobileUsage <= model.limits.warningValue) {
setAccessibilityAttributes(canvas);
return;
}
// Warning mode
if (mobileUsage <= model.limits.value) {
var limitValue = Math.floor(model.axis.Y.get(model.limits.value));
var warningValue =
Math.round(model.axis.Y.get(model.limits.warningValue));
ctx.beginPath();
ctx.fillStyle = 'rgba(255, 112, 0, 0.5)';
ctx.fillRect(
model.originX, limitValue,
model.axis.X.len + 0.5, warningValue - limitValue
);
setAccessibilityAttributes(canvas, 'warning-layer', {
value: Formatting.formatData(Formatting.roundData(
-mobileUsageDifference))
});
return;
}
// Limit exceeded
var limitValueExceeded = model.axis.Y.get(model.limits.value);
ctx.beginPath();
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fillRect(
model.originX, 0,
model.axis.X.len + 0.5, limitValueExceeded
);
setAccessibilityAttributes(canvas, 'limit-exceeded-layer', {
value: Formatting.formatData(Formatting.roundData(mobileUsageDifference))
});
}
return {
// Constants
WIFI_CHART_STROKE: WIFI_CHART_STROKE,
WIFI_CHART_FILL: WIFI_CHART_FILL,
MOBILE_CHART_STROKE: MOBILE_CHART_STROKE,
MOBILE_CHART_FILL: MOBILE_CHART_FILL,
// Functions
toDevicePixels: toDevicePixels,
makeCSSFontString: makeCSSFontString,
formatChartDate: formatChartDate,
getLimitInBytes: getLimitInBytes,
calculateUpperDate: calculateUpperDate,
calculateLowerDate: calculateLowerDate,
expandModel: expandModel,
clipAndDrawSegment: clipAndDrawSegment,
drawTodayMark: drawTodayMark,
drawBackgroundLayer: drawBackgroundLayer,
drawTodayLayer: drawTodayLayer,
drawAxisLayer: drawAxisLayer,
drawLimits: drawLimits,
drawDataLayer: drawDataLayer,
drawWarningLayer: drawWarningLayer
};
}());

View file

@ -0,0 +1,39 @@
/* exported debug */
'use strict';
var DEBUGGING = false;
var debug = (function() {
var SEQ_ID = 0;
var PROCESS_ID = Date.now();
var DEBUG_PREFIX = 'CC';
return function _debug() {
if (!DEBUGGING) {
return;
}
var currentWindow = window;
var parents = [];
while (currentWindow !== currentWindow.parent) {
parents.push(currentWindow.location.pathname);
currentWindow = currentWindow.parent;
}
parents.push(currentWindow.location.pathname);
parents = parents.reverse().join('>') + ':';
var uId = PROCESS_ID;
var message = ['(' + uId + '-' + (SEQ_ID++) + ')', DEBUG_PREFIX, parents];
for (var i = 0, len = arguments.length, obj; i < len; i++) {
obj = arguments[i];
message.push(typeof obj === 'object' ? JSON.stringify(obj) : obj);
}
if (window.dump) {
message.push('\n');
window.dump(message.join(' '));
} else if (console && console.log) {
console.log(message.join(' '));
}
};
}());

View file

@ -0,0 +1,241 @@
/* global _ */
/* exported Formatting */
'use strict';
var Formatting = (function() {
var MINUTE = 60 * 1000;
var HOUR = 60 * MINUTE;
var DAY = 24 * HOUR;
function getDaysOfDifference(a, b) {
return Math.floor((a.getTime() - b.getTime()) / DAY);
}
function getFormattedDate(timestamp, formatter) {
return formatter.format(timestamp);
}
function formatTime(timestamp, formatter) {
if (!timestamp) {
return _('never');
}
var now = new Date(), then = new Date(timestamp);
if (formatter) {
return getFormattedDate(then, formatter);
}
var date, time;
var daysOfDifference = getDaysOfDifference(now, then);
if (daysOfDifference === 0) {
date = _('today');
} else if (daysOfDifference === 1) {
date = _('yesterday');
} else {
date = getFormattedDate(timestamp, Formatting.formatters.shortWeekday);
}
time = getFormattedDate(timestamp, Formatting.formatters.shortTime);
return _('day-hour-format', {
day: date,
time: time
});
}
function formatTimeSinceNow(timestamp) {
var now = new Date(), then = new Date(timestamp);
var formattedTime = formatTime(timestamp);
if (getDaysOfDifference(now, then) > 0) {
return formattedTime;
}
var age = now - then;
if (age < MINUTE) {
formattedTime = _('minutes-ago-short', { value: 0 });
} else if (age < HOUR) {
var minutes = Math.floor(age / MINUTE);
formattedTime = _('minutes-ago-short', { value: minutes });
} else if (age < DAY) {
var hours = Math.floor(age / HOUR);
formattedTime = _('hours-ago-short', { value: hours });
}
return formattedTime;
}
function formatData(dataArray) {
return isNaN(dataArray[0]) ? '' :
_('magnitude', { value: dataArray[0], unit: dataArray[1] });
}
function roundData(value, positions) {
positions = (typeof positions === 'undefined') ? 2 : positions;
if (value < 1000) {
return [value.toFixed(positions), _('B')];
}
if (value < 1000000) {
return [(value / 1000).toFixed(positions), _('KB')];
}
if (value < 1000000000) {
return [(value / 1000000).toFixed(positions), _('MB')];
}
return [(value / 1000000000).toFixed(positions), _('GB')];
}
function getPositions(value, deltaPositions) {
deltaPositions = (typeof deltaPositions === 'undefined') ? 0 :
deltaPositions;
if (parseInt(value) === value) {
return 0;
}
if (value < 10) {
return 2 + deltaPositions;
}
if (value < 100) {
return 1 + deltaPositions;
}
return 0;
}
function smartRound(value, deltaPositions) {
deltaPositions = (typeof deltaPositions === 'undefined') ? 0 :
deltaPositions;
if (value < 1000) {
var bPositions = getPositions(value, deltaPositions);
return [value.toFixed(bPositions), _('B')];
}
if (value < 1000000) {
var kbytes = value / 1000;
var kbPositions = getPositions(kbytes, deltaPositions);
return [kbytes.toFixed(kbPositions), _('KB')];
}
if (value < 1000000000) {
var mbytes = value / 1000000;
var mPositions = getPositions(mbytes, deltaPositions);
return [mbytes.toFixed(mPositions), _('MB')];
}
var gbytes = value / 1000000000;
var gPositions = getPositions(gbytes, deltaPositions);
return [gbytes.toFixed(gPositions), _('GB')];
}
function formatTimeHTML(timestampA, timestampB) {
function timeElement(content) {
var time = document.createElement('time');
time.textContent = content;
return time;
}
var fragment = document.createDocumentFragment();
// No interval
if (typeof timestampB === 'undefined') {
fragment.appendChild(timeElement(Formatting.formatTime(timestampA)));
return fragment;
}
// Same day case
var dateA = new Date(timestampA);
var dateB = new Date(timestampB);
if (dateA.getFullYear() === dateB.getFullYear() &&
dateA.getMonth() === dateB.getMonth() &&
dateA.getDay() === dateB.getDay()) {
return formatTimeHTML(timestampB);
}
// Interval
fragment.appendChild(
timeElement(
Formatting.formatTime(timestampA, Formatting.formatters.shortDate)
)
);
fragment.appendChild(document.createTextNode(' – '));
fragment.appendChild(timeElement(Formatting.formatTime(timestampB)));
return fragment;
}
function computeTelephonyMinutes(activity) {
// Right now the activity for telephony is computed in milliseconds
return Math.ceil(activity.calltime / 60000);
}
return {
getFormattedDate: getFormattedDate,
/*
* Used with a Date and a format string, it is the same than using
* `l10n.localeFormat()`.
*
* If format is not provided, default format is assumed to be:
* "Today|Yesterday|<WeekDay>, hh:mm"
*
*/
formatTime: formatTime,
/*
* Returns a human friendly string describing time transcurred since the
* present moment to the timestamp passed as parameter in minutes or hours.
*
* If timestamps is yesterday or a day before yesterday, it is shown as
* in `Formatting.formatTime()`
*/
formatTimeSinceNow: formatTimeSinceNow,
// Format data using magnitude localization
// It exepcts a pair with the value and the unit
formatData: formatData,
// Return a fixed point data value in KB/MB/GB
roundData: roundData,
getPositions: getPositions,
smartRound: smartRound,
formatTimeHTML: formatTimeHTML,
// Given the API information compute the human friendly minutes
computeTelephonyMinutes: computeTelephonyMinutes,
formatters: {}
};
}());
function setFormatters() {
const locales = navigator.languages;
Formatting.formatters.longDate = Intl.DateTimeFormat(locales, {
month: 'long',
day: 'numeric',
year: 'numeric'
});
Formatting.formatters.shortDate = Intl.DateTimeFormat(locales, {
month: 'short',
day: 'numeric',
});
Formatting.formatters.shortTime = Intl.DateTimeFormat(locales, {
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric'
});
Formatting.formatters.shortWeekday = Intl.DateTimeFormat(locales, {
weekday: 'short'
});
}
setFormatters();
window.addEventListener('languagechange', setFormatters, false);
window.addEventListener('timeformatchange', setFormatters, false);

View file

@ -0,0 +1,52 @@
/* global DEBUGGING */
/* exported _, deepCopy, Toolkit */
'use strict';
var _ = function cc_fallbackTranslation(keystring) {
var r = window.navigator.mozL10n.get.apply(this, arguments);
return r || (DEBUGGING ? '!!' : '') + keystring;
};
// Deep copy algorithm with Data support
function deepCopy(object) {
if (object === null || !(object instanceof Object)) {
return object;
}
var clone, constructor = object.constructor;
switch (constructor) {
case Date:
clone = new constructor(object.getTime());
break;
default:
clone = constructor();
break;
}
for (var key in object) {
if (object.hasOwnProperty(key)) {
clone[key] = deepCopy(object[key]);
}
}
return clone;
}
var Toolkit = {
checkEnoughDelay: function(threshold, dateA, dateB) {
if (!dateA) {
return true;
}
var end = dateB || new Date();
return (end.getTime() - dateA.getTime()) > threshold;
},
toMidnight: function _toMidnight(date) {
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
}
};

View file

@ -0,0 +1,255 @@
/* global AccessibilityHelper, LazyLoader */
/* exported ViewManager */
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
var ViewManager = (function() {
// The ViewManager is in charge of simply manage the different views of the
// applications. ViewManager.changeViewTo() valid values are listed above
// these lines.
function ViewManager(tabs) {
tabs = tabs || [];
this._tabs = {};
tabs.forEach(function _registerTab(tabItem) {
if (typeof tabItem !== 'object') {
tabItem = { id: tabItem };
}
this._tabs[tabItem.id] = tabItem.tab || 'left';
}, this);
this._currentView = null;
this._currentTab = null;
}
// Return true if the passed view is a tab
ViewManager.prototype._isTab = function _isTab(view) {
return this._tabs.hasOwnProperty(view);
};
// Make target enter screen's main area and call callback after, passing as
// arguments if the new view is a tab, the new view id and a third parameter
// depending on if the view was a tab or not:
// If it is a tab: it returns the current overlay view id or null
// If it is not a tab: it returns the previous ovrlay view or null
ViewManager.prototype.changeViewTo = function _changeViewTo(viewHash,
obstructed,
callback) {
var hashParts = viewHash.split('?');
var viewId = hashParts[0];
if (this.isCurrentView(viewId)) {
return;
}
// Note here how we set the same value with different semantincs.
// This is used at the end of the function and the names are the correct
// because, depending on if the view is a tab or not, semantics may change.
var previousViewId, currentViewId;
previousViewId = currentViewId = this._currentView ?
this._currentView.id : null;
var view = document.getElementById(viewId);
var params = parseQueryParams(hashParts[1]);
var self = this;
// lazy load HTML of the panel
this.loadPanel(view, function() {
// Tabs are treated in a different way than overlay views
var isTab = self._isTab(viewId);
if (isTab) {
// Disposing the current view
var disposingTab = null;
if (self._currentTab) {
disposingTab = document.getElementById(self._currentTab);
}
if (disposingTab) {
disposingTab.dataset.viewport = self._tabs[disposingTab.id];
document.getElementById(disposingTab.id + '-filter').classList.remove(
'.selected');
}
// Showing the new one
view.dataset.viewport = '';
document.getElementById(view.id + '-filter').classList.add('.selected');
AccessibilityHelper.setAriaSelected(
document.getElementById(view.id + '-control'),
document.querySelectorAll('[role="tab"]'));
self._currentTab = viewId;
// Overlay view
} else {
self.closeCurrentView();
previousViewId = self._currentView ? self._currentView.id : '';
self._currentView = {
id: viewId,
defaultViewport: view.dataset.viewport,
obstructed: obstructed
};
// With a combination of CSS, we actually animate and display the view
delete view.dataset.viewport;
if (obstructed) {
document.querySelector(obstructed).classList.add('behind');
}
}
if (typeof callback === 'function') {
callback(isTab, viewId, isTab ? currentViewId : previousViewId);
}
notifyViewChange(isTab, viewId, params);
});
};
function notifyViewChange(isTab, current, params) {
var type = isTab ? 'tabchanged' : 'viewchanged';
var event = new CustomEvent(type, {
detail: {
id: current,
params: params
}
});
window.dispatchEvent(event);
}
ViewManager.prototype.loadPanel = function _loadPanel(panel, callback) {
if (!panel || panel.hidden === false) {
if (typeof callback === 'function') {
callback();
}
return;
}
LazyLoader.load(panel, function() {
var resourceRemaining = 0;
function loadResource(node) {
node.onload = onResourceLoaded;
document.head.appendChild(node);
resourceRemaining++;
}
// activate all styles
Array.from(panel.querySelectorAll('link')).forEach((node) => {
if (!document.querySelector('head > link[href="' + node.href + '"]')) {
var styleLink = document.createElement('link');
styleLink.href = node.href;
styleLink.media = node.media;
styleLink.rel = node.rel;
styleLink.type = node.type;
loadResource(styleLink);
}
node.remove();
});
// activate all scripts
Array.from(panel.querySelectorAll('script')).forEach((node) => {
if (!document.querySelector('head > script[src="' + node.src + '"]')) {
var scriptLink = document.createElement('script');
scriptLink.src = node.src;
scriptLink.type = node.type;
loadResource(scriptLink);
}
node.remove();
});
// add listeners
var headers = Array.from(
panel.querySelectorAll('gaia-header[action="close"]')
);
headers.forEach((headerWithClose) => {
headerWithClose.addEventListener('action', function() {
window.parent.location.hash = '#';
});
});
panel.hidden = false;
checkCallback();
function checkCallback() {
if (resourceRemaining === 0 && typeof callback === 'function') {
callback();
}
}
function onResourceLoaded() {
resourceRemaining--;
checkCallback();
}
});
};
// Close the current view returning to the previous one
ViewManager.prototype.closeCurrentView = function _closeCurrentView() {
if (!this._currentView) {
return;
}
var view = document.getElementById(this._currentView.id);
// With a combination of CSS, Restoring the last viewport we actually
// animate and hide the current view
view.dataset.viewport = this._currentView.defaultViewport;
if (this._currentView.obstructed) {
var obstructed = document.querySelector(this._currentView.obstructed);
obstructed.classList.remove('behind');
}
this._currentView = null;
};
// Test if the current view is the one passed as parameter
ViewManager.prototype.isCurrentView = function _isCurrentView(view) {
return this._currentView && this._currentView.id === view;
};
// Return the current view id or null if not current view
ViewManager.prototype.getCurrentView = function _getCurrentView() {
return this._currentView ? this._currentView.id : null;
};
// Return true if the tab id passed is the current tab
ViewManager.prototype.isCurrentTab = function _isCurrentTab(tab) {
return this._currentTab && this._currentTab === tab;
};
// Return the current tab
ViewManager.prototype.getCurrentTab = function _getCurrentTab() {
return this._currentTab;
};
function parseQueryParams(queryParams) {
var params = {};
queryParams = (queryParams || '').split('&');
queryParams.forEach(function(param) {
if (!param) { return; }
var pair = param.split('=');
var key = pair[0];
var value = pair[1];
if (params[key]) {
if (!Array.isArray(params[key])) {
params[key] = [params[key]];
}
params[key].push(value);
return;
}
params[key] = value;
});
return params;
}
return ViewManager;
}());

View file

@ -0,0 +1,44 @@
/* exported BalanceLowLimitView */
'use strict';
function BalanceLowLimitView(lowLimitEnabler, lowLimitInput) {
var self = this;
self._lowLimitEnabler = lowLimitEnabler;
self._lowLimitInput = lowLimitInput;
self._lowLimitEnabler.addEventListener('change', self);
self._lowLimitInput.addEventListener('input', self);
installProperties();
self.validate();
function installProperties() {
var disabled = false;
Object.defineProperty(self, 'disabled', {
get: function() {
return disabled;
},
set: function(value) {
disabled = value;
self._lowLimitEnabler.disabled = disabled;
self._lowLimitInput.disabled = disabled;
self.validate();
}
});
}
}
BalanceLowLimitView.prototype.handleEvent = function() {
this.validate();
};
BalanceLowLimitView.prototype.validate = function(e) {
var numericValue = parseFloat(this._lowLimitInput.value, 10);
this._isValid = this.disabled || !this._lowLimitEnabler.checked ||
!Number.isNaN(numericValue) && numericValue > 0;
this._lowLimitInput.classList[this._isValid ? 'remove' : 'add']('error');
if (typeof this.onvalidation === 'function') {
this.onvalidation({type: 'validation', isValid: this._isValid});
}
};

View file

@ -0,0 +1,83 @@
/* global _, Formatting */
/* exported BalanceView */
'use strict';
function BalanceView(balanceLabel, timestampLabel, minimumDelay) {
minimumDelay = minimumDelay || 0;
var viewTimestamp;
var timeHighlightColors = [
{threshold: minimumDelay / 3, className: 'first-third'},
{threshold: 2 * minimumDelay / 3, className: 'second-third'},
{threshold: minimumDelay, className: 'third-third'},
{threshold: 24 * 60 * 60 * 1000, className: 'yesterday'},
{threshold: Infinity, className: 'before'}
];
function update(balanceResult, isUpdating) {
if (!balanceResult) {
setBalanceToNotAvailable();
} else {
updateBalance(balanceResult.balance, balanceResult.currency);
updateTimestamp(balanceResult.timestamp, isUpdating);
}
}
function setBalanceToNotAvailable() {
balanceLabel.setAttribute('data-l10n-id', 'not-available');
timestampLabel.innerHTML = '';
}
function updateBalance(balance, currencySymbol) {
balanceLabel.textContent = _('currency', {
value: balance,
currency: currencySymbol
});
}
function updateTimeFormat() {
updateTimestamp(viewTimestamp);
}
function updateTimestamp(timestamp, isUpdating) {
timestampLabel.innerHTML = '';
viewTimestamp = timestamp;
if (isUpdating) {
timestampLabel.setAttribute('data-l10n-id', 'updating-ellipsis');
} else if (typeof timestamp !== 'undefined') {
var time = getTimeTagFor(timestamp);
timestampLabel.appendChild(time);
time.classList.add(getHighlightColor(timestamp));
}
}
function getTimeTagFor(timestamp) {
var time = document.createElement('time');
time.setAttribute('datetime', timestamp.toISOString());
time.textContent = Formatting.formatTimeSinceNow(timestamp);
return time;
}
function getHighlightColor(timestamp) {
if (!minimumDelay) {
return 'first-third';
}
var now = new Date(), then = new Date(timestamp);
var age = now - then;
for (var i = 0, l = timeHighlightColors.length; i < l; i++) {
var category = timeHighlightColors[i];
if (age < category.threshold) {
return category.className;
}
}
return timeHighlightColors[l - 1].className;
}
window.addEventListener('timeformatchange', updateTimeFormat);
this.update = update;
}

View file

@ -0,0 +1,31 @@
'use strict';
/* global Common */
function ConfirmDialog(container, viewManager) {
this.id = container.id;
this.container = container;
this.viewManager = viewManager;
this.message = container.querySelector('p');
this.okButton = container.querySelector('.danger');
this.cancelButton = container.querySelector('.close-reset-dialog');
this.cancelButton.onclick = this.closeConfirmDialog.bind(this);
}
ConfirmDialog.prototype.setMessage = function(l10nId, data) {
Common.localize(this.message, l10nId, data || {});
};
ConfirmDialog.prototype.updateAcceptAction = function(acceptAction) {
this.okButton.onclick = acceptAction;
};
ConfirmDialog.prototype.showConfirmDialog = function() {
this.viewManager.changeViewTo(this.id, '#settings-view');
};
ConfirmDialog.prototype.closeConfirmDialog = function() {
this.viewManager.closeCurrentView();
};

View file

@ -0,0 +1,80 @@
'use strict';
/* global Common */
/* global _ */
function NonReadyScreen(container) {
this.id = container.id;
this.container = container;
this.activity = container.querySelector('progress');
this.header = container.querySelector('h1');
this.message = container.querySelector('p');
this.button = container.querySelector('button');
this.button.onclick = Common.closeApplication;
}
// Configure the non ready screen depending on the card status. For
// absent, pinRequired and pukRequired, shows a message and give the
// user the opportunity to close the app. For other states, it shows
// a progress spinner.
NonReadyScreen.prototype.updateForState = function(cardState) {
switch (cardState) {
case null:
case 'absent':
case 'unknown':
case 'pinRequired':
case 'pukRequired':
case 'airplaneMode':
this.setMessageMode(cardState);
break;
default:
this.setWaitingMode();
break;
}
};
NonReadyScreen.prototype.setMessageMode = function(cardState) {
this.activity.hidden = true;
[this.header, this.message, this.button.parentNode].forEach(function (el) {
el.hidden = false;
});
var messageId = this.getMessageIdFor(cardState);
if (messageId) {
var header = _('widget-' + messageId + '-heading');
var msg = _('widget-' + messageId + '-meta');
this.header.textContent = header;
this.message.textContent = msg;
}
};
NonReadyScreen.prototype.setWaitingMode = function() {
this.activity.hidden = false;
[this.header, this.message, this.button.parentNode].forEach(function (el) {
el.hidden = true;
});
};
NonReadyScreen.prototype.getMessageIdFor = function(cardState) {
var message;
// SIM is absent
if (!cardState || cardState === 'absent') {
message = 'no-sim3';
// SIM is locked
} else if (
cardState === 'pinRequired' ||
cardState === 'pukRequired'
) {
message = 'sim2-locked';
} else if (cardState === 'airplaneMode') {
message = 'airplane-mode';
} else if (cardState === 'unknown') {
message = 'unknown';
}
return message;
};

View file

@ -0,0 +1,57 @@
'use strict';
/* global Common, Settings, Formatting */
function ResetMenuDialog(container, viewManager) {
this.id = container.id;
this.container = container;
this.viewManager = viewManager;
this.dataUsage = {mobile:0, wifi:0};
this.resetModes = ['wifi', 'mobile', 'all'];
container.addEventListener('gaiamenu-cancel',
this.closeResetMenuDialog.bind(this));
}
ResetMenuDialog.prototype.initializeResetModes = function(confirmResetDialog) {
this.resetModes.forEach(function(resetMode) {
var newButton = document.createElement('button');
newButton.id = 'reset-' + resetMode + '-data-usage';
newButton.type = 'button';
newButton.classList.add('reset-data-button');
newButton.dataset.l10nId = 'reset-' + resetMode;
newButton.onclick = function() {
this.updateConfirmDialog(resetMode, confirmResetDialog);
}.bind(this);
this.container.appendChild(newButton);
}.bind(this));
};
ResetMenuDialog.prototype.updateConfirmDialog = function(type, confirmDialog) {
var mobileUsage =
Formatting.formatData(Formatting.roundData(this.dataUsage.mobile));
var wifiUsage =
Formatting.formatData(Formatting.roundData(this.dataUsage.wifi));
var data = {
mobileData: mobileUsage,
wifiData: wifiUsage
};
var l10nId = 'reset-' + type + '-confirmation-warning';
confirmDialog.setMessage(l10nId, data);
function resetAction() {
// Reset data type, take in count spent offsets to fix the charts
Common.resetData(type);
Settings.updateUI();
confirmDialog.closeConfirmDialog();
}
confirmDialog.updateAcceptAction(resetAction);
confirmDialog.showConfirmDialog();
};
ResetMenuDialog.prototype.showResetMenuDialog = function(dataUsage) {
this.dataUsage = dataUsage;
this.viewManager.changeViewTo(this.id, '#settings-view');
};
ResetMenuDialog.prototype.closeResetMenuDialog = function() {
this.viewManager.closeCurrentView();
};

View file

@ -0,0 +1,16 @@
/* exported TopUpLayoutView */
'use strict';
function TopUpLayoutView(ussdTopUpButton, codeTopUpButton) {
function setupLayout(topUpConfig) {
var ussdTopUpDisabled = !topUpConfig || !topUpConfig.ussd_destination;
var codeTopUpDisabled = !topUpConfig || !topUpConfig.destination;
ussdTopUpButton.disabled = ussdTopUpDisabled;
ussdTopUpButton.hidden = ussdTopUpDisabled;
codeTopUpButton.disabled = codeTopUpDisabled;
codeTopUpButton.hidden = codeTopUpDisabled;
}
this.setupLayout = setupLayout;
}

View file

@ -0,0 +1,261 @@
/* global _, debug, CostControl, Common, Formatting, ChartUtils,
SimManager, ConfigManager */
/* jshint -W120 */
/**
* The App Detail view is in charge of usage charts and settings for an
* individual app.
*/
'use strict';
var AppDetailView = (function() {
var costcontrol,
initialized,
els,
app,
localizedAppName,
model;
function initialize() {
if (initialized) {
return;
}
els = {};
CostControl.getInstance(function(instance) {
costcontrol = instance;
// Get elements
els.view = document.getElementById('appdetail-view');
els.header = els.view.querySelector('gaia-header');
els.headerHeading = els.header.querySelector('h1');
els.usage = els.view.querySelector('.usage');
els.graphicArea = els.usage.querySelector('#app-graphic-area');
els.backgroundLayer =
els.graphicArea.querySelector('#app-background-layer');
els.wifiLayer = els.graphicArea.querySelector('#app-wifi-layer');
els.mobileLayer = els.graphicArea.querySelector('#app-mobile-layer');
els.axisLayer = els.graphicArea.querySelector('#app-axis-layer');
els.todayLayer = els.graphicArea.querySelector('#app-today-layer');
els.warningLayer = els.graphicArea.querySelector('#app-warning-layer');
els.limitsLayer = els.graphicArea.querySelector('#app-limits-layer');
els.graphicPattern =
els.graphicArea.querySelector('#app-graphic-pattern');
els.dataUsedSince =
els.usage.querySelector('p[data-l10n-id="data-used-since"]');
els.dataUsedThisWeek =
els.usage.querySelector('p[data-l10n-id="data-used-this-week"]');
els.dataUsedThisMonth =
els.usage.querySelector('p[data-l10n-id="data-used-this-month"]');
// Setup the model
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
debug('First time setup for model');
model = {
height: ChartUtils.toDevicePixels(els.graphicArea.clientHeight),
width: ChartUtils.toDevicePixels(els.graphicArea.clientWidth),
originX: Math.floor(
ChartUtils.toDevicePixels(els.graphicArea.clientWidth) * 0.15),
endX: Math.floor(
ChartUtils.toDevicePixels(els.graphicArea.clientWidth) * 0.95),
axis: {
Y: {
lower: 0,
margin: 0.20
},
X: {
lower: ChartUtils.calculateLowerDate(settings),
upper: ChartUtils.calculateUpperDate(settings)
}
},
limits: {
enabled: settings.dataLimit,
value: ChartUtils.getLimitInBytes(settings),
dataLimitValue: settings.dataLimitValue,
dataLimitUnit: settings.dataLimitUnit
},
data: {
wifi: {
enabled: true
},
mobile: {
enabled: true
}
},
todayLabel: {}
};
});
});
// Attach event listeners
window.addEventListener('viewchanged', function(evt) {
if (evt.detail.id === 'appdetail-view') {
setAppManifestURL(evt.detail.params.manifestURL);
}
});
els.header.addEventListener('action', function(evt) {
var type = evt.detail && evt.detail.type;
if (type === 'back') {
window.location.hash = '#';
}
});
initialized = true;
});
}
function setAppManifestURL(manifestURL) {
// Get App object
app = Common.getApp(manifestURL);
// Update app usage data
updateAppUsageData(app);
// Localize app name
localizedAppName = Common.getLocalizedAppName(app);
// Update the localized app name throughout the view
els.headerHeading.textContent = localizedAppName;
}
function updateAppUsageData(app) {
// Hide usage element until it has been updated/localized.
els.usage.classList.remove('in');
var usageRequest = {
type: 'datausage',
apps: [app.manifestURL]
};
// Request usage for this specific app
costcontrol.request(usageRequest, function(result) {
if (result.status !== 'success') {
console.error('Error requesting data usage. This should not happen.');
return;
}
var data = result.data;
var mobileDataTotal = data.mobile.total;
var formattedMobileDataTotal = Formatting.formatData(
Formatting.roundData(mobileDataTotal));
// Localize the data usage info
els.dataUsedSince.textContent =
_('data-used-since', {
'amount': formattedMobileDataTotal,
'start-date':
Formatting.getFormattedDate(data.start,
Formatting.formatters.longDate)
});
els.dataUsedThisWeek.textContent =
_('data-used-this-week', {
'amount': formattedMobileDataTotal
});
els.dataUsedThisMonth.textContent =
_('data-used-this-month', {
'amount': formattedMobileDataTotal
});
els.dataUsedSince.hidden = true;
els.dataUsedThisWeek.hidden = true;
els.dataUsedThisMonth.hidden = true;
// Update the charts
prepareChartData(result, function() {
// Render the charts
renderChart();
// Fade in usage element
els.usage.classList.add('in');
});
});
}
function prepareChartData(result, callback) {
if (result.status === 'success') {
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
debug('Updating model');
var modelData = result.data;
model.data.wifi.samples = modelData.wifi.samples;
model.data.wifi.total = modelData.wifi.total;
model.data.wifi.apps = modelData.wifi.apps;
model.data.mobile.samples = modelData.mobile.samples;
model.data.mobile.total = modelData.mobile.total;
model.data.mobile.apps = modelData.mobile.apps;
model.limits.enabled = settings.dataLimit;
model.limits.value = ChartUtils.getLimitInBytes(settings);
model.limits.dataLimitValue = settings.dataLimitValue;
model.axis.X.upper = ChartUtils.calculateUpperDate(settings);
model.axis.X.lower = ChartUtils.calculateLowerDate(settings);
ChartUtils.expandModel(model);
// Show correct usage text label
switch (settings.trackingPeriod) {
case 'weekly':
els.dataUsedSince.hidden = true;
els.dataUsedThisWeek.hidden = false;
els.dataUsedThisMonth.hidden = true;
break;
case 'monthly':
els.dataUsedSince.hidden = true;
els.dataUsedThisWeek.hidden = true;
els.dataUsedThisMonth.hidden = false;
break;
default:
els.dataUsedSince.hidden = false;
els.dataUsedThisWeek.hidden = true;
els.dataUsedThisMonth.hidden = true;
break;
}
debug('Rendering');
callback();
});
});
} else {
console.error('Error requesting data usage. This should not happen.');
}
}
function renderChart() {
ChartUtils.drawBackgroundLayer(els.backgroundLayer, model, true);
ChartUtils.drawTodayLayer(els.todayLayer, model);
ChartUtils.drawAxisLayer(els.axisLayer, model, true);
ChartUtils.drawDataLayer(els.wifiLayer, model, 'wifi', {
stroke: ChartUtils.WIFI_CHART_STROKE,
fill: ChartUtils.WIFI_CHART_FILL
});
ChartUtils.drawDataLayer(els.mobileLayer, model, 'mobile', {
stroke: ChartUtils.MOBILE_CHART_STROKE,
fill: ChartUtils.MOBILE_CHART_FILL,
pattern: els.graphicPattern
});
ChartUtils.drawWarningLayer(els.warningLayer, model);
ChartUtils.drawLimits(els.limitsLayer, model, true);
}
function finalize() {
if (!initialized) {
return;
}
initialized = false;
}
return {
initialize: initialize,
finalize: finalize
};
}());
AppDetailView.initialize();

View file

@ -0,0 +1,468 @@
/* global BalanceView, TopUpLayoutView, CostControl, ConfigManager,
ViewManager, debug, MozActivity, getTopUpTimeout, LazyLoader,
sendBalanceThresholdNotification
*/
/*
* The balance tab is in charge of show balance details and allows the use
* to manually update or top up via USSD or code.
*
* It has four areas of drawing: balance, top up dialog, top up countdown and
* error area. The update is done by demand when the application is shown or
* when the user taps on the update button. Furthermore, the balance update
* can be triggered by a successful top up request.
*/
'use strict';
var BalanceTab = (function() {
var view, updateButton;
var topUpUSSD, topUp, topUpDialog, topUpCodeInput, sendCode, countdownSpan;
var balanceView, topUpLayoutView;
var costcontrol, initialized;
function showTopUpWithCode(evt) {
topUpWithCode();
}
function setupTab() {
if (initialized) {
return;
}
CostControl.getInstance(function _onCostControl(instance) {
costcontrol = instance;
// we need to load topUpDialog to get elements from inside
// it should be a separate function run on startup of the module
// after HTML is generated
topUpDialog = document.getElementById('topup-dialog');
var vm = new ViewManager(); // XXX: used to eagerly load top up dialog
vm.loadPanel(topUpDialog);
// HTML entities
view = document.getElementById('balance-tab');
updateButton = document.getElementById('balance-tab-update-button');
sendCode = document.getElementById('topup-send-button');
topUpUSSD = document.getElementById('balance-tab-topup-ussd-button');
topUp = document.getElementById('balance-tab-topup-button');
topUpCodeInput = document.getElementById('topup-code-input');
countdownSpan = document.getElementById('top-up-countdown');
// Subviews
topUpLayoutView = new TopUpLayoutView(topUpUSSD, topUp);
var balanceConfig = ConfigManager.configuration.balance;
balanceView = new BalanceView(
document.getElementById('balance-tab-credit'),
document.getElementById('balance-tab-time'),
balanceConfig ? balanceConfig.minimum_delay : undefined
);
window.addEventListener('localized', localize);
// Configure updates
document.addEventListener('visibilitychange', updateWhenVisible);
updateButton.addEventListener('click', lockAndUpdateUI);
ConfigManager.observe('lowLimit', toogleLimits, true);
ConfigManager.observe('lowLimitThreshold', resetNotification, true);
ConfigManager.observe('lastBalance', onBalance, true);
ConfigManager.observe('errors', onBalanceTimeout, true);
// Configure top up
topUpUSSD.addEventListener('click', topUpWithUSSD);
topUp.addEventListener('click', showTopUpWithCode);
topUpCodeInput.addEventListener('input', toogleSend);
sendCode.addEventListener('click', requestTopUp);
ConfigManager.observe('waitingForTopUp', onConfirmation, true);
ConfigManager.observe('errors', onTopUpErrors, true);
toogleSend();
updateUI();
initialized = true;
});
}
function localize() {
if (initialized) {
updateUI();
}
}
function finalize() {
if (!initialized) {
return;
}
document.removeEventListener('visibilitychange', updateWhenVisible);
updateButton.removeEventListener('click', lockAndUpdateUI);
ConfigManager.removeObserver('lowLimit', toogleLimits);
ConfigManager.removeObserver('lowLimitThreshold', resetNotification);
ConfigManager.removeObserver('lastBalance', onBalance);
ConfigManager.removeObserver('errors', onBalanceTimeout);
topUpUSSD.removeEventListener('click', topUpWithUSSD);
topUp.removeEventListener('click', showTopUpWithCode);
topUpCodeInput.removeEventListener('input', toogleSend);
sendCode.removeEventListener('click', requestTopUp);
ConfigManager.removeObserver('waitingForTopUp', onConfirmation);
ConfigManager.removeObserver('errors', onTopUpErrors);
topUpLayoutSet = false;
initialized = false;
}
// BALANCE ACTIONS
// On showing the application
function updateWhenVisible() {
if (!document.hidden && initialized) {
updateUI();
} else {
clearInterval(topUpCountdown);
}
}
// On tapping update
function lockAndUpdateUI() {
updateButton.disabled = true;
updateUI(true);
}
// On changing low limit alert
function toogleLimits(isEnabled, old, key, settings) {
updateBalance(settings.lastBalance,
isEnabled && settings.lowLimitThreshold);
if (isEnabled) {
sendBalanceThresholdNotification(settings.lastBalance, settings, false);
updateBalance(settings.lastBalance, settings.lowLimitThreshold);
}
}
// On changing the threshold for low limit
function resetNotification() {
ConfigManager.setOption({ 'lowLimitNotified': false });
ConfigManager.requestAll(function _onSettings(configuration, settings) {
updateBalance(settings.lastBalance, settings.lowLimitThreshold);
sendBalanceThresholdNotification(settings.lastBalance, settings, false);
});
}
// On balance update received
function onBalance(balance, old, key, settings) {
debug('Balance received:', balance);
setBalanceMode();
updateBalance(balance, settings.lowLimit && settings.lowLimitThreshold);
debug('Balance updated!');
}
// On balance timeout
function onBalanceTimeout(errors) {
if (!errors.BALANCE_TIMEOUT) {
return;
}
debug('Balance timeout!');
setBalanceMode('warning');
setError('balance_error');
// Error handled, disabling
errors.BALANCE_TIMEOUT = false;
ConfigManager.setOption({errors: errors});
}
// TOP UP ACTIONS
// On tapping Top Up and Pay
function topUpWithUSSD() {
var dialing;
dialing = new MozActivity({
name: 'dial',
data: {
type: 'webtelephony/number',
number: ConfigManager.configuration.topup.ussd_destination
}
});
}
// On tapping Top Up with code
function topUpWithCode(lastWasError) {
window.location.hash = '##topup-dialog';
if (lastWasError) {
setTopUpMode('incorrect_code');
}
topUpCodeInput.focus();
}
// On typing the code
function toogleSend(evt) {
sendCode.disabled = !topUpCodeInput.value.trim();
}
// On tapping send code button
var DIALOG_TIMEOUT = 10 * 1000; // 10s
function requestTopUp() {
sendCode.disabled = true;
var code = topUpCodeInput.value.trim();
var requestObj = { type: 'topup', data: code };
costcontrol.request(requestObj, function _onTopUpRequest(result) {
debug(result);
sendCode.disabled = false;
var status = result.status;
if (status === 'success' || status === 'in_progress') {
setTopUpMode('in_progress');
setTimeout(closeTopUp, DIALOG_TIMEOUT);
} else if (status === 'error') {
setTopUpMode(result.details);
}
});
}
function closeTopUp() {
// Remove from hash
if (isTopUpShown()) {
window.location.hash = '#';
}
}
function isTopUpShown() {
return window.location.hash.split('#')[2] === topUpDialog.id;
}
// On confirmation SMS for top up received
function onConfirmation(isWaiting) {
if (isWaiting !== null) {
return;
}
debug('TopUp confirmed!');
setTopUpMode('default');
topUpCodeInput.value = '';
updateUI();
}
// On top up timeout or incorrect code
function onTopUpErrors(errors) {
debug('ERRORS:', errors);
var mode;
if (errors.TOPUP_TIMEOUT) {
errors.TOPUP_TIMEOUT = false;
mode = 'topup_timeout';
}
if (errors.INCORRECT_TOPUP_CODE) {
errors.INCORRECT_TOPUP_CODE = false;
mode = 'incorrect_code';
}
debug('Most important error: ', mode);
// XXX: Very important to call this only if something has changed. If not,
// infinite events are triggered.
if (mode) {
setTopUpMode(mode);
ConfigManager.setOption({errors: errors});
}
}
// USER INTERFACE
var topUpLayoutSet = false;
function updateUI(force) {
ConfigManager.requestAll(function _onSettings(configuration, settings) {
if (!topUpLayoutSet) {
topUpLayoutView.setupLayout(configuration.topup);
topUpLayoutSet = true;
}
resetTopUpCountdown();
updateBalance(settings.lastBalance,
settings.lowLimit && settings.lowLimitThreshold);
var requestObj = { type: 'balance', force: !!force };
costcontrol.request(requestObj, function _onRequest(result) {
debug(result);
updateButton.disabled = false;
var status = result.status;
var balance = result.data;
var isError = status === 'error' && result.details !== 'minimum_delay';
var isUpdating = status !== 'error';
if (isError) {
setBalanceMode('warning');
setError(result.details);
} else {
setBalanceMode(isUpdating ? 'updating' : 'default');
setError();
}
updateBalance(balance,
settings.lowLimit && settings.lowLimitThreshold);
});
});
}
// Update the balance in balance view
function updateBalance(balance, limit) {
if (!balance) {
debug('Balance not available');
balanceView.update();
return;
}
var isUpdating = view.classList.contains('updating');
balanceView.update(balance, isUpdating);
// Limits: reaching zero / low limit
if (balance.balance === 0) {
view.classList.add('no-credit');
} else {
view.classList.remove('no-credit');
if (limit && balance.balance < limit) {
view.classList.add('low-credit');
} else {
view.classList.remove('low-credit');
}
}
}
// Set warning / updating modes
var lastBalanceMode;
function setBalanceMode(mode) {
if (mode === lastBalanceMode) {
return;
}
lastBalanceMode = mode;
view.classList.remove('updating');
view.classList.remove('warning');
if (mode === 'warning') {
view.classList.add('warning');
}
if (mode === 'updating') {
view.classList.add('updating');
}
}
// Control info messages in the top up dialog as well as the top up countdown
// and error messages in the error area
var lastTopUpMode;
function setTopUpMode(mode) {
if (lastTopUpMode === mode) {
return;
}
lastTopUpMode = mode;
// Messages in top up dialog
var isShown;
var defaultMessage = document.getElementById('topup-code-explanation');
var inprogressMessage = document.getElementById('topup-in-progress');
var errorMessage = document.getElementById('topup-error');
var incorrectCodeMessage = document.getElementById('topup-incorrect-code');
isShown = (mode === 'default' || mode === 'topup_timeout');
defaultMessage.hidden = !isShown;
isShown = (mode === 'in_progress');
inprogressMessage.hidden = !isShown;
isShown = (mode === 'request_fail' || mode === 'no_service');
errorMessage.hidden = !isShown;
isShown = (mode === 'incorrect_code');
incorrectCodeMessage.hidden = !isShown;
// Count down
var countdownArea = document.getElementById('cost-control-topup-countdown');
isShown = (mode === 'in_progress');
countdownArea.hidden = !isShown;
if (isShown) {
resetTopUpCountdown();
}
// Messages in error message area
if (mode === 'topup_timeout') {
setError(mode);
}
}
var topUpCountdown, countdown;
function resetTopUpCountdown() {
getTopUpTimeout(function(timeout) {
if (!timeout) {
return;
}
countdown = Math.floor((timeout.getTime() - Date.now()) / 1000);
if (countdown < 0) {
return;
}
clearInterval(topUpCountdown);
topUpCountdown = setInterval(function _updateCountdown() {
var minutes = Math.floor(countdown / 60);
var seconds = (countdown % 60).toFixed(0);
var padding = '';
if (seconds < 10) {
padding = '0';
}
countdownSpan.textContent = (minutes + ' : ' + padding + seconds);
if (countdown > 0) {
countdown -= 1;
}
}, 1000);
});
}
var ERRORS = {
'airplane_mode': { priority: 1, string: 'airplane-mode-error-message2' },
'no_service': { priority: 2, string: 'no-coverage-error-message2' },
'no_coverage': { priority: 2, string: 'no-coverage-error-message' },
'topup_timeout': { priority: 3, string: 'top-up-timed-out2' },
'balance_error': { priority: 4, string: 'balance-error-message2' },
'non_free_in_roaming': { priority: 4, string: 'on-roaming-message2' }
};
var currentError = '';
// Decide which error should be shown taking in count error priorities
function setError(error) {
// Ignore showing message if the message is not registered
if (error && !ERRORS[error]) {
return;
}
debug('Error mode:', error ? error : 'no error');
var messageArea = document.getElementById('cost-control-message-area');
var message = document.getElementById('error-message-placeholder');
if (!error) {
messageArea.hidden = true;
message.removeAttribute('data-l10n-id');
message.textContent = '';
} else {
messageArea.hidden = false;
var curPriority = currentError ? ERRORS[currentError].priority : 0;
var newPriority = ERRORS[error] ? ERRORS[error].priority : 0;
if (newPriority >= curPriority) {
message.setAttribute('data-l10n-id', ERRORS[error].string);
currentError = error;
}
}
}
return {
topUpWithCode: topUpWithCode,
initialize: function() {
var SCRIPTS_NEEDED = [
'js/views/TopUpLayoutView.js',
'js/views/BalanceView.js'
];
LazyLoader.load(SCRIPTS_NEEDED, setupTab);
},
finalize: finalize
};
}());
BalanceTab.initialize();

View file

@ -0,0 +1,492 @@
/* global debug, ConfigManager, CostControl, Formatting,
SimManager, Common, ChartUtils */
/* jshint -W120 */
/*
* The data usage tab is in charge of usage charts of mobile and wi-fi networks.
*
* It has several canvas areas layered one above the others.
*/
'use strict';
var DataUsageTab = (function() {
var graphicArea, graphicPattern;
var backgroundLayer, wifiLayer, mobileLayer, axisLayer, todayLayer,
warningLayer, limitsLayer;
var wifiOverview, mobileOverview;
var wifiToggle, mobileToggle;
var appList, noData;
var costcontrol, initialized, model;
function setupTab() {
if (initialized) {
return;
}
CostControl.getInstance(function _onCostControl(instance) {
costcontrol = instance;
// HTML entities
graphicArea = document.getElementById('graphic-area');
graphicPattern = document.getElementById('graphic-pattern');
backgroundLayer = document.getElementById('background-layer');
wifiLayer = document.getElementById('wifi-layer');
mobileLayer = document.getElementById('mobile-layer');
axisLayer = document.getElementById('axis-layer');
todayLayer = document.getElementById('today-layer');
warningLayer = document.getElementById('warning-layer');
limitsLayer = document.getElementById('limits-layer');
wifiOverview = document.getElementById('wifiOverview');
mobileOverview = document.getElementById('mobileOverview');
wifiToggle = document.getElementById('wifiCheck');
mobileToggle = document.getElementById('mobileCheck');
appList = document.getElementById('app-usage-list');
noData = document.getElementById('app-usage-no-data');
window.addEventListener('localized', localize);
// Update and chart visibility
document.addEventListener('visibilitychange', updateWhenVisible);
wifiToggle.addEventListener('change', toggleWifi);
mobileToggle.addEventListener('change', toggleMobile);
// Setup the model
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
debug('First time setup for model');
model = {
height: ChartUtils.toDevicePixels(graphicArea.clientHeight),
width: ChartUtils.toDevicePixels(graphicArea.clientWidth),
originX: Math.floor(
ChartUtils.toDevicePixels(graphicArea.clientWidth) * 0.15),
endX: Math.floor(
ChartUtils.toDevicePixels(graphicArea.clientWidth) * 0.95),
axis: {
Y: {
lower: 0,
margin: 0.20
},
X: {
lower: ChartUtils.calculateLowerDate(settings),
upper: ChartUtils.calculateUpperDate(settings)
}
},
limits: {
enabled: settings.dataLimit,
value: ChartUtils.getLimitInBytes(settings),
dataLimitValue: settings.dataLimitValue,
dataLimitUnit: settings.dataLimitUnit
},
data: {
wifi: {
enabled: true
},
mobile: {
enabled: true
}
},
todayLabel: {}
};
ChartUtils.expandModel(model);
resetButtonState(settings);
ConfigManager.observe('dataLimit', toggleDataLimit, true);
ConfigManager.observe('dataLimitValue', setDataLimit, true);
ConfigManager.observe('lastCompleteDataReset', updateDataUsage, true);
ConfigManager.observe('lastDataReset', updateDataUsage, true);
ConfigManager.observe('nextReset', changeNextReset, true);
function finishInit() {
initialized = true;
updateDataUsage();
}
Common.loadApps()
.then(finishInit)
.catch(function(reason) {
debug(reason);
finishInit();
});
});
});
});
}
function localize() {
if (initialized) {
ChartUtils.drawTodayLayer(todayLayer, model);
ChartUtils.drawAxisLayer(axisLayer, model, mobileToggle.checked);
ChartUtils.drawLimits(limitsLayer, model, mobileToggle.checked);
}
}
function finalize() {
if (!initialized) {
return;
}
document.removeEventListener('visibilitychange', updateWhenVisible);
wifiToggle.removeEventListener('click', toggleWifi);
mobileToggle.removeEventListener('click', toggleMobile);
ConfigManager.removeObserver('dataLimit', toggleDataLimit);
ConfigManager.removeObserver('dataLimitValue', setDataLimit);
ConfigManager.removeObserver('lastCompleteDataReset', updateDataUsage);
ConfigManager.removeObserver('lastDataReset', updateDataUsage);
ConfigManager.removeObserver('nextReset', changeNextReset);
initialized = false;
}
function resetButtonState(settings) {
var isMobileChartVisible = settings.isMobileChartVisible;
if (typeof isMobileChartVisible === 'undefined') {
isMobileChartVisible = true;
}
if (isMobileChartVisible !== mobileToggle.checked) {
mobileToggle.checked = isMobileChartVisible;
toggleMobile();
}
var isWifiChartVisible = settings.isWifiChartVisible;
if (typeof isWifiChartVisible === 'undefined') {
isWifiChartVisible = false;
}
if (isWifiChartVisible !== wifiToggle.checked) {
wifiToggle.checked = isWifiChartVisible;
toggleWifi();
}
}
// UPDATES
// On visibility change
function updateWhenVisible(evt) {
if (!document.hidden) {
updateDataUsage();
}
}
function requestDataUsage(perApp) {
return new Promise(function(resolve) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
var request = { type: 'datausage' };
if (perApp) {
request.apps = Common.allApps.map(function(app) {
return app.manifestURL;
});
}
costcontrol.request(request, resolve);
});
});
});
}
function maybeRequestPerAppUsage() {
if (!model) {
return;
}
if (!model.data.mobile.total) {
noData.hidden = false;
appList.hidden = true;
// Repeated here to not rely on the deferred `updateApps()` call which
// would add an annoying delay until clearing the data.
clearAppList();
} else {
noData.hidden = true;
appList.hidden = false;
// Bug 1064491: request per-app data usage on the next tick of
// the main loop to avoid performance regression in startup
setTimeout(function() {
requestDataUsage(true).then(updateApps);
}, 0);
}
}
function updateCharts(result) {
return new Promise(function(resolve, reject) {
if (result.status === 'success') {
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
var modelData = result.data;
model.data.wifi.samples = modelData.wifi.samples;
model.data.wifi.total = modelData.wifi.total;
model.data.mobile.samples = modelData.mobile.samples;
model.data.mobile.total = modelData.mobile.total;
model.limits.enabled = settings.dataLimit;
model.limits.value = ChartUtils.getLimitInBytes(settings);
model.limits.dataLimitValue = settings.dataLimitValue;
model.axis.X.upper = ChartUtils.calculateUpperDate(settings);
model.axis.X.lower = ChartUtils.calculateLowerDate(settings);
ChartUtils.expandModel(model);
resolve();
debug('Rendering');
drawCharts();
});
});
} else {
reject(new Error(
'Error requesting data usage. This should not happen.'));
}
});
}
function updateApps(result) {
if (result.status === 'success') {
model.data.wifi.apps = result.data.wifi.apps;
model.data.mobile.apps = result.data.mobile.apps;
drawApps(model);
}
}
// OBSERVERS
function toggleDataLimit(value) {
model.limits.enabled = value;
ChartUtils.drawBackgroundLayer(backgroundLayer, model,
mobileToggle.checked);
ChartUtils.drawAxisLayer(axisLayer, model, mobileToggle.checked);
ChartUtils.drawLimits(limitsLayer, model, mobileToggle.checked);
ChartUtils.drawWarningLayer(warningLayer, model);
}
function setDataLimit(value, old, key, settings) {
model.limits.value = ChartUtils.getLimitInBytes(settings);
model.limits.dataLimitValue = settings.dataLimitValue;
ChartUtils.expandModel(model);
drawCharts();
}
function updateDataUsage() {
requestDataUsage()
.then(updateCharts)
.then(maybeRequestPerAppUsage);
}
function changeNextReset(value, old, key, settings) {
model.axis.X.upper = ChartUtils.calculateUpperDate(settings);
model.axis.X.lower = ChartUtils.calculateLowerDate(settings);
ChartUtils.expandModel(model);
drawCharts();
}
// USER INTERFACE
// On tapping on wifi toggle
function toggleWifi() {
var isChecked = wifiToggle.checked;
wifiLayer.hidden = !isChecked;
// save wifi toggled state
ConfigManager.setOption({ isWifiChartVisible: isChecked });
if (model) {
drawApps(model);
}
}
// On tapping on mobile toggle
function toggleMobile() {
var isChecked = mobileToggle.checked;
mobileLayer.hidden = !isChecked;
warningLayer.hidden = !isChecked;
limitsLayer.hidden = !isChecked;
// save wifi toggled state
ConfigManager.setOption({ isMobileChartVisible: isChecked });
if (model) {
ChartUtils.drawBackgroundLayer(backgroundLayer, model, isChecked);
ChartUtils.drawAxisLayer(axisLayer, model, isChecked);
ChartUtils.drawLimits(limitsLayer, model, isChecked);
drawApps(model);
}
}
function drawCharts() {
// Update overview
var wifiData = Formatting.roundData(model.data.wifi.total);
var mobileData = Formatting.roundData(model.data.mobile.total);
wifiOverview.textContent = Formatting.formatData(wifiData);
mobileOverview.textContent = Formatting.formatData(mobileData);
// Render the charts
ChartUtils.drawBackgroundLayer(backgroundLayer, model,
mobileToggle.checked);
ChartUtils.drawTodayLayer(todayLayer, model);
ChartUtils.drawAxisLayer(axisLayer, model, mobileToggle.checked);
ChartUtils.drawDataLayer(wifiLayer, model, 'wifi', {
stroke: ChartUtils.WIFI_CHART_STROKE,
fill: ChartUtils.WIFI_CHART_FILL
});
ChartUtils.drawDataLayer(mobileLayer, model, 'mobile', {
stroke: ChartUtils.MOBILE_CHART_STROKE,
fill: ChartUtils.MOBILE_CHART_FILL,
pattern: graphicPattern
});
ChartUtils.drawWarningLayer(warningLayer, model);
ChartUtils.drawLimits(limitsLayer, model, mobileToggle.checked);
drawApps(model);
}
var cachedAppItems = {};
function drawApps(model) {
function createAppItem(app) {
var isSystem = app.manifestURL === Common.SYSTEM_MANIFEST;
var appElement = document.createElement('li');
appElement.className = 'app-item';
var linkElement = document.createElement('a');
if (!isSystem) {
linkElement.href = '##appdetail-view?manifestURL=' + app.manifestURL;
}
appElement.appendChild(linkElement);
var imgElement = document.createElement('img');
imgElement.className = 'app-image';
imgElement.src = Common.getAppIcon(app);
imgElement.setAttribute('role', 'presentation');
imgElement.onerror = function() {
console.warn('Unable to load icon: ' + this.src);
this.src = '/style/images/app/icons/default.png';
};
linkElement.appendChild(imgElement);
var appInfoElement = document.createElement('div');
appInfoElement.className = 'app-info';
linkElement.appendChild(appInfoElement);
var nameElement = document.createElement('div');
nameElement.className = 'app-info-row app-name';
nameElement.textContent = Common.getLocalizedAppName(app);
appInfoElement.appendChild(nameElement);
var barElement = document.createElement('div');
barElement.className = 'app-info-row app-usage-bar';
barElement.setAttribute('role', 'presentation');
appInfoElement.appendChild(barElement);
var usedBarElement = document.createElement('div');
usedBarElement.className = 'app-usage-bar-used';
usedBarElement.setAttribute('role', 'presentation');
barElement.appendChild(usedBarElement);
var usageElement = document.createElement('div');
usageElement.className = 'app-info-row app-usage-total';
appInfoElement.appendChild(usageElement);
return appElement;
}
function updateAppItemUsage(appItem, total) {
var usedBarElement =
appItem.getElementsByClassName('app-usage-bar-used')[0];
var totalElement =
appItem.getElementsByClassName('app-usage-total')[0];
var barTotal = mobileTotal;
if (model.limits.enabled && model.limits.value !== null) {
barTotal = Math.max(barTotal, model.limits.value);
}
var usedPercent = (total / barTotal) * 100;
usedBarElement.style.width = usedPercent + '%';
totalElement.textContent = '' + Formatting.formatData(
Formatting.roundData(total));
}
// Front-end workaround for Bug 1083680: Noticeable difference between by
// application breakdown totals and the total displayed in chart and widget.
// This method adds the residual traffic (the traffic that cannot be not
// allocated to an app) to the System application.
function fixResidualTraffic() {
var breakdownTotal = 0;
mobileApps[Common.SYSTEM_MANIFEST] = {total: 0};
if (manifests.length > 0) {
breakdownTotal =
manifests.reduce(function(accumulatedTraffic, appManifest) {
return accumulatedTraffic + mobileApps[appManifest].total;
}, 0);
}
var residualTraffic = mobileTotal - breakdownTotal;
// System traffic is the residual traffic
if (residualTraffic > 0) {
mobileApps[Common.SYSTEM_MANIFEST].total = residualTraffic;
if (!manifests[Common.SYSTEM_MANIFEST]) {
manifests.push(Common.SYSTEM_MANIFEST);
}
}
}
clearAppList();
var mobileTotal = model.data.mobile.total;
var mobileApps = model.data.mobile.apps;
if (!mobileApps) {
return;
}
// Filter out apps that have not used any data yet.
var manifests = Object.keys(mobileApps).filter(function(key) {
return mobileApps[key].total > 0;
});
// Note: The second premise (mobileTotal === 0) must be removed when the
// fixResidualTraffic method will be eliminated.
if (manifests.length === 0 && mobileTotal === 0) {
return;
}
fixResidualTraffic();
// Sort by total data usage, descending
manifests.sort(function(a, b) {
return mobileApps[b].total - mobileApps[a].total;
});
var fragment = document.createDocumentFragment();
manifests.forEach(function(manifestURL) {
var app = Common.allApps.find(function(app) {
return app.manifestURL === manifestURL;
});
if (!app) {
debug('No app with manifest URL: ' + manifestURL);
return;
}
var appTotal = mobileApps[manifestURL].total;
var appItem = cachedAppItems[manifestURL];
if (!appItem) {
appItem = cachedAppItems[manifestURL] = createAppItem(app);
}
updateAppItemUsage(appItem, appTotal);
fragment.appendChild(appItem);
});
appList.appendChild(fragment);
}
function clearAppList() {
appList.innerHTML = '';
}
return {
initialize: setupTab,
finalize: finalize
};
}());
DataUsageTab.initialize();

View file

@ -0,0 +1,127 @@
/* global CostControl, ConfigManager, debug,
Formatting, SimManager, Common */
/*
* The telephony tab is in charge of show telephony and billing cycle
* information.
*
* It has two areas of drawing: one for the counters and another for
*/
'use strict';
var TelephonyTab = (function() {
var costcontrol, initialized = false;
var view, smscount, calltime, time, resetDate;
var telephonyPeriod = { begin: null, end: null };
function setupTab() {
if (initialized) {
return;
}
CostControl.getInstance(function _onCostControl(instance) {
costcontrol = instance;
// HTML entities
view = document.getElementById('telephony-tab');
smscount = document.getElementById('telephony-smscount');
calltime = document.getElementById('telephony-calltime');
time = document.getElementById('telephony-time');
resetDate = document.getElementById('reset-date');
window.addEventListener('localized', localize);
// Configure updates
document.addEventListener('visibilitychange', updateWhenVisible, true);
ConfigManager.observe('lastTelephonyActivity', updateCounters, true);
ConfigManager.observe('lastTelephonyReset', updateUI, true);
ConfigManager.observe('nextReset', updateNextReset, true);
// Timeformat
window.addEventListener('timeformatchange', function () {
updateTimePeriod(
telephonyPeriod.begin, null, null, telephonyPeriod.end);
});
updateUI();
initialized = true;
});
}
function localize() {
if (initialized) {
updateUI();
}
}
function finalize() {
if (!initialized) {
return;
}
document.removeEventListener('visibilitychange', updateWhenVisible);
ConfigManager.removeObserver('lastTelephonyActivity', updateCounters);
ConfigManager.removeObserver('lastTelephonyReset', updateUI);
ConfigManager.removeObserver('nextReset', updateNextReset);
initialized = false;
}
function updateWhenVisible() {
if (!document.hidden && initialized) {
updateUI();
}
}
function updateUI() {
var requestObj = { type: 'telephony' };
SimManager.requestDataSimIcc(function(dataSimIcc) {
ConfigManager.requestSettings(dataSimIcc.iccId,
function _onSettings(settings) {
costcontrol.request(requestObj, function _afterRequest(result) {
var telephonyActivity = result.data;
debug('Last telephony activity:', telephonyActivity);
updateTimePeriod(settings.lastTelephonyReset, null, null, settings);
updateCounters(telephonyActivity);
updateNextReset(settings.nextReset, null, null, settings);
});
});
});
}
function updateTimePeriod(lastReset, old, key, settings) {
telephonyPeriod.begin = lastReset;
telephonyPeriod.end = settings.lastTelephonyActivity.timestamp;
time.innerHTML = '';
time.appendChild(Formatting.formatTimeHTML(lastReset, telephonyPeriod.end));
}
function updateCounters(activity) {
Common.localize(smscount, 'magnitude', {
value: activity.smscount,
unit: 'SMS'
});
Common.localize(calltime, 'magnitude', {
value: Formatting.computeTelephonyMinutes(activity),
unit: 'min.'
});
}
function updateNextReset(reset, old, key, settings) {
var billingCycle = document.getElementById('billing-cycle');
if (settings.trackingPeriod === 'never') {
billingCycle.hidden = true;
} else {
billingCycle.hidden = false;
var content = Formatting.getFormattedDate(settings.nextReset,
Formatting.formatters.shortDate);
resetDate.textContent = content;
}
}
return {
initialize: setupTab,
finalize: finalize
};
}());
TelephonyTab.initialize();

View file

@ -0,0 +1,563 @@
/* global _, debug, BalanceView, AirplaneModeHelper, setNextReset, SimManager,
ConfigManager, Common, CostControl, MozActivity, LazyLoader,
Formatting
*/
/* exported activity */
'use strict';
/*
* The widget is in charge of show balance, telephony data and data usage
* statistics depending on the SIM inserted.
*
* It has three areas of drawing: balance, telephony and data usage.
* The update is done by demand when the widget is shown or in reaction of
* a balance update or timeout.
*/
var Widget = (function() {
var costcontrol, activity;
function checkSIMStatus(dataSim) {
var dataSimIcc = dataSim.icc;
var cardState = checkCardState(dataSimIcc);
if (cardState !== 'ready') {
debug('SIM not ready:', dataSimIcc.cardState);
initialized = false;
dataSimIcc.oncardstatechange = checkSIMStatus.bind(null, dataSim);
// SIM ready
} else {
debug('SIM ready. ICCID:', dataSim.iccId);
dataSimIcc.oncardstatechange = undefined;
var SCRIPTS_NEEDED_TO_START = [
'js/costcontrol.js',
'js/config/config_manager.js'
];
LazyLoader.load(SCRIPTS_NEEDED_TO_START, startWidget);
}
}
// Check the card status. Return 'ready' if all OK or take actions for
// special situations such as 'pin/puk locked' or 'absent'.
function checkCardState(dataSimIcc) {
var state, cardState;
state = cardState = dataSimIcc.cardState;
// SIM is absent
if (!cardState || cardState === 'absent') {
debug('There is no SIM');
Widget.showSimError('no-sim3');
// SIM is not initialized correctly
} else if (cardState === 'unknown') {
debug('Unknow state fo the sim');
Widget.showSimError('unknown');
// SIM is locked
} else if (
cardState === 'pinRequired' ||
cardState === 'pukRequired'
) {
Widget.showSimError('sim2-locked');
state = 'locked';
}
return state;
}
function loadMessageHandler() {
if (ConfigManager.option('nextReset')) {
window.addEventListener('messagehandlerready', function _setNextReset() {
window.removeEventListener('messagehandlerready', _setNextReset);
LazyLoader.load('js/settings/networkUsageAlarm.js', function() {
Common.loadNetworkInterfaces(function() {
setNextReset(ConfigManager.option('nextReset'));
});
});
});
}
document.getElementById('message-handler').src = 'message_handler.html';
}
function startWidget() {
CostControl.getInstance(function _onCostControlReady(instance) {
costcontrol = instance;
loadMessageHandler();
setupWidget();
});
}
var initialized, widget, leftPanel, rightPanel, fte, views = {};
var balanceView;
function _openBalanceTab() {
activity = new MozActivity({ name: 'costcontrol/balance' });
}
function _openTelephonyTab() {
activity = new MozActivity({ name: 'costcontrol/telephony' });
}
function _openDataUsageTab() {
activity = new MozActivity({ name: 'costcontrol/data_usage' });
}
function _onVisibilityChange(evt) {
AirplaneModeHelper.ready(function() {
if (!document.hidden && initialized &&
(AirplaneModeHelper.getStatus() === 'disabled')) {
SimManager.requestDataSimIcc(function(dataSimIcc) {
checkCardState(dataSimIcc.icc);
updateUI();
});
}
});
}
function _onHashChange() {
if (window.location.hash.split('#')[1] === 'update') {
updateUI(true); // update only data usage
}
}
function _onLocalize() {
if (initialized) {
updateUI();
}
}
function setupWidget() {
var mode = ConfigManager.getApplicationMode();
var isDataUsageOnly = (mode === 'DATA_USAGE_ONLY');
var isBalanceEnabled = !isDataUsageOnly;
// HTML entities
widget = document.getElementById('cost-control');
leftPanel = document.getElementById('left-panel');
rightPanel = document.getElementById('right-panel');
fte = document.getElementById('fte-view');
views.dataUsage = document.getElementById('datausage-view');
views.limitedDataUsage = document.getElementById('datausage-limit-view');
views.telephony = document.getElementById('telephony-view');
views.balance = document.getElementById('balance-view');
if (isBalanceEnabled) {
// Use observers to handle not on-demand updates
ConfigManager.observe('lastBalance', onBalance, true);
ConfigManager.observe('waitingForBalance', onErrors, true);
ConfigManager.observe('errors', onErrors, true);
LazyLoader.load('js/views/BalanceView.js', function() {
// Subviews
var balanceConfig = ConfigManager.configuration.balance;
balanceView = new BalanceView(
document.getElementById('balance-credit'),
document.querySelector('#balance-credit + .meta'),
balanceConfig ? balanceConfig.minimum_delay : undefined
);
});
// Open application with the proper view
views.balance.addEventListener('click', _openBalanceTab);
views.telephony.addEventListener('click', _openTelephonyTab);
}
// Use observers to handle not on-demand updates
ConfigManager.observe('lastCompleteDataReset', onReset, true);
ConfigManager.observe('lastTelephonyReset', onReset, true);
// Update UI when visible
document.addEventListener('visibilitychange', _onVisibilityChange);
// Update data usage on network activity
window.addEventListener('hashchange', _onHashChange);
window.addEventListener('localized', _onLocalize);
// Open application with the proper view
leftPanel.addEventListener('click', _openDataUsageTab);
LazyLoader.load([
'shared/js/date_time_helper.js',
'js/utils/formatting.js'
], function() {
updateUI();
});
if (SimManager.isMultiSim()) {
window.addEventListener('dataSlotChange', function _onDataSimChange() {
// Before updating the widget, it's necessary remove the cached values
// of costcontrol and config to force an update
CostControl.reset();
Widget.reset();
ConfigManager.setConfig(null);
SimManager.requestDataSimIcc(checkSIMStatus);
});
}
initialized = true;
}
// BALANCE ACTIONS
// On balance update received
function onBalance(balance, old, key, settings) {
debug('Balance received:', balance);
setBalanceMode('default');
updateBalance(balance, settings.lowLimit && settings.lowLimitThreshold);
debug('Balance updated!');
}
// On balance update fail
function onErrors(errors, old, key, settings) {
if (!errors || !errors.BALANCE_TIMEOUT) {
return;
}
debug('Balance timeout!');
setBalanceMode('warning');
errors.BALANCE_TIMEOUT = false;
ConfigManager.setOption({errors: errors});
}
// On reset telephony or data usage
function onReset(value, old, key, settings) {
updateUI();
}
// USER INTERFACE
// Reuse fte panel to display errors
function _showSimError(status, updateTextOnly) {
// Wait to l10n resources are ready
navigator.mozL10n.ready(function showErrorStatus() {
var widget = document.getElementById('cost-control');
var fte = document.getElementById('fte-view');
var leftPanel = document.getElementById('left-panel');
var rightPanel = document.getElementById('right-panel');
if (!updateTextOnly) {
widget.hidden = false;
fte.hidden = false;
leftPanel.hidden = true;
rightPanel.hidden = true;
}
var className = 'widget-' + status;
Common.localize(fte.querySelector('p:first-child'), className +
'-heading');
Common.localize(fte.querySelector('p:last-child'), className +
'-meta');
});
}
function setupFte(provider, mode) {
widget.hidden = false;
fte.hidden = false;
leftPanel.hidden = true;
rightPanel.hidden = true;
fte.addEventListener('click', function launchFte(evt) {
fte.removeEventListener('click', launchFte);
evt.stopImmediatePropagation();
activity = new MozActivity({ name: 'costcontrol/balance' });
});
var keyLookup = {
PREPAID: 'widget-authed-sim2',
POSTPAID: 'widget-authed-sim2',
DATA_USAGE_ONLY: 'widget-nonauthed-sim2'
};
var simKey = keyLookup[mode];
Common.localize(
fte.querySelector('p:first-child'),
simKey + '-heading',
{provider: provider}
);
Common.localize(fte.querySelector('p:last-child'), simKey +
'-meta');
}
var hashMark = 0;
function updateUI(updateOnlyDataUsage) {
ConfigManager.requestAll(function _onInfo(configuration, settings) {
var mode = ConfigManager.getApplicationMode();
debug('Widget UI mode:', mode);
var isPrepaid = (mode === 'PREPAID');
var isDataUsageOnly = (mode === 'DATA_USAGE_ONLY');
// Show fte mode widget
if (settings.fte) {
setupFte(configuration.provider, mode);
debug('Widget in FTE mode');
return;
}
// Layout
widget.hidden = true;
fte.hidden = true;
leftPanel.hidden = false;
rightPanel.hidden = false;
var isLimited = settings.dataLimit;
views.dataUsage.hidden = isLimited;
views.limitedDataUsage.hidden = !isLimited;
// Always data usage
rightPanel.hidden = isDataUsageOnly;
// And the other view if applies...
if (isDataUsageOnly) {
widget.classList.add('full');
widget.hidden = false;
} else {
widget.classList.remove('full');
views.balance.hidden = !isPrepaid;
views.telephony.hidden = isPrepaid;
widget.hidden = false;
}
// Content for data statistics
var requestObj = { type: 'datausage' };
costcontrol.request(requestObj, function _onDataStatistics(result) {
debug(result);
var stats = result.data;
var data = Formatting.roundData(stats.mobile.total);
if (isLimited) {
// UI elements
var dataLimit = views.limitedDataUsage.querySelector('#data-limit');
var dataAvailable =
views.limitedDataUsage.querySelector('#data-available');
var current = stats.mobile.total;
var limit = Common.getDataLimit(settings);
debug(limit);
// State
views.limitedDataUsage.classList.remove('nearby-limit');
views.limitedDataUsage.classList.remove('reached-limit');
// Limit trespassed
var limitTrespassed = (current > limit);
if (limitTrespassed) {
views.limitedDataUsage.classList.add('reached-limit');
// Warning percentage of the limit reached
} else if (current >= limit * Common.DATA_USAGE_WARNING) {
views.limitedDataUsage.classList.add('nearby-limit');
}
// Texts
var limitText = Formatting.roundData(limit);
limitText = _('magnitude', {
value: limitText[0],
unit: limitText[1]
});
navigator.mozL10n.setAttributes(dataLimit, 'data-limit',
{ value: limitText });
var rawValue = Math.abs(limit - current);
var text = Formatting.roundData(rawValue);
if (!limitTrespassed) {
navigator.mozL10n.setAttributes(dataAvailable, 'data-available2',
{ value: parseFloat(text[0]), unit: text[1] });
} else {
navigator.mozL10n.setAttributes(dataAvailable, 'over-limit',
{ value: parseFloat(text[0]), unit: text[1] });
}
} else {
// Texts
document.getElementById('mobile-usage-value').textContent =
_('magnitude', { value: data[0], unit: data[1] });
var meta = views.dataUsage.querySelector('.meta');
meta.innerHTML = '';
meta.appendChild(Formatting.formatTimeHTML(stats.timestamp));
}
// inform driver in system we are finished to update the widget
hashMark = 1 - hashMark; // toogle between 0 and 1
window.location.hash = '#updateDone#' + hashMark;
});
// Content for balance or telephony
if (!isDataUsageOnly && !updateOnlyDataUsage) {
if (mode === 'PREPAID') {
updateBalance(settings.lastBalance,
settings.lowLimit && settings.lowLimitThreshold);
requestObj = { type: 'balance' };
costcontrol.request(requestObj, function _onRequest(result) {
debug(result);
var status = result.status;
var balance = result.data;
setBalanceMode(status === 'error' ? 'warning' : 'updating');
updateBalance(balance,
settings.lowLimit && settings.lowLimitThreshold);
});
} else if (mode === 'POSTPAID') {
requestObj = { type: 'telephony' };
costcontrol.request(requestObj, function _onRequest(result) {
var dataActivity = result.data;
document.getElementById('telephony-calltime').textContent =
_('magnitude', {
value: Formatting.computeTelephonyMinutes(dataActivity),
unit: 'min'
}
);
document.getElementById('telephony-smscount').textContent =
_('magnitude', {
value: dataActivity.smscount,
unit: 'SMS'
}
);
var meta = views.telephony.querySelector('.meta');
meta.innerHTML = '';
meta.appendChild(Formatting.formatTimeHTML(dataActivity.timestamp));
});
}
}
});
}
// Update the balance in balance view
function updateBalance(balance, limit) {
if (!balance) {
debug('Balance not available');
balanceView.update();
return;
}
var isUpdating = views.balance.classList.contains('updating');
balanceView.update(balance, isUpdating);
// Limits: reaching zero / low limit
if (balance.balance === 0) {
views.balance.classList.add('no-credit');
} else {
views.balance.classList.remove('no-credit');
if (limit && balance.balance < limit) {
views.balance.classList.add('low-credit');
} else {
views.balance.classList.remove('low-credit');
}
}
}
// Set warning / updating modes
var lastBalanceMode;
function setBalanceMode(mode) {
if (mode === lastBalanceMode) {
return;
}
lastBalanceMode = mode;
views.balance.classList.remove('updating');
views.balance.classList.remove('warning');
if (mode === 'warning') {
views.balance.classList.add('warning');
}
if (mode === 'updating') {
views.balance.classList.add('updating');
}
}
window.addEventListener('airplaneModeDisabled',
function _onAirplanemodeDisabled(evt) {
if (evt.detail && evt.detail.serviceId === 'data') {
SimManager.requestDataSimIcc(checkSIMStatus);
}
});
function initWidget() {
var isWaitingForIcc = false;
function waitForIccAndCheckSim() {
if (!isWaitingForIcc) {
var iccManager = window.navigator.mozIccManager;
iccManager.addEventListener('iccdetected',
function _oniccdetected() {
isWaitingForIcc = false;
iccManager.removeEventListener('iccdetected', _oniccdetected);
if (AirplaneModeHelper.getStatus() === 'disabled') {
SimManager.requestDataSimIcc(checkSIMStatus);
}
}
);
isWaitingForIcc = true;
}
}
SimManager.requestDataSimIcc(checkSIMStatus, function _errorNoSim() {
AirplaneModeHelper.ready(function() {
waitForIccAndCheckSim();
var errorMessageId = (AirplaneModeHelper.getStatus() === 'enabled') ?
'airplane-mode' : 'no-sim3';
console.warn('Error when trying to get the ICC ID');
Widget.showSimError(errorMessageId);
});
});
AirplaneModeHelper.addEventListener('statechange',
function _onAirplaneModeChange(state) {
if (state === 'enabled') {
Widget.showSimError('airplane-mode');
} else if (isWaitingForIcc) {
var updateTextOnly = true;
Widget.showSimError('no-sim3', updateTextOnly);
}
}
);
}
return {
init: function() {
var SCRIPTS_NEEDED = [
'js/common.js',
'js/utils/toolkit.js'
];
// Check if the mandatory APIs to work exist.
if (!window.navigator.mozMobileConnections ||
!window.navigator.mozIccManager ||
!window.navigator.mozNetworkStats) {
LazyLoader.load(SCRIPTS_NEEDED, function _showError() {
Widget.showSimError('no-sim3');
});
} else {
SCRIPTS_NEEDED = [
'js/sim_manager.js',
'js/common.js',
'js/utils/toolkit.js',
'js/utils/debug.js'
];
LazyLoader.load(SCRIPTS_NEEDED, initWidget);
}
},
showSimError: _showSimError,
reset: function() {
document.removeEventListener('visibilitychange', _onVisibilityChange);
window.removeEventListener('hashchange', _onHashChange);
window.removeEventListener('localized', _onLocalize);
leftPanel.removeEventListener('click', _openDataUsageTab);
if (views.balance) {
views.balance.removeEventListener('click', _openBalanceTab);
}
if (views.telephony) {
views.telephony.removeEventListener('click', _openTelephonyTab);
}
}
};
}());
Widget.init();