/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- Copyright (C) 2023-2025 Free Software Foundation, Inc. This file is part of GNU Emacs. GNU Emacs is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. GNU Emacs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.KeyEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextAttribute; import android.view.inputmethod.TextSnapshot; import android.util.Log; /* Android input methods, take number six. See textconv.c for more details; this is more-or-less a thin wrapper around that file. */ public final class EmacsInputConnection implements InputConnection { private static final String TAG = "EmacsInputConnection"; /* View associated with this input connection. */ private EmacsView view; /* The handle ID associated with that view's window. */ private long windowHandle; /* Number of batch edits currently underway. Used to avoid synchronizing with the Emacs thread after each `endBatchEdit'. */ private int batchEditCount; /* Whether or not to synchronize and call `updateIC' with the selection position after committing text. This helps with on screen keyboard programs found in some vendor versions of Android, which rely on immediate updates to the point position after text is committed in order to place the cursor within that text. */ private static boolean syncAfterCommit; /* Whether or not to return empty text with the offset set to zero if a request arrives that has no flags set and has requested no characters at all. This is necessary with on screen keyboard programs found in some vendor versions of Android which don't rely on the documented meaning of `ExtractedText.startOffset', and instead take the selection offset inside at face value. */ private static boolean extractAbsoluteOffsets; static { if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei") || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) extractAbsoluteOffsets = syncAfterCommit = true; /* The Samsung and Vivo keyboards take `selectionStart' at face value if some text is returned, and also searches for words solely within that text. However, when no text is returned, it falls back to getTextAfterCursor and getTextBeforeCursor. */ if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung") || Build.MANUFACTURER.equalsIgnoreCase ("Vivo")) extractAbsoluteOffsets = true; }; public EmacsInputConnection (EmacsView view) { this.view = view; this.windowHandle = view.window.handle; } /* The functions below are called by input methods whenever they need to perform an edit. */ @Override public boolean beginBatchEdit () { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "beginBatchEdit"); EmacsNative.beginBatchEdit (windowHandle); /* Keep a record of the number of outstanding batch edits here as well. */ batchEditCount++; return true; } @Override public boolean endBatchEdit () { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "endBatchEdit"); EmacsNative.endBatchEdit (windowHandle); /* Subtract one from the UI thread record of the number of batch edits currently under way. */ if (batchEditCount > 0) batchEditCount -= 1; return batchEditCount > 0; } public boolean commitCompletion (CompletionInfo info) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "commitCompletion: " + info); EmacsNative.commitCompletion (windowHandle, info.getText ().toString (), info.getPosition ()); return true; } @Override public boolean commitCorrection (CorrectionInfo info) { /* The input method calls this function not to commit text, but to indicate that a subsequent edit will consist of a correction. Emacs has no use for this information. Of course this completely contradicts the provided documentation, but this is how Android actually behaves. */ return false; } @Override public boolean commitText (CharSequence text, int newCursorPosition) { int[] selection; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "commitText: " + text + " " + newCursorPosition); EmacsNative.commitText (windowHandle, text.toString (), newCursorPosition); if (syncAfterCommit) { /* Synchronize with the Emacs thread, obtain the new selection, and report it immediately. */ selection = EmacsNative.getSelection (windowHandle); if (EmacsService.DEBUG_IC && selection != null) Log.d (TAG, "commitText: new selection is " + selection[0] + ", by " + selection[1]); if (selection != null) /* N.B. that the composing region is removed after text is committed. */ view.imManager.updateSelection (view, selection[0], selection[1], -1, -1); } return true; } @Override public boolean commitText (CharSequence text, int newCursorPosition, TextAttribute textAttribute) { return commitText (text, newCursorPosition); } @Override public boolean deleteSurroundingText (int leftLength, int rightLength) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, ("deleteSurroundingText: " + leftLength + " " + rightLength)); EmacsNative.deleteSurroundingText (windowHandle, leftLength, rightLength); return true; } @Override public boolean deleteSurroundingTextInCodePoints (int leftLength, int rightLength) { /* Emacs returns characters which cannot be represented in a Java `char' as NULL characters, so code points always reflect characters themselves. */ return deleteSurroundingText (leftLength, rightLength); } @Override public boolean finishComposingText () { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "finishComposingText"); EmacsNative.finishComposingText (windowHandle); return true; } @Override public String getSelectedText (int flags) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return null; if (EmacsService.DEBUG_IC) Log.d (TAG, "getSelectedText: " + flags); return EmacsNative.getSelectedText (windowHandle, flags); } @Override public String getTextAfterCursor (int length, int flags) { String string; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return null; if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); string = EmacsNative.getTextAfterCursor (windowHandle, length, flags); if (EmacsService.DEBUG_IC) Log.d (TAG, " --> " + string); return string; } @Override public String getTextBeforeCursor (int length, int flags) { String string; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return null; if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); string = EmacsNative.getTextBeforeCursor (windowHandle, length, flags); if (EmacsService.DEBUG_IC) Log.d (TAG, " --> " + string); return string; } @Override public boolean setComposingText (CharSequence text, int newCursorPosition) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, ("setComposingText: " + text + " ## " + newCursorPosition)); EmacsNative.setComposingText (windowHandle, text.toString (), newCursorPosition); return true; } @Override public boolean setComposingText (CharSequence text, int newCursorPosition, TextAttribute textAttribute) { return setComposingText (text, newCursorPosition); } @Override public boolean setComposingRegion (int start, int end) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "setComposingRegion: " + start + " " + end); EmacsNative.setComposingRegion (windowHandle, start, end); return true; } @Override public boolean setComposingRegion (int start, int end, TextAttribute textAttribute) { return setComposingRegion (start, end); } @Override public boolean performEditorAction (int editorAction) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "performEditorAction: " + editorAction); EmacsNative.performEditorAction (windowHandle, editorAction); return true; } @Override public boolean performContextMenuAction (int contextMenuAction) { int action; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "performContextMenuAction: " + contextMenuAction); /* Translate the action in Java code. That way, a great deal of JNI boilerplate can be avoided. */ switch (contextMenuAction) { case android.R.id.selectAll: action = 0; break; case android.R.id.startSelectingText: action = 1; break; case android.R.id.stopSelectingText: action = 2; break; case android.R.id.cut: action = 3; break; case android.R.id.copy: action = 4; break; case android.R.id.paste: action = 5; break; default: return true; } EmacsNative.performContextMenuAction (windowHandle, action); return true; } @Override public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) { ExtractedText text; int[] selection; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return null; if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " + request.hintMaxLines + " " + flags); /* If a request arrives with hintMaxChars, hintMaxLines and flags set to 0, and the system is known to be buggy, return an empty extracted text object with the absolute selection positions. */ if (extractAbsoluteOffsets && request.hintMaxChars == 0 && request.hintMaxLines == 0 && flags == 0) { /* Obtain the selection. */ selection = EmacsNative.getSelection (windowHandle); if (selection == null) return null; /* Create the workaround extracted text. */ text = new ExtractedText (); text.partialStartOffset = -1; text.partialEndOffset = -1; text.text = ""; text.selectionStart = selection[0]; text.selectionEnd = selection[1]; } else text = EmacsNative.getExtractedText (windowHandle, request, flags); if (text == null) { if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: text is NULL"); return null; } if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + text.text + " @" + text.startOffset + ":" + text.selectionStart + ", " + text.selectionEnd); return text; } @Override public boolean setSelection (int start, int end) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "setSelection: " + start + " " + end); EmacsNative.setSelection (windowHandle, start, end); return true; } @Override /* ACTION_MULTIPLE is apparently obsolete. */ @SuppressWarnings ("deprecation") public boolean sendKeyEvent (KeyEvent key) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "sendKeyEvent: " + key); /* Use the standard API if possible. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) view.imManager.dispatchKeyEventFromInputMethod (view, key); else { /* Fall back to dispatching the event manually if not. */ switch (key.getAction ()) { case KeyEvent.ACTION_DOWN: view.onKeyDown (key.getKeyCode (), key); break; case KeyEvent.ACTION_UP: view.onKeyUp (key.getKeyCode (), key); break; case KeyEvent.ACTION_MULTIPLE: view.onKeyMultiple (key.getKeyCode (), key.getRepeatCount (), key); break; } } return true; } @Override public boolean requestCursorUpdates (int cursorUpdateMode) { /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return false; if (EmacsService.DEBUG_IC) Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode); return true; } @Override public boolean requestCursorUpdates (int cursorUpdateMode, int filter) { if (filter != 0) return false; return requestCursorUpdates (cursorUpdateMode); } @Override public SurroundingText getSurroundingText (int beforeLength, int afterLength, int flags) { SurroundingText text; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return null; if (EmacsService.DEBUG_IC) Log.d (TAG, ("getSurroundingText: " + beforeLength + ", " + afterLength)); text = EmacsNative.getSurroundingText (windowHandle, beforeLength, afterLength, flags); if (EmacsService.DEBUG_IC && text != null) Log.d (TAG, ("getSurroundingText: " + text.getSelectionStart () + "," + text.getSelectionEnd () + "+" + text.getOffset () + ": " + text.getText ())); return text; } @Override public TextSnapshot takeSnapshot () { TextSnapshot snapshot; /* Return if the input connection is out of date. */ if (view.icSerial < view.icGeneration) return null; snapshot = EmacsNative.takeSnapshot (windowHandle); if (EmacsService.DEBUG_IC) Log.d (TAG, ("takeSnapshot: " + snapshot.getSurroundingText ().getText () + " @ " + snapshot.getCompositionEnd () + ", " + snapshot.getCompositionStart ())); return snapshot; } @Override public void closeConnection () { batchEditCount = 0; } @Override public boolean replaceText (int start, int end, CharSequence text, int newCursorPosition, TextAttribute attributes) { if (EmacsService.DEBUG_IC) Log.d (TAG, ("replaceText: " + text + ":: " + start + "," + end + "," + newCursorPosition)); EmacsNative.replaceText (windowHandle, start, end, text.toString (), newCursorPosition, attributes); return true; } public void reset () { batchEditCount = 0; } /* Override functions which are not implemented. */ @Override public Handler getHandler () { return null; } @Override public boolean commitContent (InputContentInfo inputContentInfo, int flags, Bundle opts) { return false; } @Override public boolean setImeConsumesInput (boolean imeConsumesInput) { return false; } @Override public boolean clearMetaKeyStates (int states) { return false; } @Override public boolean reportFullscreenMode (boolean enabled) { return false; } @Override public boolean performSpellCheck () { return false; } @Override public boolean performPrivateCommand (String action, Bundle data) { return false; } @Override public int getCursorCapsMode (int reqModes) { return 0; } }