Files
darkfi/bin/app/java/autosuggest/CustomInputConnection.java
2026-01-01 11:40:45 +00:00

427 lines
16 KiB
Java

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