| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 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 <https://www.gnu.org/licenses/>.
- */
- 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("</")
- .append(span.getClass().getSimpleName())
- .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("</")
- .append(span.getClass().getSimpleName())
- .append(">");
- }
- }
- return xmlBuilder.toString();
- }
- }
|