• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5cr.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