package android.accessibilityservice;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.SurroundingText;
import android.view.inputmethod.TextAttribute;
import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
import com.android.internal.inputmethod.RemoteAccessibilityInputConnection;
public class InputMethod {
private static final String LOG_TAG = "A11yInputMethod";
private final AccessibilityService mService;
private boolean mInputStarted;
private RemoteAccessibilityInputConnection mStartedInputConnection;
private EditorInfo mInputEditorInfo;
public InputMethod(@NonNull AccessibilityService service) {
mService = service;
}
@Nullable
public final AccessibilityInputConnection getCurrentInputConnection() {
if (mStartedInputConnection != null) {
return new AccessibilityInputConnection(mStartedInputConnection);
}
return null;
}
public final boolean getCurrentInputStarted() {
return mInputStarted;
}
@Nullable
public final EditorInfo getCurrentInputEditorInfo() {
return mInputEditorInfo;
}
public void onStartInput(@NonNull EditorInfo attribute, boolean restarting) {
// Intentionally empty
}
public void onFinishInput() {
// Intentionally empty
}
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
int newSelEnd, int candidatesStart, int candidatesEnd) {
// Intentionally empty
}
final void createImeSession(IAccessibilityInputMethodSessionCallback callback) {
final AccessibilityInputMethodSessionWrapper wrapper =
new AccessibilityInputMethodSessionWrapper(mService.getMainLooper(),
new SessionImpl());
try {
callback.sessionCreated(wrapper, mService.getConnectionId());
} catch (RemoteException ignored) {
}
}
final void startInput(@Nullable RemoteAccessibilityInputConnection ic,
@NonNull EditorInfo attribute) {
Log.v(LOG_TAG, "startInput(): editor=" + attribute);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AccessibilityService.startInput");
doStartInput(ic, attribute, false /* restarting */);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
final void restartInput(@Nullable RemoteAccessibilityInputConnection ic,
@NonNull EditorInfo attribute) {
Log.v(LOG_TAG, "restartInput(): editor=" + attribute);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AccessibilityService.restartInput");
doStartInput(ic, attribute, true /* restarting */);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
final void doStartInput(RemoteAccessibilityInputConnection ic, EditorInfo attribute,
boolean restarting) {
if ((ic == null || !restarting) && mInputStarted) {
doFinishInput();
if (ic == null) {
// Unlike InputMethodService, A11y IME should not observe fallback InputConnection.
return;
}
}
mInputStarted = true;
mStartedInputConnection = ic;
mInputEditorInfo = attribute;
Log.v(LOG_TAG, "CALL: onStartInput");
onStartInput(attribute, restarting);
}
final void doFinishInput() {
Log.v(LOG_TAG, "CALL: doFinishInput");
if (mInputStarted) {
Log.v(LOG_TAG, "CALL: onFinishInput");
onFinishInput();
}
mInputStarted = false;
mStartedInputConnection = null;
mInputEditorInfo = null;
}
public final class AccessibilityInputConnection {
private final RemoteAccessibilityInputConnection mIc;
AccessibilityInputConnection(RemoteAccessibilityInputConnection ic) {
this.mIc = ic;
}
public void commitText(@NonNull CharSequence text, int newCursorPosition,
@Nullable TextAttribute textAttribute) {
if (mIc != null) {
mIc.commitText(text, newCursorPosition, textAttribute);
}
}
public void setSelection(int start, int end) {
if (mIc != null) {
mIc.setSelection(start, end);
}
}
@Nullable
public SurroundingText getSurroundingText(
@IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength,
@InputConnection.GetTextType int flags) {
if (mIc != null) {
return mIc.getSurroundingText(beforeLength, afterLength, flags);
}
return null;
}
public void deleteSurroundingText(int beforeLength, int afterLength) {
if (mIc != null) {
mIc.deleteSurroundingText(beforeLength, afterLength);
}
}
public void sendKeyEvent(@NonNull KeyEvent event) {
if (mIc != null) {
mIc.sendKeyEvent(event);
}
}
public void performEditorAction(int editorAction) {
if (mIc != null) {
mIc.performEditorAction(editorAction);
}
}
public void performContextMenuAction(int id) {
if (mIc != null) {
mIc.performContextMenuAction(id);
}
}
public int getCursorCapsMode(int reqModes) {
if (mIc != null) {
return mIc.getCursorCapsMode(reqModes);
}
return 0;
}
public void clearMetaKeyStates(int states) {
if (mIc != null) {
mIc.clearMetaKeyStates(states);
}
}
}
/**
* Concrete implementation of {@link AccessibilityInputMethodSession} that provides all of the
* standard behavior for an A11y input method session.
*/
private final class SessionImpl implements AccessibilityInputMethodSession {
boolean mEnabled = true;
@Override
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
@Override
public void finishInput() {
if (mEnabled) {
doFinishInput();
}
}
@Override
public void updateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
int newSelEnd, int candidatesStart, int candidatesEnd) {
if (mEnabled) {
InputMethod.this.onUpdateSelection(oldSelEnd, oldSelEnd, newSelStart,
newSelEnd, candidatesStart, candidatesEnd);
}
}
@Override
public void invalidateInput(EditorInfo editorInfo,
IRemoteAccessibilityInputConnection connection, int sessionId) {
if (!mEnabled || mStartedInputConnection == null
|| !mStartedInputConnection.isSameConnection(connection)) {
// This is not an error, and can be safely ignored.
return;
}
editorInfo.makeCompatible(mService.getApplicationInfo().targetSdkVersion);
restartInput(new RemoteAccessibilityInputConnection(mStartedInputConnection, sessionId),
editorInfo);
}
}
}
这个代码是一个安卓的辅助服务类,用于提供输入法的API。代码的逻辑和作用如下:
-
InputMethod类:这个类是一个辅助服务的输入法类,它有以下主要方法:
- 构造方法:创建一个新的InputMethod实例,绑定一个辅助服务对象,用于控制编辑。
- getCurrentInputConnection:获取当前绑定的输入连接对象,或者返回null。
- getCurrentInputStarted:判断输入是否已经开始。
- getCurrentInputEditorInfo:获取当前编辑器的属性信息。
- onStartInput:当输入开始时,初始化输入的状态,根据编辑器的属性信息,可以重写这个方法。
- onFinishInput:当输入结束时,清理输入的状态,可以重写这个方法。
- onUpdateSelection:当编辑器报告新的文本选择区域时,可以重写这个方法。
- createImeSession:创建一个输入法会话对象,用于和远程输入连接通信。
- startInput:开始输入,调用doStartInput方法。
- restartInput:重新开始输入,调用doStartInput方法。
- doStartInput:根据输入连接对象和编辑器属性信息,初始化输入的状态,调用onStartInput方法。
- doFinishInput:结束输入,调用onFinishInput方法。
-
AccessibilityInputConnection类:这个类是一个辅助服务的输入连接类,它封装了一个远程输入连接对象,提供了以下主要方法:
- commitText:提交文本到文本框,并设置新的光标位置,可以提供额外的文本信息。
- setSelection:设置文本编辑器的选择区域,如果开始和结束位置相同,就是设置光标位置。
- getSurroundingText:获取当前光标周围的文本,包括选择的文本,可以指定前后的长度和返回的文本类型。
- deleteSurroundingText:删除光标前后的文本,不包括选择的文本,可以指定前后的长度。
- sendKeyEvent:发送一个键盘事件到当前绑定的进程,事件会像正常的键盘事件一样被分发到当前聚焦的视图。
- performEditorAction:执行编辑器支持的一个动作,比如回车,搜索等。
- performContextMenuAction:执行编辑器的一个上下文菜单动作,比如全选,剪切,复制,粘贴等。
- getCursorCapsMode:获取当前光标位置的大小写模式,可以指定所需的模式。
- clearMetaKeyStates:清除给定的元键按下状态,可以用于清除硬件键盘的锁定元键状态。
-
SessionImpl类:这个类是一个辅助服务的输入法会话类,它实现了AccessibilityInputMethodSession接口,提供了以下主要方法:
- setEnabled:设置会话是否可用。
- finishInput:结束输入,调用doFinishInput方法。
- updateSelection:更新选择区域,调用onUpdateSelection方法。
- invalidateInput:使输入失效,如果输入连接对象和会话ID匹配,就重新开始输入,调用restartInput方法。