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 import android.view.ViewConfiguration; 28 29 import com.android.inputmethod.keyboard.Key; 30 import com.android.inputmethod.keyboard.Keyboard; 31 import com.android.inputmethod.keyboard.KeyboardId; 32 import com.android.inputmethod.keyboard.LatinKeyboardView; 33 import com.android.inputmethod.keyboard.PointerTracker; 34 import com.android.inputmethod.latin.R; 35 36 public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { 37 private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy(); 38 39 private InputMethodService mInputMethod; 40 private LatinKeyboardView mView; 41 private AccessibilityEntityProvider mAccessibilityNodeProvider; 42 43 private Key mLastHoverKey = null; 44 45 /** 46 * Inset in pixels to look for keys when the user's finger exits the 47 * keyboard area. See {@link ViewConfiguration#getScaledEdgeSlop()}. 48 */ 49 private int mEdgeSlop; 50 51 public static void init(InputMethodService inputMethod) { 52 sInstance.initInternal(inputMethod); 53 } 54 55 public static AccessibleKeyboardViewProxy getInstance() { 56 return sInstance; 57 } 58 59 private AccessibleKeyboardViewProxy() { 60 // Not publicly instantiable. 61 } 62 63 private void initInternal(InputMethodService inputMethod) { 64 mInputMethod = inputMethod; 65 mEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop(); 66 } 67 68 /** 69 * Sets the view wrapped by this proxy. 70 * 71 * @param view The view to wrap. 72 */ 73 public void setView(LatinKeyboardView 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 * Receives hover events when accessibility is turned on in SDK versions ICS 109 * and higher. 110 * 111 * @param event The hover event. 112 * @return {@code true} if the event is handled 113 */ 114 public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) { 115 final int x = (int) event.getX(); 116 final int y = (int) event.getY(); 117 final Key key = tracker.getKeyOn(x, y); 118 final Key previousKey = mLastHoverKey; 119 120 mLastHoverKey = key; 121 122 switch (event.getAction()) { 123 case MotionEvent.ACTION_HOVER_EXIT: 124 // Make sure we're not getting an EXIT event because the user slid 125 // off the keyboard area, then force a key press. 126 if (pointInView(x, y) && (key != null)) { 127 getAccessibilityNodeProvider().simulateKeyPress(key); 128 } 129 //$FALL-THROUGH$ 130 case MotionEvent.ACTION_HOVER_ENTER: 131 return onHoverKey(key, event); 132 case MotionEvent.ACTION_HOVER_MOVE: 133 if (key != previousKey) { 134 return onTransitionKey(key, previousKey, event); 135 } else { 136 return onHoverKey(key, event); 137 } 138 } 139 140 return false; 141 } 142 143 /** 144 * @return A lazily-instantiated node provider for this view proxy. 145 */ 146 private AccessibilityEntityProvider getAccessibilityNodeProvider() { 147 // Instantiate the provide only when requested. Since the system 148 // will call this method multiple times it is a good practice to 149 // cache the provider instance. 150 if (mAccessibilityNodeProvider == null) { 151 mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod); 152 } 153 return mAccessibilityNodeProvider; 154 } 155 156 /** 157 * Utility method to determine whether the given point, in local 158 * coordinates, is inside the view, where the area of the view is contracted 159 * by the edge slop factor. 160 * 161 * @param localX The local x-coordinate. 162 * @param localY The local y-coordinate. 163 */ 164 private boolean pointInView(int localX, int localY) { 165 return (localX >= mEdgeSlop) && (localY >= mEdgeSlop) 166 && (localX < (mView.getWidth() - mEdgeSlop)) 167 && (localY < (mView.getHeight() - mEdgeSlop)); 168 } 169 170 /** 171 * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT 172 * on the previous key, a HOVER_ENTER on the current key, and a HOVER_MOVE 173 * on the current key. 174 * 175 * @param currentKey The currently hovered key. 176 * @param previousKey The previously hovered key. 177 * @param event The event that triggered the transition. 178 * @return {@code true} if the event was handled. 179 */ 180 private boolean onTransitionKey(Key currentKey, Key previousKey, MotionEvent event) { 181 final int savedAction = event.getAction(); 182 183 event.setAction(MotionEvent.ACTION_HOVER_EXIT); 184 onHoverKey(previousKey, event); 185 186 event.setAction(MotionEvent.ACTION_HOVER_ENTER); 187 onHoverKey(currentKey, event); 188 189 event.setAction(MotionEvent.ACTION_HOVER_MOVE); 190 final boolean handled = onHoverKey(currentKey, event); 191 192 event.setAction(savedAction); 193 194 return handled; 195 } 196 197 /** 198 * Handles a hover event on a key. If {@link Key} extended View, this would 199 * be analogous to calling View.onHoverEvent(MotionEvent). 200 * 201 * @param key The currently hovered key. 202 * @param event The hover event. 203 * @return {@code true} if the event was handled. 204 */ 205 private boolean onHoverKey(Key key, MotionEvent event) { 206 // Null keys can't receive events. 207 if (key == null) { 208 return false; 209 } 210 211 final AccessibilityEntityProvider provider = getAccessibilityNodeProvider(); 212 213 switch (event.getAction()) { 214 case MotionEvent.ACTION_HOVER_ENTER: 215 provider.sendAccessibilityEventForKey( 216 key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); 217 provider.performActionForKey( 218 key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); 219 break; 220 case MotionEvent.ACTION_HOVER_EXIT: 221 provider.sendAccessibilityEventForKey( 222 key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); 223 break; 224 } 225 226 return true; 227 } 228 229 /** 230 * Notifies the user of changes in the keyboard shift state. 231 */ 232 public void notifyShiftState() { 233 final Keyboard keyboard = mView.getKeyboard(); 234 final KeyboardId keyboardId = keyboard.mId; 235 final int elementId = keyboardId.mElementId; 236 final Context context = mView.getContext(); 237 final CharSequence text; 238 239 switch (elementId) { 240 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 241 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 242 text = context.getText(R.string.spoken_description_shiftmode_locked); 243 break; 244 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 245 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 246 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 247 text = context.getText(R.string.spoken_description_shiftmode_on); 248 break; 249 default: 250 text = context.getText(R.string.spoken_description_shiftmode_off); 251 } 252 253 AccessibilityUtils.getInstance().speak(text); 254 } 255 256 /** 257 * Notifies the user of changes in the keyboard symbols state. 258 */ 259 public void notifySymbolsState() { 260 final Keyboard keyboard = mView.getKeyboard(); 261 final Context context = mView.getContext(); 262 final KeyboardId keyboardId = keyboard.mId; 263 final int elementId = keyboardId.mElementId; 264 final int resId; 265 266 switch (elementId) { 267 case KeyboardId.ELEMENT_ALPHABET: 268 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 269 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 270 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 271 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 272 resId = R.string.spoken_description_mode_alpha; 273 break; 274 case KeyboardId.ELEMENT_SYMBOLS: 275 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 276 resId = R.string.spoken_description_mode_symbol; 277 break; 278 case KeyboardId.ELEMENT_PHONE: 279 resId = R.string.spoken_description_mode_phone; 280 break; 281 case KeyboardId.ELEMENT_PHONE_SYMBOLS: 282 resId = R.string.spoken_description_mode_phone_shift; 283 break; 284 default: 285 resId = -1; 286 } 287 288 if (resId < 0) { 289 return; 290 } 291 292 final String text = context.getString(resId); 293 AccessibilityUtils.getInstance().speak(text); 294 } 295 } 296