Home | History | Annotate | Download | only in chromeos
      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