mirror of
git://git.sv.gnu.org/emacs.git
synced 2025-12-05 22:20:24 -08:00
* doc/emacs/input.texi (On-Screen Keyboards): * doc/lispref/commands.texi (Misc Events): Improve documentation of text conversion stuff. * java/org/gnu/emacs/EmacsInputConnection.java (beginBatchEdit) (endBatchEdit, commitCompletion, commitText, deleteSurroundingText) (finishComposingText, getSelectedText, getTextAfterCursor) (EmacsInputConnection, setComposingRegion, performEditorAction) (getExtractedText): Condition debug code on DEBUG_IC. * java/org/gnu/emacs/EmacsService.java (EmacsService, updateIC): Likewise. * lisp/bindings.el (global-map): * lisp/electric.el (global-map): Make `text-conversion' `analyze-text-conversion'. * lisp/progmodes/prog-mode.el (prog-mode): Enable text conversion in input methods. * lisp/simple.el (analyze-text-conversion): New function. * lisp/textmodes/text-mode.el (text-conversion-style) (text-mode): Likewise. * src/androidterm.c (android_handle_ime_event): Handle set_point_and_mark. (android_sync_edit): Give Emacs 100 ms instead. (android_perform_conversion_query): Skip the active region, not the conversion region. (getSelectedText): Implement properly. (android_update_selection): Expose mark to input methods. (android_reset_conversion): Handle `text-conversion-style'. * src/buffer.c (init_buffer_once, syms_of_buffer): Add buffer local variable `text-conversion-style'. * src/buffer.h (struct buffer, bset_text_conversion_style): New fields. * src/emacs.c (android_emacs_init): Call syms_of_textconv. * src/frame.h (enum text_conversion_operation): Rename TEXTCONV_SET_POINT. * src/lisp.h: Export syms_of_textconv. * src/marker.c (set_marker_internal): Force redisplay when the mark is set and the buffer is visible on builds that use text conversion. Explain why. * src/textconv.c (copy_buffer): Fix copying past gap. (get_mark): New function. (textconv_query): Implement new flag. (sync_overlay): New function. Display conversion text in an overlay. (record_buffer_change, really_commit_text) (really_set_composing_text, really_set_composing_region) (really_delete_surrounding_text, really_set_point) (handle_pending_conversion_events_1, decrement_inside) (handle_pending_conversion_events, textconv_set_point) (get_extracted_text, register_textconv_interface): Various fixes and improvements. * src/textconv.h (struct textconv_interface): Update documentation. * src/window.h (GCALIGNED_STRUCT): New field `prev_mark'. * src/xdisp.c (mark_window_display_accurate_1): Handle prev_mark.
638 lines
14 KiB
Java
638 lines
14 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 java.io.IOException;
|
||
import java.util.List;
|
||
import java.util.ArrayList;
|
||
|
||
import android.graphics.Canvas;
|
||
import android.graphics.Bitmap;
|
||
import android.graphics.Point;
|
||
|
||
import android.view.View;
|
||
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.ClipboardManager;
|
||
import android.content.Context;
|
||
import android.content.Intent;
|
||
import android.content.pm.ApplicationInfo;
|
||
import android.content.pm.PackageManager.ApplicationInfoFlags;
|
||
import android.content.pm.PackageManager;
|
||
import android.content.res.AssetManager;
|
||
|
||
import android.net.Uri;
|
||
|
||
import android.os.Build;
|
||
import android.os.Looper;
|
||
import android.os.IBinder;
|
||
import android.os.Handler;
|
||
import android.os.Vibrator;
|
||
import android.os.VibratorManager;
|
||
import android.os.VibrationEffect;
|
||
|
||
import android.util.Log;
|
||
import android.util.DisplayMetrics;
|
||
|
||
import android.hardware.input.InputManager;
|
||
|
||
class Holder<T>
|
||
{
|
||
T thing;
|
||
};
|
||
|
||
/* EmacsService is the service that starts the thread running Emacs
|
||
and handles requests by that Emacs instance. */
|
||
|
||
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;
|
||
|
||
/* 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;
|
||
|
||
public static final boolean DEBUG_IC = false;
|
||
|
||
@Override
|
||
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;
|
||
}
|
||
|
||
@Override
|
||
public IBinder
|
||
onBind (Intent intent)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
@SuppressWarnings ("deprecation")
|
||
private String
|
||
getApkFile ()
|
||
{
|
||
PackageManager manager;
|
||
ApplicationInfo info;
|
||
|
||
manager = getPackageManager ();
|
||
|
||
try
|
||
{
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
|
||
info = manager.getApplicationInfo ("org.gnu.emacs", 0);
|
||
else
|
||
info = manager.getApplicationInfo ("org.gnu.emacs",
|
||
ApplicationInfoFlags.of (0));
|
||
|
||
/* Return an empty string upon failure. */
|
||
|
||
if (info.sourceDir != null)
|
||
return info.sourceDir;
|
||
|
||
return "";
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private String
|
||
getLibraryDirectory ()
|
||
{
|
||
int apiLevel;
|
||
Context context;
|
||
|
||
context = getApplicationContext ();
|
||
apiLevel = Build.VERSION.SDK_INT;
|
||
|
||
if (apiLevel >= Build.VERSION_CODES.GINGERBREAD)
|
||
return context.getApplicationInfo().nativeLibraryDir;
|
||
else if (apiLevel >= Build.VERSION_CODES.DONUT)
|
||
return context.getApplicationInfo().dataDir + "/lib";
|
||
|
||
return "/data/data/" + context.getPackageName() + "/lib";
|
||
}
|
||
|
||
@Override
|
||
public void
|
||
onCreate ()
|
||
{
|
||
AssetManager manager;
|
||
Context app_context;
|
||
String filesDir, libDir, cacheDir, classPath;
|
||
double pixelDensityX;
|
||
double pixelDensityY;
|
||
|
||
SERVICE = this;
|
||
handler = new Handler (Looper.getMainLooper ());
|
||
manager = getAssets ();
|
||
app_context = getApplicationContext ();
|
||
metrics = getResources ().getDisplayMetrics ();
|
||
pixelDensityX = metrics.xdpi;
|
||
pixelDensityY = metrics.ydpi;
|
||
|
||
try
|
||
{
|
||
/* Configure Emacs with the asset manager and other necessary
|
||
parameters. */
|
||
filesDir = app_context.getFilesDir ().getCanonicalPath ();
|
||
libDir = getLibraryDirectory ();
|
||
cacheDir = app_context.getCacheDir ().getCanonicalPath ();
|
||
|
||
/* Now provide this application's apk file, so a recursive
|
||
invocation of app_process (through android-emacs) can
|
||
find EmacsNoninteractive. */
|
||
classPath = getApkFile ();
|
||
|
||
Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir
|
||
+ ", libDir = " + libDir + ", and classPath = " + classPath);
|
||
|
||
EmacsNative.setEmacsParams (manager, filesDir, libDir,
|
||
cacheDir, (float) pixelDensityX,
|
||
(float) pixelDensityY,
|
||
classPath, this);
|
||
|
||
/* Start the thread that runs Emacs. */
|
||
thread = new EmacsThread (this, needDashQ);
|
||
thread.start ();
|
||
}
|
||
catch (IOException exception)
|
||
{
|
||
EmacsNative.emacsAbort ();
|
||
return;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/* Functions from here on must only be called from the Emacs
|
||
thread. */
|
||
|
||
public void
|
||
runOnUiThread (Runnable runnable)
|
||
{
|
||
handler.post (runnable);
|
||
}
|
||
|
||
public EmacsView
|
||
getEmacsView (final EmacsWindow window, final int visibility,
|
||
final boolean isFocusedByDefault)
|
||
{
|
||
Runnable runnable;
|
||
final Holder<EmacsView> view;
|
||
|
||
view = new Holder<EmacsView> ();
|
||
|
||
runnable = new Runnable () {
|
||
public void
|
||
run ()
|
||
{
|
||
synchronized (this)
|
||
{
|
||
view.thing = new EmacsView (window);
|
||
view.thing.setVisibility (visibility);
|
||
|
||
/* The following function is only present on Android 26
|
||
or later. */
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||
view.thing.setFocusedByDefault (isFocusedByDefault);
|
||
|
||
notify ();
|
||
}
|
||
}
|
||
};
|
||
|
||
syncRunnable (runnable);
|
||
return view.thing;
|
||
}
|
||
|
||
public void
|
||
getLocationOnScreen (final EmacsView view, final int[] coordinates)
|
||
{
|
||
Runnable runnable;
|
||
|
||
runnable = new Runnable () {
|
||
public void
|
||
run ()
|
||
{
|
||
synchronized (this)
|
||
{
|
||
view.getLocationOnScreen (coordinates);
|
||
notify ();
|
||
}
|
||
}
|
||
};
|
||
|
||
syncRunnable (runnable);
|
||
}
|
||
|
||
public void
|
||
fillRectangle (EmacsDrawable drawable, EmacsGC gc,
|
||
int x, int y, int width, int height)
|
||
{
|
||
EmacsFillRectangle.perform (drawable, gc, x, y,
|
||
width, height);
|
||
}
|
||
|
||
public void
|
||
fillPolygon (EmacsDrawable drawable, EmacsGC gc,
|
||
Point points[])
|
||
{
|
||
EmacsFillPolygon.perform (drawable, gc, points);
|
||
}
|
||
|
||
public void
|
||
drawRectangle (EmacsDrawable drawable, EmacsGC gc,
|
||
int x, int y, int width, int height)
|
||
{
|
||
EmacsDrawRectangle.perform (drawable, gc, x, y,
|
||
width, height);
|
||
}
|
||
|
||
public void
|
||
drawLine (EmacsDrawable drawable, EmacsGC gc,
|
||
int x, int y, int x2, int y2)
|
||
{
|
||
EmacsDrawLine.perform (drawable, gc, x, y,
|
||
x2, y2);
|
||
}
|
||
|
||
public void
|
||
drawPoint (EmacsDrawable drawable, EmacsGC gc,
|
||
int x, int y)
|
||
{
|
||
EmacsDrawPoint.perform (drawable, gc, x, y);
|
||
}
|
||
|
||
public void
|
||
copyArea (EmacsDrawable srcDrawable, EmacsDrawable dstDrawable,
|
||
EmacsGC gc,
|
||
int srcX, int srcY, int width, int height, int destX,
|
||
int destY)
|
||
{
|
||
EmacsCopyArea.perform (srcDrawable, gc, dstDrawable,
|
||
srcX, srcY, width, height, destX,
|
||
destY);
|
||
}
|
||
|
||
public void
|
||
clearWindow (EmacsWindow window)
|
||
{
|
||
window.clearWindow ();
|
||
}
|
||
|
||
public void
|
||
clearArea (EmacsWindow window, int x, int y, int width,
|
||
int height)
|
||
{
|
||
window.clearArea (x, y, width, height);
|
||
}
|
||
|
||
@SuppressWarnings ("deprecation")
|
||
public void
|
||
ringBell ()
|
||
{
|
||
Vibrator vibrator;
|
||
VibrationEffect effect;
|
||
VibratorManager vibratorManager;
|
||
Object tem;
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||
{
|
||
tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE);
|
||
vibratorManager = (VibratorManager) tem;
|
||
vibrator = vibratorManager.getDefaultVibrator ();
|
||
}
|
||
else
|
||
vibrator
|
||
= (Vibrator) getSystemService (Context.VIBRATOR_SERVICE);
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||
{
|
||
effect
|
||
= VibrationEffect.createOneShot (50,
|
||
VibrationEffect.DEFAULT_AMPLITUDE);
|
||
vibrator.vibrate (effect);
|
||
}
|
||
else
|
||
vibrator.vibrate (50);
|
||
}
|
||
|
||
public short[]
|
||
queryTree (EmacsWindow window)
|
||
{
|
||
short[] array;
|
||
List<EmacsWindow> windowList;
|
||
int i;
|
||
|
||
if (window == null)
|
||
/* Just return all the windows without a parent. */
|
||
windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows ();
|
||
else
|
||
windowList = window.children;
|
||
|
||
array = new short[windowList.size () + 1];
|
||
i = 1;
|
||
|
||
array[0] = (window == null
|
||
? 0 : (window.parent != null
|
||
? window.parent.handle : 0));
|
||
|
||
for (EmacsWindow treeWindow : windowList)
|
||
array[i++] = treeWindow.handle;
|
||
|
||
return array;
|
||
}
|
||
|
||
public int
|
||
getScreenWidth (boolean mmWise)
|
||
{
|
||
DisplayMetrics metrics;
|
||
|
||
metrics = getResources ().getDisplayMetrics ();
|
||
|
||
if (!mmWise)
|
||
return metrics.widthPixels;
|
||
else
|
||
return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0);
|
||
}
|
||
|
||
public int
|
||
getScreenHeight (boolean mmWise)
|
||
{
|
||
DisplayMetrics metrics;
|
||
|
||
metrics = getResources ().getDisplayMetrics ();
|
||
|
||
if (!mmWise)
|
||
return metrics.heightPixels;
|
||
else
|
||
return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0);
|
||
}
|
||
|
||
public boolean
|
||
detectMouse ()
|
||
{
|
||
InputManager manager;
|
||
InputDevice device;
|
||
int[] ids;
|
||
int i;
|
||
|
||
if (Build.VERSION.SDK_INT
|
||
< Build.VERSION_CODES.JELLY_BEAN)
|
||
return false;
|
||
|
||
manager = (InputManager) getSystemService (Context.INPUT_SERVICE);
|
||
ids = manager.getInputDeviceIds ();
|
||
|
||
for (i = 0; i < ids.length; ++i)
|
||
{
|
||
device = manager.getInputDevice (ids[i]);
|
||
|
||
if (device == null)
|
||
continue;
|
||
|
||
if (device.supportsSource (InputDevice.SOURCE_MOUSE))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public String
|
||
nameKeysym (int keysym)
|
||
{
|
||
return KeyEvent.keyCodeToString (keysym);
|
||
}
|
||
|
||
public void
|
||
sync ()
|
||
{
|
||
Runnable runnable;
|
||
|
||
runnable = new Runnable () {
|
||
public void
|
||
run ()
|
||
{
|
||
synchronized (this)
|
||
{
|
||
notify ();
|
||
}
|
||
}
|
||
};
|
||
|
||
syncRunnable (runnable);
|
||
}
|
||
|
||
|
||
|
||
/* 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)
|
||
{
|
||
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));
|
||
}
|
||
}
|
||
|
||
/* Ask the system to open the specified URL.
|
||
Value is NULL upon success, or a string describing the error
|
||
upon failure. */
|
||
|
||
public String
|
||
browseUrl (String url)
|
||
{
|
||
Intent intent;
|
||
|
||
try
|
||
{
|
||
intent = new Intent (Intent.ACTION_VIEW, Uri.parse (url));
|
||
intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK);
|
||
startActivity (intent);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
return e.toString ();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/* Get a SDK 11 ClipboardManager.
|
||
|
||
Android 4.0.x requires that this be called from the main
|
||
thread. */
|
||
|
||
public ClipboardManager
|
||
getClipboardManager ()
|
||
{
|
||
final Holder<ClipboardManager> manager;
|
||
Runnable runnable;
|
||
|
||
manager = new Holder<ClipboardManager> ();
|
||
|
||
runnable = new Runnable () {
|
||
public void
|
||
run ()
|
||
{
|
||
Object tem;
|
||
|
||
synchronized (this)
|
||
{
|
||
tem = getSystemService (Context.CLIPBOARD_SERVICE);
|
||
manager.thing = (ClipboardManager) tem;
|
||
notify ();
|
||
}
|
||
}
|
||
};
|
||
|
||
syncRunnable (runnable);
|
||
return manager.thing;
|
||
}
|
||
|
||
public void
|
||
restartEmacs ()
|
||
{
|
||
Intent intent;
|
||
|
||
intent = new Intent (this, EmacsActivity.class);
|
||
intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
|
||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||
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)
|
||
{
|
||
if (DEBUG_IC)
|
||
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)
|
||
{
|
||
if (DEBUG_IC)
|
||
Log.d (TAG, "resetIC: " + window);
|
||
|
||
window.view.setICMode (icMode);
|
||
window.view.imManager.restartInput (window.view);
|
||
}
|
||
};
|