CustomInputConnection.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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.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 = true;
  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. if (textUnchanged || prevText.equals("")) {
  107. log("setEditableText updating selection");
  108. // updateSelection should be called when a manual selection change occurs.
  109. // Should not be called if text is being entered else issues can occur
  110. // e.g. backspace to undo autocorrection will not work with the default OSK.
  111. getInputMethodManager().updateSelection(mInternalView,
  112. selectionStart, selectionEnd, compositionStart, compositionEnd);
  113. }
  114. }
  115. @Override
  116. public Editable getEditable() {
  117. log("getEditable() -> %s", editableToXml(mEditable));
  118. return mEditable;
  119. }
  120. @Override
  121. public boolean setComposingText(CharSequence text, int newCursorPosition) {
  122. log("setComposingText(%s, %d)", text, newCursorPosition);
  123. super.setComposingText(text, newCursorPosition);
  124. shouldUpdateImeSelection = true;
  125. onCompose(id, text.toString(), newCursorPosition, false);
  126. return true;
  127. }
  128. @Override
  129. public boolean commitText(CharSequence text, int newCursorPosition) {
  130. log("commitText(%s, %d)", text, newCursorPosition);
  131. super.commitText(text, newCursorPosition);
  132. shouldUpdateImeSelection = true;
  133. onCompose(id, text.toString(), newCursorPosition, text.length() > 0);
  134. return true;
  135. }
  136. @Override
  137. public boolean performEditorAction(int actionCode) {
  138. log("performEditorAction(%d)", actionCode);
  139. switch (actionCode) {
  140. case EditorInfo.IME_ACTION_NEXT:
  141. cancelComposition();
  142. // Send TAB key event
  143. long timeStampMs = System.currentTimeMillis();
  144. //mImeAdapter.sendSyntheticKeyEvent(
  145. // sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
  146. return true;
  147. case EditorInfo.IME_ACTION_GO:
  148. case EditorInfo.IME_ACTION_SEARCH:
  149. //mImeAdapter.dismissInput(true);
  150. break;
  151. }
  152. return super.performEditorAction(actionCode);
  153. }
  154. @Override
  155. public boolean performContextMenuAction(int id) {
  156. log("performContextMenuAction(%d)", id);
  157. /*
  158. switch (id) {
  159. case android.R.id.selectAll:
  160. return mImeAdapter.selectAll();
  161. case android.R.id.cut:
  162. return mImeAdapter.cut();
  163. case android.R.id.copy:
  164. return mImeAdapter.copy();
  165. case android.R.id.paste:
  166. return mImeAdapter.paste();
  167. default:
  168. return false;
  169. }
  170. */
  171. return false;
  172. }
  173. @Override
  174. public CharSequence getTextAfterCursor(int length, int flags) {
  175. log("getTextAfterCursor(%d, %d)", length, flags);
  176. return super.getTextAfterCursor(length, flags);
  177. }
  178. @Override
  179. public CharSequence getTextBeforeCursor(int length, int flags) {
  180. log("getTextBeforeCursor(%d, %d)", length, flags);
  181. return super.getTextBeforeCursor(length, flags);
  182. }
  183. @Override
  184. public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) {
  185. log("getSurroundingText(%d, %d, %d)", beforeLength, afterLength, flags);
  186. return super.getSurroundingText(beforeLength, afterLength, flags);
  187. }
  188. @Override
  189. public CharSequence getSelectedText(int flags) {
  190. log("getSelectedText(%d)", flags);
  191. return super.getSelectedText(flags);
  192. }
  193. @Override
  194. public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
  195. log("getExtractedText(...)");
  196. ExtractedText et = new ExtractedText();
  197. et.text = mEditable.toString();
  198. et.partialEndOffset = mEditable.length();
  199. et.selectionStart = Selection.getSelectionStart(mEditable);
  200. et.selectionEnd = Selection.getSelectionEnd(mEditable);
  201. et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
  202. return et;
  203. }
  204. @Override
  205. public boolean deleteSurroundingText(int leftLength, int rightLength) {
  206. log("deleteSurroundingText(%d, %d)", leftLength, rightLength);
  207. if (!super.deleteSurroundingText(leftLength, rightLength)) {
  208. return false;
  209. }
  210. shouldUpdateImeSelection = true;
  211. //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
  212. onDeleteSurroundingText(id, leftLength, rightLength);
  213. return true;
  214. }
  215. @Override
  216. public boolean sendKeyEvent(KeyEvent event) {
  217. int action = event.getAction();
  218. int keycode = event.getKeyCode();
  219. log("sendKeyEvent() [action=%d, keycode=%d]", action, keycode);
  220. //mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
  221. //mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
  222. // If this is a key-up, and backspace/del or if the key has a character representation,
  223. // need to update the underlying Editable (i.e. the local representation of the text
  224. // being edited).
  225. if (event.getAction() == KeyEvent.ACTION_UP) {
  226. if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
  227. super.deleteSurroundingText(1, 0);
  228. } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
  229. super.deleteSurroundingText(0, 1);
  230. } else {
  231. int unicodeChar = event.getUnicodeChar();
  232. if (unicodeChar != 0) {
  233. Editable editable = getEditable();
  234. int selectionStart = Selection.getSelectionStart(editable);
  235. int selectionEnd = Selection.getSelectionEnd(editable);
  236. if (selectionStart > selectionEnd) {
  237. int temp = selectionStart;
  238. selectionStart = selectionEnd;
  239. selectionEnd = temp;
  240. }
  241. editable.replace(selectionStart, selectionEnd,
  242. Character.toString((char)unicodeChar));
  243. }
  244. }
  245. }
  246. shouldUpdateImeSelection = true;
  247. return super.sendKeyEvent(event);
  248. }
  249. @Override
  250. public boolean finishComposingText() {
  251. log("finishComposingText()");
  252. if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
  253. return true;
  254. }
  255. super.finishComposingText();
  256. onFinishCompose(id);
  257. return true;
  258. }
  259. @Override
  260. public boolean setSelection(int start, int end) {
  261. log("setSelection(%d, %d)", start, end);
  262. if (start < 0 || end < 0) return true;
  263. super.setSelection(start, end);
  264. shouldUpdateImeSelection = true;
  265. //return mImeAdapter.setEditableSelectionOffsets(start, end);
  266. return true;
  267. }
  268. /**
  269. * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
  270. * is no longer a current composition. Note this differs from finishComposingText, which
  271. * is called by the IME when it wants to end a composition.
  272. */
  273. void cancelComposition() {
  274. log("cancelComposition()");
  275. getInputMethodManager().restartInput(mInternalView);
  276. }
  277. @Override
  278. public boolean setComposingRegion(int start, int end) {
  279. log("setComposingRegion(%d, %d)", start, end);
  280. int a = Math.min(start, end);
  281. int b = Math.max(start, end);
  282. super.setComposingRegion(a, b);
  283. onSetComposeRegion(id, a, b);
  284. return true;
  285. }
  286. boolean isActive() {
  287. return getInputMethodManager().isActive();
  288. }
  289. private InputMethodManager getInputMethodManager() {
  290. InputMethodManager imm = (InputMethodManager)mInternalView.getContext()
  291. .getSystemService(Context.INPUT_METHOD_SERVICE);
  292. if (imm == null) {
  293. Log.e("darkfi", "[IC]: InputMethodManager is NULL!");
  294. }
  295. return imm;
  296. }
  297. private void updateImeSelection() {
  298. log("updateImeSelection()");
  299. getInputMethodManager().updateSelection(
  300. mInternalView,
  301. Selection.getSelectionStart(mEditable),
  302. Selection.getSelectionEnd(mEditable),
  303. getComposingSpanStart(mEditable),
  304. getComposingSpanEnd(mEditable)
  305. );
  306. log("updateImeSelection() DONE");
  307. }
  308. @Override
  309. public boolean beginBatchEdit() {
  310. log("beginBatchEdit");
  311. ++numBatchEdits;
  312. return false;
  313. }
  314. @Override
  315. public boolean endBatchEdit() {
  316. log("endBatchEdit");
  317. if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
  318. updateImeSelection();
  319. shouldUpdateImeSelection = false;
  320. }
  321. log("endBatchEdit DONE");
  322. return false;
  323. }
  324. public static String editableToXml(Editable editable) {
  325. StringBuilder xmlBuilder = new StringBuilder();
  326. int length = editable.length();
  327. Object[] spans = editable.getSpans(0, editable.length(), Object.class);
  328. for (int i = 0; i < length; i++) {
  329. // Find spans starting at this position
  330. for (Object span : spans) {
  331. if (editable.getSpanStart(span) == i) {
  332. xmlBuilder
  333. .append("<")
  334. .append(span.getClass().getSimpleName())
  335. .append(">");
  336. }
  337. }
  338. // Append the character
  339. char c = editable.charAt(i);
  340. xmlBuilder.append(c);
  341. if (Character.isHighSurrogate(c)) {
  342. if (i + 1 < editable.length() && Character.isLowSurrogate(editable.charAt(i + 1))) {
  343. i += 1;
  344. xmlBuilder.append(editable.charAt(i));
  345. }
  346. }
  347. // Find spans ending at this position
  348. for (Object span : spans) {
  349. if (editable.getSpanEnd(span) == i) {
  350. xmlBuilder
  351. .append("</")
  352. .append(span.getClass().getSimpleName())
  353. .append(">");
  354. }
  355. }
  356. }
  357. // Find spans starting at this position
  358. for (Object span : spans) {
  359. if (editable.getSpanStart(span) == length) {
  360. xmlBuilder
  361. .append("<")
  362. .append(span.getClass().getSimpleName())
  363. .append(">");
  364. }
  365. }
  366. // Find spans ending at this position
  367. for (Object span : spans) {
  368. if (editable.getSpanEnd(span) == length) {
  369. xmlBuilder
  370. .append("</")
  371. .append(span.getClass().getSimpleName())
  372. .append(">");
  373. }
  374. }
  375. return xmlBuilder.toString();
  376. }
  377. }