1 // Copyright (c) 2012 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. 4 5 cr.define('options', function() { 6 /** @const */ var Page = cr.ui.pageManager.Page; 7 /** @const */ var PageManager = cr.ui.pageManager.PageManager; 8 9 /** 10 * Enumeration of possible states during pairing. The value associated with 11 * each state maps to a localized string in the global variable 12 * |loadTimeData|. 13 * @enum {string} 14 */ 15 var PAIRING = { 16 STARTUP: 'bluetoothStartConnecting', 17 ENTER_PIN_CODE: 'bluetoothEnterPinCode', 18 ENTER_PASSKEY: 'bluetoothEnterPasskey', 19 REMOTE_PIN_CODE: 'bluetoothRemotePinCode', 20 REMOTE_PASSKEY: 'bluetoothRemotePasskey', 21 CONFIRM_PASSKEY: 'bluetoothConfirmPasskey', 22 CONNECT_FAILED: 'bluetoothConnectFailed', 23 CANCELED: 'bluetoothPairingCanceled', 24 DISMISSED: 'bluetoothPairingDismissed', // pairing dismissed(succeeded or 25 // canceled). 26 }; 27 28 /** 29 * List of IDs for conditionally visible elements in the dialog. 30 * @type {Array.<string>} 31 * @const 32 */ 33 var ELEMENTS = ['bluetooth-pairing-passkey-display', 34 'bluetooth-pairing-passkey-entry', 35 'bluetooth-pairing-pincode-entry', 36 'bluetooth-pair-device-connect-button', 37 'bluetooth-pair-device-cancel-button', 38 'bluetooth-pair-device-accept-button', 39 'bluetooth-pair-device-reject-button', 40 'bluetooth-pair-device-dismiss-button']; 41 42 /** 43 * Encapsulated handling of the Bluetooth device pairing page. 44 * @constructor 45 * @extends {cr.ui.pageManager.Page} 46 */ 47 function BluetoothPairing() { 48 Page.call(this, 'bluetoothPairing', 49 loadTimeData.getString('bluetoothOptionsPageTabTitle'), 50 'bluetooth-pairing'); 51 } 52 53 cr.addSingletonGetter(BluetoothPairing); 54 55 BluetoothPairing.prototype = { 56 __proto__: Page.prototype, 57 58 /** 59 * Description of the bluetooth device. 60 * @type {?BluetoothDevice} 61 * @private 62 */ 63 device_: null, 64 65 /** 66 * Can the dialog be programmatically dismissed. 67 * @type {boolean} 68 */ 69 dismissible_: true, 70 71 /** @override */ 72 initializePage: function() { 73 Page.prototype.initializePage.call(this); 74 var self = this; 75 $('bluetooth-pair-device-cancel-button').onclick = function() { 76 PageManager.closeOverlay(); 77 }; 78 $('bluetooth-pair-device-reject-button').onclick = function() { 79 chrome.send('updateBluetoothDevice', 80 [self.device_.address, 'reject']); 81 self.device_.pairing = PAIRING.DISMISSED; 82 PageManager.closeOverlay(); 83 }; 84 $('bluetooth-pair-device-connect-button').onclick = function() { 85 var args = [self.device_.address, 'connect']; 86 var passkey = self.device_.passkey; 87 if (passkey) 88 args.push(String(passkey)); 89 else if (!$('bluetooth-pairing-passkey-entry').hidden) 90 args.push($('bluetooth-passkey').value); 91 else if (!$('bluetooth-pairing-pincode-entry').hidden) 92 args.push($('bluetooth-pincode').value); 93 chrome.send('updateBluetoothDevice', args); 94 // Prevent sending a 'connect' command twice. 95 $('bluetooth-pair-device-connect-button').disabled = true; 96 }; 97 $('bluetooth-pair-device-accept-button').onclick = function() { 98 chrome.send('updateBluetoothDevice', 99 [self.device_.address, 'accept']); 100 // Prevent sending a 'accept' command twice. 101 $('bluetooth-pair-device-accept-button').disabled = true; 102 }; 103 $('bluetooth-pair-device-dismiss-button').onclick = function() { 104 PageManager.closeOverlay(); 105 }; 106 $('bluetooth-passkey').oninput = function() { 107 var inputField = $('bluetooth-passkey'); 108 var value = inputField.value; 109 // Note that using <input type="number"> is insufficient to restrict 110 // the input as it allows negative numbers and does not limit the 111 // number of charactes typed even if a range is set. Furthermore, 112 // it sometimes produces strange repaint artifacts. 113 var filtered = value.replace(/[^0-9]/g, ''); 114 if (filtered != value) 115 inputField.value = filtered; 116 $('bluetooth-pair-device-connect-button').disabled = 117 inputField.value.length == 0; 118 }; 119 $('bluetooth-pincode').oninput = function() { 120 $('bluetooth-pair-device-connect-button').disabled = 121 $('bluetooth-pincode').value.length == 0; 122 }; 123 $('bluetooth-passkey').addEventListener('keydown', 124 this.keyDownEventHandler_.bind(this)); 125 $('bluetooth-pincode').addEventListener('keydown', 126 this.keyDownEventHandler_.bind(this)); 127 }, 128 129 /** @override */ 130 didClosePage: function() { 131 if (this.device_.pairing != PAIRING.DISMISSED && 132 this.device_.pairing != PAIRING.CONNECT_FAILED) { 133 this.device_.pairing = PAIRING.CANCELED; 134 chrome.send('updateBluetoothDevice', 135 [this.device_.address, 'cancel']); 136 } 137 }, 138 139 /** 140 * Override to prevent showing the overlay if the Bluetooth device details 141 * have not been specified. Prevents showing an empty dialog if the user 142 * quits and restarts Chrome while in the process of pairing with a device. 143 * @return {boolean} True if the overlay can be displayed. 144 */ 145 canShowPage: function() { 146 return !!(this.device_ && this.device_.address && this.device_.pairing); 147 }, 148 149 /** 150 * Sets input focus on the passkey or pincode field if appropriate. 151 */ 152 didShowPage: function() { 153 if (!$('bluetooth-pincode').hidden) 154 $('bluetooth-pincode').focus(); 155 else if (!$('bluetooth-passkey').hidden) 156 $('bluetooth-passkey').focus(); 157 }, 158 159 /** 160 * Configures the overlay for pairing a device. 161 * @param {Object} device Description of the bluetooth device. 162 */ 163 update: function(device) { 164 this.device_ = /** @type {BluetoothDevice} */({}); 165 for (var key in device) 166 this.device_[key] = device[key]; 167 // Update the pairing instructions. 168 var instructionsEl = assert($('bluetooth-pairing-instructions')); 169 this.clearElement_(instructionsEl); 170 this.dismissible_ = ('dismissible' in device) ? 171 device.dismissible : true; 172 173 var message = loadTimeData.getString(device.pairing); 174 message = message.replace('%1', this.device_.name); 175 instructionsEl.textContent = message; 176 177 // Update visibility of dialog elements. 178 if (this.device_.passkey) { 179 this.updatePasskey_(String(this.device_.passkey)); 180 if (this.device_.pairing == PAIRING.CONFIRM_PASSKEY) { 181 // Confirming a match between displayed passkeys. 182 this.displayElements_(['bluetooth-pairing-passkey-display', 183 'bluetooth-pair-device-accept-button', 184 'bluetooth-pair-device-reject-button']); 185 $('bluetooth-pair-device-accept-button').disabled = false; 186 } else { 187 // Remote entering a passkey. 188 this.displayElements_(['bluetooth-pairing-passkey-display', 189 'bluetooth-pair-device-cancel-button']); 190 } 191 } else if (this.device_.pincode) { 192 this.updatePasskey_(String(this.device_.pincode)); 193 this.displayElements_(['bluetooth-pairing-passkey-display', 194 'bluetooth-pair-device-cancel-button']); 195 } else if (this.device_.pairing == PAIRING.ENTER_PIN_CODE) { 196 // Prompting the user to enter a PIN code. 197 this.displayElements_(['bluetooth-pairing-pincode-entry', 198 'bluetooth-pair-device-connect-button', 199 'bluetooth-pair-device-cancel-button']); 200 $('bluetooth-pincode').value = ''; 201 } else if (this.device_.pairing == PAIRING.ENTER_PASSKEY) { 202 // Prompting the user to enter a passkey. 203 this.displayElements_(['bluetooth-pairing-passkey-entry', 204 'bluetooth-pair-device-connect-button', 205 'bluetooth-pair-device-cancel-button']); 206 $('bluetooth-passkey').value = ''; 207 } else if (this.device_.pairing == PAIRING.STARTUP) { 208 // Starting the pairing process. 209 this.displayElements_(['bluetooth-pair-device-cancel-button']); 210 } else { 211 // Displaying an error message. 212 this.displayElements_(['bluetooth-pair-device-dismiss-button']); 213 } 214 // User is required to enter a passkey or pincode before the connect 215 // button can be enabled. The 'oninput' methods for the input fields 216 // determine when the connect button becomes active. 217 $('bluetooth-pair-device-connect-button').disabled = true; 218 }, 219 220 /** 221 * Handles the ENTER key for the passkey or pincode entry field. 222 * @param {Event} event A keydown event. 223 * @private 224 */ 225 keyDownEventHandler_: function(event) { 226 /** @const */ var ENTER_KEY_CODE = 13; 227 if (event.keyCode == ENTER_KEY_CODE) { 228 var button = $('bluetooth-pair-device-connect-button'); 229 if (!button.hidden) 230 button.click(); 231 } 232 }, 233 234 /** 235 * Updates the visibility of elements in the dialog. 236 * @param {Array.<string>} list List of conditionally visible elements that 237 * are to be made visible. 238 * @private 239 */ 240 displayElements_: function(list) { 241 var enabled = {}; 242 for (var i = 0; i < list.length; i++) { 243 var key = list[i]; 244 enabled[key] = true; 245 } 246 for (var i = 0; i < ELEMENTS.length; i++) { 247 var key = ELEMENTS[i]; 248 $(key).hidden = !enabled[key]; 249 } 250 }, 251 252 /** 253 * Removes all children from an element. 254 * @param {!Element} element Target element to clear. 255 */ 256 clearElement_: function(element) { 257 var child = element.firstChild; 258 while (child) { 259 element.removeChild(child); 260 child = element.firstChild; 261 } 262 }, 263 264 /** 265 * Formats an element for displaying the passkey or PIN code. 266 * @param {string} key Passkey or PIN to display. 267 */ 268 updatePasskey_: function(key) { 269 var passkeyEl = assert($('bluetooth-pairing-passkey-display')); 270 var keyClass = (this.device_.pairing == PAIRING.REMOTE_PASSKEY || 271 this.device_.pairing == PAIRING.REMOTE_PIN_CODE) ? 272 'bluetooth-keyboard-button' : 'bluetooth-passkey-char'; 273 this.clearElement_(passkeyEl); 274 // Passkey should always have 6 digits. 275 key = '000000'.substring(0, 6 - key.length) + key; 276 var progress = this.device_.entered; 277 for (var i = 0; i < key.length; i++) { 278 var keyEl = document.createElement('span'); 279 keyEl.textContent = key.charAt(i); 280 keyEl.className = keyClass; 281 if (progress != undefined) { 282 if (i < progress) 283 keyEl.classList.add('key-typed'); 284 else if (i == progress) 285 keyEl.classList.add('key-next'); 286 else 287 keyEl.classList.add('key-untyped'); 288 } 289 passkeyEl.appendChild(keyEl); 290 } 291 if (this.device_.pairing == PAIRING.REMOTE_PASSKEY || 292 this.device_.pairing == PAIRING.REMOTE_PIN_CODE) { 293 // Add enter key. 294 var label = loadTimeData.getString('bluetoothEnterKey'); 295 var keyEl = document.createElement('span'); 296 keyEl.textContent = label; 297 keyEl.className = keyClass; 298 keyEl.id = 'bluetooth-enter-key'; 299 if (progress != undefined) { 300 if (progress > key.length) 301 keyEl.classList.add('key-typed'); 302 else if (progress == key.length) 303 keyEl.classList.add('key-next'); 304 else 305 keyEl.classList.add('key-untyped'); 306 } 307 passkeyEl.appendChild(keyEl); 308 } 309 passkeyEl.hidden = false; 310 }, 311 }; 312 313 /** 314 * Configures the device pairing instructions and displays the pairing 315 * overlay. 316 * @param {Object} device Description of the Bluetooth device. 317 */ 318 BluetoothPairing.showDialog = function(device) { 319 BluetoothPairing.getInstance().update(device); 320 PageManager.showPageByName('bluetoothPairing', false); 321 }; 322 323 /** 324 * Displays a message from the Bluetooth adapter. 325 * @param {{message: string, address: string}} data Data for constructing the 326 * message. |data.message| is the name of message to show. |data.address| 327 * is the device address. 328 */ 329 BluetoothPairing.showMessage = function(data) { 330 var name = data.address; 331 if (name.length == 0) 332 return; 333 var dialog = BluetoothPairing.getInstance(); 334 if (dialog.device_ && name == dialog.device_.address && 335 dialog.device_.pairing == PAIRING.CANCELED) { 336 // Do not show any error message after cancelation of the pairing. 337 return; 338 } 339 340 var list = $('bluetooth-paired-devices-list'); 341 if (list) { 342 var index = list.find(name); 343 if (index == undefined) { 344 list = $('bluetooth-unpaired-devices-list'); 345 index = list.find(name); 346 } 347 if (index != undefined) { 348 var entry = list.dataModel.item(index); 349 if (entry && entry.name) 350 name = entry.name; 351 } 352 } 353 BluetoothPairing.showDialog({name: name, 354 address: data.address, 355 pairing: data.message, 356 dismissible: false}); 357 }; 358 359 /** 360 * Closes the Bluetooth pairing dialog. 361 */ 362 BluetoothPairing.dismissDialog = function() { 363 var overlay = PageManager.getTopmostVisiblePage(); 364 var dialog = BluetoothPairing.getInstance(); 365 if (overlay == dialog && dialog.dismissible_) { 366 dialog.device_.pairing = PAIRING.DISMISSED; 367 PageManager.closeOverlay(); 368 } 369 }; 370 371 // Export 372 return { 373 BluetoothPairing: BluetoothPairing 374 }; 375 }); 376