/* 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, * enable Bluetooth and queue the action. If Bluetooth is already enabled, * request the bluetooth adapter and perform the action. * @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 { Service.request('Bluetooth:adapter').then((adapter) => { this.debug('Got the adapter, start the action'); this._adapter = adapter; action.callback.apply(this, action.args); }).catch((err) => { this._logVisibly('Failed to get bluetooth adapter'); }); } }, /** * 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); 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) => { 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 (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 */ _doInitiateFileTransfer: function _doInitiateFileTransfer(msg) { this.debug('initiateFileTransfer'); /* * 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._doInitiateFileTransfer, 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(); } }); }());