Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.accessibility;
     18 
     19 import android.graphics.Rect;
     20 import android.inputmethodservice.InputMethodService;
     21 import android.os.Bundle;
     22 import android.os.SystemClock;
     23 import android.support.v4.view.ViewCompat;
     24 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     25 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     26 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
     27 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     28 import android.util.Log;
     29 import android.util.SparseArray;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.accessibility.AccessibilityEvent;
     33 import android.view.inputmethod.EditorInfo;
     34 
     35 import com.android.inputmethod.keyboard.Key;
     36 import com.android.inputmethod.keyboard.Keyboard;
     37 import com.android.inputmethod.keyboard.KeyboardView;
     38 import com.android.inputmethod.latin.settings.Settings;
     39 import com.android.inputmethod.latin.settings.SettingsValues;
     40 import com.android.inputmethod.latin.utils.CollectionUtils;
     41 import com.android.inputmethod.latin.utils.CoordinateUtils;
     42 
     43 /**
     44  * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
     45  * {@link AccessibilityEvent}s for individual {@link Key}s.
     46  * <p>
     47  * A virtual sub-tree is composed of imaginary {@link View}s that are reported
     48  * as a part of the view hierarchy for accessibility purposes. This enables
     49  * custom views that draw complex content to report them selves as a tree of
     50  * virtual views, thus conveying their logical structure.
     51  * </p>
     52  */
     53 public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
     54     private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
     55     private static final int UNDEFINED = Integer.MIN_VALUE;
     56 
     57     private final InputMethodService mInputMethodService;
     58     private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
     59     private final AccessibilityUtils mAccessibilityUtils;
     60 
     61     /** A map of integer IDs to {@link Key}s. */
     62     private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray();
     63 
     64     /** Temporary rect used to calculate in-screen bounds. */
     65     private final Rect mTempBoundsInScreen = new Rect();
     66 
     67     /** The parent view's cached on-screen location. */
     68     private final int[] mParentLocation = CoordinateUtils.newInstance();
     69 
     70     /** The virtual view identifier for the focused node. */
     71     private int mAccessibilityFocusedView = UNDEFINED;
     72 
     73     /** The current keyboard view. */
     74     private KeyboardView mKeyboardView;
     75 
     76     public AccessibilityEntityProvider(final KeyboardView keyboardView,
     77             final InputMethodService inputMethod) {
     78         mInputMethodService = inputMethod;
     79         mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
     80         mAccessibilityUtils = AccessibilityUtils.getInstance();
     81         setView(keyboardView);
     82     }
     83 
     84     /**
     85      * Sets the keyboard view represented by this node provider.
     86      *
     87      * @param keyboardView The keyboard view to represent.
     88      */
     89     public void setView(final KeyboardView keyboardView) {
     90         mKeyboardView = keyboardView;
     91         updateParentLocation();
     92 
     93         // Since this class is constructed lazily, we might not get a subsequent
     94         // call to setKeyboard() and therefore need to call it now.
     95         setKeyboard();
     96     }
     97 
     98     /**
     99      * Sets the keyboard represented by this node provider.
    100      */
    101     public void setKeyboard() {
    102         assignVirtualViewIds();
    103     }
    104 
    105     /**
    106      * Creates and populates an {@link AccessibilityEvent} for the specified key
    107      * and event type.
    108      *
    109      * @param key A key on the host keyboard view.
    110      * @param eventType The event type to create.
    111      * @return A populated {@link AccessibilityEvent} for the key.
    112      * @see AccessibilityEvent
    113      */
    114     public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
    115         final int virtualViewId = generateVirtualViewIdForKey(key);
    116         final String keyDescription = getKeyDescription(key);
    117         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    118         event.setPackageName(mKeyboardView.getContext().getPackageName());
    119         event.setClassName(key.getClass().getName());
    120         event.setContentDescription(keyDescription);
    121         event.setEnabled(true);
    122         final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
    123         record.setSource(mKeyboardView, virtualViewId);
    124         return event;
    125     }
    126 
    127     /**
    128      * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
    129      * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
    130      * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
    131      * <p>
    132      * A virtual descendant is an imaginary View that is reported as a part of
    133      * the view hierarchy for accessibility purposes. This enables custom views
    134      * that draw complex content to report them selves as a tree of virtual
    135      * views, thus conveying their logical structure.
    136      * </p>
    137      * <p>
    138      * The implementer is responsible for obtaining an accessibility node info
    139      * from the pool of reusable instances and setting the desired properties of
    140      * the node info before returning it.
    141      * </p>
    142      *
    143      * @param virtualViewId A client defined virtual view id.
    144      * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
    145      * View.
    146      * @see AccessibilityNodeInfoCompat
    147      */
    148     @Override
    149     public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
    150         if (virtualViewId == UNDEFINED) {
    151             return null;
    152         }
    153         if (virtualViewId == View.NO_ID) {
    154             // We are requested to create an AccessibilityNodeInfo describing
    155             // this View, i.e. the root of the virtual sub-tree.
    156             final AccessibilityNodeInfoCompat rootInfo =
    157                     AccessibilityNodeInfoCompat.obtain(mKeyboardView);
    158             ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
    159 
    160             // Add the virtual children of the root View.
    161             final Keyboard keyboard = mKeyboardView.getKeyboard();
    162             final Key[] keys = keyboard.getKeys();
    163             for (Key key : keys) {
    164                 final int childVirtualViewId = generateVirtualViewIdForKey(key);
    165                 rootInfo.addChild(mKeyboardView, childVirtualViewId);
    166             }
    167             return rootInfo;
    168         }
    169 
    170         // Find the view that corresponds to the given id.
    171         final Key key = mVirtualViewIdToKey.get(virtualViewId);
    172         if (key == null) {
    173             Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
    174             return null;
    175         }
    176         final String keyDescription = getKeyDescription(key);
    177         final Rect boundsInParent = key.getHitBox();
    178 
    179         // Calculate the key's in-screen bounds.
    180         mTempBoundsInScreen.set(boundsInParent);
    181         mTempBoundsInScreen.offset(
    182                 CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
    183         final Rect boundsInScreen = mTempBoundsInScreen;
    184 
    185         // Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
    186         final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
    187         info.setPackageName(mKeyboardView.getContext().getPackageName());
    188         info.setClassName(key.getClass().getName());
    189         info.setContentDescription(keyDescription);
    190         info.setBoundsInParent(boundsInParent);
    191         info.setBoundsInScreen(boundsInScreen);
    192         info.setParent(mKeyboardView);
    193         info.setSource(mKeyboardView, virtualViewId);
    194         info.setBoundsInScreen(boundsInScreen);
    195         info.setEnabled(true);
    196         info.setVisibleToUser(true);
    197 
    198         if (mAccessibilityFocusedView == virtualViewId) {
    199             info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    200         } else {
    201             info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
    202         }
    203         return info;
    204     }
    205 
    206     /**
    207      * Simulates a key press by injecting touch events into the keyboard view.
    208      * This avoids the complexity of trackers and listeners within the keyboard.
    209      *
    210      * @param key The key to press.
    211      */
    212     void simulateKeyPress(final Key key) {
    213         final int x = key.getHitBox().centerX();
    214         final int y = key.getHitBox().centerY();
    215         final long downTime = SystemClock.uptimeMillis();
    216         final MotionEvent downEvent = MotionEvent.obtain(
    217                 downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
    218         final MotionEvent upEvent = MotionEvent.obtain(
    219                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
    220 
    221         mKeyboardView.onTouchEvent(downEvent);
    222         mKeyboardView.onTouchEvent(upEvent);
    223         downEvent.recycle();
    224         upEvent.recycle();
    225     }
    226 
    227     @Override
    228     public boolean performAction(final int virtualViewId, final int action,
    229             final Bundle arguments) {
    230         final Key key = mVirtualViewIdToKey.get(virtualViewId);
    231         if (key == null) {
    232             return false;
    233         }
    234         return performActionForKey(key, action, arguments);
    235     }
    236 
    237     /**
    238      * Performs the specified accessibility action for the given key.
    239      *
    240      * @param key The on which to perform the action.
    241      * @param action The action to perform.
    242      * @param arguments The action's arguments.
    243      * @return The result of performing the action, or false if the action is not supported.
    244      */
    245     boolean performActionForKey(final Key key, final int action, final Bundle arguments) {
    246         final int virtualViewId = generateVirtualViewIdForKey(key);
    247 
    248         switch (action) {
    249         case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
    250             if (mAccessibilityFocusedView == virtualViewId) {
    251                 return false;
    252             }
    253             mAccessibilityFocusedView = virtualViewId;
    254             sendAccessibilityEventForKey(
    255                     key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    256             return true;
    257         case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    258             if (mAccessibilityFocusedView != virtualViewId) {
    259                 return false;
    260             }
    261             mAccessibilityFocusedView = UNDEFINED;
    262             sendAccessibilityEventForKey(
    263                     key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    264             return true;
    265         default:
    266             return false;
    267         }
    268     }
    269 
    270     /**
    271      * Sends an accessibility event for the given {@link Key}.
    272      *
    273      * @param key The key that's sending the event.
    274      * @param eventType The type of event to send.
    275      */
    276     void sendAccessibilityEventForKey(final Key key, final int eventType) {
    277         final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
    278         mAccessibilityUtils.requestSendAccessibilityEvent(event);
    279     }
    280 
    281     /**
    282      * Returns the context-specific description for a {@link Key}.
    283      *
    284      * @param key The key to describe.
    285      * @return The context-specific description of the key.
    286      */
    287     private String getKeyDescription(final Key key) {
    288         final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
    289         final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
    290         final SettingsValues currentSettings = Settings.getInstance().getCurrent();
    291         final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
    292                 mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure);
    293         if (currentSettings.isWordSeparator(key.getCode())) {
    294             return mAccessibilityUtils.getAutoCorrectionDescription(
    295                     keyCodeDescription, shouldObscure);
    296         } else {
    297             return keyCodeDescription;
    298         }
    299     }
    300 
    301     /**
    302      * Assigns virtual view IDs to keyboard keys and populates the related maps.
    303      */
    304     private void assignVirtualViewIds() {
    305         final Keyboard keyboard = mKeyboardView.getKeyboard();
    306         if (keyboard == null) {
    307             return;
    308         }
    309         mVirtualViewIdToKey.clear();
    310 
    311         final Key[] keys = keyboard.getKeys();
    312         for (Key key : keys) {
    313             final int virtualViewId = generateVirtualViewIdForKey(key);
    314             mVirtualViewIdToKey.put(virtualViewId, key);
    315         }
    316     }
    317 
    318     /**
    319      * Updates the parent's on-screen location.
    320      */
    321     private void updateParentLocation() {
    322         mKeyboardView.getLocationOnScreen(mParentLocation);
    323     }
    324 
    325     /**
    326      * Generates a virtual view identifier for the given key. Returned
    327      * identifiers are valid until the next global layout state change.
    328      *
    329      * @param key The key to identify.
    330      * @return A virtual view identifier.
    331      */
    332     private static int generateVirtualViewIdForKey(final Key key) {
    333         // The key x- and y-coordinates are stable between layout changes.
    334         // Generate an identifier by bit-shifting the x-coordinate to the
    335         // left-half of the integer and OR'ing with the y-coordinate.
    336         return ((0xFFFF & key.getX()) << (Integer.SIZE / 2)) | (0xFFFF & key.getY());
    337     }
    338 }
    339