mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
714 lines
22 KiB
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;
|
|
}
|
|
}
|
|
}
|