CustomInputConnection.java 18 KB

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