summaryrefslogtreecommitdiff
path: root/java/org/gnu/emacs/EmacsInputConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/org/gnu/emacs/EmacsInputConnection.java')
-rw-r--r--java/org/gnu/emacs/EmacsInputConnection.java713
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;
+ }
+}