Release Preview 20 shared desktop, Android and Sargo updates

Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
This commit is contained in:
OverSu 2026-10-13 00:19:45 +02:00
commit 1311bed5e9
170 changed files with 19698 additions and 2495 deletions

View file

@ -139,10 +139,6 @@ define(function(require) {
*/
DateTime.prototype._init = function() {
this._mozTime = window.navigator.mozTime;
if (!this._mozTime) {
console.error('Could not get window.navigator.mozTime');
return;
}
this._boundSetTimeAutoEnabled = function(event) {
this._clockAutoEnabled = event.settingValue;
@ -320,9 +316,7 @@ define(function(require) {
switch (type) {
case 'date':
tokens = value.split('-');
newDate.setYear(tokens[0]);
newDate.setMonth(tokens[1] - 1);
newDate.setDate(tokens[2]);
newDate.setFullYear(Number(tokens[0]), Number(tokens[1]) - 1, Number(tokens[2]));
break;
case 'time':
@ -331,7 +325,8 @@ define(function(require) {
newDate.setMinutes(parseInt(tokens[1]));
break;
}
this._mozTime.set(newDate);
if (!this._mozTime) return;
return this._mozTime.set(newDate);
};
/**

View file

@ -99,37 +99,35 @@ define(function(require) {
_transit: function dm__transit(method, dialog, options) {
var promise = new Promise(function(resolve) {
var panel = dialog.panel;
panel.addEventListener('transitionend', function paintWait(evt) {
if ((method === 'close' || method === 'open') &&
evt.propertyName === 'visibility') {
// After transition, we have to `hide` the panel, otherwise
// the panel would still exist on the layer and would block
// the scrolling event.
if (method === 'close') {
panel.hidden = true;
}
panel.removeEventListener('transitionend', paintWait);
resolve();
}
});
// Before transition, we have to `show` the panel, otherwise
// the panel before applying transition class.
if (method === 'open') {
panel.hidden = false;
var completed = false;
var fallback;
function finish(event) {
if (event && (event.target !== panel || event.propertyName !== 'visibility')) return;
if (completed) return;
completed = true;
clearTimeout(fallback);
panel.removeEventListener('transitionend', finish);
panel.removeEventListener('transitioncancel', finish);
if (method === 'close') panel.hidden = true;
resolve();
}
// We need to apply class later otherwise Gecko can't apply
// this transition and 150ms is an approximate number after doing
// several rounds of manual tests.
if (method === 'open') panel.hidden = false;
setTimeout(function() {
if (method === 'open') {
// Let's unhide the panel first
panel.classList.add('current');
} else {
panel.classList.remove('current');
panel.addEventListener('transitionend', finish);
panel.addEventListener('transitioncancel', finish);
panel.classList.toggle('current', method === 'open');
var style = window.getComputedStyle(panel);
function milliseconds(value) {
return parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0;
}
var durations = style.transitionDuration.split(',').map(milliseconds);
var delays = style.transitionDelay.split(',').map(milliseconds);
var duration = Math.max.apply(Math, durations.map(function(value, index) {
return value + delays[index % delays.length];
}));
// Reduced motion, display changes or interrupted transitions may
// omit the visibility event. Always release the dialog queue.
fallback = setTimeout(finish, Math.max(0, duration) + 100);
}, 150);
});
return promise;

View file

@ -190,29 +190,30 @@ define(function(require) {
if ('#' + panelId === currentPanel) {
defer.reject('You are showing the same panel #' + panelId);
} else {
options.onWrapSubmit = function() {
DialogManager.close(dialog, 'submit', options)
.then(function(result) {
defer.resolve({
type: 'submit',
value: result
});
var opening;
var closing = false;
function close(type) {
if (closing) return;
closing = true;
// A tap during the opening animation must not run hide/show
// concurrently, or a queued dialog can inherit stale form values.
Promise.resolve(opening).then(function() {
return DialogManager.close(dialog, type, options);
}).then(function(result) {
self._navigating = false;
defer.resolve({type: type, value: result});
self._execPendingRequest();
}, function(error) {
// Panel validation rejects to keep the form open. Allow a
// correction or cancellation without rejecting the show promise.
closing = false;
dialogDOM.dispatchEvent(new CustomEvent('dialogvalidationfailed', {
detail: error
}));
});
};
options.onWrapCancel = function() {
DialogManager.close(dialog, 'cancel', options)
.then(function(result) {
defer.resolve({
type: 'cancel',
value: result
});
self._navigating = false;
self._execPendingRequest();
});
};
}
options.onWrapSubmit = function() { close('submit'); };
options.onWrapCancel = function() { close('cancel'); };
switch (options.type) {
case 'alert':
@ -229,7 +230,17 @@ define(function(require) {
break;
}
this._navigating = true;
DialogManager.open(dialog, options);
opening = DialogManager.open(dialog, options);
opening.catch(function(error) {
// A missing host API must not lock every later Settings dialog.
dialogDOM.classList.remove('current');
dialogDOM.hidden = true;
var overlay = document.querySelector('.settings-dialog-overlay');
if (overlay) overlay.hidden = true;
self._navigating = false;
defer.reject(error);
self._execPendingRequest();
});
}
}

View file

@ -246,6 +246,17 @@ define(function(require) {
// directly
Settings._currentPanel = '#' + panelId;
callback();
}).catch(function(error) {
console.error('Settings panel ' + panelId + ':', error);
newPanelElement.classList.remove('current');
if (currentPanelElement) currentPanelElement.classList.add('current');
// Release the navigation lock even when a hardware query fails.
callback(error);
if (window.VulpesGaiaUI) {
window.VulpesGaiaUI.notice(document.documentElement.lang.indexOf('fr') === 0 ?
'Ce panneau n’a pas pu être chargé. Vous pouvez revenir aux autres paramètres.' :
'This panel could not be loaded. You can return to the other settings.');
}
});
});
});

View file

@ -103,9 +103,10 @@ define(function(require) {
* @access public
*/
_onAppChoose: function pl__onAppChoose(evt) {
if (evt.target.dataset && evt.target.dataset.appIndex) {
var target = evt.target.closest('[data-app-index]');
if (target && this._listRoot.contains(target)) {
SettingsService.navigate('appPermissions-details', {
app: this._apps[evt.target.dataset.appIndex],
app: this._apps[target.dataset.appIndex],
permissionsTable: this._permissionTable
});
}
@ -262,7 +263,7 @@ define(function(require) {
* @return {Bool}
*/
_isExplicitPerm: function pl_isExplicitPerm(perm) {
return mozPerms.isExplicit(perm, this._currentApp.manifestURL,
return mozPerms && mozPerms.isExplicit(perm, this._currentApp.manifestURL,
this._currentApp.origin, false);
},

View file

@ -21,7 +21,7 @@ define(function(require) {
* refer to the JSON File Data.
*/
this._callSupportInfo = null;
var url = 'http://support.mozilla.org/products/firefox-os';
var url = 'https://vulpes-os.org/';
this._elements.userGuide.addEventListener('click', () => {
this._elements.userGuide.blur();
var activity = new window.MozActivity({
@ -55,7 +55,10 @@ define(function(require) {
.then(function loadSupportInfo(data) {
this._supportInfo = data;
callback(this._supportInfo);
}.bind(this));
}.bind(this)).catch(function(error) {
console.warn('Support information:', error);
callback({onlinesupport: {title: 'Vulpes OS', href: 'https://vulpes-os.org/'}, callsupport: []});
});
},
/**
* create support url items
@ -103,7 +106,7 @@ define(function(require) {
_renderSupportInfo: function support_renderSupportInfo(result) {
var onlineSupportInfo = null;
var onlineSupportTitle = result['support.onlinesupport.title'];
if (onlineSupportTitle !== '') {
if (typeof onlineSupportTitle === 'string' && onlineSupportTitle !== '') {
onlineSupportInfo = { title: onlineSupportTitle };
onlineSupportInfo.href = result['support.onlinesupport.href'];
this._setOnlineSupportInfo(onlineSupportInfo);
@ -114,7 +117,7 @@ define(function(require) {
// Check to see if we have a title for the first support number.
var callSupport1Title = result['support.callsupport1.title'];
// If we have a title we'll go ahead and load the href for it too.
if (callSupport1Title !== '') {
if (typeof callSupport1Title === 'string' && callSupport1Title !== '') {
this._callSupportInfo = [
{
'title': callSupport1Title,
@ -125,7 +128,7 @@ define(function(require) {
// Now check to see if we have a title for the second
// support number. If we do, we'll load it's href as well.
var callSupport2Title = result['support.callsupport2.title'];
if (callSupport2Title !== '') {
if (typeof callSupport2Title === 'string' && callSupport2Title !== '') {
this._callSupportInfo.push({
'title': callSupport2Title,
'href': result['support.callsupport2.href']

View file

@ -66,8 +66,11 @@ define(function(require) {
},
_bindEvents: function() {
this._elements.passcodeInput.onkeypress =
this._onPasscodeInputKeypress.bind(this);
this._elements.passcodeInput.type = 'password';
this._elements.passcodeInput.inputMode = 'numeric';
this._elements.passcodeInput.maxLength = 8;
this._elements.passcodeInput.oninput =
this._onPasscodeInput.bind(this);
this._elements.passcodeContainer.onclick =
this._onPasscodeContainerClick.bind(this);
},
@ -77,37 +80,14 @@ define(function(require) {
this._elements.passcodeInput.focus();
},
_onPasscodeInputKeypress: function(evt) {
// key code for key strokes from the keypad are 0 (numbers) and 8
// (backspace). Filter out the events that are not from the keypad.
var keyCode = evt.keyCode;
if (keyCode !== 0 && keyCode !== 8) {
return;
_onPasscodeInput: function() {
// Android IMEs commit text without legacy keypress/charCode events.
var value = this._elements.passcodeInput.value.replace(/[^0-9]/g, '').slice(0, 8);
this._hideErrorMessage();
if (this._elements.panel.dataset.passcodeStatus === 'success') {
this._resetPasscodeStatus();
}
evt.preventDefault();
if (this._passcodeBuffer === '') {
this._hideErrorMessage();
}
var code = evt.charCode;
if (code !== 0 && (code < 0x30 || code > 0x39)) {
return;
}
var key = String.fromCharCode(code);
if (evt.charCode === 0) {
if (this._passcodeBuffer.length > 0) {
this._passcodeBuffer = this._getPasscodeBuffer('all');
if (this._elements.panel.dataset.passcodeStatus === 'success') {
this._resetPasscodeStatus();
}
}
} else if (this._passcodeBuffer.length < 8) {
this._passcodeBuffer += key;
}
this._passcodeBuffer = value;
this._updatePassCodeUI();
this._processPasscode();
},
@ -134,6 +114,7 @@ define(function(require) {
},
_updatePassCodeUI: function() {
this._elements.passcodeInput.value = this._passcodeBuffer;
for (var i = 0; i < 8; i++) {
if (i < this._passcodeBuffer.length) {
this._elements.passcodeDigits[i].dataset.dot = true;

View file

@ -304,7 +304,7 @@ define(function(require) {
*/
_updateSimSettingsUI: function scm__updateSimSettingsUI() {
var firstCardInfo = this._simcards[0].getInfo();
var secondCardInfo = this._simcards[1].getInfo();
var secondCardInfo = (this._simcards[1] ? this._simcards[1].getInfo() : {absent: true});
var hidden = firstCardInfo.absent && secondCardInfo.absent ||
this._isAirplaneMode;
@ -322,7 +322,7 @@ define(function(require) {
*/
_updateSelectOptionsUI: function scm__updateSelectOptionsUI() {
var firstCardInfo = this._simcards[0].getInfo();
var secondCardInfo = this._simcards[1].getInfo();
var secondCardInfo = (this._simcards[1] ? this._simcards[1].getInfo() : {absent: true});
// two cards all are not absent, we have to update separately
if (!firstCardInfo.absent && !secondCardInfo.absent) {

View file

@ -41,7 +41,8 @@ define(function(require) {
var info = wifiManager.connectionInformation || {};
elements.ip.textContent = info.ipAddress || '';
document.l10n.setAttributes(elements.speed,
'linkSpeedMbs', { linkSpeed: info.linkSpeed });
Number.isFinite(info.linkSpeed) ? 'linkSpeedMbs' : 'unavailable',
Number.isFinite(info.linkSpeed) ? { linkSpeed: info.linkSpeed } : {});
}
});
};