InputConnection.java 23 KB

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