'use strict'; // handle Bluetooth settings navigator.mozL10n.once(function bluetoothSettings() { // Service ID for profiles var Profiles = { 'HFP': 0x111E, 'A2DP': 0x110D }; var _ = navigator.mozL10n.get; var l10n = navigator.mozL10n; var settings = navigator.mozSettings; var bluetooth = navigator.mozBluetooth; var defaultAdapter = null; const DEVICE_CONNECTED_KEY = 'bluetooth.device.connected'; var MAX_DEVICE_NAME_LENGTH = 20; if (!settings || !bluetooth) { return; } var header = document.querySelector('gaia-header'); header.addEventListener('action', function backToSettings() { window.close(); }); var gBluetoothCheckBox = document.querySelector('.bluetooth-status gaia-switch'); // display Bluetooth power state function updateBluetoothState(value) { gBluetoothCheckBox.checked = value; } // activate main button gBluetoothCheckBox.addEventListener('change', function() { console.log("GOT CHANGE EVENT!, setting disabled"); var req = settings.createLock().set({'bluetooth.enabled': this.checked}); gBluetoothCheckBox.setAttribute('disabled', true); req.onerror = function() { gBluetoothCheckBox.removeAttribute('disabled'); }; }); function initialDefaultAdapter() { if (!bluetooth.enabled) return; var req = bluetooth.getDefaultAdapter(); req.onsuccess = function bt_getAdapterSuccess() { defaultAdapter = req.result; if (defaultAdapter == null) { // we can do nothing without DefaultAdapter, so set bluetooth disabled settings.createLock().set({'bluetooth.enabled': false}); return; } defaultAdapter.ondevicefound = gDeviceList.onDeviceFound; // initial related components that need defaultAdapter. gMyDeviceInfo.initWithAdapter(); gDeviceList.initWithAdapter(); }; req.onerror = function bt_getAdapterFailed() { // we can do nothing without DefaultAdapter, so set bluetooth disabled settings.createLock().set({'bluetooth.enabled': false}); }; } // device information var gMyDeviceInfo = (function deviceInfo() { var visibleItem = document.querySelector('.device-visible'); var visibleName = document.querySelector('.bluetooth-device-name'); var visibleCheckBox = document.querySelector('.device-visible gaia-switch'); var bluetoothRename = document.getElementById('bluetooth-rename'); var renameButton = document.getElementById('rename-device'); var updateNameDialog = document.getElementById('update-device-name'); var updateNameInput = document.getElementById('update-device-name-input'); var updateNameCancelButton = document.getElementById('update-device-name-cancel'); var updateNameConfirmButton = document.getElementById('update-device-name-confirm'); var visibleTimeout = null; var visibleTimeoutTime = 120000; // visibility will timeout after 2 minutes var myName = ''; visibleCheckBox.addEventListener('change', function() { setDiscoverable(visibleCheckBox.checked); }); renameButton.onclick = function renameBtnClicked() { if (myName === '') { myName = visibleName.textContent = defaultAdapter.name; } updateNameInput.value = myName; updateNameDialog.hidden = false; // Focus the input field to trigger showing the keyboard updateNameInput.focus(); var cursorPos = updateNameInput.value.length; updateNameInput.setSelectionRange(0, cursorPos); }; updateNameCancelButton.onclick = function updateNameCancelClicked(evt) { if (evt) evt.preventDefault(); updateNameDialog.hidden = true; }; updateNameConfirmButton.onclick = function updateNameConfirmClicked(evt) { if (evt) evt.preventDefault(); var nameEntered = updateNameInput.value; nameEntered = nameEntered.replace(/^\s+|\s+$/g, ''); if (nameEntered.length > MAX_DEVICE_NAME_LENGTH) { var wantToRetry = window.confirm(_('bluetooth-name-maxlength-alert', { length: MAX_DEVICE_NAME_LENGTH })); if (!wantToRetry) { updateNameDialog.hidden = true; } return; } if (nameEntered === myName || !bluetooth.enabled || !defaultAdapter) { updateNameDialog.hidden = true; return; } if (nameEntered !== '') { updateDeviceName(nameEntered); } else { // Bug 847459: Default name of the bluetooth device is set by bluetoothd // to the value of the Android ro.product.model property upon first // start. In case the user gives an empty bluetooth device name, we want // to revert to the original ro.product.model. Gecko exposes it under // the deviceinfo.product_model setting. var deviceInfo = settings.createLock().get('deviceinfo.product_model'); deviceInfo.onsuccess = function bt_getProductModel() { var productModel = deviceInfo.result['deviceinfo.product_model']; updateDeviceName(productModel); }; } updateNameDialog.hidden = true; }; function updateDeviceName(nameEntered) { var req = defaultAdapter.setName(nameEntered); req.onsuccess = function bt_renameSuccess() { myName = visibleName.textContent = defaultAdapter.name; }; } // immediatly UI update, DOM element manipulation. function updateDeviceInfo(show) { bluetoothRename.hidden = !show; if (show) { visibleItem.hidden = false; // get last user setting for device visible var req = settings.createLock().get('bluetooth.visible'); req.onsuccess = function bt_getVisibleSuccess() { var visible = req.result['bluetooth.visible']; if (typeof visible === 'undefined') { visible = true; } setDiscoverable(visible); }; req.onerror = function bt_getVisibleError() { visibleCheckBox.checked = true; }; } else { visibleItem.hidden = true; renameButton.disabled = true; if (visibleTimeout) { clearTimeout(visibleTimeout); visibleTimeout = null; } } } // initial this device information and do default actions // when DefaultAdapter is ready. function initial() { visibleCheckBox.checked = defaultAdapter.discoverable; setDiscoverable(visibleCheckBox.checked); // we can't get device name immediately, wait a while setTimeout(function() { myName = visibleName.textContent = defaultAdapter.name; renameButton.disabled = false; }, 1000); } function setDiscoverable(visible) { if (!bluetooth.enabled || !defaultAdapter) return; settings.createLock().set({'bluetooth.visible': visible}); defaultAdapter.setDiscoverable(visible); // Visibility will time out after 2 mins. if (visible) { if (!visibleTimeout) { visibleTimeout = setTimeout(function() { setDiscoverable(false); }, visibleTimeoutTime); } } else { if (visibleTimeout) { clearTimeout(visibleTimeout); visibleTimeout = null; } } visibleCheckBox.checked = visible; } // API return { update: updateDeviceInfo, initWithAdapter: initial }; })(); // device list var gDeviceList = (function deviceList() { var bluetoothSearch = document.getElementById('bluetooth-search'); var searchAgainBtn = document.getElementById('search-device'); var searchingItem = document.getElementById('bluetooth-searching'); var enableMsg = document.getElementById('bluetooth-enable-msg'); var pairingAddress = null; var connectingAddress = null; var connectedAddress = null; // stop discover other device after 60 seconds var discoverTimeoutTime = 60000; var discoverTimeout = null; var pairList = { title: document.getElementById('bluetooth-paired-title'), list: document.getElementById('bluetooth-paired-devices'), index: [], clear: function emptyList() { while (this.list.hasChildNodes()) { this.list.removeChild(this.list.lastChild); } this.index = []; }, show: function showArea(isShown) { if (!isShown) { this.clear(); } this.title.hidden = !isShown; this.list.hidden = !isShown; } }; var openList = { title: document.getElementById('bluetooth-found-title'), list: document.getElementById('bluetooth-devices'), index: [], clear: function emptyList() { while (this.list.hasChildNodes()) { this.list.removeChild(this.list.lastChild); } this.index = []; }, show: function showArea(isShown) { if (!isShown) { this.clear(); } this.title.hidden = !isShown; this.list.hidden = !isShown; } }; var optionMenu = { menu: document.getElementById('paired-device-option'), connectOpt: document.getElementById('connect-option'), disconnectOpt: document.getElementById('disconnect-option'), unpairOpt: document.getElementById('unpair-option'), confirmDlg: document.getElementById('unpair-device'), unpairCancel: document.getElementById('unpair-device-cancel'), confirmOpt: document.getElementById('confirm-option'), showActions: function showActions() { var self = this; if (connectedAddress && this.device.address === connectedAddress) { this.connectOpt.style.display = 'none'; this.disconnectOpt.style.display = 'block'; this.disconnectOpt.onclick = function() { setDeviceDisconnect(self.device); }; } else { this.connectOpt.style.display = 'block'; this.disconnectOpt.style.display = 'none'; this.connectOpt.onclick = function() { setDeviceConnect(self.device); }; } this.unpairOpt.onclick = function() { setDeviceUnpair(self.device); }; this.menu.addEventListener('gaiamenu-cancel', function closeMenu() { return self.close(); }); this.menu.show(); }, showConfirm: function showConfirm() { var self = this; this.unpairCancel.onclick = function() { return self.close(); }; this.confirmOpt.onclick = function() { setDeviceUnpair(self.device); return self.close(); }; this.confirmDlg.hidden = false; }, show: function showMenu(device) { this.device = device; // we only support audio-card device to connect atm this[this.device.icon === 'audio-card' ? 'showActions' : 'showConfirm'](); }, close: function closeMenu() { this.menu.hide(); this.confirmDlg.hidden = true; return false; } }; searchAgainBtn.onclick = function searchAgainClicked() { updateDeviceList(true); // reset network list openList.clear(); startDiscovery(); }; // private DOM helper: create a device list item function newListItem(device, descL10nId) { var deviceName = document.createElement('span'); if (device.name !== '') { deviceName.textContent = device.name; } else { deviceName.setAttribute('data-l10n-id', 'unnamed-device'); } var deviceDesc = document.createElement('small'); if (descL10nId) { deviceDesc.setAttribute('data-l10n-id', descL10nId); } var a = document.createElement('a'); a.appendChild(deviceName); a.appendChild(deviceDesc); var li = document.createElement('li'); li.classList.add('bluetooth-device'); li.classList.add('bluetooth-type-' + device.icon); li.appendChild(a); return li; } // immediatly UI update, DOM element manipulation. function updateDeviceList(show) { bluetoothSearch.hidden = !show; if (show) { enableMsg.hidden = true; openList.show(true); searchingItem.hidden = false; document.addEventListener('visibilitychange', stopDiscoveryWhenLeaveApp); } else { openList.show(false); pairList.show(false); enableMsg.hidden = false; searchingItem.hidden = true; optionMenu.close(); pairingAddress = null; connectingAddress = null; connectedAddress = null; // clear discoverTimeout clearTimeout(discoverTimeout); discoverTimeout = null; document.removeEventListener('visibilitychange', stopDiscoveryWhenLeaveApp); } } // do default actions (start discover avaliable devices) // when DefaultAdapter is ready. function initial() { defaultAdapter.onpairedstatuschanged = function bt_getPairedMessage(evt) { dispatchEvent(new CustomEvent('bluetooth-pairedstatuschanged')); showDevicePaired(evt.status, 'Authentication Failed'); }; defaultAdapter.ondiscoverystatechanged = function bt_discoveryStateChanged(evt) { if (!evt.discovering) { searchAgainBtn.disabled = false; searchingItem.hidden = true; clearTimeout(discoverTimeout); discoverTimeout = null; } else { searchAgainBtn.disabled = true; searchingItem.hidden = false; } }; defaultAdapter.onhfpstatuschanged = function bt_hfpStatusChanged(evt) { showDeviceConnected(evt.address, evt.status, Profiles.HFP); }; defaultAdapter.ona2dpstatuschanged = function bt_a2dpStatusChanged(evt) { showDeviceConnected(evt.address, evt.status, Profiles.A2DP); }; // Get paired device getPairedDevice(function() { for (var address in pairList.index) { var deviceItem = pairList.index[address]; if (Object.keys(deviceItem.connectedProfiles).length > 0) return; } // If there is no current connection and we have one device connected // before, restore it. restoreConnection(); }); startDiscovery(); } function restoreConnection() { // Reconnect the one kept in settings key. var connectedDevice = settings.createLock().get(DEVICE_CONNECTED_KEY); connectedDevice.onsuccess = function bt_getConnectedDevice() { var address = connectedDevice.result[DEVICE_CONNECTED_KEY]; if (!address || !pairList.index[address]) return; var device = pairList.index[address].device; setDeviceConnect(device); }; } function getPairedDevice(callback) { if (!bluetooth.enabled || !defaultAdapter) return; var req = defaultAdapter.getPairedDevices(); req.onsuccess = function bt_getPairedSuccess() { // copy for sorting var paired = req.result.slice(); var length = paired.length; if (length == 0) { pairList.show(false); return; } pairList.clear(); paired.sort(function(a, b) { return a.name > b.name; }); for (var i = 0; i < length; i++) { (function(device) { var aItem = newListItem(device, ''); aItem.onclick = function() { optionMenu.show(device); }; pairList.list.appendChild(aItem); pairList.index[device.address] = { device: device, item: aItem, connectedProfiles: {} }; // if the device has to be connected when it just paired // wait for a while so they can have time to communicate // their connection protocol if (device.address === connectingAddress && device.icon === 'audio-card') { var small = aItem.querySelector('small'); small.setAttribute('data-l10n-id', 'device-status-connecting'); setTimeout(function() { setDeviceConnect(device); }, 5000); } })(paired[i]); } // update the connection status getConnectedDeviceItems(function(connectedDeviceItems) { if (connectedDeviceItems.length > 0) { connectedDeviceItems.forEach(function(item) { var connectedDevice = item.device; var connectedProfiles = item.connectedProfiles; for (var profile in connectedProfiles) { showDeviceConnected(connectedDevice.address, true, profile); } }); } pairList.show(true); // the callback function now is for restoring the connected device // when the bluetooth is turned on. if (callback) callback(); }); }; } // In the callback we returns an array of connected device items. // Each device item contains "device" and "connectedProfiles". function getConnectedDeviceItems(callback) { if (!callback) return; if (!defaultAdapter) { callback([]); return; } var getConnectedDevicesByProfile = function(profileID, gcdCallback) { if (!gcdCallback) return; var req = defaultAdapter.getConnectedDevices(profileID); req.onsuccess = function() { gcdCallback(req.result || []); }; req.onerror = function() { gcdCallback(null); }; }; var connectedDeviceItemsMap = {}; // hash by device address var updateDeviceItemsMap = function(profileID, connectedDevices) { if (!connectedDevices) return; connectedDevices.forEach(function(connectedDevice) { var info = connectedDeviceItemsMap[connectedDevice.address]; if (info) { info.connectedProfiles[profileID] = true; } else { info = { 'device': connectedDevice, 'connectedProfiles': {} }; info.connectedProfiles[profileID] = true; } connectedDeviceItemsMap[connectedDevice.address] = info; }); }; // XXX: we should have better ways of doing this. getConnectedDevicesByProfile(Profiles.HFP, function(hfpResult) { updateDeviceItemsMap(Profiles.HFP, hfpResult); getConnectedDevicesByProfile(Profiles.A2DP, function(a2dpResult) { updateDeviceItemsMap(Profiles.A2DP, a2dpResult); var connectedDeviceItems = []; for (var i in connectedDeviceItemsMap) { var item = connectedDeviceItemsMap[i]; connectedDeviceItems.push(item); } callback(connectedDeviceItems); }); }); } // callback function when an avaliable device found function onDeviceFound(evt) { var device = evt.device; // Ignore duplicate and paired device. Update the name if needed. var existingDevice = openList.index[device.address] || pairList.index[device.address]; if (existingDevice) { var existingItem = existingDevice.item; if (device.name && existingItem) { var deviceName = existingItem.querySelector('a > span'); if (deviceName) { deviceName.removeAttribute('data-l10n-id'); deviceName.textContent = device.name; } } return; } var aItem = newListItem(device, 'device-status-tap-connect'); // bind paired callback aItem.onclick = function() { // block the pairing request if there is already one. if (pairingAddress) return; var small = aItem.querySelector('small'); small.setAttribute('data-l10n-id', 'device-status-pairing'); this.setAttribute('aria-disabled', true); stopDiscovery(); // pairing dialog is handled and showing via Bluetooth app var req = defaultAdapter.pair(device.address); pairingAddress = device.address; req.onerror = function bt_pairError(error) { showDevicePaired(false, req.error.name); }; }; openList.list.appendChild(aItem); openList.index[device.address] = { device: device, item: aItem, connectedProfiles: {} }; } function showDevicePaired(paired, errorMessage) { // If we don't know the pairing device address, // it means the pair request is handled by interface level. // So we just need to update paired list. if (!pairingAddress) { getPairedDevice(); return; } // clear pairingAddress first to prevent execute // the same status update twice. var workingAddress = pairingAddress; pairingAddress = null; if (paired) { // if the device is on the list, remove it. // it will show on paired list later. if (openList.index[workingAddress]) { var device = openList.index[workingAddress].device; var item = openList.index[workingAddress].item; openList.list.removeChild(item); delete openList.index[workingAddress]; connectingAddress = workingAddress; } } else { // show pair process fail. var msg = _('error-pair-title'); if (errorMessage === 'Repeated Attempts') { msg = msg + '\n' + _('error-pair-toofast'); } else if (errorMessage === 'Authentication Failed') { msg = msg + '\n' + _('error-pair-pincode'); } window.alert(msg); // rollback device status if (openList.index[workingAddress]) { var item = openList.index[workingAddress].item; var small = item.querySelector('small'); item.removeAttribute('aria-disabled'); small.setAttribute('data-l10n-id', 'device-status-tap-connect'); } } // acquire a new paired list no matter paired or unpaired getPairedDevice(); } function setDeviceUnpair(device) { if (device.address === connectedAddress) { var msg = _('unpair-title') + '\n' + _('unpair-msg'); if (!window.confirm(msg)) return; connectedAddress = null; } // backend takes responsibility to disconnect first. var req = defaultAdapter.unpair(device.address); req.onerror = function bt_pairError() { showDevicePaired(true, null); }; } function setDeviceDisconnect(device, callback) { if (!bluetooth.enabled || !defaultAdapter || device.address !== connectedAddress) { if (callback) callback(); return; } var req = defaultAdapter.disconnect(device); req.onsuccess = req.onerror = function() { if (callback) callback(); }; } function setDeviceConnect(device) { // we only support audio-card device to connect now if (!bluetooth.enabled || !defaultAdapter || device.icon !== 'audio-card' || device.address === connectedAddress) { connectingAddress = null; return; } var doConnect = function() { var connectSuccess = function bt_connectSuccess() { if (connectingAddress) { connectingAddress = null; } }; var connectError = function bt_connectError() { // Connection state might be changed before DOM request response. if (connectingAddress) { // Clear the text of connecting status. var small = pairList.index[connectingAddress].item.querySelector('small'); small.removeAttribute('data-l10n-id'); small.textContent = ''; connectingAddress = null; window.alert(_('error-connect-msg')); } }; stopDiscovery(); var req = defaultAdapter.connect(device); req.onsuccess = connectSuccess; // At least one profile is connected. req.onerror = connectError; // No available profiles are connected. connectingAddress = device.address; if (!pairList.index[connectingAddress]) { return; } var small = pairList.index[connectingAddress].item.querySelector('small'); small.setAttribute('data-l10n-id', 'device-status-connecting'); }; // disconnect current connected device first if (connectedAddress && pairList.index[connectedAddress]) { setDeviceDisconnect(pairList.index[connectedAddress].device, doConnect); } else { doConnect(); } } function showDeviceConnected(deviceAddress, connected, profile) { var deviceItem = pairList.index[deviceAddress]; if (!deviceItem) return; deviceItem.connectedProfiles[profile] = connected; var existConnectedProfile = false; if (connected) { existConnectedProfile = true; } else { // Check if there are other connected profiles for (var profile in deviceItem.connectedProfiles) { existConnectedProfile = existConnectedProfile || deviceItem.connectedProfiles[profile]; } } if (existConnectedProfile) { connectedAddress = deviceAddress; // record connected device so if Bluetooth is turned off and then on // we can restore the connection settings.createLock().set({ 'bluetooth.device.connected': connectedAddress }); } else { if (connectedAddress === deviceAddress) { connectedAddress = null; settings.createLock().set({'bluetooth.device.connected': null}); } } var l10nId = ''; var hfpConnected = deviceItem.connectedProfiles[Profiles.HFP]; var a2dpConnected = deviceItem.connectedProfiles[Profiles.A2DP]; if (hfpConnected && a2dpConnected) { l10nId = 'device-status-connected-phone-media'; } else if (hfpConnected) { l10nId = 'device-status-connected-phone'; } else if (a2dpConnected) { l10nId = 'device-status-connected-media'; } else { l10nId = null; } var small = pairList.index[deviceAddress].item.querySelector('small'); if (l10nId) { small.setAttribute('data-l10n-id', l10nId); } else { small.removeAttribute('data-l10n-id'); small.textContent = ''; } } function startDiscovery() { if (!bluetooth.enabled || !defaultAdapter || discoverTimeout || document.hidden) { return; } var req = defaultAdapter.startDiscovery(); req.onsuccess = function bt_discoveryStart() { if (!discoverTimeout) discoverTimeout = setTimeout(stopDiscovery, discoverTimeoutTime); }; req.onerror = function bt_discoveryFailed() { console.error('Can not discover nearby device'); }; } function stopDiscoveryWhenLeaveApp() { //only stop discovery when Settings app is hidden if (!document.hidden) return; stopDiscovery(); } function stopDiscovery() { if (!bluetooth.enabled || !defaultAdapter || !discoverTimeout) return; var req = defaultAdapter.stopDiscovery(); req.onerror = function bt_discoveryStopFailed() { console.error('Can not stop discover nearby device'); }; clearTimeout(discoverTimeout); discoverTimeout = null; } // API return { update: updateDeviceList, initWithAdapter: initial, startDiscovery: startDiscovery, onDeviceFound: onDeviceFound }; })(); var lastMozSettingValue = false; // enable Bluetooth if the related settings says so // register an observer to monitor bluetooth.enabled changes settings.addObserver('bluetooth.enabled', function(event) { var enabled = event.settingValue; if (lastMozSettingValue == enabled) return; // lock UI toggle gBluetoothCheckBox.setAttribute('disabled', true); lastMozSettingValue = enabled; updateBluetoothState(enabled); gDeviceList.update(enabled); gMyDeviceInfo.update(enabled); // clear defaultAdapter, we have to acquire it again when enabled. if (!enabled) defaultAdapter = null; }); // startup, update status var req = settings.createLock().get('bluetooth.enabled'); req.onsuccess = function bt_getSettingsSuccess() { lastMozSettingValue = req.result['bluetooth.enabled']; // if bluetooth is on when booting, the adapter probably is ready. if (lastMozSettingValue) initialDefaultAdapter(); updateBluetoothState(lastMozSettingValue); gDeviceList.update(lastMozSettingValue); gMyDeviceInfo.update(lastMozSettingValue); }; bluetooth.addEventListener('adapteradded', function() { // enable UI toggle gBluetoothCheckBox.removeAttribute('disabled'); initialDefaultAdapter(); }); bluetooth.addEventListener('disabled', function() { gBluetoothCheckBox.removeAttribute('disabled'); // enable UI toggle defaultAdapter = null; // clear defaultAdapter }); });