mirror of
git://git.sv.gnu.org/emacs.git
synced 2025-12-26 23:31:55 -08:00
* java/org/gnu/emacs/EmacsInputConnection.java (getSurroundingText): Don't print debug information if DEBUG_IC is off.
698 lines
17 KiB
Java
698 lines
17 KiB
Java
/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
|
||
|
||
Copyright (C) 2023 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 commited 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;
|
||
}
|
||
|
||
|
||
|
||
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;
|
||
}
|
||
}
|