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