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,205 @@
/* global SettingsListener, Service */
(function(exports) {
'use strict';
/**
* The Developer HUD displays app metrics as an overlay on top of MozApps.
* @class DeveloperHud
*/
function DeveloperHud() {
SettingsListener.observe('hud.hide', false, this.hide.bind(this));
SettingsListener.observe('devtools.overlay.system',
false, this.toggleSystemHUD.bind(this));
}
DeveloperHud.prototype = {
name: 'DeveloperHud',
start: function() {
window.addEventListener('developer-hud-update', this);
Service.register('stop', this);
},
stop: function() {
window.removeEventListener('developer-hud-update', this);
},
hide: function(hidden) {
var screen = document.getElementById('screen');
screen.classList.toggle('hidden-developer-hud', hidden);
},
_showSystemHUD: false,
toggleSystemHUD: function(enabled) {
if (!enabled) {
this.display(window, {});
}
this._showSystemHUD = enabled;
},
handleEvent: function(e) {
this.display(e.target, e.detail);
e.preventDefault();
},
/**
* Display a HUD over an app to show its metrics' values.
* @param {Object} target The iframe of the app being tracked.
* @param {Object} data The metric values to display.
*/
display: function(target, data) {
if (!target) {
return;
}
// For regular mozbrowser iframes use the div container.
var appwindow = target.parentElement;
// For system messages, directly insert the hud into the #screen
// wrapper if needed.
if (!appwindow) {
if (target === window && this._showSystemHUD) {
appwindow = document.getElementById('screen');
} else {
return;
}
}
var overlay = appwindow.querySelector(':scope > .developer-hud');
if (!overlay) {
overlay = document.createElement('div');
overlay.classList.add('developer-hud');
appwindow.appendChild(overlay);
}
if (!data.metrics || data.metrics.length < 1) {
overlay.remove();
return;
}
var canvas = overlay.querySelector('.widgets');
if (!canvas) {
canvas = document.createElement('canvas');
canvas.classList.add('widgets');
canvas.height = 30;
overlay.appendChild(canvas);
}
// The width is always set to the size of the screen in case the screen
// has been rotated. This does not cause a reflow as long as the width
// is the same.
canvas.width = window.innerWidth;
var ctx = canvas.getContext('2d');
ctx.font = '18px sans-serif';
ctx.textBaseline = 'top';
ctx.save();
// Widgets are positioned starting from the right side of the screen.
ctx.translate(canvas.width, 0);
data.metrics.reverse().forEach((function(metric) {
var widget = this.widget(metric);
if (!widget) {
return;
}
// The size of a widget is comprise between 30px and the size of its
// content. There is also an additional padding of 5px on each side.
var textWidth = ctx.measureText(widget.value).width;
var widgetWidth = Math.max(30, textWidth) + (5 * 2);
// Position the widget relatively to the last position on the left
// of the screen.
ctx.translate(-widgetWidth, 0);
// Fill widget background-color.
ctx.fillStyle = widget.color;
ctx.fillRect(0, 0, widgetWidth, canvas.height);
// Draw widget text centered both horizontally and vertically.
ctx.fillStyle = 'white';
ctx.fillText(widget.value,
(widgetWidth - textWidth) / 2,
canvas.height / 4);
}).bind(this));
ctx.restore();
},
widget: function(metric) {
var value = metric.value;
if (!value) {
return null;
}
var color;
switch(metric.name) {
case 'warnings':
color = 'orange';
break;
case 'errors':
color = 'red';
break;
case 'security':
color = 'black';
break;
case 'reflows':
color = 'purple';
break;
case 'jank':
color = 'cornflowerblue';
value += 'ms';
break;
case 'uss':
color = 'dimgrey';
value = this.formatMemory(value);
break;
case 'memory':
color = 'lightslategrey';
value = this.formatMemory(value);
break;
default:
color = this.colorHash(metric.name);
break;
}
return {
'color': color,
'value': value
};
},
colorHash: function(name) {
var hue = 0;
for (var i = 0; i < name.length; i++) {
hue += name.charCodeAt(i);
}
return 'hsl(' + (hue % 360) + ', 75%, 50%)';
},
formatMemory: function(bytes) {
var prefix = ['','K','M','G','T','P','E','Z','Y'];
var i = 0;
for (; bytes > 1024 && i < prefix.length; ++i) {
bytes /= 1024;
}
return (Math.round(bytes * 100) / 100) + ' ' + prefix[i] + 'B';
}
};
exports.DeveloperHud = DeveloperHud;
}(window));

View file

@ -0,0 +1,167 @@
'use strict';
/* global Service, DevToolsAuthDialog */
(function(exports) {
/**
* DevToolsAuth displays a prompt asking the user to scan a QR code as part of
* pairing the device with a machine running Firefox desktop. This takes
* place when debugging via WiFi to exchange keys.
* @requires DevToolsAuthDialog
* @requires ScreenManager
* @class DevToolsAuth
*/
function DevToolsAuth() {
window.addEventListener('mozChromeEvent', this);
}
DevToolsAuth.prototype = {
/**
* General event handler interface.
* Displays the pair dialog when needed.
* @memberof DevToolsAuth.prototype
* @param {DOMEvent} evt The event.
*/
handleEvent: function(e) {
if (e.detail.type !== 'devtools-auth') {
return;
}
if (e.detail.action === 'start') {
this.start();
}
if (e.detail.action === 'capture') {
this.capture();
}
if (e.detail.action === 'stop') {
this.stop();
}
},
get dialog() {
if (!this._dialog) {
this._dialog = new DevToolsAuthDialog({
onHide: this.onHide.bind(this)
});
}
return this._dialog;
},
get canvas() {
if (!this._canvas) {
this._canvas = document.createElement('canvas');
}
return this._canvas;
},
/**
* Begin capturing images that DevTools will attempt to find a QR code in
* for authentication.
* @memberof DevToolsAuth.prototype
*/
start: function() {
// We want the user's attention, so we need to turn the screen on if it's
// off.
Service.request('turnScreenOn');
this.dialog.show();
var getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia;
getUserMedia = getUserMedia.bind(navigator);
getUserMedia({
video: {
width: { min: 480 },
height: { min: 480 }
},
audio: false
}, function(stream) {
var video = this.dialog.video;
video.addEventListener('loadedmetadata', function onMetadata() {
video.removeEventListener('loadedmetadata', onMetadata);
this._resizeCanvas();
this.capture();
}.bind(this));
video.mozSrcObject = stream;
video.play();
}.bind(this), function(e) {
console.error('Unable to access camera: ' + e);
});
},
/**
* Capture another image and send to DevTools.
* @memberof DevToolsAuth.prototype
*/
capture: function() {
var video = this.dialog.video;
var canvas = this.canvas;
var context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
var url = canvas.toDataURL('image/jpeg');
this._checkImage(url);
},
/**
* An image was successfully decoded, so further capture can stop.
* @memberof DevToolsAuth.prototype
*/
stop: function() {
this.dialog.hide('complete');
},
/**
* Dialog is now hidden, perform cleanup.
* @memberof DevToolsAuth.prototype
*/
onHide: function(reason) {
var video = this.dialog.video;
video.pause();
if (video.mozSrcObject) {
video.mozSrcObject.stop();
video.mozSrcObject = null;
}
this._canvas = null;
if (reason !== 'complete') {
this.abort();
}
},
/**
* User chose to abort the auth process.
* @memberof DevToolsAuth.prototype
*/
abort: function() {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true,
{ type: 'devtools-auth',
action: 'abort' });
window.dispatchEvent(event);
},
_resizeCanvas: function() {
var video = this.dialog.video;
this.canvas.width = video.videoWidth;
this.canvas.height = video.videoHeight;
},
/**
* Dispatches an event to check a possible pairing candidate image.
* @memberof DevToolsAuth.prototype
* @param {String} url Object URL of the image to check
*/
_checkImage: function(url) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true,
{ type: 'devtools-auth',
action: 'capture',
url: url });
window.dispatchEvent(event);
}
};
exports.DevToolsAuth = DevToolsAuth;
}(window));

View file

@ -0,0 +1,53 @@
'use strict';
(function(exports) {
var DevToolsAuthDialog = function DevToolsAuthDialog(options) {
window.SystemDialog.call(this, options);
};
DevToolsAuthDialog.prototype = Object.create(window.SystemDialog.prototype);
DevToolsAuthDialog.prototype.customID = 'devtools-auth-dialog';
DevToolsAuthDialog.prototype.DEBUG = false;
DevToolsAuthDialog.prototype.view = function() {
return '<div id="' + this.instanceID + '" role="dialog" ' +
'class="generic-dialog"' +
'data-z-index-level="devtools-auth-dialog" hidden>' +
'<div class="container">' +
'<header data-l10n-id="devtools-auth-scan2"></header>' +
'<div class="video-container">' +
'<video></video>' +
'</div>' +
'<menu data-items="1">' +
'<button data-l10n-id="cancel" type="cancel"></button>' +
'</menu>' +
'</div>' +
'</div>';
};
// Get all elements when inited.
DevToolsAuthDialog.prototype._fetchElements = function() {
this.video =
document.querySelector('#devtools-auth-dialog video');
this.videoContainer =
document.querySelector('#devtools-auth-dialog .video-container');
this.buttonCancel =
document.querySelector('#devtools-auth-dialog button[type="cancel"]');
};
// Register events when all elements are got.
DevToolsAuthDialog.prototype._registerEvents = function() {
this.buttonCancel.onclick = this.cancelHandler.bind(this);
};
DevToolsAuthDialog.prototype.cancelHandler = function() {
this.hide('cancel');
return true;
};
exports.DevToolsAuthDialog = DevToolsAuthDialog;
}(window));

View file

@ -0,0 +1,307 @@
/* global dump,
ModalDialog,
MozActivity,
Notification,
NotificationHelper,
Service
*/
(function(exports) {
'use strict';
const DEBUG = false;
/**
* This developer system module captures a snapshot of the current device
* logs as displayed by logcat using DeviceStorage to persist the file for
* future access. It communicates with gecko code running in
* b2g/chrome/content/shell.js using a SystemAppProxy custom event based API.
* It requires the preference 'devtools.logshake.enabled' to be enabled
*
* @class LogShake
*/
function LogShake() {
}
function debug(str) {
if (DEBUG) {
dump('LogShake: ' + str + '\n');
}
}
LogShake.prototype = {
/**
* Start existing, observing for capture-logs events caused by Gecko
* LogShake
*/
start: function() {
Service.request('handleSystemMessageNotification', 'logshake', this);
window.addEventListener('volumeup+volumedown', this);
this.startCaptureLogsListener();
},
/**
* Stop the component, removing all listeners if necessary
*/
stop: function() {
Service.request('unhandleSystemMessageNotification', 'logshake', this);
this.stopCaptureLogsListener();
},
startCaptureLogsListener: function() {
debug('starting captureLogs listener');
window.addEventListener('capture-logs-start', this);
window.addEventListener('capture-logs-success', this);
window.addEventListener('capture-logs-error', this);
},
stopCaptureLogsListener: function() {
debug('stopping captureLogs listener');
window.removeEventListener('capture-logs-start', this);
window.removeEventListener('capture-logs-success', this);
window.removeEventListener('capture-logs-error', this);
},
/**
* Handle a capture-logs-start, capture-logs-success or capture-logs-error
* event, dispatching to the appropriate handler
*/
handleEvent: function(event) {
debug('handling event ' + event.type);
switch(event.type) {
case 'volumeup+volumedown':
this.requestSystemLogs();
break;
case 'capture-logs-start':
this.handleCaptureLogsStart(event);
break;
case 'capture-logs-success':
this.handleCaptureLogsSuccess(event);
break;
case 'capture-logs-error':
this.handleCaptureLogsError(event);
break;
}
},
_shakeId: null,
handleCaptureLogsStart: function(event) {
debug('handling capture-logs-start');
this._shakeId = Date.now();
this._notify('logsSaving');
},
requestSystemLogs: function() {
window.dispatchEvent(new CustomEvent('requestSystemLogs'));
},
/**
* Handle an event of type capture-logs-success where event.detail.logPaths
* is an array of absolute paths to the saved log files.
*/
handleCaptureLogsSuccess: function(event) {
debug('handling capture-logs-success');
navigator.vibrate(100);
this._notify('logsSavedHeader', 'logsSavedBody',
this.triggerShareLogs.bind(this, event.detail.logPaths),
event.detail);
this._shakeId = null;
},
handleCaptureLogsError: function(event) {
debug('handling capture logs error');
var error = event ? event.detail.error : '';
var errorMsg = this.formatError(error);
this._notify('logsSaveError', errorMsg,
this.showErrorMessage.bind('logsSaveError', this, error),
event.detail);
this._shakeId = null;
},
getDeviceStorage: function() {
var storageName = 'sdcard';
var storages = navigator.getDeviceStorages(storageName);
for (var i = 0; i < storages.length; i++) {
if (storages[i].storageName === storageName) {
return storages[i];
}
}
return navigator.getDeviceStorage('sdcard');
},
triggerShareLogs: function(logPaths, notif) {
var logFiles = [];
var storage = this.getDeviceStorage();
var requestsRemaining = logPaths.length;
var self = this;
function onSuccess() {
/* jshint validthis: true */
logFiles.push(this.result);
requestsRemaining -= 1;
if (requestsRemaining === 0) {
var logNames = logFiles.map(function(file) {
// For some reason file.name contains the full path despite
// File's documentation explicitly stating the opposite.
var pathComponents = file.name.split('/');
return pathComponents[pathComponents.length - 1];
});
var activity = new MozActivity({
name: 'share',
data: {
type: 'application/vnd.moz-systemlog',
blobs: logFiles,
filenames: logNames
}
});
activity.onsuccess = function() {
if (!notif) {
return;
}
if ('close' in notif) {
notif.close();
} else {
self.closeSystemMessageNotification(notif);
}
};
}
}
// Only report error one time.
var inError = false;
function onError() {
if (!inError) {
inError = true;
/* jshint validthis: true */
self.showErrorMessage('logsShareError', this.error);
}
}
for (var logPath of logPaths) {
var req = storage.get(logPath);
req.onsuccess = onSuccess;
req.onerror = onError;
}
},
ERRNO_TO_MSG: {
0: 'logsGenericError',
30: 'logsSDCardMaybeShared', // errno: filesystem ro-mounted
'NotFoundError': 'logsNotFoundError'
},
formatError: function(error) {
if (typeof error === 'string') {
return error;
}
if (typeof error === 'object') {
if ('operation' in error) {
return { id: 'logsOperationFailed',
args: { operation: error.operation }
};
}
if ('name' in error && error.name === 'NotFoundError') {
return 'logsNotFoundError';
}
}
return '';
},
showErrorMessage: function(title, error, notif) {
if (notif) {
notif.close();
}
// Do nothing for error string
if (typeof error === 'string') {
return;
}
if (typeof error !== 'object') {
console.warn('Unexpected error type: ' + typeof error);
return;
}
var errorKey;
// Small heuristic for some frequent unix error cases
if ('unixErrno' in error) {
errorKey = error.unixErrno;
} else if ('name' in error) {
errorKey = error.name;
}
debug('errorKey: ' + errorKey);
// Gracefully fallback to a generic error messages if we don't know
// this errno code.
if (!this.ERRNO_TO_MSG[errorKey]) {
errorKey = 0;
}
ModalDialog.alert(title, this.ERRNO_TO_MSG[errorKey], { title: 'ok' });
},
_notify: function(titleL10n, bodyL10n, onclick, dataPayload) {
var payload = {
bodyL10n: bodyL10n,
icon: '/style/notifications/images/bug.png',
tag: 'logshake:' + this._shakeId,
data: {
systemMessageTarget: 'logshake',
logshakePayload: dataPayload || undefined
}
};
NotificationHelper.send(titleL10n, payload).then(notification => {
if (onclick) {
notification.onclick = onclick.bind(this, notification);
}
});
},
handleSystemMessageNotification: function(message) {
debug('Received system message: ' + JSON.stringify(message));
if (!('logshakePayload' in message.data)) {
console.warn('Received logshake system message notification without ' +
'payload, silently discarding.');
return;
}
debug('Message payload: ' + message.data.logshakePayload);
var payload = message.data.logshakePayload;
if ('error' in payload) {
this.showErrorMessage('logsSaveError', payload.error);
} else if ('logPaths' in payload) {
this.triggerShareLogs(payload.logPaths, message);
} else {
console.warn('Unidentified payload: ' + JSON.stringify(payload));
}
},
closeSystemMessageNotification: function(msg) {
Notification.get({ tag: msg.tag }).then(notifs => {
notifs.forEach(notif => {
if (notif.tag) {
// Close notification with the matching tag
if (notif.tag === msg.tag) {
notif.close && notif.close();
}
}
});
});
}
};
exports.LogShake = LogShake;
// XXX: See issue described in screenshot.js
exports.logshake = new LogShake();
exports.logshake.start();
})(window);

View file

@ -0,0 +1,160 @@
'use strict';
/* global Service, ModalDialog */
(function(exports) {
/**
* Note: Values copied from Gecko:
* toolkit/devtools/security/auth.js
*/
var AuthenticationResult = {
/**
* Close all listening sockets, and disable them from opening again.
*/
DISABLE_ALL: 'DISABLE_ALL',
/**
* Deny the current connection.
*/
DENY: 'DENY',
/**
* Additional data needs to be exchanged before a result can be determined.
*/
PENDING: 'PENDING',
/**
* Allow the current connection.
*/
ALLOW: 'ALLOW',
/**
* Allow the current connection, and persist this choice for future
* connections from the same client. This requires a trustable mechanism to
* identify the client in the future, such as the cert used during OOB_CERT.
*/
ALLOW_PERSIST: 'ALLOW_PERSIST'
};
/**
* RemoteDebugger displays a prompt asking the user if they want to enable
* remote debugging on their device. This is generally called when the user
* attempts to access the device from the App Manager.
* @requires ModalDialog
* @requires Service
* @class RemoteDebugger
*/
function RemoteDebugger() {
window.addEventListener('mozChromeEvent', this);
}
RemoteDebugger.prototype = {
/**
* General event handler interface.
* Displays the modal dialog when we needed.
* @memberof RemoteDebugger.prototype
* @param {DOMEvent} evt The event.
*/
handleEvent: function(e) {
if (e.detail.type !== 'remote-debugger-prompt') {
return;
}
// We want the user attention, so we need to turn the screen on
// if it's off.
Service.request('turnScreenOn');
var session = e.detail.session;
var dialog;
if (!session.server.port) {
dialog = this._buildUSBDialog(session);
} else {
dialog = this._buildTCPDialog(session);
}
if (!dialog) {
// Session does not meet the requirements of this dialog, so deny the
// connection immediately.
this._dispatchEvent(AuthenticationResult.DENY);
return;
}
// Reusing the ModalDialog infrastructure.
ModalDialog.showWithPseudoEvent({
type: 'selectone',
title: dialog.title,
text: dialog.options,
callback: this._dispatchEvent.bind(this),
cancel: this._dispatchEvent.bind(this)
});
},
_buildUSBDialog: function(session) {
var dialog = {};
if (session.authentication !== 'PROMPT') {
// This dialog is not prepared for any other authentication method at
// this time.
return false;
}
dialog.title = 'remoteDebuggerPromptUSB';
dialog.options = [
{
id: AuthenticationResult.ALLOW,
text: 'remoteDebuggerPrompt-allow'
},
{
id: AuthenticationResult.DENY,
text: 'remoteDebuggerPrompt-deny'
}
];
return dialog;
},
_buildTCPDialog: function(session) {
var dialog = {};
if (session.authentication !== 'OOB_CERT' || !session.client.cert) {
// This dialog is not prepared for any other authentication method at
// this time.
return false;
}
dialog.title = {
id: 'remoteDebuggerPromptTCP2',
args: {
host: session.client.host,
port: session.client.port
}
};
dialog.options = [
{
id: AuthenticationResult.ALLOW,
text: 'remoteDebuggerPrompt-scan'
},
{
id: AuthenticationResult.ALLOW_PERSIST,
text: 'remoteDebuggerPrompt-scanAndRemember'
}
];
return dialog;
},
/**
* Dispatches an event based on the user selection of the modal dialog.
* @memberof RemoteDebugger.prototype
* @param {String} id ID of option chosen, or null if cancelled.
*/
_dispatchEvent: function(id) {
var authResult = id || AuthenticationResult.DENY;
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true,
{ type: 'remote-debugger-prompt',
authResult: authResult });
window.dispatchEvent(event);
}
};
exports.RemoteDebugger = RemoteDebugger;
}(window));