VulpesOS/assets/webapps/costcontrol.gaiamobile.org/js/views/datausage.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

492 lines
16 KiB
JavaScript

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