/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package autosuggest; import android.content.Context; import android.text.Editable; import android.text.Selection; import android.util.Log; import android.view.KeyEvent; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.View; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.SurroundingText; import android.view.inputmethod.InputMethodManager; public class CustomInputConnection extends BaseInputConnection { private static final boolean DEBUG = false; private int id = -1; private View mInternalView; private Editable mEditable; private boolean mSingleLine; private int numBatchEdits; private boolean shouldUpdateImeSelection; native static void onCompose(int id, String text, int newCursorPos, boolean isCommit); native static void onSetComposeRegion(int id, int start, int end); native static void onFinishCompose(int id); native static void onDeleteSurroundingText(int id, int left, int right); public CustomInputConnection(int id, Editable editable, View view) { super(view, true); log("CustomInputConnection()"); this.id = id; mEditable = editable; mInternalView = view; mSingleLine = false; } private void log(String fstr, Object... args) { if (!DEBUG) return; String text = "(" + id + "): " + String.format(fstr, args); Log.d("darkfi", text); } /** * Updates the AdapterInputConnection's internal representation of the text * being edited and its selection and composition properties. The resulting * Editable is accessible through the getEditable() method. * If the text has not changed, this also calls updateSelection on the InputMethodManager. * @param text The String contents of the field being edited * @param selectionStart The character offset of the selection start, or the caret * position if there is no selection * @param selectionEnd The character offset of the selection end, or the caret * position if there is no selection * @param compositionStart The character offset of the composition start, or -1 * if there is no composition * @param compositionEnd The character offset of the composition end, or -1 * if there is no selection */ public void setEditableText(String text, int selectionStart, int selectionEnd, int compositionStart, int compositionEnd) { log("setEditableText(%s, %d, %d, %d, %d)", text, selectionStart, selectionEnd, compositionStart, compositionEnd); int prevSelectionStart = Selection.getSelectionStart(mEditable); int prevSelectionEnd = Selection.getSelectionEnd(mEditable); int prevEditableLength = mEditable.length(); int prevCompositionStart = getComposingSpanStart(mEditable); int prevCompositionEnd = getComposingSpanEnd(mEditable); String prevText = mEditable.toString(); selectionStart = Math.min(selectionStart, text.length()); selectionEnd = Math.min(selectionEnd, text.length()); compositionStart = Math.min(compositionStart, text.length()); compositionEnd = Math.min(compositionEnd, text.length()); boolean textUnchanged = prevText.equals(text); if (textUnchanged && prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd && prevCompositionStart == compositionStart && prevCompositionEnd == compositionEnd) { // Nothing has changed; don't need to do anything return; } // When a programmatic change has been made to the editable field, both the start // and end positions for the composition will equal zero. In this case we cancel the // active composition in the editor as this no longer is relevant. if (textUnchanged && compositionStart == 0 && compositionEnd == 0) { cancelComposition(); } if (!textUnchanged) { log("replace mEditable with: %s", text); mEditable.replace(0, mEditable.length(), text); } Selection.setSelection(mEditable, selectionStart, selectionEnd); super.setComposingRegion(compositionStart, compositionEnd); //log("textUnchanged=%s prevText=%s", textUnchanged, prevText); //if (textUnchanged || prevText.equals("")) { // log("setEditableText updating selection"); // updateSelection should be called when a manual selection change occurs. // Should not be called if text is being entered else issues can occur // e.g. backspace to undo autocorrection will not work with the default OSK. getInputMethodManager().updateSelection(mInternalView, selectionStart, selectionEnd, compositionStart, compositionEnd); //} } @Override public Editable getEditable() { log("getEditable() -> %s", editableToXml(mEditable)); return mEditable; } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { log("setComposingText(%s, %d)", text, newCursorPosition); super.setComposingText(text, newCursorPosition); shouldUpdateImeSelection = true; onCompose(id, text.toString(), newCursorPosition, false); return true; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { log("commitText(%s, %d)", text, newCursorPosition); super.commitText(text, newCursorPosition); shouldUpdateImeSelection = true; onCompose(id, text.toString(), newCursorPosition, text.length() > 0); return true; } @Override public boolean performEditorAction(int actionCode) { log("performEditorAction(%d)", actionCode); switch (actionCode) { case EditorInfo.IME_ACTION_NEXT: cancelComposition(); // Send TAB key event long timeStampMs = System.currentTimeMillis(); //mImeAdapter.sendSyntheticKeyEvent( // sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0); return true; case EditorInfo.IME_ACTION_GO: case EditorInfo.IME_ACTION_SEARCH: //mImeAdapter.dismissInput(true); break; } return super.performEditorAction(actionCode); } @Override public boolean performContextMenuAction(int id) { log("performContextMenuAction(%d)", id); /* switch (id) { case android.R.id.selectAll: return mImeAdapter.selectAll(); case android.R.id.cut: return mImeAdapter.cut(); case android.R.id.copy: return mImeAdapter.copy(); case android.R.id.paste: return mImeAdapter.paste(); default: return false; } */ return false; } @Override public CharSequence getTextAfterCursor(int length, int flags) { log("getTextAfterCursor(%d, %d)", length, flags); return super.getTextAfterCursor(length, flags); } @Override public CharSequence getTextBeforeCursor(int length, int flags) { log("getTextBeforeCursor(%d, %d)", length, flags); return super.getTextBeforeCursor(length, flags); } @Override public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) { log("getSurroundingText(%d, %d, %d)", beforeLength, afterLength, flags); return super.getSurroundingText(beforeLength, afterLength, flags); } @Override public CharSequence getSelectedText(int flags) { log("getSelectedText(%d)", flags); return super.getSelectedText(flags); } @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { log("getExtractedText(...)"); ExtractedText et = new ExtractedText(); et.text = mEditable.toString(); et.partialEndOffset = mEditable.length(); et.selectionStart = Selection.getSelectionStart(mEditable); et.selectionEnd = Selection.getSelectionEnd(mEditable); et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0; return et; } @Override public boolean deleteSurroundingText(int leftLength, int rightLength) { log("deleteSurroundingText(%d, %d)", leftLength, rightLength); if (!super.deleteSurroundingText(leftLength, rightLength)) { return false; } shouldUpdateImeSelection = true; //return mImeAdapter.deleteSurroundingText(leftLength, rightLength); onDeleteSurroundingText(id, leftLength, rightLength); return true; } @Override public boolean sendKeyEvent(KeyEvent event) { int action = event.getAction(); int keycode = event.getKeyCode(); log("sendKeyEvent() [action=%d, keycode=%d]", action, keycode); //mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing(); //mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing(); // If this is a key-up, and backspace/del or if the key has a character representation, // need to update the underlying Editable (i.e. the local representation of the text // being edited). if (event.getAction() == KeyEvent.ACTION_UP) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { super.deleteSurroundingText(1, 0); } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { super.deleteSurroundingText(0, 1); } else { int unicodeChar = event.getUnicodeChar(); if (unicodeChar != 0) { Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); if (selectionStart > selectionEnd) { int temp = selectionStart; selectionStart = selectionEnd; selectionEnd = temp; } String inputChar = Character.toString((char)unicodeChar); editable.replace(selectionStart, selectionEnd, inputChar); onCompose(id, inputChar, selectionStart, true); } } } shouldUpdateImeSelection = true; return super.sendKeyEvent(event); } @Override public boolean finishComposingText() { if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) { log("finishComposingText() [DISABLED]"); return true; } log("finishComposingText()"); super.finishComposingText(); onFinishCompose(id); return true; } @Override public boolean setSelection(int start, int end) { log("setSelection(%d, %d)", start, end); if (start < 0 || end < 0) return true; super.setSelection(start, end); shouldUpdateImeSelection = true; //return mImeAdapter.setEditableSelectionOffsets(start, end); return true; } /** * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there * is no longer a current composition. Note this differs from finishComposingText, which * is called by the IME when it wants to end a composition. */ void cancelComposition() { log("cancelComposition()"); getInputMethodManager().restartInput(mInternalView); } @Override public boolean setComposingRegion(int start, int end) { log("setComposingRegion(%d, %d)", start, end); int a = Math.min(start, end); int b = Math.max(start, end); super.setComposingRegion(a, b); onSetComposeRegion(id, a, b); return true; } boolean isActive() { return getInputMethodManager().isActive(); } private InputMethodManager getInputMethodManager() { InputMethodManager imm = (InputMethodManager)mInternalView.getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) { Log.e("darkfi", "[IC]: InputMethodManager is NULL!"); } return imm; } private void updateImeSelection() { log("updateImeSelection()"); getInputMethodManager().updateSelection( mInternalView, Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable), getComposingSpanStart(mEditable), getComposingSpanEnd(mEditable) ); log("updateImeSelection() DONE"); } @Override public boolean beginBatchEdit() { log("beginBatchEdit"); ++numBatchEdits; return false; } @Override public boolean endBatchEdit() { log("endBatchEdit"); if (--numBatchEdits == 0 && shouldUpdateImeSelection) { updateImeSelection(); shouldUpdateImeSelection = false; } log("endBatchEdit DONE"); return false; } public static String editableToXml(Editable editable) { StringBuilder xmlBuilder = new StringBuilder(); int length = editable.length(); Object[] spans = editable.getSpans(0, editable.length(), Object.class); for (int i = 0; i < length; i++) { // Find spans starting at this position for (Object span : spans) { if (editable.getSpanStart(span) == i) { xmlBuilder .append("<") .append(span.getClass().getSimpleName()) .append(">"); } } // Append the character char c = editable.charAt(i); xmlBuilder.append(c); if (Character.isHighSurrogate(c)) { if (i + 1 < editable.length() && Character.isLowSurrogate(editable.charAt(i + 1))) { i += 1; xmlBuilder.append(editable.charAt(i)); } } // Find spans ending at this position for (Object span : spans) { if (editable.getSpanEnd(span) == i) { xmlBuilder .append(""); } } } // Find spans starting at this position for (Object span : spans) { if (editable.getSpanStart(span) == length) { xmlBuilder .append("<") .append(span.getClass().getSimpleName()) .append(">"); } } // Find spans ending at this position for (Object span : spans) { if (editable.getSpanEnd(span) == length) { xmlBuilder .append(""); } } return xmlBuilder.toString(); } }