Merge "Add a test AccessibilityService for A11Y IME API" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
b3c5be0d80
31
samples/SampleInputMethodAccessibilityService/Android.bp
Normal file
31
samples/SampleInputMethodAccessibilityService/Android.bp
Normal 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",
|
||||
],
|
||||
}
|
||||
33
samples/SampleInputMethodAccessibilityService/AndroidManifest.xml
Executable file
33
samples/SampleInputMethodAccessibilityService/AndroidManifest.xml
Executable 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>
|
||||
@@ -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" />
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user