Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2011 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.content.Context;
     20 import android.os.SystemClock;
     21 import android.support.v4.view.AccessibilityDelegateCompat;
     22 import android.support.v4.view.ViewCompat;
     23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     24 import android.util.Log;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.view.ViewParent;
     28 import android.view.accessibility.AccessibilityEvent;
     29 
     30 import com.android.inputmethod.keyboard.Key;
     31 import com.android.inputmethod.keyboard.KeyDetector;
     32 import com.android.inputmethod.keyboard.Keyboard;
     33 import com.android.inputmethod.keyboard.KeyboardView;
     34 
     35 /**
     36  * This class represents a delegate that can be registered in a class that extends
     37  * {@link KeyboardView} to enhance accessibility support via composition rather via inheritance.
     38  *
     39  * To implement accessibility mode, the target keyboard view has to:<p>
     40  * - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
     41  * - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
     42  *
     43  * @param  The keyboard view class type.
     44  */
     45 public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
     46         extends AccessibilityDelegateCompat {
     47     private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
     48     protected static final boolean DEBUG_HOVER = false;
     49 
     50     protected final KV mKeyboardView;
     51     protected final KeyDetector mKeyDetector;
     52     private Keyboard mKeyboard;
     53     private KeyboardAccessibilityNodeProvider<KV> mAccessibilityNodeProvider;
     54     private Key mLastHoverKey;
     55 
     56     public static final int HOVER_EVENT_POINTER_ID = 0;
     57 
     58     public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
     59         super();
     60         mKeyboardView = keyboardView;
     61         mKeyDetector = keyDetector;
     62 
     63         // Ensure that the view has an accessibility delegate.
     64         ViewCompat.setAccessibilityDelegate(keyboardView, this);
     65     }
     66 
     67     /**
     68      * Called when the keyboard layout changes.
     69      * <p>
     70      * <b>Note:</b> This method will be called even if accessibility is not
     71      * enabled.
     72      * @param keyboard The keyboard that is being set to the wrapping view.
     73      */
     74     public void setKeyboard(final Keyboard keyboard) {
     75         if (keyboard == null) {
     76             return;
     77         }
     78         if (mAccessibilityNodeProvider != null) {
     79             mAccessibilityNodeProvider.setKeyboard(keyboard);
     80         }
     81         mKeyboard = keyboard;
     82     }
     83 
     84     protected final Keyboard getKeyboard() {
     85         return mKeyboard;
     86     }
     87 
     88     protected final void setLastHoverKey(final Key key) {
     89         mLastHoverKey = key;
     90     }
     91 
     92     protected final Key getLastHoverKey() {
     93         return mLastHoverKey;
     94     }
     95 
     96     /**
     97      * Sends a window state change event with the specified string resource id.
     98      *
     99      * @param resId The string resource id of the text to send with the event.
    100      */
    101     protected void sendWindowStateChanged(final int resId) {
    102         if (resId == 0) {
    103             return;
    104         }
    105         final Context context = mKeyboardView.getContext();
    106         sendWindowStateChanged(context.getString(resId));
    107     }
    108 
    109     /**
    110      * Sends a window state change event with the specified text.
    111      *
    112      * @param text The text to send with the event.
    113      */
    114     protected void sendWindowStateChanged(final String text) {
    115         final AccessibilityEvent stateChange = AccessibilityEvent.obtain(
    116                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    117         mKeyboardView.onInitializeAccessibilityEvent(stateChange);
    118         stateChange.getText().add(text);
    119         stateChange.setContentDescription(null);
    120 
    121         final ViewParent parent = mKeyboardView.getParent();
    122         if (parent != null) {
    123             parent.requestSendAccessibilityEvent(mKeyboardView, stateChange);
    124         }
    125     }
    126 
    127     /**
    128      * Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
    129      * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
    130      * node hierarchy provider.
    131      *
    132      * @param host The host view for the provider.
    133      * @return The accessibility node provider for the current keyboard.
    134      */
    135     @Override
    136     public KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider(final View host) {
    137         return getAccessibilityNodeProvider();
    138     }
    139 
    140     /**
    141      * @return A lazily-instantiated node provider for this view delegate.
    142      */
    143     protected KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider() {
    144         // Instantiate the provide only when requested. Since the system
    145         // will call this method multiple times it is a good practice to
    146         // cache the provider instance.
    147         if (mAccessibilityNodeProvider == null) {
    148             mAccessibilityNodeProvider =
    149                     new KeyboardAccessibilityNodeProvider<>(mKeyboardView, this);
    150         }
    151         return mAccessibilityNodeProvider;
    152     }
    153 
    154     /**
    155      * Get a key that a hover event is on.
    156      *
    157      * @param event The hover event.
    158      * @return key The key that the <code>event</code> is on.
    159      */
    160     protected final Key getHoverKeyOf(final MotionEvent event) {
    161         final int actionIndex = event.getActionIndex();
    162         final int x = (int)event.getX(actionIndex);
    163         final int y = (int)event.getY(actionIndex);
    164         return mKeyDetector.detectHitKey(x, y);
    165     }
    166 
    167     /**
    168      * Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
    169      *
    170      * @param event The hover event.
    171      * @return {@code true} if the event is handled.
    172      */
    173     public boolean onHoverEvent(final MotionEvent event) {
    174         switch (event.getActionMasked()) {
    175         case MotionEvent.ACTION_HOVER_ENTER:
    176             onHoverEnter(event);
    177             break;
    178         case MotionEvent.ACTION_HOVER_MOVE:
    179             onHoverMove(event);
    180             break;
    181         case MotionEvent.ACTION_HOVER_EXIT:
    182             onHoverExit(event);
    183             break;
    184         default:
    185             Log.w(getClass().getSimpleName(), "Unknown hover event: " + event);
    186             break;
    187         }
    188         return true;
    189     }
    190 
    191     /**
    192      * Process {@link MotionEvent#ACTION_HOVER_ENTER} event.
    193      *
    194      * @param event A hover enter event.
    195      */
    196     protected void onHoverEnter(final MotionEvent event) {
    197         final Key key = getHoverKeyOf(event);
    198         if (DEBUG_HOVER) {
    199             Log.d(TAG, "onHoverEnter: key=" + key);
    200         }
    201         if (key != null) {
    202             onHoverEnterTo(key);
    203         }
    204         setLastHoverKey(key);
    205     }
    206 
    207     /**
    208      * Process {@link MotionEvent#ACTION_HOVER_MOVE} event.
    209      *
    210      * @param event A hover move event.
    211      */
    212     protected void onHoverMove(final MotionEvent event) {
    213         final Key lastKey = getLastHoverKey();
    214         final Key key = getHoverKeyOf(event);
    215         if (key != lastKey) {
    216             if (lastKey != null) {
    217                 onHoverExitFrom(lastKey);
    218             }
    219             if (key != null) {
    220                 onHoverEnterTo(key);
    221             }
    222         }
    223         if (key != null) {
    224             onHoverMoveWithin(key);
    225         }
    226         setLastHoverKey(key);
    227     }
    228 
    229     /**
    230      * Process {@link MotionEvent#ACTION_HOVER_EXIT} event.
    231      *
    232      * @param event A hover exit event.
    233      */
    234     protected void onHoverExit(final MotionEvent event) {
    235         final Key lastKey = getLastHoverKey();
    236         if (DEBUG_HOVER) {
    237             Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
    238         }
    239         if (lastKey != null) {
    240             onHoverExitFrom(lastKey);
    241         }
    242         final Key key = getHoverKeyOf(event);
    243         // Make sure we're not getting an EXIT event because the user slid
    244         // off the keyboard area, then force a key press.
    245         if (key != null) {
    246             performClickOn(key);
    247             onHoverExitFrom(key);
    248         }
    249         setLastHoverKey(null);
    250     }
    251 
    252     /**
    253      * Perform click on a key.
    254      *
    255      * @param key A key to be registered.
    256      */
    257     public void performClickOn(final Key key) {
    258         if (DEBUG_HOVER) {
    259             Log.d(TAG, "performClickOn: key=" + key);
    260         }
    261         simulateTouchEvent(MotionEvent.ACTION_DOWN, key);
    262         simulateTouchEvent(MotionEvent.ACTION_UP, key);
    263     }
    264 
    265     /**
    266      * Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}.
    267      *
    268      * @param touchAction The action of the synthesizing touch event.
    269      * @param key The key that a synthesized touch event is on.
    270      */
    271     private void simulateTouchEvent(final int touchAction, final Key key) {
    272         final int x = key.getHitBox().centerX();
    273         final int y = key.getHitBox().centerY();
    274         final long eventTime = SystemClock.uptimeMillis();
    275         final MotionEvent touchEvent = MotionEvent.obtain(
    276                 eventTime, eventTime, touchAction, x, y, 0 /* metaState */);
    277         mKeyboardView.onTouchEvent(touchEvent);
    278         touchEvent.recycle();
    279     }
    280 
    281     /**
    282      * Handles a hover enter event on a key.
    283      *
    284      * @param key The currently hovered key.
    285      */
    286     protected void onHoverEnterTo(final Key key) {
    287         if (DEBUG_HOVER) {
    288             Log.d(TAG, "onHoverEnterTo: key=" + key);
    289         }
    290         key.onPressed();
    291         mKeyboardView.invalidateKey(key);
    292         final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
    293         provider.onHoverEnterTo(key);
    294         provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
    295     }
    296 
    297     /**
    298      * Handles a hover move event on a key.
    299      *
    300      * @param key The currently hovered key.
    301      */
    302     protected void onHoverMoveWithin(final Key key) { }
    303 
    304     /**
    305      * Handles a hover exit event on a key.
    306      *
    307      * @param key The currently hovered key.
    308      */
    309     protected void onHoverExitFrom(final Key key) {
    310         if (DEBUG_HOVER) {
    311             Log.d(TAG, "onHoverExitFrom: key=" + key);
    312         }
    313         key.onReleased();
    314         mKeyboardView.invalidateKey(key);
    315         final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
    316         provider.onHoverExitFrom(key);
    317     }
    318 
    319     /**
    320      * Perform long click on a key.
    321      *
    322      * @param key A key to be long pressed on.
    323      */
    324     public void performLongClickOn(final Key key) {
    325         // A extended class should override this method to implement long press.
    326     }
    327 }
    328