CustomInputConnection.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. package autosuggest;
  19. import android.content.Context;
  20. import android.os.Bundle;
  21. import android.os.Handler;
  22. import android.text.Editable;
  23. import android.text.Selection;
  24. import android.text.SpannableStringBuilder;
  25. import android.util.Log;
  26. import android.view.KeyEvent;
  27. import android.view.inputmethod.BaseInputConnection;
  28. import android.view.inputmethod.InputConnection;
  29. import android.view.inputmethod.InputContentInfo;
  30. import android.view.inputmethod.EditorInfo;
  31. import android.view.View;
  32. import android.view.inputmethod.CompletionInfo;
  33. import android.view.inputmethod.CorrectionInfo;
  34. import android.view.inputmethod.ExtractedText;
  35. import android.view.inputmethod.ExtractedTextRequest;
  36. import android.view.inputmethod.SurroundingText;
  37. import android.view.inputmethod.InputMethodManager;
  38. //import android.view.inputmethod.TextSnapshot;
  39. //import android.view.inputmethod.TextAttribute;
  40. // This InputConnection is created by ContentView.onCreateInputConnection.
  41. // It then adapts android's IME to chrome's RenderWidgetHostView using the
  42. // native ImeAdapterAndroid via the outer class ImeAdapter.
  43. public class CustomInputConnection extends BaseInputConnection {
  44. private static final boolean DEBUG = true;
  45. private int id = -1;
  46. private View mInternalView;
  47. //private ImeAdapter mImeAdapter;
  48. private Editable mEditable;
  49. private boolean mSingleLine;
  50. private int numBatchEdits;
  51. private boolean shouldUpdateImeSelection;
  52. native static void onCompose(int id, String text, int newCursorPos, boolean isCommit);
  53. native static void onSetComposeRegion(int id, int start, int end);
  54. native static void onFinishCompose(int id);
  55. native static void onDeleteSurroundingText(int id, int left, int right);
  56. //private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
  57. public CustomInputConnection(int id, View view, EditorInfo outAttrs) {
  58. super(view, true);
  59. this.id = id;
  60. log("CustomInputConnection()");
  61. mInternalView = view;
  62. //mImeAdapter = imeAdapter;
  63. //mImeAdapter.setInputConnection(this);
  64. mSingleLine = true;
  65. outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
  66. outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
  67. | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
  68. /*
  69. if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeText) {
  70. */
  71. // Normal text field
  72. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  73. /*
  74. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTextArea ||
  75. imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeContentEditable) {
  76. // TextArea or contenteditable.
  77. outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
  78. | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
  79. | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
  80. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
  81. mSingleLine = false;
  82. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypePassword) {
  83. // Password
  84. outAttrs.inputType = InputType.TYPE_CLASS_TEXT
  85. | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
  86. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  87. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeSearch) {
  88. // Search
  89. outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
  90. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeUrl) {
  91. // Url
  92. // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
  93. // exclude it for now.
  94. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  95. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeEmail) {
  96. // Email
  97. outAttrs.inputType = InputType.TYPE_CLASS_TEXT
  98. | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
  99. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  100. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTel) {
  101. // Telephone
  102. // Number and telephone do not have both a Tab key and an
  103. // action in default OSK, so set the action to NEXT
  104. outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
  105. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
  106. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeNumber) {
  107. // Number
  108. outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
  109. | InputType.TYPE_NUMBER_VARIATION_NORMAL;
  110. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
  111. }
  112. */
  113. }
  114. private void log(String fstr, Object... args) {
  115. if (!DEBUG) return;
  116. String text = "(" + id + "): " + String.format(fstr, args);
  117. Log.d("darkfi", text);
  118. }
  119. /**
  120. * Updates the AdapterInputConnection's internal representation of the text
  121. * being edited and its selection and composition properties. The resulting
  122. * Editable is accessible through the getEditable() method.
  123. * If the text has not changed, this also calls updateSelection on the InputMethodManager.
  124. * @param text The String contents of the field being edited
  125. * @param selectionStart The character offset of the selection start, or the caret
  126. * position if there is no selection
  127. * @param selectionEnd The character offset of the selection end, or the caret
  128. * position if there is no selection
  129. * @param compositionStart The character offset of the composition start, or -1
  130. * if there is no composition
  131. * @param compositionEnd The character offset of the composition end, or -1
  132. * if there is no selection
  133. */
  134. public void setEditableText(String text, int selectionStart, int selectionEnd,
  135. int compositionStart, int compositionEnd) {
  136. log("setEditableText(%s, %d, %d, %d, %d)", text,
  137. selectionStart, selectionEnd,
  138. compositionStart, compositionEnd);
  139. if (mEditable == null) {
  140. log("setEditableText creating new editable");
  141. mEditable = Editable.Factory.getInstance().newEditable("");
  142. }
  143. int prevSelectionStart = Selection.getSelectionStart(mEditable);
  144. int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
  145. int prevEditableLength = mEditable.length();
  146. int prevCompositionStart = getComposingSpanStart(mEditable);
  147. int prevCompositionEnd = getComposingSpanEnd(mEditable);
  148. String prevText = mEditable.toString();
  149. selectionStart = Math.min(selectionStart, text.length());
  150. selectionEnd = Math.min(selectionEnd, text.length());
  151. compositionStart = Math.min(compositionStart, text.length());
  152. compositionEnd = Math.min(compositionEnd, text.length());
  153. boolean textUnchanged = prevText.equals(text);
  154. if (textUnchanged
  155. && prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
  156. && prevCompositionStart == compositionStart
  157. && prevCompositionEnd == compositionEnd) {
  158. // Nothing has changed; don't need to do anything
  159. return;
  160. }
  161. // When a programmatic change has been made to the editable field, both the start
  162. // and end positions for the composition will equal zero. In this case we cancel the
  163. // active composition in the editor as this no longer is relevant.
  164. if (textUnchanged && compositionStart == 0 && compositionEnd == 0) {
  165. cancelComposition();
  166. }
  167. if (!textUnchanged) {
  168. log("replace mEditable with: %s", text);
  169. mEditable.replace(0, mEditable.length(), text);
  170. }
  171. Selection.setSelection(mEditable, selectionStart, selectionEnd);
  172. super.setComposingRegion(compositionStart, compositionEnd);
  173. if (textUnchanged || prevText.equals("")) {
  174. log("setEditableText updating selection");
  175. // updateSelection should be called when a manual selection change occurs.
  176. // Should not be called if text is being entered else issues can occur
  177. // e.g. backspace to undo autocorrection will not work with the default OSK.
  178. getInputMethodManager().updateSelection(mInternalView,
  179. selectionStart, selectionEnd, compositionStart, compositionEnd);
  180. }
  181. }
  182. @Override
  183. public Editable getEditable() {
  184. if (mEditable == null) {
  185. log("getEditable() [create new]");
  186. mEditable = Editable.Factory.getInstance().newEditable("");
  187. Selection.setSelection(mEditable, 0);
  188. }
  189. log("getEditable() -> %s", editableToXml(mEditable));
  190. return mEditable;
  191. }
  192. @Override
  193. public boolean setComposingText(CharSequence text, int newCursorPosition) {
  194. log("setComposingText(%s, %d)", text, newCursorPosition);
  195. super.setComposingText(text, newCursorPosition);
  196. shouldUpdateImeSelection = true;
  197. onCompose(id, text.toString(), newCursorPosition, false);
  198. return true;
  199. }
  200. @Override
  201. public boolean commitText(CharSequence text, int newCursorPosition) {
  202. log("commitText(%s, %d)", text, newCursorPosition);
  203. super.commitText(text, newCursorPosition);
  204. shouldUpdateImeSelection = true;
  205. onCompose(id, text.toString(), newCursorPosition, text.length() > 0);
  206. return true;
  207. }
  208. @Override
  209. public boolean performEditorAction(int actionCode) {
  210. log("performEditorAction(%d)", actionCode);
  211. switch (actionCode) {
  212. case EditorInfo.IME_ACTION_NEXT:
  213. cancelComposition();
  214. // Send TAB key event
  215. long timeStampMs = System.currentTimeMillis();
  216. //mImeAdapter.sendSyntheticKeyEvent(
  217. // sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
  218. return true;
  219. case EditorInfo.IME_ACTION_GO:
  220. case EditorInfo.IME_ACTION_SEARCH:
  221. //mImeAdapter.dismissInput(true);
  222. break;
  223. }
  224. return super.performEditorAction(actionCode);
  225. }
  226. @Override
  227. public boolean performContextMenuAction(int id) {
  228. log("performContextMenuAction(%d)", id);
  229. /*
  230. switch (id) {
  231. case android.R.id.selectAll:
  232. return mImeAdapter.selectAll();
  233. case android.R.id.cut:
  234. return mImeAdapter.cut();
  235. case android.R.id.copy:
  236. return mImeAdapter.copy();
  237. case android.R.id.paste:
  238. return mImeAdapter.paste();
  239. default:
  240. return false;
  241. }
  242. */
  243. return false;
  244. }
  245. @Override
  246. public CharSequence getTextAfterCursor(int length, int flags) {
  247. log("getTextAfterCursor(%d, %d)", length, flags);
  248. return super.getTextAfterCursor(length, flags);
  249. }
  250. @Override
  251. public CharSequence getTextBeforeCursor(int length, int flags) {
  252. log("getTextBeforeCursor(%d, %d)", length, flags);
  253. return super.getTextBeforeCursor(length, flags);
  254. }
  255. @Override
  256. public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) {
  257. log("getSurroundingText(%d, %d, %d)", beforeLength, afterLength, flags);
  258. return super.getSurroundingText(beforeLength, afterLength, flags);
  259. }
  260. @Override
  261. public CharSequence getSelectedText(int flags) {
  262. log("getSelectedText(%d)", flags);
  263. return super.getSelectedText(flags);
  264. }
  265. @Override
  266. public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
  267. log("getExtractedText(...)");
  268. ExtractedText et = new ExtractedText();
  269. if (mEditable == null) {
  270. et.text = "";
  271. } else {
  272. et.text = mEditable.toString();
  273. et.partialEndOffset = mEditable.length();
  274. et.selectionStart = Selection.getSelectionStart(mEditable);
  275. et.selectionEnd = Selection.getSelectionEnd(mEditable);
  276. }
  277. et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
  278. return et;
  279. }
  280. @Override
  281. public boolean deleteSurroundingText(int leftLength, int rightLength) {
  282. log("deleteSurroundingText(%d, %d)", leftLength, rightLength);
  283. if (!super.deleteSurroundingText(leftLength, rightLength)) {
  284. return false;
  285. }
  286. shouldUpdateImeSelection = true;
  287. //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
  288. onDeleteSurroundingText(id, leftLength, rightLength);
  289. return true;
  290. }
  291. @Override
  292. public boolean sendKeyEvent(KeyEvent event) {
  293. int action = event.getAction();
  294. int keycode = event.getKeyCode();
  295. log("sendKeyEvent() [action=%d, keycode=%d]", action, keycode);
  296. //mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
  297. //mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
  298. // If this is a key-up, and backspace/del or if the key has a character representation,
  299. // need to update the underlying Editable (i.e. the local representation of the text
  300. // being edited).
  301. if (event.getAction() == KeyEvent.ACTION_UP) {
  302. if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
  303. super.deleteSurroundingText(1, 0);
  304. } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
  305. super.deleteSurroundingText(0, 1);
  306. } else {
  307. int unicodeChar = event.getUnicodeChar();
  308. if (unicodeChar != 0) {
  309. Editable editable = getEditable();
  310. int selectionStart = Selection.getSelectionStart(editable);
  311. int selectionEnd = Selection.getSelectionEnd(editable);
  312. if (selectionStart > selectionEnd) {
  313. int temp = selectionStart;
  314. selectionStart = selectionEnd;
  315. selectionEnd = temp;
  316. }
  317. editable.replace(selectionStart, selectionEnd,
  318. Character.toString((char)unicodeChar));
  319. }
  320. }
  321. }
  322. shouldUpdateImeSelection = true;
  323. return super.sendKeyEvent(event);
  324. }
  325. @Override
  326. public boolean finishComposingText() {
  327. log("finishComposingText()");
  328. if (mEditable == null
  329. || (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable))) {
  330. return true;
  331. }
  332. super.finishComposingText();
  333. onFinishCompose(id);
  334. return true;
  335. }
  336. @Override
  337. public boolean setSelection(int start, int end) {
  338. log("setSelection(%d, %d)", start, end);
  339. if (start < 0 || end < 0) return true;
  340. super.setSelection(start, end);
  341. shouldUpdateImeSelection = true;
  342. //return mImeAdapter.setEditableSelectionOffsets(start, end);
  343. return true;
  344. }
  345. /**
  346. * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
  347. * is no longer a current composition. Note this differs from finishComposingText, which
  348. * is called by the IME when it wants to end a composition.
  349. */
  350. void cancelComposition() {
  351. log("cancelComposition()");
  352. getInputMethodManager().restartInput(mInternalView);
  353. }
  354. @Override
  355. public boolean setComposingRegion(int start, int end) {
  356. log("setComposingRegion(%d, %d)", start, end);
  357. int a = Math.min(start, end);
  358. int b = Math.max(start, end);
  359. super.setComposingRegion(a, b);
  360. onSetComposeRegion(id, a, b);
  361. return true;
  362. }
  363. boolean isActive() {
  364. return getInputMethodManager().isActive();
  365. }
  366. private InputMethodManager getInputMethodManager() {
  367. InputMethodManager imm = (InputMethodManager)mInternalView.getContext()
  368. .getSystemService(Context.INPUT_METHOD_SERVICE);
  369. if (imm == null) {
  370. Log.e("darkfi", "[IC]: InputMethodManager is NULL!");
  371. }
  372. return imm;
  373. }
  374. private void updateImeSelection() {
  375. log("updateImeSelection()");
  376. if (mEditable == null) {
  377. return;
  378. }
  379. getInputMethodManager().updateSelection(
  380. mInternalView,
  381. Selection.getSelectionStart(mEditable),
  382. Selection.getSelectionEnd(mEditable),
  383. getComposingSpanStart(mEditable),
  384. getComposingSpanEnd(mEditable)
  385. );
  386. log("updateImeSelection() DONE");
  387. }
  388. @Override
  389. public boolean beginBatchEdit() {
  390. log("beginBatchEdit");
  391. ++numBatchEdits;
  392. return false;
  393. }
  394. @Override
  395. public boolean endBatchEdit() {
  396. log("endBatchEdit");
  397. if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
  398. updateImeSelection();
  399. shouldUpdateImeSelection = false;
  400. }
  401. log("endBatchEdit DONE");
  402. return false;
  403. }
  404. private String editableToXml(Editable editable) {
  405. StringBuilder xmlBuilder = new StringBuilder();
  406. int length = editable.length();
  407. Object[] spans = editable.getSpans(0, editable.length(), Object.class);
  408. for (int i = 0; i < length; i++) {
  409. // Find spans starting at this position
  410. for (Object span : spans) {
  411. if (editable.getSpanStart(span) == i) {
  412. xmlBuilder
  413. .append("<")
  414. .append(span.getClass().getSimpleName())
  415. .append(">");
  416. }
  417. }
  418. // Append the character
  419. char c = editable.charAt(i);
  420. xmlBuilder.append(c);
  421. if (Character.isHighSurrogate(c)) {
  422. if (i + 1 < editable.length() && Character.isLowSurrogate(editable.charAt(i + 1))) {
  423. i += 1;
  424. xmlBuilder.append(editable.charAt(i));
  425. }
  426. }
  427. // Find spans ending at this position
  428. for (Object span : spans) {
  429. if (editable.getSpanEnd(span) == i) {
  430. xmlBuilder
  431. .append("</")
  432. .append(span.getClass().getSimpleName())
  433. .append(">");
  434. }
  435. }
  436. }
  437. // Find spans starting at this position
  438. for (Object span : spans) {
  439. if (editable.getSpanStart(span) == length) {
  440. xmlBuilder
  441. .append("<")
  442. .append(span.getClass().getSimpleName())
  443. .append(">");
  444. }
  445. }
  446. // Find spans ending at this position
  447. for (Object span : spans) {
  448. if (editable.getSpanEnd(span) == length) {
  449. xmlBuilder
  450. .append("</")
  451. .append(span.getClass().getSimpleName())
  452. .append(">");
  453. }
  454. }
  455. return xmlBuilder.toString();
  456. }
  457. public String debugEditableStr() {
  458. return editableToXml(mEditable);
  459. }
  460. public String rawText() {
  461. return mEditable.toString();
  462. }
  463. public int getSelectionStart() {
  464. return Selection.getSelectionStart(mEditable);
  465. }
  466. public int getSelectionEnd() {
  467. return Selection.getSelectionEnd(mEditable);
  468. }
  469. public int getComposeStart() {
  470. return getComposingSpanStart(mEditable);
  471. }
  472. public int getComposeEnd() {
  473. return getComposingSpanEnd(mEditable);
  474. }
  475. }