Commit e8e6d1fc authored by mbrunson's avatar mbrunson Committed by Commit bot

bluetooth: Add adapter page to internals page.

Adds BluetoothAdapter::Observer callbacks to Adapter implementation
for tracking changes in Adapter state including:
  AdapterDiscoverableChanged
  AdapterPoweredChanged
  AdapterPresentChanged

Adds adapter page to display details about the current state of the adapter.
Adds ObjectFieldSet interface component for displaying properties of a JavaScript object.

Screenshot: https://goo.gl/photos/dCbsULiydMbiAtiJ9

BUG=651282
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:closure_compilation

Review-Url: https://codereview.chromium.org/2567983007
Cr-Commit-Position: refs/heads/master@{#443065}
parent db4968cf
......@@ -101,12 +101,14 @@
<include name="IDR_BLUETOOTH_UUID_MOJO_JS" file="${root_gen_dir}\device\bluetooth\public\interfaces\uuid.mojom.js" use_base_dir="false" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_CSS" file="resources\bluetooth_internals\bluetooth_internals.css" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_ADAPTER_BROKER_JS" file="resources\bluetooth_internals\adapter_broker.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_ADAPTER_PAGE_JS" file="resources\bluetooth_internals\adapter_page.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_DEVICE_COLLECTION_JS" file="resources\bluetooth_internals\device_collection.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_DEVICE_TABLE_JS" file="resources\bluetooth_internals\device_table.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_DEVICES_PAGE_JS" file="resources\bluetooth_internals\devices_page.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_HTML" file="resources\bluetooth_internals\bluetooth_internals.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_INTERFACES_JS" file="resources\bluetooth_internals\interfaces.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_JS" file="resources\bluetooth_internals\bluetooth_internals.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_OBJECT_FIELDSET_JS" file="resources\bluetooth_internals\object_fieldset.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_SIDEBAR_JS" file="resources\bluetooth_internals\sidebar.js" type="BINDATA" compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_SNACKBAR_JS" file="resources\bluetooth_internals\snackbar.js" type="BINDATA" compress="gzip" />
<include name="IDR_BOOKMARKS_MANIFEST" file="resources\bookmark_manager\manifest.json" type="BINDATA" />
......
......@@ -19,7 +19,10 @@ cr.define('adapter_broker', function() {
* @enum {string}
*/
var AdapterProperty = {
DISCOVERABLE: 'discoverable',
DISCOVERING: 'discovering',
POWERED: 'powered',
PRESENT: 'present',
};
/**
......@@ -122,7 +125,49 @@ cr.define('adapter_broker', function() {
AdapterClient.prototype = {
/**
* Fires adapterchanged event.
* Fires adapterchanged event with "present" property.
* @param {boolean} present
*/
presentChanged: function(present) {
var event = new CustomEvent('adapterchanged', {
detail: {
property: AdapterProperty.PRESENT,
value: present,
}
});
this.adapterBroker_.dispatchEvent(event);
},
/**
* Fires adapterchanged event with "powered" property changed.
* @param {boolean} powered
*/
poweredChanged: function(powered) {
var event = new CustomEvent('adapterchanged', {
detail: {
property: AdapterProperty.POWERED,
value: powered,
}
});
this.adapterBroker_.dispatchEvent(event);
},
/**
* Fires adapterchanged event with "discoverable" property changed.
* @param {boolean} discoverable
*/
discoverableChanged: function(discoverable) {
var event = new CustomEvent('adapterchanged', {
detail: {
property: AdapterProperty.DISCOVERABLE,
value: discoverable,
}
});
this.adapterBroker_.dispatchEvent(event);
},
/**
* Fires adapterchanged event with "discovering" property changed.
* @param {boolean} discovering
*/
discoveringChanged: function(discovering) {
......
// Copyright 2017 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.
/**
* Javascript for AdapterPage, served from chrome://bluetooth-internals/.
*/
cr.define('adapter_page', function() {
/** @const */ var Page = cr.ui.pageManager.Page;
var PROPERTY_NAMES = {
address: 'Address',
name: 'Name',
initialized: 'Initialized',
present: 'Present',
powered: 'Powered',
discoverable: 'Discoverable',
discovering: 'Discovering',
};
/**
* Page that contains an ObjectFieldSet that displays the latest AdapterInfo.
* @constructor
* @extends {cr.ui.pageManager.Page}
*/
function AdapterPage() {
Page.call(this, 'adapter', 'Adapter', 'adapter');
this.adapterFieldSet = new object_fieldset.ObjectFieldSet();
this.adapterFieldSet.setPropertyDisplayNames(PROPERTY_NAMES);
this.pageDiv.appendChild(this.adapterFieldSet);
this.refreshBtn_ = $('adapter-refresh-btn');
this.refreshBtn_.addEventListener('click', function() {
this.refreshBtn_.disabled = true;
this.pageDiv.dispatchEvent(new CustomEvent('refreshpressed'));
}.bind(this));
}
AdapterPage.prototype = {
__proto__: Page.prototype,
/**
* Sets the information to display in fieldset.
* @param {!interfaces.BluetoothAdapter.AdapterInfo} info
*/
setAdapterInfo: function(info) {
this.adapterFieldSet.setObject(info);
this.refreshBtn_.disabled = false;
},
/**
* Redraws the fieldset displaying the adapter info.
*/
redraw: function() {
this.adapterFieldSet.redraw();
this.refreshBtn_.disabled = false;
},
};
return {
AdapterPage: AdapterPage,
};
});
......@@ -21,6 +21,18 @@ h1 {
color: rgb(92, 97, 102);
}
.toggle-status {
background-image: url(../../../../ui/webui/resources/images/cancel_red.svg);
background-repeat: no-repeat;
min-height: 24px;
min-width: 24px;
}
.toggle-status.checked {
background-image:
url(../../../../ui/webui/resources/images/check_circle_green.svg);
}
/* Page container */
#page-container {
-webkit-margin-start: var(--sidebar-width);
......@@ -297,4 +309,27 @@ table .removed {
opacity: 1;
transform: translate3d(0, 0, 0);
visibility: visible;
}
/* Adapter Page */
@media screen and (min-width: 601px) {
#adapter {
display: flex;
}
}
/* Object Fieldset */
.object-fieldset .status {
align-items: center;
display: flex;
margin-bottom: 0.8em;
}
.object-fieldset .status div:first-child {
-webkit-margin-end: 1em;
white-space: nowrap;
}
.object-fieldset .status:last-child {
margin-bottom: 0;
}
\ No newline at end of file
......@@ -22,6 +22,8 @@
<script src="snackbar.js"></script>
<script src="interfaces.js"></script>
<script src="adapter_broker.js"></script>
<script src="object_fieldset.js"></script>
<script src="adapter_page.js"></script>
<script src="device_collection.js"></script>
<script src="device_table.js"></script>
<script src="devices_page.js"></script>
......@@ -35,6 +37,11 @@
<button id="menu-btn" class="custom-appearance"></button>
<h1 class="page-title"></h1>
</header>
<section id="adapter" hidden>
<div class="header-extras">
<button id="adapter-refresh-btn">Refresh</button>
</div>
</section>
<section id="devices" hidden>
<div class="header-extras">
<button id="scan-btn">Start Scan</button>
......@@ -50,7 +57,10 @@
</header>
<nav>
<ul role="tablist">
<li class="selected" data-page-name="devices">
<li class="selected" data-page-name="adapter">
<button class="custom-appearance">Adapter</button>
</li>
<li data-page-name="devices">
<button class="custom-appearance">Devices</button>
</li>
</ul>
......
......@@ -13,6 +13,7 @@ var devices = null;
var sidebarObj = null;
cr.define('bluetooth_internals', function() {
/** @const */ var AdapterPage = adapter_page.AdapterPage;
/** @const */ var DevicesPage = devices_page.DevicesPage;
/** @const */ var PageManager = cr.ui.pageManager.PageManager;
/** @const */ var Snackbar = snackbar.Snackbar;
......@@ -47,6 +48,8 @@ cr.define('bluetooth_internals', function() {
/** @type {!device_collection.DeviceCollection} */
devices = new device_collection.DeviceCollection([]);
/** @type {adapter_page.AdapterPage} */
var adapterPage = null;
/** @type {devices_page.DevicesPage} */
var devicesPage = null;
......@@ -121,9 +124,11 @@ cr.define('bluetooth_internals', function() {
}
function setupAdapterSystem(response) {
console.log('adapter', response.info);
adapterBroker.addEventListener('adapterchanged', function(event) {
adapterPage.adapterFieldSet.value[event.detail.property] =
event.detail.value;
adapterPage.redraw();
if (event.detail.property == adapter_broker.AdapterProperty.DISCOVERING &&
!event.detail.value && !userRequestedScanStop && discoverySession) {
updateStoppedDiscoverySession();
......@@ -131,6 +136,14 @@ cr.define('bluetooth_internals', function() {
'Discovery session ended unexpectedly', SnackbarType.WARNING);
}
});
adapterPage.setAdapterInfo(response.info);
adapterPage.pageDiv.addEventListener('refreshpressed', function() {
adapterBroker.getInfo().then(function(response) {
adapterPage.setAdapterInfo(response.info);
});
});
}
function setupDeviceSystem(response) {
......@@ -197,6 +210,8 @@ cr.define('bluetooth_internals', function() {
devicesPage = new DevicesPage();
PageManager.register(devicesPage);
adapterPage = new AdapterPage();
PageManager.register(adapterPage);
// Set up hash-based navigation.
window.addEventListener('hashchange', function() {
......@@ -204,7 +219,7 @@ cr.define('bluetooth_internals', function() {
});
if (!window.location.hash) {
PageManager.showPageByName(devicesPage.name);
PageManager.showPageByName(adapterPage.name);
return;
}
......
// Copyright 2017 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.
/**
* Javascript for ObjectFieldSet, a UI element for displaying the properties
* of a given Javascript object. These properties are displayed in a fieldset
* as a series of rows for each key-value pair.
* Served from chrome://bluetooth-internals/.
*/
cr.define('object_fieldset', function() {
/**
* A fieldset that lists the properties of a given object. These properties
* are displayed as a series of rows for each key-value pair.
* Only the object's own properties are displayed. Boolean values are
* displayed using images: a green check for 'true', and a red cancel 'x' for
* 'false'. All other types are converted to their string representation for
* display.
* @constructor
* @extends {HTMLFieldSetElement}
*/
var ObjectFieldSet = cr.ui.define('fieldset');
ObjectFieldSet.prototype = {
__proto__: HTMLFieldSetElement.prototype,
/**
* Decorates the element as an ObjectFieldset.
*/
decorate: function() {
this.classList.add('object-fieldset');
/** @type {?Object} */
this.value = null;
/** @private {?Object<string, string>} */
this.nameMap_ = null;
},
/**
* Sets the object data to be displayed in the fieldset.
* @param {!Object} value
*/
setObject: function(value) {
this.value = value;
this.redraw();
},
/**
* Sets the object used to map property names to display names. If a display
* name is not provided, the default property name will be used.
* @param {!Object<string, string>} nameMap
*/
setPropertyDisplayNames: function(nameMap) {
this.nameMap_ = nameMap;
},
/**
* Deletes and recreates the table structure with current object data.
*/
redraw: function() {
this.innerHTML = '';
Object.keys(this.value).forEach(function(propName) {
var name = this.nameMap_[propName] || propName;
var value = this.value[propName];
var newField = document.createElement('div');
newField.classList.add('status');
var nameDiv = document.createElement('div');
nameDiv.textContent = name + ':';
newField.appendChild(nameDiv);
var valueDiv = document.createElement('div');
valueDiv.dataset.field = propName;
if (typeof(value) === 'boolean') {
valueDiv.classList.add('toggle-status');
valueDiv.classList.toggle('checked', value);
} else {
valueDiv.textContent = String(value);
}
newField.appendChild(valueDiv);
this.appendChild(newField);
}, this);
},
};
return {
ObjectFieldSet: ObjectFieldSet,
};
});
......@@ -18,6 +18,8 @@ BluetoothInternalsUI::BluetoothInternalsUI(content::WebUI* web_ui)
// Add required resources.
html_source->AddResourcePath("adapter_broker.js",
IDR_BLUETOOTH_INTERNALS_ADAPTER_BROKER_JS);
html_source->AddResourcePath("adapter_page.js",
IDR_BLUETOOTH_INTERNALS_ADAPTER_PAGE_JS);
html_source->AddResourcePath("bluetooth_internals.css",
IDR_BLUETOOTH_INTERNALS_CSS);
html_source->AddResourcePath("bluetooth_internals.js",
......@@ -30,6 +32,8 @@ BluetoothInternalsUI::BluetoothInternalsUI(content::WebUI* web_ui)
IDR_BLUETOOTH_INTERNALS_DEVICES_PAGE_JS);
html_source->AddResourcePath("interfaces.js",
IDR_BLUETOOTH_INTERNALS_INTERFACES_JS);
html_source->AddResourcePath("object_fieldset.js",
IDR_BLUETOOTH_INTERNALS_OBJECT_FIELDSET_JS);
html_source->AddResourcePath("sidebar.js",
IDR_BLUETOOTH_INTERNALS_SIDEBAR_JS);
html_source->AddResourcePath("snackbar.js",
......
......@@ -203,10 +203,14 @@ BluetoothInternalsTest.prototype = {
};
TEST_F('BluetoothInternalsTest', 'Startup_BluetoothInternals', function() {
/** @const */ var PageManager = cr.ui.pageManager.PageManager;
var adapterFactory = null;
var adapterFieldSet = null;
var deviceTable = null;
var sidebarNode = null;
var fakeAdapterInfo = this.fakeAdapterInfo;
var fakeDeviceInfo1 = this.fakeDeviceInfo1;
var fakeDeviceInfo2 = this.fakeDeviceInfo2;
var fakeDeviceInfo3 = this.fakeDeviceInfo3;
......@@ -231,6 +235,7 @@ TEST_F('BluetoothInternalsTest', 'Startup_BluetoothInternals', function() {
});
setup(function() {
adapterFieldSet = document.querySelector('#adapter fieldset');
deviceTable = document.querySelector('#devices table');
sidebarNode = document.querySelector('#sidebar');
devices.splice(0, devices.length);
......@@ -242,6 +247,7 @@ TEST_F('BluetoothInternalsTest', 'Startup_BluetoothInternals', function() {
adapterFactory.reset();
sidebarObj.close();
snackbar.Snackbar.dismiss(true);
PageManager.registeredPages['adapter'].setAdapterInfo(fakeAdapterInfo());
});
/**
......@@ -411,7 +417,7 @@ TEST_F('BluetoothInternalsTest', 'Startup_BluetoothInternals', function() {
var sidebarItems = Array.from(
sidebarNode.querySelectorAll('.sidebar-content li'));
['devices'].forEach(function(pageName) {
['adapter', 'devices'].forEach(function(pageName) {
expectTrue(sidebarItems.some(function(item) {
return item.dataset.pageName === pageName;
}));
......@@ -545,8 +551,56 @@ TEST_F('BluetoothInternalsTest', 'Startup_BluetoothInternals', function() {
expectFalse(!!snackbar.Snackbar.current_);
}).then(finishSnackbarTest);
});
});
/* AdapterPage Tests */
function checkAdapterFieldSet(adapterInfo) {
for (var propName in adapterInfo) {
var valueCell = adapterFieldSet.querySelector(
'[data-field="' + propName + '"]');
var value = adapterInfo[propName];
if (typeof(value) === 'boolean') {
expectEquals(value, valueCell.classList.contains('checked'));
} else if (typeof(value) === 'string') {
expectEquals(value, valueCell.textContent);
} else {
assert('boolean or string type expected but got ' + typeof(value));
}
}
}
test('AdapterPage_DefaultState', function() {
checkAdapterFieldSet(adapterFieldSet.value);
});
test('AdapterPage_AdapterChanged', function() {
var adapterInfo = adapterFieldSet.value;
adapterInfo.present = !adapterInfo.present;
adapterBroker.adapterClient_.presentChanged(adapterInfo.present);
checkAdapterFieldSet(adapterInfo);
adapterInfo.discovering = !adapterInfo.discovering;
adapterBroker.adapterClient_.discoveringChanged(adapterInfo.discovering);
checkAdapterFieldSet(adapterInfo);
});
test('AdapterPage_AdapterChanged_RepeatTwice', function() {
var adapterInfo = adapterFieldSet.value;
adapterInfo.present = !adapterInfo.present;
adapterBroker.adapterClient_.presentChanged(adapterInfo.present);
checkAdapterFieldSet(adapterInfo);
adapterBroker.adapterClient_.presentChanged(adapterInfo.present);
checkAdapterFieldSet(adapterInfo);
adapterInfo.discovering = !adapterInfo.discovering;
adapterBroker.adapterClient_.discoveringChanged(adapterInfo.discovering);
checkAdapterFieldSet(adapterInfo);
adapterBroker.adapterClient_.discoveringChanged(adapterInfo.discovering);
checkAdapterFieldSet(adapterInfo);
});
});
// Run all registered tests.
mocha.run();
......
......@@ -79,6 +79,24 @@ void Adapter::StartDiscoverySession(
weak_ptr_factory_.GetWeakPtr(), callback));
}
void Adapter::AdapterPresentChanged(device::BluetoothAdapter* adapter,
bool present) {
if (client_)
client_->PresentChanged(present);
}
void Adapter::AdapterPoweredChanged(device::BluetoothAdapter* adapter,
bool powered) {
if (client_)
client_->PoweredChanged(powered);
}
void Adapter::AdapterDiscoverableChanged(device::BluetoothAdapter* adapter,
bool discoverable) {
if (client_)
client_->DiscoverableChanged(discoverable);
}
void Adapter::AdapterDiscoveringChanged(device::BluetoothAdapter* adapter,
bool discovering) {
if (client_)
......
......@@ -37,6 +37,12 @@ class Adapter : public mojom::Adapter,
const StartDiscoverySessionCallback& callback) override;
// device::BluetoothAdapter::Observer overrides:
void AdapterPresentChanged(device::BluetoothAdapter* adapter,
bool present) override;
void AdapterPoweredChanged(device::BluetoothAdapter* adapter,
bool powered) override;
void AdapterDiscoverableChanged(device::BluetoothAdapter* adapter,
bool discoverable) override;
void AdapterDiscoveringChanged(device::BluetoothAdapter* adapter,
bool discovering) override;
void DeviceAdded(device::BluetoothAdapter* adapter,
......
......@@ -44,4 +44,4 @@ class DiscoverySession : public mojom::DiscoverySession {
} // namespace bluetooth
#endif
\ No newline at end of file
#endif
......@@ -82,6 +82,15 @@ interface Adapter {
};
interface AdapterClient {
// Called when the presence of the adapter changes.
PresentChanged(bool present);
// Called when the radio power state of the adapter changes.
PoweredChanged(bool powered);
// Called when the discoverability state of the adapter changes.
DiscoverableChanged(bool discoverable);
// Called when the discovering state of the adapter changes.
DiscoveringChanged(bool discovering);
......
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#DB4437">
<path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#0F9D58">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment