1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-01-10 21:50:37 -08:00

Update Android port

* configure.ac (HAVE_TEXT_CONVERSION): Define on Android.
* doc/emacs/input.texi (On-Screen Keyboards): Document ``text
conversion'' slightly.
* doc/lispref/commands.texi (Misc Events): Document new
`text-conversion' event.
* java/org/gnu/emacs/EmacsContextMenu.java (display): Use
`syncRunnable'.
* java/org/gnu/emacs/EmacsDialog.java (display): Likewise.
* java/org/gnu/emacs/EmacsEditable.java: Delete file.
* java/org/gnu/emacs/EmacsInputConnection.java
(EmacsInputConnection): Reimplement from scratch.
* java/org/gnu/emacs/EmacsNative.java (EmacsNative): Add new
functions.
* java/org/gnu/emacs/EmacsService.java (EmacsService, getEmacsView)
(getLocationOnScreen, sync, getClipboardManager, restartEmacs):
Use syncRunnable.
(syncRunnable): New function.
(updateIC, resetIC): New functions.

* java/org/gnu/emacs/EmacsView.java (EmacsView): New field
`inputConnection' and `icMode'.
(onCreateInputConnection): Update accordingly.
(setICMode, getICMode): New functions.

* lisp/bindings.el (global-map): Ignore text conversion events.
* src/alloc.c (mark_frame): Mark text conversion data.
* src/android.c (struct android_emacs_service): New fields
`update_ic' and `reset_ic'.
(event_serial): Export.
(android_query_sem): New function.
(android_init_events): Initialize new semaphore.
(android_write_event): Export.
(android_select): Check for UI thread code.
(setEmacsParams, android_init_emacs_service): Initialize new
methods.
(android_check_query, android_begin_query, android_end_query)
(android_run_in_emacs_thread):
(android_update_ic, android_reset_ic): New functions for
managing synchronous queries from one thread to another.

* src/android.h: Export new functions.
* src/androidgui.h (enum android_event_type): Add input method
events.
(enum android_ime_operation, struct android_ime_event)
(union android_event, enum android_ic_mode): New structs and
enums.

* src/androidterm.c (android_window_to_frame): Allow DPYINFO to
be NULL.
(android_decode_utf16, android_handle_ime_event)
(handle_one_android_event, android_sync_edit)
(android_copy_java_string, beginBatchEdit, endBatchEdit)
(commitCompletion, deleteSurroundingText, finishComposingText)
(getSelectedtext, getTextAfterCursor, getTextBeforeCursor)
(setComposingText, setComposingRegion, setSelection, getSelection)
(performEditorAction, getExtractedText): New functions.
(struct android_conversion_query_context):
(android_perform_conversion_query):
(android_text_to_string):
(struct android_get_selection_context):
(android_get_selection):
(struct android_get_extracted_text_context):
(android_get_extracted_text):
(struct android_extracted_text_request_class):
(struct android_extracted_text_class):
(android_update_selection):
(android_reset_conversion):
(android_set_point):
(android_compose_region_changed):
(android_notify_conversion):
(text_conversion_interface): New functions and structures.
(android_term_init): Initialize text conversion.

* src/coding.c (syms_of_coding): Define Qutf_16le on Android.
* src/frame.c (make_frame): Clear conversion data.
(delete_frame): Reset conversion state.

* src/frame.h (enum text_conversion_operation)
(struct text_conversion_action, struct text_conversion_state)
(GCALIGNED_STRUCT): Update structures.
* src/keyboard.c (read_char, readable_events, kbd_buffer_get_event)
(syms_of_keyboard): Handle text conversion events.
* src/lisp.h:
* src/process.c: Fix includes.

* src/textconv.c (enum textconv_batch_edit_flags, textconv_query)
(reset_frame_state, detect_conversion_events)
(restore_selected_window, really_commit_text)
(really_finish_composing_text, really_set_composing_text)
(really_set_composing_region, really_delete_surrounding_text)
(really_set_point, complete_edit)
(handle_pending_conversion_events_1)
(handle_pending_conversion_events, start_batch_edit)
(end_batch_edit, commit_text, finish_composing_text)
(set_composing_text, set_composing_region, textconv_set_point)
(delete_surrounding_text, get_extracted_text)
(report_selected_window_change, report_point_change)
(register_texconv_interface): New functions.

* src/textconv.h (struct textconv_interface)
(TEXTCONV_SKIP_CONVERSION_REGION): Update prototype.
* src/xdisp.c (mark_window_display_accurate_1):
* src/xfns.c (xic_string_conversion_callback):
* src/xterm.c (init_xterm): Adjust accordingly.
This commit is contained in:
Po Lu 2023-02-15 12:23:03 +08:00
parent 5a7855e84a
commit a158c1d5b9
27 changed files with 2806 additions and 525 deletions

View file

@ -279,20 +279,7 @@ public class EmacsContextMenu
}
};
synchronized (runnable)
{
EmacsService.SERVICE.runOnUiThread (runnable);
try
{
runnable.wait ();
}
catch (InterruptedException e)
{
EmacsNative.emacsAbort ();
}
}
EmacsService.syncRunnable (runnable);
return rc.thing;
}

View file

@ -317,20 +317,7 @@ public class EmacsDialog implements DialogInterface.OnDismissListener
}
};
synchronized (runnable)
{
EmacsService.SERVICE.runOnUiThread (runnable);
try
{
runnable.wait ();
}
catch (InterruptedException e)
{
EmacsNative.emacsAbort ();
}
}
EmacsService.syncRunnable (runnable);
return rc.thing;
}

View file

@ -1,300 +0,0 @@
/* 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.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpanWatcher;
import android.text.Selection;
import android.content.Context;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.text.Spannable;
import android.util.Log;
import android.os.Build;
/* Android input methods insist on having access to buffer contents.
Since Emacs is not designed like ``any other Android text editor'',
that is not possible.
This file provides a fake editing buffer that is designed to weasel
as much information as possible out of an input method, without
actually providing buffer contents to Emacs.
The basic idea is to have the fake editing buffer be initially
empty.
When the input method inserts composed text, it sets a flag.
Updates to the buffer while the flag is set are sent to Emacs to be
displayed as ``preedit text''.
Once some heuristics decide that composition has been completed,
the composed text is sent to Emacs, and the text that was inserted
in this editing buffer is erased. */
public class EmacsEditable extends SpannableStringBuilder
implements SpanWatcher
{
private static final String TAG = "EmacsEditable";
/* Whether or not composition is currently in progress. */
private boolean isComposing;
/* The associated input connection. */
private EmacsInputConnection connection;
/* The associated IM manager. */
private InputMethodManager imManager;
/* Any extracted text an input method may be monitoring. */
private ExtractedText extractedText;
/* The corresponding text request. */
private ExtractedTextRequest extractRequest;
/* The number of nested batch edits. */
private int batchEditCount;
/* Whether or not invalidateInput should be called upon batch edits
ending. */
private boolean pendingInvalidate;
/* The ``composing span'' indicating the bounds of an ongoing
character composition. */
private Object composingSpan;
public
EmacsEditable (EmacsInputConnection connection)
{
/* Initialize the editable with one initial space, so backspace
always works. */
super ();
Object tem;
Context context;
this.connection = connection;
context = connection.view.getContext ();
tem = context.getSystemService (Context.INPUT_METHOD_SERVICE);
imManager = (InputMethodManager) tem;
/* To watch for changes to text properties on Android, you
add... a text property. */
setSpan (this, 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
public void
endBatchEdit ()
{
if (batchEditCount < 1)
return;
if (--batchEditCount == 0 && pendingInvalidate)
invalidateInput ();
}
public void
beginBatchEdit ()
{
++batchEditCount;
}
public void
setExtractedTextAndRequest (ExtractedText text,
ExtractedTextRequest request,
boolean monitor)
{
/* Extract the text. If monitor is set, also record it as the
text that is currently being extracted. */
text.startOffset = 0;
text.selectionStart = Selection.getSelectionStart (this);
text.selectionEnd = Selection.getSelectionStart (this);
text.text = this;
if (monitor)
{
extractedText = text;
extractRequest = request;
}
}
public void
compositionStart ()
{
isComposing = true;
}
public void
compositionEnd ()
{
isComposing = false;
sendComposingText (null);
}
private void
sendComposingText (String string)
{
EmacsWindow window;
long time, serial;
window = connection.view.window;
if (window.isDestroyed ())
return;
time = System.currentTimeMillis ();
/* A composition event is simply a special key event with a
keycode of -1. */
synchronized (window.eventStrings)
{
serial
= EmacsNative.sendKeyPress (window.handle, time, 0, -1, -1);
/* Save the string so that android_lookup_string can find
it. */
if (string != null)
window.saveUnicodeString ((int) serial, string);
}
}
private void
invalidateInput ()
{
int start, end, composingSpanStart, composingSpanEnd;
if (batchEditCount > 0)
{
Log.d (TAG, "invalidateInput: deferring for batch edit");
pendingInvalidate = true;
return;
}
pendingInvalidate = false;
start = Selection.getSelectionStart (this);
end = Selection.getSelectionEnd (this);
if (composingSpan != null)
{
composingSpanStart = getSpanStart (composingSpan);
composingSpanEnd = getSpanEnd (composingSpan);
}
else
{
composingSpanStart = -1;
composingSpanEnd = -1;
}
Log.d (TAG, "invalidateInput: now " + start + ", " + end);
/* Tell the input method that the cursor changed. */
imManager.updateSelection (connection.view, start, end,
composingSpanStart,
composingSpanEnd);
/* If there is any extracted text, tell the IME that it has
changed. */
if (extractedText != null)
imManager.updateExtractedText (connection.view,
extractRequest.token,
extractedText);
}
public SpannableStringBuilder
replace (int start, int end, CharSequence tb, int tbstart,
int tbend)
{
super.replace (start, end, tb, tbstart, tbend);
/* If a change happens during composition, perform the change and
then send the text being composed. */
if (isComposing)
sendComposingText (toString ());
return this;
}
private boolean
isSelectionSpan (Object span)
{
return ((Selection.SELECTION_START == span
|| Selection.SELECTION_END == span)
&& (getSpanFlags (span)
& Spanned.SPAN_INTERMEDIATE) == 0);
}
@Override
public void
onSpanAdded (Spannable text, Object what, int start, int end)
{
Log.d (TAG, "onSpanAdded: " + text + " " + what + " "
+ start + " " + end);
/* Try to find the composing span. This isn't a public API. */
if (what.getClass ().getName ().contains ("ComposingText"))
composingSpan = what;
if (isSelectionSpan (what))
invalidateInput ();
}
@Override
public void
onSpanChanged (Spannable text, Object what, int ostart,
int oend, int nstart, int nend)
{
Log.d (TAG, "onSpanChanged: " + text + " " + what + " "
+ nstart + " " + nend);
if (isSelectionSpan (what))
invalidateInput ();
}
@Override
public void
onSpanRemoved (Spannable text, Object what,
int start, int end)
{
Log.d (TAG, "onSpanRemoved: " + text + " " + what + " "
+ start + " " + end);
if (isSelectionSpan (what))
invalidateInput ();
}
public boolean
isInBatchEdit ()
{
return batchEditCount > 0;
}
}

View file

@ -25,6 +25,7 @@ import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.SurroundingText;
import android.view.inputmethod.TextSnapshot;
import android.view.KeyEvent;
import android.text.Editable;
@ -38,35 +39,115 @@ import android.util.Log;
public class EmacsInputConnection extends BaseInputConnection
{
private static final String TAG = "EmacsInputConnection";
public EmacsView view;
private EmacsEditable editable;
/* The length of the last string to be committed. */
private int lastCommitLength;
int currentLargeOffset;
private EmacsView view;
private short windowHandle;
public
EmacsInputConnection (EmacsView view)
{
super (view, false);
super (view, true);
this.view = view;
this.editable = new EmacsEditable (this);
this.windowHandle = view.window.handle;
}
@Override
public Editable
getEditable ()
public boolean
beginBatchEdit ()
{
return editable;
Log.d (TAG, "beginBatchEdit");
EmacsNative.beginBatchEdit (windowHandle);
return true;
}
@Override
public boolean
endBatchEdit ()
{
Log.d (TAG, "endBatchEdit");
EmacsNative.endBatchEdit (windowHandle);
return true;
}
@Override
public boolean
commitCompletion (CompletionInfo info)
{
Log.d (TAG, "commitCompletion: " + info);
EmacsNative.commitCompletion (windowHandle,
info.getText ().toString (),
info.getPosition ());
return true;
}
@Override
public boolean
commitText (CharSequence text, int newCursorPosition)
{
Log.d (TAG, "commitText: " + text + " " + newCursorPosition);
EmacsNative.commitText (windowHandle, text.toString (),
newCursorPosition);
return true;
}
@Override
public boolean
deleteSurroundingText (int leftLength, int rightLength)
{
Log.d (TAG, ("deleteSurroundingText: "
+ leftLength + " " + rightLength));
EmacsNative.deleteSurroundingText (windowHandle, leftLength,
rightLength);
return true;
}
@Override
public boolean
finishComposingText ()
{
Log.d (TAG, "finishComposingText");
EmacsNative.finishComposingText (windowHandle);
return true;
}
@Override
public String
getSelectedText (int flags)
{
Log.d (TAG, "getSelectedText: " + flags);
return EmacsNative.getSelectedText (windowHandle, flags);
}
@Override
public String
getTextAfterCursor (int length, int flags)
{
Log.d (TAG, "getTextAfterCursor: " + length + " " + flags);
return EmacsNative.getTextAfterCursor (windowHandle, length,
flags);
}
@Override
public String
getTextBeforeCursor (int length, int flags)
{
Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags);
return EmacsNative.getTextBeforeCursor (windowHandle, length,
flags);
}
@Override
public boolean
setComposingText (CharSequence text, int newCursorPosition)
{
editable.compositionStart ();
super.setComposingText (text, newCursorPosition);
Log.d (TAG, "setComposingText: " + newCursorPosition);
EmacsNative.setComposingText (windowHandle, text.toString (),
newCursorPosition);
return true;
}
@ -74,102 +155,40 @@ public class EmacsInputConnection extends BaseInputConnection
public boolean
setComposingRegion (int start, int end)
{
int i;
if (lastCommitLength != 0)
{
Log.d (TAG, "Restarting composition for: " + lastCommitLength);
for (i = 0; i < lastCommitLength; ++i)
sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_DEL));
lastCommitLength = 0;
}
editable.compositionStart ();
super.setComposingRegion (start, end);
return true;
}
@Override
public boolean
finishComposingText ()
{
editable.compositionEnd ();
return super.finishComposingText ();
}
@Override
public boolean
beginBatchEdit ()
{
editable.beginBatchEdit ();
return super.beginBatchEdit ();
}
@Override
public boolean
endBatchEdit ()
{
editable.endBatchEdit ();
return super.endBatchEdit ();
}
@Override
public boolean
commitText (CharSequence text, int newCursorPosition)
{
editable.compositionEnd ();
super.commitText (text, newCursorPosition);
/* An observation is that input methods rarely recompose trailing
spaces. Avoid re-setting the commit length in that case. */
if (text.toString ().equals (" "))
lastCommitLength += 1;
else
/* At this point, the editable is now empty.
The input method may try to cancel the edit upon a subsequent
backspace by calling setComposingRegion with a region that is
the length of TEXT.
Record this length in order to be able to send backspace
events to ``delete'' the text in that case. */
lastCommitLength = text.length ();
Log.d (TAG, "commitText: \"" + text + "\"");
Log.d (TAG, "setComposingRegion: " + start + " " + end);
EmacsNative.setComposingRegion (windowHandle, start, end);
return true;
}
/* Return a large offset, cycling through 100000, 30000, 0.
The offset is typically used to force the input method to update
its notion of ``surrounding text'', bypassing any caching that
it might have in progress.
There must be another way to do this, but I can't find it. */
public int
largeSelectionOffset ()
@Override
public boolean
performEditorAction (int editorAction)
{
switch (currentLargeOffset)
{
case 0:
currentLargeOffset = 100000;
return 100000;
Log.d (TAG, "performEditorAction: " + editorAction);
case 100000:
currentLargeOffset = 30000;
return 30000;
EmacsNative.performEditorAction (windowHandle, editorAction);
return true;
}
case 30000:
currentLargeOffset = 0;
return 0;
}
@Override
public ExtractedText
getExtractedText (ExtractedTextRequest request, int flags)
{
Log.d (TAG, "getExtractedText: " + request + " " + flags);
currentLargeOffset = 0;
return -1;
return EmacsNative.getExtractedText (windowHandle, request,
flags);
}
/* Override functions which are not implemented. */
@Override
public TextSnapshot
takeSnapshot ()
{
Log.d (TAG, "takeSnapshot");
return null;
}
}

View file

@ -22,6 +22,8 @@ package org.gnu.emacs;
import java.lang.System;
import android.content.res.AssetManager;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
public class EmacsNative
{
@ -161,6 +163,50 @@ public class EmacsNative
descriptor, or NULL if there is none. */
public static native byte[] getProcName (int fd);
/* Notice that the Emacs thread will now start waiting for the main
thread's looper to respond. */
public static native void beginSynchronous ();
/* Notice that the Emacs thread will has finished waiting for the
main thread's looper to respond. */
public static native void endSynchronous ();
/* Input connection functions. These mostly correspond to their
counterparts in Android's InputConnection. */
public static native void beginBatchEdit (short window);
public static native void endBatchEdit (short window);
public static native void commitCompletion (short window, String text,
int position);
public static native void commitText (short window, String text,
int position);
public static native void deleteSurroundingText (short window,
int leftLength,
int rightLength);
public static native void finishComposingText (short window);
public static native String getSelectedText (short window, int flags);
public static native String getTextAfterCursor (short window, int length,
int flags);
public static native String getTextBeforeCursor (short window, int length,
int flags);
public static native void setComposingText (short window, String text,
int newCursorPosition);
public static native void setComposingRegion (short window, int start,
int end);
public static native void setSelection (short window, int start, int end);
public static native void performEditorAction (short window,
int editorAction);
public static native ExtractedText getExtractedText (short window,
ExtractedTextRequest req,
int flags);
/* Return the current value of the selection, or -1 upon
failure. */
public static native int getSelection (short window);
static
{
/* Older versions of Android cannot link correctly with shared

View file

@ -80,6 +80,11 @@ public class EmacsService extends Service
private EmacsThread thread;
private Handler handler;
/* Keep this in synch with androidgui.h. */
public static final int IC_MODE_NULL = 0;
public static final int IC_MODE_ACTION = 1;
public static final int IC_MODE_TEXT = 2;
/* Display metrics used by font backends. */
public DisplayMetrics metrics;
@ -258,20 +263,7 @@ public class EmacsService extends Service
}
};
synchronized (runnable)
{
runOnUiThread (runnable);
try
{
runnable.wait ();
}
catch (InterruptedException e)
{
EmacsNative.emacsAbort ();
}
}
syncRunnable (runnable);
return view.thing;
}
@ -292,19 +284,7 @@ public class EmacsService extends Service
}
};
synchronized (runnable)
{
runOnUiThread (runnable);
try
{
runnable.wait ();
}
catch (InterruptedException e)
{
EmacsNative.emacsAbort ();
}
}
syncRunnable (runnable);
}
public void
@ -502,19 +482,7 @@ public class EmacsService extends Service
}
};
synchronized (runnable)
{
runOnUiThread (runnable);
try
{
runnable.wait ();
}
catch (InterruptedException e)
{
EmacsNative.emacsAbort ();
}
}
syncRunnable (runnable);
}
@ -594,20 +562,7 @@ public class EmacsService extends Service
}
};
synchronized (runnable)
{
runOnUiThread (runnable);
try
{
runnable.wait ();
}
catch (InterruptedException e)
{
EmacsNative.emacsAbort ();
}
}
syncRunnable (runnable);
return manager.thing;
}
@ -622,4 +577,58 @@ public class EmacsService extends Service
startActivity (intent);
System.exit (0);
}
/* Wait synchronously for the specified RUNNABLE to complete in the
UI thread. Must be called from the Emacs thread. */
public static void
syncRunnable (Runnable runnable)
{
EmacsNative.beginSynchronous ();
synchronized (runnable)
{
SERVICE.runOnUiThread (runnable);
while (true)
{
try
{
runnable.wait ();
break;
}
catch (InterruptedException e)
{
continue;
}
}
}
EmacsNative.endSynchronous ();
}
public void
updateIC (EmacsWindow window, int newSelectionStart,
int newSelectionEnd, int composingRegionStart,
int composingRegionEnd)
{
Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart
+ " " + newSelectionEnd + " "
+ composingRegionStart + " "
+ composingRegionEnd));
window.view.imManager.updateSelection (window.view,
newSelectionStart,
newSelectionEnd,
composingRegionStart,
composingRegionEnd);
}
public void
resetIC (EmacsWindow window, int icMode)
{
Log.d (TAG, "resetIC: " + window);
window.view.setICMode (icMode);
window.view.imManager.restartInput (window.view);
}
};

View file

@ -103,6 +103,13 @@ public class EmacsView extends ViewGroup
displayed whenever possible. */
public boolean isCurrentlyTextEditor;
/* The associated input connection. */
private EmacsInputConnection inputConnection;
/* The current IC mode. See `android_reset_ic' for more
details. */
private int icMode;
public
EmacsView (EmacsWindow window)
{
@ -554,14 +561,46 @@ public class EmacsView extends ViewGroup
public InputConnection
onCreateInputConnection (EditorInfo info)
{
int selection, mode;
/* Figure out what kind of IME behavior Emacs wants. */
mode = getICMode ();
/* Make sure the input method never displays a full screen input
box that obscures Emacs. */
info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
/* Set a reasonable inputType. */
info.inputType = InputType.TYPE_NULL;
info.inputType = InputType.TYPE_CLASS_TEXT;
return null;
/* Obtain the current position of point and set it as the
selection. */
selection = EmacsNative.getSelection (window.handle);
Log.d (TAG, "onCreateInputConnection: current selection is: " + selection);
/* If this fails or ANDROID_IC_MODE_NULL was requested, then don't
initialize the input connection. */
if (selection == -1 || mode == EmacsService.IC_MODE_NULL)
{
info.inputType = InputType.TYPE_NULL;
return null;
}
if (mode == EmacsService.IC_MODE_ACTION)
info.imeOptions |= EditorInfo.IME_ACTION_DONE;
/* Set the initial selection fields. */
info.initialSelStart = selection;
info.initialSelEnd = selection;
/* Create the input connection if necessary. */
if (inputConnection == null)
inputConnection = new EmacsInputConnection (this);
/* Return the input connection. */
return inputConnection;
}
@Override
@ -572,4 +611,16 @@ public class EmacsView extends ViewGroup
keyboard. */
return isCurrentlyTextEditor;
}
public synchronized void
setICMode (int icMode)
{
this.icMode = icMode;
}
public synchronized int
getICMode ()
{
return icMode;
}
};