VulpesOS/gaia/apps/costcontrol/js/costcontrol.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

720 lines
22 KiB
JavaScript
Executable file

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