mobile/android/base/GeckoAccessibility.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

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 }

mercurial