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