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