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.os.Bundle;
     21 import android.support.v4.view.ViewCompat;
     22 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     24 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
     25 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     26 import android.util.Log;
     27 import android.view.View;
     28 import android.view.accessibility.AccessibilityEvent;
     29 import android.view.inputmethod.EditorInfo;
     30 
     31 import com.android.inputmethod.keyboard.Key;
     32 import com.android.inputmethod.keyboard.Keyboard;
     33 import com.android.inputmethod.keyboard.KeyboardView;
     34 import com.android.inputmethod.latin.common.CoordinateUtils;
     35 import com.android.inputmethod.latin.settings.Settings;
     36 import com.android.inputmethod.latin.settings.SettingsValues;
     37 
     38 import java.util.List;
     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 final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView>
     51         extends AccessibilityNodeProviderCompat {
     52     private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName();
     53 
     54     // From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
     55     private static final int UNDEFINED = Integer.MAX_VALUE;
     56 
     57     private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
     58     private final AccessibilityUtils mAccessibilityUtils;
     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 = CoordinateUtils.newInstance();
     65 
     66     /** The virtual view identifier for the focused node. */
     67     private int mAccessibilityFocusedView = UNDEFINED;
     68 
     69     /** The virtual view identifier for the hovering node. */
     70     private int mHoveringNodeId = UNDEFINED;
     71 
     72     /** The keyboard view to provide an accessibility node info. */
     73     private final KV mKeyboardView;
     74     /** The accessibility delegate. */
     75     private final KeyboardAccessibilityDelegate<KV> mDelegate;
     76 
     77     /** The current keyboard. */
     78     private Keyboard mKeyboard;
     79 
     80     public KeyboardAccessibilityNodeProvider(final KV keyboardView,
     81             final KeyboardAccessibilityDelegate<KV> delegate) {
     82         super();
     83         mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
     84         mAccessibilityUtils = AccessibilityUtils.getInstance();
     85         mKeyboardView = keyboardView;
     86         mDelegate = delegate;
     87 
     88         // Since this class is constructed lazily, we might not get a subsequent
     89         // call to setKeyboard() and therefore need to call it now.
     90         setKeyboard(keyboardView.getKeyboard());
     91     }
     92 
     93     /**
     94      * Sets the keyboard represented by this node provider.
     95      *
     96      * @param keyboard The keyboard that is being set to the keyboard view.
     97      */
     98     public void setKeyboard(final Keyboard keyboard) {
     99         mKeyboard = keyboard;
    100     }
    101 
    102     private Key getKeyOf(final int virtualViewId) {
    103         if (mKeyboard == null) {
    104             return null;
    105         }
    106         final List<Key> sortedKeys = mKeyboard.getSortedKeys();
    107         // Use a virtual view id as an index of the sorted keys list.
    108         if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
    109             return sortedKeys.get(virtualViewId);
    110         }
    111         return null;
    112     }
    113 
    114     private int getVirtualViewIdOf(final Key key) {
    115         if (mKeyboard == null) {
    116             return View.NO_ID;
    117         }
    118         final List<Key> sortedKeys = mKeyboard.getSortedKeys();
    119         final int size = sortedKeys.size();
    120         for (int index = 0; index < size; index++) {
    121             if (sortedKeys.get(index) == key) {
    122                 // Use an index of the sorted keys list as a virtual view id.
    123                 return index;
    124             }
    125         }
    126         return View.NO_ID;
    127     }
    128 
    129     /**
    130      * Creates and populates an {@link AccessibilityEvent} for the specified key
    131      * and event type.
    132      *
    133      * @param key A key on the host keyboard view.
    134      * @param eventType The event type to create.
    135      * @return A populated {@link AccessibilityEvent} for the key.
    136      * @see AccessibilityEvent
    137      */
    138     public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
    139         final int virtualViewId = getVirtualViewIdOf(key);
    140         final String keyDescription = getKeyDescription(key);
    141         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    142         event.setPackageName(mKeyboardView.getContext().getPackageName());
    143         event.setClassName(key.getClass().getName());
    144         event.setContentDescription(keyDescription);
    145         event.setEnabled(true);
    146         final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
    147         record.setSource(mKeyboardView, virtualViewId);
    148         return event;
    149     }
    150 
    151     public void onHoverEnterTo(final Key key) {
    152         final int id = getVirtualViewIdOf(key);
    153         if (id == View.NO_ID) {
    154             return;
    155         }
    156         // Start hovering on the key. Because our accessibility model is lift-to-type, we should
    157         // report the node info without click and long click actions to avoid unnecessary
    158         // announcements.
    159         mHoveringNodeId = id;
    160         // Invalidate the node info of the key.
    161         sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
    162         sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
    163     }
    164 
    165     public void onHoverExitFrom(final Key key) {
    166         mHoveringNodeId = UNDEFINED;
    167         // Invalidate the node info of the key to be able to revert the change we have done
    168         // in {@link #onHoverEnterTo(Key)}.
    169         sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
    170         sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
    171     }
    172 
    173     /**
    174      * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
    175      * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
    176      * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
    177      * <p>
    178      * A virtual descendant is an imaginary View that is reported as a part of
    179      * the view hierarchy for accessibility purposes. This enables custom views
    180      * that draw complex content to report them selves as a tree of virtual
    181      * views, thus conveying their logical structure.
    182      * </p>
    183      * <p>
    184      * The implementer is responsible for obtaining an accessibility node info
    185      * from the pool of reusable instances and setting the desired properties of
    186      * the node info before returning it.
    187      * </p>
    188      *
    189      * @param virtualViewId A client defined virtual view id.
    190      * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
    191      * View.
    192      * @see AccessibilityNodeInfoCompat
    193      */
    194     @Override
    195     public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
    196         if (virtualViewId == UNDEFINED) {
    197             return null;
    198         }
    199         if (virtualViewId == View.NO_ID) {
    200             // We are requested to create an AccessibilityNodeInfo describing
    201             // this View, i.e. the root of the virtual sub-tree.
    202             final AccessibilityNodeInfoCompat rootInfo =
    203                     AccessibilityNodeInfoCompat.obtain(mKeyboardView);
    204             ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
    205             updateParentLocation();
    206 
    207             // Add the virtual children of the root View.
    208             final List<Key> sortedKeys = mKeyboard.getSortedKeys();
    209             final int size = sortedKeys.size();
    210             for (int index = 0; index < size; index++) {
    211                 final Key key = sortedKeys.get(index);
    212                 if (key.isSpacer()) {
    213                     continue;
    214                 }
    215                 // Use an index of the sorted keys list as a virtual view id.
    216                 rootInfo.addChild(mKeyboardView, index);
    217             }
    218             return rootInfo;
    219         }
    220 
    221         // Find the key that corresponds to the given virtual view id.
    222         final Key key = getKeyOf(virtualViewId);
    223         if (key == null) {
    224             Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
    225             return null;
    226         }
    227         final String keyDescription = getKeyDescription(key);
    228         final Rect boundsInParent = key.getHitBox();
    229 
    230         // Calculate the key's in-screen bounds.
    231         mTempBoundsInScreen.set(boundsInParent);
    232         mTempBoundsInScreen.offset(
    233                 CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
    234         final Rect boundsInScreen = mTempBoundsInScreen;
    235 
    236         // Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
    237         final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
    238         info.setPackageName(mKeyboardView.getContext().getPackageName());
    239         info.setClassName(key.getClass().getName());
    240         info.setContentDescription(keyDescription);
    241         info.setBoundsInParent(boundsInParent);
    242         info.setBoundsInScreen(boundsInScreen);
    243         info.setParent(mKeyboardView);
    244         info.setSource(mKeyboardView, virtualViewId);
    245         info.setEnabled(key.isEnabled());
    246         info.setVisibleToUser(true);
    247         // Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
    248         // See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
    249         if (virtualViewId != mHoveringNodeId) {
    250             info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    251             if (key.isLongPressEnabled()) {
    252                 info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
    253             }
    254         }
    255 
    256         if (mAccessibilityFocusedView == virtualViewId) {
    257             info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    258         } else {
    259             info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
    260         }
    261         return info;
    262     }
    263 
    264     @Override
    265     public boolean performAction(final int virtualViewId, final int action,
    266             final Bundle arguments) {
    267         final Key key = getKeyOf(virtualViewId);
    268         if (key == null) {
    269             return false;
    270         }
    271         return performActionForKey(key, action);
    272     }
    273 
    274     /**
    275      * Performs the specified accessibility action for the given key.
    276      *
    277      * @param key The on which to perform the action.
    278      * @param action The action to perform.
    279      * @return The result of performing the action, or false if the action is not supported.
    280      */
    281     boolean performActionForKey(final Key key, final int action) {
    282         switch (action) {
    283         case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
    284             mAccessibilityFocusedView = getVirtualViewIdOf(key);
    285             sendAccessibilityEventForKey(
    286                     key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    287             return true;
    288         case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    289             mAccessibilityFocusedView = UNDEFINED;
    290             sendAccessibilityEventForKey(
    291                     key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    292             return true;
    293         case AccessibilityNodeInfoCompat.ACTION_CLICK:
    294             sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED);
    295             mDelegate.performClickOn(key);
    296             return true;
    297         case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
    298             sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
    299             mDelegate.performLongClickOn(key);
    300             return true;
    301         default:
    302             return false;
    303         }
    304     }
    305 
    306     /**
    307      * Sends an accessibility event for the given {@link Key}.
    308      *
    309      * @param key The key that's sending the event.
    310      * @param eventType The type of event to send.
    311      */
    312     void sendAccessibilityEventForKey(final Key key, final int eventType) {
    313         final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
    314         mAccessibilityUtils.requestSendAccessibilityEvent(event);
    315     }
    316 
    317     /**
    318      * Returns the context-specific description for a {@link Key}.
    319      *
    320      * @param key The key to describe.
    321      * @return The context-specific description of the key.
    322      */
    323     private String getKeyDescription(final Key key) {
    324         final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
    325         final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
    326         final SettingsValues currentSettings = Settings.getInstance().getCurrent();
    327         final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
    328                 mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
    329         if (currentSettings.isWordSeparator(key.getCode())) {
    330             return mAccessibilityUtils.getAutoCorrectionDescription(
    331                     keyCodeDescription, shouldObscure);
    332         }
    333         return keyCodeDescription;
    334     }
    335 
    336     /**
    337      * Updates the parent's on-screen location.
    338      */
    339     private void updateParentLocation() {
    340         mKeyboardView.getLocationOnScreen(mParentLocation);
    341     }
    342 }
    343