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