Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
430 lines
15 KiB
JavaScript
430 lines
15 KiB
JavaScript
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
|
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
|
|
|
/* global NotificationHelper */
|
|
|
|
define(['require','modules/bluetooth/bluetooth_adapter_manager','modules/bluetooth/bluetooth_context','views/pair_expired_dialog'],function(require) {
|
|
'use strict';
|
|
|
|
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
|
|
var BtContext = require('modules/bluetooth/bluetooth_context');
|
|
var PairExpiredDialog = require('views/pair_expired_dialog');
|
|
|
|
var _debug = false;
|
|
var debug = function() {};
|
|
if (_debug) {
|
|
debug = function btam_debug(msg) {
|
|
console.log('--> [PairManager]: ' + msg);
|
|
};
|
|
}
|
|
|
|
/*
|
|
* PairManager is responsible for:
|
|
* 1. Handling system message 'bluetooth-pairing-request' while there is an
|
|
* incoming/outgoing pairing request.
|
|
* 2. Handling dom event from BluetoothPairingListener while there is an
|
|
* incoming/outgoing pairing request.
|
|
* 3. Handling dom event 'ondeviceunpaired' while some remote devices
|
|
* request for canceling an overdue pairing request. The reason could be
|
|
* authentication fails, remote device down, and internal error happens.
|
|
*/
|
|
var PairManager = {
|
|
/**
|
|
* Default adapter of Bluetooth.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManger
|
|
* @type {Object} BluetoothAdapter
|
|
*/
|
|
_defaultAdapter: null,
|
|
/**
|
|
* Indicate if the pair request is expired
|
|
* @type {Boolean}
|
|
*/
|
|
_isExpired: false,
|
|
|
|
init: function() {
|
|
// Observe 'defaultAdapter' property for reaching default adapter.
|
|
AdapterManager.observe('defaultAdapter',
|
|
this._onDefaultAdapterChanged.bind(this));
|
|
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
|
|
|
|
// Watch pairing events.
|
|
this._watchOndisplaypasskeyreq();
|
|
this._watchOnenterpincodereq();
|
|
this._watchOnpairingconfirmationreq();
|
|
this._watchOnpairingconsentreq();
|
|
this._watchOnpairingaborted();
|
|
|
|
navigator.mozSetMessageHandler('bluetooth-pairing-request',
|
|
this._onRequestPairingFromSystemMessage.bind(this)
|
|
);
|
|
|
|
// Observe screen lockscreen locked/unlocked state for show pending
|
|
// pairing request immediately.
|
|
navigator.mozSettings.addObserver('lockscreen.locked',
|
|
function gotScreenLockedChanged(event) {
|
|
this.showPendingPairing(event.settingValue);
|
|
}.bind(this));
|
|
|
|
// Observe Bluetooth 'enabled' property from hardware side.
|
|
// Then, close pairing dialog immediately.
|
|
BtContext.observe('enabled', (enabled) => {
|
|
if (!enabled) {
|
|
this.onBluetoothDisabled();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 'defaultAdapter' change event handler from adapter manager for
|
|
* updating it immediately.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
* @param {Object} BluetoothAdapter newAdapter
|
|
*/
|
|
_onDefaultAdapterChanged: function(newAdapter) {
|
|
// save default adapter
|
|
this._defaultAdapter = newAdapter;
|
|
},
|
|
|
|
/**
|
|
* Watch 'ondisplaypasskeyreq' dom event for pairing.
|
|
* A handler to trigger when a remote bluetooth device requests to display
|
|
* passkey on the screen during pairing process.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_watchOndisplaypasskeyreq: function() {
|
|
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
|
|
debug('_watchOndisplaypasskeyreq() no adapter or pairingReqs');
|
|
return;
|
|
}
|
|
|
|
this._defaultAdapter.pairingReqs.addEventListener('displaypasskeyreq',
|
|
(evt) => {
|
|
debug('_watchOndisplaypasskeyreq()');
|
|
this._handlePairingRequest({
|
|
method: 'displaypasskey',
|
|
evt: evt
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Watch 'onenterpincodereq' dom event for pairing.
|
|
* A handler to trigger when a remote bluetooth device requests user enter
|
|
* PIN code during pairing process.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_watchOnenterpincodereq: function() {
|
|
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
|
|
debug('_watchOnenterpincodereq() no adapter or pairingReqs');
|
|
return;
|
|
}
|
|
|
|
this._defaultAdapter.pairingReqs.addEventListener('enterpincodereq',
|
|
(evt) => {
|
|
debug('_watchOnenterpincodereq()');
|
|
this._handlePairingRequest({
|
|
method: 'enterpincode',
|
|
evt: evt
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Watch 'onpairingconfirmationreq' dom event for pairing.
|
|
* A handler to trigger when a remote bluetooth device requests user
|
|
* confirm passkey during pairing process. Applications may prompt passkey
|
|
* to user for confirmation, or confirm the passkey for user proactively.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_watchOnpairingconfirmationreq: function() {
|
|
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
|
|
debug('_watchOnpairingconfirmationreq() no adapter or pairingReqs');
|
|
return;
|
|
}
|
|
|
|
this._defaultAdapter.pairingReqs.addEventListener(
|
|
'pairingconfirmationreq', (evt) => {
|
|
debug('_watchOnpairingconfirmationreq: evt = ' + JSON.stringify(evt));
|
|
this._handlePairingRequest({
|
|
method: 'confirmation',
|
|
evt: evt
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Watch 'onpairingconsentreq' dom event for pairing.
|
|
* A handler to trigger when a remote bluetooth device requests user
|
|
* confirm pairing during pairing process. Applications may prompt user
|
|
* for confirmation or confirm for user proactively.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_watchOnpairingconsentreq: function() {
|
|
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
|
|
debug('_watchOnpairingconsentreq() no adapter or pairingReqs');
|
|
return;
|
|
}
|
|
|
|
this._defaultAdapter.pairingReqs.addEventListener('pairingconsentreq',
|
|
(evt) => {
|
|
debug('_watchOnpairingconsentreq: evt = ' + JSON.stringify(evt));
|
|
this._handlePairingRequest({
|
|
method: 'consent',
|
|
evt: evt
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Watch 'onpairingaborted' dom event for pairing aborted.
|
|
* A handler to trigger when pairing fails due to one of
|
|
* following conditions:
|
|
* - authentication fails
|
|
* - remote device down (bluetooth ACL becomes disconnected)
|
|
* - internal error happens
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_watchOnpairingaborted: function() {
|
|
if (!this._defaultAdapter) {
|
|
return;
|
|
}
|
|
|
|
this._defaultAdapter.addEventListener('pairingaborted',
|
|
this._onPairingAborted.bind(this));
|
|
},
|
|
|
|
/**
|
|
* A handler to handle 'onpairingaborted' event while it's coming.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_onPairingAborted: function(evt) {
|
|
debug('_onPairingAborted(): evt = ' + JSON.stringify(evt));
|
|
// if the attention screen still open, close it
|
|
if (this.childWindow) {
|
|
this.childWindow.Pairview.closeInput();
|
|
this.childWindow.close();
|
|
}
|
|
|
|
// If "onpairingaborted" event is coming, and there is a pending pairing
|
|
// request, we have reset the object. Because the callback is useless
|
|
// since the older pairing request is expired.
|
|
// The moment is fit to notify user that the later pairing request
|
|
// is timeout or canceled. According to user story, do nothing here.
|
|
// Clear up the pending pairing request only.
|
|
if (this.pendingPairing) {
|
|
this.pendingPairing = null;
|
|
}
|
|
|
|
// Close pairing expired dialog if it is showing.
|
|
if (PairExpiredDialog.isVisible) {
|
|
PairExpiredDialog.close();
|
|
// Have to close Bluetooth app after the dialog is closed.
|
|
window.close();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Receive the system message event for launch Bluetooth app here.
|
|
*
|
|
* @access private
|
|
* @memberOf PairManager
|
|
*/
|
|
_onRequestPairingFromSystemMessage: function() {
|
|
debug('onRequestPairingFromSystemMessage():');
|
|
},
|
|
|
|
/**
|
|
* It is used to handle each pairing request from different pairing methods.
|
|
*
|
|
* @memberOf PairManager
|
|
* @access private
|
|
* @param {Object} pairingInfo
|
|
* @param {Object} pairingInfo.method - method of this pairing request
|
|
* @param {Object} pairingInfo.evt - DOM evt of this pairing request
|
|
*/
|
|
_handlePairingRequest: function(pairingInfo) {
|
|
debug('_onRequestPairing():' +
|
|
' pairingInfo.method = ' + pairingInfo.method +
|
|
' pairingInfo.evt = ' + pairingInfo.evt);
|
|
|
|
var req = navigator.mozSettings.createLock().get('lockscreen.locked');
|
|
req.onsuccess = () => {
|
|
if (req.result['lockscreen.locked']) {
|
|
// notify user that we are receiving pairing request
|
|
this.fireNotification(pairingInfo);
|
|
} else {
|
|
// We have clear up pending one before show the new pairing requst.
|
|
// Becasue the pending pairing request is no longer usefull.
|
|
this.cleanPendingPairing();
|
|
|
|
// show pair view directly while lock screen is unlocked
|
|
this.showPairview(pairingInfo);
|
|
}
|
|
};
|
|
req.onerror = () => {
|
|
// We have clear up pending one before show the new pairing requst.
|
|
// Becasue the pending pairing request is no longer usefull.
|
|
this.cleanPendingPairing();
|
|
|
|
// fallback to default value 'unlocked'
|
|
this.showPairview(pairingInfo);
|
|
};
|
|
},
|
|
|
|
fireNotification: function(pairingInfo) {
|
|
// Once we received a pairing request in screen locked mode,
|
|
// overwrite the object with the latest pairing request. Because the
|
|
// older pairing request might be timeout and useless now.
|
|
this.pendingPairing = {
|
|
showPairviewCallback: this.showPairview.bind(this, pairingInfo)
|
|
};
|
|
// Prepare notification toast.
|
|
var titleL10n = 'bluetooth-pairing-request-now-title';
|
|
var bodyL10n = pairingInfo.evt.deviceName ?
|
|
{ raw: pairingInfo.evt.deviceName } :
|
|
'unnamed-device';
|
|
var iconUrl =
|
|
'app://bluetooth.gaiamobile.org/style/images/icon_bluetooth.png';
|
|
// We always use tag "pairing-request" to manage these notifications.
|
|
var notificationId = 'pairing-request';
|
|
return NotificationHelper.send(titleL10n, {
|
|
bodyL10n: bodyL10n,
|
|
icon: iconUrl,
|
|
tag: notificationId,
|
|
closeOnClick: false
|
|
}).then(notification => {
|
|
// set onclick handler for the notification
|
|
notification.onclick =
|
|
this.pairingRequestExpiredNotificationHandler.bind(this,
|
|
notification);
|
|
});
|
|
},
|
|
|
|
// According to user story, it won't notify user again
|
|
// while the pending pairing request is just timeout or canceled.
|
|
// So we have to set onclick handler in this moment.
|
|
// The handler will pop out a pairing request expired prompt only.
|
|
pairingRequestExpiredNotificationHandler: function(notification) {
|
|
var req = navigator.mozSettings.createLock().get('lockscreen.locked');
|
|
req.onsuccess = () => {
|
|
// Avoid to do nothting while the notification toast is showing
|
|
// and a user is able to trigger onclick event. Make sure screen
|
|
// is unlocked, then show the prompt in bluetooth app.
|
|
if (!req.result['lockscreen.locked']) {
|
|
// Clean the pairing request notficiation which is expired.
|
|
notification.close();
|
|
|
|
navigator.mozApps.getSelf().onsuccess = (evt) => {
|
|
var app = evt.target.result;
|
|
|
|
// launch bluetooth app to foreground for showing the prompt
|
|
if (PairExpiredDialog.isVisible) {
|
|
app.launch();
|
|
}
|
|
|
|
// show an alert with the overdue message
|
|
if (!PairExpiredDialog.isVisible && this._isExpired) {
|
|
debug('show expired dialog');
|
|
PairExpiredDialog.showConfirm(function() {
|
|
// Have to close Bluetooth app after the dialog is closed.
|
|
window.close();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
};
|
|
},
|
|
|
|
// If there is a pending pairing request while a user just unlocks screen,
|
|
// we will show pair view immediately. Then, we clear up the notification.
|
|
// If pendingPairing object is not exist, it means pair request is expired.
|
|
showPendingPairing: function(screenLocked) {
|
|
debug('showPendingPairing when screenLocked:' + screenLocked);
|
|
if (!screenLocked) {
|
|
if (this.pendingPairing) {
|
|
this._isExpired = false;
|
|
// show pair view from the callback function
|
|
if (this.pendingPairing.showPairviewCallback) {
|
|
this.pendingPairing.showPairviewCallback();
|
|
}
|
|
|
|
this.cleanPendingPairing();
|
|
} else {
|
|
this._isExpired = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
cleanPendingPairing: function() {
|
|
debug('cleanPendingPairing(): has pendingPairing = ' +
|
|
(this.pendingPairing));
|
|
|
|
// Clear up the pending pairing request
|
|
if (this.pendingPairing) {
|
|
this.pendingPairing = null;
|
|
}
|
|
|
|
// Clear up the pairing request from notification.
|
|
this.cleanNotifications();
|
|
},
|
|
|
|
// Clean all notifications which are fired with tag 'pairing-request'.
|
|
cleanNotifications: function() {
|
|
Notification.get().then(function(notifications) {
|
|
if (notifications) {
|
|
notifications.forEach(function(notification) {
|
|
// Compare tags, as the tag is based on the "pairing-request" and
|
|
// we only have one notification for the request. Plus, there
|
|
// is no "id" field on the notification.
|
|
if (notification.tag === 'pairing-request' &&
|
|
notification.close) {
|
|
notification.close();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
showPairview: function(pairingInfo) {
|
|
debug('showPairview(): pairingInfo = ' + pairingInfo);
|
|
var protocol = window.location.protocol;
|
|
var host = window.location.host;
|
|
this.childWindow = window.open(protocol + '//' + host + '/onpair.html',
|
|
'pair_screen', 'attention');
|
|
this.childWindow.onload = () => {
|
|
this.childWindow.Pairview.init(pairingInfo.method, pairingInfo.evt);
|
|
};
|
|
},
|
|
|
|
onBluetoothDisabled: function() {
|
|
debug('onBluetoothDisabled():');
|
|
|
|
// if the attention screen still open, close it
|
|
if (this.childWindow) {
|
|
this.childWindow.Pairview.closeInput();
|
|
this.childWindow.close();
|
|
}
|
|
|
|
// Since Bluetooth is off, close itself.
|
|
window.close();
|
|
}
|
|
};
|
|
|
|
return PairManager;
|
|
});
|