package com.javispedro.vndroid; import android.os.Bundle; import android.util.Log; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.Nullable; import com.javispedro.vndroid.keymaps.KeyActionHandler; import com.javispedro.vndroid.keymaps.KeyHandler; import java.util.ArrayList; import java.util.List; public class KeyEventOutput implements KeyActionHandler { private final String TAG = KeyEventOutput.class.getSimpleName(); private final List handlers = new ArrayList(); @Nullable private AccessibilityNodeInfo getFocusNode() { ControlService service = ControlService.getInstance(); if (service != null) { return service.findFocus(AccessibilityNodeInfo.FOCUS_INPUT); } return null; } public void addHandler(KeyHandler handler) { handler.setActionHandler(this); handlers.add(handler); } public void postKeyEvent(int key, boolean state) { if (state) Log.d(TAG, "keysym pressed: " + Integer.toHexString(key)); for (KeyHandler handler : handlers) { boolean handled; if (state) { handled = handler.keyDown(key); } else { handled = handler.keyUp(key); } if (handled) { break; } } } @Override public void postText(CharSequence text) { AccessibilityNodeInfo node = getFocusNode(); if (node == null || !node.isEditable()) { Log.d(TAG, "no input focus or not editable"); return; } final CharSequence curText = node.getText(); final int curTextLen = curText != null ? curText.length() : 0; final int textLen = text.length(); int selStart = node.getTextSelectionStart(); int selEnd = node.getTextSelectionEnd(); StringBuilder builder = new StringBuilder(curTextLen + textLen); Log.d(TAG, " cur text: " + curText + " start=" + selStart + " end=" + selEnd); if (selStart == -1 || selEnd == -1 || curTextLen == 0) { // No selection, no cursor if (curTextLen > 0) builder.append(curText); builder.append(text); } else { builder.append(curText.subSequence(0, selStart)); builder.append(text); builder.append(curText.subSequence(selEnd, curText.length())); selStart = selEnd + textLen; selEnd = selStart; } Log.d(TAG, " new text: " + builder.toString() + " start=" + selStart + " end=" + selEnd); Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, builder.toString()); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); if (selStart != -1 && selEnd != -1 && selStart != builder.length()) { args = new Bundle(); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, selStart); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, selEnd); node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, args); } } @Override public void postChar(char c) { AccessibilityNodeInfo node = getFocusNode(); if (node == null || !node.isEditable()) { Log.d(TAG, "no input focus or not editable"); return; } final CharSequence curText = node.getText(); final int curTextLen = curText != null ? curText.length() : 0; int selStart = node.getTextSelectionStart(); int selEnd = node.getTextSelectionEnd(); StringBuilder builder = new StringBuilder(curTextLen + 1); Log.d(TAG, " cur text: " + curText + " start=" + selStart + " end=" + selEnd); if (selStart == -1 || selEnd == -1 || curTextLen == 0) { // No selection, no cursor if (curTextLen > 0) builder.append(curText); builder.append(c); } else { builder.append(curText.subSequence(0, selStart)); builder.append(c); builder.append(curText.subSequence(selEnd, curText.length())); selStart = selEnd + 1; selEnd = selStart; } Log.d(TAG, " new text: " + builder.toString() + " start=" + selStart + " end=" + selEnd); Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, builder.toString()); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); if (selStart != -1 && selEnd != -1 && selStart != builder.length()) { args = new Bundle(); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, selStart); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, selEnd); node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, args); } } @Override public void postLocalAction(int action) { AccessibilityNodeInfo node = getFocusNode(); if (node == null || !node.isEditable()) { Log.d(TAG, "no input focus or not editable"); return; } final CharSequence curText = node.getText(); final int curTextLen = curText != null ? curText.length() : 0; int selStart = node.getTextSelectionStart(); int selEnd = node.getTextSelectionEnd(); StringBuilder builder = null; Log.d(TAG, " cur text: " + curText + " start=" + selStart + " end=" + selEnd); switch (action) { case KeyActionHandler.ACTION_BACKSPACE: if (selStart == -1 || selEnd == -1 || curTextLen == 0) { return; } builder = new StringBuilder(curTextLen - 1); if (selStart == selEnd && selStart > 0) { builder.append(curText.subSequence(0, selStart - 1)); builder.append(curText.subSequence(selEnd, curTextLen)); selStart = selStart - 1; selEnd = selStart; } else if (selStart != selEnd) { builder.append(curText.subSequence(0, selStart)); builder.append(curText.subSequence(selEnd, curTextLen)); selEnd = selStart; } else { return; } break; case KeyActionHandler.ACTION_DEL: if (selStart == -1 || selEnd == -1 || curTextLen == 0) { return; } builder = new StringBuilder(curTextLen - 1); if (selStart == selEnd && selStart < curTextLen) { builder.append(curText.subSequence(0, selStart)); builder.append(curText.subSequence(selEnd + 1, curTextLen)); selStart = selStart; selEnd = selStart; } else if (selStart != selEnd) { builder.append(curText.subSequence(0, selStart)); builder.append(curText.subSequence(selEnd, curTextLen)); selEnd = selStart; } else { return; } break; case KeyActionHandler.ACTION_MOVE_LEFT: if (selStart == -1 || selEnd == -1 || curTextLen == 0) { return; } if (selStart > 0) { selStart = selStart - 1; selEnd = selStart; } else { return; } break; case KeyActionHandler.ACTION_MOVE_RIGHT: if (selStart == -1 || selEnd == -1 || curTextLen == 0) { return; } if (selEnd < curTextLen) { selStart = selEnd + 1; selEnd = selStart; } else { return; } break; case KeyActionHandler.ACTION_MOVE_LEFTMOST: selStart = 0; selEnd = 0; break; case KeyActionHandler.ACTION_MOVE_RIGHTMOST: if (curTextLen == 0) { return; } selStart = curTextLen; selEnd = curTextLen; break; case KeyActionHandler.ACTION_SELECT_LEFT: if (selStart == -1 || selEnd == -1 || curTextLen == 0) { return; } if (selStart > 0) { selStart = selStart - 1; } else { return; } break; case KeyActionHandler.ACTION_SELECT_RIGHT: if (selStart == -1 || selEnd == -1 || curTextLen == 0) { return; } if (selEnd < curTextLen) { selEnd = selEnd + 1; } else { return; } break; case KeyActionHandler.ACTION_SELECT_LEFTMOST: selStart = 0; break; case KeyActionHandler.ACTION_SELECT_RIGHTMOST: if (curTextLen == 0) { return; } selEnd = curTextLen; break; default: return; } if (builder != null) { Log.d(TAG, " new text: " + builder.toString() + " start=" + selStart + " end=" + selEnd); } else { Log.d(TAG, " new start=" + selStart + " end=" + selEnd); } if (builder != null && builder.toString() != curText) { Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, builder.toString()); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); } if (selStart != -1 && selEnd != -1 && (builder == null || selStart != builder.length())) { Bundle args = new Bundle(); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, selStart); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, selEnd); node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, args); } } @Override public void postGlobalAction(int action) { ControlService service = ControlService.getInstance(); if (service != null) { if (!service.performGlobalAction(action)) { Log.e(TAG, "could not perform global action: " + action); } } } }