Merge "Add a test AccessibilityService for A11Y IME API" into tm-dev

This commit is contained in:
TreeHugger Robot
2022-03-29 00:37:29 +00:00
committed by Android (Google) Code Review
8 changed files with 953 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
//
// Copyright (C) 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {
name: "SampleInputMethodAccessibilityService",
min_sdk_version: "33",
target_sdk_version: "33",
sdk_version: "current",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
static_libs: [
"androidx.annotation_annotation",
],
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.sampleinputmethodaccessibilityservice">
<application android:label="SampleInputMethodAccessibilityService">
<service android:name=".SampleInputMethodAccessibilityService"
android:exported="true"
android:label="SampleInputMethodAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
<category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
</intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagInputMethodEditor"
android:notificationTimeout="0" />

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.view.MotionEvent;
import android.view.View;
final class DragToMoveTouchListener implements View.OnTouchListener {
@FunctionalInterface
interface OnMoveCallback {
void onMove(int dx, int dy);
}
private final OnMoveCallback mCallback;
private int mPointId = -1;
private float mLastTouchX;
private float mLastTouchY;
DragToMoveTouchListener(OnMoveCallback callback) {
mCallback = callback;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
final int pointId = event.getPointerId(event.getActionIndex());
switch (event.getAction()) {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN: {
if (mPointId != -1) {
break;
}
v.setPressed(true);
mPointId = pointId;
mLastTouchX = event.getRawX();
mLastTouchY = event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
if (pointId != mPointId) {
break;
}
final float x = event.getRawX();
final float y = event.getRawY();
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mCallback.onMove((int) dx, (int) dy);
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (pointId != mPointId) {
break;
}
mPointId = -1;
v.setPressed(false);
break;
}
default:
break;
}
return true;
}
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
final class EditorInfoUtil {
/**
* Not intended to be instantiated.
*/
private EditorInfoUtil() {
}
static String dump(@Nullable EditorInfo editorInfo) {
if (editorInfo == null) {
return "null";
}
final StringBuilder sb = new StringBuilder();
dump(sb, editorInfo);
return sb.toString();
}
static void dump(@NonNull StringBuilder sb, @NonNull EditorInfo editorInfo) {
sb.append("packageName=").append(editorInfo.packageName).append("\n")
.append("inputType=");
dumpInputType(sb, editorInfo.inputType);
sb.append("\n");
sb.append("imeOptions=");
dumpImeOptions(sb, editorInfo.imeOptions);
sb.append("\n");
sb.append("initialSelection=(").append(editorInfo.initialSelStart)
.append(",").append(editorInfo.initialSelEnd).append(")");
sb.append("\n");
sb.append("initialCapsMode=");
dumpCapsMode(sb, editorInfo.initialCapsMode);
sb.append("\n");
}
static void dumpInputType(@NonNull StringBuilder sb, int inputType) {
final int inputClass = inputType & EditorInfo.TYPE_MASK_CLASS;
final int inputVariation = inputType & EditorInfo.TYPE_MASK_VARIATION;
final int inputFlags = inputType & EditorInfo.TYPE_MASK_FLAGS;
switch (inputClass) {
case EditorInfo.TYPE_NULL:
sb.append("Null");
break;
case EditorInfo.TYPE_CLASS_TEXT: {
sb.append("Text");
switch (inputVariation) {
case EditorInfo.TYPE_TEXT_VARIATION_NORMAL:
break;
case EditorInfo.TYPE_TEXT_VARIATION_URI:
sb.append(":URI");
break;
case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
sb.append(":EMAIL_ADDRESS");
break;
case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT:
sb.append(":EMAIL_SUBJECT");
break;
case EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE:
sb.append(":SHORT_MESSAGE");
break;
case EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE:
sb.append(":LONG_MESSAGE");
break;
case EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME:
sb.append(":PERSON_NAME");
break;
case EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS:
sb.append(":POSTAL_ADDRESS");
break;
case EditorInfo.TYPE_TEXT_VARIATION_PASSWORD:
sb.append(":PASSWORD");
break;
case EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:
sb.append(":VISIBLE_PASSWORD");
break;
case EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT:
sb.append(":WEB_EDIT_TEXT");
break;
case EditorInfo.TYPE_TEXT_VARIATION_FILTER:
sb.append(":FILTER");
break;
case EditorInfo.TYPE_TEXT_VARIATION_PHONETIC:
sb.append(":PHONETIC");
break;
case EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
sb.append(":WEB_EMAIL_ADDRESS");
break;
case EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD:
sb.append(":WEB_PASSWORD");
break;
default:
sb.append(":UNKNOWN=").append(inputVariation);
break;
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
sb.append("|CAP_CHARACTERS");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
sb.append("|CAP_WORDS");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
sb.append("|CAP_SENTENCES");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0) {
sb.append("|AUTO_CORRECT");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
sb.append("|AUTO_COMPLETE");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0) {
sb.append("|MULTI_LINE");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
sb.append("|NO_SUGGESTIONS");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS)
!= 0) {
sb.append("|ENABLE_TEXT_CONVERSION_SUGGESTIONS");
}
break;
}
case EditorInfo.TYPE_CLASS_NUMBER: {
sb.append("Number");
switch (inputVariation) {
case EditorInfo.TYPE_NUMBER_VARIATION_NORMAL:
sb.append(":NORMAL");
break;
case EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD:
sb.append(":PASSWORD");
break;
default:
sb.append(":UNKNOWN=").append(inputVariation);
break;
}
if ((inputFlags & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0) {
sb.append("|SIGNED");
}
if ((inputFlags & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0) {
sb.append("|DECIMAL");
}
break;
}
case EditorInfo.TYPE_CLASS_PHONE:
sb.append("Phone");
break;
case EditorInfo.TYPE_CLASS_DATETIME: {
sb.append("DateTime");
switch (inputVariation) {
case EditorInfo.TYPE_DATETIME_VARIATION_NORMAL:
sb.append(":NORMAL");
break;
case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
sb.append(":DATE");
break;
case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
sb.append(":TIME");
break;
default:
sb.append(":UNKNOWN=").append(inputVariation);
break;
}
break;
}
default:
sb.append("UnknownClass=").append(inputClass);
if (inputVariation != 0) {
sb.append(":variation=").append(inputVariation);
}
if (inputFlags != 0) {
sb.append("|flags=0x").append(Integer.toHexString(inputFlags));
}
break;
}
}
static void dumpImeOptions(@NonNull StringBuilder sb, int imeOptions) {
final int action = imeOptions & EditorInfo.IME_MASK_ACTION;
final int flags = imeOptions & ~EditorInfo.IME_MASK_ACTION;
sb.append("Action:");
switch (action) {
case EditorInfo.IME_ACTION_UNSPECIFIED:
sb.append("UNSPECIFIED");
break;
case EditorInfo.IME_ACTION_NONE:
sb.append("NONE");
break;
case EditorInfo.IME_ACTION_GO:
sb.append("GO");
break;
case EditorInfo.IME_ACTION_SEARCH:
sb.append("SEARCH");
break;
case EditorInfo.IME_ACTION_SEND:
sb.append("SEND");
break;
case EditorInfo.IME_ACTION_NEXT:
sb.append("NEXT");
break;
case EditorInfo.IME_ACTION_DONE:
sb.append("DONE");
break;
case EditorInfo.IME_ACTION_PREVIOUS:
sb.append("PREVIOUS");
break;
default:
sb.append("UNKNOWN=").append(action);
break;
}
if ((flags & EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0) {
sb.append("|NO_PERSONALIZED_LEARNING");
}
if ((flags & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0) {
sb.append("|NO_FULLSCREEN");
}
if ((flags & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0) {
sb.append("|NAVIGATE_PREVIOUS");
}
if ((flags & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
sb.append("|NAVIGATE_NEXT");
}
if ((flags & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0) {
sb.append("|NO_EXTRACT_UI");
}
if ((flags & EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) != 0) {
sb.append("|NO_ACCESSORY_ACTION");
}
if ((flags & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
sb.append("|NO_ENTER_ACTION");
}
if ((flags & EditorInfo.IME_FLAG_FORCE_ASCII) != 0) {
sb.append("|FORCE_ASCII");
}
}
static void dumpCapsMode(@NonNull StringBuilder sb, int capsMode) {
if (capsMode == 0) {
sb.append("none");
return;
}
boolean addSeparator = false;
if ((capsMode & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
sb.append("CHARACTERS");
addSeparator = true;
}
if ((capsMode & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
if (addSeparator) {
sb.append('|');
}
sb.append("WORDS");
addSeparator = true;
}
if ((capsMode & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
if (addSeparator) {
sb.append('|');
}
sb.append("SENTENCES");
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.os.Parcel;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.Nullable;
final class EventMonitor {
@FunctionalInterface
interface DebugMessageCallback {
void onMessageChanged(String message);
}
private enum State {
BeforeFirstStartInput,
InputStarted,
InputRestarted,
InputFinished,
}
private State mState = State.BeforeFirstStartInput;
private int mStartInputCount = 0;
private int mUpdateSelectionCount = 0;
private int mFinishInputCount = 0;
private int mSelStart = -1;
private int mSelEnd = -1;
private int mCompositionStart = -1;
private int mCompositionEnd = -1;
@Nullable
private EditorInfo mEditorInfo;
@Nullable
private final DebugMessageCallback mDebugMessageCallback;
void onStartInput(EditorInfo attribute, boolean restarting) {
++mStartInputCount;
mState = restarting ? State.InputRestarted : State.InputStarted;
mSelStart = attribute.initialSelStart;
mSelEnd = attribute.initialSelEnd;
mCompositionStart = -1;
mCompositionEnd = -1;
mEditorInfo = cloneEditorInfo(attribute);
updateMessage();
}
void onFinishInput() {
++mFinishInputCount;
mState = State.InputFinished;
mEditorInfo = null;
updateMessage();
}
void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
int newSelEnd, int candidatesStart, int candidatesEnd) {
++mUpdateSelectionCount;
mSelStart = newSelStart;
mSelEnd = newSelEnd;
mCompositionStart = candidatesStart;
mCompositionEnd = candidatesEnd;
updateMessage();
}
EventMonitor(@Nullable DebugMessageCallback callback) {
mDebugMessageCallback = callback;
}
private void updateMessage() {
if (mDebugMessageCallback == null) {
return;
}
final StringBuilder sb = new StringBuilder();
sb.append("state=").append(mState).append("\n")
.append("startInputCount=").append(mStartInputCount).append("\n")
.append("finishInputCount=").append(mFinishInputCount).append("\n")
.append("updateSelectionCount=").append(mUpdateSelectionCount).append("\n");
if (mSelStart == -1 && mSelEnd == -1) {
sb.append("selection=none\n");
} else {
sb.append("selection=(").append(mSelStart).append(",").append(mSelEnd).append(")\n");
}
if (mCompositionStart == -1 && mCompositionEnd == -1) {
sb.append("composition=none");
} else {
sb.append("composition=(")
.append(mCompositionStart).append(",").append(mCompositionEnd).append(")");
}
if (mEditorInfo != null) {
sb.append("\n");
sb.append("packageName=").append(mEditorInfo.packageName).append("\n");
sb.append("inputType=");
EditorInfoUtil.dumpInputType(sb, mEditorInfo.inputType);
sb.append("\n");
sb.append("imeOptions=");
EditorInfoUtil.dumpImeOptions(sb, mEditorInfo.imeOptions);
}
mDebugMessageCallback.onMessageChanged(sb.toString());
}
private static EditorInfo cloneEditorInfo(EditorInfo original) {
Parcel parcel = null;
try {
parcel = Parcel.obtain();
original.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
return EditorInfo.CREATOR.createFromParcel(parcel);
} finally {
if (parcel != null) {
parcel.recycle();
}
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
final class OverlayWindowBuilder {
@NonNull
private final View mContentView;
private int mWidth = WindowManager.LayoutParams.WRAP_CONTENT;
private int mHeight = WindowManager.LayoutParams.WRAP_CONTENT;
private int mGravity = Gravity.NO_GRAVITY;
private int mRelX = 0;
private int mRelY = 0;
private Integer mBackgroundColor = null;
private boolean mShown = false;
private OverlayWindowBuilder(@NonNull View contentView) {
mContentView = contentView;
}
static OverlayWindowBuilder from(@NonNull View contentView) {
return new OverlayWindowBuilder(contentView);
}
OverlayWindowBuilder setSize(int width, int height) {
mWidth = width;
mHeight = height;
return this;
}
OverlayWindowBuilder setGravity(int gravity) {
mGravity = gravity;
return this;
}
OverlayWindowBuilder setRelativePosition(int relX, int relY) {
mRelX = relX;
mRelY = relY;
return this;
}
OverlayWindowBuilder setBackgroundColor(@ColorInt int color) {
mBackgroundColor = color;
return this;
}
void show() {
if (mShown) {
throw new UnsupportedOperationException("show() can be called only once.");
}
final Context context = mContentView.getContext();
final WindowManager windowManager = context.getSystemService(WindowManager.class);
final FrameLayout contentFrame = new FrameLayout(context) {
@Override
public boolean requestSendAccessibilityEvent(View view, AccessibilityEvent event) {
return false;
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
}
};
if (mBackgroundColor != null) {
contentFrame.setBackgroundColor(mBackgroundColor);
}
contentFrame.setOnTouchListener(new DragToMoveTouchListener((dx, dy) -> {
final WindowManager.LayoutParams lp =
(WindowManager.LayoutParams) contentFrame.getLayoutParams();
lp.x += dx;
lp.y += dy;
windowManager.updateViewLayout(contentFrame, lp);
}));
contentFrame.addView(mContentView);
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
mWidth, mHeight,
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.gravity = mGravity;
params.x = mRelX;
params.y = mRelY;
windowManager.addView(contentFrame, params);
mShown = true;
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.InputMethod;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import android.view.Gravity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import java.util.function.BiConsumer;
/**
* A sample {@link AccessibilityService} to demo how to use IME APIs.
*/
public final class SampleInputMethodAccessibilityService extends AccessibilityService {
private static final String TAG = "SampleImeA11yService";
private EventMonitor mEventMonitor;
private final class InputMethodImpl extends InputMethod {
InputMethodImpl(AccessibilityService service) {
super(service);
}
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
Log.d(TAG, String.format("onStartInput(%s,%b)", attribute, restarting));
mEventMonitor.onStartInput(attribute, restarting);
}
@Override
public void onFinishInput() {
Log.d(TAG, "onFinishInput()");
mEventMonitor.onFinishInput();
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
int newSelEnd, int candidatesStart, int candidatesEnd) {
Log.d(TAG, String.format("onUpdateSelection(%d,%d,%d,%d,%d,%d)", oldSelStart, oldSelEnd,
newSelStart, newSelEnd, candidatesStart, candidatesEnd));
mEventMonitor.onUpdateSelection(oldSelStart, oldSelEnd,
newSelStart, newSelEnd, candidatesStart, candidatesEnd);
}
}
private static <T> Pair<CharSequence, T> item(@NonNull CharSequence label, @Nullable T value) {
return Pair.create(label, value);
}
private <T> void addButtons(@NonNull LinearLayout parentView, @NonNull String headerText,
@NonNull List<Pair<CharSequence, T>> items,
@NonNull BiConsumer<T, InputMethod.AccessibilityInputConnection> action) {
final LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
{
final TextView headerTextView = new TextView(this, null,
android.R.attr.listSeparatorTextViewStyle);
headerTextView.setAllCaps(false);
headerTextView.setText(headerText);
layout.addView(headerTextView);
}
{
final LinearLayout itemLayout = new LinearLayout(this);
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
for (Pair<CharSequence, T> item : items) {
final Button button = new Button(this, null, android.R.attr.buttonStyleSmall);
button.setAllCaps(false);
button.setText(item.first);
button.setOnClickListener(view -> {
final InputMethod ime = getInputMethod();
if (ime == null) {
return;
}
final InputMethod.AccessibilityInputConnection ic =
ime.getCurrentInputConnection();
if (ic == null) {
return;
}
action.accept(item.second, ic);
});
itemLayout.addView(button);
}
final HorizontalScrollView scrollView = new HorizontalScrollView(this);
scrollView.addView(itemLayout);
layout.addView(scrollView);
}
parentView.addView(layout);
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
final WindowManager windowManager = getSystemService(WindowManager.class);
final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
// Create a monitor window.
{
final TextView textView = new TextView(this);
mEventMonitor = new EventMonitor(textView::setText);
final LinearLayout monitorWindowContent = new LinearLayout(this);
monitorWindowContent.setOrientation(LinearLayout.VERTICAL);
monitorWindowContent.setPadding(10, 10, 10, 10);
monitorWindowContent.addView(textView);
OverlayWindowBuilder.from(monitorWindowContent)
.setSize((metrics.getBounds().width() * 3) / 4,
WindowManager.LayoutParams.WRAP_CONTENT)
.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
.setBackgroundColor(0xeed2e3fc)
.show();
}
final LinearLayout contentView = new LinearLayout(this);
contentView.setOrientation(LinearLayout.VERTICAL);
{
final TextView textView = new TextView(this, null, android.R.attr.windowTitleStyle);
textView.setGravity(Gravity.CENTER);
textView.setText("A11Y IME");
contentView.addView(textView);
}
{
final LinearLayout buttonLayout = new LinearLayout(this);
buttonLayout.setBackgroundColor(0xfffeefc3);
buttonLayout.setPadding(10, 10, 10, 10);
buttonLayout.setOrientation(LinearLayout.VERTICAL);
addButtons(buttonLayout,
"commitText", List.of(
item("A", "A"),
item("Hello World", "Hello World"),
item("\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F",
"\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F")),
(value, ic) -> ic.commitText(value, 1, null));
addButtons(buttonLayout,
"sendKeyEvent", List.of(
item("A", KeyEvent.KEYCODE_A),
item("DEL", KeyEvent.KEYCODE_DEL),
item("DPAD_LEFT", KeyEvent.KEYCODE_DPAD_LEFT),
item("DPAD_RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT),
item("COPY", KeyEvent.KEYCODE_COPY),
item("CUT", KeyEvent.KEYCODE_CUT),
item("PASTE", KeyEvent.KEYCODE_PASTE)),
(keyCode, ic) -> {
final long eventTime = SystemClock.uptimeMillis();
ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(),
KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
});
addButtons(buttonLayout,
"performEditorAction", List.of(
item("UNSPECIFIED", EditorInfo.IME_ACTION_UNSPECIFIED),
item("NONE", EditorInfo.IME_ACTION_NONE),
item("GO", EditorInfo.IME_ACTION_GO),
item("SEARCH", EditorInfo.IME_ACTION_SEARCH),
item("SEND", EditorInfo.IME_ACTION_SEND),
item("NEXT", EditorInfo.IME_ACTION_NEXT),
item("DONE", EditorInfo.IME_ACTION_DONE),
item("PREVIOUS", EditorInfo.IME_ACTION_PREVIOUS)),
(action, ic) -> ic.performEditorAction(action));
addButtons(buttonLayout,
"performContextMenuAction", List.of(
item("selectAll", android.R.id.selectAll),
item("startSelectingText", android.R.id.startSelectingText),
item("stopSelectingText", android.R.id.stopSelectingText),
item("cut", android.R.id.cut),
item("copy", android.R.id.copy),
item("paste", android.R.id.paste),
item("copyUrl", android.R.id.copyUrl),
item("switchInputMethod", android.R.id.switchInputMethod)),
(action, ic) -> ic.performContextMenuAction(action));
addButtons(buttonLayout,
"setSelection", List.of(
item("(0,0)", Pair.create(0, 0)),
item("(0,1)", Pair.create(0, 1)),
item("(1,1)", Pair.create(1, 1)),
item("(0,999)", Pair.create(0, 999))),
(pair, ic) -> ic.setSelection(pair.first, pair.second));
addButtons(buttonLayout,
"deleteSurroundingText", List.of(
item("(0,0)", Pair.create(0, 0)),
item("(0,1)", Pair.create(0, 1)),
item("(1,0)", Pair.create(1, 0)),
item("(1,1)", Pair.create(1, 1)),
item("(999,0)", Pair.create(999, 0)),
item("(0,999)", Pair.create(0, 999))),
(pair, ic) -> ic.deleteSurroundingText(pair.first, pair.second));
final ScrollView scrollView = new ScrollView(this);
scrollView.addView(buttonLayout);
contentView.addView(scrollView);
// Set margin
{
final LinearLayout.LayoutParams lp =
((LinearLayout.LayoutParams) scrollView.getLayoutParams());
lp.leftMargin = lp.rightMargin = lp.bottomMargin = 20;
scrollView.setLayoutParams(lp);
}
}
OverlayWindowBuilder.from(contentView)
.setSize((metrics.getBounds().width() * 3) / 4,
metrics.getBounds().height() / 5)
.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
.setRelativePosition(300, 300)
.setBackgroundColor(0xfffcc934)
.show();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
@Override
public void onInterrupt() {
}
@Override
public InputMethod onCreateInputMethod() {
Log.d(TAG, "onCreateInputMethod");
return new InputMethodImpl(this);
}
}