/* Copyright © 2013, Deutsche Telekom, Inc. */
/* globals NDEFUtils, NfcConnectSystemDialog, LazyLoader,
NDEF, NfcUtils, NotificationHelper, BaseModule, Service */
'use strict';
/**
* NfcHandoverManager handles handovers from other Bluetooth devices according
* to the specification of the NFC Forum
* (Document: NFCForum-TS-ConnectionHandover_1_2.doc).
* @class NfcHandoverManager
*/
(function() {
var NfcHandoverManager = function() {};
NfcHandoverManager.STATES = [
'isHandoverInProgress'
];
NfcHandoverManager.SETTINGS = [
'nfc.debugging.enabled'
];
NfcHandoverManager.EVENTS = [
'bluetooth-enabled',
'bluetooth-disabled',
'nfc-transfer-started',
'nfc-transfer-completed'
];
BaseModule.create(NfcHandoverManager, {
name: 'NfcHandoverManager',
/**
* Flag which turns on debuging messages
* @type {boolean}
* @memberof NfcHandoverManager.prototype
*/
DEBUG: false,
/**
* Default bluetooth adapter
* @type {Object}
* @memberof NfcHandoverManager.prototype
*/
_adapter: null,
/**
* Keeps a list of actions that need to be performed after
* Bluetooth is turned on.
* @type {Array}
* @memberof NfcHandoverManager.prototype
*/
actionQueue: [],
/**
* Keeps a list of send file requests made via peer.sendFile(blob).
* It will be inspected in the handling of Handover Select messages
* to distinguish between static and negotiated handovers.
* @type {Object}
* @memberof NfcHandoverManager.prototype
*/
sendFileQueue: [],
/**
* The length of the timeout in milliseconds to wait for an outstanding
* handover response.
* @type {Number}
* @memberof NfcHandoverManager.prototype
*/
responseTimeoutMillis: 9000,
/**
* Set whenever a timeout is defined while waiting for an outstanding
* handover response.
* @type {Object}
* @memberof NfcHandoverManager.prototype
*/
responseTimeoutFunction: null,
/**
* Set to true during a file transfer that was initiated by another device.
* @type {boolean}
* @memberof NfcHandoverManager.prototype
*/
incomingFileTransferInProgress: false,
/**
* Remembers whether Bluetooth was already saved during an earlier
* file transfer.
* @type {boolean}
* @memberof NfcHandoverManager.prototype
*/
bluetoothStatusSaved: false,
/**
* Remembers whether Bluetooth was enabled or automatically.
* @type {boolean}
* @memberof NfcHandoverManager.prototype
*/
bluetoothAutoEnabled: false,
/**
* Used to prevent triggering Settings multiple times.
* @memberof NfcHandoverManager.prototype
*/
settingsNotified: false,
/**
* Logs message in logcat
* @param {String} msg debug messages
* @param {Object} opObject object to printed after doing JSON.stringfy
* @memberof NfcHandoverManager.prototype
*/
_logVisibly: function _logVisibly(msg, optObject) {
var output = '[NfcHandoverManager]: ' + msg;
if (optObject) {
output += JSON.stringify(optObject);
}
console.log(output);
},
'_observe_nfc.debugging.enabled': function(enabled) {
this.DEBUG = enabled;
},
/**
* Initializes event and message handlers, initializes properties.
* @memberof NfcHandoverManager.prototype
*/
_start: function _start() {
this.incomingFileTransferInProgress = false;
this.bluetoothStatusSaved = false;
this.bluetoothAutoEnabled = false;
window.navigator.mozSetMessageHandler('nfc-manager-send-file',
(msg) => {
this.debug('In New event nfc-manager-send-file' +
JSON.stringify(msg));
this.handleFileTransfer(msg);
});
},
'_handle_nfc-transfer-completed': function(evt) {
this.transferComplete(evt);
},
'_handle_nfc-transfer-started': function(evt) {
this._transferStarted(evt);
},
'_handle_bluetooth-disabled': function() {
this.debug('bluetooth-disabled');
this._adapter = null;
this._clearBluetoothStatus();
},
'_handle_bluetooth-enabled': function() {
this.debug('bluetooth-enabled');
// wait for adapter is ready
Service.request('Bluetooth:adapter').then((adapter) => {
this._adapter = adapter;
this.settingsNotified = false;
this.debug('MAC address: ' + adapter.address);
this.debug('MAC name: ' + adapter.name);
this.debug('process queued actions');
/*
* Call all actions that have queued up while Bluetooth
* was turned on.
*/
for (var i = 0, len = this.actionQueue.length; i < len; i++) {
var action = this.actionQueue[i];
action.callback.apply(this, action.args);
}
this.actionQueue = [];
}).catch(() => {
this._logVisibly('event listener: Failed to get bluetooth adapter');
});
},
/**
* Save the on/off status of Bluetooth.
* @memberof NfcHandoverManager.prototype
*/
_saveBluetoothStatus: function _saveBluetoothStatus() {
if (!this.bluetoothStatusSaved) {
this.bluetoothStatusSaved = true;
this.bluetoothAutoEnabled = !Service.query('Bluetooth.isEnabled');
}
},
/**
* Restore the Bluetooth status.
* @memberof NfcHandoverManager.prototype
*/
_restoreBluetoothStatus: function _restoreBluetoothStatus() {
if (!this.isHandoverInProgress() &&
Service.query('BluetoothTransfer.isSendFileQueueEmpty')) {
if (this.bluetoothAutoEnabled) {
this.debug('Disabling Bluetooth');
this.publish('request-disable-bluetooth', this,
/* no prefix */ true);
this.bluetoothAutoEnabled = false;
}
this.bluetoothStatusSaved = false;
}
},
/**
* Forget a previously saved Bluetooth status.
* @memberof NfcHandoverManager.prototype
*/
_clearBluetoothStatus: function _clearBluetoothStatus() {
this.bluetoothStatusSaved = false;
},
/*
* Performs an action once Bluetooth is enabled. If Bluetooth is disabled,
* it is enabled and the action is queued. If Bluetooth is already enabled,
* performs the action directly.
* @param {Object} action action to be performed
* @param {function} action.callback function to be executed
* @param {Array} action.args arguments for the function
* @memberof NfcHandoverManager.prototype
*/
_doAction: function _doAction(action) {
var enabled = Service.query('Bluetooth.isEnabled');
if (enabled === undefined) {
this._logVisibly('Bluetooth is not available yet');
return;
}
if (!enabled) {
this.debug('Bluetooth: not yet enabled');
this.actionQueue.push(action);
if (this.settingsNotified === false) {
this.publish('request-enable-bluetooth', this,
/* no prefix */ true);
this.settingsNotified = true;
}
} else {
action.callback.apply(this, action.args);
}
},
/**
* Gets the data about other device taking part in handover proces
* from NDEF message
* @param {Array} ndef NDEF message
* @returns {Object} ssp - object containing info about other devices
* @returns {string} ssp.mac - mac addres of other devices
* @returns {string} ssp.localName - local name if present in NDEF message,
* null otherwise
* @memberof NfcHandoverManager.prototype
*/
_getBluetoothSSP: function _getBluetoothSSP(ndef) {
var handover = NDEFUtils.parseHandoverNDEF(ndef);
if (handover == null) {
// Bad handover message. Just ignore.
this.debug('Bad handover messsage');
return null;
}
var btsspRecord = NDEFUtils.searchForBluetoothAC(handover);
if (btsspRecord == null) {
// There is no Bluetooth Alternative Carrier record in the
// Handover Select message. Since we cannot handle WiFi Direct,
// just ignore.
this.debug('No BT AC');
return null;
}
return NDEFUtils.parseBluetoothSSP(btsspRecord);
},
/**
* Look for a paired device and invoke the appropriate callback function.
* @param {string} mac MAC address of the device
* @param {function} foundCb Found callback
* @param {function} notFoundCb Not found callback
* @memberof NfcHandoverManager.prototype
*/
_findPairedDevice: function _findPairedDevice(mac, foundCb, notFoundCb) {
this.debug('_findPairedDevice');
Service.request('Bluetooth:getPairedDevices').then((devices) => {
this.debug('# devices: ' + devices.length);
for (var i = 0, len = devices.length; i < len; i++) {
var device = devices[i];
this.debug('Address: ' + device.address);
if (device.address.toLowerCase() === mac.toLowerCase()) {
this.debug('Found device ' + mac);
foundCb(device);
return;
}
}
if (notFoundCb) {
notFoundCb();
}
}).catch((err) => {
this._logVisibly('Cannot get paired devices from adapter: ' + err);
});
},
/**
* Connects via bluetooth to the paired device.
* @param {string} device Device to be paired
* @memberof NfcHandoverManager.prototype
*/
_doConnect: function _doConnect(device) {
this.debug('doConnect with: ' + device.address);
if (this._adapter === null) {
this._logVisibly('No Bluetooth Adapter');
return;
}
var req = this._adapter.connect(device);
req.onsuccess = () => { this.debug('Connect succeeded'); };
req.onerror = () => { this.debug('Connect failed'); };
},
/**
* Performs bluetooth pairing with other device
* @param {string} mac MAC address of the peer device
* @memberof NfcHandoverManager.prototype
*/
_doPairing: function _doPairing(mac) {
this.debug('doPairing: ' + mac);
var alreadyPaired = (device) => {
if (this._adapter === null) {
this._logVisibly('No Bluetooth Adapter');
return;
}
this._adapter.connect(device);
};
var notYetPaired = () => {
this.debug('Device not yet paired');
Service.request('Bluetooth:pair', mac).then(() => {
this.debug('Pairing succeeded');
this._clearBluetoothStatus();
/*
* Bug 979427:
* After pairing we connect to the remote device. The only thing we
* know here is the MAC address, but the defaultAdapter.connect()
* requires a BluetoothDevice argument. So we use _findPairedDevice()
* to map the MAC to a BluetoothDevice instance.
*/
this._findPairedDevice(mac, (device) => {
this._doConnect(device);
});
}).catch((err) => {
this.debug(err);
this._logVisibly('Pairing failed');
this._restoreBluetoothStatus();
});
};
this._findPairedDevice(mac, alreadyPaired, notYetPaired);
},
/**
* Show an error notification when file transfer failed.
* @param {String} msg Optional message.
* @memberof NfcHandoverManager.prototype
*/
_showFailedNotification: function _showFailedNotification(title, msg) {
var body = (msg !== undefined) ? msg : '';
var icon = 'style/bluetooth_transfer/images/icon_bluetooth.png';
NotificationHelper.send(title, {
body: body,
icon: icon
});
},
/**
* Show 'send failed, try again' notification.
* @memberof NfcHandoverManager.prototype
*/
_showTryAgainNotification: function _showTryAgainNotification() {
var _ = navigator.mozL10n.get;
this._showFailedNotification('transferFinished-sentFailed-title',
_('transferFinished-try-again-description'));
},
/**
* This function will be called after a timeout when we did not receive the
* Hs record within three seconds. At this point we cancel the file
* transfer.
* @memberof NfcHandoverManager.prototype
*/
_cancelSendFileTransfer: function _cancelSendFileTransfer() {
this.debug('_cancelSendFileTransfer');
this.responseTimeoutFunction = null;
var job = this.sendFileQueue.pop();
job.onerror();
this._showFailedNotification('transferFinished-sentFailed-title',
job.blob.name);
this._restoreBluetoothStatus();
},
/**
* This function will be called after a timeout when we did not receive
* the Hs record within three seconds. At this point we cancel the file
* transfer.
* @memberof NfcHandoverManager.prototype
*/
_cancelIncomingFileTransfer: function _cancelIncomingFileTransfer() {
this.debug('_cancelIncomingFileTransfer');
this.responseTimeoutFunction = null;
this.incomingFileTransferInProgress = false;
this._showFailedNotification('transferFinished-receivedFailed-title');
this._restoreBluetoothStatus();
},
/**
* Performs bluetooth file transfer if this.sendFileRequest exists
* to other device
* @param {string} mac MAC address of the other device
* @memberof NfcHandoverManager.prototype
*/
_doFileTransfer: function _doFileTransfer(mac) {
this.debug('doFileTransfer');
if (this.sendFileQueue.length === 0) {
// Nothing to do
this.debug('sendFileQueue empty');
return;
}
this.debug('Send blob to ' + mac);
var blob = this.sendFileQueue[0].blob;
this.publish('bluetooth-sendfile-via-handover', {
mac: mac,
blob: blob
});
},
/**
* Performs tha actual handover request
* @param {Array} ndef NDEF message conating the handover request record
* @param {MozNFCPeer} MozNFCPeer object.
* @memberof NfcHandoverManager.prototype
*/
_doHandoverRequest: function _doHandoverRequest(ndef, nfcPeer) {
this.debug('doHandoverRequest');
if (this._getBluetoothSSP(ndef) == null) {
/*
* The handover request didn't contain a valid MAC address. Simply
* ignore the request.
*/
return;
}
if (this._adapter === null) {
this._logVisibly('No Bluetooth Adapter');
return;
}
if (nfcPeer.isLost) {
this._logVisibly('NFC peer went away during doHandoverRequest');
this._showFailedNotification('transferFinished-receivedFailed-title');
this._restoreBluetoothStatus();
return;
}
var cps = Service.query('Bluetooth.isEnabled') ?
NDEF.CPS_ACTIVE : NDEF.CPS_ACTIVATING;
var mac = this._adapter.address;
var hs = NDEFUtils.encodeHandoverSelect(mac, cps);
nfcPeer.sendNDEF(hs).then(() => {
this.debug('sendNDEF(hs) succeeded');
this.incomingFileTransferInProgress = true;
}).catch((e) => {
this._logVisibly('sendNDEF(hs) failed : ' + e);
this._clearTimeout();
this._restoreBluetoothStatus();
});
this._clearTimeout();
this.responseTimeoutFunction =
setTimeout(this._cancelIncomingFileTransfer.bind(this),
this.responseTimeoutMillis);
},
/**
* Initiate a file transfer by sending a Handover Request to the
* remote device.
* @param {Object} msg message object
* @param {MozNFCPeer} msg.peer An instance of MozNFCPeer object.
* @param {Blob} msg.blob File to be sent.
* @param {String} msg.requestId Request ID.
* @memberof NfcHandoverManager.prototype
*/
_initiateFileTransfer:
function _initiateFileTransfer(msg) {
this.debug('initiateFileTransfer');
if (this._adapter === null) {
this._logVisibly('No Bluetooth Adapter');
this._restoreBluetoothStatus();
return;
}
/*
* Initiate a file transfer by sending a Handover Request to the
* remote device.
*/
var onsuccess = () => {
this._dispatchSendFileStatus(0, msg.requestId);
};
var onerror = () => {
this._dispatchSendFileStatus(1, msg.requestId);
};
if (msg.peer.isLost) {
this._logVisibly('NFC peer went away during initiateFileTransfer');
onerror();
this._restoreBluetoothStatus();
this._showFailedNotification('transferFinished-sentFailed-title',
msg.blob.name);
return;
}
var job = {nfcPeer: msg.peer, blob: msg.blob, requestId: msg.requestId,
onsuccess: onsuccess, onerror: onerror};
this.sendFileQueue.push(job);
var cps = Service.query('Bluetooth.isEnabled') ?
NDEF.CPS_ACTIVE : NDEF.CPS_ACTIVATING;
var mac = this._adapter.address;
var hr = NDEFUtils.encodeHandoverRequest(mac, cps);
msg.peer.sendNDEF(hr).then(() => {
this.debug('sendNDEF(hr) succeeded');
}).catch((e) => {
this.debug('sendNDEF(hr) failed : ' + e);
onerror();
this.sendFileQueue.pop();
this._clearTimeout();
this._restoreBluetoothStatus();
this._showFailedNotification('transferFinished-sentFailed-title',
msg.blob.name);
});
this._clearTimeout();
this.responseTimeoutFunction =
setTimeout(this._cancelSendFileTransfer.bind(this),
this.responseTimeoutMillis);
},
/**
* Clears timeout that handles the case an outstanding handover message
* has not been received within a certain timeframe.
* @memberof NfcHandoverManager.prototype
*/
_clearTimeout: function _clearTimeout() {
this.debug('_clearTimeout');
if (this.responseTimeoutFunction != null) {
// Clear the timeout that handles error
this.debug('clearing timeout');
clearTimeout(this.responseTimeoutFunction);
this.responseTimeoutFunction = null;
}
},
/**
* Dispatches status of file sending to mozNfc.
* @param {number} status status of file send operation
* @param {string} request ID of the operation
* @memberof NfcHandoverManager.prototype
*/
_dispatchSendFileStatus:
function _dispatchSendFileStatus(status, requestId) {
this.debug('In dispatchSendFileStatus ' + status);
navigator.mozNfc.notifySendFileStatus(status, requestId);
},
/**
* Handles connection request by asking user for confirmation.
* @param {Object} btssp BT SSP (Secure Simple Pairing) data.
* @memberof NfcHandoverManager.prototype
*/
_onRequestConnect: function _onRequestConnect(btssp) {
var onconfirm = () => {
this.debug('Connect confirmed');
this._doAction({callback: this._doPairing, args: [btssp.mac]});
};
var onabort = () => {
this.debug('Connect aborted');
};
if (!this.nfcConnectSystemDialog) {
LazyLoader.load('js/system_nfc_connect_dialog.js').then(() => {
this.nfcConnectSystemDialog = new NfcConnectSystemDialog();
this.nfcConnectSystemDialog.show(btssp.localName, onconfirm, onabort);
}).catch((err) => {
console.error(err);
});
} else {
this.nfcConnectSystemDialog.show(btssp.localName, onconfirm, onabort);
}
},
/**
* Check if a device is already paired and connected.
*
* BTv2 deprecate the device.connected property.
* connect stat should be retrieved by adapter.getConnectedDevices API
*
* @param {Object} btssp BT SSP record
* @memberof NfcHandoverManager.prototype
*/
_checkConnected: function _checkConnected(btssp) {
if (!Service.query('Bluetooth.isEnabled')) {
this._onRequestConnect(btssp);
return;
}
if (this._adapter === null) {
this._logVisibly('No Bluetooth Adapter');
return;
}
var connected = false;
// 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 = this._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 == btssp.mac) {
connected = true;
}
}
if (!connected) {
this._onRequestConnect(btssp);
}
} else {
this._logVisibly('Can not get connected device result.');
return;
}
};
req.onerror = () => {
this.debug('Can not check is device connected from adapter.');
this._onRequestConnect(btssp);
};
},
/**
* Handles simplified pairing record.
* @param {Array} ndef NDEF message containing simplified pairing record
* @memberof NfcHandoverManager.prototype
*/
_handleSimplifiedPairingRecord: function _handlePairingRecord(ndef) {
this.debug('_handleSimplifiedPairingRecord');
var pairingRecord = ndef[0];
var btssp = NDEFUtils.parseBluetoothSSP(pairingRecord);
this.debug('Simplified pairing with: ' + btssp.mac);
this._checkConnected(btssp);
},
/**
* Handle NDEF Handover Select message.
* @param {Array} ndef NDEF message containing handover select record
* @memberof NfcHandoverManager.prototype
*/
_handleHandoverSelect: function _handleHandoverSelect(ndef) {
this.debug('_handleHandoverSelect');
this._clearTimeout();
var btssp = this._getBluetoothSSP(ndef);
if (btssp == null) {
if (this.sendFileQueue.length !== 0) {
// We tried to send a file but the other device gave us an empty AC
// record. This will happen if the other device is currently
// transferring a file. Show a 'try later' notification.
this.debug('Other device is transferring file. Aborting');
var job = this.sendFileQueue.shift();
job.onerror();
this._showTryAgainNotification();
}
this._restoreBluetoothStatus();
return;
}
if (this.sendFileQueue.length !== 0) {
// This is the response to a file transfer request (negotiated handover)
this._doAction({callback: this._doFileTransfer, args: [btssp.mac]});
} else {
// This is a static handover
this._checkConnected(btssp);
}
},
/**
* Handles NDEF Handover Request message.
* @param {Array} ndef NDEF message containing handover request record
* @param {MozNFCPeer} MozNFCPeer object.
* @memberof NfcHandoverManager.prototype
*/
_handleHandoverRequest: function _handleHandoverRequest(ndef, nfcPeer) {
this.debug('_handleHandoverRequest');
if (Service.query('BluetoothTransfer.isFileTransferInProgress')) {
// We don't allow concurrent file transfers
this.debug('This device is currently transferring a file. ' +
'Aborting via empty Hs');
var hs = NDEFUtils.encodeEmptyHandoverSelect();
nfcPeer.sendNDEF(hs);
return;
}
this._saveBluetoothStatus();
this._doAction({
callback: this._doHandoverRequest,
args: [ndef, nfcPeer]
});
},
/**
* Checks if the first record of NDEF message is a handover record.
* If yes the NDEF message is handled according to handover record type.
* @param {Array} ndefMsg array of NDEF records
* @param {MozNFCPeer} MozNFCPeer object.
* @returns {boolean} true if handover record was found and handled, false
* if no handover record was found
* @memberof NfcHandoverManager.prototype
*/
tryHandover: function(ndefMsg, nfcPeer) {
this.debug('tryHandover: ', ndefMsg);
var nfcUtils = new NfcUtils();
if (!Array.isArray(ndefMsg) || !ndefMsg.length) {
return false;
}
var record = ndefMsg[0];
if (record.tnf === NDEF.TNF_WELL_KNOWN) {
if (nfcUtils.equalArrays(record.type, NDEF.RTD_HANDOVER_SELECT)) {
this._handleHandoverSelect(ndefMsg);
return true;
} else if (nfcUtils.equalArrays(
record.type, NDEF.RTD_HANDOVER_REQUEST)) {
this._handleHandoverRequest(ndefMsg, nfcPeer);
return true;
}
} else if ((record.tnf === NDEF.TNF_MIME_MEDIA) &&
nfcUtils.equalArrays(record.type, NDEF.MIME_BLUETOOTH_OOB)) {
this._handleSimplifiedPairingRecord(ndefMsg);
return true;
}
return false;
},
/**
* Trigger a file transfer with a remote device via BT.
* @param {Object} msg message object
* @param {MozNFCPeer} msg.peer An instance of MozNFCPeer object.
* @param {Blob} msg.blob File to be sent.
* @param {String} msg.requestId Request ID.
* @memberof NfcHandoverManager.prototype
*/
handleFileTransfer: function handleFileTransfer(msg) {
this.debug('handleFileTransfer');
if (Service.query('BluetoothTransfer.isFileTransferInProgress')) {
// We don't allow concurrent file transfers
this.debug('This device is already transferring a file. Aborting');
this._dispatchSendFileStatus(1, msg.requestId);
this._showTryAgainNotification();
return;
}
this._saveBluetoothStatus();
this._doAction({
callback: this._initiateFileTransfer,
args: [msg]});
},
/**
* Returns true if a handover is in progress.
* @returns {boolean} true if handover is in progress.
* @memberof NfcHandoverManager.prototype
*/
isHandoverInProgress: function isHandoverInProgress() {
return (this.sendFileQueue.length !== 0) ||
(this.incomingFileTransferInProgress === true);
},
/**
* BluetoothTransfer notifies us that a file transfer has started.
* @memberof NfcHandoverManager.prototype
*/
_transferStarted: function bt__transferStarted() {
this._clearTimeout();
},
/**
* Tells NfcHandoverManager that a BT file transfer
* has been completed.
* @param details succeeded True if file transfer was successfull.
* @memberof NfcHandoverManager.prototype
*/
transferComplete: function transferComplete(evt) {
var details = evt.detail;
this.debug('transferComplete: ' + JSON.stringify(details));
if (!details.received && details.viaHandover) {
// Completed an outgoing send file request. Call onsuccess/onerror
var job = this.sendFileQueue.shift();
if (details.success) {
job.onsuccess();
} else {
job.onerror();
}
}
if (details.received) {
// We know that a file was received but we do not know if that
// file was sent via a NFC handover. Clearing the
// incomingFileTransferInProgress flag here could lead to an
// unavoidable race condition
this.incomingFileTransferInProgress = false;
}
this._restoreBluetoothStatus();
}
});
}());