Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
538
gaia/apps/costcontrol/js/app.js
Executable file
538
gaia/apps/costcontrol/js/app.js
Executable 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
|
||||
};
|
||||
}());
|
||||
4
gaia/apps/costcontrol/js/application_init.js
Executable file
4
gaia/apps/costcontrol/js/application_init.js
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
/* global CostControlApp */
|
||||
'use strict';
|
||||
|
||||
CostControlApp.init();
|
||||
456
gaia/apps/costcontrol/js/common.js
Executable file
456
gaia/apps/costcontrol/js/common.js
Executable 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
|
||||
}
|
||||
}
|
||||
};
|
||||
373
gaia/apps/costcontrol/js/config/config_manager.js
Executable file
373
gaia/apps/costcontrol/js/config/config_manager.js
Executable 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
|
||||
};
|
||||
|
||||
}());
|
||||
4
gaia/apps/costcontrol/js/config/default/config.js
Executable file
4
gaia/apps/costcontrol/js/config/default/config.js
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
/* global ConfigManager */
|
||||
'use strict';
|
||||
|
||||
ConfigManager.setConfig({});
|
||||
9
gaia/apps/costcontrol/js/config/index.json
Executable file
9
gaia/apps/costcontrol/js/config/index.json
Executable 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"
|
||||
}
|
||||
22
gaia/apps/costcontrol/js/config/movistar_colombia/config.js
Executable file
22
gaia/apps/costcontrol/js/config/movistar_colombia/config.js
Executable 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);
|
||||
19
gaia/apps/costcontrol/js/config/movistar_peru/config.js
Executable file
19
gaia/apps/costcontrol/js/config/movistar_peru/config.js
Executable 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);
|
||||
23
gaia/apps/costcontrol/js/config/movistar_uruguay/config.js
Executable file
23
gaia/apps/costcontrol/js/config/movistar_uruguay/config.js
Executable 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);
|
||||
126
gaia/apps/costcontrol/js/config/vivo/config.js
Executable file
126
gaia/apps/costcontrol/js/config/vivo/config.js
Executable 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);
|
||||
720
gaia/apps/costcontrol/js/costcontrol.js
Executable file
720
gaia/apps/costcontrol/js/costcontrol.js
Executable 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;
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
65
gaia/apps/costcontrol/js/desktop-only/async_storage.js
Executable file
65
gaia/apps/costcontrol/js/desktop-only/async_storage.js
Executable 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);
|
||||
24
gaia/apps/costcontrol/js/desktop-only/navigator_moz_alarms.js
Executable file
24
gaia/apps/costcontrol/js/desktop-only/navigator_moz_alarms.js
Executable 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() {}
|
||||
};
|
||||
})();
|
||||
33
gaia/apps/costcontrol/js/desktop-only/navigator_moz_apps_mgmt.js
Executable file
33
gaia/apps/costcontrol/js/desktop-only/navigator_moz_apps_mgmt.js
Executable 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;
|
||||
}
|
||||
};
|
||||
})();
|
||||
21
gaia/apps/costcontrol/js/desktop-only/navigator_moz_icc_manager.js
Executable file
21
gaia/apps/costcontrol/js/desktop-only/navigator_moz_icc_manager.js
Executable 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
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
11
gaia/apps/costcontrol/js/desktop-only/navigator_moz_mobile_connections.js
Executable file
11
gaia/apps/costcontrol/js/desktop-only/navigator_moz_mobile_connections.js
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
if (navigator.mozMobileConnections) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.mozMobileConnections = navigator.mozIccManager.iccIds.map((id) => {
|
||||
return { iccId: id};
|
||||
});
|
||||
})();
|
||||
145
gaia/apps/costcontrol/js/desktop-only/navigator_moz_network_stats.js
Executable file
145
gaia/apps/costcontrol/js/desktop-only/navigator_moz_network_stats.js
Executable 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;
|
||||
}
|
||||
};
|
||||
})();
|
||||
35
gaia/apps/costcontrol/js/desktop-only/navigator_moz_settings.js
Executable file
35
gaia/apps/costcontrol/js/desktop-only/navigator_moz_settings.js
Executable 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
289
gaia/apps/costcontrol/js/fte.js
Executable 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;
|
||||
};
|
||||
}
|
||||
|
||||
}());
|
||||
90
gaia/apps/costcontrol/js/iac_manager.js
Executable file
90
gaia/apps/costcontrol/js/iac_manager.js
Executable 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
|
||||
};
|
||||
}());
|
||||
619
gaia/apps/costcontrol/js/message_handler.js
Executable file
619
gaia/apps/costcontrol/js/message_handler.js
Executable 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();
|
||||
}());
|
||||
288
gaia/apps/costcontrol/js/settings/autosettings.js
Executable file
288
gaia/apps/costcontrol/js/settings/autosettings.js
Executable 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
|
||||
};
|
||||
|
||||
}());
|
||||
234
gaia/apps/costcontrol/js/settings/limitdialog.js
Executable file
234
gaia/apps/costcontrol/js/settings/limitdialog.js
Executable 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);
|
||||
});
|
||||
|
||||
}
|
||||
89
gaia/apps/costcontrol/js/settings/networkUsageAlarm.js
Executable file
89
gaia/apps/costcontrol/js/settings/networkUsageAlarm.js
Executable 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
|
||||
};
|
||||
}());
|
||||
367
gaia/apps/costcontrol/js/settings/settings.js
Executable file
367
gaia/apps/costcontrol/js/settings/settings.js
Executable 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();
|
||||
197
gaia/apps/costcontrol/js/sim_manager.js
Executable file
197
gaia/apps/costcontrol/js/sim_manager.js
Executable 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
|
||||
};
|
||||
})();
|
||||
|
||||
585
gaia/apps/costcontrol/js/utils/chart.js
Executable file
585
gaia/apps/costcontrol/js/utils/chart.js
Executable 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
|
||||
};
|
||||
}());
|
||||
39
gaia/apps/costcontrol/js/utils/debug.js
Executable file
39
gaia/apps/costcontrol/js/utils/debug.js
Executable 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(' '));
|
||||
}
|
||||
};
|
||||
}());
|
||||
241
gaia/apps/costcontrol/js/utils/formatting.js
Executable file
241
gaia/apps/costcontrol/js/utils/formatting.js
Executable 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);
|
||||
52
gaia/apps/costcontrol/js/utils/toolkit.js
Executable file
52
gaia/apps/costcontrol/js/utils/toolkit.js
Executable 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;
|
||||
}
|
||||
|
||||
};
|
||||
255
gaia/apps/costcontrol/js/view_manager.js
Executable file
255
gaia/apps/costcontrol/js/view_manager.js
Executable 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;
|
||||
}());
|
||||
44
gaia/apps/costcontrol/js/views/BalanceLowLimitView.js
Executable file
44
gaia/apps/costcontrol/js/views/BalanceLowLimitView.js
Executable 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});
|
||||
}
|
||||
};
|
||||
83
gaia/apps/costcontrol/js/views/BalanceView.js
Executable file
83
gaia/apps/costcontrol/js/views/BalanceView.js
Executable 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;
|
||||
}
|
||||
31
gaia/apps/costcontrol/js/views/ConfirmDialog.js
Executable file
31
gaia/apps/costcontrol/js/views/ConfirmDialog.js
Executable 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();
|
||||
};
|
||||
80
gaia/apps/costcontrol/js/views/NonReadyScreen.js
Executable file
80
gaia/apps/costcontrol/js/views/NonReadyScreen.js
Executable 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;
|
||||
};
|
||||
57
gaia/apps/costcontrol/js/views/ResetMenuDialog.js
Executable file
57
gaia/apps/costcontrol/js/views/ResetMenuDialog.js
Executable 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();
|
||||
};
|
||||
16
gaia/apps/costcontrol/js/views/TopUpLayoutView.js
Executable file
16
gaia/apps/costcontrol/js/views/TopUpLayoutView.js
Executable 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;
|
||||
}
|
||||
261
gaia/apps/costcontrol/js/views/appdetail.js
Executable file
261
gaia/apps/costcontrol/js/views/appdetail.js
Executable 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();
|
||||
468
gaia/apps/costcontrol/js/views/balance.js
Executable file
468
gaia/apps/costcontrol/js/views/balance.js
Executable 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();
|
||||
492
gaia/apps/costcontrol/js/views/datausage.js
Executable file
492
gaia/apps/costcontrol/js/views/datausage.js
Executable 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();
|
||||
127
gaia/apps/costcontrol/js/views/telephony.js
Executable file
127
gaia/apps/costcontrol/js/views/telephony.js
Executable 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();
|
||||
563
gaia/apps/costcontrol/js/widget.js
Executable file
563
gaia/apps/costcontrol/js/widget.js
Executable 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();
|
||||
Loading…
Reference in a new issue