/** * The Bluetooth panel * * Bluetooth v2 panel. */ define(function(require) { 'use strict'; var BtContext = require('modules/bluetooth/bluetooth_context'); var BtConnectionManager = require('modules/bluetooth/bluetooth_connection_manager'); var BtTemplateFactory = require('views/phone/bluetooth/bt_template_factory'); var DialogService = require('modules/dialog_service'); var ListView = require('modules/mvvm/list_view'); var SettingsPanel = require('modules/settings_panel'); const MAX_DEVICE_NAME_LENGTH = 20; const VISIBLE_TIMEOUT_TIME = 120000; // Visibility will timeout after 2 mins. var _debug = false; var debug = function() {}; if (_debug) { debug = function btp_debug(msg) { console.log('--> [Bluetooth][Panel]: ' + msg); }; } return function ctor_bluetooth() { var elements; var pairedDeviceTemplate; var foundDeviceTemplate; var _pairedDevicesListView; var _foundDevicesListView; return SettingsPanel({ onInit: function(panel) { debug('onInit():'); // Init state for checking left app or not. this._leftApp = false; // Init an instance to maintain visibility timeout. this._visibleTimeout = null; // Init bounding instances for observe/un-observe property. this._boundUpdateEnableCheckbox = this._updateEnableCheckbox.bind(this); this._boundUpdateVisibleItem = this._updateVisibleItem.bind(this); this._boundUpdateVisibleCheckbox = this._updateVisibleCheckbox.bind(this); this._boundUpdateVisibilityTimer = this._updateVisibilityTimer.bind(this); this._boundUpdateVisibleName = this._updateVisibleName.bind(this); this._boundUpdateRenameItem = this._updateRenameItem.bind(this); this._boundUpdatePairedDevicesHeader = this._updatePairedDevicesHeader.bind(this); this._boundUpdatePairedDevicesList = this._updatePairedDevicesList.bind(this); this._boundUpdateFoundDevicesList = this._updateFoundDevicesList.bind(this); this._boundUpdateSearchItem = this._updateSearchItem.bind(this); this._boundUpdateSearchingItem = this._updateSearchingItem.bind(this); elements = { panel: panel, enableCheckbox: panel.querySelector('.bluetooth-status gaia-switch'), enableCheckboxMsg: panel.querySelector('.bluetooth-enable-msg'), visible: { visibleItem: panel.querySelector('.device-visible'), visibleName: panel.querySelector('.bluetooth-device-name'), visibleCheckBox: panel.querySelector('.device-visible gaia-switch') }, rename: { renameItem: panel.querySelector('.bluetooth-rename'), renameButton: panel.querySelector('.rename-device') }, paired: { pairedDevicesHeader: panel.querySelector('.bluetooth-paired-title'), pairedDevicesList: panel.querySelector('.bluetooth-paired-devices') }, found: { foundDevicesHeader: panel.querySelector('.bluetooth-found-title'), foundDevicesList: panel.querySelector('.bluetooth-devices') }, search: { searchingItem: panel.querySelector('.bluetooth-searching'), searchItem: panel.querySelector('.bluetooth-search'), searchButton: panel.querySelector('.search-device') }, actionMenu: { actionMenuDialog: panel.querySelector('.paired-device-option'), connectOption: panel.querySelector('.connect-option'), disconnectOption: panel.querySelector('.disconnect-option'), unpairOption: panel.querySelector('.unpair-option') } }; // element related events elements.enableCheckbox.addEventListener('change', this._onEnableCheckboxClick.bind(this)); elements.visible.visibleCheckBox.addEventListener('change', this._onVisibleCheckBoxClick.bind(this)); elements.rename.renameButton.addEventListener('click', this._onRenameButtonClick.bind(this)); elements.search.searchButton.addEventListener('click', this._onSearchButtonClick.bind(this)); // paired devices list item click events pairedDeviceTemplate = BtTemplateFactory('paired', this._onPairedDeviceItemClick.bind(this)); // create found devices list view _pairedDevicesListView = ListView(elements.paired.pairedDevicesList, BtContext.getPairedDevices(), pairedDeviceTemplate); // found devices list item click events foundDeviceTemplate = BtTemplateFactory('remote', this._onFoundDeviceItemClick.bind(this)); // create found devices list view _foundDevicesListView = ListView(elements.found.foundDevicesList, BtContext.getRemoteDevices(), foundDeviceTemplate); }, onBeforeShow: function() { debug('onBeforeShow():'); // enable/disable BtContext.observe('state', this._boundUpdateEnableCheckbox); this._updateEnableCheckbox(BtContext.state); // visible BtContext.observe('state', this._boundUpdateVisibleItem); this._updateVisibleItem(BtContext.state); BtContext.observe('discoverable', this._boundUpdateVisibleCheckbox); this._updateVisibleCheckbox(BtContext.discoverable); BtContext.observe('discoverable', this._boundUpdateVisibilityTimer); this._updateVisibilityTimer(BtContext.discoverable); BtContext.observe('name', this._boundUpdateVisibleName); this._updateVisibleName(BtContext.name); // rename BtContext.observe('state', this._boundUpdateRenameItem); this._updateRenameItem(BtContext.state); // paired devices header BtContext.observe('hasPairedDevice', this._boundUpdatePairedDevicesHeader); this._updatePairedDevicesHeader(BtContext.hasPairedDevice); // paired devices list BtContext.observe('state', this._boundUpdatePairedDevicesList); this._updatePairedDevicesList(BtContext.state); // found devices list BtContext.observe('state', this._boundUpdateFoundDevicesList); this._updateFoundDevicesList(BtContext.state); _pairedDevicesListView.enabled = true; _foundDevicesListView.enabled = true; // search BtContext.observe('state', this._boundUpdateSearchItem); this._updateSearchItem(BtContext.state); BtContext.observe('discovering', this._boundUpdateSearchingItem); this._updateSearchingItem(BtContext.discovering); }, onShow: function() { debug('onShow():'); if (!this._leftApp) { // If settings app is still in the forground, // we start discovering device automatically // while Bluetooth panel is onShow. BtContext.startDiscovery().then(() => { debug('onShow(): startDiscovery successfully'); }, (reason) => { debug('onShow(): startDiscovery failed, ' + 'reason = ' + reason); }); } this._leftApp = false; }, onBeforeHide: function() { debug('onBeforeHide():'); BtContext.unobserve('state', this._boundUpdateEnableCheckbox); BtContext.unobserve('state', this._boundUpdateVisibleItem); BtContext.unobserve('discoverable', this._boundUpdateVisibleCheckbox); BtContext.unobserve('discoverable', this._boundUpdateVisibilityTimer); BtContext.unobserve('name', this._boundUpdateVisibleName); BtContext.unobserve('state', this._boundUpdateRenameItem); BtContext.unobserve('hasPairedDevice', this._boundUpdatePairedDevicesHeader); BtContext.unobserve('state', this._boundUpdatePairedDevicesList); BtContext.unobserve('state', this._boundUpdateFoundDevicesList); BtContext.unobserve('state', this._boundUpdateSearchItem); BtContext.unobserve('discovering', this._boundUpdateSearchingItem); }, onHide: function() { debug('onHide():'); this._leftApp = document.hidden; if (!this._leftApp) { // If settings app is still in the forground, // we stop discovering device automatically // while Bluetooth panel is onHide. BtContext.stopDiscovery(); } if (_pairedDevicesListView) { _pairedDevicesListView.enabled = false; } if (_foundDevicesListView) { _foundDevicesListView.enabled = false; } }, _onEnableCheckboxClick: function() { var checkbox = elements.enableCheckbox; debug('_onEnableCheckboxClick(): checked = ' + checkbox.checked); BtContext.setEnabled(checkbox.checked).then(() => { debug('_onEnableCheckboxClick(): setEnabled ' + checkbox.checked + ' successfully'); }, (reason) => { debug('_onEnableCheckboxClick(): setEnabled ' + checkbox.checked + ' failed, reason = ' + reason); }); }, _onVisibleCheckBoxClick: function() { var checkbox = elements.visible.visibleCheckBox; debug('_onVisibleCheckBoxClick(): checked = ' + checkbox.checked); BtContext.setDiscoverable(checkbox.checked).then(() => { debug('_onVisibleCheckBoxClick(): setDiscoverable ' + checkbox.checked + ' successfully'); }, (reason) => { debug('_onVisibleCheckBoxClick(): setDiscoverable ' + checkbox.checked + ' failed, reason = ' + reason); }); }, _onRenameButtonClick: function() { var messageL10nId = 'change-btdevice-name-desc'; var titleL10nId = 'change-btdevice-name'; var myDeviceName = BtContext.name; debug('_onRenameButtonClick(): myDeviceName = ' + myDeviceName); DialogService.prompt(messageL10nId, { title: titleL10nId, defaultValue: myDeviceName, submitButton: 'ok', cancelButton: 'cancel' }).then((result) => { var type = result.type; var value = result.value; if (type === 'submit') { this._onRenameSubmit(value); } }); }, _onRenameSubmit: function(nameEntered) { debug('_onRenameSubmit(): nameEntered = ' + nameEntered); // Before set the entered name to platform, we check length of name is // over threshold or not. nameEntered = nameEntered.replace(/^\s+|\s+$/g, ''); if (nameEntered.length > MAX_DEVICE_NAME_LENGTH) { this._confirmNameEnteredOverLength(); // Early return here since name entered is over threshold. return; } // Only set non-empty string to be new name. // Otherwise, set name by product model. if (nameEntered !== '') { debug('_onRenameSubmit(): set new name = ' + nameEntered); BtContext.setName(nameEntered).then(() => { debug('_onRenameSubmit(): setName = ' + nameEntered + ' successfully'); }, (reason) => { debug('_onRenameSubmit(): setName = ' + nameEntered + ' failed, reason = ' + reason); }); } else { debug('_onRenameSubmit(): set name by product model'); BtContext.setNameByProductModel(); } }, _confirmNameEnteredOverLength: function() { var messageL10nId = { id: 'bluetooth-name-maxlength-alert', args: {'length': MAX_DEVICE_NAME_LENGTH} }; var titleL10nId = 'settings'; DialogService.confirm(messageL10nId, { title: titleL10nId, submitButton: 'ok', cancelButton: 'cancel' }).then((result) => { var type = result.type; if (type === 'submit') { this._onRenameButtonClick(); } else { // Just return here since user give up to set name. return; } }); }, _updateEnableCheckbox: function(state) { debug('_updateEnableCheckbox(): ' + 'callback from observe "state" = ' + state); // Update Bluetooth enable checkbox elements.enableCheckbox.checked = (state === 'enabled') || (state === 'enabling'); if ((state === 'enabling') || (state === 'disabling')) { elements.enableCheckbox.setAttribute('disabled', 'true'); } else { elements.enableCheckbox.removeAttribute('disabled'); } // Update Bluetooth enable checkbox message elements.enableCheckboxMsg.hidden = (state === 'enabled') || (state === 'enabling'); }, _updateVisibleItem: function(state) { debug('_updateVisibleItem(): ' + 'callback from observe "state" = ' + state); elements.visible.visibleItem.hidden = !((state === 'enabled') || (state === 'enabling')); elements.visible.visibleItem.disabled = (state === 'enabled'); }, _updateVisibleCheckbox: function(discoverable) { debug('_updateVisibleCheckbox(): ' + 'callback from observe "discoverable" = ' + discoverable); elements.visible.visibleCheckBox.checked = discoverable; }, _updateVisibilityTimer: function(discoverable) { debug('_updateVisibilityTimer(): ' + 'callback from observe "discoverable" = ' + discoverable); // Visibility will time out after 2 mins. if (discoverable && !this._visibleTimeout) { debug('_updateVisibilityTimer(): setTimeout to disable visibility ' + 'after 2 minutes'); this._visibleTimeout = setTimeout(() => { BtContext.setDiscoverable(false); }, VISIBLE_TIMEOUT_TIME); // Early return here since already set timeout. return; } if (!discoverable && this._visibleTimeout) { debug('_updateVisibilityTimer(): clearTimeout'); clearTimeout(this._visibleTimeout); this._visibleTimeout = null; } }, _updateVisibleName: function(name) { debug('_updateVisibleName(): ' + 'callback from observe "name" = ' + name); elements.visible.visibleName.textContent = name; }, _updateRenameItem: function(state) { debug('_updateRenameItem(): ' + 'callback from observe "state" = ' + state); elements.rename.renameItem.hidden = ((state === 'enabled') || (state === 'enabling')) ? false : true; elements.rename.renameButton.disabled = (state === 'enabled') ? false : true; }, _updatePairedDevicesHeader: function(hasPairedDevice) { debug('_updatePairedDevicesHeader(): ' + 'callback from observe "hasPairedDevice" = ' + hasPairedDevice); elements.paired.pairedDevicesHeader.hidden = (hasPairedDevice && (BtContext.state === 'enabled')) ? false : true; }, _updatePairedDevicesList: function(state) { debug('_updatePairedDevicesList(): ' + 'callback from observe "state" = ' + state); elements.paired.pairedDevicesHeader.hidden = ((state === 'enabled') && (BtContext.hasPairedDevice)) ? false : true; elements.paired.pairedDevicesList.hidden = (state === 'enabled') ? false : true; }, _updateFoundDevicesList: function(state) { debug('_updateFoundDevicesList(): ' + 'callback from observe "state" = ' + state); elements.found.foundDevicesHeader.hidden = (state === 'enabled') ? false : true; elements.found.foundDevicesList.hidden = (state === 'enabled') ? false : true; }, _updateSearchItem: function(state) { debug('_updateSearchItem(): ' + 'callback from observe "state" = ' + state); elements.search.searchItem.hidden = (state === 'enabled') ? false : true; }, _updateSearchingItem: function(discovering) { debug('_updateSearchingItem(): ' + 'callback from observe "discovering" = ' + discovering); elements.search.searchingItem.hidden = !discovering; elements.search.searchButton.disabled = discovering; }, _onSearchButtonClick: function() { debug('_onSearchButtonClick():'); BtContext.startDiscovery().then(() => { debug('_onSearchButtonClick(): startDiscovery successfully'); }, (reason) => { debug('_onSearchButtonClick(): startDiscovery failed, ' + 'reason = ' + reason); }); }, _onPairedDeviceItemClick: function(deviceItem) { debug('_onPairedDeviceItemClick(): deviceItem.address = ' + deviceItem.address); debug('_onPairedDeviceItemClick(): deviceItem.paired = ' + deviceItem.paired); // Pop out dialog and show operation correspond to device paired device // type. switch (deviceItem.type) { case 'audio-card': case 'audio-input-microphone': // We only support 'audio-card', 'audio-input-microphone' device // to connect. Before pop out a dialog for operation, we should // check the paired device is connected or not. this._showActionMenu(deviceItem); break; default: // These devices are supported to pair/unpair only. // Pop confirmation dialog to make sure user's decision. this._confirmUserWantToUnpairDevice(deviceItem); break; } }, _showActionMenu: function(deviceItem) { if (deviceItem.connectionStatus === 'connected') { elements.actionMenu.connectOption.style.display = 'none'; elements.actionMenu.disconnectOption.style.display = 'block'; elements.actionMenu.disconnectOption.onclick = () => { BtConnectionManager.disconnect(deviceItem.data); elements.actionMenu.actionMenuDialog.hide(); }; elements.actionMenu.unpairOption.onclick = () => { // Show a confirmation dialog while a user wants to unpair // the connected device. Because the device is connected to use now. this._confirmUserWantToUnpairDeviceWhileItisConnected(deviceItem); elements.actionMenu.actionMenuDialog.hide(); }; } else if (deviceItem.connectionStatus === 'disconnected') { elements.actionMenu.connectOption.style.display = 'block'; elements.actionMenu.disconnectOption.style.display = 'none'; elements.actionMenu.unpairOption.onclick = () => { this._confirmToUnpair(deviceItem); elements.actionMenu.actionMenuDialog.hide(); }; elements.actionMenu.connectOption.onclick = () => { this._connectHeadsetDevice(deviceItem); elements.actionMenu.actionMenuDialog.hide(); }; } // Show the action menu. elements.actionMenu.actionMenuDialog.show(); }, _confirmUserWantToUnpairDeviceWhileItisConnected: function(deviceItem) { var messageL10nId = { id: 'unpair-msg' }; var titleL10nId = 'unpair-title'; DialogService.confirm(messageL10nId, { title: titleL10nId, submitButton: 'ok', cancelButton: 'cancel' }).then((result) => { var type = result.type; if (type === 'submit') { this._confirmToUnpair(deviceItem); } else { // Just return here since user give up to unpair. return; } }); }, _confirmUserWantToUnpairDevice: function(deviceItem) { var messageL10nId = { id: 'device-option-unpair-device' }; var titleL10nId = 'device-option-unpair-confirmation'; DialogService.confirm(messageL10nId, { title: titleL10nId, submitButton: { id: 'device-option-confirm', style: 'danger' }, cancelButton: 'cancel' }).then((result) => { var type = result.type; if (type === 'submit') { this._confirmToUnpair(deviceItem); } else { // Just return here since user give up to unpair. return; } }); }, _confirmToUnpair: function(deviceItem) { debug('_confirmToUnpair(): deviceItem.address = ' + deviceItem.address); BtContext.unpair(deviceItem.address).then(() => { debug('_onPairedDeviceItemClick(): unpair successfully'); }, (reason) => { debug('_onPairedDeviceItemClick(): unpair failed, ' + 'reason = ' + reason); }); }, _onFoundDeviceItemClick: function(deviceItem) { debug('_onFoundDeviceItemClick(): deviceItem.address = ' + deviceItem.address); // Update device pairing status first. deviceItem.paired = 'pairing'; // Pair with the remote device. BtContext.pair(deviceItem.address).then(() => { debug('_onFoundDeviceItemClick(): pair successfully'); // Connect the device which is just paired. this._connectHeadsetDevice(deviceItem); }, (reason) => { debug('_onFoundDeviceItemClick(): pair failed, ' + 'reason = ' + reason); // Reset the paired status back to false, // since the 'pairing' status is given in Gaia side. deviceItem.paired = false; // Show alert message while pair device failed. this._alertPairFailedErrorMessage(reason); }); }, _connectHeadsetDevice: function(deviceItem) { if (!((deviceItem.type === 'audio-card') || (deviceItem.type === 'audio-input-microphone'))) { return; } BtConnectionManager.connect(deviceItem.data).then(() => { debug('_connectHeadsetDevice(): connect device successfully'); }, (reason) => { debug('_connectHeadsetDevice(): connect device failed, ' + 'reason = ' + reason); // Show alert message while connect device failed. this._alertConnectErrorMessage(); }); }, _alertConnectErrorMessage: function() { DialogService.alert('error-connect-msg', {title: 'settings'}); }, _alertPairFailedErrorMessage: function(errorReason) { var errorMessage = 'error-pair-title'; // This is a default message. if (errorReason === 'Repeated Attempts') { errorMessage = 'error-pair-toofast'; } else if (errorReason === 'Authentication Failed') { errorMessage = 'error-pair-pincode'; } DialogService.alert(errorMessage, {title: 'error-pair-title'}); } }); }; });