CustomInputConnection.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 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.text.Editable;
  21. import android.text.Selection;
  22. import android.util.Log;
  23. import android.view.KeyEvent;
  24. import android.view.inputmethod.BaseInputConnection;
  25. import android.view.inputmethod.EditorInfo;
  26. import android.view.View;
  27. import android.view.inputmethod.ExtractedText;
  28. import android.view.inputmethod.ExtractedTextRequest;
  29. import android.view.inputmethod.SurroundingText;
  30. import android.view.inputmethod.InputMethodManager;
  31. public class CustomInputConnection extends BaseInputConnection {
  32. private static final boolean DEBUG = false;
  33. private int id = -1;
  34. private View mInternalView;
  35. private Editable mEditable;
  36. private boolean mSingleLine;
  37. private int numBatchEdits;
  38. private boolean shouldUpdateImeSelection;
  39. native static void onCompose(int id, String text, int newCursorPos, boolean isCommit);
  40. native static void onSetComposeRegion(int id, int start, int end);
  41. native static void onFinishCompose(int id);
  42. native static void onDeleteSurroundingText(int id, int left, int right);
  43. public CustomInputConnection(int id, Editable editable, View view) {
  44. super(view, true);
  45. log("CustomInputConnection()");
  46. this.id = id;
  47. mEditable = editable;
  48. mInternalView = view;
  49. mSingleLine = false;
  50. }
  51. private void log(String fstr, Object... args) {
  52. if (!DEBUG) return;
  53. String text = "(" + id + "): " + String.format(fstr, args);
  54. Log.d("darkfi", text);
  55. }
  56. /**
  57. * Updates the AdapterInputConnection's internal representation of the text
  58. * being edited and its selection and composition properties. The resulting
  59. * Editable is accessible through the getEditable() method.
  60. * If the text has not changed, this also calls updateSelection on the InputMethodManager.
  61. * @param text The String contents of the field being edited
  62. * @param selectionStart The character offset of the selection start, or the caret
  63. * position if there is no selection
  64. * @param selectionEnd The character offset of the selection end, or the caret
  65. * position if there is no selection
  66. * @param compositionStart The character offset of the composition start, or -1
  67. * if there is no composition
  68. * @param compositionEnd The character offset of the composition end, or -1
  69. * if there is no selection
  70. */
  71. public void setEditableText(String text, int selectionStart, int selectionEnd,
  72. int compositionStart, int compositionEnd) {
  73. log("setEditableText(%s, %d, %d, %d, %d)", text,
  74. selectionStart, selectionEnd,
  75. compositionStart, compositionEnd);
  76. int prevSelectionStart = Selection.getSelectionStart(mEditable);
  77. int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
  78. int prevEditableLength = mEditable.length();
  79. int prevCompositionStart = getComposingSpanStart(mEditable);
  80. int prevCompositionEnd = getComposingSpanEnd(mEditable);
  81. String prevText = mEditable.toString();
  82. selectionStart = Math.min(selectionStart, text.length());
  83. selectionEnd = Math.min(selectionEnd, text.length());
  84. compositionStart = Math.min(compositionStart, text.length());
  85. compositionEnd = Math.min(compositionEnd, text.length());
  86. boolean textUnchanged = prevText.equals(text);
  87. if (textUnchanged
  88. && prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
  89. && prevCompositionStart == compositionStart
  90. && prevCompositionEnd == compositionEnd) {
  91. // Nothing has changed; don't need to do anything
  92. return;
  93. }
  94. // When a programmatic change has been made to the editable field, both the start
  95. // and end positions for the composition will equal zero. In this case we cancel the
  96. // active composition in the editor as this no longer is relevant.
  97. if (textUnchanged && compositionStart == 0 && compositionEnd == 0) {
  98. cancelComposition();
  99. }
  100. if (!textUnchanged) {
  101. log("replace mEditable with: %s", text);
  102. mEditable.replace(0, mEditable.length(), text);
  103. }
  104. Selection.setSelection(mEditable, selectionStart, selectionEnd);
  105. super.setComposingRegion(compositionStart, compositionEnd);
  106. //log("textUnchanged=%s prevText=%s", textUnchanged, prevText);
  107. //if (textUnchanged || prevText.equals("")) {
  108. // log("setEditableText updating selection");
  109. // updateSelection should be called when a manual selection change occurs.
  110. // Should not be called if text is being entered else issues can occur
  111. // e.g. backspace to undo autocorrection will not work with the default OSK.
  112. getInputMethodManager().updateSelection(mInternalView,
  113. selectionStart, selectionEnd, compositionStart, compositionEnd);
  114. //}
  115. }
  116. @Override
  117. public Editable getEditable() {
  118. log("getEditable() -> %s", editableToXml(mEditable));
  119. return mEditable;
  120. }
  121. @Override
  122. public boolean setComposingText(CharSequence text, int newCursorPosition) {
  123. log("setComposingText(%s, %d)", text, newCursorPosition);
  124. super.setComposingText(text, newCursorPosition);
  125. shouldUpdateImeSelection = true;
  126. onCompose(id, text.toString(), newCursorPosition, false);
  127. return true;
  128. }
  129. @Override
  130. public boolean commitText(CharSequence text, int newCursorPosition) {
  131. log("commitText(%s, %d)", text, newCursorPosition);
  132. super.commitText(text, newCursorPosition);
  133. shouldUpdateImeSelection = true;
  134. onCompose(id, text.toString(), newCursorPosition, text.length() > 0);
  135. return true;
  136. }
  137. @Override
  138. public boolean performEditorAction(int actionCode) {
  139. log("performEditorAction(%d)", actionCode);
  140. switch (actionCode) {
  141. case EditorInfo.IME_ACTION_NEXT:
  142. cancelComposition();
  143. // Send TAB key event
  144. long timeStampMs = System.currentTimeMillis();
  145. //mImeAdapter.sendSyntheticKeyEvent(
  146. // sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
  147. return true;
  148. case EditorInfo.IME_ACTION_GO:
  149. case EditorInfo.IME_ACTION_SEARCH:
  150. //mImeAdapter.dismissInput(true);
  151. break;
  152. }
  153. return super.performEditorAction(actionCode);
  154. }
  155. @Override
  156. public boolean performContextMenuAction(int id) {
  157. log("performContextMenuAction(%d)", id);
  158. /*
  159. switch (id) {
  160. case android.R.id.selectAll:
  161. return mImeAdapter.selectAll();
  162. case android.R.id.cut:
  163. return mImeAdapter.cut();
  164. case android.R.id.copy:
  165. return mImeAdapter.copy();
  166. case android.R.id.paste:
  167. return mImeAdapter.paste();
  168. default:
  169. return false;
  170. }
  171. */
  172. return false;
  173. }
  174. @Override
  175. public CharSequence getTextAfterCursor(int length, int flags) {
  176. log("getTextAfterCursor(%d, %d)", length, flags);
  177. return super.getTextAfterCursor(length, flags);
  178. }
  179. @Override
  180. public CharSequence getTextBeforeCursor(int length, int flags) {
  181. log("getTextBeforeCursor(%d, %d)", length, flags);
  182. return super.getTextBeforeCursor(length, flags);
  183. }
  184. @Override
  185. public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) {
  186. log("getSurroundingText(%d, %d, %d)", beforeLength, afterLength, flags);
  187. return super.getSurroundingText(beforeLength, afterLength, flags);
  188. }
  189. @Override
  190. public CharSequence getSelectedText(int flags) {
  191. log("getSelectedText(%d)", flags);
  192. return super.getSelectedText(flags);
  193. }
  194. @Override
  195. public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
  196. log("getExtractedText(...)");
  197. ExtractedText et = new ExtractedText();
  198. et.text = mEditable.toString();
  199. et.partialEndOffset = mEditable.length();
  200. et.selectionStart = Selection.getSelectionStart(mEditable);
  201. et.selectionEnd = Selection.getSelectionEnd(mEditable);
  202. et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
  203. return et;
  204. }
  205. @Override
  206. public boolean deleteSurroundingText(int leftLength, int rightLength) {
  207. log("deleteSurroundingText(%d, %d)", leftLength, rightLength);
  208. if (!super.deleteSurroundingText(leftLength, rightLength)) {
  209. return false;
  210. }
  211. shouldUpdateImeSelection = true;
  212. //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
  213. onDeleteSurroundingText(id, leftLength, rightLength);
  214. return true;
  215. }
  216. @Override
  217. public boolean sendKeyEvent(KeyEvent event) {
  218. int action = event.getAction();
  219. int keycode = event.getKeyCode();
  220. log("sendKeyEvent() [action=%d, keycode=%d]", action, keycode);
  221. //mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
  222. //mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
  223. // If this is a key-up, and backspace/del or if the key has a character representation,
  224. // need to update the underlying Editable (i.e. the local representation of the text
  225. // being edited).
  226. if (event.getAction() == KeyEvent.ACTION_UP) {
  227. if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
  228. super.deleteSurroundingText(1, 0);
  229. } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
  230. super.deleteSurroundingText(0, 1);
  231. } else {
  232. int unicodeChar = event.getUnicodeChar();
  233. if (unicodeChar != 0) {
  234. Editable editable = getEditable();
  235. int selectionStart = Selection.getSelectionStart(editable);
  236. int selectionEnd = Selection.getSelectionEnd(editable);
  237. if (selectionStart > selectionEnd) {
  238. int temp = selectionStart;
  239. selectionStart = selectionEnd;
  240. selectionEnd = temp;
  241. }
  242. String inputChar = Character.toString((char)unicodeChar);
  243. editable.replace(selectionStart, selectionEnd, inputChar);
  244. onCompose(id, inputChar, selectionStart, true);
  245. }
  246. }
  247. }
  248. shouldUpdateImeSelection = true;
  249. return super.sendKeyEvent(event);
  250. }
  251. @Override
  252. public boolean finishComposingText() {
  253. if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
  254. log("finishComposingText() [DISABLED]");
  255. return true;
  256. }
  257. log("finishComposingText()");
  258. super.finishComposingText();
  259. onFinishCompose(id);
  260. return true;
  261. }
  262. @Override
  263. public boolean setSelection(int start, int end) {
  264. log("setSelection(%d, %d)", start, end);
  265. if (start < 0 || end < 0) return true;
  266. super.setSelection(start, end);
  267. shouldUpdateImeSelection = true;
  268. //return mImeAdapter.setEditableSelectionOffsets(start, end);
  269. return true;
  270. }
  271. /**
  272. * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
  273. * is no longer a current composition. Note this differs from finishComposingText, which
  274. * is called by the IME when it wants to end a composition.
  275. */
  276. void cancelComposition() {
  277. log("cancelComposition()");
  278. getInputMethodManager().restartInput(mInternalView);
  279. }
  280. @Override
  281. public boolean setComposingRegion(int start, int end) {
  282. log("setComposingRegion(%d, %d)", start, end);
  283. int a = Math.min(start, end);
  284. int b = Math.max(start, end);
  285. super.setComposingRegion(a, b);
  286. onSetComposeRegion(id, a, b);
  287. return true;
  288. }
  289. boolean isActive() {
  290. return getInputMethodManager().isActive();
  291. }
  292. private InputMethodManager getInputMethodManager() {
  293. InputMethodManager imm = (InputMethodManager)mInternalView.getContext()
  294. .getSystemService(Context.INPUT_METHOD_SERVICE);
  295. if (imm == null) {
  296. Log.e("darkfi", "[IC]: InputMethodManager is NULL!");
  297. }
  298. return imm;
  299. }
  300. private void updateImeSelection() {
  301. log("updateImeSelection()");
  302. getInputMethodManager().updateSelection(
  303. mInternalView,
  304. Selection.getSelectionStart(mEditable),
  305. Selection.getSelectionEnd(mEditable),
  306. getComposingSpanStart(mEditable),
  307. getComposingSpanEnd(mEditable)
  308. );
  309. log("updateImeSelection() DONE");
  310. }
  311. @Override
  312. public boolean beginBatchEdit() {
  313. log("beginBatchEdit");
  314. ++numBatchEdits;
  315. return false;
  316. }
  317. @Override
  318. public boolean endBatchEdit() {
  319. log("endBatchEdit");
  320. if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
  321. updateImeSelection();
  322. shouldUpdateImeSelection = false;
  323. }
  324. log("endBatchEdit DONE");
  325. return false;
  326. }
  327. public static String editableToXml(Editable editable) {
  328. StringBuilder xmlBuilder = new StringBuilder();
  329. int length = editable.length();
  330. Object[] spans = editable.getSpans(0, editable.length(), Object.class);
  331. for (int i = 0; i < length; i++) {
  332. // Find spans starting at this position
  333. for (Object span : spans) {
  334. if (editable.getSpanStart(span) == i) {
  335. xmlBuilder
  336. .append("<")
  337. .append(span.getClass().getSimpleName())
  338. .append(">");
  339. }
  340. }
  341. // Append the character
  342. char c = editable.charAt(i);
  343. xmlBuilder.append(c);
  344. if (Character.isHighSurrogate(c)) {
  345. if (i + 1 < editable.length() && Character.isLowSurrogate(editable.charAt(i + 1))) {
  346. i += 1;
  347. xmlBuilder.append(editable.charAt(i));
  348. }
  349. }
  350. // Find spans ending at this position
  351. for (Object span : spans) {
  352. if (editable.getSpanEnd(span) == i) {
  353. xmlBuilder
  354. .append("</")
  355. .append(span.getClass().getSimpleName())
  356. .append(">");
  357. }
  358. }
  359. }
  360. // Find spans starting at this position
  361. for (Object span : spans) {
  362. if (editable.getSpanStart(span) == length) {
  363. xmlBuilder
  364. .append("<")
  365. .append(span.getClass().getSimpleName())
  366. .append(">");
  367. }
  368. }
  369. // Find spans ending at this position
  370. for (Object span : spans) {
  371. if (editable.getSpanEnd(span) == length) {
  372. xmlBuilder
  373. .append("</")
  374. .append(span.getClass().getSimpleName())
  375. .append(">");
  376. }
  377. }
  378. return xmlBuilder.toString();
  379. }
  380. }