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