InputConnection.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. /*
  2. * Copyright (C) 2021 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package textinput;
  17. import static android.view.inputmethod.EditorInfo.IME_ACTION_UNSPECIFIED;
  18. import android.app.Activity;
  19. import android.content.Context;
  20. import android.os.Bundle;
  21. import android.text.Editable;
  22. import android.text.InputFilter;
  23. import android.text.Selection;
  24. import android.text.SpannableString;
  25. import android.text.SpannableStringBuilder;
  26. import android.text.Spanned;
  27. import android.text.TextUtils;
  28. import android.util.Log;
  29. import android.view.KeyEvent;
  30. import android.view.View;
  31. import android.view.inputmethod.BaseInputConnection;
  32. import android.view.inputmethod.CompletionInfo;
  33. import android.view.inputmethod.CorrectionInfo;
  34. import android.view.inputmethod.EditorInfo;
  35. import android.view.inputmethod.ExtractedText;
  36. import android.view.inputmethod.ExtractedTextRequest;
  37. import android.view.inputmethod.InputMethodManager;
  38. import androidx.core.graphics.Insets;
  39. import androidx.core.view.ViewCompat;
  40. import androidx.core.view.WindowCompat;
  41. import androidx.core.view.WindowInsetsCompat;
  42. import textinput.GameTextInput.Pair;
  43. public class InputConnection extends BaseInputConnection implements View.OnKeyListener {
  44. private final InputMethodManager imm;
  45. private final View targetView;
  46. private final Settings settings;
  47. private final Editable mEditable;
  48. private Listener listener;
  49. private boolean mSoftKeyboardActive;
  50. private void log(String text) {
  51. //Log.d("darkfi", text);
  52. }
  53. /*
  54. * This class filters EOL characters from the input. For details of how InputFilter.filter
  55. * function works, refer to its documentation. If the suggested change is accepted without
  56. * modifications, filter() should return null.
  57. */
  58. private class SingeLineFilter implements InputFilter {
  59. public CharSequence filter(
  60. CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
  61. boolean keepOriginal = true;
  62. StringBuilder builder = new StringBuilder(end - start);
  63. for (int i = start; i < end; i++) {
  64. char c = source.charAt(i);
  65. if (c == '\n') {
  66. keepOriginal = false;
  67. } else {
  68. builder.append(c);
  69. }
  70. }
  71. if (keepOriginal) {
  72. return null;
  73. }
  74. if (source instanceof Spanned) {
  75. SpannableString s = new SpannableString(builder);
  76. TextUtils.copySpansFrom((Spanned) source, start, builder.length(), null, s, 0);
  77. return s;
  78. } else {
  79. return builder;
  80. }
  81. }
  82. }
  83. private static final int MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT = 5000;
  84. /**
  85. * Constructor
  86. *
  87. * @param ctx The app's context
  88. * @param targetView The view created this input connection
  89. * @param settings EditorInfo and other settings needed by this class
  90. * InputConnection.
  91. */
  92. public InputConnection(Context ctx, View targetView, Settings settings) {
  93. super(targetView, settings.mEditorInfo.inputType != 0);
  94. log("InputConnection created");
  95. this.targetView = targetView;
  96. this.settings = settings;
  97. Object imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
  98. if (imm == null) {
  99. throw new java.lang.RuntimeException("Can't get IMM");
  100. } else {
  101. this.imm = (InputMethodManager) imm;
  102. this.mEditable = (Editable) (new SpannableStringBuilder());
  103. }
  104. // Listen for insets changes
  105. WindowCompat.setDecorFitsSystemWindows(((Activity) targetView.getContext()).getWindow(), false);
  106. targetView.setOnKeyListener(this);
  107. // Apply EditorInfo settings
  108. this.setEditorInfo(settings.mEditorInfo);
  109. }
  110. /**
  111. * Restart the input method manager. This is useful to apply changes to the keyboard
  112. * after calling setEditorInfo.
  113. */
  114. public void restartInput() {
  115. imm.restartInput(targetView);
  116. }
  117. /**
  118. * Get whether the soft keyboard is visible.
  119. *
  120. * @return true if the soft keyboard is visible, false otherwise
  121. */
  122. public final boolean getSoftKeyboardActive() {
  123. return this.mSoftKeyboardActive;
  124. }
  125. /**
  126. * Request the soft keyboard to become visible or invisible.
  127. *
  128. * @param active True if the soft keyboard should be made visible, otherwise false.
  129. * @param flags See
  130. * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#showSoftInput(android.view.View,%20int)
  131. */
  132. public final void setSoftKeyboardActive(boolean active, int flags) {
  133. log("setSoftKeyboardActive, active: " + active);
  134. this.mSoftKeyboardActive = active;
  135. if (active) {
  136. this.targetView.setFocusableInTouchMode(true);
  137. this.targetView.requestFocus();
  138. this.imm.showSoftInput(this.targetView, flags);
  139. } else {
  140. this.imm.hideSoftInputFromWindow(this.targetView.getWindowToken(), flags);
  141. }
  142. restartInput();
  143. }
  144. /**
  145. * Get the current EditorInfo used to configure the InputConnection's behaviour.
  146. *
  147. * @return The current EditorInfo.
  148. */
  149. public final EditorInfo getEditorInfo() {
  150. return this.settings.mEditorInfo;
  151. }
  152. /**
  153. * Set the current EditorInfo used to configure the InputConnection's behaviour.
  154. *
  155. * @param editorInfo The EditorInfo to use
  156. */
  157. public final void setEditorInfo(EditorInfo editorInfo) {
  158. log("setEditorInfo");
  159. settings.mEditorInfo = editorInfo;
  160. // Depending on the multiline state, we might need a different set of filters.
  161. // Filters are being used to filter specific characters for hardware keyboards
  162. // (software input methods already support TYPE_TEXT_FLAG_MULTI_LINE).
  163. if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
  164. mEditable.setFilters(
  165. new InputFilter[] {new InputFilter.LengthFilter(MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT),
  166. new SingeLineFilter()});
  167. } else {
  168. mEditable.setFilters(new InputFilter[] {});
  169. }
  170. }
  171. /**
  172. * Set the text, selection and composing region state.
  173. *
  174. * @param state The state to be used by the IME.
  175. * This replaces any text, selections and composing regions currently active.
  176. */
  177. public final void setState(State state) {
  178. if (state == null)
  179. return;
  180. log(
  181. "setState: '" + state.text + "', selection=(" + state.selectionStart + ","
  182. + state.selectionEnd + "), composing region=(" + state.composingRegionStart + ","
  183. + state.composingRegionEnd + ")");
  184. mEditable.clear();
  185. mEditable.clearSpans();
  186. mEditable.insert(0, (CharSequence) state.text);
  187. setSelection(state.selectionStart, state.selectionEnd);
  188. if (state.composingRegionStart != state.composingRegionEnd) {
  189. setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
  190. }
  191. restartInput();
  192. }
  193. /**
  194. * Get the current listener for state changes.
  195. *
  196. * @return The current Listener
  197. */
  198. public final Listener getListener() {
  199. return listener;
  200. }
  201. /**
  202. * Set a listener for state changes.
  203. *
  204. * @param listener
  205. * @return This InputConnection, for setter chaining.
  206. */
  207. public final InputConnection setListener(Listener listener) {
  208. this.listener = listener;
  209. return this;
  210. }
  211. // From View.OnKeyListener
  212. @Override
  213. public boolean onKey(View view, int i, KeyEvent keyEvent) {
  214. log("onKey: " + keyEvent);
  215. if (!getSoftKeyboardActive()) {
  216. return false;
  217. }
  218. // Don't call sendKeyEvent as it might produce an infinite loop.
  219. if (processKeyEvent(keyEvent)) {
  220. // IMM seems to cache the content of Editable, so we update it with restartInput
  221. // Also it caches selection and composing region, so let's notify it about updates.
  222. stateUpdated();
  223. immUpdateSelection();
  224. restartInput();
  225. return true;
  226. }
  227. return false;
  228. }
  229. // From BaseInputConnection
  230. @Override
  231. public Editable getEditable() {
  232. log("getEditable");
  233. return mEditable;
  234. }
  235. // From BaseInputConnection
  236. @Override
  237. public boolean setSelection(int start, int end) {
  238. log("setSelection: " + start + ":" + end);
  239. return super.setSelection(start, end);
  240. }
  241. // From BaseInputConnection
  242. @Override
  243. public boolean setComposingText(CharSequence text, int newCursorPosition) {
  244. log(
  245. "setComposingText='" + text + "' newCursorPosition=" + newCursorPosition);
  246. if (text == null) {
  247. return false;
  248. }
  249. return super.setComposingText(text, newCursorPosition);
  250. }
  251. @Override
  252. public boolean setComposingRegion(int start, int end) {
  253. log("setComposingRegion: " + start + ":" + end);
  254. return super.setComposingRegion(start, end);
  255. }
  256. // From BaseInputConnection
  257. @Override
  258. public boolean finishComposingText() {
  259. log("finishComposingText");
  260. return super.finishComposingText();
  261. }
  262. @Override
  263. public boolean endBatchEdit() {
  264. log("endBatchEdit");
  265. stateUpdated();
  266. return super.endBatchEdit();
  267. }
  268. @Override
  269. public boolean commitCompletion(CompletionInfo text) {
  270. log("commitCompletion");
  271. return super.commitCompletion(text);
  272. }
  273. @Override
  274. public boolean commitCorrection(CorrectionInfo text) {
  275. log("commitCompletion");
  276. return super.commitCorrection(text);
  277. }
  278. // From BaseInputConnection
  279. @Override
  280. public boolean commitText(CharSequence text, int newCursorPosition) {
  281. log(
  282. (new StringBuilder())
  283. .append("commitText: ")
  284. .append(text)
  285. .append(", new pos = ")
  286. .append(newCursorPosition)
  287. .toString());
  288. return super.commitText(text, newCursorPosition);
  289. }
  290. // From BaseInputConnection
  291. @Override
  292. public boolean deleteSurroundingText(int beforeLength, int afterLength) {
  293. log("deleteSurroundingText: " + beforeLength + ":" + afterLength);
  294. return super.deleteSurroundingText(beforeLength, afterLength);
  295. }
  296. // From BaseInputConnection
  297. @Override
  298. public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
  299. log("deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength);
  300. return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
  301. }
  302. // From BaseInputConnection
  303. @Override
  304. public boolean sendKeyEvent(KeyEvent event) {
  305. log("sendKeyEvent: " + event);
  306. return super.sendKeyEvent(event);
  307. }
  308. // From BaseInputConnection
  309. @Override
  310. public CharSequence getSelectedText(int flags) {
  311. CharSequence result = super.getSelectedText(flags);
  312. if (result == null) {
  313. result = "";
  314. }
  315. log("getSelectedText: " + flags + ", result: " + result);
  316. return result;
  317. }
  318. // From BaseInputConnection
  319. @Override
  320. public CharSequence getTextAfterCursor(int length, int flags) {
  321. log("getTextAfterCursor: " + length + ":" + flags);
  322. if (length < 0) {
  323. Log.i("darkfi", "getTextAfterCursor: returning null to due to an invalid length=" + length);
  324. return null;
  325. }
  326. return super.getTextAfterCursor(length, flags);
  327. }
  328. // From BaseInputConnection
  329. @Override
  330. public CharSequence getTextBeforeCursor(int length, int flags) {
  331. log("getTextBeforeCursor: " + length + ", flags=" + flags);
  332. if (length < 0) {
  333. Log.i("darkfi", "getTextBeforeCursor: returning null to due to an invalid length=" + length);
  334. return null;
  335. }
  336. return super.getTextBeforeCursor(length, flags);
  337. }
  338. // From BaseInputConnection
  339. @Override
  340. public boolean requestCursorUpdates(int cursorUpdateMode) {
  341. log("Request cursor updates: " + cursorUpdateMode);
  342. return super.requestCursorUpdates(cursorUpdateMode);
  343. }
  344. // From BaseInputConnection
  345. @Override
  346. public void closeConnection() {
  347. log("closeConnection");
  348. super.closeConnection();
  349. }
  350. @Override
  351. public boolean setImeConsumesInput(boolean imeConsumesInput) {
  352. log("setImeConsumesInput: " + imeConsumesInput);
  353. return super.setImeConsumesInput(imeConsumesInput);
  354. }
  355. @Override
  356. public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
  357. log("getExtractedText");
  358. return super.getExtractedText(request, flags);
  359. }
  360. @Override
  361. public boolean performPrivateCommand(String action, Bundle data) {
  362. log("performPrivateCommand");
  363. return super.performPrivateCommand(action, data);
  364. }
  365. private void immUpdateSelection() {
  366. Pair selection = this.getSelection();
  367. Pair cr = this.getComposingRegion();
  368. log(
  369. "immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + ","
  370. + cr.second);
  371. settings.mEditorInfo.initialSelStart = selection.first;
  372. settings.mEditorInfo.initialSelEnd = selection.second;
  373. imm.updateSelection(targetView, selection.first, selection.second, cr.first, cr.second);
  374. }
  375. private Pair getSelection() {
  376. return new Pair(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
  377. }
  378. private Pair getComposingRegion() {
  379. return new Pair(getComposingSpanStart(mEditable), getComposingSpanEnd(mEditable));
  380. }
  381. private boolean processKeyEvent(KeyEvent event) {
  382. if (event == null) {
  383. return false;
  384. }
  385. int keyCode = event.getKeyCode();
  386. log(
  387. "processKeyEvent(key=" + keyCode + ") text=" + this.mEditable.toString());
  388. // Filter out Enter keys if multi-line mode is disabled.
  389. if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0
  390. && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
  391. && event.hasNoModifiers()) {
  392. sendEditorAction(settings.mEditorInfo.actionId);
  393. return true;
  394. }
  395. if (event.getAction() != KeyEvent.ACTION_DOWN) {
  396. return false;
  397. }
  398. // If no selection is set, move the selection to the end.
  399. // This is the case when first typing on keys when the selection is not set.
  400. // Note that for InputType.TYPE_CLASS_TEXT, this is not be needed because the
  401. // selection is set in setComposingText.
  402. Pair selection = this.getSelection();
  403. if (selection.first == -1) {
  404. selection.first = this.mEditable.length();
  405. selection.second = this.mEditable.length();
  406. }
  407. if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
  408. if (selection.first == selection.second) {
  409. int newIndex = findIndexBackward(mEditable, selection.first, 1);
  410. setSelection(newIndex, newIndex);
  411. } else {
  412. setSelection(selection.first, selection.first);
  413. }
  414. return true;
  415. }
  416. if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
  417. if (selection.first == selection.second) {
  418. int newIndex = findIndexForward(mEditable, selection.second, 1);
  419. setSelection(newIndex, newIndex);
  420. } else {
  421. setSelection(selection.second, selection.second);
  422. }
  423. return true;
  424. }
  425. if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
  426. setSelection(0, 0);
  427. return true;
  428. }
  429. if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
  430. setSelection(this.mEditable.length(), this.mEditable.length());
  431. return true;
  432. }
  433. if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
  434. if (selection.first != selection.second) {
  435. this.mEditable.delete(selection.first, selection.second);
  436. return true;
  437. }
  438. if (keyCode == KeyEvent.KEYCODE_DEL) {
  439. if (selection.first > 0) {
  440. finishComposingText();
  441. deleteSurroundingTextInCodePoints(1, 0);
  442. return true;
  443. }
  444. }
  445. if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
  446. if (selection.first < this.mEditable.length()) {
  447. finishComposingText();
  448. deleteSurroundingTextInCodePoints(0, 1);
  449. return true;
  450. }
  451. }
  452. return false;
  453. }
  454. if (event.getUnicodeChar() == 0) {
  455. return false;
  456. }
  457. if (selection.first != selection.second) {
  458. log("processKeyEvent: deleting selection");
  459. this.mEditable.delete(selection.first, selection.second);
  460. }
  461. String charsToInsert = Character.toString((char) event.getUnicodeChar());
  462. this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
  463. int length = this.mEditable.length();
  464. // Same logic as in setComposingText(): we must update composing region,
  465. // so make sure it points to a valid range.
  466. Pair composingRegion = this.getComposingRegion();
  467. if (composingRegion.first == -1) {
  468. composingRegion = this.getSelection();
  469. if (composingRegion.first == -1) {
  470. composingRegion = new Pair(0, 0);
  471. }
  472. }
  473. composingRegion.second = composingRegion.first + length;
  474. this.setComposingRegion(composingRegion.first, composingRegion.second);
  475. int new_cursor = selection.first + charsToInsert.length();
  476. setSelection(new_cursor, new_cursor);
  477. log("processKeyEvent: exit, text=" + this.mEditable.toString());
  478. return true;
  479. }
  480. private final void stateUpdated() {
  481. Pair selection = this.getSelection();
  482. Pair cr = this.getComposingRegion();
  483. State state = new State(
  484. this.mEditable.toString(), selection.first, selection.second, cr.first, cr.second);
  485. settings.mEditorInfo.initialSelStart = selection.first;
  486. settings.mEditorInfo.initialSelEnd = selection.second;
  487. // Keep a reference to the listener to avoid a race condition when setting the listener.
  488. Listener listener = this.listener;
  489. // We always propagate state change events because unfortunately keyboard visibility functions
  490. // are unreliable, and text editor logic should not depend on them.
  491. if (listener != null) {
  492. listener.stateChanged(state, /*dismissed=*/false);
  493. }
  494. }
  495. /**
  496. * Get the current IME insets.
  497. *
  498. * @return The current IME insets
  499. */
  500. public Insets getImeInsets() {
  501. if (this.targetView == null) {
  502. return Insets.NONE;
  503. }
  504. WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this.targetView);
  505. if (insets == null) {
  506. return Insets.NONE;
  507. }
  508. return insets.getInsets(WindowInsetsCompat.Type.ime());
  509. }
  510. /**
  511. * Returns true if software keyboard is visible, false otherwise.
  512. *
  513. * @return whether software IME is visible or not.
  514. */
  515. public boolean isSoftwareKeyboardVisible() {
  516. if (this.targetView == null) {
  517. return false;
  518. }
  519. WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this.targetView);
  520. if (insets == null) {
  521. return false;
  522. }
  523. return insets.isVisible(WindowInsetsCompat.Type.ime());
  524. }
  525. /**
  526. * This is an event handler from InputConnection interface.
  527. * It's called when action button is triggered (typically this means Enter was pressed).
  528. *
  529. * @param action Action code, either one from EditorInfo.imeOptions or a custom one.
  530. * @return Returns true on success, false if the input connection is no longer valid.
  531. */
  532. @Override
  533. public boolean performEditorAction(int action) {
  534. log("performEditorAction, action=" + action);
  535. if (action == IME_ACTION_UNSPECIFIED) {
  536. // Super emulates Enter key press/release
  537. return super.performEditorAction(action);
  538. }
  539. return sendEditorAction(action);
  540. }
  541. /**
  542. * Delivers editor action to listener
  543. *
  544. * @param action Action code, either one from EditorInfo.imeOptions or a custom one.
  545. * @return Returns true on success, false if the input connection is no longer valid.
  546. */
  547. private boolean sendEditorAction(int action) {
  548. Listener listener = this.listener;
  549. if (listener != null) {
  550. listener.onEditorAction(action);
  551. return true;
  552. }
  553. return false;
  554. }
  555. private static int INVALID_INDEX = -1;
  556. // Implementation copy from BaseInputConnection
  557. private static int findIndexBackward(
  558. final CharSequence cs, final int from, final int numCodePoints) {
  559. int currentIndex = from;
  560. boolean waitingHighSurrogate = false;
  561. final int N = cs.length();
  562. if (currentIndex < 0 || N < currentIndex) {
  563. return INVALID_INDEX; // The starting point is out of range.
  564. }
  565. if (numCodePoints < 0) {
  566. return INVALID_INDEX; // Basically this should not happen.
  567. }
  568. int remainingCodePoints = numCodePoints;
  569. while (true) {
  570. if (remainingCodePoints == 0) {
  571. return currentIndex; // Reached to the requested length in code points.
  572. }
  573. --currentIndex;
  574. if (currentIndex < 0) {
  575. if (waitingHighSurrogate) {
  576. return INVALID_INDEX; // An invalid surrogate pair is found.
  577. }
  578. return 0; // Reached to the beginning of the text w/o any invalid surrogate pair.
  579. }
  580. final char c = cs.charAt(currentIndex);
  581. if (waitingHighSurrogate) {
  582. if (!java.lang.Character.isHighSurrogate(c)) {
  583. return INVALID_INDEX; // An invalid surrogate pair is found.
  584. }
  585. waitingHighSurrogate = false;
  586. --remainingCodePoints;
  587. continue;
  588. }
  589. if (!java.lang.Character.isSurrogate(c)) {
  590. --remainingCodePoints;
  591. continue;
  592. }
  593. if (java.lang.Character.isHighSurrogate(c)) {
  594. return INVALID_INDEX; // A invalid surrogate pair is found.
  595. }
  596. waitingHighSurrogate = true;
  597. }
  598. }
  599. // Implementation copy from BaseInputConnection
  600. private static int findIndexForward(
  601. final CharSequence cs, final int from, final int numCodePoints) {
  602. int currentIndex = from;
  603. boolean waitingLowSurrogate = false;
  604. final int N = cs.length();
  605. if (currentIndex < 0 || N < currentIndex) {
  606. return INVALID_INDEX; // The starting point is out of range.
  607. }
  608. if (numCodePoints < 0) {
  609. return INVALID_INDEX; // Basically this should not happen.
  610. }
  611. int remainingCodePoints = numCodePoints;
  612. while (true) {
  613. if (remainingCodePoints == 0) {
  614. return currentIndex; // Reached to the requested length in code points.
  615. }
  616. if (currentIndex >= N) {
  617. if (waitingLowSurrogate) {
  618. return INVALID_INDEX; // An invalid surrogate pair is found.
  619. }
  620. return N; // Reached to the end of the text w/o any invalid surrogate pair.
  621. }
  622. final char c = cs.charAt(currentIndex);
  623. if (waitingLowSurrogate) {
  624. if (!java.lang.Character.isLowSurrogate(c)) {
  625. return INVALID_INDEX; // An invalid surrogate pair is found.
  626. }
  627. --remainingCodePoints;
  628. waitingLowSurrogate = false;
  629. ++currentIndex;
  630. continue;
  631. }
  632. if (!java.lang.Character.isSurrogate(c)) {
  633. --remainingCodePoints;
  634. ++currentIndex;
  635. continue;
  636. }
  637. if (java.lang.Character.isLowSurrogate(c)) {
  638. return INVALID_INDEX; // A invalid surrogate pair is found.
  639. }
  640. waitingLowSurrogate = true;
  641. ++currentIndex;
  642. }
  643. }
  644. }