From 0198b8cffd82893412c738dae8e50c45a99286f1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 12 Feb 2023 20:32:25 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Environment): Document notifications permission. * java/org/gnu/emacs/EmacsEditable.java (EmacsEditable): * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): New files. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Load library dependencies in a less verbose fashion. * java/org/gnu/emacs/EmacsView.java (EmacsView): Make imManager public. (onCreateInputConnection): Set InputType to TYPE_NULL for now. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, onKeyDown) (onKeyUp, getEventUnicodeChar): Correctly handle key events with strings. * lisp/term/android-win.el (android-clear-preedit-text) (android-preedit-text): New special event handlers. * src/android.c (struct android_emacs_window): Add function lookup_string. (android_init_emacs_window): Adjust accordingly. (android_wc_lookup_string): New function. * src/androidgui.h (struct android_key_event): Improve commentary. (enum android_lookup_status): New enum. * src/androidterm.c (handle_one_android_event): Synchronize IM lookup code with X. * src/coding.c (from_unicode_buffer): Implement on Android. * src/coding.h: * src/sfnt.c: Fix commentary. --- java/org/gnu/emacs/EmacsEditable.java | 300 +++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsInputConnection.java | 175 ++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 166 +++------------ java/org/gnu/emacs/EmacsView.java | 7 +- java/org/gnu/emacs/EmacsWindow.java | 96 ++++++++- 5 files changed, 588 insertions(+), 156 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsEditable.java create mode 100644 java/org/gnu/emacs/EmacsInputConnection.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsEditable.java b/java/org/gnu/emacs/EmacsEditable.java new file mode 100644 index 00000000000..79af65a6ccd --- /dev/null +++ b/java/org/gnu/emacs/EmacsEditable.java @@ -0,0 +1,300 @@ +/* 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 . */ + +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; + } +} diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java new file mode 100644 index 00000000000..897a393b984 --- /dev/null +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -0,0 +1,175 @@ +/* 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 . */ + +package org.gnu.emacs; + +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.SurroundingText; +import android.view.KeyEvent; + +import android.text.Editable; + +import android.util.Log; + +/* Android input methods, take number six. + + See EmacsEditable for more details. */ + +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; + + public + EmacsInputConnection (EmacsView view) + { + super (view, false); + this.view = view; + this.editable = new EmacsEditable (this); + } + + @Override + public Editable + getEditable () + { + return editable; + } + + @Override + public boolean + setComposingText (CharSequence text, int newCursorPosition) + { + editable.compositionStart (); + super.setComposingText (text, newCursorPosition); + return true; + } + + @Override + 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 + "\""); + + 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 () + { + switch (currentLargeOffset) + { + case 0: + currentLargeOffset = 100000; + return 100000; + + case 100000: + currentLargeOffset = 30000; + return 30000; + + case 30000: + currentLargeOffset = 0; + return 0; + } + + currentLargeOffset = 0; + return -1; + } +} diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 939348ba420..f0219843d35 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,10 @@ import android.content.res.AssetManager; public class EmacsNative { + /* List of native libraries that must be loaded during class + initialization. */ + private static final String[] libraryDeps; + /* Obtain the fingerprint of this build of Emacs. The fingerprint can be used to determine the dump file name. */ public static native String getFingerprint (); @@ -167,148 +171,26 @@ public class EmacsNative Every time you add a new shared library dependency to Emacs, please add it here as well. */ - try - { - System.loadLibrary ("png_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("selinux_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("crypto_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("pcre_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("packagelistparser_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("gnutls_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("gmp_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("nettle_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("p11-kit_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("tasn1_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("hogweed_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("jansson_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("jpeg_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("tiff_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("xml2_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("icuuc_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ + libraryDeps = new String[] { "png_emacs", "selinux_emacs", + "crypto_emacs", "pcre_emacs", + "packagelistparser_emacs", + "gnutls_emacs", "gmp_emacs", + "nettle_emacs", "p11-kit_emacs", + "tasn1_emacs", "hogweed_emacs", + "jansson_emacs", "jpeg_emacs", + "tiff_emacs", "xml2_emacs", + "icuuc_emacs", }; + + for (String dependency : libraryDeps) + { + try + { + System.loadLibrary (dependency); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } } System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 4fc8104e31f..bc3716f6da8 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -94,7 +94,7 @@ public class EmacsView extends ViewGroup private long lastClipSerial; /* The InputMethodManager for this view's context. */ - private InputMethodManager imManager; + public InputMethodManager imManager; /* Whether or not this view is attached to a window. */ public boolean isAttachedToWindow; @@ -558,8 +558,9 @@ public class EmacsView extends ViewGroup box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; - /* But don't return an InputConnection, in order to force the on - screen keyboard to work correctly. */ + /* Set a reasonable inputType. */ + info.inputType = InputType.TYPE_NULL; + return null; } diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 9e2f2f53270..0eca35cec61 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -23,6 +23,8 @@ import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.List; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import android.content.Context; @@ -100,7 +102,7 @@ public class EmacsWindow extends EmacsHandleObject /* The button state and keyboard modifier mask at the time of the last button press or release event. */ - private int lastButtonState, lastModifiers; + public int lastButtonState, lastModifiers; /* Whether or not the window is mapped, and whether or not it is deiconified. */ @@ -122,6 +124,10 @@ public class EmacsWindow extends EmacsHandleObject to quit Emacs. */ private long lastVolumeButtonRelease; + /* Linked list of character strings which were recently sent as + events. */ + public LinkedHashMap eventStrings; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -155,6 +161,19 @@ public class EmacsWindow extends EmacsHandleObject } scratchGC = new EmacsGC ((short) 0); + + /* Create the map of input method-committed strings. Keep at most + ten strings in the map. */ + + eventStrings + = new LinkedHashMap () { + @Override + protected boolean + removeEldestEntry (Map.Entry entry) + { + return size () > 10; + } + }; } public void @@ -507,10 +526,40 @@ public class EmacsWindow extends EmacsHandleObject return view.getBitmap (); } + /* event.getCharacters is used because older input methods still + require it. */ + @SuppressWarnings ("deprecation") + public int + getEventUnicodeChar (KeyEvent event, int state) + { + String characters; + + if (event.getUnicodeChar (state) != 0) + return event.getUnicodeChar (state); + + characters = event.getCharacters (); + + if (characters != null && characters.length () == 1) + return characters.charAt (0); + + return characters == null ? 0 : -1; + } + + public void + saveUnicodeString (int serial, String string) + { + eventStrings.put (serial, string); + } + + /* event.getCharacters is used because older input methods still + require it. */ + @SuppressWarnings ("deprecation") public void onKeyDown (int keyCode, KeyEvent event) { int state, state_1; + long serial; + String characters; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -537,18 +586,28 @@ public class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - EmacsNative.sendKeyPress (this.handle, - event.getEventTime (), - state, keyCode, - event.getUnicodeChar (state_1)); - lastModifiers = state; + synchronized (eventStrings) + { + serial + = EmacsNative.sendKeyPress (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); + lastModifiers = state; + + characters = event.getCharacters (); + + if (characters != null && characters.length () > 1) + saveUnicodeString ((int) serial, characters); + } } public void onKeyUp (int keyCode, KeyEvent event) { int state, state_1; - long time; + long time, serial; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -575,10 +634,12 @@ public class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - EmacsNative.sendKeyRelease (this.handle, - event.getEventTime (), - state, keyCode, - event.getUnicodeChar (state_1)); + serial + = EmacsNative.sendKeyRelease (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); lastModifiers = state; if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) @@ -1105,4 +1166,17 @@ public class EmacsWindow extends EmacsHandleObject } }); } + + public String + lookupString (int eventSerial) + { + String any; + + synchronized (eventStrings) + { + any = eventStrings.remove (eventSerial); + } + + return any; + } }; -- cgit v1.2.1