From d44b60c2f001d57b010f0e9b82f798fbad9a23d6 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 20 Jan 2023 19:06:32 +0800 Subject: Update Android port * .gitignore: Don't ignore verbose.mk.android. * doc/emacs/Makefile.in (EMACSSOURCES): Add android.texi and input.texi. * doc/emacs/android.texi (Android): Document support for the on-screen keyboard. (Android Startup): Document how to start Emacs with -Q on Android. (Android Environment): Document how Emacs works around the system ``task killer''. Document changes to frame deletion behavior. * doc/emacs/emacs.texi (Top): * doc/emacs/input.texi (Other Input Devices, On-Screen Keyboards): Document how to use Emacs with virtual keyboards. * doc/lispref/commands.texi (Touchscreen Events): Document changes to `touch-screen-track-drag'. * doc/lispref/frames.texi (Frames, On-Screen Keyboards): New node. * java/AndroidManifest.xml.in: Add settings activity and appropriate OSK adjustment mode. * java/org/gnu/emacs/EmacsActivity.java (onCreate): Allow creating Emacs with -Q. (onDestroy): Don't remove if killed by the system. * java/org/gnu/emacs/EmacsContextMenu.java (inflateMenuItems): Fix context menus again. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Make all event sending functions return long. * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): New class. * java/org/gnu/emacs/EmacsService.java (EmacsService) (onStartCommand, onCreate, startEmacsService): Start as a foreground service if necessary to bypass system restrictions. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsThread.java (EmacsThread, run): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout) (onDetachedFromWindow): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, viewLayout): Implement frame resize synchronization.. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager, removeWindowConsumer): Adjust accordingly for changes to frame deletion behavior. * lisp/frame.el (android-toggle-on-screen-keyboard) (frame-toggle-on-screen-keyboard): New function. * lisp/minibuffer.el (minibuffer-setup-on-screen-keyboard) (minibuffer-exit-on-screen-keyboard): New functions. (minibuffer-setup-hook, minibuffer-exit-hook): Add new functions to hooks. * lisp/touch-screen.el (touch-screen-relative-xy): Accept new value of window `frame'. Return frame coordinates in that case. (touch-screen-set-point-commands): New variable. (touch-screen-handle-point-up): Respect that variable. (touch-screen-track-drag): Return `no-drag' where appropriate. (touch-screen-drag-mode-line-1, touch-screen-drag-mode-line): Refactor to use `no-drag'. * src/android.c (struct android_emacs_window): New methods. Make all event sending functions return the event serial. (android_toggle_on_screen_keyboard, android_window_updated): New functions. * src/android.h: Update prototypes. * src/androidfns.c (Fandroid_toggle_on_screen_keyboard) (syms_of_androidfns): New function. * src/androidgui.h (struct android_any_event) (struct android_key_event, struct android_configure_event) (struct android_focus_event, struct android_window_action_event) (struct android_crossing_event, struct android_motion_event) (struct android_button_event, struct android_touch_event) (struct android_wheel_event, struct android_iconify_event) (struct android_menu_event): Add `serial' fields. * src/androidterm.c (handle_one_android_event) (android_frame_up_to_date): * src/androidterm.h (struct android_output): Implement frame resize synchronization. --- java/AndroidManifest.xml.in | 12 ++ java/org/gnu/emacs/EmacsActivity.java | 20 +++- java/org/gnu/emacs/EmacsContextMenu.java | 1 + java/org/gnu/emacs/EmacsNative.java | 39 ++++--- java/org/gnu/emacs/EmacsPreferencesActivity.java | 98 ++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 62 +++++++++- java/org/gnu/emacs/EmacsSurfaceView.java | 127 +++++++++++++++------ java/org/gnu/emacs/EmacsThread.java | 12 +- java/org/gnu/emacs/EmacsView.java | 55 ++++++++- java/org/gnu/emacs/EmacsWindow.java | 58 ++++++++-- .../gnu/emacs/EmacsWindowAttachmentManager.java | 4 +- 11 files changed, 412 insertions(+), 76 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsPreferencesActivity.java (limited to 'java') diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index b680137a9d0..74f69d2a8e5 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -62,8 +62,10 @@ along with GNU Emacs. If not, see . --> android:theme="@android:style/Theme" android:debuggable="true" android:extractNativeLibs="true"> + @@ -73,8 +75,18 @@ along with GNU Emacs. If not, see . --> + + + + + + + . */ + +package org.gnu.emacs; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; +import android.widget.TextView; + +import android.R; + +/* This module provides a ``preferences'' display for Emacs. It is + supposed to be launched from inside the Settings application to + perform various actions, such as starting Emacs with the ``-Q'' + option, which would not be possible otherwise, as there is no + command line on Android. */ + +public class EmacsPreferencesActivity extends Activity +{ + /* The linear layout associated with the activity. */ + private LinearLayout layout; + + /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, and + tell the system to EmacsActivity with some parameters later. */ + + private void + startEmacsQ () + { + Intent intent; + + intent = new Intent (this, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra ("org.gnu.emacs.START_DASH_Q", true); + startActivity (intent); + System.exit (0); + } + + @Override + public void + onCreate (Bundle savedInstanceState) + { + LinearLayout layout; + TextView textView; + LinearLayout.LayoutParams params; + int resid; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + setTheme (R.style.Theme_DeviceDefault_Settings); + else if (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (R.style.Theme_DeviceDefault); + + layout = new LinearLayout (this); + layout.setOrientation (LinearLayout.VERTICAL); + setContentView (layout); + + textView = new TextView (this); + textView.setPadding (8, 20, 20, 8); + + params = new LinearLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + textView.setLayoutParams (params); + textView.setText ("(Re)start Emacs with -Q"); + textView.setOnClickListener (new View.OnClickListener () { + @Override + public void + onClick (View view) + { + startEmacsQ (); + } + }); + layout.addView (textView); + + super.onCreate (savedInstanceState); + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index bcf8d9ff6e8..95f21b211a3 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -32,7 +32,13 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.annotation.TargetApi; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.app.PendingIntent; import android.app.Service; + import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; @@ -63,6 +69,7 @@ public class EmacsService extends Service public static final String TAG = "EmacsService"; public static final int MAX_PENDING_REQUESTS = 256; public static volatile EmacsService SERVICE; + public static boolean needDashQ; private EmacsThread thread; private Handler handler; @@ -74,6 +81,31 @@ public class EmacsService extends Service public int onStartCommand (Intent intent, int flags, int startId) { + Notification notification; + NotificationManager manager; + NotificationChannel channel; + String infoBlurb; + Object tem; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + tem = getSystemService (Context.NOTIFICATION_SERVICE); + manager = (NotificationManager) tem; + infoBlurb = ("See (emacs)Android Environment for more" + + " details about this notification."); + channel + = new NotificationChannel ("emacs", "Emacs persistent notification", + NotificationManager.IMPORTANCE_DEFAULT); + manager.createNotificationChannel (channel); + notification = (new Notification.Builder (this, "emacs") + .setContentTitle ("Emacs") + .setContentText (infoBlurb) + .setSmallIcon (android.R.drawable.sym_def_app_icon) + .build ()); + manager.notify (1, notification); + startForeground (1, notification); + } + return START_NOT_STICKY; } @@ -137,7 +169,7 @@ public class EmacsService extends Service this); /* Start the thread that runs Emacs. */ - thread = new EmacsThread (this); + thread = new EmacsThread (this, needDashQ); thread.start (); } catch (IOException exception) @@ -444,4 +476,32 @@ public class EmacsService extends Service } } } + + + + /* Start the Emacs service if necessary. On Android 26 and up, + start Emacs as a foreground service with a notification, to avoid + it being killed by the system. + + On older systems, simply start it as a normal background + service. */ + + public static void + startEmacsService (Context context) + { + PendingIntent intent; + + if (EmacsService.SERVICE == null) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + /* Start the Emacs service now. */ + context.startService (new Intent (context, + EmacsService.class)); + else + /* Display the permanant notification and start Emacs as a + foreground service. */ + context.startForegroundService (new Intent (context, + EmacsService.class)); + } + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index f713818d4bc..2fe9e103b2b 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -32,53 +32,106 @@ import android.util.Log; public class EmacsSurfaceView extends SurfaceView { private static final String TAG = "EmacsSurfaceView"; - public Object surfaceChangeLock; private boolean created; + private EmacsView view; - public - EmacsSurfaceView (final EmacsView view) - { - super (view.getContext ()); - - surfaceChangeLock = new Object (); + /* This is the callback used on Android 8 to 25. */ - getHolder ().addCallback (new SurfaceHolder.Callback () { - @Override - public void - surfaceChanged (SurfaceHolder holder, int format, - int width, int height) + private class Callback implements SurfaceHolder.Callback + { + @Override + public void + surfaceChanged (SurfaceHolder holder, int format, + int width, int height) + { + Log.d (TAG, "surfaceChanged: " + view + ", " + view.pendingConfigure); + + /* Make sure not to swap buffers if there is pending + configuration, because otherwise the redraw callback will not + run correctly. */ + + if (view.pendingConfigure == 0) + view.swapBuffers (); + } + + @Override + public void + surfaceCreated (SurfaceHolder holder) + { + synchronized (surfaceChangeLock) { - Log.d (TAG, "surfaceChanged: " + view); - view.swapBuffers (); + Log.d (TAG, "surfaceCreated: " + view); + created = true; } - @Override - public void - surfaceCreated (SurfaceHolder holder) - { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceCreated: " + view); - created = true; - } - - /* Drop the lock when doing this, or a deadlock can - result. */ - view.swapBuffers (); - } + /* Drop the lock when doing this, or a deadlock can + result. */ + view.swapBuffers (); + } - @Override - public void - surfaceDestroyed (SurfaceHolder holder) + @Override + public void + surfaceDestroyed (SurfaceHolder holder) + { + synchronized (surfaceChangeLock) { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceDestroyed: " + view); - created = false; - } + Log.d (TAG, "surfaceDestroyed: " + view); + created = false; } - }); + } + } + + /* And this is the callback used on Android 26 and later. It is + used because it can tell the system when drawing completes. */ + + private class Callback2 extends Callback implements SurfaceHolder.Callback2 + { + @Override + public void + surfaceRedrawNeeded (SurfaceHolder holder) + { + /* This version is not supported. */ + return; + } + + @Override + public void + surfaceRedrawNeededAsync (SurfaceHolder holder, + Runnable drawingFinished) + { + Runnable old; + + Log.d (TAG, "surfaceRedrawNeededAsync: " + view.pendingConfigure); + + /* The system calls this function when it wants to know whether + or not Emacs is still configuring itself in response to a + resize. + + If the view did not send an outstanding ConfigureNotify + event, then call drawingFinish immediately. Else, give it to + the view to execute after drawing completes. */ + + if (view.pendingConfigure == 0) + drawingFinished.run (); + else + /* And set this runnable to run once drawing completes. */ + view.drawingFinished = drawingFinished; + } + } + + public + EmacsSurfaceView (final EmacsView view) + { + super (view.getContext ()); + + this.surfaceChangeLock = new Object (); + this.view = view; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + getHolder ().addCallback (new Callback ()); + else + getHolder ().addCallback (new Callback2 ()); } public boolean diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 0882753747f..f9bc132f354 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -23,12 +23,13 @@ import java.lang.Thread; public class EmacsThread extends Thread { - EmacsService context; + /* Whether or not Emacs should be started -Q. */ + private boolean startDashQ; public - EmacsThread (EmacsService service) + EmacsThread (EmacsService service, boolean startDashQ) { - context = service; + this.startDashQ = startDashQ; } public void @@ -36,7 +37,10 @@ public class EmacsThread extends Thread { String args[]; - args = new String[] { "libandroid-emacs.so", }; + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; /* Run the native code now. */ EmacsNative.initEmacs (args); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 82f44acaebe..74bbb7b3ecc 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import android.content.Context; import android.content.res.ColorStateList; import android.view.ContextMenu; @@ -27,6 +28,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; + import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; @@ -86,11 +89,23 @@ public class EmacsView extends ViewGroup /* The serial of the last clip rectangle change. */ private long lastClipSerial; + /* The InputMethodManager for this view's context. */ + private InputMethodManager imManager; + + /* Runnable that will run once drawing completes. */ + public Runnable drawingFinished; + + /* Serial of the last ConfigureNotify event sent that Emacs has not + yet responded to. 0 if there is no such outstanding event. */ + public long pendingConfigure; + public EmacsView (EmacsWindow window) { super (EmacsService.SERVICE); + Object tem; + this.window = window; this.damageRegion = new Region (); this.paint = new Paint (); @@ -111,6 +126,10 @@ public class EmacsView extends ViewGroup /* Get rid of the default focus highlight. */ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) setDefaultFocusHighlightEnabled (false); + + /* Obtain the input method manager. */ + tem = getContext ().getSystemService (Context.INPUT_METHOD_SERVICE); + imManager = (InputMethodManager) tem; } private void @@ -259,7 +278,8 @@ public class EmacsView extends ViewGroup if (changed || mustReportLayout) { mustReportLayout = false; - window.viewLayout (left, top, right, bottom); + pendingConfigure + = window.viewLayout (left, top, right, bottom); } measuredWidth = right - left; @@ -538,4 +558,37 @@ public class EmacsView extends ViewGroup Runtime.getRuntime ().gc (); } } + + public void + showOnScreenKeyboard () + { + /* Specifying no flags at all tells the system the user asked for + the input method to be displayed. */ + imManager.showSoftInput (this, 0); + } + + public void + hideOnScreenKeyboard () + { + imManager.hideSoftInputFromWindow (this.getWindowToken (), + 0); + } + + public void + windowUpdated (long serial) + { + Log.d (TAG, "windowUpdated: serial is " + serial); + + if (pendingConfigure <= serial + /* Detect wraparound. */ + || pendingConfigure - serial >= 0x7fffffff) + { + pendingConfigure = 0; + + if (drawingFinished != null) + drawingFinished.run (); + + drawingFinished = null; + } + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index c5b1522086c..8511af9193e 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -233,7 +233,7 @@ public class EmacsWindow extends EmacsHandleObject return attached; } - public void + public long viewLayout (int left, int top, int right, int bottom) { int rectWidth, rectHeight; @@ -249,10 +249,10 @@ public class EmacsWindow extends EmacsHandleObject rectWidth = right - left; rectHeight = bottom - top; - EmacsNative.sendConfigureNotify (this.handle, - System.currentTimeMillis (), - left, top, rectWidth, - rectHeight); + return EmacsNative.sendConfigureNotify (this.handle, + System.currentTimeMillis (), + left, top, rectWidth, + rectHeight); } public void @@ -589,11 +589,20 @@ public class EmacsWindow extends EmacsHandleObject EmacsActivity.invalidateFocus (); } + /* Notice that the activity has been detached or destroyed. + + ISFINISHING is set if the activity is not the main activity, or + if the activity was not destroyed in response to explicit user + action. */ + public void - onActivityDetached () + onActivityDetached (boolean isFinishing) { - /* Destroy the associated frame when the activity is detached. */ - EmacsNative.sendWindowAction (this.handle, 0); + /* Destroy the associated frame when the activity is detached in + response to explicit user action. */ + + if (isFinishing) + EmacsNative.sendWindowAction (this.handle, 0); } /* Look through the button state to determine what button EVENT was @@ -1064,4 +1073,37 @@ public class EmacsWindow extends EmacsHandleObject /* Return the resulting coordinates. */ return array; } + + public void + toggleOnScreenKeyboard (final boolean on) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + if (on) + view.showOnScreenKeyboard (); + else + view.hideOnScreenKeyboard (); + } + }); + } + + /* Notice that outstanding configure events have been processed. + SERIAL is checked in the UI thread to verify that no new + configure events have been generated in the mean time. */ + + public void + windowUpdated (final long serial) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.windowUpdated (serial); + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 15eb3bb65c2..510300571b8 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -134,7 +134,7 @@ public class EmacsWindowAttachmentManager } public void - removeWindowConsumer (WindowConsumer consumer) + removeWindowConsumer (WindowConsumer consumer, boolean isFinishing) { EmacsWindow window; @@ -147,7 +147,7 @@ public class EmacsWindowAttachmentManager Log.d (TAG, "removeWindowConsumer: detaching " + window); consumer.detachWindow (); - window.onActivityDetached (); + window.onActivityDetached (isFinishing); } Log.d (TAG, "removeWindowConsumer: removing " + consumer); -- cgit v1.2.1