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,717 @@
/* API Summary:
stopSendingFile(in DOMString aDeviceAddress);
confirmReceivingFile(in DOMString aDeviceAddress, in bool aConfirmation); */
'use strict';
/* global MimeMapper, Service, LazyLoader,
MozActivity, NotificationHelper, UtilityTray */
/* exported BluetoothTransfer */
(function(exports) {
var BluetoothTransfer = {
name: 'BluetoothTransfer',
// The first-in-first-out queue maintain each scheduled sending task.
// Each element is a object for scheduled sending tasks.
_sendingFilesQueue: [],
_bindOnCancelTransferTask: null,
/**
* Debug message.
*
* @private
* @type {Boolean} turn on/off the console log
*/
onDebug: false,
bluetoothIcon: 'style/bluetooth_transfer/images/icon_bluetooth.png',
get _deviceStorage() {
return navigator.getDeviceStorage('sdcard');
},
get transferStatusList() {
var transferStatusList =
document.getElementById('bluetooth-transfer-status-list');
delete this.transferStatusList;
this.transferStatusList = transferStatusList;
return transferStatusList;
},
/**
* Initialize BluetoothTransfer module.
*
* Service request handleSystemMessageNotification is used to filter
* and relay BluetoothTransfer related notifications handle event after
* reboot. Now only `_onTransferComplete` method generate notifications
* that need to be handled after reboot.
*
* @public
*/
start: function bt_start() {
// Bind message handler for sending files from Bluetooth app
window.addEventListener('iac-bluetoothTransfercomms',
this._onFilesSending.bind(this)
);
// Bind message handler for transferring file callback
navigator.mozSetMessageHandler(
'bluetooth-opp-receiving-file-confirmation',
this.onReceivingFileConfirmation.bind(this)
);
// Listen to 'bluetooth-opp-transfer-start' from bluetooth.js
window.addEventListener('bluetooth-opp-transfer-start',
this._onUpdateProgress.bind(this, 'start')
);
navigator.mozSetMessageHandler('bluetooth-opp-update-progress',
this._onUpdateProgress.bind(this, 'progress')
);
// Listen to 'bluetooth-opp-transfer-complete' from bluetooth.js
window.addEventListener('bluetooth-opp-transfer-complete',
this._onTransferComplete.bind(this));
window.addEventListener('bluetooth-sendfile-via-handover',
this.sendFileViaHandover.bind(this));
Service.registerState('isSendFileQueueEmpty', this);
Service.registerState('isFileTransferInProgress', this);
Service.request('handleSystemMessageNotification',
'BluetoothTransfer', this);
this._bindOnCancelTransferTask = this.onCancelTransferTask.bind(this);
},
getDeviceName: function bt_getDeviceName(address) {
return new Promise((resolve) => {
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter === null) {
var msg = 'Since cannot get Bluetooth adapter, ' +
'resolve with an unknown device.';
this.debug(msg);
navigator.mozL10n.formatValue('unknown-device').then(resolve);
return;
}
// Service Class Name: OBEXObjectPush, UUID: 0x1105
// Specification: Object Push Profile (OPP)
// NOTE: Used as both Service Class Identifier and Profile.
// Allowed Usage: Service Class/Profile
// https://www.bluetooth.org/en-us/specification/assigned-numbers/
// service-discovery
var serviceUuid = '0x1105';
var req = adapter.getConnectedDevices(serviceUuid);
req.onsuccess = () => {
if (req.result) {
this.debug('got connectedList');
var connectedList = req.result;
var length = connectedList.length;
for (var i = 0; i < length; i++) {
if (connectedList[i].address == address) {
resolve(connectedList[i].name);
}
}
} else {
navigator.mozL10n.formatValue('unknown-device').then(resolve);
}
};
req.onerror = () => {
var msg = 'Can not check is device connected from adapter.';
this.debug(msg);
navigator.mozL10n.formatValue('unknown-device').then(resolve);
};
});
},
debug: function bt_debug(msg) {
if (!this.onDebug) {
return;
}
console.log('[System Bluetooth Transfer]: ' + msg);
},
humanizeSize: function bt_humanizeSize(bytes) {
var _ = navigator.mozL10n.get;
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var size, e;
if (bytes) {
e = Math.floor(Math.log(bytes) / Math.log(1024));
size = (bytes / Math.pow(1024, e)).toFixed(2);
} else {
e = 0;
size = '0.00';
}
return _('fileSize', {
size: size,
unit: _('byteUnit-' + units[e])
});
},
_onFilesSending: function bt__onFilesSending(evt) {
// Notify user that we are sending files
var icon = 'style/bluetooth_transfer/images/transfer.png';
NotificationHelper.send('transferHasStartedTitle', {
'bodyL10n': 'transferHasStartedDescription',
'icon': icon
}).then(function(notification) {
notification.addEventListener('click',
UtilityTray.show.bind(UtilityTray));
});
// Push sending files request in queue
var sendingFilesSchedule = evt.detail;
this._sendingFilesQueue.push(sendingFilesSchedule);
var msg = 'push sending files request in queue, queued length = ' +
this._sendingFilesQueue.length;
this.debug(msg);
},
onReceivingFileConfirmation: function bt_onReceivingFileConfirmation(evt) {
if (Service.query('NfcHandoverManager.isHandoverInProgress')) {
// Bypassing confirm dialog while incoming file transfer via
// NFC Handover
this.debug('Incoming file via NFC Handover. Bypassing confirm dialog');
window.dispatchEvent(new CustomEvent('nfc-transfer-started'));
this.acceptReceive(evt);
return;
}
// Prompt appears when a transfer request from a paired device is
// received.
this.debug('show receive confirm dialog');
var address = evt.address;
this.getDeviceName(address).then((deviceName) => {
var title = {
id: 'transfer-confirmation-title',
args: { deviceName: deviceName }
};
NotificationHelper.send(title, {
'bodyL10n': 'transfer-confirmation-desc',
'icon': this.bluetoothIcon,
'mozbehavior': {noclear: true}
}).then((notification) => {
notification.addEventListener('click', () => {
UtilityTray.hide();
this.showReceivePrompt(evt);
});
});
});
},
showReceivePrompt: function bt_showReceivePrompt(evt) {
var address = evt.address;
var fileName = evt.fileName;
var fileSize = this.humanizeSize(evt.fileLength);
var cancel = {
title: 'deny',
callback: this.declineReceive.bind(this, address)
};
var confirm = {
title: 'transfer',
callback: this.acceptReceive.bind(this, evt),
recommend: true
};
this.getDeviceName(address).then(function(deviceName) {
Service.request('showCustomDialog', 'acceptFileTransfer',
{
id: 'want-to-receive-file',
args: {
deviceName: deviceName,
fileName: fileName,
fileSize: fileSize
}
},
cancel,
confirm
);
});
},
declineReceive: function bt_declineReceive(address) {
Service.request('hideCustomDialog');
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter != null) {
adapter.confirmReceivingFile(address, false);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
},
acceptReceive: function bt_acceptReceive(evt) {
this.debug('accepted the file transfer');
Service.request('hideCustomDialog');
// Check storage is available or not before confirm receiving file
var address = evt.address;
var fileSize = evt.fileLength;
this.checkStorageSpace(fileSize,
(isStorageAvailable, errorMessage) => {
var option = (isStorageAvailable) ? true : false;
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter) {
adapter.confirmReceivingFile(address, option);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
// Storage is not available, then pop out a prompt with the reason
if (!isStorageAvailable) {
this.showStorageUnavaliablePrompt(errorMessage);
}
});
},
showStorageUnavaliablePrompt:
function bt_showStorageUnavaliablePrompt(msg) {
var confirm = {
title: 'confirm',
callback: function() {
Service.request('hideCustomDialog');
}
};
Service.request('showCustomDialog',
'cannotReceiveFile', msg, confirm, null);
},
checkStorageSpace: function bt_checkStorageSpace(fileSize, callback) {
if (!callback) {
return;
}
var storage = this._deviceStorage;
var availreq = storage.available();
availreq.onsuccess = function(e) {
switch (availreq.result) {
case 'available':
// skip down to the code below
break;
case 'unavailable':
callback(false, 'sdcard-not-exist2');
return;
case 'shared':
callback(false, 'sdcard-in-use');
return;
default:
callback(false, 'unknown-error');
return;
}
// If we get here, then the sdcard is available, so we need to find out
// if there is enough free space on it
var freereq = storage.freeSpace();
freereq.onsuccess = function() {
if (freereq.result >= fileSize) {
callback(true, '');
} else {
callback(false, 'sdcard-no-space2');
}
};
freereq.onerror = function() {
callback(false, 'cannotGetStorageState');
};
};
availreq.onerror = function(e) {
callback(false, 'cannotGetStorageState');
};
},
isSendFileQueueEmpty: function() {
return this._sendingFilesQueue.length === 0;
},
isFileTransferInProgress: function() {
var jobs = this.transferStatusList.querySelector('div');
return jobs != null;
},
sendFileViaHandover: function bt_sendFileViaHandover(evt) {
var mac = evt.detail.mac;
var blob = evt.detail.blob;
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter !== null) {
var sendingFilesSchedule = {
viaHandover: true,
numberOfFiles: 1,
numSuccessful: 0,
numUnsuccessful: 0
};
this._onFilesSending({detail: sendingFilesSchedule});
// XXX: Bug 915602 - [Bluetooth] Call sendFile api will crash
// the system while device is just paired.
// The paired device is ready to send file.
// Since above issue is existed, we use a setTimeout with 3 secs delay
var waitConnectionReadyTimeoutTime = 3000;
setTimeout(() => {
adapter.sendFile(mac, blob);
}, waitConnectionReadyTimeoutTime);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
},
_onUpdateProgress: function bt__onUpdateProgress(mode, evt) {
switch (mode) {
case 'start':
var transferInfo = evt.detail.transferInfo;
this.initProgress(transferInfo);
break;
case 'progress':
var processedLength = evt.processedLength;
var fileLength = evt.fileLength;
var progress = 0;
if (fileLength === 0) {
//XXX: May need to handle unknow progress
} else if (processedLength > fileLength) {
// According Bluetooth spec.,
// the processed length is a referenced value only.
// XXX: If processed length is bigger than file length,
// show an unknown progress
} else {
progress = processedLength / fileLength;
}
this.updateProgress(progress, evt);
break;
}
},
initProgress: function bt_initProgress(evt) {
// Create progress dynamically in notification center
var address = evt.address;
var transferMode =
(evt.received === true) ?
'bluetooth-receiving-progress2' : 'bluetooth-sending-progress2';
var content =
`<div data-icon="bluetooth-transfer-circle" aria-hidden="true"></div>
<div class="title-container" data-l10n-id="${transferMode}"></div>
<progress value="0" max="1"></progress>`;
var transferTask = document.createElement('div');
transferTask.id = 'bluetooth-transfer-status';
transferTask.className = 'fake-notification';
transferTask.setAttribute('data-id', address);
transferTask.setAttribute('role', 'link');
transferTask.innerHTML = content;
transferTask.addEventListener('click', this._bindOnCancelTransferTask);
this.transferStatusList.appendChild(transferTask);
},
updateProgress: function bt_updateProgress(value, evt) {
var address = evt.address;
var id = 'div[data-id="' + address + '"] progress';
var progressEl = this.transferStatusList.querySelector(id);
progressEl.value = value;
},
removeProgress: function bt_removeProgress(evt) {
var address = evt.address;
var id = 'div[data-id="' + address + '"]';
var finishedTask = this.transferStatusList.querySelector(id);
// If we decline receiving file, Bluetooth won't callback
// 'bluetooth-opp-transfer-start', 'bluetooth-opp-update-progress' event.
// So that there is no progress element which was created on notification.
// There is only 'bluetooth-opp-transfer-complete' event to notify Gaia
// the transferring request in failed case.
if (finishedTask == null) {
return;
}
finishedTask.removeEventListener('click', this._bindOnCancelTransferTask);
this.transferStatusList.removeChild(finishedTask);
},
onCancelTransferTask: function bt_onCancelTransferTask(evt) {
var address = evt.currentTarget.dataset.id;
// Show confirm dialog for user to cancel transferring task
UtilityTray.hide();
this.showCancelTransferPrompt(address);
},
showCancelTransferPrompt: function bt_showCancelTransferPrompt(address) {
var cancel = {
title: 'no',
callback: this.continueTransfer.bind(this)
};
var confirm = {
title: 'yes',
callback: this.cancelTransfer.bind(this, address)
};
Service.request('showCustomDialog',
'cancelFileTransfer',
'cancelFileTransfer',
cancel,
confirm
);
},
continueTransfer: function bt_continueTransfer() {
Service.request('hideCustomDialog');
},
cancelTransfer: function bt_cancelTransfer(address) {
Service.request('hideCustomDialog');
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter !== null) {
adapter.stopSendingFile(address);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
},
/**
* Create notification when transfer complete.
*
* @param {Object} evt bluetooth-opp-transfer-complete event
*/
_onTransferComplete: function bt__onTransferComplete(evt) {
var transferInfo = evt.detail.transferInfo;
// Remove transferring progress
this.removeProgress(transferInfo);
// Show notification
var nData = {
titleL10n: null,
icon: this.bluetoothIcon,
transferInfo: null,
onclick: null
};
if (transferInfo.success === true) {
if (transferInfo.received) {
// Received file can be opened only
nData.titleL10n = 'transferFinished-receivedSuccessful-title';
nData.transferInfo = transferInfo;
nData.onclick = this.openReceivedFile.bind(this, transferInfo);
} else {
nData.titleL10n = 'transferFinished-sentSuccessful-title';
}
} else {
if (transferInfo.received) {
nData.titleL10n = 'transferFinished-receivedFailed-title';
} else {
nData.titleL10n = 'transferFinished-canceled';
}
}
var l10nArgs = {
icon: nData.icon,
data: {
systemMessageTarget: 'BluetoothTransfer'
}
};
if (transferInfo.fileName) {
l10nArgs.body = transferInfo.fileName;
// put original message in data for reference
l10nArgs.data.transferInfo = nData.transferInfo;
} else {
l10nArgs.bodyL10n = 'unknown-file';
}
var promise = NotificationHelper.send(nData.titleL10n, l10nArgs);
if (nData.onclick) {
promise.then(function(notification) {
notification.addEventListener('click', nData.onclick);
});
}
var viaHandover = false;
if (this._sendingFilesQueue.length > 0) {
viaHandover = this._sendingFilesQueue[0].viaHandover || false;
}
// Have a report notification for sending multiple files.
this.summarizeSentFilesReport(transferInfo);
// Inform NfcHandoverManager that the transfer completed
var details = {received: transferInfo.received,
success: transferInfo.success,
viaHandover: viaHandover};
window.dispatchEvent(new CustomEvent('nfc-transfer-completed', {
detail: details}));
},
/**
* Handle related system notification message operation after reboot.
* Need close notification message by ourself.
*
* @param {Object} message system notification message
*/
handleSystemMessageNotification: function(message) {
this.debug('Received system message: ' + JSON.stringify(message));
if (message.data && message.data.transferInfo) {
var evt = {
fileName: message.data.transferInfo.fileName,
contentType: message.data.transferInfo.contentType
};
this.openReceivedFile(evt);
} else {
console.error('The notification does not carry correct data');
}
this.closeSystemMessageNotification(message);
},
/**
* Close the system notification message.
*
* @param {Object} message system notification message
*/
closeSystemMessageNotification: function(message) {
Notification.get({ tag: message.tag }).then(notifs => {
notifs.forEach(notif => {
// check on the body
if (notif.body === message.body) {
notif.close && notif.close();
}
});
});
},
summarizeSentFilesReport:
function bt_summarizeSentFilesReport(transferInfo) {
// Ignore received files
if (transferInfo.received) {
return;
}
// Consumer: System app consume each sending file request from Bluetooth
// app
var msg = 'remove the finished sending task from queue, ' +
'queue length = ';
var successful = transferInfo.success;
var sendingFilesSchedule = this._sendingFilesQueue[0];
var numberOfFiles = sendingFilesSchedule.numberOfFiles;
if (numberOfFiles == 1) { // for sent one file only.
// We don't need to summarize a report for sent one file only.
// Remove the finished sending task from the queue
this._sendingFilesQueue.shift();
msg += this._sendingFilesQueue.length;
this.debug(msg);
} else { // The scheduled task is for sent multiple files.
// Create a report in notification.
// Record each transferring report.
if (successful) {
this._sendingFilesQueue[0].numSuccessful++;
} else {
this._sendingFilesQueue[0].numUnsuccessful++;
}
var numSuccessful = this._sendingFilesQueue[0].numSuccessful;
var numUnsuccessful = this._sendingFilesQueue[0].numUnsuccessful;
if ((numSuccessful + numUnsuccessful) == numberOfFiles) {
// In this item of queue, all files were sent completely.
NotificationHelper.send('transferReportTitle', {
'bodyL10n': {
id: 'transferReport-description',
args: {
numSuccessful: numSuccessful,
numUnsuccessful: numUnsuccessful
}
},
'icon': this.bluetoothIcon
});
// Remove the finished sending task from the queue
this._sendingFilesQueue.shift();
msg += this._sendingFilesQueue.length;
this.debug(msg);
}
}
},
openReceivedFile: function bt_openReceivedFile(evt) {
// Launch the gallery with an open activity to view this specific photo
// XXX: Bug 897434 - Save received/downloaded files in one specific folder
// with meaningful path and filename
var filePath = 'Download/Bluetooth/' + evt.fileName;
var contentType = evt.contentType;
var storageType = 'sdcard';
var storage = navigator.getDeviceStorage(storageType);
var getreq = storage.get(filePath);
var loader = LazyLoader.load(['shared/js/mime_mapper.js']);
Promise.all([getreq, loader]).then(() => {
var file = getreq.result;
// When we got the file by storage type of "sdcard"
// use the file.type to replace the empty fileType which is given by API
var fileName = file.name;
var extension = fileName.split('.').pop();
var originalType = file.type || contentType;
var mappedType = (MimeMapper.isSupportedType(originalType)) ?
originalType : MimeMapper.guessTypeFromExtension(extension);
var a = new MozActivity({
name: mappedType == 'text/vcard' ? 'import' : 'open',
data: {
type: mappedType,
blob: file,
// XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=812098
// Pass the file name for Music APP since it can not open blob
filename: fileName
}
});
a.onerror = (e) => {
var msg = 'open activity error:' + a.error.name;
this.debug(msg);
switch (a.error.name) {
case 'NO_PROVIDER':
UtilityTray.hide();
// Cannot identify MIMETYPE
// So, show cannot open file dialog with unknow media type
this.showUnknownMediaPrompt(fileName);
return;
case 'ActivityCanceled':
return;
case 'USER_ABORT':
return;
default:
return;
}
};
a.onsuccess = (e) => {
var msg = 'open activity onsuccess';
this.debug(msg);
};
}, (err) => {
if (getreq.readyState === 'done' && getreq.error) {
// DeviceStorage Error
var msg = 'failed to get file:' + filePath + ' ' + err.name;
this.debug(msg);
} else { // LazyLoader Error
throw err;
}
}).catch((err) => {
console.error(err);
});
},
showUnknownMediaPrompt: function bt_showUnknownMediaPrompt(fileName) {
var confirm = {
title: 'confirm',
callback: function() {
Service.request('hideCustomDialog');
}
};
var body = {id: 'unknownMediaTypeToOpenFile', args: {fileName: fileName}};
Service.request('showCustomDialog',
'cannotOpenFile', body, confirm, null);
}
};
exports.BluetoothTransfer = BluetoothTransfer;
})(window);