1 /* 2 * Copyright (C) 2022 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 android.accessibilityservice; 18 19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; 20 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.os.RemoteException; 25 import android.os.Trace; 26 import android.util.Log; 27 import android.view.KeyCharacterMap; 28 import android.view.KeyEvent; 29 import android.view.inputmethod.EditorInfo; 30 import android.view.inputmethod.InputConnection; 31 import android.view.inputmethod.InputMethodManager; 32 import android.view.inputmethod.SurroundingText; 33 import android.view.inputmethod.TextAttribute; 34 35 import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback; 36 import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; 37 import com.android.internal.inputmethod.RemoteAccessibilityInputConnection; 38 39 /** 40 * This class provides input method APIs. Some public methods such as 41 * @link #onUpdateSelection(int, int, int, int, int, int)} do nothing by default and service 42 * developers should override them as needed. Developers should also override 43 * {@link AccessibilityService#onCreateInputMethod()} to return 44 * their custom InputMethod implementation. Accessibility services also need to set the 45 * {@link AccessibilityServiceInfo#FLAG_INPUT_METHOD_EDITOR} flag to use input method APIs. 46 */ 47 public class InputMethod { 48 private static final String LOG_TAG = "A11yInputMethod"; 49 50 private final AccessibilityService mService; 51 private boolean mInputStarted; 52 private RemoteAccessibilityInputConnection mStartedInputConnection; 53 private EditorInfo mInputEditorInfo; 54 55 /** 56 * Creates a new InputMethod instance for the given <code>service</code>, so that the 57 * accessibility service can control editing. 58 */ InputMethod(@onNull AccessibilityService service)59 public InputMethod(@NonNull AccessibilityService service) { 60 mService = service; 61 } 62 63 /** 64 * Retrieve the currently active InputConnection that is bound to 65 * the input method, or null if there is none. 66 */ 67 @Nullable getCurrentInputConnection()68 public final AccessibilityInputConnection getCurrentInputConnection() { 69 if (mStartedInputConnection != null) { 70 return new AccessibilityInputConnection(mStartedInputConnection); 71 } 72 return null; 73 } 74 75 /** 76 * Whether the input has started. 77 */ getCurrentInputStarted()78 public final boolean getCurrentInputStarted() { 79 return mInputStarted; 80 } 81 82 /** 83 * Get the EditorInfo which describes several attributes of a text editing object 84 * that an accessibility service is communicating with (typically an EditText). 85 */ 86 @Nullable getCurrentInputEditorInfo()87 public final EditorInfo getCurrentInputEditorInfo() { 88 return mInputEditorInfo; 89 } 90 91 /** 92 * Called to inform the accessibility service that text input has started in an 93 * editor. You should use this callback to initialize the state of your 94 * input to match the state of the editor given to it. 95 * 96 * @param attribute The attributes of the editor that input is starting 97 * in. 98 * @param restarting Set to true if input is restarting in the same 99 * editor such as because the application has changed the text in 100 * the editor. Otherwise will be false, indicating this is a new 101 * session with the editor. 102 */ onStartInput(@onNull EditorInfo attribute, boolean restarting)103 public void onStartInput(@NonNull EditorInfo attribute, boolean restarting) { 104 // Intentionally empty 105 } 106 107 /** 108 * Called to inform the accessibility service that text input has finished in 109 * the last editor. At this point there may be a call to 110 * {@link #onStartInput(EditorInfo, boolean)} to perform input in a 111 * new editor, or the accessibility service may be left idle. This method is 112 * <em>not</em> called when input restarts in the same editor. 113 * 114 * <p>The default 115 * implementation uses the InputConnection to clear any active composing 116 * text; you can override this (not calling the base class implementation) 117 * to perform whatever behavior you would like. 118 */ onFinishInput()119 public void onFinishInput() { 120 // Intentionally empty 121 } 122 123 /** 124 * Called when the application has reported a new selection region of 125 * the text. This is called whether or not the accessibility service has requested 126 * extracted text updates, although if so it will not receive this call 127 * if the extracted text has changed as well. 128 * 129 * <p>Be careful about changing the text in reaction to this call with 130 * methods such as setComposingText, commitText or 131 * deleteSurroundingText. If the cursor moves as a result, this method 132 * will be called again, which may result in an infinite loop. 133 */ onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)134 public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, 135 int newSelEnd, int candidatesStart, int candidatesEnd) { 136 // Intentionally empty 137 } 138 createImeSession(IAccessibilityInputMethodSessionCallback callback)139 final void createImeSession(IAccessibilityInputMethodSessionCallback callback) { 140 final AccessibilityInputMethodSessionWrapper wrapper = 141 new AccessibilityInputMethodSessionWrapper(mService.getMainLooper(), 142 new SessionImpl()); 143 try { 144 callback.sessionCreated(wrapper, mService.getConnectionId()); 145 } catch (RemoteException ignored) { 146 } 147 } 148 startInput(@ullable RemoteAccessibilityInputConnection ic, @NonNull EditorInfo attribute)149 final void startInput(@Nullable RemoteAccessibilityInputConnection ic, 150 @NonNull EditorInfo attribute) { 151 Log.v(LOG_TAG, "startInput(): editor=" + attribute); 152 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AccessibilityService.startInput"); 153 doStartInput(ic, attribute, false /* restarting */); 154 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 155 } 156 restartInput(@ullable RemoteAccessibilityInputConnection ic, @NonNull EditorInfo attribute)157 final void restartInput(@Nullable RemoteAccessibilityInputConnection ic, 158 @NonNull EditorInfo attribute) { 159 Log.v(LOG_TAG, "restartInput(): editor=" + attribute); 160 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AccessibilityService.restartInput"); 161 doStartInput(ic, attribute, true /* restarting */); 162 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 163 } 164 165 doStartInput(RemoteAccessibilityInputConnection ic, EditorInfo attribute, boolean restarting)166 final void doStartInput(RemoteAccessibilityInputConnection ic, EditorInfo attribute, 167 boolean restarting) { 168 if ((ic == null || !restarting) && mInputStarted) { 169 doFinishInput(); 170 if (ic == null) { 171 // Unlike InputMethodService, A11y IME should not observe fallback InputConnection. 172 return; 173 } 174 } 175 mInputStarted = true; 176 mStartedInputConnection = ic; 177 mInputEditorInfo = attribute; 178 Log.v(LOG_TAG, "CALL: onStartInput"); 179 onStartInput(attribute, restarting); 180 } 181 doFinishInput()182 final void doFinishInput() { 183 Log.v(LOG_TAG, "CALL: doFinishInput"); 184 if (mInputStarted) { 185 Log.v(LOG_TAG, "CALL: onFinishInput"); 186 onFinishInput(); 187 } 188 mInputStarted = false; 189 mStartedInputConnection = null; 190 mInputEditorInfo = null; 191 } 192 193 /** 194 * This class provides the allowed list of {@link InputConnection} APIs for 195 * accessibility services. 196 */ 197 public final class AccessibilityInputConnection { 198 private final RemoteAccessibilityInputConnection mIc; AccessibilityInputConnection(RemoteAccessibilityInputConnection ic)199 AccessibilityInputConnection(RemoteAccessibilityInputConnection ic) { 200 this.mIc = ic; 201 } 202 203 /** 204 * Commit text to the text box and set the new cursor position. This method is 205 * used to allow the IME to provide extra information while setting up text. 206 * 207 * <p>This method commits the contents of the currently composing text, and then 208 * moves the cursor according to {@code newCursorPosition}. If there 209 * is no composing text when this method is called, the new text is 210 * inserted at the cursor position, removing text inside the selection 211 * if any. 212 * 213 * <p>Calling this method will cause the editor to call 214 * {@link #onUpdateSelection(int, int, int, int, 215 * int, int)} on the current accessibility service after the batch input is over. 216 * <strong>Editor authors</strong>, for this to happen you need to 217 * make the changes known to the accessibility service by calling 218 * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)}, 219 * but be careful to wait until the batch edit is over if one is 220 * in progress.</p> 221 * 222 * @param text The text to commit. This may include styles. 223 * @param newCursorPosition The new cursor position around the text, 224 * in Java characters. If > 0, this is relative to the end 225 * of the text - 1; if <= 0, this is relative to the start 226 * of the text. So a value of 1 will always advance the cursor 227 * to the position after the full text being inserted. Note that 228 * this means you can't position the cursor within the text, 229 * because the editor can make modifications to the text 230 * you are providing so it is not possible to correctly specify 231 * locations there. 232 * @param textAttribute The extra information about the text. 233 */ commitText(@onNull CharSequence text, int newCursorPosition, @Nullable TextAttribute textAttribute)234 public void commitText(@NonNull CharSequence text, int newCursorPosition, 235 @Nullable TextAttribute textAttribute) { 236 if (mIc != null) { 237 mIc.commitText(text, newCursorPosition, textAttribute); 238 } 239 } 240 241 /** 242 * Set the selection of the text editor. To set the cursor 243 * position, start and end should have the same value. 244 * 245 * <p>Since this moves the cursor, calling this method will cause 246 * the editor to call 247 * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, 248 * int,int, int)} on the current IME after the batch input is over. 249 * <strong>Editor authors</strong>, for this to happen you need to 250 * make the changes known to the input method by calling 251 * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)}, 252 * but be careful to wait until the batch edit is over if one is 253 * in progress.</p> 254 * 255 * <p>This has no effect on the composing region which must stay 256 * unchanged. The order of start and end is not important. In 257 * effect, the region from start to end and the region from end to 258 * start is the same. Editor authors, be ready to accept a start 259 * that is greater than end.</p> 260 * 261 * @param start the character index where the selection should start. 262 * @param end the character index where the selection should end. 263 */ setSelection(int start, int end)264 public void setSelection(int start, int end) { 265 if (mIc != null) { 266 mIc.setSelection(start, end); 267 } 268 } 269 270 /** 271 * Gets the surrounding text around the current cursor, with <var>beforeLength</var> 272 * characters of text before the cursor (start of the selection), <var>afterLength</var> 273 * characters of text after the cursor (end of the selection), and all of the selected 274 * text. The range are for java characters, not glyphs that can be multiple characters. 275 * 276 * <p>This method may fail either if the input connection has become invalid (such as its 277 * process crashing), or the client is taking too long to respond with the text (it is 278 * given a couple seconds to return), or the protocol is not supported. In any of these 279 * cases, null is returned. 280 * 281 * <p>This method does not affect the text in the editor in any way, nor does it affect the 282 * selection or composing spans.</p> 283 * 284 * <p>If {@link InputConnection#GET_TEXT_WITH_STYLES} is supplied as flags, the editor 285 * should return a {@link android.text.Spanned} with all the spans set on the text.</p> 286 * 287 * <p><strong>Accessibility service authors:</strong> please consider this will trigger an 288 * IPC round-trip that will take some time. Assume this method consumes a lot of time. 289 * 290 * @param beforeLength The expected length of the text before the cursor. 291 * @param afterLength The expected length of the text after the cursor. 292 * @param flags Supplies additional options controlling how the text is returned. May be 293 * either {@code 0} or {@link InputConnection#GET_TEXT_WITH_STYLES}. 294 * @return an {@link android.view.inputmethod.SurroundingText} object describing the 295 * surrounding text and state of selection, or null if the input connection is no longer 296 * valid, or the editor can't comply with the request for some reason, or the application 297 * does not implement this method. The length of the returned text might be less than the 298 * sum of <var>beforeLength</var> and <var>afterLength</var> . 299 * @throws IllegalArgumentException if {@code beforeLength} or {@code afterLength} is 300 * negative. 301 */ 302 @Nullable getSurroundingText( @ntRangefrom = 0) int beforeLength, @IntRange(from = 0) int afterLength, @InputConnection.GetTextType int flags)303 public SurroundingText getSurroundingText( 304 @IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength, 305 @InputConnection.GetTextType int flags) { 306 if (mIc != null) { 307 return mIc.getSurroundingText(beforeLength, afterLength, flags); 308 } 309 return null; 310 } 311 312 /** 313 * Delete <var>beforeLength</var> characters of text before the 314 * current cursor position, and delete <var>afterLength</var> 315 * characters of text after the current cursor position, excluding 316 * the selection. Before and after refer to the order of the 317 * characters in the string, not to their visual representation: 318 * this means you don't have to figure out the direction of the 319 * text and can just use the indices as-is. 320 * 321 * <p>The lengths are supplied in Java chars, not in code points 322 * or in glyphs.</p> 323 * 324 * <p>Since this method only operates on text before and after the 325 * selection, it can't affect the contents of the selection. This 326 * may affect the composing span if the span includes characters 327 * that are to be deleted, but otherwise will not change it. If 328 * some characters in the composing span are deleted, the 329 * composing span will persist but get shortened by however many 330 * chars inside it have been removed.</p> 331 * 332 * <p><strong>Accessibility service authors:</strong> please be careful not to 333 * delete only half of a surrogate pair. Also take care not to 334 * delete more characters than are in the editor, as that may have 335 * ill effects on the application. Calling this method will cause 336 * the editor to call {@link InputMethod#onUpdateSelection(int, int, int, int, int, int)} 337 * on your service after the batch input is over.</p> 338 * 339 * <p><strong>Editor authors:</strong> please be careful of race 340 * conditions in implementing this call. An IME can make a change 341 * to the text or change the selection position and use this 342 * method right away; you need to make sure the effects are 343 * consistent with the results of the latest edits. Also, although 344 * the IME should not send lengths bigger than the contents of the 345 * string, you should check the values for overflows and trim the 346 * indices to the size of the contents to avoid crashes. Since 347 * this changes the contents of the editor, you need to make the 348 * changes known to the input method by calling 349 * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)}, 350 * but be careful to wait until the batch edit is over if one is 351 * in progress.</p> 352 * 353 * @param beforeLength The number of characters before the cursor to be deleted, in code 354 * unit. If this is greater than the number of existing characters between the 355 * beginning of the text and the cursor, then this method does not fail but deletes 356 * all the characters in that range. 357 * @param afterLength The number of characters after the cursor to be deleted, in code unit. 358 * If this is greater than the number of existing characters between the cursor and 359 * the end of the text, then this method does not fail but deletes all the characters 360 * in that range. 361 */ deleteSurroundingText(int beforeLength, int afterLength)362 public void deleteSurroundingText(int beforeLength, int afterLength) { 363 if (mIc != null) { 364 mIc.deleteSurroundingText(beforeLength, afterLength); 365 } 366 } 367 368 /** 369 * Send a key event to the process that is currently attached 370 * through this input connection. The event will be dispatched 371 * like a normal key event, to the currently focused view; this 372 * generally is the view that is providing this InputConnection, 373 * but due to the asynchronous nature of this protocol that can 374 * not be guaranteed and the focus may have changed by the time 375 * the event is received. 376 * 377 * <p>This method can be used to send key events to the 378 * application. For example, an on-screen keyboard may use this 379 * method to simulate a hardware keyboard. There are three types 380 * of standard keyboards, numeric (12-key), predictive (20-key) 381 * and ALPHA (QWERTY). You can specify the keyboard type by 382 * specify the device id of the key event.</p> 383 * 384 * <p>You will usually want to set the flag 385 * {@link KeyEvent#FLAG_SOFT_KEYBOARD KeyEvent.FLAG_SOFT_KEYBOARD} 386 * on all key event objects you give to this API; the flag will 387 * not be set for you.</p> 388 * 389 * <p>Note that it's discouraged to send such key events in normal 390 * operation; this is mainly for use with 391 * {@link android.text.InputType#TYPE_NULL} type text fields. Use 392 * the {@link #commitText} family of methods to send text to the 393 * application instead.</p> 394 * 395 * @param event The key event. 396 * 397 * @see KeyEvent 398 * @see KeyCharacterMap#NUMERIC 399 * @see KeyCharacterMap#PREDICTIVE 400 * @see KeyCharacterMap#ALPHA 401 */ sendKeyEvent(@onNull KeyEvent event)402 public void sendKeyEvent(@NonNull KeyEvent event) { 403 if (mIc != null) { 404 mIc.sendKeyEvent(event); 405 } 406 } 407 408 /** 409 * Have the editor perform an action it has said it can do. 410 * 411 * @param editorAction This must be one of the action constants for 412 * {@link EditorInfo#imeOptions EditorInfo.imeOptions}, such as 413 * {@link EditorInfo#IME_ACTION_GO EditorInfo.EDITOR_ACTION_GO}, or the value of 414 * {@link EditorInfo#actionId EditorInfo.actionId} if a custom action is available. 415 */ performEditorAction(int editorAction)416 public void performEditorAction(int editorAction) { 417 if (mIc != null) { 418 mIc.performEditorAction(editorAction); 419 } 420 } 421 422 /** 423 * Perform a context menu action on the field. The given id may be one of: 424 * {@link android.R.id#selectAll}, 425 * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText}, 426 * {@link android.R.id#cut}, {@link android.R.id#copy}, 427 * {@link android.R.id#paste}, {@link android.R.id#copyUrl}, 428 * or {@link android.R.id#switchInputMethod} 429 */ performContextMenuAction(int id)430 public void performContextMenuAction(int id) { 431 if (mIc != null) { 432 mIc.performContextMenuAction(id); 433 } 434 } 435 436 /** 437 * Retrieve the current capitalization mode in effect at the 438 * current cursor position in the text. See 439 * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode} 440 * for more information. 441 * 442 * <p>This method may fail either if the input connection has 443 * become invalid (such as its process crashing) or the client is 444 * taking too long to respond with the text (it is given a couple 445 * seconds to return). In either case, 0 is returned.</p> 446 * 447 * <p>This method does not affect the text in the editor in any 448 * way, nor does it affect the selection or composing spans.</p> 449 * 450 * <p><strong>Editor authors:</strong> please be careful of race 451 * conditions in implementing this call. An IME can change the 452 * cursor position and use this method right away; you need to make 453 * sure the returned value is consistent with the results of the 454 * latest edits and changes to the cursor position.</p> 455 * 456 * @param reqModes The desired modes to retrieve, as defined by 457 * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode}. These 458 * constants are defined so that you can simply pass the current 459 * {@link EditorInfo#inputType TextBoxAttribute.contentType} value 460 * directly in to here. 461 * @return the caps mode flags that are in effect at the current 462 * cursor position. See TYPE_TEXT_FLAG_CAPS_* in {@link android.text.InputType}. 463 */ getCursorCapsMode(int reqModes)464 public int getCursorCapsMode(int reqModes) { 465 if (mIc != null) { 466 return mIc.getCursorCapsMode(reqModes); 467 } 468 return 0; 469 } 470 471 /** 472 * Clear the given meta key pressed states in the given input 473 * connection. 474 * 475 * <p>This can be used by the accessibility service to clear the meta key states set 476 * by a hardware keyboard with latched meta keys, if the editor 477 * keeps track of these.</p> 478 * 479 * @param states The states to be cleared, may be one or more bits as 480 * per {@link KeyEvent#getMetaState() KeyEvent.getMetaState()}. 481 */ clearMetaKeyStates(int states)482 public void clearMetaKeyStates(int states) { 483 if (mIc != null) { 484 mIc.clearMetaKeyStates(states); 485 } 486 } 487 } 488 489 /** 490 * Concrete implementation of {@link AccessibilityInputMethodSession} that provides all of the 491 * standard behavior for an A11y input method session. 492 */ 493 private final class SessionImpl implements AccessibilityInputMethodSession { 494 boolean mEnabled = true; 495 496 @Override setEnabled(boolean enabled)497 public void setEnabled(boolean enabled) { 498 mEnabled = enabled; 499 } 500 501 @Override finishInput()502 public void finishInput() { 503 if (mEnabled) { 504 doFinishInput(); 505 } 506 } 507 508 @Override updateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)509 public void updateSelection(int oldSelStart, int oldSelEnd, int newSelStart, 510 int newSelEnd, int candidatesStart, int candidatesEnd) { 511 if (mEnabled) { 512 InputMethod.this.onUpdateSelection(oldSelEnd, oldSelEnd, newSelStart, 513 newSelEnd, candidatesStart, candidatesEnd); 514 } 515 } 516 517 @Override invalidateInput(EditorInfo editorInfo, IRemoteAccessibilityInputConnection connection, int sessionId)518 public void invalidateInput(EditorInfo editorInfo, 519 IRemoteAccessibilityInputConnection connection, int sessionId) { 520 if (!mEnabled || mStartedInputConnection == null 521 || !mStartedInputConnection.isSameConnection(connection)) { 522 // This is not an error, and can be safely ignored. 523 return; 524 } 525 editorInfo.makeCompatible(mService.getApplicationInfo().targetSdkVersion); 526 restartInput(new RemoteAccessibilityInputConnection(mStartedInputConnection, sessionId), 527 editorInfo); 528 } 529 } 530 } 531