InputConnection.java 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  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 text, selection and composing region state.
  170. *
  171. * @param state The state to be used by the IME.
  172. * This replaces any text, selections and composing regions currently active.
  173. */
  174. public final void setState(State state) {
  175. if (state == null)
  176. return;
  177. log(
  178. "setState: '" + state.text + "', selection=(" + state.selectionStart + ","
  179. + state.selectionEnd + "), composing region=(" + state.composingRegionStart + ","
  180. + state.composingRegionEnd + ")");
  181. mEditable.clear();
  182. mEditable.clearSpans();
  183. mEditable.insert(0, (CharSequence) state.text);
  184. setSelection(state.selectionStart, state.selectionEnd);
  185. if (state.composingRegionStart != state.composingRegionEnd) {
  186. setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
  187. }
  188. restartInput();
  189. }
  190. /**
  191. * Get the current listener for state changes.
  192. *
  193. * @return The current Listener
  194. */
  195. public final Listener getListener() {
  196. return listener;
  197. }
  198. /**
  199. * Set a listener for state changes.
  200. *
  201. * @param listener
  202. * @return This InputConnection, for setter chaining.
  203. */
  204. public final InputConnection setListener(Listener listener) {
  205. this.listener = listener;
  206. return this;
  207. }
  208. // From View.OnKeyListener
  209. @Override
  210. public boolean onKey(View view, int i, KeyEvent keyEvent) {
  211. log("onKey: " + keyEvent);
  212. if (!getSoftKeyboardActive()) {
  213. return false;
  214. }
  215. // Don't call sendKeyEvent as it might produce an infinite loop.
  216. if (processKeyEvent(keyEvent)) {
  217. // IMM seems to cache the content of Editable, so we update it with restartInput
  218. // Also it caches selection and composing region, so let's notify it about updates.
  219. stateUpdated();
  220. immUpdateSelection();
  221. restartInput();
  222. return true;
  223. }
  224. return false;
  225. }
  226. // From BaseInputConnection
  227. @Override
  228. public Editable getEditable() {
  229. log("getEditable");
  230. return mEditable;
  231. }
  232. // From BaseInputConnection
  233. @Override
  234. public boolean setSelection(int start, int end) {
  235. log("setSelection: " + start + ":" + end);
  236. return super.setSelection(start, end);
  237. }
  238. // From BaseInputConnection
  239. @Override
  240. public boolean setComposingText(CharSequence text, int newCursorPosition) {
  241. log(
  242. "setComposingText='" + text + "' newCursorPosition=" + newCursorPosition);
  243. if (text == null) {
  244. return false;
  245. }
  246. return super.setComposingText(text, newCursorPosition);
  247. }
  248. @Override
  249. public boolean setComposingRegion(int start, int end) {
  250. log("setComposingRegion: " + start + ":" + end);
  251. return super.setComposingRegion(start, end);
  252. }
  253. // From BaseInputConnection
  254. @Override
  255. public boolean finishComposingText() {
  256. log("finishComposingText");
  257. return super.finishComposingText();
  258. }
  259. @Override
  260. public boolean endBatchEdit() {
  261. log("endBatchEdit");
  262. stateUpdated();
  263. return super.endBatchEdit();
  264. }
  265. @Override
  266. public boolean commitCompletion(CompletionInfo text) {
  267. log("commitCompletion");
  268. return super.commitCompletion(text);
  269. }
  270. @Override
  271. public boolean commitCorrection(CorrectionInfo text) {
  272. log("commitCompletion");
  273. return super.commitCorrection(text);
  274. }
  275. // From BaseInputConnection
  276. @Override
  277. public boolean commitText(CharSequence text, int newCursorPosition) {
  278. log(
  279. (new StringBuilder())
  280. .append("commitText: ")
  281. .append(text)
  282. .append(", new pos = ")
  283. .append(newCursorPosition)
  284. .toString());
  285. return super.commitText(text, newCursorPosition);
  286. }
  287. // From BaseInputConnection
  288. @Override
  289. public boolean deleteSurroundingText(int beforeLength, int afterLength) {
  290. log("deleteSurroundingText: " + beforeLength + ":" + afterLength);
  291. return super.deleteSurroundingText(beforeLength, afterLength);
  292. }
  293. // From BaseInputConnection
  294. @Override
  295. public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
  296. log("deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength);
  297. return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
  298. }
  299. // From BaseInputConnection
  300. @Override
  301. public boolean sendKeyEvent(KeyEvent event) {
  302. log("sendKeyEvent: " + event);
  303. return super.sendKeyEvent(event);
  304. }
  305. // From BaseInputConnection
  306. @Override
  307. public CharSequence getSelectedText(int flags) {
  308. CharSequence result = super.getSelectedText(flags);
  309. if (result == null) {
  310. result = "";
  311. }
  312. log("getSelectedText: " + flags + ", result: " + result);
  313. return result;
  314. }
  315. // From BaseInputConnection
  316. @Override
  317. public CharSequence getTextAfterCursor(int length, int flags) {
  318. log("getTextAfterCursor: " + length + ":" + flags);
  319. if (length < 0) {
  320. Log.i("darkfi", "getTextAfterCursor: returning null to due to an invalid length=" + length);
  321. return null;
  322. }
  323. return super.getTextAfterCursor(length, flags);
  324. }
  325. // From BaseInputConnection
  326. @Override
  327. public CharSequence getTextBeforeCursor(int length, int flags) {
  328. log("getTextBeforeCursor: " + length + ", flags=" + flags);
  329. if (length < 0) {
  330. Log.i("darkfi", "getTextBeforeCursor: returning null to due to an invalid length=" + length);
  331. return null;
  332. }
  333. return super.getTextBeforeCursor(length, flags);
  334. }
  335. // From BaseInputConnection
  336. @Override
  337. public boolean requestCursorUpdates(int cursorUpdateMode) {
  338. log("Request cursor updates: " + cursorUpdateMode);
  339. return super.requestCursorUpdates(cursorUpdateMode);
  340. }
  341. // From BaseInputConnection
  342. @Override
  343. public void closeConnection() {
  344. log("closeConnection");
  345. super.closeConnection();
  346. }
  347. @Override
  348. public boolean setImeConsumesInput(boolean imeConsumesInput) {
  349. log("setImeConsumesInput: " + imeConsumesInput);
  350. return super.setImeConsumesInput(imeConsumesInput);
  351. }
  352. @Override
  353. public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
  354. log("getExtractedText");
  355. return super.getExtractedText(request, flags);
  356. }
  357. @Override
  358. public boolean performPrivateCommand(String action, Bundle data) {
  359. log("performPrivateCommand");
  360. return super.performPrivateCommand(action, data);
  361. }
  362. private void immUpdateSelection() {
  363. Pair selection = this.getSelection();
  364. Pair cr = this.getComposingRegion();
  365. log(
  366. "immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + ","
  367. + cr.second);
  368. settings.mEditorInfo.initialSelStart = selection.first;
  369. settings.mEditorInfo.initialSelEnd = selection.second;
  370. imm.updateSelection(targetView, selection.first, selection.second, cr.first, cr.second);
  371. }
  372. private Pair getSelection() {
  373. return new Pair(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
  374. }
  375. private Pair getComposingRegion() {
  376. return new Pair(getComposingSpanStart(mEditable), getComposingSpanEnd(mEditable));
  377. }
  378. private boolean processKeyEvent(KeyEvent event) {
  379. if (event == null) {
  380. return false;
  381. }
  382. int keyCode = event.getKeyCode();
  383. log(
  384. "processKeyEvent(key=" + keyCode + ") text=" + this.mEditable.toString());
  385. // Filter out Enter keys if multi-line mode is disabled.
  386. if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0
  387. && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
  388. && event.hasNoModifiers()) {
  389. sendEditorAction(settings.mEditorInfo.actionId);
  390. return true;
  391. }
  392. if (event.getAction() != KeyEvent.ACTION_DOWN) {
  393. return false;
  394. }
  395. // If no selection is set, move the selection to the end.
  396. // This is the case when first typing on keys when the selection is not set.
  397. // Note that for InputType.TYPE_CLASS_TEXT, this is not be needed because the
  398. // selection is set in setComposingText.
  399. Pair selection = this.getSelection();
  400. if (selection.first == -1) {
  401. selection.first = this.mEditable.length();
  402. selection.second = this.mEditable.length();
  403. }
  404. if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
  405. if (selection.first == selection.second) {
  406. int newIndex = findIndexBackward(mEditable, selection.first, 1);
  407. setSelection(newIndex, newIndex);
  408. } else {
  409. setSelection(selection.first, selection.first);
  410. }
  411. return true;
  412. }
  413. if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
  414. if (selection.first == selection.second) {
  415. int newIndex = findIndexForward(mEditable, selection.second, 1);
  416. setSelection(newIndex, newIndex);
  417. } else {
  418. setSelection(selection.second, selection.second);
  419. }
  420. return true;
  421. }
  422. if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
  423. setSelection(0, 0);
  424. return true;
  425. }
  426. if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
  427. setSelection(this.mEditable.length(), this.mEditable.length());
  428. return true;
  429. }
  430. if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
  431. if (selection.first != selection.second) {
  432. this.mEditable.delete(selection.first, selection.second);
  433. return true;
  434. }
  435. if (keyCode == KeyEvent.KEYCODE_DEL) {
  436. if (selection.first > 0) {
  437. finishComposingText();
  438. deleteSurroundingTextInCodePoints(1, 0);
  439. return true;
  440. }
  441. }
  442. if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
  443. if (selection.first < this.mEditable.length()) {
  444. finishComposingText();
  445. deleteSurroundingTextInCodePoints(0, 1);
  446. return true;
  447. }
  448. }
  449. return false;
  450. }
  451. if (event.getUnicodeChar() == 0) {
  452. return false;
  453. }
  454. if (selection.first != selection.second) {
  455. log("processKeyEvent: deleting selection");
  456. this.mEditable.delete(selection.first, selection.second);
  457. }
  458. String charsToInsert = Character.toString((char) event.getUnicodeChar());
  459. this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
  460. int length = this.mEditable.length();
  461. // Same logic as in setComposingText(): we must update composing region,
  462. // so make sure it points to a valid range.
  463. Pair composingRegion = this.getComposingRegion();
  464. if (composingRegion.first == -1) {
  465. composingRegion = this.getSelection();
  466. if (composingRegion.first == -1) {
  467. composingRegion = new Pair(0, 0);
  468. }
  469. }
  470. composingRegion.second = composingRegion.first + length;
  471. this.setComposingRegion(composingRegion.first, composingRegion.second);
  472. int new_cursor = selection.first + charsToInsert.length();
  473. setSelection(new_cursor, new_cursor);
  474. log("processKeyEvent: exit, text=" + this.mEditable.toString());
  475. return true;
  476. }
  477. private final void stateUpdated() {
  478. Pair selection = this.getSelection();
  479. Pair cr = this.getComposingRegion();
  480. State state = new State(
  481. this.mEditable.toString(), selection.first, selection.second, cr.first, cr.second);
  482. settings.mEditorInfo.initialSelStart = selection.first;
  483. settings.mEditorInfo.initialSelEnd = selection.second;
  484. // Keep a reference to the listener to avoid a race condition when setting the listener.
  485. Listener listener = this.listener;
  486. // We always propagate state change events because unfortunately keyboard visibility functions
  487. // are unreliable, and text editor logic should not depend on them.
  488. if (listener != null) {
  489. listener.stateChanged(state, /*dismissed=*/false);
  490. }
  491. }
  492. /**
  493. * Get the current IME insets.
  494. *
  495. * @return The current IME insets
  496. */
  497. public Insets getImeInsets() {
  498. if (this.targetView == null) {
  499. return Insets.NONE;
  500. }
  501. WindowInsets insets = this.targetView.getRootWindowInsets();
  502. if (insets == null) {
  503. return Insets.NONE;
  504. }
  505. return insets.getInsets(WindowInsets.Type.ime());
  506. }
  507. /**
  508. * Returns true if software keyboard is visible, false otherwise.
  509. *
  510. * @return whether software IME is visible or not.
  511. */
  512. public boolean isSoftwareKeyboardVisible() {
  513. if (this.targetView == null) {
  514. return false;
  515. }
  516. WindowInsets insets = this.targetView.getRootWindowInsets();
  517. if (insets == null) {
  518. return false;
  519. }
  520. return insets.isVisible(WindowInsets.Type.ime());
  521. }
  522. /**
  523. * This is an event handler from InputConnection interface.
  524. * It's called when action button is triggered (typically this means Enter was pressed).
  525. *
  526. * @param action Action code, either one from EditorInfo.imeOptions or a custom one.
  527. * @return Returns true on success, false if the input connection is no longer valid.
  528. */
  529. @Override
  530. public boolean performEditorAction(int action) {
  531. log("performEditorAction, action=" + action);
  532. if (action == IME_ACTION_UNSPECIFIED) {
  533. // Super emulates Enter key press/release
  534. return super.performEditorAction(action);
  535. }
  536. return sendEditorAction(action);
  537. }
  538. /**
  539. * Delivers editor action to listener
  540. *
  541. * @param action Action code, either one from EditorInfo.imeOptions or a custom one.
  542. * @return Returns true on success, false if the input connection is no longer valid.
  543. */
  544. private boolean sendEditorAction(int action) {
  545. Listener listener = this.listener;
  546. if (listener != null) {
  547. listener.onEditorAction(action);
  548. return true;
  549. }
  550. return false;
  551. }
  552. private static int INVALID_INDEX = -1;
  553. // Implementation copy from BaseInputConnection
  554. private static int findIndexBackward(
  555. final CharSequence cs, final int from, final int numCodePoints) {
  556. int currentIndex = from;
  557. boolean waitingHighSurrogate = false;
  558. final int N = cs.length();
  559. if (currentIndex < 0 || N < currentIndex) {
  560. return INVALID_INDEX; // The starting point is out of range.
  561. }
  562. if (numCodePoints < 0) {
  563. return INVALID_INDEX; // Basically this should not happen.
  564. }
  565. int remainingCodePoints = numCodePoints;
  566. while (true) {
  567. if (remainingCodePoints == 0) {
  568. return currentIndex; // Reached to the requested length in code points.
  569. }
  570. --currentIndex;
  571. if (currentIndex < 0) {
  572. if (waitingHighSurrogate) {
  573. return INVALID_INDEX; // An invalid surrogate pair is found.
  574. }
  575. return 0; // Reached to the beginning of the text w/o any invalid surrogate pair.
  576. }
  577. final char c = cs.charAt(currentIndex);
  578. if (waitingHighSurrogate) {
  579. if (!java.lang.Character.isHighSurrogate(c)) {
  580. return INVALID_INDEX; // An invalid surrogate pair is found.
  581. }
  582. waitingHighSurrogate = false;
  583. --remainingCodePoints;
  584. continue;
  585. }
  586. if (!java.lang.Character.isSurrogate(c)) {
  587. --remainingCodePoints;
  588. continue;
  589. }
  590. if (java.lang.Character.isHighSurrogate(c)) {
  591. return INVALID_INDEX; // A invalid surrogate pair is found.
  592. }
  593. waitingHighSurrogate = true;
  594. }
  595. }
  596. // Implementation copy from BaseInputConnection
  597. private static int findIndexForward(
  598. final CharSequence cs, final int from, final int numCodePoints) {
  599. int currentIndex = from;
  600. boolean waitingLowSurrogate = false;
  601. final int N = cs.length();
  602. if (currentIndex < 0 || N < currentIndex) {
  603. return INVALID_INDEX; // The starting point is out of range.
  604. }
  605. if (numCodePoints < 0) {
  606. return INVALID_INDEX; // Basically this should not happen.
  607. }
  608. int remainingCodePoints = numCodePoints;
  609. while (true) {
  610. if (remainingCodePoints == 0) {
  611. return currentIndex; // Reached to the requested length in code points.
  612. }
  613. if (currentIndex >= N) {
  614. if (waitingLowSurrogate) {
  615. return INVALID_INDEX; // An invalid surrogate pair is found.
  616. }
  617. return N; // Reached to the end of the text w/o any invalid surrogate pair.
  618. }
  619. final char c = cs.charAt(currentIndex);
  620. if (waitingLowSurrogate) {
  621. if (!java.lang.Character.isLowSurrogate(c)) {
  622. return INVALID_INDEX; // An invalid surrogate pair is found.
  623. }
  624. --remainingCodePoints;
  625. waitingLowSurrogate = false;
  626. ++currentIndex;
  627. continue;
  628. }
  629. if (!java.lang.Character.isSurrogate(c)) {
  630. --remainingCodePoints;
  631. ++currentIndex;
  632. continue;
  633. }
  634. if (java.lang.Character.isLowSurrogate(c)) {
  635. return INVALID_INDEX; // A invalid surrogate pair is found.
  636. }
  637. waitingLowSurrogate = true;
  638. ++currentIndex;
  639. }
  640. }
  641. }