Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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