Add emacsclient desktop file equivalent on Android
* doc/emacs/android.texi (Android File System): * java/AndroidManifest.xml.in: Update with new activity. Remove Android 10 restrictions through a special flag. * java/org/gnu/emacs/EmacsNative.java (getProcName): New function. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): New file. * java/org/gnu/emacs/EmacsService.java (getLibraryDirection): Remove unused annotation. * lib-src/emacsclient.c (decode_options): Set alt_display on Android. * src/android.c (android_proc_name): New function. (NATIVE_NAME): Export via JNI.
This commit is contained in:
parent
bfce0ce57f
commit
420533a8f9
7 changed files with 490 additions and 13 deletions
|
@ -167,8 +167,8 @@ system settings.
|
|||
The external storage directory is found at @file{/sdcard}; the other
|
||||
directories are not found at any fixed location.
|
||||
|
||||
@cindex file system limitations, Android 10
|
||||
On Android 10 and later, the Android system restricts applications
|
||||
@cindex file system limitations, Android 11
|
||||
On Android 11 and later, the Android system restricts applications
|
||||
from accessing files in the @file{/sdcard} directory using
|
||||
file-related system calls such as @code{open} and @code{readdir}.
|
||||
|
||||
|
@ -177,16 +177,8 @@ makes the system more secure. Unfortunately, it also means that Emacs
|
|||
cannot access files in those directories, despite holding the
|
||||
necessary permissions. Thankfully, the Open Handset Alliance's
|
||||
version of Android allows this restriction to be disabled on a
|
||||
per-program basis; on Android 10, the corresponding option in the
|
||||
system settings panel is:
|
||||
|
||||
@indentedblock
|
||||
System -> Developer Options -> App Compatibility Changes -> Emacs ->
|
||||
DEFAULT_SCOPED_STORAGE
|
||||
@end indentedblock
|
||||
|
||||
And on Android 11 and later, the corresponding option in the systems
|
||||
settings panel is:
|
||||
per-program basis; the corresponding option in the system settings
|
||||
panel is:
|
||||
|
||||
@indentedblock
|
||||
System -> Apps -> Special App Access -> All files access -> Emacs
|
||||
|
|
|
@ -24,6 +24,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
|
|||
package="org.gnu.emacs"
|
||||
android:targetSandboxVersion="1"
|
||||
android:installLocation="auto"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:versionCode="@emacs_major_version@"
|
||||
android:versionName="@version@">
|
||||
|
||||
|
@ -82,6 +83,84 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="org.gnu.emacs.EmacsOpenActivity"
|
||||
android:exported="true">
|
||||
|
||||
<!-- Allow Emacs to open all kinds of files known to Android. -->
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.EDIT"/>
|
||||
<action android:name="android.intent.action.PICK"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:mimeType="image/aces"/>
|
||||
<data android:mimeType="image/avci"/>
|
||||
<data android:mimeType="image/avcs"/>
|
||||
<data android:mimeType="image/avif"/>
|
||||
<data android:mimeType="image/bmp"/>
|
||||
<data android:mimeType="image/cgm"/>
|
||||
<data android:mimeType="image/dicom-rle"/>
|
||||
<data android:mimeType="image/dpx"/>
|
||||
<data android:mimeType="image/emf"/>
|
||||
<data android:mimeType="image/example"/>
|
||||
<data android:mimeType="image/fits"/>
|
||||
<data android:mimeType="image/g3fax"/>
|
||||
<data android:mimeType="image/heic"/>
|
||||
<data android:mimeType="image/heic-sequence"/>
|
||||
<data android:mimeType="image/heif"/>
|
||||
<data android:mimeType="image/heif-sequence"/>
|
||||
<data android:mimeType="image/hej2k"/>
|
||||
<data android:mimeType="image/hsj2"/>
|
||||
<data android:mimeType="image/jls"/>
|
||||
<data android:mimeType="image/jp2"/>
|
||||
<data android:mimeType="image/jph"/>
|
||||
<data android:mimeType="image/jphc"/>
|
||||
<data android:mimeType="image/jpm"/>
|
||||
<data android:mimeType="image/jpx"/>
|
||||
<data android:mimeType="image/jxr"/>
|
||||
<data android:mimeType="image/jxrA"/>
|
||||
<data android:mimeType="image/jxrS"/>
|
||||
<data android:mimeType="image/jxs"/>
|
||||
<data android:mimeType="image/jxsc"/>
|
||||
<data android:mimeType="image/jxsi"/>
|
||||
<data android:mimeType="image/jxss"/>
|
||||
<data android:mimeType="image/ktx"/>
|
||||
<data android:mimeType="image/ktx2"/>
|
||||
<data android:mimeType="image/naplps"/>
|
||||
<data android:mimeType="image/png"/>
|
||||
<data android:mimeType="image/prs.btif"/>
|
||||
<data android:mimeType="image/prs.pti"/>
|
||||
<data android:mimeType="image/pwg-raster"/>
|
||||
<data android:mimeType="image/svg+xml"/>
|
||||
<data android:mimeType="image/t38"/>
|
||||
<data android:mimeType="image/tiff"/>
|
||||
<data android:mimeType="image/tiff-fx"/>
|
||||
<data android:mimeType="text/*"/>
|
||||
<data android:mimeType="application/*xml"/>
|
||||
<data android:mimeType="application/atom+xml"/>
|
||||
<data android:mimeType="application/dxf"/>
|
||||
<data android:mimeType="application/ecmascript"/>
|
||||
<data android:mimeType="application/javascript"/>
|
||||
<data android:mimeType="application/json"/>
|
||||
<data android:mimeType="application/*log*"/>
|
||||
<data android:mimeType="application/octet-stream"/>
|
||||
<data android:mimeType="application/soap+xm"/>
|
||||
<data android:mimeType="application/x-caramel"/>
|
||||
<data android:mimeType="application/x-klaunch"/>
|
||||
<data android:mimeType="application/x-latex"/>
|
||||
<data android:mimeType="application/x-sh"/>
|
||||
<data android:mimeType="application/x-tcl"/>
|
||||
<data android:mimeType="application/x-tex*"/>
|
||||
<data android:mimeType="application/x-troff*"/>
|
||||
<data android:mimeType="application/xhtml+xml"/>
|
||||
<data android:mimeType="application/xml*"/>
|
||||
<data android:mimeType="application/zip"/>
|
||||
<data android:mimeType="application/x-zip-compressed"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="org.gnu.emacs.EmacsMultitaskActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true"
|
||||
|
|
|
@ -153,6 +153,10 @@ public static native long sendWheel (short window, int x, int y,
|
|||
public static native long sendExpose (short window, int x, int y,
|
||||
int width, int height);
|
||||
|
||||
/* Return the file name associated with the specified file
|
||||
descriptor, or NULL if there is none. */
|
||||
public static native byte[] getProcName (int fd);
|
||||
|
||||
static
|
||||
{
|
||||
System.loadLibrary ("emacs");
|
||||
|
|
357
java/org/gnu/emacs/EmacsOpenActivity.java
Normal file
357
java/org/gnu/emacs/EmacsOpenActivity.java
Normal file
|
@ -0,0 +1,357 @@
|
|||
/* 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;
|
||||
|
||||
/* This class makes the Emacs server work reasonably on Android.
|
||||
|
||||
There is no way to make the Unix socket publicly available on
|
||||
Android.
|
||||
|
||||
Instead, this activity tries to connect to the Emacs server, to
|
||||
make it open files the system asks Emacs to open, and to emulate
|
||||
some reasonable behavior when Emacs has not yet started.
|
||||
|
||||
First, Emacs registers itself as an application that can open text
|
||||
and image files.
|
||||
|
||||
Then, when the user is asked to open a file and selects ``Emacs''
|
||||
as the application that will open the file, the system pops up a
|
||||
window, this activity, and calls the `onCreate' function.
|
||||
|
||||
`onCreate' then tries very to find the file name of the file that
|
||||
was selected, and give it to emacsclient.
|
||||
|
||||
If emacsclient successfully opens the file, then this activity
|
||||
starts EmacsActivity (to bring it on to the screen); otherwise, it
|
||||
displays the output of emacsclient or any error message that occurs
|
||||
and exits. */
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class EmacsOpenActivity extends Activity
|
||||
implements DialogInterface.OnClickListener
|
||||
{
|
||||
private class EmacsClientThread extends Thread
|
||||
{
|
||||
private ProcessBuilder builder;
|
||||
|
||||
public
|
||||
EmacsClientThread (ProcessBuilder processBuilder)
|
||||
{
|
||||
builder = processBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void
|
||||
run ()
|
||||
{
|
||||
Process process;
|
||||
InputStream error;
|
||||
String errorText;
|
||||
|
||||
try
|
||||
{
|
||||
/* Start emacsclient. */
|
||||
process = builder.start ();
|
||||
process.waitFor ();
|
||||
|
||||
/* Now figure out whether or not starting the process was
|
||||
successful. */
|
||||
if (process.exitValue () == 0)
|
||||
finishSuccess ();
|
||||
else
|
||||
finishFailure ("Error opening file", null);
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
finishFailure ("Internal error", exception.toString ());
|
||||
}
|
||||
catch (InterruptedException exception)
|
||||
{
|
||||
finishFailure ("Internal error", exception.toString ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void
|
||||
onClick (DialogInterface dialog, int which)
|
||||
{
|
||||
finish ();
|
||||
}
|
||||
|
||||
public String
|
||||
readEmacsClientLog ()
|
||||
{
|
||||
File file, cache;
|
||||
FileReader reader;
|
||||
char[] buffer;
|
||||
int rc;
|
||||
String what;
|
||||
|
||||
cache = getCacheDir ();
|
||||
file = new File (cache, "emacsclient.log");
|
||||
what = "";
|
||||
|
||||
try
|
||||
{
|
||||
reader = new FileReader (file);
|
||||
buffer = new char[2048];
|
||||
|
||||
while ((rc = reader.read (buffer, 0, 2048)) != -1)
|
||||
what += String.valueOf (buffer, 0, 2048);
|
||||
|
||||
reader.close ();
|
||||
return what;
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
return ("Couldn't read emacsclient.log: "
|
||||
+ exception.toString ());
|
||||
}
|
||||
}
|
||||
|
||||
private void
|
||||
displayFailureDialog (String title, String text)
|
||||
{
|
||||
AlertDialog.Builder builder;
|
||||
AlertDialog dialog;
|
||||
|
||||
builder = new AlertDialog.Builder (this);
|
||||
dialog = builder.create ();
|
||||
dialog.setTitle (title);
|
||||
|
||||
if (text == null)
|
||||
/* Read in emacsclient.log instead. */
|
||||
text = readEmacsClientLog ();
|
||||
|
||||
dialog.setMessage (text);
|
||||
dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this);
|
||||
dialog.show ();
|
||||
}
|
||||
|
||||
/* Finish this activity in response to emacsclient having
|
||||
successfully opened a file.
|
||||
|
||||
In the main thread, close this window, and open a window
|
||||
belonging to an Emacs frame. */
|
||||
|
||||
public void
|
||||
finishSuccess ()
|
||||
{
|
||||
runOnUiThread (new Runnable () {
|
||||
@Override
|
||||
public void
|
||||
run ()
|
||||
{
|
||||
Intent intent;
|
||||
|
||||
intent = new Intent (EmacsOpenActivity.this,
|
||||
EmacsActivity.class);
|
||||
intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity (intent);
|
||||
|
||||
EmacsOpenActivity.this.finish ();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Finish this activity after displaying a dialog associated with
|
||||
failure to open a file.
|
||||
|
||||
Use TITLE as the title of the dialog. If TEXT is non-NULL,
|
||||
display that text in the dialog. Otherwise, use the contents of
|
||||
emacsclient.log in the cache directory instead. */
|
||||
|
||||
public void
|
||||
finishFailure (final String title, final String text)
|
||||
{
|
||||
runOnUiThread (new Runnable () {
|
||||
@Override
|
||||
public void
|
||||
run ()
|
||||
{
|
||||
displayFailureDialog (title, text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public 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";
|
||||
}
|
||||
|
||||
public void
|
||||
startEmacsClient (String fileName)
|
||||
{
|
||||
String libDir;
|
||||
ProcessBuilder builder;
|
||||
Process process;
|
||||
EmacsClientThread thread;
|
||||
File file;
|
||||
|
||||
file = new File (getCacheDir (), "emacsclient.log");
|
||||
|
||||
libDir = getLibraryDirectory ();
|
||||
builder = new ProcessBuilder (libDir + "/libemacsclient.so",
|
||||
fileName, "--reuse-frame",
|
||||
"--timeout=10", "--no-wait");
|
||||
|
||||
/* Redirect standard error to a file so that errors can be
|
||||
meaningfully reported. */
|
||||
|
||||
if (file.exists ())
|
||||
file.delete ();
|
||||
|
||||
builder.redirectError (file);
|
||||
|
||||
/* Track process output in a new thread, since this is the UI
|
||||
thread and doing so here can cause deadlocks when EmacsService
|
||||
decides to wait for something. */
|
||||
|
||||
thread = new EmacsClientThread (builder);
|
||||
thread.start ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void
|
||||
onCreate (Bundle savedInstanceState)
|
||||
{
|
||||
String action, fileName;
|
||||
Intent intent;
|
||||
Uri uri;
|
||||
ContentResolver resolver;
|
||||
ParcelFileDescriptor fd;
|
||||
byte[] names;
|
||||
String errorBlurb;
|
||||
|
||||
super.onCreate (savedInstanceState);
|
||||
|
||||
/* Obtain the intent that started Emacs. */
|
||||
intent = getIntent ();
|
||||
action = intent.getAction ();
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
finish ();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Now see if the action specified is supported by Emacs. */
|
||||
|
||||
if (action.equals ("android.intent.action.VIEW")
|
||||
|| action.equals ("android.intent.action.EDIT")
|
||||
|| action.equals ("android.intent.action.PICK"))
|
||||
{
|
||||
/* Obtain the URI of the action. */
|
||||
uri = intent.getData ();
|
||||
|
||||
if (uri == null)
|
||||
{
|
||||
finish ();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Now, try to get the file name. */
|
||||
|
||||
if (uri.getScheme ().equals ("file"))
|
||||
fileName = uri.getPath ();
|
||||
else
|
||||
{
|
||||
fileName = null;
|
||||
|
||||
if (uri.getScheme ().equals ("content"))
|
||||
{
|
||||
/* This is one of the annoying Android ``content''
|
||||
URIs. Most of the time, there is actually an
|
||||
underlying file, but it cannot be found without
|
||||
opening the file and doing readlink on its file
|
||||
descriptor in /proc/self/fd. */
|
||||
resolver = getContentResolver ();
|
||||
|
||||
try
|
||||
{
|
||||
fd = resolver.openFileDescriptor (uri, "r");
|
||||
names = EmacsNative.getProcName (fd.getFd ());
|
||||
fd.close ();
|
||||
|
||||
/* What is the right encoding here? */
|
||||
|
||||
if (names != null)
|
||||
fileName = new String (names, "UTF-8");
|
||||
}
|
||||
catch (FileNotFoundException exception)
|
||||
{
|
||||
/* Do nothing. */
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
/* Do nothing. */
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName == null)
|
||||
{
|
||||
errorBlurb = ("The URI: " + uri + " could not be opened"
|
||||
+ ", as it does not encode file name inform"
|
||||
+ "ation.");
|
||||
displayFailureDialog ("Error opening file", errorBlurb);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* And start emacsclient. */
|
||||
startEmacsClient (fileName);
|
||||
}
|
||||
else
|
||||
finish ();
|
||||
}
|
||||
}
|
|
@ -152,7 +152,6 @@ public class EmacsService extends Service
|
|||
}
|
||||
}
|
||||
|
||||
@TargetApi (Build.VERSION_CODES.GINGERBREAD)
|
||||
private String
|
||||
getLibraryDirectory ()
|
||||
{
|
||||
|
|
|
@ -626,6 +626,8 @@ decode_options (int argc, char **argv)
|
|||
alt_display = "w32";
|
||||
#elif defined (HAVE_HAIKU)
|
||||
alt_display = "be";
|
||||
#elif defined (HAVE_ANDROID)
|
||||
alt_display = "android";
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_PGTK
|
||||
|
|
|
@ -1369,6 +1369,27 @@ android_get_home_directory (void)
|
|||
return android_files_dir;
|
||||
}
|
||||
|
||||
/* Return the name of the file behind a file descriptor FD by reading
|
||||
/proc/self/fd/. Place the name in BUFFER, which should be able to
|
||||
hold size bytes. Value is 0 upon success, and 1 upon failure. */
|
||||
|
||||
static int
|
||||
android_proc_name (int fd, char *buffer, size_t size)
|
||||
{
|
||||
char format[sizeof "/proc/self/fd/"
|
||||
+ INT_STRLEN_BOUND (int)];
|
||||
ssize_t read;
|
||||
|
||||
sprintf (format, "/proc/self/fd/%d", fd);
|
||||
read = readlink (format, buffer, size - 1);
|
||||
|
||||
if (read == -1)
|
||||
return 1;
|
||||
|
||||
buffer[read] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* JNI functions called by Java. */
|
||||
|
@ -1598,6 +1619,29 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object,
|
|||
now. */
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
NATIVE_NAME (getProcName) (JNIEnv *env, jobject object, jint fd)
|
||||
{
|
||||
char buffer[PATH_MAX + 1];
|
||||
size_t length;
|
||||
jbyteArray array;
|
||||
|
||||
if (android_proc_name (fd, buffer, PATH_MAX + 1))
|
||||
return NULL;
|
||||
|
||||
/* Return a byte array, as Java strings cannot always encode file
|
||||
names. */
|
||||
length = strlen (buffer);
|
||||
array = (*env)->NewByteArray (env, length);
|
||||
if (!array)
|
||||
return NULL;
|
||||
|
||||
(*env)->SetByteArrayRegion (env, array, 0, length,
|
||||
(jbyte *) buffer);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/* Initialize service_class, aborting if something goes wrong. */
|
||||
|
||||
static void
|
||||
|
|
Loading…
Add table
Reference in a new issue