/* * 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); } }