| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview |
| * Dialog used for pairing a provided |pairing-device|. Set |show-error| to |
| * show the error results from a pairing event instead of the pairing UI. |
| * NOTE: This module depends on I18nBehavior which depends on loadTimeData. |
| */ |
| |
| const PairingEventType = chrome.bluetoothPrivate.PairingEventType; |
| |
| Polymer({ |
| is: 'bluetooth-dialog', |
| |
| behaviors: [I18nBehavior], |
| |
| properties: { |
| /** |
| * Interface for bluetooth calls. Set in bluetooth-page. |
| * @type {Bluetooth} |
| * @private |
| */ |
| bluetooth: { |
| type: Object, |
| value: chrome.bluetooth, |
| }, |
| |
| /** |
| * Interface for bluetoothPrivate calls. |
| * @type {BluetoothPrivate} |
| */ |
| bluetoothPrivate: { |
| type: Object, |
| value: chrome.bluetoothPrivate, |
| }, |
| |
| noCancel: Boolean, |
| |
| dialogTitle: String, |
| |
| /** |
| * Current Pairing device. |
| * @type {!chrome.bluetooth.Device|undefined} |
| */ |
| pairingDevice: Object, |
| |
| /** |
| * Current Pairing event. |
| * @private {?chrome.bluetoothPrivate.PairingEvent} |
| */ |
| pairingEvent_: { |
| type: Object, |
| value: null, |
| }, |
| |
| /** |
| * May be set by the host to show a pairing error result, or may be |
| * set by the dialog if a pairing or connect error occured. |
| * @private |
| */ |
| errorMessage_: String, |
| |
| /** |
| * Pincode or passkey value, used to trigger connect enabled changes. |
| * @private |
| */ |
| pinOrPass_: String, |
| |
| /** |
| * @const {!Array<number>} |
| * @private |
| */ |
| digits_: { |
| type: Array, |
| readOnly: true, |
| value: [0, 1, 2, 3, 4, 5], |
| }, |
| }, |
| |
| observers: [ |
| 'dialogUpdated_(errorMessage_, pairingEvent_)', |
| 'pairingChanged_(pairingDevice, pairingEvent_)', |
| ], |
| |
| /** |
| * Listener for chrome.bluetoothPrivate.onPairing events. |
| * @private {?function(!chrome.bluetoothPrivate.PairingEvent)} |
| */ |
| bluetoothPrivateOnPairingListener_: null, |
| |
| /** |
| * Listener for chrome.bluetooth.onBluetoothDeviceChanged events. |
| * @private {?function(!chrome.bluetooth.Device)} |
| */ |
| bluetoothDeviceChangedListener_: null, |
| |
| open: function() { |
| this.startPairing(); |
| this.pinOrPass_ = ''; |
| this.getDialog_().showModal(); |
| this.itemWasFocused_ = false; |
| }, |
| |
| close: function() { |
| this.endPairing(); |
| const dialog = this.getDialog_(); |
| if (dialog.open) |
| dialog.close(); |
| }, |
| |
| /** |
| * Updates the dialog after a connect attempt. |
| * @param {!chrome.bluetooth.Device} device The device connected to |
| * @param {!{message: string}} lastError chrome.runtime.lastError |
| * @param {chrome.bluetoothPrivate.ConnectResultType} result The connect |
| * result |
| * @return {boolean} |
| */ |
| handleError: function(device, lastError, result) { |
| let error; |
| if (lastError) { |
| error = lastError.message; |
| } else { |
| switch (result) { |
| case chrome.bluetoothPrivate.ConnectResultType.IN_PROGRESS: |
| case chrome.bluetoothPrivate.ConnectResultType.ALREADY_CONNECTED: |
| case chrome.bluetoothPrivate.ConnectResultType.AUTH_CANCELED: |
| case chrome.bluetoothPrivate.ConnectResultType.SUCCESS: |
| this.errorMessage_ = ''; |
| return false; |
| default: |
| error = result; |
| } |
| } |
| |
| const name = device.name || device.address; |
| const id = 'bluetooth_connect_' + error; |
| if (this.i18nExists(id)) { |
| this.errorMessage_ = this.i18n(id, name); |
| } else { |
| this.errorMessage_ = error; |
| console.error('Unexpected error connecting to: ' + name + ': ' + error); |
| } |
| return true; |
| }, |
| |
| /** @private */ |
| dialogUpdated_: function() { |
| if (this.showEnterPincode_()) |
| this.$$('#pincode').focus(); |
| else if (this.showEnterPasskey_()) |
| this.$$('#passkey').focus(); |
| }, |
| |
| /** |
| * @return {!CrDialogElement} |
| * @private |
| */ |
| getDialog_: function() { |
| return /** @type {!CrDialogElement} */ (this.$.dialog); |
| }, |
| |
| /** @private */ |
| onCancelTap_: function() { |
| this.getDialog_().cancel(); |
| }, |
| |
| /** @private */ |
| onDialogCanceled_: function() { |
| if (!this.errorMessage_) |
| this.sendResponse_(chrome.bluetoothPrivate.PairingResponse.CANCEL); |
| this.endPairing(); |
| }, |
| |
| /** Called when the dialog is opened. Starts listening for pairing events. */ |
| startPairing: function() { |
| if (!this.bluetoothPrivateOnPairingListener_) { |
| this.bluetoothPrivateOnPairingListener_ = |
| this.onBluetoothPrivateOnPairing_.bind(this); |
| this.bluetoothPrivate.onPairing.addListener( |
| this.bluetoothPrivateOnPairingListener_); |
| } |
| if (!this.bluetoothDeviceChangedListener_) { |
| this.bluetoothDeviceChangedListener_ = |
| this.onBluetoothDeviceChanged_.bind(this); |
| this.bluetooth.onDeviceChanged.addListener( |
| this.bluetoothDeviceChangedListener_); |
| } |
| }, |
| |
| /** Called when the dialog is closed. */ |
| endPairing: function() { |
| if (this.bluetoothPrivateOnPairingListener_) { |
| this.bluetoothPrivate.onPairing.removeListener( |
| this.bluetoothPrivateOnPairingListener_); |
| this.bluetoothPrivateOnPairingListener_ = null; |
| } |
| if (this.bluetoothDeviceChangedListener_) { |
| this.bluetooth.onDeviceChanged.removeListener( |
| this.bluetoothDeviceChangedListener_); |
| this.bluetoothDeviceChangedListener_ = null; |
| } |
| this.pairingEvent_ = null; |
| }, |
| |
| /** |
| * Process bluetoothPrivate.onPairing events. |
| * @param {!chrome.bluetoothPrivate.PairingEvent} event |
| * @private |
| */ |
| onBluetoothPrivateOnPairing_: function(event) { |
| if (!this.pairingDevice || |
| event.device.address != this.pairingDevice.address) { |
| return; |
| } |
| if (event.pairing == PairingEventType.KEYS_ENTERED && |
| event.passkey === undefined && this.pairingEvent_) { |
| // 'keysEntered' event might not include the updated passkey so preserve |
| // the current one. |
| event.passkey = this.pairingEvent_.passkey; |
| } |
| this.pairingEvent_ = event; |
| }, |
| |
| /** |
| * Process bluetooth.onDeviceChanged events. This ensures that the dialog |
| * updates when the connection state changes. |
| * @param {!chrome.bluetooth.Device} device |
| * @private |
| */ |
| onBluetoothDeviceChanged_: function(device) { |
| if (!this.pairingDevice || device.address != this.pairingDevice.address) |
| return; |
| this.pairingDevice = device; |
| }, |
| |
| /** @private */ |
| pairingChanged_: function() { |
| if (this.pairingDevice === undefined) |
| return; |
| |
| // Auto-close the dialog when pairing completes. |
| if (this.pairingDevice.paired && !this.pairingDevice.connecting && |
| this.pairingDevice.connected) { |
| this.close(); |
| return; |
| } |
| this.errorMessage_ = ''; |
| this.pinOrPass_ = ''; |
| }, |
| |
| /** |
| * @return {string} |
| * @private |
| */ |
| getMessage_: function() { |
| let message; |
| if (!this.pairingEvent_) |
| message = 'bluetoothStartConnecting'; |
| else |
| message = this.getEventDesc_(this.pairingEvent_.pairing); |
| return this.i18n(message, this.pairingDevice.name || ''); |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| showEnterPincode_: function() { |
| return !!this.pairingEvent_ && |
| this.pairingEvent_.pairing == PairingEventType.REQUEST_PINCODE; |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| showEnterPasskey_: function() { |
| return !!this.pairingEvent_ && |
| this.pairingEvent_.pairing == PairingEventType.REQUEST_PASSKEY; |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| showDisplayPassOrPin_: function() { |
| if (!this.pairingEvent_) |
| return false; |
| const pairing = this.pairingEvent_.pairing; |
| return ( |
| pairing == PairingEventType.DISPLAY_PINCODE || |
| pairing == PairingEventType.DISPLAY_PASSKEY || |
| pairing == PairingEventType.CONFIRM_PASSKEY || |
| pairing == PairingEventType.KEYS_ENTERED); |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| showAcceptReject_: function() { |
| return !!this.pairingEvent_ && |
| this.pairingEvent_.pairing == PairingEventType.CONFIRM_PASSKEY; |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| showConnect_: function() { |
| if (!this.pairingEvent_) |
| return false; |
| const pairing = this.pairingEvent_.pairing; |
| return pairing == PairingEventType.REQUEST_PINCODE || |
| pairing == PairingEventType.REQUEST_PASSKEY; |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| enableConnect_: function() { |
| if (!this.showConnect_()) |
| return false; |
| const inputId = |
| (this.pairingEvent_.pairing == PairingEventType.REQUEST_PINCODE) ? |
| '#pincode' : |
| '#passkey'; |
| const crInput = /** @type {!CrInputElement} */ (this.$$(inputId)); |
| assert(crInput); |
| /** @type {string} */ const value = crInput.value; |
| return !!value && crInput.validate(); |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| showDismiss_: function() { |
| return this.pairingDevice.paired || |
| (!!this.pairingEvent_ && |
| this.pairingEvent_.pairing == PairingEventType.COMPLETE); |
| }, |
| |
| /** @private */ |
| onAcceptTap_: function() { |
| this.sendResponse_(chrome.bluetoothPrivate.PairingResponse.CONFIRM); |
| }, |
| |
| /** @private */ |
| onConnectTap_: function() { |
| this.sendResponse_(chrome.bluetoothPrivate.PairingResponse.CONFIRM); |
| }, |
| |
| /** @private */ |
| onRejectTap_: function() { |
| this.sendResponse_(chrome.bluetoothPrivate.PairingResponse.REJECT); |
| }, |
| |
| /** |
| * @param {!chrome.bluetoothPrivate.PairingResponse} response |
| * @private |
| */ |
| sendResponse_: function(response) { |
| if (!this.pairingDevice) |
| return; |
| const options = |
| /** @type {!chrome.bluetoothPrivate.SetPairingResponseOptions} */ ( |
| {device: this.pairingDevice, response: response}); |
| if (response == chrome.bluetoothPrivate.PairingResponse.CONFIRM) { |
| const pairing = this.pairingEvent_.pairing; |
| if (pairing == PairingEventType.REQUEST_PINCODE) |
| options.pincode = this.$$('#pincode').value; |
| else if (pairing == PairingEventType.REQUEST_PASSKEY) |
| options.passkey = parseInt(this.$$('#passkey').value, 10); |
| } |
| this.bluetoothPrivate.setPairingResponse(options, () => { |
| if (chrome.runtime.lastError) { |
| // TODO(stevenjb): Show error. |
| console.error( |
| 'Error setting pairing response: ' + options.device.name + |
| ': Response: ' + options.response + |
| ': Error: ' + chrome.runtime.lastError.message); |
| } |
| this.close(); |
| }); |
| |
| this.fire('response', options); |
| }, |
| |
| /** |
| * @param {!PairingEventType} eventType |
| * @return {string} |
| * @private |
| */ |
| getEventDesc_: function(eventType) { |
| assert(eventType); |
| if (eventType == PairingEventType.COMPLETE || |
| eventType == PairingEventType.KEYS_ENTERED || |
| eventType == PairingEventType.REQUEST_AUTHORIZATION) { |
| return 'bluetoothStartConnecting'; |
| } |
| return 'bluetooth_' + /** @type {string} */ (eventType); |
| }, |
| |
| /** |
| * @param {number} index |
| * @return {string} |
| * @private |
| */ |
| getPinDigit_: function(index) { |
| if (!this.pairingEvent_) |
| return ''; |
| let digit = '0'; |
| const pairing = this.pairingEvent_.pairing; |
| if (pairing == PairingEventType.DISPLAY_PINCODE && |
| this.pairingEvent_.pincode && |
| index < this.pairingEvent_.pincode.length) { |
| digit = this.pairingEvent_.pincode[index]; |
| } else if ( |
| this.pairingEvent_.passkey && |
| (pairing == PairingEventType.DISPLAY_PASSKEY || |
| pairing == PairingEventType.KEYS_ENTERED || |
| pairing == PairingEventType.CONFIRM_PASSKEY)) { |
| const passkeyString = |
| String(this.pairingEvent_.passkey).padStart(this.digits_.length, '0'); |
| digit = passkeyString[index]; |
| } |
| return digit; |
| }, |
| |
| /** |
| * @param {number} index |
| * @return {string} |
| * @private |
| */ |
| getPinClass_: function(index) { |
| if (!this.pairingEvent_) |
| return ''; |
| if (this.pairingEvent_.pairing == PairingEventType.CONFIRM_PASSKEY) |
| return 'confirm'; |
| let cssClass = 'display'; |
| if (this.pairingEvent_.pairing == PairingEventType.DISPLAY_PASSKEY) { |
| if (index == 0) |
| cssClass += ' next'; |
| else |
| cssClass += ' untyped'; |
| } else if ( |
| this.pairingEvent_.pairing == PairingEventType.KEYS_ENTERED && |
| this.pairingEvent_.enteredKey) { |
| const enteredKey = this.pairingEvent_.enteredKey; // 1-7 |
| const lastKey = this.digits_.length; // 6 |
| if ((index == -1 && enteredKey > lastKey) || (index + 1 == enteredKey)) |
| cssClass += ' next'; |
| else if (index > enteredKey) |
| cssClass += ' untyped'; |
| } |
| return cssClass; |
| }, |
| }); |