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"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.accessibility;
     18 
     19 import android.content.Context;
     20 import android.inputmethodservice.InputMethodService;
     21 import android.support.v4.view.AccessibilityDelegateCompat;
     22 import android.support.v4.view.ViewCompat;
     23 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 
     28 import com.android.inputmethod.keyboard.Key;
     29 import com.android.inputmethod.keyboard.Keyboard;
     30 import com.android.inputmethod.keyboard.KeyboardId;
     31 import com.android.inputmethod.keyboard.MainKeyboardView;
     32 import com.android.inputmethod.keyboard.PointerTracker;
     33 import com.android.inputmethod.latin.R;
     34 
     35 public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
     36     private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();
     37 
     38     private InputMethodService mInputMethod;
     39     private MainKeyboardView mView;
     40     private AccessibilityEntityProvider mAccessibilityNodeProvider;
     41 
     42     private Key mLastHoverKey = null;
     43 
     44     /**
     45      * Inset in pixels to look for keys when the user's finger exits the
     46      * keyboard area.
     47      */
     48     private int mEdgeSlop;
     49 
     50     public static void init(InputMethodService inputMethod) {
     51         sInstance.initInternal(inputMethod);
     52     }
     53 
     54     public static AccessibleKeyboardViewProxy getInstance() {
     55         return sInstance;
     56     }
     57 
     58     private AccessibleKeyboardViewProxy() {
     59         // Not publicly instantiable.
     60     }
     61 
     62     private void initInternal(InputMethodService inputMethod) {
     63         mInputMethod = inputMethod;
     64         mEdgeSlop = inputMethod.getResources().getDimensionPixelSize(
     65                 R.dimen.accessibility_edge_slop);
     66     }
     67 
     68     /**
     69      * Sets the view wrapped by this proxy.
     70      *
     71      * @param view The view to wrap.
     72      */
     73     public void setView(MainKeyboardView view) {
     74         if (view == null) {
     75             // Ignore null views.
     76             return;
     77         }
     78 
     79         mView = view;
     80 
     81         // Ensure that the view has an accessibility delegate.
     82         ViewCompat.setAccessibilityDelegate(view, this);
     83 
     84         if (mAccessibilityNodeProvider != null) {
     85             mAccessibilityNodeProvider.setView(view);
     86         }
     87     }
     88 
     89     public void setKeyboard(Keyboard keyboard) {
     90         if (mAccessibilityNodeProvider != null) {
     91             mAccessibilityNodeProvider.setKeyboard(keyboard);
     92         }
     93     }
     94 
     95     /**
     96      * Proxy method for View.getAccessibilityNodeProvider(). This method is
     97      * called in SDK version 15 and higher to obtain the virtual node hierarchy
     98      * provider.
     99      *
    100      * @return The accessibility node provider for the current keyboard.
    101      */
    102     @Override
    103     public AccessibilityEntityProvider getAccessibilityNodeProvider(View host) {
    104         return getAccessibilityNodeProvider();
    105     }
    106 
    107     /**
    108      * Intercepts touch events before dispatch when touch exploration is turned
    109      * on in ICS and higher.
    110      *
    111      * @param event The motion event being dispatched.
    112      * @return {@code true} if the event is handled
    113      */
    114     public boolean dispatchTouchEvent(MotionEvent event) {
    115         // To avoid accidental key presses during touch exploration, always drop
    116         // touch events generated by the user.
    117         return false;
    118     }
    119 
    120     /**
    121      * Receives hover events when touch exploration is turned on in SDK versions
    122      * ICS and higher.
    123      *
    124      * @param event The hover event.
    125      * @return {@code true} if the event is handled
    126      */
    127     public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) {
    128         final int x = (int) event.getX();
    129         final int y = (int) event.getY();
    130         final Key previousKey = mLastHoverKey;
    131         final Key key;
    132 
    133         if (pointInView(x, y)) {
    134             key = tracker.getKeyOn(x, y);
    135         } else {
    136             key = null;
    137         }
    138 
    139         mLastHoverKey = key;
    140 
    141         switch (event.getAction()) {
    142         case MotionEvent.ACTION_HOVER_EXIT:
    143             // Make sure we're not getting an EXIT event because the user slid
    144             // off the keyboard area, then force a key press.
    145             if (key != null) {
    146                 getAccessibilityNodeProvider().simulateKeyPress(key);
    147             }
    148             //$FALL-THROUGH$
    149         case MotionEvent.ACTION_HOVER_ENTER:
    150             return onHoverKey(key, event);
    151         case MotionEvent.ACTION_HOVER_MOVE:
    152             if (key != previousKey) {
    153                 return onTransitionKey(key, previousKey, event);
    154             } else {
    155                 return onHoverKey(key, event);
    156             }
    157         }
    158 
    159         return false;
    160     }
    161 
    162     /**
    163      * @return A lazily-instantiated node provider for this view proxy.
    164      */
    165     private AccessibilityEntityProvider getAccessibilityNodeProvider() {
    166         // Instantiate the provide only when requested. Since the system
    167         // will call this method multiple times it is a good practice to
    168         // cache the provider instance.
    169         if (mAccessibilityNodeProvider == null) {
    170             mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod);
    171         }
    172         return mAccessibilityNodeProvider;
    173     }
    174 
    175     /**
    176      * Utility method to determine whether the given point, in local
    177      * coordinates, is inside the view, where the area of the view is contracted
    178      * by the edge slop factor.
    179      *
    180      * @param localX The local x-coordinate.
    181      * @param localY The local y-coordinate.
    182      */
    183     private boolean pointInView(int localX, int localY) {
    184         return (localX >= mEdgeSlop) && (localY >= mEdgeSlop)
    185                 && (localX < (mView.getWidth() - mEdgeSlop))
    186                 && (localY < (mView.getHeight() - mEdgeSlop));
    187     }
    188 
    189     /**
    190      * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT
    191      * on the previous key, a HOVER_ENTER on the current key, and a HOVER_MOVE
    192      * on the current key.
    193      *
    194      * @param currentKey The currently hovered key.
    195      * @param previousKey The previously hovered key.
    196      * @param event The event that triggered the transition.
    197      * @return {@code true} if the event was handled.
    198      */
    199     private boolean onTransitionKey(Key currentKey, Key previousKey, MotionEvent event) {
    200         final int savedAction = event.getAction();
    201 
    202         event.setAction(MotionEvent.ACTION_HOVER_EXIT);
    203         onHoverKey(previousKey, event);
    204 
    205         event.setAction(MotionEvent.ACTION_HOVER_ENTER);
    206         onHoverKey(currentKey, event);
    207 
    208         event.setAction(MotionEvent.ACTION_HOVER_MOVE);
    209         final boolean handled = onHoverKey(currentKey, event);
    210 
    211         event.setAction(savedAction);
    212 
    213         return handled;
    214     }
    215 
    216     /**
    217      * Handles a hover event on a key. If {@link Key} extended View, this would
    218      * be analogous to calling View.onHoverEvent(MotionEvent).
    219      *
    220      * @param key The currently hovered key.
    221      * @param event The hover event.
    222      * @return {@code true} if the event was handled.
    223      */
    224     private boolean onHoverKey(Key key, MotionEvent event) {
    225         // Null keys can't receive events.
    226         if (key == null) {
    227             return false;
    228         }
    229 
    230         final AccessibilityEntityProvider provider = getAccessibilityNodeProvider();
    231 
    232         switch (event.getAction()) {
    233         case MotionEvent.ACTION_HOVER_ENTER:
    234             provider.sendAccessibilityEventForKey(
    235                     key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
    236             provider.performActionForKey(
    237                     key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
    238             break;
    239         case MotionEvent.ACTION_HOVER_EXIT:
    240             provider.sendAccessibilityEventForKey(
    241                     key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
    242             break;
    243         }
    244 
    245         return true;
    246     }
    247 
    248     /**
    249      * Notifies the user of changes in the keyboard shift state.
    250      */
    251     public void notifyShiftState() {
    252         final Keyboard keyboard = mView.getKeyboard();
    253         final KeyboardId keyboardId = keyboard.mId;
    254         final int elementId = keyboardId.mElementId;
    255         final Context context = mView.getContext();
    256         final CharSequence text;
    257 
    258         switch (elementId) {
    259         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
    260         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
    261             text = context.getText(R.string.spoken_description_shiftmode_locked);
    262             break;
    263         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
    264         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
    265         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
    266             text = context.getText(R.string.spoken_description_shiftmode_on);
    267             break;
    268         default:
    269             text = context.getText(R.string.spoken_description_shiftmode_off);
    270         }
    271 
    272         AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
    273     }
    274 
    275     /**
    276      * Notifies the user of changes in the keyboard symbols state.
    277      */
    278     public void notifySymbolsState() {
    279         final Keyboard keyboard = mView.getKeyboard();
    280         final Context context = mView.getContext();
    281         final KeyboardId keyboardId = keyboard.mId;
    282         final int elementId = keyboardId.mElementId;
    283         final int resId;
    284 
    285         switch (elementId) {
    286         case KeyboardId.ELEMENT_ALPHABET:
    287         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
    288         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
    289         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
    290         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
    291             resId = R.string.spoken_description_mode_alpha;
    292             break;
    293         case KeyboardId.ELEMENT_SYMBOLS:
    294         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
    295             resId = R.string.spoken_description_mode_symbol;
    296             break;
    297         case KeyboardId.ELEMENT_PHONE:
    298             resId = R.string.spoken_description_mode_phone;
    299             break;
    300         case KeyboardId.ELEMENT_PHONE_SYMBOLS:
    301             resId = R.string.spoken_description_mode_phone_shift;
    302             break;
    303         default:
    304             resId = -1;
    305         }
    306 
    307         if (resId < 0) {
    308             return;
    309         }
    310 
    311         final String text = context.getString(resId);
    312         AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
    313     }
    314 }
    315