diff --git a/samples/SampleInputMethodAccessibilityService/Android.bp b/samples/SampleInputMethodAccessibilityService/Android.bp
new file mode 100644
index 000000000..311004b1e
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/Android.bp
@@ -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",
+ ],
+}
diff --git a/samples/SampleInputMethodAccessibilityService/AndroidManifest.xml b/samples/SampleInputMethodAccessibilityService/AndroidManifest.xml
new file mode 100755
index 000000000..7f85fb936
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/SampleInputMethodAccessibilityService/res/xml/accessibility.xml b/samples/SampleInputMethodAccessibilityService/res/xml/accessibility.xml
new file mode 100644
index 000000000..d80e46a6b
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/res/xml/accessibility.xml
@@ -0,0 +1,21 @@
+
+
+
diff --git a/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/DragToMoveTouchListener.java b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/DragToMoveTouchListener.java
new file mode 100644
index 000000000..08e4b20ac
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/DragToMoveTouchListener.java
@@ -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;
+ }
+}
diff --git a/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/EditorInfoUtil.java b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/EditorInfoUtil.java
new file mode 100644
index 000000000..74206aa60
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/EditorInfoUtil.java
@@ -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");
+ }
+ }
+}
diff --git a/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/EventMonitor.java b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/EventMonitor.java
new file mode 100644
index 000000000..ca673b32c
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/EventMonitor.java
@@ -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();
+ }
+ }
+ }
+}
diff --git a/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/OverlayWindowBuilder.java b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/OverlayWindowBuilder.java
new file mode 100644
index 000000000..87542f838
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/OverlayWindowBuilder.java
@@ -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;
+ }
+}
diff --git a/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/SampleInputMethodAccessibilityService.java b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/SampleInputMethodAccessibilityService.java
new file mode 100644
index 000000000..74c9e455e
--- /dev/null
+++ b/samples/SampleInputMethodAccessibilityService/src/com/example/android/sampleinputmethodaccessibilityservice/SampleInputMethodAccessibilityService.java
@@ -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 Pair item(@NonNull CharSequence label, @Nullable T value) {
+ return Pair.create(label, value);
+ }
+
+ private void addButtons(@NonNull LinearLayout parentView, @NonNull String headerText,
+ @NonNull List> items,
+ @NonNull BiConsumer 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 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);
+ }
+}