diff options
Diffstat (limited to 'java/org/gnu/emacs/EmacsInputConnection.java')
-rw-r--r-- | java/org/gnu/emacs/EmacsInputConnection.java | 713 |
1 files changed, 713 insertions, 0 deletions
diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java new file mode 100644 index 00000000000..054eca66cf3 --- /dev/null +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -0,0 +1,713 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023-2024 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 <https://www.gnu.org/licenses/>. */ + +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 short 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; + } +} |