blob: b3ece1c910739e416428ba8c74b95f1ac71ce10c [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Enumeration of possible states during pairing. The value associated with
* each state maps to a localized string in the global variable
* |loadTimeData|.
* @enum {string}
*/
var BluetoothPairingEventType = {
CONNECTING: 'bluetoothStartConnecting',
ENTER_PIN_CODE: 'bluetoothEnterPinCode',
ENTER_PASSKEY: 'bluetoothEnterPasskey',
REMOTE_PIN_CODE: 'bluetoothRemotePinCode',
REMOTE_PASSKEY: 'bluetoothRemotePasskey',
CONFIRM_PASSKEY: 'bluetoothConfirmPasskey',
CONNECT_FAILED: 'bluetoothConnectFailed',
CANCELED: 'bluetoothPairingCanceled',
DISMISSED: 'bluetoothPairingDismissed', // pairing dismissed (succeeded or
// canceled).
NOOP: '' // Update device but do not show or update the dialog.
};
/**
* @typedef {{pairing: (BluetoothPairingEventType|undefined),
* device: !chrome.bluetooth.Device,
* pincode: (string|undefined),
* passkey: (number|undefined),
* enteredKey: (number|undefined)}}
*/
var BluetoothPairingEvent;
/**
* Returns a BluetoothPairingEventType corresponding to |event_type|.
* @param {!chrome.bluetoothPrivate.PairingEventType} event_type
* @return {BluetoothPairingEventType}
*/
function GetBluetoothPairingEvent(event_type) {
switch (event_type) {
case chrome.bluetoothPrivate.PairingEventType.REQUEST_PINCODE:
return BluetoothPairingEventType.ENTER_PIN_CODE;
case chrome.bluetoothPrivate.PairingEventType.DISPLAY_PINCODE:
return BluetoothPairingEventType.REMOTE_PIN_CODE;
case chrome.bluetoothPrivate.PairingEventType.REQUEST_PASSKEY:
return BluetoothPairingEventType.ENTER_PASSKEY;
case chrome.bluetoothPrivate.PairingEventType.DISPLAY_PASSKEY:
return BluetoothPairingEventType.REMOTE_PASSKEY;
case chrome.bluetoothPrivate.PairingEventType.KEYS_ENTERED:
return BluetoothPairingEventType.NOOP;
case chrome.bluetoothPrivate.PairingEventType.CONFIRM_PASSKEY:
return BluetoothPairingEventType.CONFIRM_PASSKEY;
case chrome.bluetoothPrivate.PairingEventType.REQUEST_AUTHORIZATION:
return BluetoothPairingEventType.NOOP;
case chrome.bluetoothPrivate.PairingEventType.COMPLETE:
return BluetoothPairingEventType.NOOP;
}
return BluetoothPairingEventType.NOOP;
}
cr.define('options', function() {
/** @const */ var Page = cr.ui.pageManager.Page;
/** @const */ var PageManager = cr.ui.pageManager.PageManager;
/**
* List of IDs for conditionally visible elements in the dialog.
* @type {Array<string>}
* @const
*/
var ELEMENTS = ['bluetooth-pairing-passkey-display',
'bluetooth-pairing-passkey-entry',
'bluetooth-pairing-pincode-entry',
'bluetooth-pair-device-connect-button',
'bluetooth-pair-device-cancel-button',
'bluetooth-pair-device-accept-button',
'bluetooth-pair-device-reject-button',
'bluetooth-pair-device-dismiss-button'];
/**
* Encapsulated handling of the Bluetooth device pairing page.
* @constructor
* @extends {cr.ui.pageManager.Page}
*/
function BluetoothPairing() {
Page.call(this, 'bluetoothPairing',
loadTimeData.getString('bluetoothOptionsPageTabTitle'),
'bluetooth-pairing');
}
cr.addSingletonGetter(BluetoothPairing);
BluetoothPairing.prototype = {
__proto__: Page.prototype,
/**
* Device pairing event.
* @type {?BluetoothPairingEvent}
* @private
*/
event_: null,
/**
* Can the dialog be programmatically dismissed.
* @type {boolean}
*/
dismissible_: true,
/** @override */
initializePage: function() {
Page.prototype.initializePage.call(this);
var self = this;
$('bluetooth-pair-device-cancel-button').onclick = function() {
PageManager.closeOverlay();
};
$('bluetooth-pair-device-reject-button').onclick = function() {
var options = {
device: self.event_.device,
response: chrome.bluetoothPrivate.PairingResponse.REJECT
};
chrome.bluetoothPrivate.setPairingResponse(options);
self.event_.pairing = BluetoothPairingEventType.DISMISSED;
PageManager.closeOverlay();
};
$('bluetooth-pair-device-connect-button').onclick = function() {
// Prevent sending a 'connect' command twice.
$('bluetooth-pair-device-connect-button').disabled = true;
var options = {
device: self.event_.device,
response: chrome.bluetoothPrivate.PairingResponse.CONFIRM
};
var passkey = self.event_.passkey;
if (passkey) {
options.passkey = passkey;
} else if (!$('bluetooth-pairing-passkey-entry').hidden) {
options.passkey = parseInt($('bluetooth-passkey').value, 10);
} else if (!$('bluetooth-pairing-pincode-entry').hidden) {
options.pincode = $('bluetooth-pincode').value;
} else {
BluetoothPairing.connect(self.event_.device);
return;
}
chrome.bluetoothPrivate.setPairingResponse(options);
var event = /** @type {!BluetoothPairingEvent} */ ({
pairing: BluetoothPairingEventType.CONNECTING,
device: self.event_.device
});
BluetoothPairing.showDialog(event);
};
$('bluetooth-pair-device-accept-button').onclick = function() {
var options = {
device: self.event_.device,
response: chrome.bluetoothPrivate.PairingResponse.CONFIRM
};
chrome.bluetoothPrivate.setPairingResponse(options);
// Prevent sending a 'accept' command twice.
$('bluetooth-pair-device-accept-button').disabled = true;
};
$('bluetooth-pair-device-dismiss-button').onclick = function() {
PageManager.closeOverlay();
};
$('bluetooth-passkey').oninput = function() {
var inputField = $('bluetooth-passkey');
var value = inputField.value;
// Note that using <input type="number"> is insufficient to restrict
// the input as it allows negative numbers and does not limit the
// number of charactes typed even if a range is set. Furthermore,
// it sometimes produces strange repaint artifacts.
var filtered = value.replace(/[^0-9]/g, '');
if (filtered != value)
inputField.value = filtered;
$('bluetooth-pair-device-connect-button').disabled =
inputField.value.length == 0;
};
$('bluetooth-pincode').oninput = function() {
$('bluetooth-pair-device-connect-button').disabled =
$('bluetooth-pincode').value.length == 0;
};
$('bluetooth-passkey').addEventListener('keydown',
this.keyDownEventHandler_.bind(this));
$('bluetooth-pincode').addEventListener('keydown',
this.keyDownEventHandler_.bind(this));
},
/** @override */
didClosePage: function() {
if (this.event_ &&
this.event_.pairing != BluetoothPairingEventType.DISMISSED &&
this.event_.pairing != BluetoothPairingEventType.CONNECT_FAILED) {
this.event_.pairing = BluetoothPairingEventType.CANCELED;
var options = {
device: this.event_.device,
response: chrome.bluetoothPrivate.PairingResponse.CANCEL
};
chrome.bluetoothPrivate.setPairingResponse(options);
}
},
/**
* Override to prevent showing the overlay if the Bluetooth device details
* have not been specified. Prevents showing an empty dialog if the user
* quits and restarts Chrome while in the process of pairing with a device.
* @return {boolean} True if the overlay can be displayed.
*/
canShowPage: function() {
return !!(this.event_ && this.event_.device.address &&
this.event_.pairing);
},
/**
* Sets input focus on the passkey or pincode field if appropriate.
*/
didShowPage: function() {
if (!$('bluetooth-pincode').hidden)
$('bluetooth-pincode').focus();
else if (!$('bluetooth-passkey').hidden)
$('bluetooth-passkey').focus();
},
/**
* Configures the overlay for pairing a device.
* @param {!BluetoothPairingEvent} event
* @param {boolean=} opt_notDismissible
*/
update: function(event, opt_notDismissible) {
assert(event);
assert(event.device);
if (this.event_ == undefined ||
this.event_.device.address != event.device.address) {
// New event or device, create a new BluetoothPairingEvent.
this.event_ =
/** @type {BluetoothPairingEvent} */ ({device: event.device});
} else {
// Update to an existing event; just update |device| in case it changed.
this.event_.device = event.device;
}
if (event.pairing)
this.event_.pairing = event.pairing;
if (!this.event_.pairing)
return;
if (this.event_.pairing == BluetoothPairingEventType.CANCELED) {
// If we receive an update after canceling a pairing (e.g. a key
// press), ignore it and clear the device so that future updates for
// the device will also be ignored.
this.event_.device.address = '';
return;
}
if (event.pincode != undefined)
this.event_.pincode = event.pincode;
if (event.passkey != undefined)
this.event_.passkey = event.passkey;
if (event.enteredKey != undefined)
this.event_.enteredKey = event.enteredKey;
// Update the list model (in case, e.g. the name changed).
if (this.event_.device.name) {
var list = $('bluetooth-unpaired-devices-list');
if (list) { // May be undefined in tests.
var index = list.find(this.event_.device.address);
if (index != undefined)
list.dataModel.splice(index, 1, this.event_.device);
}
}
// Update the pairing instructions.
var instructionsEl = assert($('bluetooth-pairing-instructions'));
this.clearElement_(instructionsEl);
this.dismissible_ = opt_notDismissible !== true;
var message = loadTimeData.getString(this.event_.pairing);
assert(typeof this.event_.device.name == 'string');
message = message.replace(
'%1', /** @type {string} */(this.event_.device.name));
instructionsEl.textContent = message;
// Update visibility of dialog elements.
if (this.event_.passkey) {
this.updatePasskey_(String(this.event_.passkey));
if (this.event_.pairing == BluetoothPairingEventType.CONFIRM_PASSKEY) {
// Confirming a match between displayed passkeys.
this.displayElements_(['bluetooth-pairing-passkey-display',
'bluetooth-pair-device-accept-button',
'bluetooth-pair-device-reject-button']);
$('bluetooth-pair-device-accept-button').disabled = false;
} else {
// Remote entering a passkey.
this.displayElements_(['bluetooth-pairing-passkey-display',
'bluetooth-pair-device-cancel-button']);
}
} else if (this.event_.pincode) {
this.updatePasskey_(String(this.event_.pincode));
this.displayElements_(['bluetooth-pairing-passkey-display',
'bluetooth-pair-device-cancel-button']);
} else if (this.event_.pairing ==
BluetoothPairingEventType.ENTER_PIN_CODE) {
// Prompting the user to enter a PIN code.
this.displayElements_(['bluetooth-pairing-pincode-entry',
'bluetooth-pair-device-connect-button',
'bluetooth-pair-device-cancel-button']);
$('bluetooth-pincode').value = '';
} else if (this.event_.pairing ==
BluetoothPairingEventType.ENTER_PASSKEY) {
// Prompting the user to enter a passkey.
this.displayElements_(['bluetooth-pairing-passkey-entry',
'bluetooth-pair-device-connect-button',
'bluetooth-pair-device-cancel-button']);
$('bluetooth-passkey').value = '';
} else if (this.event_.pairing == BluetoothPairingEventType.CONNECTING) {
// Starting the pairing process.
this.displayElements_(['bluetooth-pair-device-cancel-button']);
} else {
// Displaying an error message.
this.displayElements_(['bluetooth-pair-device-dismiss-button']);
}
// User is required to enter a passkey or pincode before the connect
// button can be enabled. The 'oninput' methods for the input fields
// determine when the connect button becomes active.
$('bluetooth-pair-device-connect-button').disabled = true;
},
/**
* Handles the ENTER key for the passkey or pincode entry field.
* @param {Event} event A keydown event.
* @private
*/
keyDownEventHandler_: function(event) {
/** @const */ var ENTER_KEY_CODE = 13;
if (event.keyCode == ENTER_KEY_CODE) {
var button = $('bluetooth-pair-device-connect-button');
if (!button.hidden)
button.click();
}
},
/**
* Updates the visibility of elements in the dialog.
* @param {Array<string>} list List of conditionally visible elements that
* are to be made visible.
* @private
*/
displayElements_: function(list) {
var enabled = {};
for (var i = 0; i < list.length; i++) {
var key = list[i];
enabled[key] = true;
}
for (var i = 0; i < ELEMENTS.length; i++) {
var key = ELEMENTS[i];
$(key).hidden = !enabled[key];
}
},
/**
* Removes all children from an element.
* @param {!Element} element Target element to clear.
*/
clearElement_: function(element) {
var child = element.firstChild;
while (child) {
element.removeChild(child);
child = element.firstChild;
}
},
/**
* Formats an element for displaying the passkey or PIN code.
* @param {string} key Passkey or PIN to display.
*/
updatePasskey_: function(key) {
var passkeyEl = assert($('bluetooth-pairing-passkey-display'));
var keyClass =
(this.event_.pairing == BluetoothPairingEventType.REMOTE_PASSKEY ||
this.event_.pairing == BluetoothPairingEventType.REMOTE_PIN_CODE) ?
'bluetooth-keyboard-button' :
'bluetooth-passkey-char';
this.clearElement_(passkeyEl);
// Passkey should always have 6 digits.
key = '000000'.substring(0, 6 - key.length) + key;
var progress = this.event_.enteredKey;
for (var i = 0; i < key.length; i++) {
var keyEl = document.createElement('span');
keyEl.textContent = key.charAt(i);
keyEl.className = keyClass;
if (progress != undefined) {
if (i < progress)
keyEl.classList.add('key-typed');
else if (i == progress)
keyEl.classList.add('key-next');
else
keyEl.classList.add('key-untyped');
}
passkeyEl.appendChild(keyEl);
}
if (this.event_.pairing == BluetoothPairingEventType.REMOTE_PASSKEY ||
this.event_.pairing == BluetoothPairingEventType.REMOTE_PIN_CODE) {
// Add enter key.
var label = loadTimeData.getString('bluetoothEnterKey');
var keyEl = document.createElement('span');
keyEl.textContent = label;
keyEl.className = keyClass;
keyEl.id = 'bluetooth-enter-key';
if (progress != undefined) {
if (progress > key.length)
keyEl.classList.add('key-typed');
else if (progress == key.length)
keyEl.classList.add('key-next');
else
keyEl.classList.add('key-untyped');
}
passkeyEl.appendChild(keyEl);
}
passkeyEl.hidden = false;
},
};
/**
* Configures the device pairing instructions and displays the pairing
* overlay.
* @param {!BluetoothPairingEvent} event
* @param {boolean=} opt_notDismissible If set to true, the dialog can not
* be dismissed.
*/
BluetoothPairing.showDialog = function(event, opt_notDismissible) {
BluetoothPairing.getInstance().update(event, opt_notDismissible);
PageManager.showPageByName('bluetoothPairing', false);
};
/**
* Handles bluetoothPrivate onPairing events.
* @param {!chrome.bluetoothPrivate.PairingEvent} event
*/
BluetoothPairing.onBluetoothPairingEvent = function(event) {
var dialog = BluetoothPairing.getInstance();
if (!dialog.event_ || dialog.event_.device.address != event.device.address)
return; // Ignore events not associated with an active connect or pair.
var pairingEvent = /** @type {!BluetoothPairingEvent} */ ({
pairing: GetBluetoothPairingEvent(event.pairing),
device: event.device,
pincode: event.pincode,
passkey: event.passkey,
enteredKey: event.enteredKey
});
dialog.update(pairingEvent);
PageManager.showPageByName('bluetoothPairing', false);
};
/**
* Displays a message from the Bluetooth adapter.
* @param {{message: string, address: string}} data Data for constructing the
* message. |data.message| is the name of message to show. |data.address|
* is the device address.
*/
BluetoothPairing.showMessage = function(data) {
/** @type {string} */ var name = data.address;
if (name.length == 0)
return;
var dialog = BluetoothPairing.getInstance();
if (dialog.event_ && name == dialog.event_.device.address &&
dialog.event_.pairing == BluetoothPairingEventType.CANCELED) {
// Do not show any error message after cancelation of the pairing.
return;
}
var list = $('bluetooth-paired-devices-list');
if (list) {
var index = list.find(name);
if (index == undefined) {
list = $('bluetooth-unpaired-devices-list');
index = list.find(name);
}
if (index != undefined) {
var entry = list.dataModel.item(index);
if (entry && entry.name)
name = /** @type {string} */ (entry.name);
}
}
var event = /** @type {!BluetoothPairingEvent} */ ({
pairing: /** @type {BluetoothPairingEventType} */ (data.message),
device: /** @type {!chrome.bluetooth.Device} */ ({
name: name,
address: data.address,
})
});
BluetoothPairing.showDialog(event, true /* not dismissible */);
};
/**
* Sends a connect request to the bluetoothPrivate API. If there is an error
* the pairing dialog will be shown with the error message.
* @param {!chrome.bluetooth.Device} device
* @param {boolean=} opt_showConnecting If true, show 'connecting' message in
* the pairing dialog.
*/
BluetoothPairing.connect = function(device, opt_showConnecting) {
if (opt_showConnecting) {
var event = /** @type {!BluetoothPairingEvent} */ (
{pairing: BluetoothPairingEventType.CONNECTING, device: device});
BluetoothPairing.showDialog(event);
}
var address = device.address;
chrome.bluetoothPrivate.connect(address, function(result) {
BluetoothPairing.connectCompleted_(address, result);
});
};
/**
* Connect request completion callback.
* @param {string} address
* @param {chrome.bluetoothPrivate.ConnectResultType} result
*/
BluetoothPairing.connectCompleted_ = function(address, result) {
var message;
if (chrome.runtime.lastError) {
var errorMessage = chrome.runtime.lastError.message;
if (errorMessage != 'Connect failed') {
console.error('bluetoothPrivate.connect: Unexpected error for: ' +
address + ': ' + errorMessage);
}
}
switch (result) {
case chrome.bluetoothPrivate.ConnectResultType.SUCCESS:
case chrome.bluetoothPrivate.ConnectResultType.ALREADY_CONNECTED:
BluetoothPairing.dismissDialog();
return;
case chrome.bluetoothPrivate.ConnectResultType.UNKNOWN_ERROR:
message = 'bluetoothConnectUnknownError';
break;
case chrome.bluetoothPrivate.ConnectResultType.IN_PROGRESS:
message = 'bluetoothConnectInProgress';
break;
case chrome.bluetoothPrivate.ConnectResultType.FAILED:
message = 'bluetoothConnectFailed';
break;
case chrome.bluetoothPrivate.ConnectResultType.AUTH_FAILED:
message = 'bluetoothConnectAuthFailed';
break;
case chrome.bluetoothPrivate.ConnectResultType.AUTH_CANCELED:
message = 'bluetoothConnectAuthCanceled';
break;
case chrome.bluetoothPrivate.ConnectResultType.AUTH_REJECTED:
message = 'bluetoothConnectAuthRejected';
break;
case chrome.bluetoothPrivate.ConnectResultType.AUTH_TIMEOUT:
message = 'bluetoothConnectAuthTimeout';
break;
case chrome.bluetoothPrivate.ConnectResultType.UNSUPPORTED_DEVICE:
message = 'bluetoothConnectUnsupportedDevice';
break;
case chrome.bluetoothPrivate.ConnectResultType.ATTRIBUTE_LENGTH_INVALID:
message = 'bluetoothConnectAttributeLengthInvalid';
break;
case chrome.bluetoothPrivate.ConnectResultType.CONNECTION_CONGESTED:
message = 'bluetoothConnectConnectionCongested';
break;
case chrome.bluetoothPrivate.ConnectResultType.INSUFFICIENT_ENCRYPTION:
message = 'bluetoothConnectInsufficientEncryption';
break;
case chrome.bluetoothPrivate.ConnectResultType.OFFSET_INVALID:
message = 'bluetoothConnectOffsetInvalid';
break;
case chrome.bluetoothPrivate.ConnectResultType.READ_NOT_PERMITTED:
message = 'bluetoothConnectReadNotPermitted';
break;
case chrome.bluetoothPrivate.ConnectResultType.REQUEST_NOT_SUPPORTED:
message = 'bluetoothConnectRequestNotSupported';
break;
case chrome.bluetoothPrivate.ConnectResultType.WRITE_NOT_PERMITTED:
message = 'bluetoothConnectWriteNotPermitted';
break;
}
if (message)
BluetoothPairing.showMessage({message: message, address: address});
};
/**
* Closes the Bluetooth pairing dialog.
*/
BluetoothPairing.dismissDialog = function() {
var overlay = PageManager.getTopmostVisiblePage();
var dialog = BluetoothPairing.getInstance();
if (overlay == dialog && dialog.dismissible_) {
if (dialog.event_)
dialog.event_.pairing = BluetoothPairingEventType.DISMISSED;
PageManager.closeOverlay();
}
};
// Export
return {
BluetoothPairing: BluetoothPairing
};
});