Bug 1449364 - Create SessionAccessibility for GeckoSession. r=jchen r=yzen

This commit is contained in:
Eitan Isaacson 2018-04-11 15:21:00 +03:00
Родитель 5054e2ed0f
Коммит 29d6c7e5d5
21 изменённых файлов: 528 добавлений и 1152 удалений

Просмотреть файл

@ -13,32 +13,32 @@ if (Utils.MozBuildApp === "mobile/android") {
ChromeUtils.import("resource://gre/modules/Messaging.jsm");
}
// const ACCESSFU_DISABLE = 0;
const ACCESSFU_ENABLE = 1;
const ACCESSFU_AUTO = 2;
const SCREENREADER_SETTING = "accessibility.screenreader";
const QUICKNAV_MODES_PREF = "accessibility.accessfu.quicknav_modes";
const QUICKNAV_INDEX_PREF = "accessibility.accessfu.quicknav_index";
const GECKOVIEW_MESSAGE = {
ACTIVATE: "GeckoView:AccessibilityActivate",
VIEW_FOCUSED: "GeckoView:AccessibilityViewFocused",
LONG_PRESS: "GeckoView:AccessibilityLongPress",
BY_GRANULARITY: "GeckoView:AccessibilityByGranularity",
NEXT: "GeckoView:AccessibilityNext",
PREVIOUS: "GeckoView:AccessibilityPrevious",
SCROLL_BACKWARD: "GeckoView:AccessibilityScrollBackward",
SCROLL_FORWARD: "GeckoView:AccessibilityScrollForward",
};
var AccessFu = {
/**
* Initialize chrome-layer accessibility functionality.
* If accessibility is enabled on the platform, then a special accessibility
* mode is started.
*/
attach: function attach(aWindow) {
attach: function attach(aWindow, aInTest = false) {
Utils.init(aWindow);
if (Utils.MozBuildApp === "mobile/android") {
EventDispatcher.instance.dispatch("Accessibility:Ready");
EventDispatcher.instance.registerListener(this, "Accessibility:Settings");
if (!aInTest) {
this._enable();
}
this._activatePref = new PrefCache(
"accessibility.accessfu.activate", this._enableOrDisable.bind(this));
this._enableOrDisable();
},
/**
@ -49,10 +49,7 @@ var AccessFu = {
if (this._enabled) {
this._disable();
}
if (Utils.MozBuildApp === "mobile/android") {
EventDispatcher.instance.unregisterListener(this, "Accessibility:Settings");
}
delete this._activatePref;
Utils.uninit();
},
@ -117,16 +114,8 @@ var AccessFu = {
PointerAdapter.start();
if (Utils.MozBuildApp === "mobile/android") {
EventDispatcher.instance.registerListener(this, [
"Accessibility:ActivateObject",
"Accessibility:Focus",
"Accessibility:LongPress",
"Accessibility:MoveByGranularity",
"Accessibility:NextObject",
"Accessibility:PreviousObject",
"Accessibility:ScrollBackward",
"Accessibility:ScrollForward",
]);
Utils.win.WindowEventDispatcher.registerListener(this,
Object.values(GECKOVIEW_MESSAGE));
}
Services.obs.addObserver(this, "remote-browser-shown");
@ -172,16 +161,8 @@ var AccessFu = {
Services.obs.removeObserver(this, "inprocess-browser-shown");
if (Utils.MozBuildApp === "mobile/android") {
EventDispatcher.instance.unregisterListener(this, [
"Accessibility:ActivateObject",
"Accessibility:Focus",
"Accessibility:LongPress",
"Accessibility:MoveByGranularity",
"Accessibility:NextObject",
"Accessibility:PreviousObject",
"Accessibility:ScrollBackward",
"Accessibility:ScrollForward",
]);
Utils.win.WindowEventDispatcher.unregisterListener(this,
Object.values(GECKOVIEW_MESSAGE));
}
delete this._quicknavModesPref;
@ -195,23 +176,6 @@ var AccessFu = {
Logger.info("AccessFu:Disabled");
},
_enableOrDisable: function _enableOrDisable() {
try {
if (!this._activatePref) {
return;
}
let activatePref = this._activatePref.value;
if (activatePref == ACCESSFU_ENABLE ||
this._systemPref && activatePref == ACCESSFU_AUTO) {
this._enable();
} else {
this._disable();
}
} catch (x) {
dump("Error " + x.message + " " + x.fileName + ":" + x.lineNumber);
}
},
receiveMessage: function receiveMessage(aMessage) {
Logger.debug(() => {
return ["Recieved", aMessage.name, JSON.stringify(aMessage.json)];
@ -296,40 +260,43 @@ var AccessFu = {
onEvent(event, data, callback) {
switch (event) {
case "Accessibility:Settings":
this._systemPref = data.enabled;
this._enableOrDisable();
case GECKOVIEW_MESSAGE.SETTINGS:
if (data.enabled) {
this._enable();
} else {
this._disable();
}
break;
case "Accessibility:NextObject":
case "Accessibility:PreviousObject": {
case GECKOVIEW_MESSAGE.NEXT:
case GECKOVIEW_MESSAGE.PREVIOUS: {
let rule = "Simple";
if (data && data.rule && data.rule.length) {
rule = data.rule.substr(0, 1).toUpperCase() +
data.rule.substr(1).toLowerCase();
}
let method = event.replace(/Accessibility:(\w+)Object/, "move$1");
let method = event.replace(/GeckoView:Accessibility(\w+)/, "move$1");
this.Input.moveCursor(method, rule, "gesture");
break;
}
case "Accessibility:ActivateObject":
case GECKOVIEW_MESSAGE.ACTIVATE:
this.Input.activateCurrent(data);
break;
case "Accessibility:LongPress":
case GECKOVIEW_MESSAGE.LONG_PRESS:
this.Input.sendContextMenuMessage();
break;
case "Accessibility:ScrollForward":
case GECKOVIEW_MESSAGE.SCROL_LFORWARD:
this.Input.androidScroll("forward");
break;
case "Accessibility:ScrollBackward":
case GECKOVIEW_MESSAGE.SCROLL_BACKWARD:
this.Input.androidScroll("backward");
break;
case "Accessibility:Focus":
case GECKOVIEW_MESSAGE.VIEW_FOCUSED:
this._focused = data.gainFocus;
if (this._focused) {
this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
}
break;
case "Accessibility:MoveByGranularity":
case GECKOVIEW_MESSAGE.BY_GRANULARITY:
this.Input.moveByGranularity(data);
break;
}
@ -384,14 +351,7 @@ var AccessFu = {
break;
}
default:
{
// A settings change, it does not have an event type
if (aEvent.settingName == SCREENREADER_SETTING) {
this._systemPref = aEvent.settingValue;
this._enableOrDisable();
}
break;
}
}
},
@ -585,7 +545,7 @@ var Output = {
const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
for (let androidEvent of aDetails) {
androidEvent.type = "Accessibility:Event";
androidEvent.type = "GeckoView:AccessibilityEvent";
if (androidEvent.bounds) {
androidEvent.bounds = AccessFu.adjustContentBounds(
androidEvent.bounds, aBrowser);

Просмотреть файл

@ -141,7 +141,8 @@ var Utils = { // jshint ignore:line
get CurrentBrowser() {
if (!this.BrowserApp) {
return null;
// Get the first content browser element when no 'BrowserApp' exists.
return this.win.document.querySelector("browser[type=content]");
}
if (this.MozBuildApp == "b2g") {
return this.BrowserApp.contentBrowser;

Просмотреть файл

@ -141,7 +141,11 @@ var AccessFuTest = {
// Start AccessFu and put it in stand-by.
ChromeUtils.import("resource://gre/modules/accessibility/AccessFu.jsm");
AccessFu.attach(getMainChromeWindow(window));
let chromeWin = getMainChromeWindow(window);
chromeWin.WindowEventDispatcher = {
dispatch: () => {},
sendRequest: () => {}
};
AccessFu.readyCallback = function readyCallback() {
// Enable logging to the console service.
@ -149,6 +153,8 @@ var AccessFuTest = {
Logger.logLevel = Logger.DEBUG;
};
AccessFu.attach(chromeWin, true);
var prefs = [["accessibility.accessfu.notify_output", 1]];
prefs.push.apply(prefs, aAdditionalPrefs);

Просмотреть файл

@ -13,14 +13,6 @@
src="./jsatcommon.js"></script>
<script type="application/javascript">
function prefStart() {
AccessFuTest.once_log("AccessFu:Enabled", () =>
ok(AccessFu._enabled, "AccessFu was enabled again."));
AccessFuTest.once_log("EventManager.start", AccessFuTest.nextTest);
// Start AccessFu via pref.
SpecialPowers.pushPrefEnv({"set": [["accessibility.accessfu.activate", 1]]});
}
// Listen for 'EventManager.stop' and enable AccessFu again.
function settingsStart() {
isnot(AccessFu._enabled, true, "AccessFu was disabled.");
@ -47,19 +39,7 @@
AccessFu._disable();
}
// Listen for initial 'EventManager.start' and disable AccessFu.
function prefStop() {
ok(AccessFu._enabled, "AccessFu was started via preference.");
AccessFuTest.once_log("AccessFu:Disabled", () =>
isnot(AccessFu._enabled, true, "AccessFu was disabled."));
AccessFuTest.once_log("EventManager.stop", AccessFuTest.nextTest);
SpecialPowers.pushPrefEnv({"set": [["accessibility.accessfu.activate", 0]]});
}
function doTest() {
AccessFuTest.addFunc(prefStart);
AccessFuTest.addFunc(prefStop);
AccessFuTest.addFunc(settingsStart);
AccessFuTest.addFunc(settingsStop);
AccessFuTest.waitForExplicitFinish();

Просмотреть файл

@ -14,13 +14,13 @@
<script type="application/javascript">
function startAccessFu() {
SpecialPowers.pushPrefEnv({"set": [["accessibility.accessfu.activate", 1]]});
AccessFuTest.once_log("EventManager.start", AccessFuTest.nextTest);
AccessFu._enable();
}
function stopAccessFu() {
SpecialPowers.pushPrefEnv({"set": [["accessibility.accessfu.activate", 0]]});
AccessFuTest.once_log("EventManager.stop", () => AccessFuTest.finish());
AccessFu._disable();
}
function hide(id) {

Просмотреть файл

@ -13,10 +13,9 @@
src="./jsatcommon.js"></script>
<script type="application/javascript">
function prefStart() {
// Start AccessFu via pref.
SpecialPowers.pushPrefEnv({"set": [["accessibility.accessfu.activate", 1]]});
function startAccessFu() {
AccessFuTest.once_log("EventManager.start", AccessFuTest.nextTest);
AccessFu._enable();
}
function nextMode(aCurrentMode, aNextMode) {
@ -69,15 +68,14 @@
}
}
// Listen for initial 'EventManager.start' and disable AccessFu.
function prefStop() {
ok(AccessFu._enabled, "AccessFu was started via preference.");
function stopAccessFu() {
ok(AccessFu._enabled, "AccessFu is enabled.");
AccessFuTest.once_log("EventManager.stop", () => AccessFuTest.finish());
SpecialPowers.pushPrefEnv({"set": [["accessibility.accessfu.activate", 0]]});
AccessFu._disable();
}
function doTest() {
AccessFuTest.addFunc(prefStart);
AccessFuTest.addFunc(startAccessFu);
AccessFuTest.addFunc(nextMode("Link", "Heading"));
AccessFuTest.addFunc(nextMode("Heading", "FormElement"));
AccessFuTest.addFunc(nextMode("FormElement", "Link"));
@ -86,7 +84,7 @@
AccessFuTest.addFunc(prevMode("Link", "FormElement"));
AccessFuTest.addFunc(setMode(1, "Heading"));
AccessFuTest.addFunc(reconfigureModes);
AccessFuTest.addFunc(prefStop);
AccessFuTest.addFunc(stopAccessFu);
AccessFuTest.waitForExplicitFinish();
AccessFuTest.runTests([ // Will call SimpleTest.finish();
["accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement"]]);

Просмотреть файл

@ -883,7 +883,7 @@ public class BrowserApp extends GeckoApp
null);
EventDispatcher.getInstance().registerUiThreadListener(this,
"Accessibility:Enabled",
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
@ -1715,7 +1715,7 @@ public class BrowserApp extends GeckoApp
null);
EventDispatcher.getInstance().unregisterUiThreadListener(this,
"Accessibility:Enabled",
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
@ -1968,7 +1968,7 @@ public class BrowserApp extends GeckoApp
break;
case "Accessibility:Enabled":
case "GeckoView:AccessibilityEnabled":
mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("enabled"));
break;

Просмотреть файл

@ -1,401 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.geckoview.GeckoView;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
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;
public class GeckoAccessibility {
private static final String LOGTAG = "GeckoAccessibility";
private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
private static final int VIRTUAL_CURSOR_POSITION = 2;
private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
private static boolean sEnabled;
// Used to store the JSON message and populate the event later in the code path.
private static GeckoBundle sHoverEnter;
private static AccessibilityNodeInfo sVirtualCursorNode;
private static int sCurrentNode;
// This is the number Brailleback uses to start indexing routing keys.
private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
private static SelfBrailleClient sSelfBrailleClient;
public static void updateAccessibilitySettings (final Context context) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final AccessibilityManager accessibilityManager = (AccessibilityManager)
context.getSystemService(Context.ACCESSIBILITY_SERVICE);
sEnabled = accessibilityManager.isEnabled() &&
accessibilityManager.isTouchExplorationEnabled();
if (Build.VERSION.SDK_INT >= 16 && sEnabled && sSelfBrailleClient == null) {
sSelfBrailleClient = new SelfBrailleClient(context, false);
}
final GeckoBundle ret = new GeckoBundle(1);
ret.putBoolean("enabled", sEnabled);
// "Accessibility:Settings" is dispatched to the Gecko thread.
EventDispatcher.getInstance().dispatch("Accessibility:Settings", ret);
// "Accessibility:Enabled" is dispatched to the UI thread.
EventDispatcher.getInstance().dispatch("Accessibility:Enabled", ret);
}
});
}
private static void populateEventFromJSON (AccessibilityEvent event, GeckoBundle message) {
final String[] textArray = message.getStringArray("text");
if (textArray != null) {
for (int i = 0; i < textArray.length; i++)
event.getText().add(textArray[i] != null ? textArray[i] : "");
}
event.setContentDescription(message.getString("description", ""));
event.setEnabled(message.getBoolean("enabled", true));
event.setChecked(message.getBoolean("checked"));
event.setPassword(message.getBoolean("password"));
event.setAddedCount(message.getInt("addedCount", -1));
event.setRemovedCount(message.getInt("removedCount", -1));
event.setFromIndex(message.getInt("fromIndex", -1));
event.setItemCount(message.getInt("itemCount", -1));
event.setCurrentItemIndex(message.getInt("currentItemIndex", -1));
event.setBeforeText(message.getString("beforeText", ""));
event.setToIndex(message.getInt("toIndex", -1));
event.setScrollable(message.getBoolean("scrollable"));
event.setScrollX(message.getInt("scrollX", -1));
event.setScrollY(message.getInt("scrollY", -1));
event.setMaxScrollX(message.getInt("maxScrollX", -1));
event.setMaxScrollY(message.getInt("maxScrollY", -1));
}
private static void sendDirectAccessibilityEvent(int eventType, GeckoBundle message) {
final Context context = GeckoAppShell.getApplicationContext();
final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
accEvent.setClassName(GeckoAccessibility.class.getName());
accEvent.setPackageName(context.getPackageName());
populateEventFromJSON(accEvent, message);
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
try {
accessibilityManager.sendAccessibilityEvent(accEvent);
} catch (IllegalStateException e) {
// Accessibility is off.
}
}
public static boolean isEnabled() {
return sEnabled;
}
public static void sendAccessibilityEvent(final GeckoView view,
final GeckoBundle message) {
if (!sEnabled)
return;
final int eventType = message.getInt("eventType", -1);
if (eventType < 0) {
Log.e(LOGTAG, "No accessibility event type provided");
return;
}
sendAccessibilityEvent(view, message, eventType);
}
public static void sendAccessibilityEvent(final GeckoView view, final GeckoBundle message,
final int eventType) {
if (!sEnabled)
return;
final String exitView = message.getString("exitView", "");
if (exitView.equals("moveNext")) {
sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
} else if (exitView.equals("movePrevious")) {
sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
} else {
sCurrentNode = VIRTUAL_CURSOR_POSITION;
}
if (Build.VERSION.SDK_INT < 16) {
// Before Jelly Bean we send events directly from here while spoofing the source by setting
// the package and class name manually.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
sendDirectAccessibilityEvent(eventType, message);
}
});
} else {
// In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
// it work with TalkBack.
if (sVirtualCursorNode == null) {
sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
}
sVirtualCursorNode.setEnabled(message.getBoolean("enabled", true));
sVirtualCursorNode.setClickable(message.getBoolean("clickable"));
sVirtualCursorNode.setCheckable(message.getBoolean("checkable"));
sVirtualCursorNode.setChecked(message.getBoolean("checked"));
sVirtualCursorNode.setPassword(message.getBoolean("password"));
final String[] textArray = message.getStringArray("text");
StringBuilder sb = new StringBuilder();
if (textArray != null && textArray.length > 0) {
sb.append(textArray[0] != null ? textArray[0] : "");
for (int i = 1; i < textArray.length; i++) {
sb.append(' ').append(textArray[i] != null ? textArray[i] : "");
}
sVirtualCursorNode.setText(sb.toString());
}
sVirtualCursorNode.setContentDescription(message.getString("description", ""));
final GeckoBundle bounds = message.getBundle("bounds");
if (bounds != null) {
Rect relativeBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
bounds.getInt("right"), bounds.getInt("bottom"));
sVirtualCursorNode.setBoundsInParent(relativeBounds);
final Matrix matrix = new Matrix();
final float[] origin = new float[2];
view.getSession().getClientToScreenMatrix(matrix);
matrix.mapPoints(origin);
relativeBounds.offset((int) origin[0], (int) origin[1]);
sVirtualCursorNode.setBoundsInScreen(relativeBounds);
}
final GeckoBundle braille = message.getBundle("brailleOutput");
if (braille != null) {
sendBrailleText(view, braille.getString("text", ""),
braille.getInt("selectionStart"), braille.getInt("selectionEnd"));
}
if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
sHoverEnter = message;
}
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
event.setClassName(GeckoAccessibility.class.getName());
if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
event.setSource(view, View.NO_ID);
} else {
event.setSource(view, VIRTUAL_CURSOR_POSITION);
}
populateEventFromJSON(event, message);
((ViewParent) view).requestSendAccessibilityEvent(view, event);
}
}
private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
WriteData data = WriteData.forInfo(info);
data.setText(text);
// Set either the focus blink or the current caret position/selection
data.setSelectionStart(selectionStart);
data.setSelectionEnd(selectionEnd);
sSelfBrailleClient.write(data);
}
public static void setDelegate(View view) {
// Only use this delegate in Jelly Bean.
if (Build.VERSION.SDK_INT >= 16) {
view.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(final View v, final boolean hasFocus) {
onLayerViewFocusChanged(hasFocus);
}
});
}
@TargetApi(19)
public static void setAccessibilityManagerListeners(final Context context) {
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
@Override
public void onAccessibilityStateChanged(boolean enabled) {
updateAccessibilitySettings(context);
}
});
if (Build.VERSION.SDK_INT >= 19) {
accessibilityManager.addTouchExplorationStateChangeListener(new AccessibilityManager.TouchExplorationStateChangeListener() {
@Override
public void onTouchExplorationStateChanged(boolean enabled) {
updateAccessibilitySettings(context);
}
});
}
}
public static void onLayerViewFocusChanged(boolean gainFocus) {
if (sEnabled) {
final GeckoBundle data = new GeckoBundle(1);
data.putBoolean("gainFocus", gainFocus);
EventDispatcher.getInstance().dispatch("Accessibility:Focus", data);
}
}
public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
AccessibilityNodeProvider mAccessibilityNodeProvider;
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider(final View hostView) {
if (!(hostView instanceof GeckoView)) {
return super.getAccessibilityNodeProvider(hostView);
}
final GeckoView host = (GeckoView) hostView;
if (mAccessibilityNodeProvider == null)
// The accessibility node structure for web content consists of 3 LayerView child nodes:
// 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
// 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
// 3. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
AccessibilityNodeInfo.obtain(host, virtualDescendantId);
switch (virtualDescendantId) {
case View.NO_ID:
// This is the parent LayerView node, populate it with children.
onInitializeAccessibilityNodeInfo(host, info);
info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
info.addChild(host, VIRTUAL_CURSOR_POSITION);
info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
break;
default:
info.setParent(host);
info.setSource(host, virtualDescendantId);
info.setVisibleToUser(host.isShown());
info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
info.setClassName(host.getClass().getName());
info.setEnabled(true);
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
break;
}
return info;
}
@Override
public boolean performAction (int virtualViewId, int action, Bundle arguments) {
if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
// The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
// When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
GeckoAccessibility.sendAccessibilityEvent(host, sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
} else {
final GeckoBundle data = new GeckoBundle(1);
data.putBoolean("gainFocus", true);
EventDispatcher.getInstance().dispatch("Accessibility:Focus", data);
}
return true;
} else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
EventDispatcher.getInstance().dispatch("Accessibility:ActivateObject", null);
return true;
} else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
EventDispatcher.getInstance().dispatch("Accessibility:LongPress", null);
return true;
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
EventDispatcher.getInstance().dispatch("Accessibility:ScrollForward", null);
return true;
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
EventDispatcher.getInstance().dispatch("Accessibility:ScrollBackward", null);
return true;
} else if ((action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ||
action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT) && virtualViewId == VIRTUAL_CURSOR_POSITION) {
final GeckoBundle data;
if (arguments != null) {
data = new GeckoBundle(1);
data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
} else {
data = null;
}
EventDispatcher.getInstance().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
"Accessibility:NextObject" : "Accessibility:PreviousObject", data);
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;
// the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
// Other negative values are used by ChromeVox, but we don't support them.
// FAKE_GRANULARITY_READ_CURRENT = -1
// FAKE_GRANULARITY_READ_TITLE = -2
// FAKE_GRANULARITY_STOP_SPEECH = -3
// FAKE_GRANULARITY_CHANGE_SHIFTER = -4
int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
final GeckoBundle data = new GeckoBundle(1);
data.putInt("keyIndex", keyIndex);
EventDispatcher.getInstance().dispatch("Accessibility:ActivateObject", data);
} else if (granularity > 0) {
final GeckoBundle data = new GeckoBundle(2);
data.putString("direction", "Next");
data.putInt("granularity", granularity);
EventDispatcher.getInstance().dispatch("Accessibility:MoveByGranularity", data);
}
return true;
} else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
virtualViewId == VIRTUAL_CURSOR_POSITION) {
int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
final GeckoBundle data = new GeckoBundle(2);
data.putString("direction", "Previous");
data.putInt("granularity", granularity);
if (granularity > 0) {
EventDispatcher.getInstance().dispatch("Accessibility:MoveByGranularity", data);
}
return true;
}
return host.performAccessibilityAction(action, arguments);
}
};
return mAccessibilityNodeProvider;
}
}
}

Просмотреть файл

@ -687,12 +687,6 @@ public abstract class GeckoApp extends GeckoActivity
finish();
}
} else if ("Accessibility:Ready".equals(event)) {
GeckoAccessibility.updateAccessibilitySettings(this);
} else if ("Accessibility:Event".equals(event)) {
GeckoAccessibility.sendAccessibilityEvent(mLayerView, message);
} else if ("Contact:Add".equals(event)) {
final String email = message.getString("email");
final String phone = message.getString("phone");
@ -1038,7 +1032,6 @@ public abstract class GeckoApp extends GeckoActivity
// To prevent races, register startup events before launching the Gecko thread.
EventDispatcher.getInstance().registerGeckoThreadListener(this,
"Accessibility:Ready",
"Gecko:Ready",
null);
@ -1091,15 +1084,12 @@ public abstract class GeckoApp extends GeckoActivity
mLayerView.setSession(session, GeckoRuntime.getDefault(this));
mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
GeckoAccessibility.setDelegate(mLayerView);
getAppEventDispatcher().registerGeckoThreadListener(this,
"Locale:Set",
"PrivateBrowsing:Data",
null);
getAppEventDispatcher().registerUiThreadListener(this,
"Accessibility:Event",
"Contact:Add",
"DevToolsAuth:Scan",
"DOMFullScreen:Start",
@ -2089,7 +2079,6 @@ public abstract class GeckoApp extends GeckoActivity
}
EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
"Accessibility:Ready",
"Gecko:Ready",
null);
@ -2106,7 +2095,6 @@ public abstract class GeckoApp extends GeckoActivity
null);
getAppEventDispatcher().unregisterUiThreadListener(this,
"Accessibility:Event",
"Contact:Add",
"DevToolsAuth:Scan",
"DOMFullScreen:Start",

Просмотреть файл

@ -367,8 +367,6 @@ public class GeckoApplication extends Application
});
}
GeckoAccessibility.setAccessibilityManagerListeners(this);
AudioFocusAgent.getInstance().attachToContext(this);
}

Просмотреть файл

@ -37,7 +37,6 @@ import org.mozilla.gecko.Clipboard;
import org.mozilla.gecko.DoorHangerPopup;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.FormAssistPopup;
import org.mozilla.gecko.GeckoAccessibility;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.R;
@ -124,8 +123,6 @@ public class CustomTabsActivity extends AppCompatActivity
mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
GeckoAccessibility.setDelegate(mGeckoView);
final GeckoSessionSettings settings = new GeckoSessionSettings();
settings.setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, false);
settings.setBoolean(

Просмотреть файл

@ -26,7 +26,6 @@ import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.DoorHangerPopup;
import org.mozilla.gecko.FormAssistPopup;
import org.mozilla.gecko.GeckoAccessibility;
import org.mozilla.gecko.GeckoScreenOrientation;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.preferences.GeckoPreferences;
@ -143,8 +142,6 @@ public class WebAppActivity extends AppCompatActivity
}
});
GeckoAccessibility.setDelegate(mGeckoView);
mPromptService = new PromptService(this, mGeckoView.getEventDispatcher());
mDoorHangerPopup = new DoorHangerPopup(this, mGeckoView.getEventDispatcher());

Просмотреть файл

@ -86,6 +86,8 @@ function startup() {
"GeckoViewTrackingProtection");
ModuleManager.add("resource://gre/modules/GeckoViewSelectionAction.jsm",
"GeckoViewSelectionAction");
ModuleManager.add("resource://gre/modules/GeckoViewAccessibility.jsm",
"GeckoViewAccessibility");
// Move focus to the content window at the end of startup,
// so things like text selection can work properly.

Просмотреть файл

@ -92,6 +92,8 @@ public class GeckoSession extends LayerSession
private final SessionTextInput mTextInput = new SessionTextInput(this, mNativeQueue);
private SessionAccessibility mSessionAccessibility;
private String mId = UUID.randomUUID().toString().replace("-", "");
/* package */ String getId() { return mId; }
@ -825,6 +827,18 @@ public class GeckoSession extends LayerSession
return mTextInput;
}
/**
* Get the SessionAccessibility instance for this session.
*
* @return SessionAccessibility instance.
*/
public @NonNull SessionAccessibility getAccessibility() {
if (mSessionAccessibility == null) {
mSessionAccessibility = new SessionAccessibility(this);
}
return mSessionAccessibility;
}
@IntDef(flag = true,
value = { LOAD_FLAGS_NONE, LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_PROXY,
LOAD_FLAGS_EXTERNAL, LOAD_FLAGS_ALLOW_POPUPS })

Просмотреть файл

@ -34,8 +34,8 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
@ -45,8 +45,6 @@ public class GeckoView extends FrameLayout {
private static final String LOGTAG = "GeckoView";
private static final boolean DEBUG = false;
private static AccessibilityManager sAccessibilityManager;
protected final Display mDisplay = new Display();
protected GeckoSession mSession;
protected GeckoRuntime mRuntime;
@ -167,6 +165,7 @@ public class GeckoView extends FrameLayout {
private void init() {
setFocusable(true);
setFocusableInTouchMode(true);
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
// We are adding descendants to this LayerView, but we don't want the
// descendants to affect the way LayerView retains its focus.
@ -325,6 +324,8 @@ public class GeckoView extends FrameLayout {
mSession.getTextInput().setView(this);
mSession.getAccessibility().setView(this);
super.onAttachedToWindow();
}
@ -333,7 +334,8 @@ public class GeckoView extends FrameLayout {
super.onDetachedFromWindow();
if (mSession != null) {
mSession.getTextInput().setView(null);
mSession.getTextInput().setView(null);
mSession.getAccessibility().setView(null);
}
if (mStateSaved) {
@ -507,21 +509,12 @@ public class GeckoView extends FrameLayout {
mSession.getPanZoomController().onTouchEvent(event);
}
protected static boolean isAccessibilityEnabled(final Context context) {
if (sAccessibilityManager == null) {
sAccessibilityManager = (AccessibilityManager)
context.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
return sAccessibilityManager.isEnabled() &&
sAccessibilityManager.isTouchExplorationEnabled();
}
@Override
public boolean onHoverEvent(final MotionEvent event) {
// If we get a touchscreen hover event, and accessibility is not enabled, don't
// send it to Gecko.
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN &&
!isAccessibilityEnabled(getContext())) {
!SessionAccessibility.Settings.isEnabled()) {
return false;
}

Просмотреть файл

@ -0,0 +1,416 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
public class SessionAccessibility {
private static final String LOGTAG = "GeckoAccessibility";
// This is a special ID we use for nodes that are eent sources.
// We expose it as a fragment and not an actual child of the View node.
private static final int VIRTUAL_CONTENT_ID = -2;
// This is the number BrailleBack uses to start indexing routing keys.
private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
// Gecko session we are proxying
/* package */ final GeckoSession mSession;
// This is the view that delegates accessibility to us. We also sends event through it.
private View mView;
// Aave we reached the last item in content?
private boolean mLastItem;
// Used to store the JSON message and populate the event later in the code path.
private AccessibilityNodeInfo mVirtualContentNode;
/* package */ SessionAccessibility(final GeckoSession session) {
mSession = session;
Settings.getInstance().dispatch();
session.getEventDispatcher().registerUiThreadListener(new BundleEventListener() {
@Override
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
sendAccessibilityEvent(message);
}
}, "GeckoView:AccessibilityEvent", null);
}
/**
* Get the GeckoView instance that delegates accessibility to this session.
*
* @return GeckoView instance.
*/
public View getView() {
return mView;
}
/**
* Set the GeckoView instance that should delegate accessibility to this session.
*/
public void setView(final View view) {
if (mView != null) {
mView.setAccessibilityDelegate(null);
}
mView = view;
mLastItem = false;
if (mView == null) {
return;
}
mView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
private AccessibilityNodeProvider mAccessibilityNodeProvider;
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider(final View hostView) {
if (mAccessibilityNodeProvider == null)
mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
assertAttachedView(hostView);
AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null) ?
AccessibilityNodeInfo.obtain(mVirtualContentNode) :
AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
switch (virtualDescendantId) {
case View.NO_ID:
// This is the parent View node.
// We intentionally don't add VIRTUAL_CONTENT_ID
// as a child. It is a source for events,
// but not a member of the tree you
// can get to by traversing down.
onInitializeAccessibilityNodeInfo(mView, info);
info.setClassName("android.webkit.WebView"); // TODO: WTF
Bundle bundle = info.getExtras();
bundle.putCharSequence(
"ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
"ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL,FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6,HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN,MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD,UNVISITED_LINK,VISITED_LINK");
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addChild(hostView, VIRTUAL_CONTENT_ID);
break;
default:
info.setParent(mView);
info.setSource(mView, virtualDescendantId);
info.setVisibleToUser(mView.isShown());
info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
info.setClassName(mView.getClass().getName());
info.setEnabled(true);
info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
break;
}
return info;
}
@Override
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
assertAttachedView(hostView);
if (virtualViewId == View.NO_ID) {
return performRootAction(action, arguments);
}
return performContentAction(action, arguments);
}
private boolean performRootAction(int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
final GeckoBundle data = new GeckoBundle(1);
data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
return true;
}
return mView.performAccessibilityAction(action, arguments);
}
private boolean performContentAction(int action, Bundle arguments) {
final GeckoBundle data;
switch (action) {
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
return true;
case AccessibilityNodeInfo.ACTION_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
return true;
case AccessibilityNodeInfo.ACTION_LONG_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
return true;
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
return true;
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
return true;
case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
if (mLastItem) {
return false;
}
case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
if (arguments != null) {
data = new GeckoBundle(1);
data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
} else {
data = null;
}
mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
"GeckoView:AccessibilityNext" : "GeckoView:AccessibilityPrevious", data);
return true;
case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
// XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
// the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
// Other negative values are used by ChromeVox, but we don't support them.
// FAKE_GRANULARITY_READ_CURRENT = -1
// FAKE_GRANULARITY_READ_TITLE = -2
// FAKE_GRANULARITY_STOP_SPEECH = -3
// FAKE_GRANULARITY_CHANGE_SHIFTER = -4
int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
data = new GeckoBundle(1);
data.putInt("keyIndex", keyIndex);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
} else if (granularity > 0) {
data = new GeckoBundle(2);
data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
data.putInt("granularity", granularity);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
}
return true;
}
return mView.performAccessibilityAction(action, arguments);
}
private void assertAttachedView(final View view) {
if (view != mView) {
throw new AssertionError("delegate used with wrong view.");
}
}
};
return mAccessibilityNodeProvider;
}
});
}
public static class Settings {
private static final Settings INSTANCE = new Settings();
private boolean mEnabled;
public Settings() {
EventDispatcher.getInstance().registerUiThreadListener(new BundleEventListener() {
@Override
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
updateAccessibilitySettings();
}
}, "GeckoView:AccessibilityReady", null);
final Context context = GeckoAppShell.getApplicationContext();
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = accessibilityManager.isEnabled() &&
accessibilityManager.isTouchExplorationEnabled();
accessibilityManager.addAccessibilityStateChangeListener(
new AccessibilityManager.AccessibilityStateChangeListener() {
@Override
public void onAccessibilityStateChanged(boolean enabled) {
updateAccessibilitySettings();
}
}
);
if (Build.VERSION.SDK_INT >= 19) {
accessibilityManager.addTouchExplorationStateChangeListener(
new AccessibilityManager.TouchExplorationStateChangeListener() {
@Override
public void onTouchExplorationStateChanged(boolean enabled) {
updateAccessibilitySettings();
}
}
);
}
}
public static Settings getInstance() {
return INSTANCE;
}
public static boolean isEnabled() {
return INSTANCE.mEnabled;
}
private void updateAccessibilitySettings() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final AccessibilityManager accessibilityManager = (AccessibilityManager)
GeckoAppShell.getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = accessibilityManager.isEnabled() &&
accessibilityManager.isTouchExplorationEnabled();
dispatch();
}
});
}
private void dispatch() {
final GeckoBundle ret = new GeckoBundle(1);
ret.putBoolean("enabled", mEnabled);
// "GeckoView:AccessibilitySettings" is dispatched to the Gecko thread.
EventDispatcher.getInstance().dispatch("GeckoView:AccessibilitySettings", ret);
// "GeckoView:AccessibilityEnabled" is dispatched to the UI thread.
EventDispatcher.getInstance().dispatch("GeckoView:AccessibilityEnabled", ret);
}
}
private AccessibilityEvent obtainEvent(final int eventType, final int sourceId) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
event.setClassName(SessionAccessibility.class.getName());
event.setSource(mView, sourceId);
return event;
}
private static void populateEventFromJSON(AccessibilityEvent event, final GeckoBundle message) {
final String[] textArray = message.getStringArray("text");
if (textArray != null) {
for (int i = 0; i < textArray.length; i++)
event.getText().add(textArray[i] != null ? textArray[i] : "");
}
event.setContentDescription(message.getString("description", ""));
event.setEnabled(message.getBoolean("enabled", true));
event.setChecked(message.getBoolean("checked"));
event.setPassword(message.getBoolean("password"));
event.setAddedCount(message.getInt("addedCount", -1));
event.setRemovedCount(message.getInt("removedCount", -1));
event.setFromIndex(message.getInt("fromIndex", -1));
event.setItemCount(message.getInt("itemCount", -1));
event.setCurrentItemIndex(message.getInt("currentItemIndex", -1));
event.setBeforeText(message.getString("beforeText", ""));
event.setToIndex(message.getInt("toIndex", -1));
event.setScrollable(message.getBoolean("scrollable"));
event.setScrollX(message.getInt("scrollX", -1));
event.setScrollY(message.getInt("scrollY", -1));
event.setMaxScrollX(message.getInt("maxScrollX", -1));
event.setMaxScrollY(message.getInt("maxScrollY", -1));
}
private void populateNodeInfoFromJSON(AccessibilityNodeInfo node, final GeckoBundle message) {
node.setEnabled(message.getBoolean("enabled", true));
node.setClickable(message.getBoolean("clickable"));
node.setCheckable(message.getBoolean("checkable"));
node.setChecked(message.getBoolean("checked"));
node.setPassword(message.getBoolean("password"));
final String[] textArray = message.getStringArray("text");
StringBuilder sb = new StringBuilder();
if (textArray != null && textArray.length > 0) {
sb.append(textArray[0] != null ? textArray[0] : "");
for (int i = 1; i < textArray.length; i++) {
sb.append(' ').append(textArray[i] != null ? textArray[i] : "");
}
node.setText(sb.toString());
}
node.setContentDescription(message.getString("description", ""));
final GeckoBundle bounds = message.getBundle("bounds");
if (bounds != null) {
Rect relativeBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
bounds.getInt("right"), bounds.getInt("bottom"));
node.setBoundsInParent(relativeBounds);
final Matrix matrix = new Matrix();
final float[] origin = new float[2];
mSession.getClientToScreenMatrix(matrix);
matrix.mapPoints(origin);
relativeBounds.offset((int) origin[0], (int) origin[1]);
node.setBoundsInScreen(relativeBounds);
}
}
private void sendAccessibilityEvent(final GeckoBundle message) {
if (mView == null || !Settings.isEnabled())
return;
final int eventType = message.getInt("eventType", -1);
if (eventType < 0) {
Log.e(LOGTAG, "No accessibility event type provided");
return;
}
int eventSource = VIRTUAL_CONTENT_ID;
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
final String exitView = message.getString("exitView", "");
mLastItem = exitView.equals("moveNext");
if (mLastItem) {
return;
}
if (exitView.equals("movePrevious")) {
eventSource = View.NO_ID;
}
}
if (eventSource != View.NO_ID) {
// In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
// it work with TalkBack.
if (mVirtualContentNode == null) {
mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
}
populateNodeInfoFromJSON(mVirtualContentNode, message);
}
final AccessibilityEvent accessibilityEvent = obtainEvent(eventType, eventSource);
populateEventFromJSON(accessibilityEvent, message);
((ViewParent) mView).requestSendAccessibilityEvent(mView, accessibilityEvent);
}
}

Просмотреть файл

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["GeckoViewAccessibility"];
ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
EventDispatcher: "resource://gre/modules/Messaging.jsm",
AccessFu: "resource://gre/modules/accessibility/AccessFu.jsm"
});
XPCOMUtils.defineLazyGetter(this, "dump", () =>
ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
{}).AndroidLog.d.bind(null, "GeckoAccessibility"));
class GeckoViewAccessibility extends GeckoViewModule {
init() {
EventDispatcher.instance.dispatch("GeckoView:AccessibilityReady");
EventDispatcher.instance.registerListener((aEvent, aData, aCallback) => {
if (aData.enabled) {
AccessFu.attach(this.window);
} else {
AccessFu.detach();
}
}, "GeckoView:AccessibilitySettings");
}
}

Просмотреть файл

@ -6,6 +6,7 @@
EXTRA_JS_MODULES += [
'AndroidLog.jsm',
'GeckoViewAccessibility.jsm',
'GeckoViewContent.jsm',
'GeckoViewContentModule.jsm',
'GeckoViewModule.jsm',

Просмотреть файл

@ -1,147 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package com.googlecode.eyesfree.braille.selfbraille;
/**
* Interface for a client to control braille output for a part of the
* accessibility node tree.
*/
public interface ISelfBrailleService extends android.os.IInterface {
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements
com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
private static final java.lang.String DESCRIPTOR = "com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService";
/** Construct the stub at attach it to the interface. */
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an
* com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService
* interface, generating a proxy if needed.
*/
public static com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService asInterface(
android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService))) {
return ((com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService) iin);
}
return new com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService.Stub.Proxy(
obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
@Override
public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags)
throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_write: {
data.enforceInterface(DESCRIPTOR);
android.os.IBinder _arg0;
_arg0 = data.readStrongBinder();
com.googlecode.eyesfree.braille.selfbraille.WriteData _arg1;
if ((0 != data.readInt())) {
_arg1 = com.googlecode.eyesfree.braille.selfbraille.WriteData.CREATOR
.createFromParcel(data);
} else {
_arg1 = null;
}
this.write(_arg0, _arg1);
reply.writeNoException();
return true;
}
case TRANSACTION_disconnect: {
data.enforceInterface(DESCRIPTOR);
android.os.IBinder _arg0;
_arg0 = data.readStrongBinder();
this.disconnect(_arg0);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements
com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
@Override
public void write(
android.os.IBinder clientToken,
com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder(clientToken);
if ((writeData != null)) {
_data.writeInt(1);
writeData.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_write, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
@Override
public void disconnect(android.os.IBinder clientToken)
throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder(clientToken);
mRemote.transact(Stub.TRANSACTION_disconnect, _data, null,
android.os.IBinder.FLAG_ONEWAY);
} finally {
_data.recycle();
}
}
}
static final int TRANSACTION_write = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_disconnect = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public void write(android.os.IBinder clientToken,
com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
throws android.os.RemoteException;
public void disconnect(android.os.IBinder clientToken)
throws android.os.RemoteException;
}

Просмотреть файл

@ -1,267 +0,0 @@
/*
* 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();
}
}
}

Просмотреть файл

@ -1,192 +0,0 @@
/*
* 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();
}
}