Files
darkfi/bin/app/java/textinput/InputConnection.java

714 lines
22 KiB
Java

/*
* Copyright (C) 2021 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 textinput;
import static android.view.inputmethod.EditorInfo.IME_ACTION_UNSPECIFIED;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Selection;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputMethodManager;
import android.graphics.Insets;
import android.view.WindowInsets;
import textinput.GameTextInput.Pair;
public class InputConnection extends BaseInputConnection implements View.OnKeyListener {
private final InputMethodManager imm;
private final View targetView;
private final Settings settings;
private final Editable mEditable;
private Listener listener;
private boolean mSoftKeyboardActive;
private void log(String text) {
//Log.d("darkfi", text);
}
/*
* This class filters EOL characters from the input. For details of how InputFilter.filter
* function works, refer to its documentation. If the suggested change is accepted without
* modifications, filter() should return null.
*/
private class SingeLineFilter implements InputFilter {
public CharSequence filter(
CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
boolean keepOriginal = true;
StringBuilder builder = new StringBuilder(end - start);
for (int i = start; i < end; i++) {
char c = source.charAt(i);
if (c == '\n') {
keepOriginal = false;
} else {
builder.append(c);
}
}
if (keepOriginal) {
return null;
}
if (source instanceof Spanned) {
SpannableString s = new SpannableString(builder);
TextUtils.copySpansFrom((Spanned) source, start, builder.length(), null, s, 0);
return s;
} else {
return builder;
}
}
}
private static final int MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT = 5000;
/**
* Constructor
*
* @param ctx The app's context
* @param targetView The view created this input connection
* @param settings EditorInfo and other settings needed by this class
* InputConnection.
*/
public InputConnection(Context ctx, View targetView, Settings settings) {
super(targetView, settings.mEditorInfo.inputType != 0);
log("InputConnection created");
this.targetView = targetView;
this.settings = settings;
Object imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) {
throw new java.lang.RuntimeException("Can't get IMM");
} else {
this.imm = (InputMethodManager) imm;
this.mEditable = (Editable) (new SpannableStringBuilder());
}
// Listen for insets changes
targetView.setOnKeyListener(this);
// Apply EditorInfo settings
this.setEditorInfo(settings.mEditorInfo);
}
/**
* Restart the input method manager. This is useful to apply changes to the keyboard
* after calling setEditorInfo.
*/
public void restartInput() {
imm.restartInput(targetView);
}
/**
* Get whether the soft keyboard is visible.
*
* @return true if the soft keyboard is visible, false otherwise
*/
public final boolean getSoftKeyboardActive() {
return this.mSoftKeyboardActive;
}
/**
* Request the soft keyboard to become visible or invisible.
*
* @param active True if the soft keyboard should be made visible, otherwise false.
* @param flags See
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#showSoftInput(android.view.View,%20int)
*/
public final void setSoftKeyboardActive(boolean active, int flags) {
log("setSoftKeyboardActive, active: " + active);
this.mSoftKeyboardActive = active;
if (active) {
this.targetView.setFocusableInTouchMode(true);
this.targetView.requestFocus();
this.imm.showSoftInput(this.targetView, flags);
} else {
this.imm.hideSoftInputFromWindow(this.targetView.getWindowToken(), flags);
}
restartInput();
}
/**
* Get the current EditorInfo used to configure the InputConnection's behaviour.
*
* @return The current EditorInfo.
*/
public final EditorInfo getEditorInfo() {
return this.settings.mEditorInfo;
}
/**
* Set the current EditorInfo used to configure the InputConnection's behaviour.
*
* @param editorInfo The EditorInfo to use
*/
public final void setEditorInfo(EditorInfo editorInfo) {
log("setEditorInfo");
settings.mEditorInfo = editorInfo;
// Depending on the multiline state, we might need a different set of filters.
// Filters are being used to filter specific characters for hardware keyboards
// (software input methods already support TYPE_TEXT_FLAG_MULTI_LINE).
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
mEditable.setFilters(
new InputFilter[] {new InputFilter.LengthFilter(MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT),
new SingeLineFilter()});
} else {
mEditable.setFilters(new InputFilter[] {});
}
}
/**
* Set the text, selection and composing region state.
*
* @param state The state to be used by the IME.
* This replaces any text, selections and composing regions currently active.
*/
public final void setState(State state) {
if (state == null)
return;
log(
"setState: '" + state.text + "', selection=(" + state.selectionStart + ","
+ state.selectionEnd + "), composing region=(" + state.composingRegionStart + ","
+ state.composingRegionEnd + ")");
mEditable.clear();
mEditable.clearSpans();
mEditable.insert(0, (CharSequence) state.text);
setSelection(state.selectionStart, state.selectionEnd);
if (state.composingRegionStart != state.composingRegionEnd) {
setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
}
restartInput();
}
/**
* Get the current listener for state changes.
*
* @return The current Listener
*/
public final Listener getListener() {
return listener;
}
/**
* Set a listener for state changes.
*
* @param listener
* @return This InputConnection, for setter chaining.
*/
public final InputConnection setListener(Listener listener) {
this.listener = listener;
return this;
}
// From View.OnKeyListener
@Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
log("onKey: " + keyEvent);
if (!getSoftKeyboardActive()) {
return false;
}
// Don't call sendKeyEvent as it might produce an infinite loop.
if (processKeyEvent(keyEvent)) {
// IMM seems to cache the content of Editable, so we update it with restartInput
// Also it caches selection and composing region, so let's notify it about updates.
stateUpdated();
immUpdateSelection();
restartInput();
return true;
}
return false;
}
// From BaseInputConnection
@Override
public Editable getEditable() {
log("getEditable");
return mEditable;
}
// From BaseInputConnection
@Override
public boolean setSelection(int start, int end) {
log("setSelection: " + start + ":" + end);
return super.setSelection(start, end);
}
// From BaseInputConnection
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
log(
"setComposingText='" + text + "' newCursorPosition=" + newCursorPosition);
if (text == null) {
return false;
}
return super.setComposingText(text, newCursorPosition);
}
@Override
public boolean setComposingRegion(int start, int end) {
log("setComposingRegion: " + start + ":" + end);
return super.setComposingRegion(start, end);
}
// From BaseInputConnection
@Override
public boolean finishComposingText() {
log("finishComposingText");
return super.finishComposingText();
}
@Override
public boolean endBatchEdit() {
log("endBatchEdit");
stateUpdated();
return super.endBatchEdit();
}
@Override
public boolean commitCompletion(CompletionInfo text) {
log("commitCompletion");
return super.commitCompletion(text);
}
@Override
public boolean commitCorrection(CorrectionInfo text) {
log("commitCompletion");
return super.commitCorrection(text);
}
// From BaseInputConnection
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
log(
(new StringBuilder())
.append("commitText: ")
.append(text)
.append(", new pos = ")
.append(newCursorPosition)
.toString());
return super.commitText(text, newCursorPosition);
}
// From BaseInputConnection
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
log("deleteSurroundingText: " + beforeLength + ":" + afterLength);
return super.deleteSurroundingText(beforeLength, afterLength);
}
// From BaseInputConnection
@Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
log("deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength);
return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
}
// From BaseInputConnection
@Override
public boolean sendKeyEvent(KeyEvent event) {
log("sendKeyEvent: " + event);
return super.sendKeyEvent(event);
}
// From BaseInputConnection
@Override
public CharSequence getSelectedText(int flags) {
CharSequence result = super.getSelectedText(flags);
if (result == null) {
result = "";
}
log("getSelectedText: " + flags + ", result: " + result);
return result;
}
// From BaseInputConnection
@Override
public CharSequence getTextAfterCursor(int length, int flags) {
log("getTextAfterCursor: " + length + ":" + flags);
if (length < 0) {
Log.i("darkfi", "getTextAfterCursor: returning null to due to an invalid length=" + length);
return null;
}
return super.getTextAfterCursor(length, flags);
}
// From BaseInputConnection
@Override
public CharSequence getTextBeforeCursor(int length, int flags) {
log("getTextBeforeCursor: " + length + ", flags=" + flags);
if (length < 0) {
Log.i("darkfi", "getTextBeforeCursor: returning null to due to an invalid length=" + length);
return null;
}
return super.getTextBeforeCursor(length, flags);
}
// From BaseInputConnection
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
log("Request cursor updates: " + cursorUpdateMode);
return super.requestCursorUpdates(cursorUpdateMode);
}
// From BaseInputConnection
@Override
public void closeConnection() {
log("closeConnection");
super.closeConnection();
}
@Override
public boolean setImeConsumesInput(boolean imeConsumesInput) {
log("setImeConsumesInput: " + imeConsumesInput);
return super.setImeConsumesInput(imeConsumesInput);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
log("getExtractedText");
return super.getExtractedText(request, flags);
}
@Override
public boolean performPrivateCommand(String action, Bundle data) {
log("performPrivateCommand");
return super.performPrivateCommand(action, data);
}
private void immUpdateSelection() {
Pair selection = this.getSelection();
Pair cr = this.getComposingRegion();
log(
"immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + ","
+ cr.second);
settings.mEditorInfo.initialSelStart = selection.first;
settings.mEditorInfo.initialSelEnd = selection.second;
imm.updateSelection(targetView, selection.first, selection.second, cr.first, cr.second);
}
private Pair getSelection() {
return new Pair(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
}
private Pair getComposingRegion() {
return new Pair(getComposingSpanStart(mEditable), getComposingSpanEnd(mEditable));
}
private boolean processKeyEvent(KeyEvent event) {
if (event == null) {
return false;
}
int keyCode = event.getKeyCode();
log(
"processKeyEvent(key=" + keyCode + ") text=" + this.mEditable.toString());
// Filter out Enter keys if multi-line mode is disabled.
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0
&& (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
&& event.hasNoModifiers()) {
sendEditorAction(settings.mEditorInfo.actionId);
return true;
}
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
// If no selection is set, move the selection to the end.
// This is the case when first typing on keys when the selection is not set.
// Note that for InputType.TYPE_CLASS_TEXT, this is not be needed because the
// selection is set in setComposingText.
Pair selection = this.getSelection();
if (selection.first == -1) {
selection.first = this.mEditable.length();
selection.second = this.mEditable.length();
}
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
if (selection.first == selection.second) {
int newIndex = findIndexBackward(mEditable, selection.first, 1);
setSelection(newIndex, newIndex);
} else {
setSelection(selection.first, selection.first);
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if (selection.first == selection.second) {
int newIndex = findIndexForward(mEditable, selection.second, 1);
setSelection(newIndex, newIndex);
} else {
setSelection(selection.second, selection.second);
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
setSelection(0, 0);
return true;
}
if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
setSelection(this.mEditable.length(), this.mEditable.length());
return true;
}
if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
if (selection.first != selection.second) {
this.mEditable.delete(selection.first, selection.second);
return true;
}
if (keyCode == KeyEvent.KEYCODE_DEL) {
if (selection.first > 0) {
finishComposingText();
deleteSurroundingTextInCodePoints(1, 0);
return true;
}
}
if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
if (selection.first < this.mEditable.length()) {
finishComposingText();
deleteSurroundingTextInCodePoints(0, 1);
return true;
}
}
return false;
}
if (event.getUnicodeChar() == 0) {
return false;
}
if (selection.first != selection.second) {
log("processKeyEvent: deleting selection");
this.mEditable.delete(selection.first, selection.second);
}
String charsToInsert = Character.toString((char) event.getUnicodeChar());
this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
int length = this.mEditable.length();
// Same logic as in setComposingText(): we must update composing region,
// so make sure it points to a valid range.
Pair composingRegion = this.getComposingRegion();
if (composingRegion.first == -1) {
composingRegion = this.getSelection();
if (composingRegion.first == -1) {
composingRegion = new Pair(0, 0);
}
}
composingRegion.second = composingRegion.first + length;
this.setComposingRegion(composingRegion.first, composingRegion.second);
int new_cursor = selection.first + charsToInsert.length();
setSelection(new_cursor, new_cursor);
log("processKeyEvent: exit, text=" + this.mEditable.toString());
return true;
}
private final void stateUpdated() {
Pair selection = this.getSelection();
Pair cr = this.getComposingRegion();
State state = new State(
this.mEditable.toString(), selection.first, selection.second, cr.first, cr.second);
settings.mEditorInfo.initialSelStart = selection.first;
settings.mEditorInfo.initialSelEnd = selection.second;
// Keep a reference to the listener to avoid a race condition when setting the listener.
Listener listener = this.listener;
// We always propagate state change events because unfortunately keyboard visibility functions
// are unreliable, and text editor logic should not depend on them.
if (listener != null) {
listener.stateChanged(state, /*dismissed=*/false);
}
}
/**
* Get the current IME insets.
*
* @return The current IME insets
*/
public Insets getImeInsets() {
if (this.targetView == null) {
return Insets.NONE;
}
WindowInsets insets = this.targetView.getRootWindowInsets();
if (insets == null) {
return Insets.NONE;
}
return insets.getInsets(WindowInsets.Type.ime());
}
/**
* Returns true if software keyboard is visible, false otherwise.
*
* @return whether software IME is visible or not.
*/
public boolean isSoftwareKeyboardVisible() {
if (this.targetView == null) {
return false;
}
WindowInsets insets = this.targetView.getRootWindowInsets();
if (insets == null) {
return false;
}
return insets.isVisible(WindowInsets.Type.ime());
}
/**
* This is an event handler from InputConnection interface.
* It's called when action button is triggered (typically this means Enter was pressed).
*
* @param action Action code, either one from EditorInfo.imeOptions or a custom one.
* @return Returns true on success, false if the input connection is no longer valid.
*/
@Override
public boolean performEditorAction(int action) {
log("performEditorAction, action=" + action);
if (action == IME_ACTION_UNSPECIFIED) {
// Super emulates Enter key press/release
return super.performEditorAction(action);
}
return sendEditorAction(action);
}
/**
* Delivers editor action to listener
*
* @param action Action code, either one from EditorInfo.imeOptions or a custom one.
* @return Returns true on success, false if the input connection is no longer valid.
*/
private boolean sendEditorAction(int action) {
Listener listener = this.listener;
if (listener != null) {
listener.onEditorAction(action);
return true;
}
return false;
}
private static int INVALID_INDEX = -1;
// Implementation copy from BaseInputConnection
private static int findIndexBackward(
final CharSequence cs, final int from, final int numCodePoints) {
int currentIndex = from;
boolean waitingHighSurrogate = false;
final int N = cs.length();
if (currentIndex < 0 || N < currentIndex) {
return INVALID_INDEX; // The starting point is out of range.
}
if (numCodePoints < 0) {
return INVALID_INDEX; // Basically this should not happen.
}
int remainingCodePoints = numCodePoints;
while (true) {
if (remainingCodePoints == 0) {
return currentIndex; // Reached to the requested length in code points.
}
--currentIndex;
if (currentIndex < 0) {
if (waitingHighSurrogate) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
return 0; // Reached to the beginning of the text w/o any invalid surrogate pair.
}
final char c = cs.charAt(currentIndex);
if (waitingHighSurrogate) {
if (!java.lang.Character.isHighSurrogate(c)) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
waitingHighSurrogate = false;
--remainingCodePoints;
continue;
}
if (!java.lang.Character.isSurrogate(c)) {
--remainingCodePoints;
continue;
}
if (java.lang.Character.isHighSurrogate(c)) {
return INVALID_INDEX; // A invalid surrogate pair is found.
}
waitingHighSurrogate = true;
}
}
// Implementation copy from BaseInputConnection
private static int findIndexForward(
final CharSequence cs, final int from, final int numCodePoints) {
int currentIndex = from;
boolean waitingLowSurrogate = false;
final int N = cs.length();
if (currentIndex < 0 || N < currentIndex) {
return INVALID_INDEX; // The starting point is out of range.
}
if (numCodePoints < 0) {
return INVALID_INDEX; // Basically this should not happen.
}
int remainingCodePoints = numCodePoints;
while (true) {
if (remainingCodePoints == 0) {
return currentIndex; // Reached to the requested length in code points.
}
if (currentIndex >= N) {
if (waitingLowSurrogate) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
return N; // Reached to the end of the text w/o any invalid surrogate pair.
}
final char c = cs.charAt(currentIndex);
if (waitingLowSurrogate) {
if (!java.lang.Character.isLowSurrogate(c)) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
--remainingCodePoints;
waitingLowSurrogate = false;
++currentIndex;
continue;
}
if (!java.lang.Character.isSurrogate(c)) {
--remainingCodePoints;
++currentIndex;
continue;
}
if (java.lang.Character.isLowSurrogate(c)) {
return INVALID_INDEX; // A invalid surrogate pair is found.
}
waitingLowSurrogate = true;
++currentIndex;
}
}
}