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