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,838 @@
'use strict';
/* global SettingsListener, LazyLoader, Service, SettingsHelper */
/* global AccessibilityQuicknavMenu */
(function(exports) {
/**
* Accessibility enables and disables the screenreader after the user
* gestures using the hardware buttons of the phone. To toggle the setting.
* the user must press volume up, then volume down three times in a row.
* @class Accessibility
* @requires SettingsListener
*/
function Accessibility() {}
Accessibility.prototype = {
/**
* How fast the autorepeat is.
* @type {Number}
* @memberof Accessibility.prototype
*/
REPEAT_INTERVAL: 600000,
/**
* Maximum interval between initial and final TOGGLE_SCREEN_READER_COUNT
* volume button presses.
* @type {Number}
* @memberof Accessibility.prototype
*/
REPEAT_BUTTON_PRESS: 15000000,
/**
* Number of times the buttons need to be pressed before the screen reader
* setting is toggled.
* @type {Number}
* @memberof Accessibility.prototype
*/
TOGGLE_SCREEN_READER_COUNT: 6,
/**
* Cap the full range of contrast. Actual -1 is completely gray, and 1
* makes things hard to see. This value is the max/min contrast.
*/
CONTRAST_CAP: 0.6,
/**
* Timeout (in milliseconds) between when a vc-change event fires
* and when interaction hints (if any) are spoken
*/
HINTS_TIMEOUT: 2000,
/**
* Default timeout value (in milliseconds) between the launch of the FTU and
* the time when screen reader instructions should be spoken.
* @type {Number}
*/
FTU_STARTED_TIMEOUT: 15000,
/**
* Current counter for button presses in short succession.
* @type {Number}
* @memberof Accessibility.prototype
*/
counter: 0,
/**
* Next expected event.
* @type {Object}
* @memberof Accessibility.prototype
*/
expectedEvent: {
type: 'volumeup',
timeStamp: 0
},
/**
* Expected complete time stamp.
* @type {Number}
* @memberof Accessibility.prototype
*/
expectedCompleteTimeStamp: 0,
/**
* Accessibility settings to be observed.
* @type {Object} name: value pairs.
* @memberof Accessibility.prototype
*/
settings: {
'accessibility.screenreader': false,
'accessibility.screenreader-volume': 1,
'accessibility.screenreader-rate': 0,
'accessibility.screenreader-captions': false,
'accessibility.screenreader-shade': false,
'accessibility.screenreader-ftu-timeout-seconds': 15,
'accessibility.screenreader-fallback-lang': 'en-US',
'accessibility.colors.enable': false,
'accessibility.colors.invert': false,
'accessibility.colors.grayscale': false,
'accessibility.colors.contrast': '0.0'
},
/**
* Audio used by the screen reader.
* Note: Lazy-loaded when first needed
* @type {Object}
* @memberof Accessibility.prototype
*/
sounds: {
clickedAudio: null,
vcKeyAudio: null,
vcMoveAudio: null,
noMoveAudio: null
},
/**
* URLs for screen reader audio files.
* @type {Object}
* @memberof Accessibility.prototype
*/
soundURLs: {
clickedAudio: './resources/sounds/screen_reader_clicked.ogg',
vcKeyAudio: './resources/sounds/screen_reader_virtual_cursor_key.ogg',
vcMoveAudio: './resources/sounds/screen_reader_virtual_cursor_move.ogg',
noMoveAudio: './resources/sounds/screen_reader_no_move.ogg'
},
/**
* Start listening for events.
* @memberof Accessibility.prototype
*/
start: function ar_init() {
this.screen = document.getElementById('screen');
this.speechSynthesizer = speechSynthesizer;
window.addEventListener('volumeup', this);
window.addEventListener('volumedown', this);
window.addEventListener('logohidden', this);
window.addEventListener('screenchange', this);
window.addEventListener('iac-ftucomms', this);
this.FTUStartedTimeout = null;
this.appOpening = false;
// Attach all observers.
Object.keys(this.settings).forEach(function attach(settingKey) {
SettingsListener.observe(settingKey, this.settings[settingKey],
function observe(aValue) {
var oldValue = this.settings[settingKey];
this.settings[settingKey] = aValue;
switch (settingKey) {
case 'accessibility.screenreader':
if (aValue) {
this.setToSupportedLanguage();
window.addEventListener('mozChromeEvent', this);
window.addEventListener('appwillopen', this);
window.addEventListener('appopened', this);
window.addEventListener('homescreenopening', this);
window.addEventListener('homescreenopened', this);
} else {
window.removeEventListener('mozChromeEvent', this);
window.removeEventListener('appwillopen', this);
window.removeEventListener('appopened', this);
window.removeEventListener('homescreenopening', this);
window.removeEventListener('homescreenopened', this);
}
if (this.settings['accessibility.screenreader-shade']) {
this.toggleShade(aValue, !aValue);
}
this.screen.classList.toggle('screenreader', aValue);
break;
case 'accessibility.colors.enable':
SettingsListener.getSettingsLock().set({
'layers.effect.invert': aValue ?
this.settings['accessibility.colors.invert'] : false,
'layers.effect.grayscale': aValue ?
this.settings['accessibility.colors.grayscale'] : false,
'layers.effect.contrast': aValue ?
this.settings['accessibility.colors.contrast'] *
this.CONTRAST_CAP : '0.0'
});
break;
case 'accessibility.screenreader-captions':
this.speechSynthesizer.captions = aValue;
// If captions are displayed hide them.
if (!aValue) {
this.speechSynthesizer.hideSpeech(true);
}
break;
case 'accessibility.screenreader-shade':
if (this.settings['accessibility.screenreader']) {
this.toggleShade(aValue, oldValue === aValue);
}
break;
case 'accessibility.colors.invert':
case 'accessibility.colors.grayscale':
case 'accessibility.colors.contrast':
if (this.settings['accessibility.colors.enable']) {
var effect = settingKey.split('.').pop();
var gfxSetting = {};
if (effect === 'contrast') {
gfxSetting['layers.effect.contrast'] =
aValue * this.CONTRAST_CAP;
} else {
gfxSetting['layers.effect.' + effect] = aValue;
}
SettingsListener.getSettingsLock().set(gfxSetting);
}
break;
case 'accessibility.screenreader-ftu-timeout-seconds':
this.FTU_STARTED_TIMEOUT = 1000 * aValue;
break;
}
}.bind(this));
}, this);
},
/**
* Reset the expected event to defaults.
* @memberof Accessibility.prototype
*/
reset: function ar_resetEvent() {
this.expectedEvent = {
type: 'volumeup',
timeStamp: 0
};
this.counter = 0;
},
/**
* Unset speaking flag and set the expected complete time stamp.
* @param {?Number} aExpectedCompleteTimeStamp Expected complete time
* stamp.
* @memberof Accessibility.prototype
*/
resetSpeaking: function ar_resetSpeaking(aExpectedCompleteTimeStamp) {
this.isSpeaking = false;
this.expectedCompleteTimeStamp = aExpectedCompleteTimeStamp || 0;
},
toggleShade: function ar_toggleShage(aEnable, aSilent) {
Service.request(aEnable ? 'turnShadeOn' : 'turnShadeOff');
if (!aSilent) {
this.speak({ string: aEnable ? 'shadeToggleOn' : 'shadeToggleOff' },
{ enqueue: true });
}
},
/**
* Checks that device language is supported in text to speech, if not
* it is set to a predetermined fallback language.
* @memberof Accessibility.prototype
*/
setToSupportedLanguage: function ar_setToSupportedLanguage() {
var settingsHelper = SettingsHelper('language.current');
var voices = this.speechSynthesizer.speech.getVoices();
var speechLangs = new Set(voices.map(v => v.lang.split('-')[0]));
settingsHelper.get((value) => {
if (!speechLangs.has(value.split('-')[0])) {
settingsHelper.set(
this.settings['accessibility.screenreader-fallback-lang']);
}
});
},
/**
* Handle volumeup and volumedown events generated from HardwareButtons.
* @param {Object} aEvent a high-level key event object generated from
* HardwareButtons.
* @memberof Accessibility.prototype
*/
handleVolumeButtonPress: function ar_handleVolumeButtonPress(aEvent) {
var type = aEvent.type;
var timeStamp = aEvent.timeStamp;
var expectedEvent = this.expectedEvent;
if (type !== expectedEvent.type || timeStamp > expectedEvent.timeStamp) {
this.reset();
if (type !== 'volumeup') {
return;
}
}
this.expectedEvent = {
type: type === 'volumeup' ? 'volumedown' :
'volumeup',
timeStamp: timeStamp + this.REPEAT_INTERVAL
};
if (++this.counter < this.TOGGLE_SCREEN_READER_COUNT) {
return;
}
this.reset();
this.disableFTUStartedTimeout();
if (!this.isSpeaking && timeStamp > this.expectedCompleteTimeStamp) {
this.cancelSpeech();
this.announceScreenReader()
.then(() => this.resetSpeaking(timeStamp + this.REPEAT_BUTTON_PRESS))
.catch(err => console.error(err));
return;
}
this.cancelSpeech();
this.resetSpeaking();
SettingsListener.getSettingsLock().set({
'accessibility.screenreader':
!this.settings['accessibility.screenreader']
});
},
/**
* Announce screen reader FTU_STARTED_TIMEOUT milliseconds after the FTU is
* loaded if the user does not proceed beyond the first step.
* @param {Object} aEvent an event object generated from FTU launcher.
*/
handleFTUStarted: function ar_handleFTUStarted(aEvent) {
if (this.settings['accessibility.screenreader']) {
// Only set the FTU timeout if the screen reader is not enabled.
return;
}
this.FTUStartedTimeout = setTimeout(() => {
this.cancelSpeech();
this.reset();
this.announceScreenReader()
.then(() => this.resetSpeaking(aEvent.timeStamp +
this.REPEAT_BUTTON_PRESS + this.FTU_STARTED_TIMEOUT * 1000))
.catch(err => console.error(err));
}, this.FTU_STARTED_TIMEOUT);
},
/**
* Disable a timeout before the screen reader starts speaking in FTU.
*/
disableFTUStartedTimeout: function ar_disableFTUStartedTimeout() {
clearTimeout(this.FTUStartedTimeout);
this.FTUStartedTimeout = null;
},
/**
* Reset FTU timeout and stop any spoken instructions if the user steps to
* the next FTU screen.
*/
handleFTUStep: function ar_handleFTUStep() {
if (!this.FTUStartedTimeout) {
// we've not got the started event yet
return;
}
this.disableFTUStartedTimeout();
this.cancelSpeech();
this.reset();
window.removeEventListener('iac-ftucomms', this);
},
/**
* Play audio for a screen reader notification.
* @param {String} aSoundKey a key for the screen reader audio.
* XXX: When Bug 848954 lands we should be able to use Web Audio API.
* @memberof Accessibility.prototype
*/
_playSound: function ar__playSound(aSoundKey) {
if (!this.sounds[aSoundKey]) {
this.sounds[aSoundKey] = new Audio(this.soundURLs[aSoundKey]);
this.sounds[aSoundKey].load();
}
var audio = this.sounds[aSoundKey].cloneNode(false);
audio.volume = this.volume;
audio.play();
},
/**
* Get current screen reader volume defined by the setting.
* @return {Number} Screen reader volume wihtin the [0, 1] interval.
* @memberof Accessibility.prototype
*/
get volume() {
return this.settings['accessibility.screenreader-volume'];
},
/**
* Get current screen reader speech rate defined by the setting.
* @return {Number} Screen reader rate within the [0.2, 10] interval.
* @memberof Accessibility.prototype
*/
get rate() {
var rate = this.settings['accessibility.screenreader-rate'];
return rate >= 0 ? rate + 1 : 1 / (Math.abs(rate) + 1);
},
/**
* Start a timeout that waits to display hints
* @memberof Accessibility.prototype
*/
setHintsTimeout: function ar_setHintsTimeout(aHints) {
clearTimeout(this.hintsTimer);
this.hintsTimer = setTimeout(function onHintsTimeout() {
this.isSpeakingHints = true;
this.speak(aHints, { enqueue: true })
.then(() => this.isSpeakingHints = false)
.catch(err => console.error(err));
}.bind(this), this.HINTS_TIMEOUT);
},
/**
* Handle accessfu mozChromeEvent.
* @param {Object} accessfu details object.
* @memberof Accessibility.prototype
*/
handleAccessFuOutput: function ar_handleAccessFuOutput(aDetails) {
this.cancelHints();
var options = aDetails.options || {};
var type = aDetails.eventType;
window.dispatchEvent(new CustomEvent('accessibility-action'));
if (this.appOpening) {
if (type === 'vc-change') {
this.lastVCChangeDetails = aDetails;
}
return;
}
switch (type) {
case 'vc-change':
// Vibrate when the virtual cursor changes.
navigator.vibrate(options.pattern);
this._playSound(options.isKey ? 'vcKeyAudio' : 'vcMoveAudio');
break;
case 'action':
if (aDetails.data[0].string === 'clickAction') {
// If element is clicked, play 'click' sound instead of speech.
this._playSound('clickedAudio');
return;
}
break;
case 'no-move':
this._playSound('noMoveAudio');
return;
}
this.speak(aDetails.data, { enqueue: options.enqueue }).then(() => {
if (options.hints) {
this.setHintsTimeout(options.hints);
}
}).catch(err => console.error(err));
},
handleAccessFuControl: function ar_handleAccessFuControls(aDetails) {
this.cancelHints();
switch (aDetails.eventType) {
case 'quicknav-menu':
if (!this.quicknav) {
LazyLoader.load(['js/accessibility_quicknav_menu.js'])
.then(() => {
this.quicknav = new AccessibilityQuicknavMenu();
this.quicknav.show();
}).catch(err => console.error(err));
} else {
this.quicknav.show();
}
break;
case 'toggle-shade':
SettingsListener.getSettingsLock().set({
'accessibility.screenreader-shade':
!this.settings['accessibility.screenreader-shade']
});
window.dispatchEvent(new CustomEvent('accessibility-action'));
break;
case 'toggle-pause':
this.speechSynthesizer.togglePause();
break;
default:
break;
}
},
/**
* Listen for screen change events and stop speaking if the
* screen is disabled (in 'off' state)
* @memberof Accessibility.prototype
*/
handleScreenChange: function ar_handleScreenChange(aDetail){
if(!aDetail.screenEnabled){
this.cancelHints();
}
},
/**
* Remove aria-hidden from the screen element to make content accessible to
* the screen reader.
* @memberof Accessibility.prototype
*/
activateScreen: function ar_activateScreen() {
// Screen reader will not say anything until the splash animation is
// hidden and the aria-hidden attribute is removed from #screen.
this.screen.removeAttribute('aria-hidden');
window.removeEventListener('logohidden', this);
},
/**
* Set the flag indicating that the app is opening.
*/
prepareForApp: function ar_prepareForApp() {
this.appOpening = true;
},
/**
* Speak the name of the app and announce latest virtual cursor change.
* @param {Object} aDetail app_window object
*/
announceApp: function ar_announceApp(aDetail) {
this.speak(aDetail.name)
.then(() => {
this.appOpening = false;
if (this.lastVCChangeDetails) {
this.handleAccessFuOutput(this.lastVCChangeDetails);
delete this.lastVCChangeDetails;
}
})
.catch(err => console.error(err));
},
/**
* Handle event.
* @param {Object} aEvent mozChromeEvent/logohidden/volumeup/volumedown.
* @memberof Accessibility.prototype
*/
handleEvent: function ar_handleEvent(aEvent) {
switch (aEvent.type) {
case 'screenchange':
this.handleScreenChange(aEvent.detail);
break;
case 'logohidden':
this.activateScreen();
break;
case 'iac-ftucomms':
if (aEvent.detail === 'started') {
this.handleFTUStarted(aEvent);
} else if(aEvent.detail.type == 'step') {
this.handleFTUStep();
}
break;
case 'mozChromeEvent':
switch (aEvent.detail.type) {
case 'accessibility-output':
this.handleAccessFuOutput(JSON.parse(aEvent.detail.details));
break;
case 'accessibility-control':
this.handleAccessFuControl(JSON.parse(aEvent.detail.details));
break;
}
break;
case 'volumeup':
case 'volumedown':
this.handleVolumeButtonPress(aEvent);
break;
case 'appwillopen':
case 'homescreenopening':
this.prepareForApp();
break;
case 'appopened':
case 'homescreenopened':
this.announceApp(aEvent.detail);
break;
}
},
/**
* Check for Hints speech/timer and clear.
* @memberof Accessibility.prototype
*/
cancelHints: function ar_cancelHints() {
clearTimeout(this.hintsTimer);
if(this.isSpeakingHints){
this.cancelSpeech();
this.isSpeakingHints = false;
}
},
/**
* Based on whether the screen reader is currently enabled, announce the
* instructions of how to enable/disable it.
* @memberof Accessibility.prototype
*/
announceScreenReader: function ar_announceScreenReader() {
var enabled = this.settings['accessibility.screenreader'];
this.isSpeaking = true;
return this.speak({
string: enabled ? 'disableScreenReaderSteps' : 'enableScreenReaderSteps'
}, {enqueue: false});
},
/**
* Use speechSynthesis to speak screen reader utterances.
* @param {?Array} aData Speech data before it is localized.
* synthesis is completed.
* @param {?Object} aOptions = {} Speech options such as enqueue etc.
* @memberof Accessibility.prototype
*/
speak: function ar_speak(aData, aOptions = {}) {
return this.speechSynthesizer.speak(
aData, aOptions, this.rate, this.volume);
},
/**
* Cancel any utterances currently being spoken by speechSynthesis.
* @memberof Accessibility.prototype
*/
cancelSpeech: function ar_cancelSpeech() {
this.speechSynthesizer.cancel();
}
};
/**
* A speech synthesizer component that handles speech localization and
* pronunciation.
* @type {Object}
*/
var speechSynthesizer = {
/**
* A delay before hiding screen reader caption.
* @type {Number}
* @memberof speechSynthesizer
*/
CAPTIONS_DELAY: 1500,
/**
* A flag used for speech captions rendering.
* @type {Boolean}
* @memberof speechSynthesizer
*/
captions: false,
/**
* Screen container getter.
* @type {Object}
* @memberof speechSynthesizer
*/
get screen() {
delete this.screen;
this.screen = document.getElementById('screen');
return this.screen;
},
/**
* Speech Synthesis
* @type {Object}
* @memberof speechSynthesizer
*/
get speech() {
delete this.speech;
// If there are no voices bundled, consider speech synthesis unavailable.
if (!window.speechSynthesis ||
window.speechSynthesis.getVoices().length === 0) {
this.speech = null;
}
this.speech = window.speechSynthesis;
return this.speech;
},
/**
* Speech utterance
* @type {Object}
* @memberof speechSynthesizer
*/
get utterance() {
delete this.utterance;
this.utterance = window.SpeechSynthesisUtterance;
return this.utterance;
},
/**
* Cancle speech if the screen reader is speaking.
* @memberof speechSynthesizer
*/
cancel: function ss_cancel() {
if (this.speech) {
this.speech.cancel();
}
},
/**
* Localize speech data.
* @param {Object} aDetails Speech data object.
* @return {String} Localized speech data.
* @memberof speechSynthesizer
*/
localize: function ss_localize(aDetails) {
if (!aDetails || typeof aDetails === 'string') {
return aDetails;
}
var string = aDetails.string;
var data = {
count: aDetails.count
};
if (!string) {
return '';
} else {
string = 'accessibility-' + string;
}
if (aDetails.args) {
data = aDetails.args.reduce(function(aData, val, index) {
aData[index] = val;
return aData;
}, data);
}
return navigator.mozL10n.get(string, data);
},
/**
* Build a complete utterance string by localizing an array of speech data.
* @param {?Array} aData Speech data.
* @return {String} A complete localized string from speech array data.
* @memberof speechSynthesizer
*/
buildUtterance: function ss_buildUtterance(aData) {
if (!Array.isArray(aData)) {
aData = [aData];
}
var words = [], localize = this.localize;
aData.reduce(function(words, details) {
var localized = localize(details);
if (localized) {
var word = localized.trim();
if (word) {
words.push(word);
}
}
return words;
}, words);
return words.join(' ');
},
/**
* Show caption for the screen reader.
* @param {String} aUtterance Current screen reader caption.
* @memberof speechSynthesizer
*/
showSpeech: function ar_showSpeech(aUtterance) {
if (!this.captionsBox) {
this.captionsBox = document.createElement('div');
this.captionsBox.id = 'accessibility-captions-box';
this.captionsBox.setAttribute('data-z-index-level',
'accessibility-captions');
this.captionsBox.setAttribute('aria-hidden', true);
this.screen.appendChild(this.captionsBox);
}
window.clearTimeout(this.captionsHideTimeout);
this.captionsHideTimeout = null;
this.captionsBox.textContent = aUtterance;
this.captionsBox.classList.add('visible');
},
/**
* Hide current screen reader caption.
* @param {Boolean} aImmediately A flag to hide captionsBox immediately.
* @memberof speechSynthesizer
*/
hideSpeech: function ar_hideSpeech(aImmediately) {
if (!this.captionsBox) {
return;
}
// Hide the caption after CAPTIONS_DELAY.
if (aImmediately) {
this.captionsBox.classList.remove('visible');
} else {
this.captionsHideTimeout = window.setTimeout(function() {
this.captionsBox.classList.remove('visible');
}.bind(this), this.CAPTIONS_DELAY);
}
},
togglePause: function ss_togglePause() {
if (this.speech.paused) {
this.speech.resume();
} else if (this.speech.speaking) {
this.speech.pause();
}
},
/**
* Utter a message with a speechSynthesizer.
* @param {?Array} aData A messages array to be localized.
* @param {JSON} aOptions Options to be used when speaking. For example: {
* enqueue: false
* }
* @param {Number} aRate Speech rate.
* @param {Number} aVolume Speech volume.
* @memberof speechSynthesizer
*/
speak: function ss_speak(aData, aOptions, aRate, aVolume) {
if (!this.speech || !this.utterance) {
return Promise.resolve();
}
if (!aOptions.enqueue) {
this.cancel();
}
if (this.speech.paused) {
this.speech.resume();
}
var sentence = this.buildUtterance(aData);
if (!sentence) {
return Promise.resolve();
}
return new Promise(resolve => {
var utterance = new this.utterance(sentence);
utterance.volume = aVolume;
utterance.rate = aRate;
utterance.addEventListener('end', () => {
if (this.captions) {
this.hideSpeech();
}
resolve();
});
if (this.captions) {
this.showSpeech(sentence);
}
this.speech.speak(utterance);
});
}
};
exports.Accessibility = Accessibility;
}(window));