/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
Copyright (C) 2023-2025 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 java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
import android.database.Cursor;
import android.graphics.Matrix;
import android.graphics.Point;
import android.webkit.MimeTypeMap;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.ExtractedText;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.UriPermission;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.util.Log;
import android.util.DisplayMetrics;
import android.widget.Toast;
/* EmacsService is the service that starts the thread running Emacs
and handles requests by that Emacs instance. */
public final class EmacsService extends Service
{
public static final String TAG = "EmacsService";
/* The started Emacs service object. */
public static EmacsService SERVICE;
/* If non-NULL, an array of extra arguments to pass to
`android_emacs_init'. */
public static String[] extraStartupArguments;
/* The thread running Emacs C code. */
private EmacsThread thread;
/* Handler used to run tasks on the main thread. */
private Handler handler;
/* Content resolver used to access URIs. */
private ContentResolver resolver;
/* 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;
public static final int IC_MODE_PASSWORD = 3;
/* Display metrics used by font backends. */
public DisplayMetrics metrics;
/* Flag that says whether or not to print verbose debugging
information when responding to an input method. */
public static final boolean DEBUG_IC = false;
/* Flag that says whether or not to stringently check that only the
Emacs thread is performing drawing calls. */
private static final boolean DEBUG_THREADS = false;
/* Atomic integer used for synchronization between
icBeginSynchronous/icEndSynchronous and viewGetSelection.
Value is 0 if no query is in progress, 1 if viewGetSelection is
being called, and 2 if icBeginSynchronous was called. */
public static final AtomicInteger servicingQuery;
/* Thread used to query document providers, or null if it hasn't
been created yet. */
private EmacsSafThread storageThread;
/* The Thread object representing the Android user interface
thread. */
private Thread mainThread;
/* "Resources" object required by GContext bookkeeping. */
public static Resources resources;
static
{
servicingQuery = new AtomicInteger ();
};
/* Return the directory leading to the directory in which native
library files are stored on behalf of CONTEXT. */
public static String
getLibraryDirectory (Context context)
{
int apiLevel;
apiLevel = Build.VERSION.SDK_INT;
if (apiLevel >= Build.VERSION_CODES.GINGERBREAD)
return context.getApplicationInfo ().nativeLibraryDir;
return context.getApplicationInfo ().dataDir + "/lib";
}
@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 = ("This notification is displayed to keep Emacs"
+ " running while it is in the background. You"
+ " may disable it if you wish;"
+ " see (emacs)Android Environment.");
channel
= new NotificationChannel ("emacs", "Emacs Background Service",
NotificationManager.IMPORTANCE_LOW);
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;
}
/* Return the display density, adjusted in accord with the user's
text scaling preferences. */
@SuppressWarnings ("deprecation")
private static float
getScaledDensity (DisplayMetrics metrics)
{
/* The scaled density has been made obsolete by the introduction
of non-linear text scaling in Android 34, where there is no
longer a fixed relation between point and pixel sizes, but
remains useful, considering that Emacs does not support
non-linear text scaling. */
return metrics.scaledDensity;
}
@Override
public void
onCreate ()
{
final AssetManager manager;
Context app_context;
final String filesDir, libDir, cacheDir, classPath;
final double pixelDensityX;
final double pixelDensityY;
final double scaledDensity;
double tempScaledDensity;
super.onCreate ();
SERVICE = this;
resources = getResources ();
handler = new Handler (Looper.getMainLooper ());
manager = getAssets ();
app_context = getApplicationContext ();
metrics = resources.getDisplayMetrics ();
pixelDensityX = metrics.xdpi;
pixelDensityY = metrics.ydpi;
tempScaledDensity = ((getScaledDensity (metrics)
/ metrics.density)
* pixelDensityX);
resolver = getContentResolver ();
mainThread = Thread.currentThread ();
/* If the density used to compute the text size is smaller than 160,
there's likely a bug with display density computation. Reset it
to 160 in that case.
Note that Android uses 160 ``dpi'' as the density where 1 point
corresponds to 1 pixel, not 72 or 96 as used elsewhere. This
difference is codified in PT_PER_INCH defined in font.h. */
if (tempScaledDensity < 160)
tempScaledDensity = 160;
/* scaledDensity is const as required to refer to it from within
the nested function below. */
scaledDensity = tempScaledDensity;
/* Remove all tasks from previous Emacs sessions but the task
created by the system at startup. */
EmacsWindowManager.MANAGER.removeOldTasks (this);
try
{
/* Configure Emacs with the asset manager and other necessary
parameters. */
filesDir = app_context.getFilesDir ().getCanonicalPath ();
libDir = getLibraryDirectory (this);
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 = EmacsApplication.apkFileName;
Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir
+ ", libDir = " + libDir + ", and classPath = " + classPath
+ "; args = " + (extraStartupArguments != null
? Arrays.toString (extraStartupArguments)
: "(none)")
+ "; display density: " + pixelDensityX + " by "
+ pixelDensityY + " scaled to " + scaledDensity);
/* Start the thread that runs Emacs. */
thread = new EmacsThread (this, new Runnable () {
@Override
public void
run ()
{
EmacsNative.setEmacsParams (manager, filesDir, libDir,
cacheDir, (float) pixelDensityX,
(float) pixelDensityY,
(float) scaledDensity,
classPath, EmacsService.this,
Build.VERSION.SDK_INT);
}
}, extraStartupArguments);
thread.start ();
}
catch (IOException exception)
{
EmacsNative.emacsAbort ();
return;
}
}
/* The native functions the subsequent two functions call do nothing
in the infrequent case the Emacs thread is awaiting a response
for the main thread. Caveat emptor! */
@Override
public void
onDestroy ()
{
/* This function is called immediately before the system kills
Emacs. In this respect, it is rather akin to a SIGDANGER
signal, so force an auto-save accordingly. */
EmacsNative.shutDownEmacs ();
super.onDestroy ();
}
@Override
public void
onLowMemory ()
{
EmacsNative.onLowMemory ();
super.onLowMemory ();
}
/* 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;
FutureTask task;
task = new FutureTask (new Callable () {
@Override
public EmacsView
call ()
{
EmacsView view;
view = new EmacsView (window);
view.setVisibility (visibility);
/* The following function is only present on Android 26
or later. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
view.setFocusedByDefault (isFocusedByDefault);
return view;
}
});
return EmacsService.syncRunnable (task);
}
public void
getLocationOnScreen (final EmacsView view, final int[] coordinates)
{
FutureTask task;
task = new FutureTask (new Callable () {
public Void
call ()
{
view.getLocationOnScreen (coordinates);
return null;
}
});
EmacsService.syncRunnable (task);
}
public void
getLocationInWindow (final EmacsView view, final int[] coordinates)
{
FutureTask task;
task = new FutureTask (new Callable () {
public Void
call ()
{
view.getLocationInWindow (coordinates);
return null;
}
});
EmacsService.syncRunnable (task);
}
public static void
checkEmacsThread ()
{
if (DEBUG_THREADS)
{
/* When SERVICE is NULL, Emacs is being executed non-interactively. */
if (SERVICE == null
/* It was previously assumed that only instances of
`EmacsThread' were valid for graphics calls, but this is
no longer true now that Lisp threads can be attached to
the JVM. */
|| (Thread.currentThread () != SERVICE.mainThread))
return;
throw new RuntimeException ("Emacs thread function"
+ " called from other thread!");
}
}
/* These drawing functions must only be called from the Emacs
thread. */
public void
fillRectangle (EmacsDrawable drawable, EmacsGC gc,
int x, int y, int width, int height)
{
checkEmacsThread ();
EmacsFillRectangle.perform (drawable, gc, x, y,
width, height);
}
public void
fillPolygon (EmacsDrawable drawable, EmacsGC gc,
Point points[])
{
checkEmacsThread ();
EmacsFillPolygon.perform (drawable, gc, points);
}
public void
drawRectangle (EmacsDrawable drawable, EmacsGC gc,
int x, int y, int width, int height)
{
checkEmacsThread ();
EmacsDrawRectangle.perform (drawable, gc, x, y,
width, height);
}
public void
drawLine (EmacsDrawable drawable, EmacsGC gc,
int x, int y, int x2, int y2)
{
checkEmacsThread ();
EmacsDrawLine.perform (drawable, gc, x, y,
x2, y2);
}
public void
drawPoint (EmacsDrawable drawable, EmacsGC gc,
int x, int y)
{
checkEmacsThread ();
EmacsDrawPoint.perform (drawable, gc, x, y);
}
@SuppressWarnings ("deprecation")
public void
ringBell (int duration)
{
Vibrator vibrator;
VibrationEffect effect;
VibratorManager vibratorManager;
Object tem;
int amplitude;
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)
{
amplitude = VibrationEffect.DEFAULT_AMPLITUDE;
effect
= VibrationEffect.createOneShot (duration, amplitude);
vibrator.vibrate (effect);
}
else
vibrator.vibrate (duration);
}
public long[]
queryTree (EmacsWindow window)
{
long[] array;
List windowList;
int i;
if (window == null)
/* Just return all the windows without a parent. */
windowList = EmacsWindowManager.MANAGER.copyWindows ();
else
windowList = window.children;
synchronized (windowList)
{
array = new long[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
/* Android 4.0 and earlier don't support mouse input events at
all. */
< 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
if (device.supportsSource (InputDevice.SOURCE_MOUSE))
return true;
}
else
{
/* `supportsSource' is only present on API level 21 and
later, but earlier versions provide a bit mask
containing each supported source. */
if ((device.getSources () & InputDevice.SOURCE_MOUSE) != 0)
return true;
}
}
return false;
}
public boolean
detectKeyboard ()
{
Configuration configuration;
configuration = getResources ().getConfiguration ();
return configuration.keyboard != Configuration.KEYBOARD_NOKEYS;
}
public String
nameKeysym (int keysym)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
return KeyEvent.keyCodeToString (keysym);
return String.valueOf (keysym);
}
/* 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 permanent notification and start Emacs as a
foreground service. */
context.startForegroundService (new Intent (context,
EmacsService.class));
}
}
/* Ask the system to open the specified URL in an application that
understands how to open it.
If SEND, tell the system to also open applications that can
``send'' the URL (through mail, for example), instead of only
those that can view the URL.
Value is NULL upon success, or a string describing the error
upon failure. */
public String
browseUrl (String url, boolean send)
{
Intent intent;
Uri uri;
try
{
/* Parse the URI. */
if (!send)
{
uri = Uri.parse (url);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
/* On Android 4.4 and later, check if URI is actually
a file name. If so, rewrite it into a content
provider URI, so that it can be accessed by other
programs. */
if (uri.getScheme ().equals ("file")
&& uri.getPath () != null)
uri
= DocumentsContract.buildDocumentUri ("org.gnu.emacs",
uri.getPath ());
}
intent = new Intent (Intent.ACTION_VIEW, uri);
/* Set several flags on the Intent prompting the system to
permit the recipient to read and edit the URI
indefinitely. */
intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
intent.addFlags (Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
else
{
intent = new Intent (Intent.ACTION_SEND);
intent.setType ("text/plain");
intent.putExtra (Intent.EXTRA_SUBJECT, "Sharing link");
intent.putExtra (Intent.EXTRA_TEXT, url);
/* Display a list of programs able to send this URL. */
intent = Intent.createChooser (intent, "Send");
/* Apparently flags need to be set after a chooser is
created. */
intent.addFlags (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 ()
{
FutureTask