/* Copyright © 2013, Deutsche Telekom, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals CustomEvent, MozActivity,
NfcUtils, NDEF, BaseModule, NfcIcon,
LazyLoader, Service */
'use strict';
(function(exports) {
const NFC_HW_EVENTS =
['enable', 'disable', 'enable-polling', 'disable-polling',
'hw-change-success', 'hw-change-failure'];
const NFC_HW_STATE_TABLE = {
'disabling': [null, null, null, null, 'disabled', 'enabled'],
'disabled': ['enabling', null, null, null, null, null],
'enabling': [null, null, null, null, 'enabled', 'disabled'],
'enabled': [null, 'disabling', 'polling-on', 'polling-off', null, null],
// enabled state in which NFC HW is polling for NFC tags/peers
'polling-on': [null, 'disabling', null, 'polling-off', null, null],
// enabled state with low power consumption, NFC HW is not actively
// polling for NFC tags/peers. Card emulation is active.
'polling-off': [null, 'disabling', 'polling-on', null, null, null]
};
/**
* NfcManager is responsible for NFC support. It controls NFC hardware
* state, detects NFC tags and triggers appropriate activities, detects NFC
* peers and takes part in NFC P2P sharing process (with ShrinkingUI),
* detects NFC Handover requests and passes them to NfcHandoverManager for
* handling.
* @class NfcManager
* @requires BaseModule
* @requires ScreenManager
* @requires MozActivity
* @requires NDEF
* @requires NfcUtils
*/
var NfcManager = function() {
};
NfcManager.SETTINGS = [
'nfc.enabled',
'nfc.debugging.enabled'
];
NfcManager.SUB_MODULES = [
'NfcHandoverManager'
];
NfcManager.STATES = [
'isActive'
];
NfcManager.EVENTS = [
'screenchange',
'lockscreen-appopened',
'lockscreen-appclosed'
];
BaseModule.create(NfcManager, {
name: 'NfcManager',
DEBUG: false,
/**
* Current NFC Hardware state
* @memberof NfcManager.prototype
* @type {String}
*/
_hwState: null,
/**
* Initializes NfcManager, sets up listeners and handlers
* @memberof NfcManager.prototype
*/
_start: function nm_start() {
this.debug('Starting NFC Manager');
this._hwState = 'disabled';
LazyLoader.load(['js/nfc_icon.js']).then(function() {
this.icon = new NfcIcon(this);
this.icon.start();
}.bind(this)).catch(function(err) {
console.error(err);
});
window.navigator.mozSetMessageHandler('nfc-manager-tech-discovered',
(msg) => this._handleTechDiscovered(msg));
window.navigator.mozSetMessageHandler('nfc-manager-tech-lost',
(msg) => this._handleTechLost(msg));
// reseting nfc.status to default state, as the device could've
// been restarted when HW change was in progress
this.writeSetting({ 'nfc.status':'disabled' });
},
/**
* Removes all listeners and handlers
* @memberof NfcManager.prototype
*/
_stop: function nm_stop() {
this.debug('Stopping NFC Manager');
window.navigator.mozSetMessageHandler('nfc-manager-tech-discovered',
null);
window.navigator.mozSetMessageHandler('nfc-manager-tech-lost', null);
},
'_observe_nfc.enabled': function(enabled) {
this._doNfcStateTransition(enabled ? 'enable' : 'disable');
},
'_observe_nfc.debugging.enabled': function(enabled) {
this.DEBUG = enabled;
},
_handle_screenchange: function(evt) {
var nfcEvt = Service.query('screenEnabled') &&
!Service.query('locked') ?
'enable-polling' : 'disable-polling';
this._doNfcStateTransition(nfcEvt);
},
'_handle_lockscreen-appopened': function(evt) {
this._doNfcStateTransition('disable-polling');
},
'_handle_lockscreen-appclosed': function(evt) {
this._doNfcStateTransition('enable-polling');
},
/**
* Returns if NFC is active or not, depending on the hardware state
* @memberof NfcManager.prototype
* returns {boolean} isActive
*/
isActive: function nm_isActive() {
return this._hwState === 'enabled' || this._hwState === 'polling-on' ||
this._hwState === 'polling-off';
},
/**
* Handler for nfc-manager-tech-discovered messages which originate from
* gecko. Basing on the first NDEF record tnf and type this method can use
* NfcHandoverManager to handle handover scenarios. Basing on the techList
* array it can either trigger P2P sharing scenario or create MozActivities
* for other apps to act upon.
* @memberof NfcManager.prototype
* @param {Object} msg gecko originated message
* @param {Array} msg.records NDEF records
* @param {string} msg.type set to 'techDiscovered'
*/
_handleTechDiscovered: function nm_handleTechDiscovered(msg) {
this.debug('Technology Discovered: ' + JSON.stringify(msg));
msg = msg || {};
msg.records = Array.isArray(msg.records) ? msg.records : [];
this.publish('nfc-tech-discovered', this, /* without prefix */ true);
window.navigator.vibrate([25, 50, 125]);
if (this.nfcHandoverManager.tryHandover(msg.records, msg.peer)) {
return;
}
if (msg.records.length) {
this._fireNDEFDiscovered(msg.records);
} else if (msg.peer) {
this._checkP2PRegistration();
} else {
this.debug('Got tag without NDEF records, ignoring.');
}
},
/**
* Handler for nfc-manager-tech-lost messages
* @memberof NfcManager.prototype
* @param {Object} msg - tech lost message
*/
_handleTechLost: function nm_handleTechLost(msg) {
this.debug('Technology Lost: ' + JSON.stringify(msg));
window.navigator.vibrate([125, 50, 25]);
window.dispatchEvent(new CustomEvent('nfc-tech-lost'));
// Clean up P2P UI events
this._cleanP2PUI();
},
/**
* Performs NFC state transition. Checks in NFC HW State Table
* if NFC HW Event (argument) triggers a transition from current HW state
* to a different one. If transition exists, _hwState is changed to new
* state and state entry function is called.
* @memberof NfcManager.prototype
* @param {string} evt - NFC HW Event
*/
_doNfcStateTransition: function(evt) {
var evtIdx = NFC_HW_EVENTS.indexOf(evt);
var state = NFC_HW_STATE_TABLE[this._hwState][evtIdx];
if (!state) {
this.debug('no transition from ' + this._hwState + '[' + evt + ']');
return;
}
this.debug('state: ' + this._hwState + '[' + evt + ']' + ' -> ' + state);
this._hwState = state;
this._processNfcStateChange();
},
/**
* State entry function. Called after transitioning to new NFC HW state.
* Depending on _hwState it can trigger a new HW change request,
* change 'nfc.status' setting or update NFC icon.
* @memberof NfcManager.prototype
*/
_processNfcStateChange: function() {
var nfc = window.navigator.mozNfc;
var promise;
switch (this._hwState) {
case 'disabling':
this.writeSetting({ 'nfc.status': this._hwState });
promise = nfc.powerOff();
break;
case 'disabled':
this.writeSetting({ 'nfc.status': this._hwState });
this.icon && this.icon.update();
break;
case 'enabling':
this.writeSetting({ 'nfc.status': this._hwState });
promise = nfc.startPoll();
break;
case 'enabled':
this.writeSetting({ 'nfc.status': this._hwState });
this.icon && this.icon.update();
break;
case 'polling-on':
promise = nfc.startPoll();
break;
case 'polling-off':
promise = nfc.stopPoll();
break;
}
if (promise) {
promise.then(() => this._doNfcStateTransition('hw-change-success'))
.catch(() => this._doNfcStateTransition('hw-change-failure'));
}
},
/**
* Step 1 of system app fallback P2P sharing.
* Queries Gecko (via NFC dom) if currently visible app has registered
* onpeerready handler. If the result is true, shrinking-start event is
* dispatched to ShrinkingUI, which will trigger UI change asking the
* user to confirm sharing.
* @memberof NfcManager.prototype
*/
_checkP2PRegistration: function nm_checkP2PRegistration() {
var nfc = window.navigator.mozNfc;
var activeApp = this.service.query('getTopMostWindow');
var manifestURL = activeApp.manifestURL ||
this.service.manifestURL;
// Do not allow shrinking if we are on the private browser landing page.
if (activeApp.isPrivateBrowser() &&
activeApp.config.url.startsWith('app://')) {
return;
}
nfc.checkP2PRegistration(manifestURL).then(result => {
if (result) {
if (activeApp.isTransitioning() || activeApp.isSheetTransitioning()) {
return;
}
this._initP2PUI();
} else {
this.debug('CheckP2PRegistration failed');
this._cleanP2PUI();
}
});
},
/**
* P2P UI clean up helper, notifies ShrinkingUI to stop shrinking animation
* and removes 'shrinking-sent' listener.
* @memberof NfcManager.prototype
*/
_cleanP2PUI: function() {
window.removeEventListener('shrinking-sent', this._handleShrinkingSent);
this.publish('shrinking-stop', this, /* without prefix */ true);
},
/**
* Notifies ShrinkingUI to start shrinking animation and starts listening
* for 'shrinking-sent' event.
* @memberof NfcManager.prototype
*/
_initP2PUI: function() {
this.publish('shrinking-start', this, /* without prefix */ true);
this._handleShrinkingSent = () => {
this._cleanP2PUI();
this._dispatchP2PUserResponse();
};
window.addEventListener('shrinking-sent', this._handleShrinkingSent);
},
/**
* Step 2 of system app fallback P2P sharing.
* Notifies Gecko to fire onpeerready handler of the currently visible app.
* @memberof NfcManager.prototype
*/
_dispatchP2PUserResponse: function nm_dispatchP2PUserResponse() {
var nfc = window.navigator.mozNfc;
var activeApp = this.service.query('getTopMostWindow');
var manifestURL = activeApp.manifestURL ||
this.service.manifestURL;
nfc.notifyUserAcceptedP2P(manifestURL);
},
/**
* Fires NDEF related activities to launch other apps to perform
* further actions with NDEF Message contents. If the first NDEF record
* contains a well know type additional parsing will be done in helper
* methods. In general the name of activity will be 'nfc-ndef-discovered',
* in some case other names may be used (e.g. 'dial' in case of tel uri)
* @memberof NfcManager.prototype
* @param {Array} records - NDEF Message
*/
_fireNDEFDiscovered: function nm_fireNDEFDiscovered(records) {
this.debug('_fireNDEFDiscovered: ' + JSON.stringify(records));
var smartPoster = this._getSmartPoster(records);
var record = smartPoster || records[0] || { tnf: NDEF.TNF_EMPTY };
var data = NDEF.payload.decode(record.tnf, record.type, record.payload);
var options = this._createNDEFActivityOptions(data);
if (data !== null) {
options.data.records = records;
}
this.debug('_fireNDEFDiscovered activity: ', JSON.stringify(options));
var activity = new MozActivity(options);
activity.onerror = () => {
this.debug('Firing nfc-ndef-discovered activity failed');
};
},
/**
* Retrieves Smart Poster record from NDEF records array following
* the rule outlined in NFCForum-SmartPoster_RTD_1.0, 3.4:
* "If an NDEF message contains one or multiple URI [URI] records
* in addition to the Smart Poster record at the top level (i.e.,
* not nested), the Smart Poster record overrides them. The NDEF
* application MUST use only the Smart Poster record."
* @memberof NfcManager.prototype
* @param {Array} records - array of NDEF records
* @returns {Object} record - SmartPostr record or null
*/
_getSmartPoster: function nm_getSmartPoster(records) {
var nfcUtils = new NfcUtils();
if (!Array.isArray(records) || !records.length) {
return null;
}
var smartPosters = records.filter(function isSmartPoster(r) {
return nfcUtils.equalArrays(r.type, NDEF.RTD_SMART_POSTER);
});
if (smartPosters.length && records[0].tnf === NDEF.TNF_WELL_KNOWN &&
(nfcUtils.equalArrays(records[0].type, NDEF.RTD_URI) ||
nfcUtils.equalArrays(records[0].type, NDEF.RTD_SMART_POSTER))) {
return smartPosters[0];
}
return null;
},
/**
* Basing on decoded payload from first record of NDEF message prepares
* activity options object which will be used to launch MozActivity
* @memberof NfcManager.prototype
* @param {Object} payload - decoded payload of first record from NDEF msg
* @returns {Object} options - object used to construct MozActivity
*/
_createNDEFActivityOptions: function nm_createNDEFActivityOptions(payload) {
var options = { name: 'nfc-ndef-discovered', data: {}};
if (payload === null) {
return options;
}
if (payload.type === 'uri') {
if (payload.uri.indexOf('tel:') === 0) {
// dial a number
options.name = 'dial';
options.data.type = 'webtelephony/number';
options.data.number = payload.uri.substring(4);
options.data.uri = payload.uri;
} else if (payload.uri.indexOf('mailto:') === 0) {
// create new mail
options.name = 'new';
options.data.type = 'mail';
options.data.url = payload.uri;
} else if (payload.uri.indexOf('http://') === 0 ||
payload.uri.indexOf('https://') === 0 ||
payload.uri.indexOf('data:text/html') === 0) {
// launch browser
options.name = 'view';
options.data.type = 'url';
options.data.url = payload.uri;
} else {
options.data = payload;
}
} else if (payload.type === 'smartposter' &&
(payload.uri.indexOf('http://') === 0 ||
payload.uri.indexOf('https://') === 0)) {
// smartposter adaptation for browser handling
options.name = 'view';
options.data = payload;
options.data.type = 'url';
options.data.url = payload.uri;
delete options.data.uri;
} else if (payload.type === 'text/vcard') {
// contact import
options.name = 'import';
options.data = payload;
} else {
options.data = payload;
}
if (options.name !== 'nfc-ndef-discovered') {
options.data.src = 'nfc';
}
return options;
}
});
}());