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 
     39 /**
     40  * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
     41  * {@link AccessibilityEvent}s for individual {@link Key}s.
     42  * <p>
     43  * A virtual sub-tree is composed of imaginary {@link View}s that are reported
     44  * as a part of the view hierarchy for accessibility purposes. This enables
     45  * custom views that draw complex content to report them selves as a tree of
     46  * virtual views, thus conveying their logical structure.
     47  * </p>
     48  */
     49 public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
     50     private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
     51     private static final int UNDEFINED = Integer.MIN_VALUE;
     52 
     53     private final InputMethodService mInputMethodService;
     54     private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
     55     private final AccessibilityUtils mAccessibilityUtils;
     56 
     57     /** A map of integer IDs to {@link Key}s. */
     58     private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>();
     59 
     60     /** Temporary rect used to calculate in-screen bounds. */
     61     private final Rect mTempBoundsInScreen = new Rect();
     62 
     63     /** The parent view's cached on-screen location. */
     64     private final int[] mParentLocation = new int[2];
     65 
     66     /** The virtual view identifier for the focused node. */
     67     private int mAccessibilityFocusedView = UNDEFINED;
     68 
     69     /** The current keyboard view. */
     70     private KeyboardView mKeyboardView;
     71 
     72     public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
     73         mInputMethodService = inputMethod;
     74 
     75         mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
     76         mAccessibilityUtils = AccessibilityUtils.getInstance();
     77 
     78         setView(keyboardView);
     79     }
     80 
     81     /**
     82      * Sets the keyboard view represented by this node provider.
     83      *
     84      * @param keyboardView The keyboard view to represent.
     85      */
     86     public void setView(KeyboardView keyboardView) {
     87         mKeyboardView = keyboardView;
     88         updateParentLocation();
     89 
     90         // Since this class is constructed lazily, we might not get a subsequent
     91         // call to setKeyboard() and therefore need to call it now.
     92         setKeyboard(mKeyboardView.getKeyboard());
     93     }
     94 
     95     /**
     96      * Sets the keyboard represented by this node provider.
     97      *
     98      * @param keyboard The keyboard to represent.
     99      */
    100     public void setKeyboard(Keyboard keyboard) {
    101         assignVirtualViewIds();
    102     }
    103 
    104     /**
    105      * Creates and populates an {@link AccessibilityEvent} for the specified key
    106      * and event type.
    107      *
    108      * @param key A key on the host keyboard view.
    109      * @param eventType The event type to create.
    110      * @return A populated {@link AccessibilityEvent} for the key.
    111      * @see AccessibilityEvent
    112      */
    113     public AccessibilityEvent createAccessibilityEvent(Key key, int eventType) {
    114         final int virtualViewId = generateVirtualViewIdForKey(key);
    115         final String keyDescription = getKeyDescription(key);
    116 
    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 
    123         final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
    124         record.setSource(mKeyboardView, virtualViewId);
    125 
    126         return event;
    127     }
    128 
    129     /**
    130      * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
    131      * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
    132      * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
    133      * <p>
    134      * A virtual descendant is an imaginary View that is reported as a part of
    135      * the view hierarchy for accessibility purposes. This enables custom views
    136      * that draw complex content to report them selves as a tree of virtual
    137      * views, thus conveying their logical structure.
    138      * </p>
    139      * <p>
    140      * The implementer is responsible for obtaining an accessibility node info
    141      * from the pool of reusable instances and setting the desired properties of
    142      * the node info before returning it.
    143      * </p>
    144      *
    145      * @param virtualViewId A client defined virtual view id.
    146      * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual
    147      *         descendant or the host View.
    148      * @see AccessibilityNodeInfoCompat
    149      */
    150     @Override
    151     public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
    152         AccessibilityNodeInfoCompat info = null;
    153 
    154         if (virtualViewId == UNDEFINED) {
    155             return null;
    156         } else  if (virtualViewId == View.NO_ID) {
    157             // We are requested to create an AccessibilityNodeInfo describing
    158             // this View, i.e. the root of the virtual sub-tree.
    159             info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
    160             ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, info);
    161 
    162             // Add the virtual children of the root View.
    163             final Keyboard keyboard = mKeyboardView.getKeyboard();
    164             final Key[] keys = keyboard.mKeys;
    165             for (Key key : keys) {
    166                 final int childVirtualViewId = generateVirtualViewIdForKey(key);
    167                 info.addChild(mKeyboardView, childVirtualViewId);
    168             }
    169         } else {
    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 
    177             final String keyDescription = getKeyDescription(key);
    178             final Rect boundsInParent = key.mHitBox;
    179 
    180             // Calculate the key's in-screen bounds.
    181             mTempBoundsInScreen.set(boundsInParent);
    182             mTempBoundsInScreen.offset(mParentLocation[0], mParentLocation[1]);
    183 
    184             final Rect boundsInScreen = mTempBoundsInScreen;
    185 
    186             // Obtain and initialize an AccessibilityNodeInfo with
    187             // information about the virtual view.
    188             info = AccessibilityNodeInfoCompat.obtain();
    189             info.setPackageName(mKeyboardView.getContext().getPackageName());
    190             info.setClassName(key.getClass().getName());
    191             info.setContentDescription(keyDescription);
    192             info.setBoundsInParent(boundsInParent);
    193             info.setBoundsInScreen(boundsInScreen);
    194             info.setParent(mKeyboardView);
    195             info.setSource(mKeyboardView, virtualViewId);
    196             info.setBoundsInScreen(boundsInScreen);
    197             info.setEnabled(true);
    198             info.setClickable(true);
    199             info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    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 
    230     @Override
    231     public boolean performAction(int virtualViewId, int action, Bundle arguments) {
    232         final Key key = mVirtualViewIdToKey.get(virtualViewId);
    233 
    234         if (key == null) {
    235             return false;
    236         }
    237 
    238         return performActionForKey(key, action, arguments);
    239     }
    240 
    241     /**
    242      * Performs the specified accessibility action for the given key.
    243      *
    244      * @param key The on which to perform the action.
    245      * @param action The action to perform.
    246      * @param arguments The action's arguments.
    247      * @return The result of performing the action, or false if the action is
    248      *         not supported.
    249      */
    250     boolean performActionForKey(Key key, int action, Bundle arguments) {
    251         final int virtualViewId = generateVirtualViewIdForKey(key);
    252 
    253         switch (action) {
    254         case AccessibilityNodeInfoCompat.ACTION_CLICK:
    255             simulateKeyPress(key);
    256             return true;
    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