// Copyright 2017 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 DeviceDetailsPage which displays all of the details of a
* device. The page is generated and managed dynamically in bluetooth_internals.
* served from chrome://bluetooth-internals/.
*/
import './service_list.js';
import './object_fieldset.js';
import {$} from 'chrome://resources/js/util.js';
import {DeviceRemote} from './device.mojom-webui.js';
import {connectToDevice} from './device_broker.js';
import {ConnectionStatus} from './device_collection.js';
import {formatManufacturerDataMap, formatServiceUuids} from './device_utils.js';
import {ObjectFieldSetElement} from './object_fieldset.js';
import {Page} from './page.js';
import {showSnackbar, SnackbarType} from './snackbar.js';
/**
* Property names that will be displayed in the ObjectFieldSet which contains
* the DeviceInfo object.
*/
const PROPERTY_NAMES = {
name: 'Name',
address: 'Address',
isGattConnected: 'GATT Connected',
'rssi.value': 'Latest RSSI',
serviceUuids: 'Services',
manufacturerDataMap: 'Manufacturer Data',
};
/**
* Page that displays all of the details of a device. This page is generated
* and managed dynamically in bluetooth_internals. The page contains two
* sections: Status and Services. The Status section displays information from
* the DeviceInfo object and the Services section contains a ServiceList
* compononent that lists all of the active services on the device.
*/
export class DeviceDetailsPage extends Page {
/**
* @param {string} id
* @param {!DeviceInfo} deviceInfo
*/
constructor(id, deviceInfo) {
super(id, deviceInfo.nameForDisplay, id);
/** @type {!DeviceInfo} */
this.deviceInfo = deviceInfo;
/** @type {?Array<ServiceInfo>} */
this.services = null;
/** @private {?DeviceRemote} */
this.device_ = null;
/** @private {!ObjectFieldSetElement} */
this.deviceFieldSet_ = document.createElement('object-field-set');
this.deviceFieldSet_.toggleAttribute('show-all', true);
this.deviceFieldSet_.dataset.nameMap = JSON.stringify(PROPERTY_NAMES);
/** @private {!ServiceList} */
this.serviceList_ = document.createElement('service-list');
/** @private {!ConnectionStatus} */
this.status_ = ConnectionStatus.DISCONNECTED;
/** @private {?Element} */
this.connectBtn_ = null;
this.pageDiv.appendChild(document.importNode(
$('device-details-template').content, true /* deep */));
this.pageDiv.querySelector('.device-details')
.appendChild(this.deviceFieldSet_);
this.pageDiv.querySelector('.services').appendChild(this.serviceList_);
this.pageDiv.querySelector('.forget').addEventListener('click', function() {
this.disconnect();
this.pageDiv.dispatchEvent(new CustomEvent('forgetpressed', {
detail: {
address: this.deviceInfo.address,
},
}));
}.bind(this));
this.connectBtn_ = this.pageDiv.querySelector('.disconnect');
this.connectBtn_.addEventListener('click', function() {
this.device_ !== null ? this.disconnect() : this.connect();
}.bind(this));
this.redraw();
}
/** Creates a connection to the Bluetooth device. */
connect() {
if (this.status_ !== ConnectionStatus.DISCONNECTED) {
return;
}
this.updateConnectionStatus_(ConnectionStatus.CONNECTING);
connectToDevice(this.deviceInfo.address)
.then(function(device) {
this.device_ = device;
this.updateConnectionStatus_(ConnectionStatus.CONNECTED);
// Fetch services asynchronously.
return this.device_.getServices();
}.bind(this))
.then(function(response) {
this.services = response.services;
this.serviceList_.load(this.deviceInfo.address);
this.redraw();
this.fireDeviceInfoChanged_();
}.bind(this))
.catch(function(error) {
// If a connection error occurs while fetching the services, the
// DeviceRemote reference must be removed.
if (this.device_) {
this.device_.disconnect();
this.device_ = null;
}
showSnackbar(
this.deviceInfo.nameForDisplay + ': ' + error.message,
SnackbarType.ERROR, 'Retry', this.connect.bind(this));
this.updateConnectionStatus_(ConnectionStatus.DISCONNECTED);
}.bind(this));
}
/** Disconnects the page from the Bluetooth device. */
disconnect() {
if (!this.device_) {
return;
}
this.device_.disconnect();
this.device_ = null;
this.updateConnectionStatus_(ConnectionStatus.DISCONNECTED);
}
/** Redraws the contents of the page with the current |deviceInfo|. */
redraw() {
const isConnected = this.deviceInfo.isGattConnected;
// Update status if connection has changed.
if (isConnected) {
this.connect();
} else {
this.disconnect();
}
const connectedText = isConnected ? 'Connected' : 'Not Connected';
const rssi = this.deviceInfo.rssi || {};
let rssiValue = 'Unknown';
if (rssi.value != null && rssi.value <= 0) {
rssiValue = rssi.value;
}
const serviceUuidsText = formatServiceUuids(this.deviceInfo.serviceUuids);
const manufacturerDataMapText =
formatManufacturerDataMap(this.deviceInfo.manufacturerDataMap);
const deviceViewObj = {
name: this.deviceInfo.nameForDisplay,
address: this.deviceInfo.address,
isGattConnected: connectedText,
'rssi.value': rssiValue,
serviceUuids: serviceUuidsText,
manufacturerDataMap: manufacturerDataMapText,
};
this.deviceFieldSet_.dataset.value = JSON.stringify(deviceViewObj);
}
/**
* Sets the page's device info and forces a redraw.
* @param {!DeviceInfo} info
*/
setDeviceInfo(info) {
this.deviceInfo = info;
this.redraw();
}
/**
* Fires an 'infochanged' event with the current |deviceInfo|
* @private
*/
fireDeviceInfoChanged_() {
this.pageDiv.dispatchEvent(new CustomEvent('infochanged', {
bubbles: true,
detail: {
info: this.deviceInfo,
},
}));
}
/**
* Updates the current connection status. Caches the latest status, updates
* the connection button message, and fires a 'connectionchanged' event when
* finished.
* @param {!ConnectionStatus} status
* @private
*/
updateConnectionStatus_(status) {
if (this.status_ === status) {
return;
}
this.status_ = status;
if (status === ConnectionStatus.DISCONNECTED) {
this.connectBtn_.textContent = 'Connect';
this.connectBtn_.disabled = false;
} else if (status === ConnectionStatus.CONNECTING) {
this.connectBtn_.textContent = 'Connecting';
this.connectBtn_.disabled = true;
} else {
this.connectBtn_.textContent = 'Disconnect';
this.connectBtn_.disabled = false;
}
this.pageDiv.dispatchEvent(new CustomEvent('connectionchanged', {
bubbles: true,
detail: {
address: this.deviceInfo.address,
status: status,
},
}));
}
}