Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko;
8 import org.mozilla.gecko.gfx.LayerView;
9 import org.mozilla.gecko.util.ThreadUtils;
10 import org.mozilla.gecko.util.UiAsyncTask;
12 import org.json.JSONArray;
13 import org.json.JSONException;
14 import org.json.JSONObject;
16 import android.app.ActivityManager;
17 import android.app.ActivityManager.RunningServiceInfo;
18 import android.content.Context;
19 import android.graphics.Rect;
20 import android.os.Build;
21 import android.os.Bundle;
22 import android.util.Log;
23 import android.view.View;
24 import android.view.accessibility.AccessibilityEvent;
25 import android.view.accessibility.AccessibilityManager;
26 import android.view.accessibility.AccessibilityNodeInfo;
27 import android.view.accessibility.AccessibilityNodeProvider;
29 import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
30 import com.googlecode.eyesfree.braille.selfbraille.WriteData;
32 import java.util.Arrays;
33 import java.util.HashSet;
34 import java.util.List;
36 public class GeckoAccessibility {
37 private static final String LOGTAG = "GeckoAccessibility";
38 private static final int VIRTUAL_CURSOR_PREVIOUS = 1;
39 private static final int VIRTUAL_CURSOR_POSITION = 2;
40 private static final int VIRTUAL_CURSOR_NEXT = 3;
42 private static boolean sEnabled = false;
43 // Used to store the JSON message and populate the event later in the code path.
44 private static JSONObject sEventMessage = null;
45 private static AccessibilityNodeInfo sVirtualCursorNode = null;
47 // This is the number Brailleback uses to start indexing routing keys.
48 private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
49 private static SelfBrailleClient sSelfBrailleClient = null;
51 private static final HashSet<String> sServiceWhitelist =
52 new HashSet<String>(Arrays.asList(new String[] {
53 "com.google.android.marvin.talkback.TalkBackService", // Google Talkback screen reader
54 "com.mot.readout.ScreenReader", // Motorola screen reader
55 "info.spielproject.spiel.SpielService", // Spiel screen reader
56 "es.codefactory.android.app.ma.MAAccessibilityService" // Codefactory Mobile Accessibility screen reader
57 }));
59 public static void updateAccessibilitySettings (final GeckoApp app) {
60 new UiAsyncTask<Void, Void, Void>(ThreadUtils.getBackgroundHandler()) {
61 @Override
62 public Void doInBackground(Void... args) {
63 JSONObject ret = new JSONObject();
64 sEnabled = false;
65 AccessibilityManager accessibilityManager =
66 (AccessibilityManager) app.getSystemService(Context.ACCESSIBILITY_SERVICE);
67 if (accessibilityManager.isEnabled()) {
68 ActivityManager activityManager =
69 (ActivityManager) app.getSystemService(Context.ACTIVITY_SERVICE);
70 List<RunningServiceInfo> runningServices = activityManager.getRunningServices(Integer.MAX_VALUE);
72 for (RunningServiceInfo runningServiceInfo : runningServices) {
73 sEnabled = sServiceWhitelist.contains(runningServiceInfo.service.getClassName());
74 if (sEnabled)
75 break;
76 }
77 if (sEnabled && sSelfBrailleClient == null &&
78 Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
79 sSelfBrailleClient = new SelfBrailleClient(GeckoAppShell.getContext(), false);
80 }
81 }
83 try {
84 ret.put("enabled", sEnabled);
85 } catch (Exception ex) {
86 Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
87 }
89 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Settings",
90 ret.toString()));
91 return null;
92 }
94 @Override
95 public void onPostExecute(Void args) {
96 // Disable the dynamic toolbar when enabling accessibility.
97 // These features tend not to interact well.
98 app.setAccessibilityEnabled(sEnabled);
99 }
100 }.execute();
101 }
103 private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
104 final JSONArray textArray = message.optJSONArray("text");
105 if (textArray != null) {
106 for (int i = 0; i < textArray.length(); i++)
107 event.getText().add(textArray.optString(i));
108 }
110 event.setContentDescription(message.optString("description"));
111 event.setEnabled(message.optBoolean("enabled", true));
112 event.setChecked(message.optBoolean("checked"));
113 event.setPassword(message.optBoolean("password"));
114 event.setAddedCount(message.optInt("addedCount", -1));
115 event.setRemovedCount(message.optInt("removedCount", -1));
116 event.setFromIndex(message.optInt("fromIndex", -1));
117 event.setItemCount(message.optInt("itemCount", -1));
118 event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
119 event.setBeforeText(message.optString("beforeText"));
120 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
121 event.setToIndex(message.optInt("toIndex", -1));
122 event.setScrollable(message.optBoolean("scrollable"));
123 event.setScrollX(message.optInt("scrollX", -1));
124 event.setScrollY(message.optInt("scrollY", -1));
125 }
126 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
127 event.setMaxScrollX(message.optInt("maxScrollX", -1));
128 event.setMaxScrollY(message.optInt("maxScrollY", -1));
129 }
130 }
132 private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
133 final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
134 accEvent.setClassName(GeckoAccessibility.class.getName());
135 accEvent.setPackageName(GeckoAppShell.getContext().getPackageName());
136 populateEventFromJSON(accEvent, message);
137 AccessibilityManager accessibilityManager =
138 (AccessibilityManager) GeckoAppShell.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
139 try {
140 accessibilityManager.sendAccessibilityEvent(accEvent);
141 } catch (IllegalStateException e) {
142 // Accessibility is off.
143 }
144 }
146 public static void sendAccessibilityEvent (final JSONObject message) {
147 if (!sEnabled)
148 return;
150 final int eventType = message.optInt("eventType", -1);
151 if (eventType < 0) {
152 Log.e(LOGTAG, "No accessibility event type provided");
153 return;
154 }
156 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
157 // Before Jelly Bean we send events directly from here while spoofing the source by setting
158 // the package and class name manually.
159 ThreadUtils.postToBackgroundThread(new Runnable() {
160 @Override
161 public void run() {
162 sendDirectAccessibilityEvent(eventType, message);
163 }
164 });
165 } else {
166 // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
167 // it work with TalkBack.
168 final LayerView view = GeckoAppShell.getLayerView();
169 if (view == null)
170 return;
172 if (sVirtualCursorNode == null)
173 sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
174 sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
175 sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
176 sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
177 sVirtualCursorNode.setChecked(message.optBoolean("checked"));
178 sVirtualCursorNode.setPassword(message.optBoolean("password"));
180 final JSONArray textArray = message.optJSONArray("text");
181 StringBuilder sb = new StringBuilder();
182 if (textArray != null && textArray.length() > 0) {
183 sb.append(textArray.optString(0));
184 for (int i = 1; i < textArray.length(); i++) {
185 sb.append(" ").append(textArray.optString(i));
186 }
187 }
188 sVirtualCursorNode.setText(sb.toString());
189 sVirtualCursorNode.setContentDescription(message.optString("description"));
191 JSONObject bounds = message.optJSONObject("bounds");
192 if (bounds != null) {
193 Rect relativeBounds = new Rect(bounds.optInt("left"), bounds.optInt("top"),
194 bounds.optInt("right"), bounds.optInt("bottom"));
195 sVirtualCursorNode.setBoundsInParent(relativeBounds);
196 int[] locationOnScreen = new int[2];
197 view.getLocationOnScreen(locationOnScreen);
198 Rect screenBounds = new Rect(relativeBounds);
199 screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
200 sVirtualCursorNode.setBoundsInScreen(screenBounds);
201 }
203 final JSONObject braille = message.optJSONObject("brailleOutput");
204 if (braille != null) {
205 sendBrailleText(view, braille.optString("text"),
206 braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
207 }
209 ThreadUtils.postToUiThread(new Runnable() {
210 @Override
211 public void run() {
212 // If this is an accessibility focus, a lot of internal voodoo happens so we perform an
213 // accessibility focus action on the view, and it in turn sends the right events.
214 switch (eventType) {
215 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
216 sEventMessage = message;
217 view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
218 break;
219 case AccessibilityEvent.TYPE_ANNOUNCEMENT:
220 case AccessibilityEvent.TYPE_VIEW_SCROLLED:
221 sEventMessage = null;
222 final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
223 view.onInitializeAccessibilityEvent(accEvent);
224 populateEventFromJSON(accEvent, message);
225 view.getParent().requestSendAccessibilityEvent(view, accEvent);
226 break;
227 default:
228 sEventMessage = message;
229 view.sendAccessibilityEvent(eventType);
230 break;
231 }
232 }
233 });
235 }
236 }
238 private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
239 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
240 WriteData data = WriteData.forInfo(info);
241 data.setText(text);
242 // Set either the focus blink or the current caret position/selection
243 data.setSelectionStart(selectionStart);
244 data.setSelectionEnd(selectionEnd);
245 sSelfBrailleClient.write(data);
246 }
248 public static void setDelegate(LayerView layerview) {
249 // Only use this delegate in Jelly Bean.
250 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
251 layerview.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
252 layerview.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
253 }
254 }
256 public static void onLayerViewFocusChanged(LayerView layerview, boolean gainFocus) {
257 if (sEnabled)
258 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Focus",
259 gainFocus ? "true" : "false"));
260 }
262 public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
263 AccessibilityNodeProvider mAccessibilityNodeProvider;
265 @Override
266 public void onPopulateAccessibilityEvent (View host, AccessibilityEvent event) {
267 super.onPopulateAccessibilityEvent(host, event);
268 if (sEventMessage != null) {
269 populateEventFromJSON(event, sEventMessage);
270 // No matter where the a11y focus is requested, we always force it back to the current vc position.
271 event.setSource(host, VIRTUAL_CURSOR_POSITION);
272 }
273 // We save the hover enter event so that we could reuse it for a subsequent accessibility focus event.
274 if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
275 sEventMessage = null;
276 }
278 @Override
279 public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
280 if (mAccessibilityNodeProvider == null)
281 // The accessibility node structure for web content consists of 3 LayerView child nodes:
282 // 1. VIRTUAL_CURSOR_PREVIOUS: Represents the virtual cursor position that is previous to the
283 // current one.
284 // 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
285 // 3. VIRTUAL_CURSOR_NEXT: Represents the next virtual cursor position.
286 mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
287 @Override
288 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
289 AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
290 AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
291 AccessibilityNodeInfo.obtain(host, virtualDescendantId);
293 switch (virtualDescendantId) {
294 case View.NO_ID:
295 // This is the parent LayerView node, populate it with children.
296 onInitializeAccessibilityNodeInfo(host, info);
297 info.addChild(host, VIRTUAL_CURSOR_PREVIOUS);
298 info.addChild(host, VIRTUAL_CURSOR_POSITION);
299 info.addChild(host, VIRTUAL_CURSOR_NEXT);
300 break;
301 default:
302 info.setParent(host);
303 info.setSource(host, virtualDescendantId);
304 info.setVisibleToUser(host.isShown());
305 info.setPackageName(GeckoAppShell.getContext().getPackageName());
306 info.setClassName(host.getClass().getName());
307 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
308 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
309 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
310 info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
311 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
312 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
313 info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
314 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
315 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
316 break;
317 }
318 return info;
319 }
321 @Override
322 public boolean performAction (int virtualViewId, int action, Bundle arguments) {
323 if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
324 // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
325 // When accessibility focus is requested on one of its siblings we move the virtual cursor
326 // either forward or backward depending on which sibling was selected.
328 switch (virtualViewId) {
329 case VIRTUAL_CURSOR_PREVIOUS:
330 GeckoAppShell.
331 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:PreviousObject", null));
332 return true;
333 case VIRTUAL_CURSOR_NEXT:
334 GeckoAppShell.
335 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:NextObject", null));
336 return true;
337 default:
338 break;
339 }
340 } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
341 GeckoAppShell.
342 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", null));
343 return true;
344 } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
345 GeckoAppShell.
346 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:LongPress", null));
347 return true;
348 } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
349 virtualViewId == VIRTUAL_CURSOR_POSITION) {
350 // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
351 // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit
352 int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
353 if (granularity < 0) {
354 int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
355 JSONObject activationData = new JSONObject();
356 try {
357 activationData.put("keyIndex", keyIndex);
358 } catch (JSONException e) {
359 return true;
360 }
361 GeckoAppShell.
362 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", activationData.toString()));
363 } else {
364 JSONObject movementData = new JSONObject();
365 try {
366 movementData.put("direction", "Next");
367 movementData.put("granularity", granularity);
368 } catch (JSONException e) {
369 return true;
370 }
371 GeckoAppShell.
372 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:MoveByGranularity", movementData.toString()));
373 }
374 return true;
375 } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
376 virtualViewId == VIRTUAL_CURSOR_POSITION) {
377 JSONObject movementData = new JSONObject();
378 try {
379 movementData.put("direction", "Previous");
380 movementData.put("granularity", arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT));
381 } catch (JSONException e) {
382 return true;
383 }
384 GeckoAppShell.
385 sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:MoveByGranularity", movementData.toString()));
386 return true;
387 }
388 return host.performAccessibilityAction(action, arguments);
389 }
390 };
392 return mAccessibilityNodeProvider;
393 }
394 }
395 }