| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * Javascript for bluetooth_internals.html, served from |
| * chrome://bluetooth-internals/. |
| */ |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {$} from 'chrome://resources/js/util.js'; |
| |
| import {DiscoverySessionRemote} from './adapter.mojom-webui.js'; |
| import {AdapterBroker, AdapterProperty, getAdapterBroker} from './adapter_broker.js'; |
| import {AdapterPage} from './adapter_page.js'; |
| import {BluetoothInternalsHandler, BluetoothInternalsHandlerRemote} from './bluetooth_internals.mojom-webui.js'; |
| import {DebugLogPage} from './debug_log_page.js'; |
| import {DeviceCollection} from './device_collection.js'; |
| import {DeviceDetailsPage} from './device_details_page.js'; |
| import {DevicesPage, ScanStatus} from './devices_page.js'; |
| import {PageManager, PageManagerObserver} from './page_manager.js'; |
| import {Sidebar} from './sidebar.js'; |
| import {showSnackbar, SnackbarType} from './snackbar.js'; |
| |
| |
| |
| // Expose for testing. |
| /** @type {AdapterBroker} */ |
| export let adapterBroker = null; |
| |
| /** @type {DeviceCollection} */ |
| export let devices = null; |
| |
| /** @type {Sidebar} */ |
| export let sidebarObj = null; |
| |
| /** @type {PageManager} */ |
| export const pageManager = PageManager.getInstance(); |
| |
| devices = new DeviceCollection([]); |
| |
| /** @type {AdapterPage} */ |
| let adapterPage = null; |
| /** @type {DevicesPage} */ |
| let devicesPage = null; |
| /** @type {DebugLogPage} */ |
| let debugLogPage = null; |
| |
| /** @type {DiscoverySessionRemote} */ |
| let discoverySession = null; |
| |
| /** @type {boolean} */ |
| let userRequestedScanStop = false; |
| |
| /** |
| * Observer for page changes. Used to update page title header. |
| */ |
| const PageObserver = class extends PageManagerObserver { |
| updateHistory(path) { |
| window.location.hash = '#' + path; |
| } |
| |
| /** |
| * Sets the page title. Called by PageManager. |
| * @override |
| * @param {string} title |
| */ |
| updateTitle(title) { |
| document.querySelector('.page-title').textContent = title; |
| } |
| }; |
| |
| /** |
| * Removes DeviceDetailsPage with matching device |address|. The associated |
| * sidebar item is also removed. |
| * @param {string} address |
| */ |
| function removeDeviceDetailsPage(address) { |
| const id = 'devices/' + address.toLowerCase(); |
| sidebarObj.removeItem(id); |
| |
| const deviceDetailsPage = |
| /** @type {!DeviceDetailsPage} */ (pageManager.registeredPages.get(id)); |
| |
| // The device details page does not necessarily exist, return early if it is |
| // not found. |
| if (!deviceDetailsPage) { |
| return; |
| } |
| |
| deviceDetailsPage.disconnect(); |
| deviceDetailsPage.pageDiv.parentNode.removeChild(deviceDetailsPage.pageDiv); |
| |
| // Inform the devices page that the user is inspecting this device. |
| // This will update the links in the device table. |
| devicesPage.setInspecting( |
| deviceDetailsPage.deviceInfo, false /* isInspecting */); |
| |
| pageManager.unregister(deviceDetailsPage); |
| } |
| |
| /** |
| * Creates a DeviceDetailsPage with the given |deviceInfo|, appends it to |
| * '#page-container', and adds a sidebar item to show the new page. If a |
| * page exists that matches |deviceInfo.address|, nothing is created and the |
| * existing page is returned. |
| * @param {!DeviceInfo} deviceInfo |
| * @return {!DeviceDetailsPage} |
| */ |
| function makeDeviceDetailsPage(deviceInfo) { |
| const deviceDetailsPageId = 'devices/' + deviceInfo.address.toLowerCase(); |
| let deviceDetailsPage = |
| /** @type {?DeviceDetailsPage} */ ( |
| pageManager.registeredPages.get(deviceDetailsPageId)); |
| if (deviceDetailsPage) { |
| return deviceDetailsPage; |
| } |
| |
| const pageSection = document.createElement('section'); |
| pageSection.hidden = true; |
| pageSection.id = deviceDetailsPageId; |
| $('page-container').appendChild(pageSection); |
| |
| deviceDetailsPage = new DeviceDetailsPage(deviceDetailsPageId, deviceInfo); |
| |
| deviceDetailsPage.pageDiv.addEventListener('infochanged', function(event) { |
| devices.addOrUpdate(event.detail.info); |
| }); |
| |
| deviceDetailsPage.pageDiv.addEventListener('forgetpressed', function(event) { |
| pageManager.showPageByName(devicesPage.name); |
| removeDeviceDetailsPage(event.detail.address); |
| }); |
| |
| // Inform the devices page that the user is inspecting this device. |
| // This will update the links in the device table. |
| devicesPage.setInspecting(deviceInfo, true /* isInspecting */); |
| pageManager.register(deviceDetailsPage); |
| |
| sidebarObj.addItem({ |
| pageName: deviceDetailsPageId, |
| text: deviceInfo.nameForDisplay, |
| }); |
| |
| deviceDetailsPage.connect(); |
| return deviceDetailsPage; |
| } |
| |
| /** |
| * Updates the DeviceDetailsPage with the matching device |address| and |
| * redraws it. |
| * @param {string} address |
| */ |
| function updateDeviceDetailsPage(address) { |
| const detailPageId = 'devices/' + address.toLowerCase(); |
| const page = pageManager.registeredPages.get(detailPageId); |
| if (page) { |
| /** @type {!DeviceDetailsPage} */ (page).redraw(); |
| } |
| } |
| |
| function updateStoppedDiscoverySession() { |
| devicesPage.setScanStatus(ScanStatus.OFF); |
| discoverySession = null; |
| } |
| |
| function setupAdapterSystem(response) { |
| adapterBroker.addEventListener('adapterchanged', function(event) { |
| const oldValue = adapterPage.adapterFieldSet.value; |
| const newValue = Object.assign({}, oldValue); |
| newValue[event.detail.property] = event.detail.value; |
| adapterPage.setAdapterInfo(newValue); |
| |
| if (event.detail.property === AdapterProperty.POWERED) { |
| devicesPage.updatedScanButtonVisibility(event.detail.value); |
| } |
| |
| if (event.detail.property === AdapterProperty.DISCOVERING && |
| !event.detail.value && !userRequestedScanStop && discoverySession) { |
| updateStoppedDiscoverySession(); |
| showSnackbar( |
| 'Discovery session ended unexpectedly', SnackbarType.WARNING); |
| } |
| }); |
| |
| adapterPage.setAdapterInfo(response.info); |
| |
| adapterPage.pageDiv.addEventListener('refreshpressed', function() { |
| adapterBroker.getInfo().then(function(response) { |
| if (response && response.info) { |
| adapterPage.setAdapterInfo(response.info); |
| } else { |
| console.error('Failed to fetch adapter info.'); |
| } |
| }); |
| }); |
| |
| // <if expr="is_chromeos"> |
| adapterPage.pageDiv.addEventListener('restart-bluetooth-click', function() { |
| const restartBluetoothBtn = |
| document.querySelector('#restart-bluetooth-btn'); |
| restartBluetoothBtn.textContent = 'Restarting system Bluetooth..'; |
| BluetoothInternalsHandler.getRemote() |
| .restartSystemBluetooth() |
| .catch((e) => { |
| console.error('Failed to restart system Bluetooth'); |
| }) |
| .finally(() => { |
| restartBluetoothBtn.textContent = 'Restart system Bluetooth'; |
| restartBluetoothBtn.disabled = false; |
| }); |
| }); |
| // </if> |
| } |
| |
| function setupDeviceSystem(response) { |
| // Hook up device collection events. |
| adapterBroker.addEventListener('deviceadded', function(event) { |
| devices.addOrUpdate(event.detail.deviceInfo); |
| updateDeviceDetailsPage(event.detail.deviceInfo.address); |
| }); |
| adapterBroker.addEventListener('devicechanged', function(event) { |
| devices.addOrUpdate(event.detail.deviceInfo); |
| updateDeviceDetailsPage(event.detail.deviceInfo.address); |
| }); |
| adapterBroker.addEventListener('deviceremoved', function(event) { |
| devices.remove(event.detail.deviceInfo); |
| updateDeviceDetailsPage(event.detail.deviceInfo.address); |
| }); |
| |
| response.devices.forEach(devices.addOrUpdate, devices /* this */); |
| |
| devicesPage.setDevices(devices); |
| |
| devicesPage.pageDiv.addEventListener('inspectpressed', function(event) { |
| const detailsPage = makeDeviceDetailsPage( |
| devices.item(devices.getByAddress(event.detail.address))); |
| pageManager.showPageByName(detailsPage.name); |
| }); |
| |
| devicesPage.pageDiv.addEventListener('forgetpressed', function(event) { |
| pageManager.showPageByName(devicesPage.name); |
| removeDeviceDetailsPage(event.detail.address); |
| }); |
| |
| devicesPage.pageDiv.addEventListener('scanpressed', function(event) { |
| if (discoverySession) { |
| userRequestedScanStop = true; |
| devicesPage.setScanStatus(ScanStatus.STOPPING); |
| |
| discoverySession.stop().then(function(response) { |
| if (response.success) { |
| updateStoppedDiscoverySession(); |
| userRequestedScanStop = false; |
| return; |
| } |
| |
| devicesPage.setScanStatus(ScanStatus.ON); |
| showSnackbar('Failed to stop discovery session', SnackbarType.ERROR); |
| userRequestedScanStop = false; |
| }); |
| |
| return; |
| } |
| |
| devicesPage.setScanStatus(ScanStatus.STARTING); |
| adapterBroker.startDiscoverySession() |
| .then(function(session) { |
| assert(session); |
| discoverySession = session; |
| |
| discoverySession.onConnectionError.addListener(() => { |
| updateStoppedDiscoverySession(); |
| showSnackbar('Discovery session ended', SnackbarType.WARNING); |
| }); |
| |
| devicesPage.setScanStatus(ScanStatus.ON); |
| }) |
| .catch(function(error) { |
| devicesPage.setScanStatus(ScanStatus.OFF); |
| showSnackbar('Failed to start discovery session', SnackbarType.ERROR); |
| console.error(error); |
| }); |
| }); |
| } |
| |
| function setupPages(bluetoothInternalsHandler) { |
| sidebarObj = new Sidebar(/** @type {!HTMLElement} */ ($('sidebar'))); |
| $('menu-btn').addEventListener('click', function() { |
| sidebarObj.open(); |
| }); |
| pageManager.addObserver(sidebarObj); |
| pageManager.addObserver(new PageObserver()); |
| |
| devicesPage = new DevicesPage(); |
| pageManager.register(devicesPage); |
| adapterPage = new AdapterPage(); |
| pageManager.register(adapterPage); |
| debugLogPage = new DebugLogPage(bluetoothInternalsHandler); |
| pageManager.register(debugLogPage); |
| |
| // Set up hash-based navigation. |
| window.addEventListener('hashchange', function() { |
| // If a user navigates and the page doesn't exist, do nothing. |
| const pageName = window.location.hash.substr(1); |
| // Device page names are invalid selectors for querySelector(), as they |
| // contain "/" and ":". |
| // eslint-disable-next-line no-restricted-properties |
| if (document.getElementById(pageName)) { |
| pageManager.showPageByName(pageName); |
| } |
| }); |
| |
| if (!window.location.hash) { |
| pageManager.showPageByName(adapterPage.name); |
| return; |
| } |
| |
| // Only the root pages are available on page load. |
| pageManager.showPageByName(window.location.hash.split('/')[0].substr(1)); |
| } |
| |
| function showRefreshPageDialog() { |
| document.getElementById('refresh-page').showModal(); |
| } |
| |
| function showNeedLocationServicesOnDialog(bluetoothInternalsHandler) { |
| const dialog = document.getElementById('need-location-services-on'); |
| const servicesLink = |
| document.getElementById('need-location-services-on-services-link'); |
| servicesLink.onclick = () => { |
| dialog.close(); |
| showRefreshPageDialog(); |
| bluetoothInternalsHandler.requestLocationServices(); |
| }; |
| dialog.showModal(); |
| } |
| |
| function showNeedLocationPermissionAndServicesOnDialog( |
| bluetoothInternalsHandler) { |
| const dialog = |
| document.getElementById('need-location-permission-and-services-on'); |
| const servicesLink = document.getElementById( |
| 'need-location-permission-and-services-on-services-link'); |
| servicesLink.onclick = () => { |
| dialog.close(); |
| showRefreshPageDialog(); |
| bluetoothInternalsHandler.requestLocationServices(); |
| }; |
| const permissionLink = document.getElementById( |
| 'need-location-permission-and-services-on-permission-link'); |
| permissionLink.onclick = () => { |
| dialog.close(); |
| showRefreshPageDialog(); |
| bluetoothInternalsHandler.requestSystemPermissions(); |
| }; |
| dialog.showModal(); |
| } |
| |
| function showNeedNearbyDevicesPermissionDialog(bluetoothInternalsHandler) { |
| const dialog = document.getElementById('need-nearby-devices-permission'); |
| const permissionLink = |
| document.getElementById('need-nearby-devices-permission-permission-link'); |
| permissionLink.onclick = () => { |
| dialog.close(); |
| showRefreshPageDialog(); |
| bluetoothInternalsHandler.requestSystemPermissions(); |
| }; |
| dialog.showModal(); |
| } |
| |
| function showNeedLocationPermissionDialog(bluetoothInternalsHandler) { |
| const dialog = document.getElementById('need-location-permission'); |
| const permissionLink = |
| document.getElementById('need-location-permission-permission-link'); |
| permissionLink.onclick = () => { |
| dialog.close(); |
| showRefreshPageDialog(); |
| bluetoothInternalsHandler.requestSystemPermissions(); |
| }; |
| dialog.showModal(); |
| } |
| |
| function showCanNotRequestPermissionsDialog() { |
| document.getElementById('can-not-request-permissions').showModal(); |
| } |
| |
| export function initializeViews(bluetoothInternalsHandler) { |
| setupPages(bluetoothInternalsHandler); |
| return getAdapterBroker(bluetoothInternalsHandler) |
| .then(function(broker) { |
| adapterBroker = broker; |
| }) |
| .then(function() { |
| return adapterBroker.getInfo(); |
| }) |
| .then(setupAdapterSystem) |
| .then(function() { |
| return adapterBroker.getDevices(); |
| }) |
| .then(setupDeviceSystem) |
| .catch(function(error) { |
| showSnackbar(error.message, SnackbarType.ERROR); |
| console.error(error); |
| }); |
| } |
| |
| /** |
| * Check if the system has all the needed system permissions for using |
| * bluetooth. |
| * @param {BluetoothInternalsHandlerRemote} bluetoothInternalsHandler Mojo |
| * remote handler. |
| * @param {Function} successCallback The callback to be called when the system |
| * has the permissions for using bluetooth. |
| */ |
| export async function checkSystemPermissions( |
| bluetoothInternalsHandler, successCallback) { |
| const { |
| needLocationPermission, |
| needNearbyDevicesPermission, |
| needLocationServices, |
| canRequestPermissions, |
| } = await bluetoothInternalsHandler.checkSystemPermissions(); |
| const havePermission = |
| !needNearbyDevicesPermission && !needLocationPermission; |
| // In order to access Bluetooth, Android S+ requires us to have Nearby Devices |
| // permission, and older versions of Android require Location permission and |
| // Location Services to be turned on. Other platforms shouldn't have any of |
| // these fields set to true. |
| if (havePermission) { |
| if (needLocationServices) { |
| showNeedLocationServicesOnDialog(bluetoothInternalsHandler); |
| } else { |
| successCallback(bluetoothInternalsHandler); |
| } |
| } else if (canRequestPermissions) { |
| if (needLocationServices) { |
| // If Location Services are needed we can assume we are on an Android |
| // version lower S and so Location, rather than Nearby Devices permission, |
| // is also needed. |
| showNeedLocationPermissionAndServicesOnDialog(bluetoothInternalsHandler); |
| } else if (needNearbyDevicesPermission) { |
| showNeedNearbyDevicesPermissionDialog(bluetoothInternalsHandler); |
| } else { |
| showNeedLocationPermissionDialog(bluetoothInternalsHandler); |
| } |
| } else { |
| showCanNotRequestPermissionsDialog(bluetoothInternalsHandler); |
| } |
| } |