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 androidx.core.view.AccessibilityDelegateCompat; 22 import androidx.core.view.ViewCompat; 23 import androidx.core.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 <KV> 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 KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector)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 */ setKeyboard(final Keyboard keyboard)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 getKeyboard()84 protected final Keyboard getKeyboard() { 85 return mKeyboard; 86 } 87 setLastHoverKey(final Key key)88 protected final void setLastHoverKey(final Key key) { 89 mLastHoverKey = key; 90 } 91 getLastHoverKey()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 */ sendWindowStateChanged(final int resId)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 */ sendWindowStateChanged(final String text)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 getAccessibilityNodeProvider(final View host)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 */ getAccessibilityNodeProvider()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 */ getHoverKeyOf(final MotionEvent event)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 */ onHoverEvent(final MotionEvent event)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 */ onHoverEnter(final MotionEvent event)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 */ onHoverMove(final MotionEvent event)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 */ onHoverExit(final MotionEvent event)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 onHoverExitFrom(key); 247 } 248 setLastHoverKey(null); 249 } 250 251 /** 252 * Perform click on a key. 253 * 254 * @param key A key to be registered. 255 */ performClickOn(final Key key)256 public void performClickOn(final Key key) { 257 if (DEBUG_HOVER) { 258 Log.d(TAG, "performClickOn: key=" + key); 259 } 260 simulateTouchEvent(MotionEvent.ACTION_DOWN, key); 261 simulateTouchEvent(MotionEvent.ACTION_UP, key); 262 } 263 264 /** 265 * Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}. 266 * 267 * @param touchAction The action of the synthesizing touch event. 268 * @param key The key that a synthesized touch event is on. 269 */ simulateTouchEvent(final int touchAction, final Key key)270 private void simulateTouchEvent(final int touchAction, final Key key) { 271 final int x = key.getHitBox().centerX(); 272 final int y = key.getHitBox().centerY(); 273 final long eventTime = SystemClock.uptimeMillis(); 274 final MotionEvent touchEvent = MotionEvent.obtain( 275 eventTime, eventTime, touchAction, x, y, 0 /* metaState */); 276 mKeyboardView.onTouchEvent(touchEvent); 277 touchEvent.recycle(); 278 } 279 280 /** 281 * Handles a hover enter event on a key. 282 * 283 * @param key The currently hovered key. 284 */ onHoverEnterTo(final Key key)285 protected void onHoverEnterTo(final Key key) { 286 if (DEBUG_HOVER) { 287 Log.d(TAG, "onHoverEnterTo: key=" + key); 288 } 289 key.onPressed(); 290 mKeyboardView.invalidateKey(key); 291 final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider(); 292 provider.onHoverEnterTo(key); 293 provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); 294 } 295 296 /** 297 * Handles a hover move event on a key. 298 * 299 * @param key The currently hovered key. 300 */ onHoverMoveWithin(final Key key)301 protected void onHoverMoveWithin(final Key key) { } 302 303 /** 304 * Handles a hover exit event on a key. 305 * 306 * @param key The currently hovered key. 307 */ onHoverExitFrom(final Key key)308 protected void onHoverExitFrom(final Key key) { 309 if (DEBUG_HOVER) { 310 Log.d(TAG, "onHoverExitFrom: key=" + key); 311 } 312 key.onReleased(); 313 mKeyboardView.invalidateKey(key); 314 final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider(); 315 provider.onHoverExitFrom(key); 316 } 317 318 /** 319 * Perform long click on a key. 320 * 321 * @param key A key to be long pressed on. 322 */ performLongClickOn(final Key key)323 public void performLongClickOn(final Key key) { 324 // A extended class should override this method to implement long press. 325 } 326 } 327