CustomInputConnection.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 = false;
  45. private int ID = (int)(Math.random() * (1 << 30));
  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(String text, int newCursorPos, boolean isCommit);
  53. native static void onSetComposeRegion(int start, int end);
  54. //private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
  55. public CustomInputConnection(View view, EditorInfo outAttrs) {
  56. super(view, true);
  57. log("CustomInputConnection()");
  58. mInternalView = view;
  59. //mImeAdapter = imeAdapter;
  60. //mImeAdapter.setInputConnection(this);
  61. mSingleLine = true;
  62. outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
  63. outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
  64. | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
  65. /*
  66. if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeText) {
  67. */
  68. // Normal text field
  69. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  70. /*
  71. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTextArea ||
  72. imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeContentEditable) {
  73. // TextArea or contenteditable.
  74. outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
  75. | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
  76. | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
  77. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
  78. mSingleLine = false;
  79. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypePassword) {
  80. // Password
  81. outAttrs.inputType = InputType.TYPE_CLASS_TEXT
  82. | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
  83. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  84. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeSearch) {
  85. // Search
  86. outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
  87. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeUrl) {
  88. // Url
  89. // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
  90. // exclude it for now.
  91. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  92. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeEmail) {
  93. // Email
  94. outAttrs.inputType = InputType.TYPE_CLASS_TEXT
  95. | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
  96. outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
  97. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTel) {
  98. // Telephone
  99. // Number and telephone do not have both a Tab key and an
  100. // action in default OSK, so set the action to NEXT
  101. outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
  102. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
  103. } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeNumber) {
  104. // Number
  105. outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
  106. | InputType.TYPE_NUMBER_VARIATION_NORMAL;
  107. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
  108. }
  109. */
  110. }
  111. private void log(String fstr, Object... args) {
  112. if (!DEBUG) return;
  113. String text = "(" + ID + "): " + String.format(fstr, args);
  114. Log.d("darkfi", text);
  115. }
  116. /**
  117. * Updates the AdapterInputConnection's internal representation of the text
  118. * being edited and its selection and composition properties. The resulting
  119. * Editable is accessible through the getEditable() method.
  120. * If the text has not changed, this also calls updateSelection on the InputMethodManager.
  121. * @param text The String contents of the field being edited
  122. * @param selectionStart The character offset of the selection start, or the caret
  123. * position if there is no selection
  124. * @param selectionEnd The character offset of the selection end, or the caret
  125. * position if there is no selection
  126. * @param compositionStart The character offset of the composition start, or -1
  127. * if there is no composition
  128. * @param compositionEnd The character offset of the composition end, or -1
  129. * if there is no selection
  130. */
  131. public void setEditableText(String text, int selectionStart, int selectionEnd,
  132. int compositionStart, int compositionEnd) {
  133. log("darkfi", "setEditableText(%s, %d, %d, %d, %d)",
  134. text, selectionStart, selectionEnd,
  135. compositionStart, compositionEnd);
  136. if (mEditable == null) {
  137. mEditable = Editable.Factory.getInstance().newEditable("");
  138. }
  139. int prevSelectionStart = Selection.getSelectionStart(mEditable);
  140. int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
  141. int prevEditableLength = mEditable.length();
  142. int prevCompositionStart = getComposingSpanStart(mEditable);
  143. int prevCompositionEnd = getComposingSpanEnd(mEditable);
  144. String prevText = mEditable.toString();
  145. selectionStart = Math.min(selectionStart, text.length());
  146. selectionEnd = Math.min(selectionEnd, text.length());
  147. compositionStart = Math.min(compositionStart, text.length());
  148. compositionEnd = Math.min(compositionEnd, text.length());
  149. boolean textUnchanged = prevText.equals(text);
  150. if (textUnchanged
  151. && prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
  152. && prevCompositionStart == compositionStart
  153. && prevCompositionEnd == compositionEnd) {
  154. // Nothing has changed; don't need to do anything
  155. return;
  156. }
  157. // When a programmatic change has been made to the editable field, both the start
  158. // and end positions for the composition will equal zero. In this case we cancel the
  159. // active composition in the editor as this no longer is relevant.
  160. if (textUnchanged && compositionStart == 0 && compositionEnd == 0) {
  161. cancelComposition();
  162. }
  163. if (!textUnchanged) {
  164. mEditable.replace(0, mEditable.length(), text);
  165. }
  166. Selection.setSelection(mEditable, selectionStart, selectionEnd);
  167. super.setComposingRegion(compositionStart, compositionEnd);
  168. if (textUnchanged || prevText.equals("")) {
  169. // updateSelection should be called when a manual selection change occurs.
  170. // Should not be called if text is being entered else issues can occur
  171. // e.g. backspace to undo autocorrection will not work with the default OSK.
  172. getInputMethodManager().updateSelection(mInternalView,
  173. selectionStart, selectionEnd, compositionStart, compositionEnd);
  174. }
  175. }
  176. @Override
  177. public Editable getEditable() {
  178. if (mEditable == null) {
  179. mEditable = Editable.Factory.getInstance().newEditable("");
  180. Selection.setSelection(mEditable, 0);
  181. }
  182. log("getEditable() -> %s", editableToXml(mEditable));
  183. return mEditable;
  184. }
  185. @Override
  186. public boolean setComposingText(CharSequence text, int newCursorPosition) {
  187. log("setComposingText(%s, %d)", text, newCursorPosition);
  188. super.setComposingText(text, newCursorPosition);
  189. shouldUpdateImeSelection = true;
  190. onCompose(text.toString(), newCursorPosition, false);
  191. return true;
  192. }
  193. @Override
  194. public boolean commitText(CharSequence text, int newCursorPosition) {
  195. log("commitText(%s, %d)", text, newCursorPosition);
  196. super.commitText(text, newCursorPosition);
  197. shouldUpdateImeSelection = true;
  198. onCompose(text.toString(), newCursorPosition, text.length() > 0);
  199. return true;
  200. }
  201. @Override
  202. public boolean performEditorAction(int actionCode) {
  203. log("performEditorAction(%d)", actionCode);
  204. switch (actionCode) {
  205. case EditorInfo.IME_ACTION_NEXT:
  206. cancelComposition();
  207. // Send TAB key event
  208. long timeStampMs = System.currentTimeMillis();
  209. //mImeAdapter.sendSyntheticKeyEvent(
  210. // sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
  211. return true;
  212. case EditorInfo.IME_ACTION_GO:
  213. case EditorInfo.IME_ACTION_SEARCH:
  214. //mImeAdapter.dismissInput(true);
  215. break;
  216. }
  217. return super.performEditorAction(actionCode);
  218. }
  219. @Override
  220. public boolean performContextMenuAction(int id) {
  221. log("performContextMenuAction(%d)", id);
  222. /*
  223. switch (id) {
  224. case android.R.id.selectAll:
  225. return mImeAdapter.selectAll();
  226. case android.R.id.cut:
  227. return mImeAdapter.cut();
  228. case android.R.id.copy:
  229. return mImeAdapter.copy();
  230. case android.R.id.paste:
  231. return mImeAdapter.paste();
  232. default:
  233. return false;
  234. }
  235. */
  236. return false;
  237. }
  238. @Override
  239. public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
  240. log("getExtractedText(...)");
  241. ExtractedText et = new ExtractedText();
  242. if (mEditable == null) {
  243. et.text = "";
  244. } else {
  245. et.text = mEditable.toString();
  246. et.partialEndOffset = mEditable.length();
  247. et.selectionStart = Selection.getSelectionStart(mEditable);
  248. et.selectionEnd = Selection.getSelectionEnd(mEditable);
  249. }
  250. et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
  251. return et;
  252. }
  253. @Override
  254. public boolean deleteSurroundingText(int leftLength, int rightLength) {
  255. log("deleteSurroundingText(%d, %d)", leftLength, rightLength);
  256. if (!super.deleteSurroundingText(leftLength, rightLength)) {
  257. return false;
  258. }
  259. shouldUpdateImeSelection = true;
  260. //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
  261. return true;
  262. }
  263. @Override
  264. public boolean sendKeyEvent(KeyEvent event) {
  265. int action = event.getAction();
  266. int keycode = event.getKeyCode();
  267. log("sendKeyEvent() [action=%d, keycode=%d]", action, keycode);
  268. //mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
  269. //mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
  270. // If this is a key-up, and backspace/del or if the key has a character representation,
  271. // need to update the underlying Editable (i.e. the local representation of the text
  272. // being edited).
  273. if (event.getAction() == KeyEvent.ACTION_UP) {
  274. if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
  275. super.deleteSurroundingText(1, 0);
  276. } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
  277. super.deleteSurroundingText(0, 1);
  278. } else {
  279. int unicodeChar = event.getUnicodeChar();
  280. if (unicodeChar != 0) {
  281. Editable editable = getEditable();
  282. int selectionStart = Selection.getSelectionStart(editable);
  283. int selectionEnd = Selection.getSelectionEnd(editable);
  284. if (selectionStart > selectionEnd) {
  285. int temp = selectionStart;
  286. selectionStart = selectionEnd;
  287. selectionEnd = temp;
  288. }
  289. editable.replace(selectionStart, selectionEnd,
  290. Character.toString((char)unicodeChar));
  291. }
  292. }
  293. }
  294. shouldUpdateImeSelection = true;
  295. return super.sendKeyEvent(event);
  296. }
  297. @Override
  298. public boolean finishComposingText() {
  299. log("finishComposingText()");
  300. if (mEditable == null
  301. || (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable))) {
  302. return true;
  303. }
  304. super.finishComposingText();
  305. onCompose("", 0, true);
  306. return true;
  307. }
  308. @Override
  309. public boolean setSelection(int start, int end) {
  310. log("setSelection(%d, %d)", start, end);
  311. if (start < 0 || end < 0) return true;
  312. super.setSelection(start, end);
  313. shouldUpdateImeSelection = true;
  314. //return mImeAdapter.setEditableSelectionOffsets(start, end);
  315. return true;
  316. }
  317. /**
  318. * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
  319. * is no longer a current composition. Note this differs from finishComposingText, which
  320. * is called by the IME when it wants to end a composition.
  321. */
  322. void cancelComposition() {
  323. log("cancelComposition()");
  324. getInputMethodManager().restartInput(mInternalView);
  325. }
  326. @Override
  327. public boolean setComposingRegion(int start, int end) {
  328. log("setComposingRegion(%d, %d)", start, end);
  329. int a = Math.min(start, end);
  330. int b = Math.max(start, end);
  331. super.setComposingRegion(a, b);
  332. onSetComposeRegion(a, b);
  333. return true;
  334. }
  335. boolean isActive() {
  336. return getInputMethodManager().isActive();
  337. }
  338. private InputMethodManager getInputMethodManager() {
  339. return (InputMethodManager) mInternalView.getContext()
  340. .getSystemService(Context.INPUT_METHOD_SERVICE);
  341. }
  342. private void updateImeSelection() {
  343. log("updateImeSelection()");
  344. if (mEditable != null) {
  345. getInputMethodManager().updateSelection(mInternalView,
  346. Selection.getSelectionStart(mEditable),
  347. Selection.getSelectionEnd(mEditable),
  348. getComposingSpanStart(mEditable),
  349. getComposingSpanEnd(mEditable));
  350. }
  351. }
  352. @Override
  353. public boolean beginBatchEdit() {
  354. log("beginBatchEdit");
  355. ++numBatchEdits;
  356. return false;
  357. }
  358. @Override
  359. public boolean endBatchEdit() {
  360. log("endBatchEdit");
  361. if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
  362. updateImeSelection();
  363. shouldUpdateImeSelection = false;
  364. }
  365. return false;
  366. }
  367. private String editableToXml(Editable editable) {
  368. StringBuilder xmlBuilder = new StringBuilder();
  369. int length = editable.length();
  370. Object[] spans = editable.getSpans(0, editable.length(), Object.class);
  371. for (int i = 0; i < length; i++) {
  372. // Find spans starting at this position
  373. for (Object span : spans) {
  374. if (editable.getSpanStart(span) == i) {
  375. xmlBuilder
  376. .append("<")
  377. .append(span.getClass().getSimpleName())
  378. .append(">");
  379. }
  380. }
  381. // Append the character
  382. xmlBuilder.append(editable.charAt(i));
  383. // Find spans ending at this position
  384. for (Object span : spans) {
  385. if (editable.getSpanEnd(span) == i) {
  386. xmlBuilder
  387. .append("</")
  388. .append(span.getClass().getSimpleName())
  389. .append(">");
  390. }
  391. }
  392. }
  393. // Find spans starting at this position
  394. for (Object span : spans) {
  395. if (editable.getSpanStart(span) == length) {
  396. xmlBuilder
  397. .append("<")
  398. .append(span.getClass().getSimpleName())
  399. .append(">");
  400. }
  401. }
  402. // Find spans ending at this position
  403. for (Object span : spans) {
  404. if (editable.getSpanEnd(span) == length) {
  405. xmlBuilder
  406. .append("</")
  407. .append(span.getClass().getSimpleName())
  408. .append(">");
  409. }
  410. }
  411. return xmlBuilder.toString();
  412. }
  413. }