VulpesOS/gaia/apps/system/js/bluetooth_v2.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

575 lines
16 KiB
JavaScript
Executable file

'use strict';
/**
* Bluetooth2 is compatible with Bluetooth APIv2 and used to enable/
* disable bluetooth hardware, get bluetooth adapter, and check target
* bluetooth profile is connected.
*
* Bluetooth api v2 is the two level structure.
* We use _btManagerHandler to handle BluetoothManager defaultAdapter
* change event and _btAdapterHandler to handle status change event.
*
* Current supported Bluetooth profiles are:
* - hfp: Hands-Free Profile
* - opp: Object Push Profile
* - a2dp: Advenced Audio Distribution Profile
* - sco: Synchronous Connection-Oriented Profile
*/
/* global Service, LazyLoader, BluetoothIcon, BluetoothTransferIcon,
BluetoothHeadphoneIcon, BluetoothTransfer */
/* exported Bluetooth2 */
(function(exports) {
var Bluetooth = function() {};
Bluetooth.prototype = {
name: 'Bluetooth',
/**
* Debug message.
*
* @public
* @type {Boolean} turn on/off the console log
*/
onDebug: false,
/**
* Store a reference of the default adapter.
* If the adapter is not availble, no bluetooth operation could be
* executed.
*
* @private
*/
_adapter: null,
/**
* Hold instance of bluetooth adapter state change handler.
*
* @private
*/
_bindBtAdapterHandler: null,
/**
* Hold instance of HFP status changed handler.
*
* @private
*/
_bindHfpStatusChangedHandler: null,
/**
* Hold instance of A2DP status changed handler.
*
* @private
*/
_bindA2dpStatusChangedHandler: null,
/**
* Hold instance of SCO status changed handler.
*
* @private
*/
_bindScoStatusChangedHandler: null,
/**
* Store a reference for mozBluetooth.
*
* @private
*/
_bluetooth: null,
/**
* State of Bluetooth default adapter.
*
* @private
*/
_isEnabled: false,
/**
* Set profile connect state.
*
* @private
* @param {String} profile profile id
* @param {Boolean} connected connect status
*/
_setProfileConnected: function bt_setProfileConnected(profile, connected) {
var value = this['_' + profile + 'Connected'];
if (value !== connected) {
this['_' + profile + 'Connected'] = connected;
// Raise an event for the profile connection changes.
window.dispatchEvent(new CustomEvent('bluetoothprofileconnectionchange',
{
detail: {
name: profile,
connected: connected
}
}
));
// Update Bluetooth and profile icons
this.icon && this.icon.update();
if (profile === 'opp' && this.transferIcon) {
this.transferIcon.update();
}
if (profile === 'a2dp' && this.headphoneIcon) {
this.headphoneIcon.update();
}
}
},
/**
* Check if bluetooth profile is connected.
*
* @private
* @param {String} profile profile name
* @return {Boolean} connected state
*/
_isProfileConnected: function bt__isProfileConnected(profile) {
var isConnected = this['_' + profile + 'Connected'];
if (isConnected === undefined) {
return false;
} else {
return isConnected;
}
},
/**
* Initialize bluetooth module.
*
* @public
*/
start: function bt_start() {
if (!window.navigator.mozSettings || !window.navigator.mozBluetooth) {
return;
}
this._bluetooth = window.navigator.mozBluetooth;
// hold the instances so we can remove it when needed
this._bindBtAdapterHandler =
this._btAdapterHandler.bind(this);
this._bindHfpStatusChangedHandler = function(evt) {
this._setProfileConnected('hfp', evt.status);
}.bind(this);
this._bindA2dpStatusChangedHandler = function(evt) {
this._setProfileConnected('a2dp', evt.status);
}.bind(this);
this._bindScoStatusChangedHandler = function(evt) {
this._setProfileConnected('sco', evt.status);
}.bind(this);
this._initDefaultAdapter();
this._bluetooth.addEventListener('attributechanged',
this._btManagerHandler.bind(this));
// decouple enable/disable bluetooth function from other system part
window.addEventListener('request-enable-bluetooth',
this._requestEnableHandler.bind(this));
window.addEventListener('request-disable-bluetooth',
this._requestDisableHandler.bind(this));
// relay bluetooth transfer system messages
navigator.mozSetMessageHandler('bluetooth-opp-transfer-start',
this._oppTransferStartHandler.bind(this));
navigator.mozSetMessageHandler('bluetooth-opp-transfer-complete',
this._oppTransferCompleteHandler.bind(this));
// expose functions to Service.request
Service.register('adapter', this);
Service.register('pair', this);
Service.register('getPairedDevices', this);
// expose functions to Service.query
Service.registerState('isEnabled', this);
Service.registerState('getAdapter', this);
Service.registerState('isOPPProfileConnected', this);
Service.registerState('isA2DPProfileConnected', this);
Service.registerState('isSCOProfileConnected', this);
// handle statusbar icons
LazyLoader.load(['js/bluetooth_transfer.js',
'js/bluetooth_icon.js',
'js/bluetooth_transfer_icon.js',
'js/bluetooth_headphone_icon.js']).then(() => {
BluetoothTransfer.start();
this.icon = new BluetoothIcon(this);
this.icon.start();
this.transferIcon = new BluetoothTransferIcon(this);
this.transferIcon.start();
this.headphoneIcon = new BluetoothHeadphoneIcon(this);
this.headphoneIcon.start();
}).catch(function(err) {
console.error(err);
});
},
stop: function() {
Service.unregister('adapter', this);
Service.unregister('pair', this);
Service.unregister('getPairedDevices', this);
Service.unregisterState('isEnabled', this);
Service.unregisterState('getAdapter', this);
Service.unregisterState('isOPPProfileConnected', this);
Service.unregisterState('isA2DPProfileConnected', this);
Service.unregisterState('isSCOProfileConnected', this);
},
/**
* Generally bluetooth adapter is already available when start
* this module and no related event will be received.
* so we try to get the adapter then listen to mozBluetooth change
* to prevent race condition
*
* @private
*/
_initDefaultAdapter: function() {
this.debug('init bluetooth adapter');
this._adapter = this._bluetooth.defaultAdapter;
if (this._adapter) {
this.debug('adapter is available');
// listen states when the adapter is available
this._adapterAvailableHandler();
// dispatch default bluetooth enable state
this._isEnabled = (this._adapter.state === 'enabled');
this._dispatchEnableState();
}
},
/**
* Once adapter is available, listen to bluetooth state and
* the connect stat of supported profiles.
*
* we only support two use cases for bluetooth connection:
* 1. connecting with a headset
* 2. transfering a file to/from another device
* So we need to listen to corresponding events to know we are (aren't)
* connected, then summarize to an event and dispatch to Statusbar
*
* @private
*/
_adapterAvailableHandler: function bt__adapterAvailableHandler() {
// listen to adapter state
this._adapter.addEventListener('attributechanged',
this._bindBtAdapterHandler);
// headset connected cases
this._adapter.addEventListener('hfpstatuschanged',
this._bindHfpStatusChangedHandler);
this._adapter.addEventListener('a2dpstatuschanged',
this._bindA2dpStatusChangedHandler);
this._adapter.addEventListener('scostatuschanged',
this._bindScoStatusChangedHandler);
},
/**
* Remove all EventListeners when default adapter is changed.
*/
_adapterUnavailableHandler: function bt__adapterUnavailableHandler() {
this.debug('default adapter is removed');
// unbind headset connected cases
this._adapter.removeEventListener('hfpstatuschanged',
this._bindHfpStatusChangedHandler);
this._adapter.removeEventListener('a2dpstatuschanged',
this._bindA2dpStatusChangedHandler);
this._adapter.removeEventListener('scostatuschanged',
this._bindScoStatusChangedHandler);
// unbind adapter
this._adapter.removeEventListener('attributechanged',
this._bindBtAdapterHandler);
},
/**
* Update settings value and enable bluetooth hardware.
*
* @private
*/
_requestEnableHandler: function bt__requestEnableHandler() {
this.debug('enabling bluetooth');
if (this._adapter) {
// the state will be dispatched in _btAdapterHandler
this._adapter.enable().then(() => { //resolve
this.debug('bluetooth enabled');
}, () => { //reject
this.debug('can not get bluetooth adapter');
});
} else { // return current state since we can't process the request
this.debug('adapter is not available');
this._dispatchEnableState();
}
},
/**
* Update settings value and disable bluetooth hardware.
*
* @private
*/
_requestDisableHandler: function bt__requestDisableHandler() {
this.debug('disabling bluetooth');
if (this._adapter) {
// the state will be dispatched in _btAdapterHandler
this._adapter.disable().then(() => { //resolve
this.debug('bluetooth disabled');
}, () => { //reject
this.debug('can not get bluetooth adapter');
});
} else { // return current state since we can't process the request
this.debug('adapter is not available');
this._dispatchEnableState();
}
},
/*
* In file transfering case:
* since System Message can't be listened in two js files within a app,
* so we listen here but dispatch events to bluetooth_transfer.js
* while getting bluetooth file transfer start system messages
*
* @private
*/
_oppTransferStartHandler: function(transferInfo) {
this._setProfileConnected('opp', true);
window.dispatchEvent(new CustomEvent('bluetooth-opp-transfer-start',
{
detail: { transferInfo: transferInfo }
}
));
},
/*
* In file transfering case:
* since System Message can't be listened in two js files within a app,
* so we listen here but dispatch events to bluetooth_transfer.js
* while getting bluetooth file transfer complete system messages
*
* @private
*/
_oppTransferCompleteHandler: function(transferInfo) {
this._setProfileConnected('opp', false);
window.dispatchEvent(new CustomEvent('bluetooth-opp-transfer-complete',
{
detail: { transferInfo: transferInfo }
}
));
},
/**
* BT APIv2: Watch 'onattributechanged' event from
* mozBluetooth.defaultAdapter for updating state information.
*
* @private
* @param {Object} evt event object
*/
_btAdapterHandler:
function bt__btAdapterHandler(evt) {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
case 'state':
if (this._adapter.state === 'enabled') {
this.debug('state true');
this._isEnabled = true;
navigator.mozSettings.createLock()
.set({'bluetooth.enabled': true});
window.dispatchEvent(new CustomEvent('bluetooth-enabled'));
this.icon && this.icon.update();
} else if (this._adapter.state === 'disabled') {
this.debug('state false');
this._isEnabled = false;
navigator.mozSettings.createLock()
.set({'bluetooth.enabled': false});
window.dispatchEvent(new CustomEvent('bluetooth-disabled'));
this.icon && this.icon.update();
}
break;
default:
break;
}
}
},
/**
* Watch 'onattributechanged' event from mozBluetooth for updating default
* adapter information.
*
* 'onattributechanged' event description:
* A handler to trigger when bluetooth manager's only property
* defaultAdapter has changed.
*
* Here are three adapter change cases:
*
* null -> adapter: set new adapter and dispatch adapter state.
* adapter -> null: clear defaultAdapter and listener.
* adapterA -> adapterB: clean defaultAdapter and listener,
* then set new adapter and dispatch adapter state.
*
* @private
* @param {Object} evt event object
*/
_btManagerHandler:
function bt__btManagerHandler(evt) {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
// Default adapter attribute change.
// Usually, it means that we reach new default adapter.
case 'defaultAdapter':
this.debug('defaultAdapter changed.');
if (this._bluetooth.defaultAdapter) {
if (this._adapter !== this._bluetooth.defaultAdapter) {
if (this._adapter !== null) { // adapter A -> adapter B
this.debug('origin adapter is removed');
this._adapterUnavailableHandler();
this._adapter = null;
}
// set new adapter and dispatch adapter state
this.debug('new adapter is added');
this._adapter = this._bluetooth.defaultAdapter;
this._isEnabled = (this._adapter.state === 'enabled');
// listen states when the adapter is available
this._adapterAvailableHandler();
}
} else { // adapter -> null
this.debug('default adapter is removed');
this._adapterUnavailableHandler();
this._adapter = null;
}
break;
default:
break;
}
}
},
/**
* Dispatch bluetooth enable/disable state to system.
*
* @private
*/
_dispatchEnableState: function bt__dispatchEnableState() {
if (this._isEnabled) {
window.dispatchEvent(new CustomEvent('bluetooth-enabled'));
} else {
window.dispatchEvent(new CustomEvent('bluetooth-disabled'));
}
},
/**
* Get adapter from bluetooth through promise interface.
* XXX: the function abstract the Bluetooth API difference.
* We can remove it and use service query once BTv1 is deprecated.
*
* @public
* @return {Promise} A promise that resolve the Bluetooth Adapter
*/
adapter: function bt__adapter() {
return new Promise((resolve, reject) => {
if (this._adapter !== null) {
resolve(this._adapter);
} else {
this.debug('No BT adapter retrieved');
reject();
}
});
},
/**
* Return device pairing result.
* XXX: the function abstract the Bluetooth API difference.
*
* @public
* @param {string} mac target device address
* @return {Promise} A promise that resolve when pair successfully,
* reject when pair failed
*/
pair: function bt__pair(mac) {
return this._adapter.pair(mac);
},
/**
* Return paired devices list.
* XXX: the function abstract the Bluetooth API difference.
* We can use service query to return list once BTv1 is deprecated.
*
* @public
* @returns {Object[]} sequence of BluetoothDevice
*/
getPairedDevices: function bt__getPairedDevices() {
return this._adapter.getPairedDevices();
},
/**
* Return bluetooth adapter.
*
* @public
*/
get getAdapter() {
return this._adapter;
},
/**
* Maintain bluetooth enable/disable state.
*
* @public
*/
get isEnabled() {
return this._isEnabled;
},
/**
* Check if bluetooth OPP profile is connected.
*
* @public
* @return {Boolean} connected state
*/
get isOPPProfileConnected() {
return this._isProfileConnected('opp');
},
/**
* Check if bluetooth A2DP profile is connected.
*
* @public
* @return {Boolean} connected state
*/
get isA2DPProfileConnected() {
return this._isProfileConnected('a2dp');
},
/**
* Check if bluetooth SCO profile is connected.
*
* @public
* @return {Boolean} connected state
*/
get isSCOProfileConnected() {
return this._isProfileConnected('sco');
},
/**
* Check if any of bluetooth profiles is connected.
* Referenced by Bluetooth icon update
*
* @public
* @return {Boolean} connected state
*/
get connected() {
return this._isProfileConnected('hfp') ||
this._isProfileConnected('a2dp') ||
this._isProfileConnected('opp');
},
/**
* Console log.
*
* @private
* @param {Object} msg debug message
*/
debug: function bt_debug(msg) {
if (!this.onDebug) {
return;
}
console.log('[System Bluetooth]: ' + msg);
}
};
exports.Bluetooth2 = Bluetooth;
})(window);