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,98 @@
/* global NotificationHelper,
Notify,
Promise,
Utils
*/
'use strict';
(function(exports) {
var form;
function init() {
form = document.querySelector('form');
form.addEventListener('submit', onFormSubmit);
navigator.mozL10n.once(function () {
sendNotification().catch((err) => {
console.error('Error while sending a notification', err);
});
});
document.addEventListener('visibilitychange', onVisibilityChange);
}
function sendNotification() {
var params = Utils.parseParams();
var fromNotification = params.notification;
if (fromNotification) {
return Promise.resolve();
}
return new Promise(function(resolve, reject) {
var titleL10n = params.title;
var body = params.body;
var req = navigator.mozApps.getSelf();
req.onsuccess = function onsuccess(event) {
var app = event.target.result;
NotificationHelper.send(titleL10n, {
body: body,
tag: '' + Date.now(), // needs to be unique
icon: NotificationHelper.getIconURI(app),
data: { title: titleL10n },
closeOnClick: false
}).then(function(notification){
notification.onerror = function onerror() {
reject(new Error('CMAS: notification API error'));
};
notification.onshow = resolve;
});
};
req.onerror = function onerror() {
reject(new Error('CMAS: App getSelf request error'));
};
});
}
function closeApp() {
// make sure we close both parent and child window
window.opener ? window.opener.close() : window.close();
}
function onFormSubmit(e) {
e.preventDefault();
// Close parent(if exist) and child attention window after user click ok
closeApp();
}
function onVisibilityChange() {
// Close app when app resized as top banner and moved to background
if (document.hidden && !document.querySelector('h1').clientHeight) {
closeApp();
}
}
function render() {
var params = Utils.parseParams();
renderForm(params);
var fromNotification = params.notification;
if (!fromNotification) {
Notify.notify();
}
}
function renderForm(params) {
var title = params.title;
var body = params.body;
form.querySelector('h1').setAttribute('data-l10n-id', title);
form.querySelector('p').textContent = body;
}
exports.Attention = { init, render };
})(window);

View file

@ -0,0 +1,120 @@
/* global
AudioContext
*/
(function(exports) {
'use strict';
const SETTINGS = {
notificationVolume: 'audio.volume.notification',
vibration: 'vibration.enabled'
};
const ATTENTION_PATTERN = [4, 1, 2, 1, 2, 1];
ATTENTION_PATTERN.push(...ATTENTION_PATTERN);
const ATTENTION_SOUND_VOLUME = 0.3;
function getSetting(key) {
if (!navigator.mozSettings) {
return Promise.reject(new Error('The mozSettings API is not available.'));
}
return navigator.mozSettings.createLock().get(key).then(
(result) => result[key]
);
}
function getSettings(settings) {
return Promise.all(
settings.map(getSetting)
).catch((e) => {
// catch and log errors
console.error('Error while retrieving settings', e.message, e);
return settings.map(() => null);
}).then((results) => {
return settings.reduce((result, setting, i) => {
result[setting] = results[i];
return result;
}, {});
});
}
// Converts from the ATTENTION_PATTERN (suitable for the Vibration API) to a
// Float32Array suitable for the Audio API.
//
// The Float32Array will be interpolated so we just need to have the changes.
// Each value will last a "unit" of time.
function getAttentionCurveWave() {
var result = [];
var currentValue = ATTENTION_SOUND_VOLUME;
ATTENTION_PATTERN.forEach(duration => {
for (var i = 0; i < duration; i++) {
result.push(currentValue);
}
currentValue = ATTENTION_SOUND_VOLUME - currentValue;
});
return new Float32Array(result);
}
function ringtone() {
var audioChannel = 'notification';
var audioCtx = new AudioContext(audioChannel);
var o1 = audioCtx.createOscillator();
var o2 = audioCtx.createOscillator();
var gain = audioCtx.createGain();
var time = audioCtx.currentTime;
o1.type = o2.type = 'sine';
o1.frequency.value = 853;
o2.frequency.value = 960;
o1.start();
o2.start();
// Eventually stop the oscillator to allow garbage collecting.
o1.stop(time + 11);
o2.stop(time + 11);
var wave = getAttentionCurveWave();
gain.gain.setValueCurveAtTime(wave, time, 11);
o1.connect(gain);
o2.connect(gain);
gain.connect(audioCtx.destination);
}
function vibrate() {
var pattern = ATTENTION_PATTERN.map((value) => value * 500);
// vibration only works when App is in the foreground
if (document.hidden) {
window.addEventListener('visibilitychange', function waitOn() {
window.removeEventListener('visibilitychange', waitOn);
navigator.vibrate(pattern);
});
} else {
navigator.vibrate(pattern);
}
}
var Notify = {
notify: function notification_ringtone() {
return getSettings(
[SETTINGS.notificationVolume, SETTINGS.vibration]
).then((settings) => {
if (settings[SETTINGS.notificationVolume]) {
ringtone();
}
if (settings[SETTINGS.vibration]) {
vibrate();
}
});
}
};
exports.Notify = Notify;
}(this));

View file

@ -0,0 +1,8 @@
/* global Attention */
'use strict';
(function() {
Attention.init();
Attention.render();
})();

View file

@ -0,0 +1,110 @@
'use strict';
// See reference TS in following document for GSM and CDMA:
// [1] GSM:
// http://www.etsi.org/deliver/etsi_ts/123000_123099/123041/11.06.00_60/ts_123041v110600p.pdf
//
// [2] CDMA:
// http://www.3gpp2.org/public_html/specs/C.R1001-G_v1.0_Param_Administration.pdf
(function() {
const CMAS_ID = 'emergency-alert-title',
CMAS_ENABLED_KEY = 'cmas.enabled';
// See GSM[1] section 9.4.1.2.2 Message Identifier and
// CDMA[2] section 9.3.3 Commercial Mobile for thet presidential alerts
const GSM_PRESIDENTIAL_ALERTS = [4370, 4383];
const CDMA_PRESIDENTIAL_ALERT = 0x1000;
window.navigator.mozSetMessageHandler(
'cellbroadcast-received',
onCellbroadcast
);
/**
* Check if the id of the message in in CMAS specific range. Please ref link
* above for GSM[1] and CDMA[2]
*
* @param {number} Message
*/
function isEmergencyAlert(message) {
var messageId = message.messageId;
var cdmaServiceCategory = message.cdmaServiceCategory;
var isGSM = cdmaServiceCategory === null;
if (isGSM) {
return (messageId >= 4370 && messageId < 4400);
} else {
return (cdmaServiceCategory >= 0x1000 && cdmaServiceCategory <= 0x10FF);
}
}
function isCmasEnabledForServiceId(serviceId) {
var getPromise = navigator.mozSettings.createLock().get(CMAS_ENABLED_KEY);
return getPromise.then(
result => !!(result[CMAS_ENABLED_KEY][serviceId])
);
}
function isPresidentialAlert(message) {
var messageId = message.messageId;
var cdmaServiceCategory = message.cdmaServiceCategory;
var isGSM = cdmaServiceCategory === null;
if (isGSM) {
return GSM_PRESIDENTIAL_ALERTS.indexOf(messageId) !== -1;
} else {
return CDMA_PRESIDENTIAL_ALERT === cdmaServiceCategory;
}
}
/**
* Handling the cellbroadcast system message.
* @param {Object} Cellbroadcast message object which contains necessary
* information like Identifier and body for displaying attention screen.
*/
function onCellbroadcast(message) {
if (!isEmergencyAlert(message)) {
window.close();
return;
}
var shouldSendAlertPromise =
isPresidentialAlert(message) ?
Promise.resolve(true) :
isCmasEnabledForServiceId(message.serviceId).catch((e) => {
console.error('CMAS: Unable to query settings database', e);
return false;
});
return shouldSendAlertPromise.then((yes) => {
if (yes) {
sendAlert({
title: CMAS_ID,
body: message.body
});
// Do not close window here because it will close attention screen
} else {
window.close();
}
});
}
/**
* Open CMAS alert attention screen with given message body content and
* title.
* @param {Object} Options object that contains the title l10n id and message
* body.
*/
function sendAlert(opts) {
var url = [
'attention.html?title=',
encodeURIComponent(opts.title),
'&body=',
encodeURIComponent(opts.body)
].join('');
window.open(url, '_blank', 'attention');
}
})();

View file

@ -0,0 +1,36 @@
'use strict';
(function(exports) {
function init() {
window.navigator.mozSetMessageHandler('notification', onNotification);
}
function closeWindow() {
window.close();
}
function onNotification(message) {
if (!message.clicked) {
closeWindow();
return;
}
var title = message.data.title;
var url = [
'attention.html?title=',
encodeURIComponent(title),
'&body=',
encodeURIComponent(message.body),
'&notification=1'
].join('');
window.open(url, '_blank', 'attention');
}
exports.NotificationHandler = {
init: init
};
})(window);

View file

@ -0,0 +1,7 @@
/* global NotificationHandler */
'use strict';
(function() {
NotificationHandler.init();
})();

View file

@ -0,0 +1,18 @@
'use strict';
(function(exports) {
var rparams = /([^?=&]+)(?:=([^&]*))?/g;
function parseParams(input) {
input = input || window.location.href;
var parsed = {};
input.replace(rparams, function($0, $1, $2) {
parsed[decodeURIComponent($1)] = $2 ? decodeURIComponent($2) : $2;
});
return parsed;
}
exports.Utils = {
parseParams: parseParams
};
})(window);