Bug 876475 - Make braille output less verbose. r=eeejay r=kats r=ted

--HG--
rename : accessible/src/jsat/UtteranceGenerator.jsm => accessible/src/jsat/OutputGenerator.jsm
rename : accessible/tests/mochitest/jsat/utterance.js => accessible/tests/mochitest/jsat/output.js
This commit is contained in:
Max Li 2013-06-17 10:36:41 -04:00
parent 88f5678f8a
commit 978da0f3ad
17 changed files with 1079 additions and 199 deletions

View File

@ -468,6 +468,10 @@ var Output = {
Utils.win.navigator.vibrate(aDetails.pattern);
},
Braille: function Braille(aDetails, aBrowser) {
Logger.debug('Braille output: ' + aDetails.text);
},
_adjustBounds: function(aJsonBounds, aBrowser) {
let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
aJsonBounds.right - aJsonBounds.left,

View File

@ -16,11 +16,11 @@ ACCESSFU_FILES := \
EventManager.jsm \
jar.mn \
Makefile.in \
OutputGenerator.jsm \
Presentation.jsm \
TouchAdapter.jsm \
TraversalRules.jsm \
Utils.jsm \
UtteranceGenerator.jsm \
$(NULL)
ACCESSFU_DEST = $(FINAL_TARGET)/modules/accessibility

View File

@ -14,9 +14,14 @@ const INCLUDE_NAME = 0x02;
const INCLUDE_CUSTOM = 0x04;
const NAME_FROM_SUBTREE_RULE = 0x08;
const UTTERANCE_DESC_FIRST = 0;
const OUTPUT_DESC_FIRST = 0;
const OUTPUT_DESC_LAST = 1;
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
'resource://gre/modules/accessibility/Utils.jsm');
let gUtteranceOrder = new PrefCache('accessibility.accessfu.utterance');
@ -24,44 +29,12 @@ var gStringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
getService(Ci.nsIStringBundleService).
createBundle('chrome://global/locale/AccessFu.properties');
this.EXPORTED_SYMBOLS = ['UtteranceGenerator'];
this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator'];
/**
* Generates speech utterances from objects, actions and state changes.
* An utterance is an array of strings.
*
* It should not be assumed that flattening an utterance array would create a
* gramatically correct sentence. For example, {@link genForObject} might
* return: ['graphic', 'Welcome to my home page'].
* Each string element in an utterance should be gramatically correct in itself.
* Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
*
* An utterance is ordered from the least to the most important. Speaking the
* last string usually makes sense, but speaking the first often won't.
* For example {@link genForAction} might return ['button', 'clicked'] for a
* clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
* not.
*/
this.UtteranceGenerator = {
gActionMap: {
jump: 'jumpAction',
press: 'pressAction',
check: 'checkAction',
uncheck: 'uncheckAction',
select: 'selectAction',
open: 'openAction',
close: 'closeAction',
switch: 'switchAction',
click: 'clickAction',
collapse: 'collapseAction',
expand: 'expandAction',
activate: 'activateAction',
cycle: 'cycleAction'
},
this.OutputGenerator = {
/**
* Generates an utterance for a PivotContext.
* Generates output for a PivotContext.
* @param {PivotContext} aContext object that generates and caches
* context information for a given accessible and its relationship with
* another accessible.
@ -70,43 +43,44 @@ this.UtteranceGenerator = {
* starting from the accessible's ancestry or accessible's subtree.
*/
genForContext: function genForContext(aContext) {
let utterance = [];
let addUtterance = function addUtterance(aAccessible) {
utterance.push.apply(utterance,
UtteranceGenerator.genForObject(aAccessible));
let output = [];
let self = this;
let addOutput = function addOutput(aAccessible) {
output.push.apply(output, self.genForObject(aAccessible));
};
let ignoreSubtree = function ignoreSubtree(aAccessible) {
let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
let nameRule = UtteranceGenerator.roleRuleMap[roleString] || 0;
let nameRule = self.roleRuleMap[roleString] || 0;
// Ignore subtree if the name is explicit and the role's name rule is the
// NAME_FROM_SUBTREE_RULE.
return (nameRule & NAME_FROM_SUBTREE_RULE) &&
(Utils.getAttributes(aAccessible)['explicit-name'] === 'true');
};
let utteranceOrder = gUtteranceOrder.value || UTTERANCE_DESC_FIRST;
let outputOrder = typeof gUtteranceOrder.value == 'number' ?
gUtteranceOrder.value : this.defaultOutputOrder;
let contextStart = this._getContextStart(aContext);
if (utteranceOrder === UTTERANCE_DESC_FIRST) {
aContext.newAncestry.forEach(addUtterance);
addUtterance(aContext.accessible);
[addUtterance(node) for
if (outputOrder === OUTPUT_DESC_FIRST) {
contextStart.forEach(addOutput);
addOutput(aContext.accessible);
[addOutput(node) for
(node of aContext.subtreeGenerator(true, ignoreSubtree))];
} else {
[addUtterance(node) for
[addOutput(node) for
(node of aContext.subtreeGenerator(false, ignoreSubtree))];
addUtterance(aContext.accessible);
aContext.newAncestry.reverse().forEach(addUtterance);
addOutput(aContext.accessible);
contextStart.reverse().forEach(addOutput);
}
// Clean up the white space.
let trimmed;
utterance = [trimmed for (word of utterance) if (trimmed = word.trim())];
return utterance;
output = [trimmed for (word of output) if (trimmed = word.trim())];
return output;
},
/**
* Generates an utterance for an object.
* Generates output for an object.
* @param {nsIAccessible} aAccessible accessible object to generate utterance
* for.
* @return {Array} Two string array. The first string describes the object
@ -116,9 +90,8 @@ this.UtteranceGenerator = {
*/
genForObject: function genForObject(aAccessible) {
let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
let func = this.objectUtteranceFunctions[roleString] ||
this.objectUtteranceFunctions.defaultFunc;
let func = this.objectOutputFunctions[roleString.replace(' ', '')] ||
this.objectOutputFunctions.defaultFunc;
let flags = this.roleRuleMap[roleString] || 0;
@ -134,68 +107,61 @@ this.UtteranceGenerator = {
},
/**
* Generates an utterance for an action performed.
* TODO: May become more verbose in the future.
* Generates output for an action performed.
* @param {nsIAccessible} aAccessible accessible object that the action was
* invoked in.
* @param {string} aActionName the name of the action, one of the keys in
* {@link gActionMap}.
* @return {Array} A one string array with the action.
*/
genForAction: function genForAction(aObject, aActionName) {
return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
},
genForAction: function genForAction(aObject, aActionName) {},
/**
* Generates an utterance for an announcement. Basically attempts to localize
* Generates output for an announcement. Basically attempts to localize
* the announcement string.
* @param {string} aAnnouncement unlocalized announcement.
* @return {Array} A one string array with the announcement.
*/
genForAnnouncement: function genForAnnouncement(aAnnouncement) {
try {
return [gStringBundle.GetStringFromName(aAnnouncement)];
} catch (x) {
return [aAnnouncement];
}
},
genForAnnouncement: function genForAnnouncement(aAnnouncement) {},
/**
* Generates an utterance for a tab state change.
* Generates output for a tab state change.
* @param {nsIAccessible} aAccessible accessible object of the tab's attached
* document.
* @param {string} aTabState the tab state name, see
* {@link Presenter.tabStateChanged}.
* @return {Array} The tab state utterace.
*/
genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
switch (aTabState) {
case 'newtab':
return [gStringBundle.GetStringFromName('tabNew')];
case 'loading':
return [gStringBundle.GetStringFromName('tabLoading')];
case 'loaded':
return [aObject.name || '',
gStringBundle.GetStringFromName('tabLoaded')];
case 'loadstopped':
return [gStringBundle.GetStringFromName('tabLoadStopped')];
case 'reload':
return [gStringBundle.GetStringFromName('tabReload')];
default:
return [];
}
},
genForTabStateChange: function genForTabStateChange(aObject, aTabState) {},
/**
* Generates an utterance for announcing entering and leaving editing mode.
* Generates output for announcing entering and leaving editing mode.
* @param {aIsEditing} boolean true if we are in editing mode
* @return {Array} The mode utterance
*/
genForEditingMode: function genForEditingMode(aIsEditing) {
return [gStringBundle.GetStringFromName(
aIsEditing ? 'editingMode' : 'navigationMode')];
genForEditingMode: function genForEditingMode(aIsEditing) {},
_getContextStart: function getContextStart(aContext) {},
_addName: function _addName(aOutput, aAccessible, aFlags) {
let name;
if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
(aFlags & INCLUDE_NAME)) {
name = aAccessible.name;
}
if (name) {
let outputOrder = typeof gUtteranceOrder.value == 'number' ?
gUtteranceOrder.value : this.defaultOutputOrder;
aOutput[outputOrder === OUTPUT_DESC_FIRST ?
'push' : 'unshift'](name);
}
},
_getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
_getLocalizedStates: function _getLocalizedStates(aStates) {},
roleRuleMap: {
'menubar': INCLUDE_DESC,
'scrollbar': INCLUDE_DESC,
@ -268,35 +234,119 @@ this.UtteranceGenerator = {
'listbox': INCLUDE_DESC,
'definitionlist': INCLUDE_DESC | INCLUDE_NAME},
objectUtteranceFunctions: {
defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
let utterance = [];
objectOutputFunctions: {
_generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aStates, aFlags) {
let output = [];
if (aFlags & INCLUDE_DESC) {
let desc = this._getLocalizedStates(aStates);
let roleStr = this._getLocalizedRole(aRoleStr);
if (roleStr)
desc.push(roleStr);
utterance.push(desc.join(' '));
output.push(desc.join(' '));
}
this._addName(utterance, aAccessible, aFlags);
this._addName(output, aAccessible, aFlags);
return utterance;
return output;
},
entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
let utterance = [];
let output = [];
let desc = this._getLocalizedStates(aStates);
desc.push(this._getLocalizedRole(
(aStates.ext & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) ?
'textarea' : 'entry'));
utterance.push(desc.join(' '));
output.push(desc.join(' '));
this._addName(utterance, aAccessible, aFlags);
this._addName(output, aAccessible, aFlags);
return utterance;
return output;
}
}
};
/**
* Generates speech utterances from objects, actions and state changes.
* An utterance is an array of strings.
*
* It should not be assumed that flattening an utterance array would create a
* gramatically correct sentence. For example, {@link genForObject} might
* return: ['graphic', 'Welcome to my home page'].
* Each string element in an utterance should be gramatically correct in itself.
* Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
*
* An utterance is ordered from the least to the most important. Speaking the
* last string usually makes sense, but speaking the first often won't.
* For example {@link genForAction} might return ['button', 'clicked'] for a
* clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
* not.
*/
this.UtteranceGenerator = {
__proto__: OutputGenerator,
defaultOutputOrder: OUTPUT_DESC_FIRST,
gActionMap: {
jump: 'jumpAction',
press: 'pressAction',
check: 'checkAction',
uncheck: 'uncheckAction',
select: 'selectAction',
open: 'openAction',
close: 'closeAction',
switch: 'switchAction',
click: 'clickAction',
collapse: 'collapseAction',
expand: 'expandAction',
activate: 'activateAction',
cycle: 'cycleAction'
},
//TODO: May become more verbose in the future.
genForAction: function genForAction(aObject, aActionName) {
return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
},
genForAnnouncement: function genForAnnouncement(aAnnouncement) {
try {
return [gStringBundle.GetStringFromName(aAnnouncement)];
} catch (x) {
return [aAnnouncement];
}
},
genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
switch (aTabState) {
case 'newtab':
return [gStringBundle.GetStringFromName('tabNew')];
case 'loading':
return [gStringBundle.GetStringFromName('tabLoading')];
case 'loaded':
return [aObject.name || '',
gStringBundle.GetStringFromName('tabLoaded')];
case 'loadstopped':
return [gStringBundle.GetStringFromName('tabLoadStopped')];
case 'reload':
return [gStringBundle.GetStringFromName('tabReload')];
default:
return [];
}
},
genForEditingMode: function genForEditingMode(aIsEditing) {
return [gStringBundle.GetStringFromName(
aIsEditing ? 'editingMode' : 'navigationMode')];
},
objectOutputFunctions: {
defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
return OutputGenerator.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
},
entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
return OutputGenerator.objectOutputFunctions.entry.apply(this, arguments);
},
heading: function heading(aAccessible, aRoleStr, aStates, aFlags) {
@ -338,25 +388,15 @@ this.UtteranceGenerator = {
application: function application(aAccessible, aRoleStr, aStates, aFlags) {
// Don't utter location of applications, it gets tiring.
if (aAccessible.name != aAccessible.DOMNode.location)
return this.objectUtteranceFunctions.defaultFunc.apply(this,
return this.objectOutputFunctions.defaultFunc.apply(this,
[aAccessible, aRoleStr, aStates, aFlags]);
return [];
}
},
_addName: function _addName(utterance, aAccessible, aFlags) {
let name;
if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
(aFlags & INCLUDE_NAME)) {
name = aAccessible.name;
}
if (name) {
let utteranceOrder = gUtteranceOrder.value || UTTERANCE_DESC_FIRST;
utterance[utteranceOrder === UTTERANCE_DESC_FIRST ?
'push' : 'unshift'](name);
}
_getContextStart: function _getContextStart(aContext) {
return aContext.newAncestry;
},
_getLocalizedRole: function _getLocalizedRole(aRoleStr) {
@ -419,3 +459,118 @@ this.UtteranceGenerator = {
return utterance;
}
};
this.BrailleGenerator = {
__proto__: OutputGenerator,
defaultOutputOrder: OUTPUT_DESC_LAST,
objectOutputFunctions: {
defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
let braille = OutputGenerator.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
if (aAccessible.indexInParent === 1 &&
aAccessible.parent.role == Ci.nsIAccessibleRole.ROLE_LISTITEM &&
aAccessible.previousSibling.role == Ci.nsIAccessibleRole.ROLE_STATICTEXT) {
if (aAccessible.parent.parent && aAccessible.parent.parent.DOMNode &&
aAccessible.parent.parent.DOMNode.nodeName == 'UL') {
braille.unshift('*');
} else {
braille.unshift(aAccessible.previousSibling.name);
}
}
return braille;
},
listitem: function listitem(aAccessible, aRoleStr, aStates, aFlags) {
let braille = [];
this._addName(braille, aAccessible, aFlags);
return braille;
},
statictext: function statictext(aAccessible, aRoleStr, aStates, aFlags) {
// Since we customize the list bullet's output, we add the static
// text from the first node in each listitem, so skip it here.
if (aAccessible.parent.role == Ci.nsIAccessibleRole.ROLE_LISTITEM) {
return [];
}
return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
},
_useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aStates, aFlags) {
let braille = [];
let desc = this._getLocalizedStates(aStates);
braille.push(desc.join(' '));
this._addName(braille, aAccessible, aFlags);
return braille;
},
checkbutton: function checkbutton(aAccessible, aRoleStr, aStates, aFlags) {
return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
},
radiobutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
},
togglebutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
},
entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
return OutputGenerator.objectOutputFunctions.entry.apply(this, arguments);
}
},
_getContextStart: function _getContextStart(aContext) {
if (aContext.accessible.parent.role == Ci.nsIAccessibleRole.ROLE_LINK) {
return [aContext.accessible.parent];
}
return [];
},
_getLocalizedRole: function _getLocalizedRole(aRoleStr) {
try {
return gStringBundle.GetStringFromName(aRoleStr.replace(' ', '') + 'Abbr');
} catch (x) {
try {
return gStringBundle.GetStringFromName(aRoleStr.replace(' ', ''));
} catch (y) {
return '';
}
}
},
_getLocalizedStates: function _getLocalizedStates(aStates) {
let stateBraille = [];
let getCheckedState = function getCheckedState() {
let resultMarker = [];
let state = aStates.base;
let fill = !!(state & Ci.nsIAccessibleStates.STATE_CHECKED) ||
!!(state & Ci.nsIAccessibleStates.STATE_PRESSED);
resultMarker.push('(');
resultMarker.push(fill ? 'x' : ' ');
resultMarker.push(')');
return resultMarker.join('');
};
if (aStates.base & Ci.nsIAccessibleStates.STATE_CHECKABLE) {
stateBraille.push(getCheckedState());
}
return stateBraille;
}
};

View File

@ -9,8 +9,17 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import('resource://gre/modules/accessibility/UtteranceGenerator.jsm');
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator',
'resource://gre/modules/accessibility/OutputGenerator.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator',
'resource://gre/modules/accessibility/OutputGenerator.jsm');
this.EXPORTED_SYMBOLS = ['Presentation'];
@ -219,6 +228,15 @@ AndroidPresenter.prototype = {
let state = Utils.getStates(aContext.accessible)[0];
let brailleText = '';
if (Utils.AndroidSdkVersion >= 16) {
if (!this._braillePresenter) {
this._braillePresenter = new BraillePresenter();
}
brailleText = this._braillePresenter.pivotChanged(aContext, aReason).
details.text;
}
androidEvents.push({eventType: (isExploreByTouch) ?
this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
text: UtteranceGenerator.genForContext(aContext),
@ -227,7 +245,8 @@ AndroidPresenter.prototype = {
checkable: !!(state &
Ci.nsIAccessibleStates.STATE_CHECKABLE),
checked: !!(state &
Ci.nsIAccessibleStates.STATE_CHECKED)});
Ci.nsIAccessibleStates.STATE_CHECKED),
brailleText: brailleText});
return {
@ -367,6 +386,29 @@ HapticPresenter.prototype = {
}
};
/**
* A braille presenter
*/
this.BraillePresenter = function BraillePresenter() {};
BraillePresenter.prototype = {
__proto__: Presenter.prototype,
type: 'Braille',
pivotChanged: function BraillePresenter_pivotChanged(aContext, aReason) {
if (!aContext.accessible) {
return null;
}
let text = BrailleGenerator.genForContext(aContext);
return { type: this.type, details: {text: text.join(' ')} };
}
};
this.Presentation = {
get presenters() {
delete this.presenters;

View File

@ -13,10 +13,11 @@ include $(DEPTH)/config/autoconf.mk
MOCHITEST_A11Y_FILES =\
jsatcommon.js \
utterance.js \
output.js \
test_alive.html \
test_explicit_names.html \
test_utterance_order.html \
test_braille.html \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,80 @@
const Cu = Components.utils;
const PREF_UTTERANCE_ORDER = "accessibility.accessfu.utterance";
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import("resource://gre/modules/accessibility/OutputGenerator.jsm", this);
/**
* Test context output generation.
*
* @param expected {Array} expected output.
* @param aAccOrElmOrID identifier to get an accessible to test.
* @param aOldAccOrElmOrID optional identifier to get an accessible relative to
* the |aAccOrElmOrID|.
* @param aGenerator the output generator to use when generating accessible
* output
*
* Note: if |aOldAccOrElmOrID| is not provided, the |aAccOrElmOrID| must be
* scoped to the "root" element in markup.
*/
function testContextOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, aGenerator) {
aOldAccOrElmOrID = aOldAccOrElmOrID || "root";
var accessible = getAccessible(aAccOrElmOrID);
var oldAccessible = getAccessible(aOldAccOrElmOrID);
var context = new PivotContext(accessible, oldAccessible);
var output = aGenerator.genForContext(context);
isDeeply(output, expected,
"Context output is correct for " + aAccOrElmOrID);
}
/**
* Test object output generated array that includes names.
* Note: test ignores outputs without the name.
*
* @param aAccOrElmOrID identifier to get an accessible to test.
* @param aGenerator the output generator to use when generating accessible
* output
*/
function testObjectOutput(aAccOrElmOrID, aGenerator) {
var accessible = getAccessible(aAccOrElmOrID);
var output = aGenerator.genForObject(accessible);
var outputOrder;
try {
outputOrder = SpecialPowers.getIntPref(PREF_UTTERANCE_ORDER);
} catch (ex) {
// PREF_UTTERANCE_ORDER not set.
outputOrder = 0;
}
var expectedNameIndex = outputOrder === 0 ? output.length - 1 : 0;
var nameIndex = output.indexOf(accessible.name);
if (nameIndex > -1) {
ok(output.indexOf(accessible.name) === expectedNameIndex,
"Object output is correct for " + aAccOrElmOrID);
}
}
/**
* Test object and context output for an accessible.
*
* @param expected {Array} expected output.
* @param aAccOrElmOrID identifier to get an accessible to test.
* @param aOldAccOrElmOrID optional identifier to get an accessible relative to
* the |aAccOrElmOrID|.
* @param aOutputKind the type of output
*/
function testOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, aOutputKind) {
var generator;
if (aOutputKind === 1) {
generator = UtteranceGenerator;
} else {
generator = BrailleGenerator;
}
testContextOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, generator);
// Just need to test object output for individual
// accOrElmOrID.
if (aOldAccOrElmOrID) {
return;
}
testObjectOutput(aAccOrElmOrID, generator);
}

View File

@ -0,0 +1,114 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=876475
-->
<head>
<title>[AccessFu] braille generation test</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="./output.js"></script>
<script type="application/javascript">
function doTest() {
// Test the following accOrElmOrID (with optional old accOrElmOrID).
// Note: each accOrElmOrID entry maps to a unique object braille
// generator function within the BrailleGenerator.
var tests = [{
accOrElmOrID: "link",
expected: [["lnk", "Link"], ["Link", "lnk"]]
},{
accOrElmOrID: "button",
expected: [["btn", "I am a button"], ["I am a button", "btn"]]
},{
accOrElmOrID: "password_input",
expected: [["passwdtxt", "Secret Password"], ["Secret Password", "passwdtxt"]]
},{
accOrElmOrID: "checkbox_unchecked",
expected: [["( )", "checkboxtext"], ["checkboxtext", "( )"]]
},{
accOrElmOrID: "checkbox_checked",
expected: [["(x)", "some more checkbox text"], ["some more checkbox text", "(x)"]]
},{
accOrElmOrID: "radio_unselected",
expected: [["( )", "any old radio button"], ["any old radio button", "( )"]]
},{
accOrElmOrID: "radio_selected",
expected: [["(x)", "a unique radio button"], ["a unique radio button", "(x)"]]
},{
accOrElmOrID: "togglebutton_notpressed",
expected: [["( )", "I ain't pressed"], ["I ain't pressed", "( )"]]
},{
accOrElmOrID: "togglebutton_pressed",
expected: [["(x)", "I am pressed!"], ["I am pressed!", "(x)"]]
},{
accOrElmOrID: "ul_li_one",
expected: [["*", "ul item 1"], ["*", "ul item 1"]]
},{
accOrElmOrID: "ol_li_one",
expected: [["1.", "ol item 1"], ["1.", "ol item 1"]]
},{
accOrElmOrID: "textarea",
expected: [["txtarea", "Here lies treasure."], ["Here lies treasure.", "txtarea"]]
}];
// Test all possible braille order preference values.
tests.forEach(function run(test) {
var brailleOrderValues = [0, 1];
brailleOrderValues.forEach(
function testBrailleOrder(brailleOrder) {
SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, brailleOrder);
var expected = test.expected[brailleOrder];
testOutput(expected, test.accOrElmOrID, test.oldAccOrElmOrID, 2);
}
);
});
// If there was an original utterance order preference, revert to it.
SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
SimpleTest.finish();
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTest);
</script>
</head>
<body>
<div id="root">
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<a href="example.com" id="link">Link</a>
<button id="button">I am a button</button>
<label for="password_input">Secret Password</label><input id="password_input" type="password"></input>
<label for="checkbox_unchecked">checkboxtext</label><input id="checkbox_unchecked" type="checkbox"></input>
<label for="checkbox_checked">some more checkbox text</label><input id="checkbox_checked" type="checkbox" checked></input>
<label for="radio_unselected">any old radio button</label><input id="radio_unselected" type="radio"></input>
<label for="radio_selected">a unique radio button</label><input id="radio_selected" type="radio" checked></input>
<div id="togglebutton_notpressed" aria-pressed="false" role="button" tabindex="-1">I ain't pressed</div>
<div id="togglebutton_pressed" aria-pressed="true" role="button" tabindex="-1">I am pressed!</div>
<ol id="ordered_list">
<li id="ol_li_one">ol item 1</li>
<li id="ol_li_two">ol item 2</li>
<li id="ol_li_three">ol item 3</li>
<li id="ol_li_three">ol item 4</li>
</ol>
<ul id="unordered_list">
<li id="ul_li_one">ul item 1</li>
<li id="ul_li_two">ul item 2</li>
<li id="ul_li_three">ul item 3</li>
<li id="ul_li_three">ul item 4</li>
</ul>
<textarea id="textarea" cols="80" rows="5">
Here lies treasure.
</textarea>
</div>
</body>
</html>

View File

@ -9,7 +9,7 @@
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="utterance.js"></script>
src="output.js"></script>
<script type="application/javascript">
function doTest() {
@ -87,11 +87,14 @@
"Plums"]
}];
SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, 0);
// Test various explicit names vs the utterance generated from subtrees.
tests.forEach(function run(test) {
testUtterance(test.expected, test.accOrElmOrID, test.oldAccOrElmOrID);
testOutput(test.expected, test.accOrElmOrID, test.oldAccOrElmOrID, 1);
});
SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
SimpleTest.finish();
}
@ -163,4 +166,4 @@
</ul>
</div>
</body>
</html>
</html>

View File

@ -13,7 +13,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=753984
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="./utterance.js"></script>
src="./output.js"></script>
<script type="application/javascript">
function doTest() {
@ -120,7 +120,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=753984
function testUtteranceOrder(utteranceOrder) {
SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, utteranceOrder);
var expected = test.expected[utteranceOrder];
testUtterance(expected, test.accOrElmOrID, test.oldAccOrElmOrID);
testOutput(expected, test.accOrElmOrID, test.oldAccOrElmOrID, 1);
}
);
});

View File

@ -1,70 +0,0 @@
const Cu = Components.utils;
const PREF_UTTERANCE_ORDER = "accessibility.accessfu.utterance";
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import("resource://gre/modules/accessibility/UtteranceGenerator.jsm",
this);
/**
* Test context utterance generation.
*
* @param expected {Array} expected utterance.
* @param aAccOrElmOrID identifier to get an accessible to test.
* @param aOldAccOrElmOrID optional identifier to get an accessible relative to
* the |aAccOrElmOrID|.
*
* Note: if |aOldAccOrElmOrID| is not provided, the |aAccOrElmOrID| must be
* scoped to the "root" element in markup.
*/
function testContextUtterance(expected, aAccOrElmOrID, aOldAccOrElmOrID) {
aOldAccOrElmOrID = aOldAccOrElmOrID || "root";
var accessible = getAccessible(aAccOrElmOrID);
var oldAccessible = getAccessible(aOldAccOrElmOrID);
var context = new PivotContext(accessible, oldAccessible);
var utterance = UtteranceGenerator.genForContext(context);
isDeeply(utterance, expected,
"Context utterance is correct for " + aAccOrElmOrID);
}
/**
* Test object utterance generated array that includes names.
* Note: test ignores utterances without the name.
*
* @param aAccOrElmOrID identifier to get an accessible to test.
*/
function testObjectUtterance(aAccOrElmOrID) {
var accessible = getAccessible(aAccOrElmOrID);
var utterance = UtteranceGenerator.genForObject(accessible);
var utteranceOrder;
try {
utteranceOrder = SpecialPowers.getIntPref(PREF_UTTERANCE_ORDER);
} catch (ex) {
// PREF_UTTERANCE_ORDER not set.
utteranceOrder = 0;
}
var expectedNameIndex = utteranceOrder === 0 ? utterance.length - 1 : 0;
var nameIndex = utterance.indexOf(accessible.name);
if (nameIndex > -1) {
ok(utterance.indexOf(accessible.name) === expectedNameIndex,
"Object utterance is correct for " + aAccOrElmOrID);
}
}
/**
* Test object and context utterance for an accessible.
*
* @param expected {Array} expected utterance.
* @param aAccOrElmOrID identifier to get an accessible to test.
* @param aOldAccOrElmOrID optional identifier to get an accessible relative to
* the |aAccOrElmOrID|.
*/
function testUtterance(expected, aAccOrElmOrID, aOldAccOrElmOrID) {
testContextUtterance(expected, aAccOrElmOrID, aOldAccOrElmOrID);
// Just need to test object utterance for individual
// accOrElmOrID.
if (aOldAccOrElmOrID) {
return;
}
testObjectUtterance(aAccOrElmOrID);
}

View File

@ -132,4 +132,12 @@ quicknav_PageTab = Page tabs
quicknav_RadioButton = Radio buttons
quicknav_Separator = Separators
quicknav_Table = Tables
quicknav_Checkbox = Check boxes
quicknav_Checkbox = Check boxes
# Shortened role names for braille
linkAbbr = lnk
pushbuttonAbbr = btn
passwordtextAbbr = passwdtxt
imagemapAbbr = imgmap
figureAbbr = fig
textareaAbbr = txtarea

View File

@ -25,6 +25,9 @@ import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
import com.googlecode.eyesfree.braille.selfbraille.WriteData;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@ -40,6 +43,8 @@ public class GeckoAccessibility {
private static JSONObject sEventMessage = null;
private static AccessibilityNodeInfo sVirtualCursorNode = null;
private static SelfBrailleClient sSelfBrailleClient = null;
private static final HashSet<String> sServiceWhitelist =
new HashSet<String>(Arrays.asList(new String[] {
"com.google.android.marvin.talkback.TalkBackService", // Google Talkback screen reader
@ -66,6 +71,10 @@ public class GeckoAccessibility {
if (sEnabled)
break;
}
if (sEnabled && sSelfBrailleClient == null &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
sSelfBrailleClient = new SelfBrailleClient(GeckoAppShell.getContext(), false);
}
}
try {
@ -188,6 +197,11 @@ public class GeckoAccessibility {
sVirtualCursorNode.setBoundsInScreen(screenBounds);
}
final String brailleText = message.optString("brailleText");
if (!brailleText.isEmpty()) {
sendBrailleText(view, brailleText);
}
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
@ -217,6 +231,16 @@ public class GeckoAccessibility {
}
}
private static void sendBrailleText(final View view, final String text) {
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
WriteData data = WriteData.forInfo(info);
data.setText(text);
// Set the focus blink
data.setSelectionStart(0);
data.setSelectionEnd(0);
sSelfBrailleClient.write(data);
}
public static void setDelegate(LayerView layerview) {
// Only use this delegate in Jelly Bean.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
@ -282,7 +306,6 @@ public class GeckoAccessibility {
info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
return info;
}
@ -309,6 +332,11 @@ public class GeckoAccessibility {
GeckoAppShell.
sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", null));
return true;
} else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY && virtualViewId == VIRTUAL_CURSOR_POSITION) {
// XXX: Self brailling gives this action with a bogus argument instead of an actual click action
GeckoAppShell.
sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", null));
return true;
}
return host.performAccessibilityAction(action, arguments);
}

View File

@ -44,6 +44,10 @@ UTIL_JAVA_FILES := \
util/UiAsyncTask.java \
$(NULL)
AIDL_AUTOGEN_FILES = \
braille/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java \
$(NULL)
FENNEC_JAVA_FILES = \
ANRReporter.java \
ActivityHandlerHelper.java \
@ -173,6 +177,8 @@ FENNEC_JAVA_FILES = \
WebAppAllocator.java \
WebAppImpl.java \
ZoomConstraints.java \
braille/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java \
braille/com/googlecode/eyesfree/braille/selfbraille/WriteData.java \
db/BrowserContract.java \
db/BrowserProvider.java \
db/FormHistoryProvider.java \
@ -1172,10 +1178,10 @@ classes.dex: $(ALL_JARS)
@echo "DX classes.dex"
$(DX) --dex --output=classes.dex jars $(ANDROID_COMPAT_LIB)
jars/gecko-browser.jar: jars/gecko-mozglue.jar jars/gecko-util.jar jars/sync-thirdparty.jar jars/websockets.jar $(addprefix $(srcdir)/,$(FENNEC_JAVA_FILES)) $(FENNEC_PP_JAVA_FILES) $(FENNEC_PP_JAVA_VIEW_FILES) $(addprefix $(srcdir)/,$(SYNC_JAVA_FILES)) $(SYNC_PP_JAVA_FILES) R.java
jars/gecko-browser.jar: jars/gecko-mozglue.jar jars/gecko-util.jar jars/sync-thirdparty.jar jars/websockets.jar $(addprefix $(srcdir)/,$(FENNEC_JAVA_FILES)) $(FENNEC_PP_JAVA_FILES) $(FENNEC_PP_JAVA_VIEW_FILES) $(addprefix $(srcdir)/,$(SYNC_JAVA_FILES)) $(SYNC_PP_JAVA_FILES) $(AIDL_AUTOGEN_FILES) R.java
@echo "JAR gecko-browser.jar"
$(NSINSTALL) -D classes/gecko-browser
$(JAVAC) $(JAVAC_FLAGS) -Xlint:all,-deprecation,-fallthrough -d classes/gecko-browser -classpath "jars/gecko-mozglue.jar:jars/gecko-util.jar:jars/sync-thirdparty.jar:jars/websockets.jar" $(addprefix $(srcdir)/,$(FENNEC_JAVA_FILES)) $(FENNEC_PP_JAVA_FILES) $(FENNEC_PP_JAVA_VIEW_FILES) $(addprefix $(srcdir)/,$(SYNC_JAVA_FILES)) $(SYNC_PP_JAVA_FILES) R.java
$(JAVAC) $(JAVAC_FLAGS) -Xlint:all,-deprecation,-fallthrough -d classes/gecko-browser -classpath "jars/gecko-mozglue.jar:jars/gecko-util.jar:jars/sync-thirdparty.jar:jars/websockets.jar" $(addprefix $(srcdir)/,$(FENNEC_JAVA_FILES)) $(FENNEC_PP_JAVA_FILES) $(FENNEC_PP_JAVA_VIEW_FILES) $(addprefix $(srcdir)/,$(SYNC_JAVA_FILES)) $(SYNC_PP_JAVA_FILES) $(addprefix $(srcdir)/,$(AIDL_AUTOGEN_FILES)) R.java
$(JAR) cMf jars/gecko-browser.jar -C classes/gecko-browser .
jars/gecko-mozglue.jar: $(addprefix $(srcdir)/,$(MOZGLUE_JAVA_FILES)) $(MOZGLUE_PP_JAVA_FILES) jars
@ -1214,6 +1220,9 @@ jars:
@echo "MKDIR jars"
$(NSINSTALL) -D jars
$(AIDL_AUTOGEN_FILES): %.java: %.aidl
$(ANDROID_PLATFORM_TOOLS)/aidl -I$(srcdir)/braille $<
CLASSES_WITH_JNI= \
org.mozilla.gecko.GeckoAppShell \
org.mozilla.gecko.GeckoJavaSampler \

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.eyesfree.braille.selfbraille;
import com.googlecode.eyesfree.braille.selfbraille.WriteData;
/**
* Interface for a client to control braille output for a part of the
* accessibility node tree.
*/
interface ISelfBrailleService {
void write(IBinder clientToken, in WriteData writeData);
oneway void disconnect(IBinder clientToken);
}

View File

@ -0,0 +1,267 @@
/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.eyesfree.braille.selfbraille;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Client-side interface to the self brailling interface.
*
* Threading: Instances of this object should be created and shut down
* in a thread with a {@link Looper} associated with it. Other methods may
* be called on any thread.
*/
public class SelfBrailleClient {
private static final String LOG_TAG =
SelfBrailleClient.class.getSimpleName();
private static final String ACTION_SELF_BRAILLE_SERVICE =
"com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE";
private static final String BRAILLE_BACK_PACKAGE =
"com.googlecode.eyesfree.brailleback";
private static final Intent mServiceIntent =
new Intent(ACTION_SELF_BRAILLE_SERVICE)
.setPackage(BRAILLE_BACK_PACKAGE);
/**
* SHA-1 hash value of the Eyes-Free release key certificate, used to sign
* BrailleBack. It was generated from the keystore with:
* $ keytool -exportcert -keystore <keystorefile> -alias android.keystore \
* > cert
* $ keytool -printcert -file cert
*/
// The typecasts are to silence a compiler warning about loss of precision
private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] {
(byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D,
(byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4,
(byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B,
(byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76,
(byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61
};
/**
* Delay before the first rebind attempt on bind error or service
* disconnect.
*/
private static final int REBIND_DELAY_MILLIS = 500;
private static final int MAX_REBIND_ATTEMPTS = 5;
private final Binder mIdentity = new Binder();
private final Context mContext;
private final boolean mAllowDebugService;
private final SelfBrailleHandler mHandler = new SelfBrailleHandler();
private boolean mShutdown = false;
/**
* Written in handler thread, read in any thread calling methods on the
* object.
*/
private volatile Connection mConnection;
/** Protected by synchronizing on mHandler. */
private int mNumFailedBinds = 0;
/**
* Constructs an instance of this class. {@code context} is used to bind
* to the self braille service. The current thread must have a Looper
* associated with it. If {@code allowDebugService} is true, this instance
* will connect to a BrailleBack service without requiring it to be signed
* by the release key used to sign BrailleBack.
*/
public SelfBrailleClient(Context context, boolean allowDebugService) {
mContext = context;
mAllowDebugService = allowDebugService;
doBindService();
}
/**
* Shuts this instance down, deallocating any global resources it is using.
* This method must be called on the same thread that created this object.
*/
public void shutdown() {
mShutdown = true;
doUnbindService();
}
public void write(WriteData writeData) {
writeData.validate();
ISelfBrailleService localService = getSelfBrailleService();
if (localService != null) {
try {
localService.write(mIdentity, writeData);
} catch (RemoteException ex) {
Log.e(LOG_TAG, "Self braille write failed", ex);
}
}
}
private void doBindService() {
Connection localConnection = new Connection();
if (!mContext.bindService(mServiceIntent, localConnection,
Context.BIND_AUTO_CREATE)) {
Log.e(LOG_TAG, "Failed to bind to service");
mHandler.scheduleRebind();
return;
}
mConnection = localConnection;
Log.i(LOG_TAG, "Bound to self braille service");
}
private void doUnbindService() {
if (mConnection != null) {
ISelfBrailleService localService = getSelfBrailleService();
if (localService != null) {
try {
localService.disconnect(mIdentity);
} catch (RemoteException ex) {
// Nothing to do.
}
}
mContext.unbindService(mConnection);
mConnection = null;
}
}
private ISelfBrailleService getSelfBrailleService() {
Connection localConnection = mConnection;
if (localConnection != null) {
return localConnection.mService;
}
return null;
}
private boolean verifyPackage() {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi;
try {
pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE,
PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException ex) {
Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE,
ex);
return false;
}
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException ex) {
Log.e(LOG_TAG, "SHA-1 not supported", ex);
return false;
}
// Check if any of the certificates match our hash.
for (Signature signature : pi.signatures) {
digest.update(signature.toByteArray());
if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) {
return true;
}
digest.reset();
}
if (mAllowDebugService) {
Log.w(LOG_TAG, String.format(
"*** %s connected to BrailleBack with invalid (debug?) "
+ "signature ***",
mContext.getPackageName()));
return true;
}
return false;
}
private class Connection implements ServiceConnection {
// Read in application threads, written in main thread.
private volatile ISelfBrailleService mService;
@Override
public void onServiceConnected(ComponentName className,
IBinder binder) {
if (!verifyPackage()) {
Log.w(LOG_TAG, String.format("Service certificate mismatch "
+ "for %s, dropping connection",
BRAILLE_BACK_PACKAGE));
mHandler.unbindService();
return;
}
Log.i(LOG_TAG, "Connected to self braille service");
mService = ISelfBrailleService.Stub.asInterface(binder);
synchronized (mHandler) {
mNumFailedBinds = 0;
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.e(LOG_TAG, "Disconnected from self braille service");
mService = null;
// Retry by rebinding.
mHandler.scheduleRebind();
}
}
private class SelfBrailleHandler extends Handler {
private static final int MSG_REBIND_SERVICE = 1;
private static final int MSG_UNBIND_SERVICE = 2;
public void scheduleRebind() {
synchronized (this) {
if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
++mNumFailedBinds;
}
}
}
public void unbindService() {
sendEmptyMessage(MSG_UNBIND_SERVICE);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REBIND_SERVICE:
handleRebindService();
break;
case MSG_UNBIND_SERVICE:
handleUnbindService();
break;
}
}
private void handleRebindService() {
if (mShutdown) {
return;
}
if (mConnection != null) {
doUnbindService();
}
doBindService();
}
private void handleUnbindService() {
doUnbindService();
}
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.eyesfree.braille.selfbraille;
parcelable WriteData;

View File

@ -0,0 +1,192 @@
/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.eyesfree.braille.selfbraille;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
/**
* Represents what should be shown on the braille display for a
* part of the accessibility node tree.
*/
public class WriteData implements Parcelable {
private static final String PROP_SELECTION_START = "selectionStart";
private static final String PROP_SELECTION_END = "selectionEnd";
private AccessibilityNodeInfo mAccessibilityNodeInfo;
private CharSequence mText;
private Bundle mProperties = Bundle.EMPTY;
/**
* Returns a new {@link WriteData} instance for the given {@code view}.
*/
public static WriteData forView(View view) {
AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(view);
WriteData writeData = new WriteData();
writeData.mAccessibilityNodeInfo = node;
return writeData;
}
public static WriteData forInfo(AccessibilityNodeInfo info){
WriteData writeData = new WriteData();
writeData.mAccessibilityNodeInfo = info;
return writeData;
}
public AccessibilityNodeInfo getAccessibilityNodeInfo() {
return mAccessibilityNodeInfo;
}
/**
* Sets the text to be displayed when the accessibility node associated
* with this instance has focus. If this method is not called (or
* {@code text} is {@code null}), this client relinquishes control over
* this node.
*/
public WriteData setText(CharSequence text) {
mText = text;
return this;
}
public CharSequence getText() {
return mText;
}
/**
* Sets the start position in the text of a text selection or cursor that
* should be marked on the display. A negative value (the default) means
* no selection will be added.
*/
public WriteData setSelectionStart(int v) {
writableProperties().putInt(PROP_SELECTION_START, v);
return this;
}
/**
* @see {@link #setSelectionStart}.
*/
public int getSelectionStart() {
return mProperties.getInt(PROP_SELECTION_START, -1);
}
/**
* Sets the end of the text selection to be marked on the display. This
* value should only be non-negative if the selection start is
* non-negative. If this value is <= the selection start, the selection
* is a cursor. Otherwise, the selection covers the range from
* start(inclusive) to end (exclusive).
*
* @see {@link android.text.Selection}.
*/
public WriteData setSelectionEnd(int v) {
writableProperties().putInt(PROP_SELECTION_END, v);
return this;
}
/**
* @see {@link #setSelectionEnd}.
*/
public int getSelectionEnd() {
return mProperties.getInt(PROP_SELECTION_END, -1);
}
private Bundle writableProperties() {
if (mProperties == Bundle.EMPTY) {
mProperties = new Bundle();
}
return mProperties;
}
/**
* Checks constraints on the fields that must be satisfied before sending
* this instance to the self braille service.
* @throws IllegalStateException
*/
public void validate() throws IllegalStateException {
if (mAccessibilityNodeInfo == null) {
throw new IllegalStateException(
"Accessibility node info can't be null");
}
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (mText == null) {
if (selectionStart > 0 || selectionEnd > 0) {
throw new IllegalStateException(
"Selection can't be set without text");
}
} else {
if (selectionStart < 0 && selectionEnd >= 0) {
throw new IllegalStateException(
"Selection end without start");
}
int textLength = mText.length();
if (selectionStart > textLength || selectionEnd > textLength) {
throw new IllegalStateException("Selection out of bounds");
}
}
}
// For Parcelable support.
public static final Parcelable.Creator<WriteData> CREATOR =
new Parcelable.Creator<WriteData>() {
@Override
public WriteData createFromParcel(Parcel in) {
return new WriteData(in);
}
@Override
public WriteData[] newArray(int size) {
return new WriteData[size];
}
};
@Override
public int describeContents() {
return 0;
}
/**
* {@inheritDoc}
* <strong>Note:</strong> The {@link AccessibilityNodeInfo} will be
* recycled by this method, don't try to use this more than once.
*/
@Override
public void writeToParcel(Parcel out, int flags) {
mAccessibilityNodeInfo.writeToParcel(out, flags);
// The above call recycles the node, so make sure we don't use it
// anymore.
mAccessibilityNodeInfo = null;
out.writeString(mText.toString());
out.writeBundle(mProperties);
}
private WriteData() {
}
private WriteData(Parcel in) {
mAccessibilityNodeInfo =
AccessibilityNodeInfo.CREATOR.createFromParcel(in);
mText = in.readString();
mProperties = in.readBundle();
}
}