Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / emulator / bluetooth_settings.js
blob377f73d7b9de9f830f45987c19c1486601e1d965
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.
5 /**
6  * A bluetooth device.
7  * @constructor
8  */
9 var BluetoothDevice = function() {
10   // The device's address (MAC format, must be unique).
11   this.address = '';
13   // The label which shows up in the devices list for this device.
14   this.alias = '';
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
23   // bluetooth devices.
24   this.discoverable = false;
26   // Whether Chrome OS pairs with this device, or this device tries to pair
27   // with Chrome OS.
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.
36   this.name = '';
38   // The designated path for the device. Must be unique.
39   this.path = '';
41   // Whether or not the device is paired with Chrome OS.
42   this.paired = false;
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 = '';
54 Polymer({
55   is: 'bluetooth-settings',
57   properties: {
58     /**
59      * The title to be displayed in a heading element for the element.
60      */
61     title: {type: String},
63     /**
64      * A set of bluetooth devices.
65      * @type !Array<!BluetoothDevice>
66      */
67     devices: {type: Array, value: function() { return []; }},
69     /**
70      * A set of predefined bluetooth devices.
71      * @type !Array<!Bluetooth>
72      */
73     predefinedDevices: {type: Array, value: function() { return []; }},
75     /**
76      * A bluetooth device object which is currently being edited.
77      * @type {BluetoothDevice}
78      */
79     currentEditableObject: {
80       type: Object,
81       value: function() { return {}; }
82     },
84     /**
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.
88      */
89     currentEditIndex: {type: Number, value: function() { return -1; }},
91     /**
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} >
96      */
97     deviceClassOptions: {
98       type: Array,
99       value: function() {
100         return [
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}
107         ];
108       }
109     },
111     /**
112      * A set of strings representing the method to be used for
113      * authenticating a device during a pair request.
114      * @type !Array<string>
115      */
116     deviceAuthenticationMethods: {
117       type: Array,
118       value: function() { return []; }
119     },
121     /**
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>
125      */
126     deviceAuthenticationActions: {
127       type: Array,
128       value: function() { return []; }
129     },
130   },
132   /**
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.
135    * @type {Object}
136    */
137   devicePaths: {},
139   ready: function() { this.title = 'Bluetooth'; },
141   initialize: function() {
142     if (!this.initialized) {
143       this.initialized = true;
144       chrome.send('requestBluetoothInfo');
145     }
146   },
148   observers: ['currentEditableObjectChanged(currentEditableObject.*)'],
150   /**
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
155    *     is, etc.)
156    */
157   currentEditableObjectChanged: function(obj) {
158     if (this.currentEditIndex >= 0) {
159       var prop = obj.path.split('.')[1];
160       this.set('devices.' + this.currentEditIndex.toString() + '.' + prop,
161                obj.value);
162     }
163   },
165   /**
166    * Called when the device edit modal is opened. Re-validates necessary input
167    * fields.
168    */
169   editDialogOpened: function() {
170     this.validateAddress();
171     this.validatePath();
172   },
174   handleAddressInput: function() {
175     this.autoFormatAddress();
176     this.validateAddress();
177   },
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');
189     input.value = val;
190   },
192   /**
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
197    * in use.
198    */
199   validateAddress: function() {
200     var input = this.$.deviceAddressInput;
201     var val = input.value;
202     var exists = false;
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) {
207           exists = true;
208           break;
209         }
210       }
212       if (!exists) {
213         for (var i = 0; i < this.devices.length; ++i) {
214           if (this.devices[i].address == val && i != this.currentEditIndex) {
215             exists = true;
216             break;
217           }
218         }
219       }
221       if (exists) {
222         input.invalid = true;
223         input.errorMessage = 'This address is already being used.';
224       } else {
225         input.invalid = false;
226       }
227     } else {
228       input.invalid = true;
229       input.errorMessage = 'Invalid address.';
230     }
231   },
233   /**
234    * Makes sure that a path is not already used.
235    */
236   validatePath: function() {
237     var input = this.$.devicePathInput;
238     var val = input.value;
239     var exists = false;
241     for (var i = 0; i < this.predefinedDevices.length; ++i) {
242       if (this.predefinedDevices[i].path == val) {
243         exists = true;
244         break;
245       }
246     }
248     if (!exists) {
249       for (var i = 0; i < this.devices.length; ++i) {
250         if (this.devices[i].path == val && i != this.currentEditIndex) {
251           exists = true;
252           break;
253         }
254       }
255     }
257     if (exists) {
258       input.invalid = true;
259       input.errorMessage = 'This path is already being used.';
260     } else {
261       input.invalid = false;
262     }
263   },
265   /**
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.
271    */
272   showAuthToken: function(pairMethod) {
273     return pairMethod && pairMethod != 'None';
274   },
276   /**
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.
280    */
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;
287   },
289   /**
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.
294    */
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
308       };
309     }
311     return deviceList;
312   },
314   /**
315    * Called when a device is paired from the Tray. Checks the paired box for
316    * the device with path |path|.
317    */
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);
327   },
329   /**
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.
334    */
335   pairDevice: function(event) {
336     var index = event.model.index;
337     var predefined =
338         /** @type {boolean} */ (event.target.dataset.predefined == 'true');
339     var device =
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);
354     } else {
355       chrome.send('removeBluetoothDevice', [device.path]);
357       var devicePath = (predefined ? 'predefinedDevices.' : 'devices.');
358       devicePath += index.toString();
359       this.set(devicePath + '.discoverable', false);
360     }
361   },
363   /**
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.
366    */
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);
375   },
377   /**
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.
381    */
382   discoverDevice: function(event) {
383     var index = event.model.index;
384     var predefined =
385         /** @type {boolean} */ (event.target.dataset.predefined == 'true');
386     var device =
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};
396     } else {
397       chrome.send('removeBluetoothDevice', [device.path]);
399       var devicePath = (predefined ? 'predefinedDevices.' : 'devices.');
400       devicePath += index.toString();
401       this.set(devicePath + '.paired', false);
402     }
403   },
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);
410   },
412   /**
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.
417    */
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);
424       return;
425     }
427     device.class = this.getTextForDeviceClass(device.classValue);
428     device.discoverable = true;
429     this.push('devices', device);
430     this.devicePaths[device.path] = {
431       predefined: false,
432       index: this.devices.length - 1
433     };
434   },
436   /**
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.
441    */
442   copyDevice: function(event) {
443     var predefined = (event.target.dataset.predefined == 'true');
444     var index = event.model.index;
445     var copyDevice =
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);
450     newDevice.path = '';
451     newDevice.address = '';
452     newDevice.name += ' (Copy)';
453     newDevice.alias += ' (Copy)';
454     newDevice.discoverable = false;
455     newDevice.paired = false;
456     this.push('devices', newDevice);
457   },
459   /**
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.
463    */
464   showEditModal: function(event) {
465     var index = event.model.index;
466     this.currentEditIndex = index;
467     this.currentEditableObject = this.devices[index];
468     this.$.editModal.toggle();
469   },
471   /**
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.
475    */
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);
484   },
486   /**
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.
491    */
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);
500   },
502   /**
503    * Returns the text for the label that corresponds to |classValue|.
504    * @param {number} classValue A number representing the bluetooth class
505    *     of a device.
506    * @return {string} The label which represents |classValue|.
507    */
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;
512     }
513   },
515   /**
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.
519    */
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;
524     }
525     return 0;
526   },