VulpesOS/assets/webapps/bluetooth.gaiamobile.org/js/modules/pair_manager.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

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;
});