Commit 66f0edf4 authored by David Tseng's avatar David Tseng Committed by Commit Bot

BTBrl: adds UI in ChromeVox options for bluetooth braille

Bug: 882261
Change-Id: I8226d70905146da97e9d1e1846a4e6a8e2cd01be
Reviewed-on: https://chromium-review.googlesource.com/c/1336656
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarYuki Awano <yawano@chromium.org>
Cr-Commit-Position: refs/heads/master@{#612345}
parent 0b4b79d2
......@@ -4,9 +4,9 @@
import("//build/config/features.gni")
import("//chrome/common/features.gni")
import("//chrome/test/base/js2gtest.gni")
import("//components/nacl/features.gni")
import("//testing/test.gni")
import("//chrome/test/base/js2gtest.gni")
import("//third_party/closure_compiler/compile_js.gni")
import("run_jsbundler.gni")
......@@ -31,6 +31,7 @@ chromevox_modules = [
"braille/braille_table.js",
"braille/braille_translator_manager.js",
"braille/bluetooth_braille_display_manager.js",
"braille/bluetooth_braille_display_ui.js",
"braille/expanding_braille_translator.js",
"braille/liblouis.js",
"braille/nav_braille.js",
......@@ -559,6 +560,7 @@ js2gtest("chromevox_unitjs_tests") {
test_type = "webui"
sources = [
"braille/bluetooth_braille_display_manager_test.unitjs",
"braille/bluetooth_braille_display_ui_test.unitjs",
"braille/braille_display_manager_test.unitjs",
"braille/braille_input_handler_test.unitjs",
"braille/expanding_braille_translator_test.unitjs",
......
......@@ -24,8 +24,9 @@ BluetoothBrailleDisplayListener.prototype = {
/**
* Called when a pincode is requested and a response can be made by calling
* BluetoothBrailleDisplayManager.finishPairing.
* @param {!chrome.bluetooth.Device} display
*/
onPincodeRequested: function() {},
onPincodeRequested: function(display) {},
};
/**
......@@ -47,8 +48,22 @@ BluetoothBrailleDisplayManager = function() {
/** @private {!Array<BluetoothBrailleDisplayListener>} */
this.listeners_ = [];
/** @private {!Array<string>} */
this.displayNames_ = ['Focus 40 BT'];
/**
* This list of braille display names was taken from other services that
* utilize Brltty (e.g. BrailleBack).
* @private {!Array<string|RegExp>}
*/
this.displayNames_ = [
'"EL12-', 'Esys-', 'Focus 14 BT', 'Focus 40 BT', 'Brailliant BI',
/Hansone|HansoneXL|BrailleSense|BrailleEDGE|SmartBeetle/, 'Refreshabraille',
'Orbit', 'VarioConnect', 'VarioUltra', 'HWG Brailliant', 'braillex trio',
/Alva BC/i, 'TSM', 'TS5',
new RegExp(
'(Actilino.*|Active Star.*|Braille Wave( BRW)?|Braillino( BL2)?' +
'|Braille Star 40( BS4)?|Easy Braille( EBR)?|Active Braille( AB4)?' +
'|Basic Braille BB[3,4,6]?)\\/[a-zA-Z][0-9]-[0-9]{5}'),
new RegExp('(BRW|BL2|BS4|EBR|AB4|BB(3|4|6)?)\\/[a-zA-Z][0-9]-[0-9]{5}')
];
/**
* The display explicitly preferred by a caller via connect. Only one such
......@@ -87,6 +102,10 @@ BluetoothBrailleDisplayManager.prototype = {
*/
start: function() {
chrome.bluetooth.startDiscovery();
// Pick up any devices already in the system including previously paired,
// but out of range displays.
this.handleDevicesChanged();
},
/**
......@@ -168,7 +187,7 @@ BluetoothBrailleDisplayManager.prototype = {
chrome.bluetooth.getDevices((devices) => {
var displayList = devices.filter((device) => {
return this.displayNames_.some((name) => {
return device.name && device.name.indexOf(name) == 0;
return device.name && device.name.search(name) == 0;
});
});
if (displayList.length == 0)
......@@ -193,7 +212,8 @@ BluetoothBrailleDisplayManager.prototype = {
handlePairing: function(pairingEvent) {
if (pairingEvent.pairing ==
chrome.bluetoothPrivate.PairingEventType.REQUEST_PINCODE)
this.listeners_.forEach((listener) => listener.onPincodeRequested());
this.listeners_.forEach(
(listener) => listener.onPincodeRequested(pairingEvent.device));
},
/**
......
// Copyright 2018 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.
/**
* @fileoverview A widget that exposes UI for interacting with a list of braille
* displays.
*/
goog.provide('BluetoothBrailleDisplayUI');
goog.require('BluetoothBrailleDisplayListener');
goog.require('BluetoothBrailleDisplayManager');
goog.require('Msgs');
/**
* A widget used for interacting with bluetooth braille displays.
* @constructor
* @implements {BluetoothBrailleDisplayListener}
*/
BluetoothBrailleDisplayUI = function() {
/** @private {!BluetoothBrailleDisplayManager} */
this.manager_ = new BluetoothBrailleDisplayManager();
this.manager_.addListener(this);
/** @private {Element} */
this.root_;
/** @private {Element} */
this.displaySelect_;
/** @private {Element} */
this.controls_;
};
BluetoothBrailleDisplayUI.prototype = {
/**
* Attaches this widget to |element|.
* @param {!Element} element
*/
attach: function(element) {
this.manager_.start();
var container = document.createElement('div');
element.appendChild(container);
this.root_ = container;
var title = document.createElement('h2');
title.textContent = Msgs.getMsg('options_bluetooth_braille_display_title');
container.appendChild(title);
var controls = document.createElement('div');
container.appendChild(controls);
this.controls_ = controls;
controls.className = 'option';
var selectLabel = document.createElement('span');
controls.appendChild(selectLabel);
selectLabel.id = 'bluetoothBrailleSelectLabel';
selectLabel.textContent =
Msgs.getMsg('options_bluetooth_braille_display_select_label');
var displaySelect = document.createElement('select');
this.displaySelect_ = displaySelect;
controls.appendChild(displaySelect);
displaySelect.setAttribute(
'aria-labelledby', 'bluetoothBrailleSelectLabel');
displaySelect.addEventListener('change', (evt) => {
this.updateControls_();
});
var connectOrDisconnect = document.createElement('button');
controls.appendChild(connectOrDisconnect);
connectOrDisconnect.id = 'connectOrDisconnect';
connectOrDisconnect.disabled = true;
var forget = document.createElement('button');
controls.appendChild(forget);
forget.id = 'forget';
forget.textContent =
Msgs.getMsg('options_bluetooth_braille_display_forget');
forget.disabled = true;
},
/**
* Detaches the rendered widget.
*/
detach: function() {
this.manager_.stop();
if (this.root_) {
this.root_.remove();
this.root_ = null;
}
},
/** @override */
onDisplayListChanged: function(displays) {
if (!this.displaySelect_)
throw 'Expected attach to have been called.';
// Remove any displays that were removed.
for (var i = 0; i < this.displaySelect_.children.length; i++) {
var domDisplay = this.displaySelect_.children[i];
if (!displays.find((display) => domDisplay.id == display.address))
domDisplay.remove();
}
displays.forEach((display) => {
// Check if the element already exists.
var displayContainer =
this.displaySelect_.querySelector('#' + CSS.escape(display.address));
// If the display already exists, no further processing is needed.
if (displayContainer)
return;
displayContainer = document.createElement('option');
this.displaySelect_.appendChild(displayContainer);
displayContainer.id = display.address;
var name = document.createElement('span');
displayContainer.appendChild(name);
name.textContent = display.name;
});
this.updateControls_();
},
/** @override */
onPincodeRequested: function(display) {
this.controls_.hidden = true;
var form = document.createElement('form');
this.controls_.parentElement.insertBefore(form, this.controls_);
// Create the text field and its label.
var label = document.createElement('label');
form.appendChild(label);
label.id = 'pincodeLabel';
label.textContent =
Msgs.getMsg('options_bluetooth_braille_display_pincode_label');
label.for = 'pincode';
var pincodeField = document.createElement('input');
pincodeField.id = 'pincode';
pincodeField.type = 'text';
pincodeField.setAttribute('aria-labelledby', 'pincodeLabel');
form.appendChild(pincodeField);
var timeoutId;
form.addEventListener('submit', (evt) => {
if (timeoutId)
clearTimeout(timeoutId);
if (pincodeField.value)
this.manager_.finishPairing(display, pincodeField.value);
this.controls_.hidden = false;
form.remove();
form = null;
evt.preventDefault();
evt.stopPropagation();
this.displaySelect_.focus();
});
// Also, schedule a 60 second timeout for pincode entry.
timeoutId = setTimeout(() => {
form.remove();
this.controls_.hidden = false;
this.displaySelect_.focus();
}, 60000);
document.body.blur();
pincodeField.focus();
},
/**
* @private
*/
updateControls_: function() {
// Only update controls if there is a selected display.
var sel = this.displaySelect_.options[this.displaySelect_.selectedIndex];
if (!sel)
return;
chrome.bluetooth.getDevice(sel.id, (display) => {
var connectOrDisconnect =
this.controls_.querySelector('#connectOrDisconnect');
connectOrDisconnect.disabled = display.connecting;
this.displaySelect_.disabled = display.connecting;
connectOrDisconnect.textContent = Msgs.getMsg(
display.connecting ?
'options_bluetooth_braille_display_connecting' :
(display.connected ?
'options_bluetooth_braille_display_disconnect' :
'options_bluetooth_braille_display_connect'));
connectOrDisconnect.onclick = function(savedDisplay, evt) {
if (savedDisplay.connected)
this.manager_.disconnect(savedDisplay);
else
this.manager_.connect(savedDisplay);
}.bind(this, display);
var forget = this.controls_.querySelector('#forget');
forget.disabled = !display.paired || display.connecting;
forget.onclick = function(savedDisplay) {
this.manager_.forget(savedDisplay);
}.bind(this, display);
});
}
};
// Copyright 2018 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.
// Include test fixture.
GEN_INCLUDE(['../testing/chromevox_unittest_base.js']);
GEN_INCLUDE(['../testing/fake_objects.js']);
// Fake out the Chrome API namespace we depend on.
var chrome = {};
/** Fake chrome.brailleDisplayPrivate object. */
chrome.brailleDisplayPrivate = {};
/** Fake chrome.bluetooth object. */
chrome.bluetooth = {};
chrome.bluetooth.getDevices = (callback) => {};
chrome.bluetooth.onDeviceAdded = new FakeChromeEvent();
chrome.bluetooth.onDeviceChanged = new FakeChromeEvent();
chrome.bluetooth.onDeviceRemoved = new FakeChromeEvent();
chrome.bluetooth.startDiscovery = () => {};
chrome.bluetooth.stopDiscovery = () => {};
/** Fake chrome.bluetoothPrivate object. */
chrome.bluetoothPrivate = {};
chrome.bluetoothPrivate.onPairing = new FakeChromeEvent();
/** Fake chrome.accessibilityPrivate object. */
chrome.accessibilityPrivate = {};
/**
* Test fixture.
* @constructor
* @extends {ChromeVoxUnitTestBase}
*/
function ChromeVoxBluetoothBrailleDisplayUIUnitTest() {
ChromeVoxUnitTestBase.call(this);
}
ChromeVoxBluetoothBrailleDisplayUIUnitTest.prototype = {
__proto__ : ChromeVoxUnitTestBase.prototype,
/** @override */
closureModuleDeps : [
'BluetoothBrailleDisplayManager',
'BluetoothBrailleDisplayUI',
'TestMsgs',
],
/** @override */
isAsync : true,
/** @override */
setUp: function() {
Msgs = TestMsgs;
},
/** Label of the select. @type {string} */
selectLabel: 'Select a bluetooth braille display',
/**
* Builds an expected stringified version of the widget, inserting static
* expected content as needed.
* @param {string} controls The expected controls block.
* @return {string} The final expectation.
*/
buildUIExpectation: function(controls) {
return `
<div>
<h2>Bluetooth Braille Display</h2>
<div class="option">
<span id="bluetoothBrailleSelectLabel">${this.selectLabel}</span>
${controls}
</div>
</div>`;
}
};
SYNC_TEST_F(
'ChromeVoxBluetoothBrailleDisplayUIUnitTest',
'NoDisplays',
function() {
var ui = new BluetoothBrailleDisplayUI();
ui.attach(document.body);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel"></select>
<button id="connectOrDisconnect" disabled=""></button>
<button id="forget" disabled="">Forget</button>`),
document.body.children[0]);
});
SYNC_TEST_F(
'ChromeVoxBluetoothBrailleDisplayUIUnitTest',
'ControlStateUpdatesNotConnectedOrPaired',
function() {
var ui = new BluetoothBrailleDisplayUI();
ui.attach(document.body);
var displays = [];
// Fake out getDevice using |display| as the backing source which changes below.
chrome.bluetooth.getDevice = (address, callback) => {
var display = displays.find((display) => display.address == address );
assertNotNullNorUndefined(display);
callback(display);
};
// One display; it automatically gets selected.
// Not connected, not paired.
displays = [{
name: 'Focus 40 BT', address: 'abcd1234'
}];
ui.onDisplayListChanged(displays);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel">
<option id="abcd1234"><span>Focus 40 BT</span></option>
</select>
<button id="connectOrDisconnect">Connect</button>
<button id="forget" disabled="">Forget</button>`),
document.body.children[0]);
ui.detach();
});
SYNC_TEST_F(
'ChromeVoxBluetoothBrailleDisplayUIUnitTest',
'ControlStateUpdatesPairedNotConnected',
function() {
var ui = new BluetoothBrailleDisplayUI();
ui.attach(document.body);
var display = [];
// Fake out getDevice using |display| as the backing source which changes
// below.
chrome.bluetooth.getDevice = (address, callback) => {
var display = displays.find((display) => display.address == address );
assertNotNullNorUndefined(display);
callback(display);
};
// One display; paired, but not connected.
displays = [{
name: 'Focus 40 BT', address: 'abcd1234', paired: true
}];
ui.onDisplayListChanged(displays);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel">
<option id="abcd1234"><span>Focus 40 BT</span></option>
</select>
<button id="connectOrDisconnect">Connect</button>
<button id="forget">Forget</button>`),
document.body.children[0]);
// Added one display; not paired, not connected.
displays = [
{name: 'Focus 40 BT', address: 'abcd1234', paired: true},
{name: 'Focus 40 BT rev 2', address: '4321dcba'}
];
ui.onDisplayListChanged(displays);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel">
<option id="abcd1234"><span>Focus 40 BT</span></option>
<option id="4321dcba"><span>Focus 40 BT rev 2</span></option>
</select>
<button id="connectOrDisconnect">Connect</button>
<button id="forget">Forget</button>`),
document.body.children[0]);
// Our selected display is connecting.
displays[0].connecting = true;
ui.onDisplayListChanged(displays);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel" disabled="">
<option id="abcd1234"><span>Focus 40 BT</span></option>
<option id="4321dcba"><span>Focus 40 BT rev 2</span></option>
</select>
<button id="connectOrDisconnect" disabled="">Connecting</button>
<button id="forget" disabled="">Forget</button>`),
document.body.children[0]);
// Our selected display connected.
displays[0].connecting = false;
displays[0].connected = true;
ui.onDisplayListChanged(displays);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel">
<option id="abcd1234"><span>Focus 40 BT</span></option>
<option id="4321dcba"><span>Focus 40 BT rev 2</span></option>
</select>
<button id="connectOrDisconnect">Disconnect</button>
<button id="forget">Forget</button>`),
document.body.children[0]);
// The user picks the second display.
// The manager has to ask for the device details.
var select = document.body.querySelector('select');
select.selectedIndex = 1;
var changeEvt = document.createEvent('HTMLEvents');
changeEvt.initEvent('change');
select.dispatchEvent(changeEvt);
// The controls update based on the newly selected display.
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel">
<option id="abcd1234"><span>Focus 40 BT</span></option>
<option id="4321dcba"><span>Focus 40 BT rev 2</span></option>
</select>
<button id="connectOrDisconnect">Connect</button>
<button id="forget" disabled="">Forget</button>`),
document.body.children[0]);
});
SYNC_TEST_F(
'ChromeVoxBluetoothBrailleDisplayUIUnitTest',
'PincodeRequest',
function() {
var ui = new BluetoothBrailleDisplayUI();
ui.attach(document.body);
// Trigger pincode screen.
ui.onPincodeRequested();
assertEqualsDOM(`
<div>
<h2>Bluetooth Braille Display</h2>
<form>
<label id="pincodeLabel">Please enter a pin</label>
<input id="pincode" type="text" aria-labelledby="pincodeLabel">
</form>
<div class="option" hidden="">
<span id="bluetoothBrailleSelectLabel">${this.selectLabel}</span>
<select aria-labelledby="bluetoothBrailleSelectLabel"></select>
<button id="connectOrDisconnect" disabled=""></button>
<button id="forget" disabled="">Forget</button>
</div>
</div>`,
document.body.children[0]);
ui.detach();
});
TEST_F(
'ChromeVoxBluetoothBrailleDisplayUIUnitTest',
'ClickControls',
function() {
var ui = new BluetoothBrailleDisplayUI();
ui.attach(document.body);
var displays = [];
// Fake out getDevice using |display| as the backing source which changes
// below.
chrome.bluetooth.getDevice = (address, callback) => {
var display = displays.find((display) => display.address == address );
assertNotNullNorUndefined(display);
callback(display);
};
// One display; paired, but not connected.
displays = [{
name: 'VarioUltra', address: 'abcd1234', paired: true
}];
ui.onDisplayListChanged(displays);
assertEqualsDOM(this.buildUIExpectation(`
<select aria-labelledby="bluetoothBrailleSelectLabel">
<option id="abcd1234"><span>VarioUltra</span></option>
</select>
<button id="connectOrDisconnect">Connect</button>
<button id="forget">Forget</button>`),
document.body.children[0]);
// Click the connect button. Only connect should be called.
chrome.bluetoothPrivate.connect = this.newCallback();
chrome.bluetoothPrivate.disconnectAll = assertNotReached;
document.getElementById('connectOrDisconnect').onclick();
// Now, update the state to be connected.
displays[0].connected = true;
ui.onDisplayListChanged(displays);
// Click the disconnect button.
chrome.bluetoothPrivate.connect = assertNotReached;
chrome.bluetoothPrivate.disconnectAll = this.newCallback();
chrome.brailleDisplayPrivate.updateBluetoothBrailleDisplayAddress =
this.newCallback();
document.getElementById('connectOrDisconnect').onclick();
// Click the forget button.
chrome.bluetoothPrivate.forgetDevice = this.newCallback();
chrome.brailleDisplayPrivate.updateBluetoothBrailleDisplayAddress =
this.newCallback();
document.getElementById('forget').onclick();
});
......@@ -104,6 +104,8 @@
</label>
</div>
<div id="bluetoothBraille"></div>
<h2 class="i18n" msgid="options_virtual_braille_display">
Virtual Braille Display
</h2>
......
......@@ -9,7 +9,7 @@
goog.provide('cvox.OptionsPage');
goog.require('BluetoothBrailleDisplayManager');
goog.require('BluetoothBrailleDisplayUI');
goog.require('ConsoleTts');
goog.require('EventStreamLogger');
goog.require('Msgs');
......@@ -47,6 +47,7 @@ cvox.OptionsPage.consoleTts;
* Initialize the options page by setting the current value of all prefs, and
* adding event listeners.
* @suppress {missingProperties} Property prefs never defined on Window
* @this {cvox.OptionsPage}
*/
cvox.OptionsPage.init = function() {
cvox.OptionsPage.prefs = chrome.extension.getBackgroundPage().prefs;
......@@ -184,6 +185,13 @@ cvox.OptionsPage.init = function() {
'virtual_braille_display_rows_input', 'virtualBrailleRows');
handleNumericalInputPref(
'virtual_braille_display_columns_input', 'virtualBrailleColumns');
/** @type {!BluetoothBrailleDisplayUI} */
cvox.OptionsPage.bluetoothBrailleDisplayUI = new BluetoothBrailleDisplayUI();
var bluetoothBraille = $('bluetoothBraille');
if (bluetoothBraille)
cvox.OptionsPage.bluetoothBrailleDisplayUI.attach(bluetoothBraille);
};
/**
......@@ -457,3 +465,7 @@ cvox.OptionsPage.getBrailleTranslatorManager = function() {
document.addEventListener('DOMContentLoaded', function() {
cvox.OptionsPage.init();
}, false);
window.addEventListener('beforeunload', function(e) {
cvox.OptionsPage.bluetoothBrailleDisplayUI.detach();
});
......@@ -3126,6 +3126,27 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button
<message desc="Part of the ChromeVox touch tutorial page. Concludes this page." name="IDS_CHROMEVOX_TUTORIAL_TOUCH_LEARN_MORE">
Explore more gestures in Learn Mode and the Chromebook Help Center
</message>
<message desc="Title of the bluetooth braille display section in ChromeVox options." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_TITLE">
Bluetooth Braille Display
</message>
<message desc="Labels a button which when pressed, connects to a selected braille display." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_CONNECT">
Connect
</message>
<message desc="Labels a button which when pressed, disconnects from a selected braille display." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_DISCONNECT">
Disconnect
</message>
<message desc="Labels a button which is disabled and indicates the system is connecting to a braille display." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_CONNECTING">
Connecting
</message>
<message desc="Labels a button which when pressed, forgets the selected braille display." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_FORGET">
Forget
</message>
<message desc="Labels a text field which prompts the user for a pincode when pairing a braille display." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_PINCODE_LABEL">
Please enter a pin
</message>
<message desc="Labels a select control which lists all bluetooth braille displays." name="IDS_CHROMEVOX_OPTIONS_BLUETOOTH_BRAILLE_DISPLAY_SELECT_LABEL">
Select a bluetooth braille display
</message>
</messages>
</release>
</grit>
......@@ -110,5 +110,22 @@ function assertNotReached() {
assertFalse(true);
}
/**
* Asserts an actual DOM equals an expected stringified DOM.
* @param {string} expected
* @param {Node} actual
*/
function assertEqualsDOM(expected, actual) {
expected = expected.replace(/>\s+</gm, '><').trim(/\s/gm);
var actualStr = actual.outerHTML;
actualStr = actualStr.replace(/>\s+</gm, '><').trim(/\s/gm);
for (var i = 0; i < expected.length; i++)
assertEquals(
expected[i], actualStr[i],
'Mismatch at index ' + i + ' in expected:\n' + expected +
'\nactual:\n' + actualStr + '\n');
}
assertSame = assertEquals;
assertNotSame = assertNotEquals;
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