1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
9 var BluetoothDevice = function() {
10 // The device's address (MAC format, must be unique).
13 // The label which shows up in the devices list for this device.
16 // The text label of the selected device class.
17 this.class = 'Computer';
19 // The uint32 value of the selected device class.
20 this.classValue = 0x104;
22 // Whether or not the device shows up in the system tray's observed list of
24 this.discoverable = false;
26 // Whether Chrome OS pairs with this device, or this device tries to pair
28 this.incoming = false;
30 // A trusted device is one which is plugged directly Chrome OS and therefore
31 // is paired by default, but not connected.
32 this.isTrusted = false;
34 // The device's name.This is not the label which shows up in the devices list
35 // here or in the system tray--use |.alias| to edit that label.
38 // The designated path for the device. Must be unique.
41 // Whether or not the device is paired with Chrome OS.
44 // The label of the selected pairing method option.
45 this.pairingMethod = 'None';
47 // The text containing a PIN key or passkey for pairing.
48 this.pairingAuthToken = '';
50 // The label of the selected pairing action option.
51 this.pairingAction = '';
55 is: 'bluetooth-settings',
59 * The title to be displayed in a heading element for the element.
61 title: {type: String},
64 * A set of bluetooth devices.
65 * @type !Array<!BluetoothDevice>
67 devices: {type: Array, value: function() { return []; }},
70 * A set of predefined bluetooth devices.
71 * @type !Array<!Bluetooth>
73 predefinedDevices: {type: Array, value: function() { return []; }},
76 * A bluetooth device object which is currently being edited.
77 * @type {BluetoothDevice}
79 currentEditableObject: {
81 value: function() { return {}; }
85 * The index of the bluetooth device object which is currently being edited.
86 * This is initially set to -1 (i.e. no device selected) because not custom
87 * devices exist when the page loads.
89 currentEditIndex: {type: Number, value: function() { return -1; }},
92 * A set of options for the possible bluetooth device classes/types.
93 * Object |value| attribute comes from values in the WebUI, set in
94 * setDeviceClassOptions.
95 * @type !Array<! {text: string, value: int} >
101 {text: 'Unknown', value: 0},
102 {text: 'Mouse', value: 0x2580},
103 {text: 'Keyboard', value: 0x2540},
104 {text: 'Audio', value: 0x240408},
105 {text: 'Phone', value: 0x7a020c},
106 {text: 'Computer', value: 0x104}
112 * A set of strings representing the method to be used for
113 * authenticating a device during a pair request.
114 * @type !Array<string>
116 deviceAuthenticationMethods: {
118 value: function() { return []; }
122 * A set of strings representing the actions which can be done when
123 * a secure device is paired/requests a pair.
124 * @type !Array<string>
126 deviceAuthenticationActions: {
128 value: function() { return []; }
133 * Contains keys for all the device paths which have been discovered. Used
134 * to look up whether or not a device is listed already.
139 ready: function() { this.title = 'Bluetooth'; },
141 initialize: function() {
142 if (!this.initialized) {
143 this.initialized = true;
144 chrome.send('requestBluetoothInfo');
148 observers: ['currentEditableObjectChanged(currentEditableObject.*)'],
151 * Called when a property of the currently editable object is edited.
152 * Sets the corresponding property for the object in |this.devices|.
153 * @param {Object} obj An object containing event information (ex. which
154 * property of |this.currentEditableObject| was changed, what its value
157 currentEditableObjectChanged: function(obj) {
158 if (this.currentEditIndex >= 0) {
159 var prop = obj.path.split('.')[1];
160 this.set('devices.' + this.currentEditIndex.toString() + '.' + prop,
166 * Called when the device edit modal is opened. Re-validates necessary input
169 editDialogOpened: function() {
170 this.validateAddress();
174 handleAddressInput: function() {
175 this.autoFormatAddress();
176 this.validateAddress();
179 autoFormatAddress: function() {
180 var input = this.$.deviceAddressInput;
181 var regex = /([a-f0-9]{2})([a-f0-9]{2})/i;
182 // Remove things that aren't hex characters from the string.
183 var val = input.value.replace(/[^a-f0-9]/ig, '');
185 // Insert a ':' in the middle of every four hex characters.
186 while (regex.test(val))
187 val = val.replace(regex, '$1:$2');
193 * Called on-input from an input element and on edit modal open.
194 * Validates whether or not the
195 * input's content matches a regular expression. If the input's value
196 * satisfies the regex, then make sure that the address is not already
199 validateAddress: function() {
200 var input = this.$.deviceAddressInput;
201 var val = input.value;
203 var addressRegex = RegExp('^([\\da-fA-F]{2}:){5}[\\da-fA-F]{2}$');
204 if (addressRegex.test(val)) {
205 for (var i = 0; i < this.predefinedDevices.length; ++i) {
206 if (this.predefinedDevices[i].address == val) {
213 for (var i = 0; i < this.devices.length; ++i) {
214 if (this.devices[i].address == val && i != this.currentEditIndex) {
222 input.invalid = true;
223 input.errorMessage = 'This address is already being used.';
225 input.invalid = false;
228 input.invalid = true;
229 input.errorMessage = 'Invalid address.';
234 * Makes sure that a path is not already used.
236 validatePath: function() {
237 var input = this.$.devicePathInput;
238 var val = input.value;
241 for (var i = 0; i < this.predefinedDevices.length; ++i) {
242 if (this.predefinedDevices[i].path == val) {
249 for (var i = 0; i < this.devices.length; ++i) {
250 if (this.devices[i].path == val && i != this.currentEditIndex) {
258 input.invalid = true;
259 input.errorMessage = 'This path is already being used.';
261 input.invalid = false;
266 * Checks whether or not the PIN/passkey input field should be shown.
267 * It should only be shown when the pair method is not 'None' or empty.
268 * @param {string} pairMethod The label of the selected pair method option
269 * for a particular device.
270 * @return {boolean} Whether the PIN/passkey input field should be shown.
272 showAuthToken: function(pairMethod) {
273 return pairMethod && pairMethod != 'None';
277 * Called by the WebUI which provides a list of devices which are connected
278 * to the main adapter.
279 * @param {!Array<!BluetoothDevice>} devices A list of bluetooth devices.
281 updateBluetoothInfo: function(predefinedDevices, loadedCustomDevices,
282 pairingMethodOptions, pairingActionOptions) {
283 this.predefinedDevices = this.loadDevicesFromList(predefinedDevices, true);
284 this.devices = this.loadDevicesFromList(loadedCustomDevices, false);
285 this.deviceAuthenticationMethods = pairingMethodOptions;
286 this.deviceAuthenticationActions = pairingActionOptions;
290 * Builds complete BluetoothDevice objects for each element in |devices_list|.
291 * @param {!Array<!BluetoothDevice>} devices_list A list of incomplete
292 * BluetoothDevice provided by the C++ WebUI.
293 * @param {boolean} predefined Whether or not the device is a predefined one.
295 loadDevicesFromList: function(devices, predefined) {
296 /** @type {!Array<!BluetoothDevice>} */ var deviceList = [];
298 for (var i = 0; i < devices.length; ++i) {
299 if (this.devicePaths[devices[i].path] != undefined) continue;
301 // Get the label for the device class which should be selected.
302 devices[i].class = this.getTextForDeviceClass(devices[i].classValue);
303 devices[i].pairingAuthToken = devices[i].pairingAuthToken.toString();
304 deviceList.push(devices[i]);
305 this.devicePaths[devices[i].path] = {
306 predefined: predefined,
307 index: deviceList.length - 1
315 * Called when a device is paired from the Tray. Checks the paired box for
316 * the device with path |path|.
318 devicePairedFromTray: function(path) {
319 var obj = this.devicePaths[path];
321 if (obj == undefined) return;
323 var index = obj.index;
324 var devicePath = (obj.predefined ? 'predefinedDevices.' : 'devices.');
325 devicePath += obj.index.toString();
326 this.set(devicePath + '.paired', true);
330 * On-change handler for a checkbox in the device list. Pairs/unpairs the
331 * device associated with the box checked/unchecked.
332 * @param {Event} event Contains event data. |event.model.index| is the index
333 * of the item which the target is contained in.
335 pairDevice: function(event) {
336 var index = event.model.index;
338 /** @type {boolean} */ (event.target.dataset.predefined == 'true');
340 predefined ? this.predefinedDevices[index] : this.devices[index];
342 if (event.target.checked) {
343 var devicePath = (predefined ? 'predefinedDevices.' : 'devices.');
344 devicePath += index.toString();
345 this.set(devicePath + '.discoverable', true);
347 // Send device info to the WebUI.
348 chrome.send('requestBluetoothPair', [device]);
349 this.devicePaths[device.path] = {predefined: predefined, index: index};
351 var devicePath = (predefined ? 'predefinedDevices.' : 'devices.');
352 devicePath += index.toString();
353 this.set(devicePath + '.paired', false);
355 chrome.send('removeBluetoothDevice', [device.path]);
357 var devicePath = (predefined ? 'predefinedDevices.' : 'devices.');
358 devicePath += index.toString();
359 this.set(devicePath + '.discoverable', false);
364 * Called from Chrome OS back-end when a pair request fails.
365 * @param {string} path The path of the device which failed to pair.
367 pairFailed: function(path) {
368 var obj = this.devicePaths[path];
370 if (obj == undefined) return;
372 var devicePath = (obj.predefined ? 'predefinedDevices.' : 'devices.');
373 devicePath += obj.index.toString();
374 this.set(devicePath + '.paired', false);
378 * On-change event handler for a checkbox in the device list.
379 * @param {Event} event Contains event data. |event.model.index| is the index
380 * of the item which the target is contained in.
382 discoverDevice: function(event) {
383 var index = event.model.index;
385 /** @type {boolean} */ (event.target.dataset.predefined == 'true');
387 predefined ? this.predefinedDevices[index] : this.devices[index];
389 if (event.target.checked) {
390 device.classValue = this.getValueForDeviceClass(device.class);
392 // Send device info to WebUI.
393 chrome.send('requestBluetoothDiscover', [device]);
395 this.devicePaths[device.path] = {predefined: predefined, index: index};
397 chrome.send('removeBluetoothDevice', [device.path]);
399 var devicePath = (predefined ? 'predefinedDevices.' : 'devices.');
400 devicePath += index.toString();
401 this.set(devicePath + '.paired', false);
405 // Adds a new device with default settings to the list of devices.
406 appendNewDevice: function() {
407 var newDevice = new BluetoothDevice();
408 newDevice.alias = 'New Device';
409 this.push('devices', newDevice);
413 * This is called when a new device is discovered by the main adapter.
414 * The device is only added to the view's list if it is not already in
415 * the list (i.e. its path has not yet been recorded in |devicePaths|).
416 * @param {BluetoothDevice} device A bluetooth device.
418 addBluetoothDevice: function(device) {
419 if (this.devicePaths[device.path] != undefined) {
420 var obj = this.devicePaths[device.path];
421 var devicePath = (obj.predefined ? 'predefinedDevices.' : 'devices.');
422 devicePath += obj.index.toString();
423 this.set(devicePath + '.discoverable', true);
427 device.class = this.getTextForDeviceClass(device.classValue);
428 device.discoverable = true;
429 this.push('devices', device);
430 this.devicePaths[device.path] = {
432 index: this.devices.length - 1
437 * Called on "copy" button from the device list clicked. Creates a copy of
438 * the selected device and adds it to the "custom" devices list.
439 * @param {Event} event Contains event data. |event.model.index| is the index
440 * of the item which the target is contained in.
442 copyDevice: function(event) {
443 var predefined = (event.target.dataset.predefined == 'true');
444 var index = event.model.index;
446 predefined ? this.predefinedDevices[index] : this.devices[index];
447 // Create a deep copy of the selected device.
448 var newDevice = new BluetoothDevice();
449 Object.assign(newDevice, copyDevice);
451 newDevice.address = '';
452 newDevice.name += ' (Copy)';
453 newDevice.alias += ' (Copy)';
454 newDevice.discoverable = false;
455 newDevice.paired = false;
456 this.push('devices', newDevice);
460 * Shows a modal dialog to edit the selected device's properties.
461 * @param {Event} event Contains event data. |event.model.index| is the index
462 * of the item which the target is contained in.
464 showEditModal: function(event) {
465 var index = event.model.index;
466 this.currentEditIndex = index;
467 this.currentEditableObject = this.devices[index];
468 this.$.editModal.toggle();
472 * A click handler for the delete button on bluetooth devices.
473 * @param {Event} event Contains event data. |event.model.index| is the index
474 * of the item which the target is contained in.
476 deleteDevice: function(event) {
477 var index = event.model.index;
478 var device = this.devices[index];
480 chrome.send('removeBluetoothDevice', [device.path]);
482 this.devicePaths[device.path] = undefined;
483 this.splice('devices', index, 1);
487 * This function is called when a device is removed from the main bluetooth
488 * adapter's device list. It sets that device's |.discoverable| and |.paired|
489 * attributes to false.
490 * @param {string} path A bluetooth device's path.
492 deviceRemovedFromMainAdapter: function(path) {
493 if (this.devicePaths[path] == undefined) return;
495 var obj = this.devicePaths[path];
496 var devicePath = (obj.predefined ? 'predefinedDevices.' : 'devices.');
497 devicePath += obj.index.toString();
498 this.set(devicePath + '.discoverable', false);
499 this.set(devicePath + '.paired', false);
503 * Returns the text for the label that corresponds to |classValue|.
504 * @param {number} classValue A number representing the bluetooth class
506 * @return {string} The label which represents |classValue|.
508 getTextForDeviceClass: function(classValue) {
509 for (var i = 0; i < this.deviceClassOptions.length; ++i) {
510 if (this.deviceClassOptions[i].value == classValue)
511 return this.deviceClassOptions[i].text;
516 * Returns the integer value which corresponds with the label |classText|.
517 * @param {string} classText The label for a device class option.
518 * @return {number} The value which |classText| represents.
520 getValueForDeviceClass: function(classText) {
521 for (var i = 0; i < this.deviceClassOptions.length; ++i) {
522 if (this.deviceClassOptions[i].text == classText)
523 return this.deviceClassOptions[i].value;