Update Android port

* doc/emacs/android.texi (Android Startup): Document `content'
special directory.
* java/debug.sh (is_root): Improve /bin/tee detection.
* java/org/gnu/emacs/EmacsNative.java (EmacsNative): New
function `dup'.
* java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity)
(checkReadableOrCopy, onCreate): Create content directory names
when the file is not readable.
* java/org/gnu/emacs/EmacsService.java (EmacsService)
(openContentUri, checkContentUri): New functions.
* src/android.c (struct android_emacs_service): New methods.
(android_content_name_p, android_get_content_name)
(android_check_content_access): New function.
(android_fstatat, android_open): Implement opening content URIs.
(dup): Export to Java.
(android_init_emacs_service): Initialize new methods.
(android_faccessat): Implement content file names.
This commit is contained in:
Po Lu 2023-02-21 15:28:06 +08:00
parent d197d73491
commit 7aa4ffddd8
6 changed files with 379 additions and 13 deletions

View file

@ -129,9 +129,15 @@ file, it invokes @command{emacsclient} with the options
and the name of the file being opened. Then, upon success, the focus
is transferred to any open Emacs frame.
It is sadly impossible to open certain kinds of files which are
provided by a ``content provider''. When that is the case, a dialog
is displayed with an explanation of the error.
@cindex /content directory, android
Some files are given to Emacs as ``content identifiers'', which the
system provides access to outside the normal filesystem APIs. Emacs
internally supports a temporary @file{/content} directory which is
used to access those files. Do not make any assumptions about the
contents of this directory, or try to open files in it yourself.
This feature is not provided on Android 4.3 and earlier, in which
case the file is copied to a temporary directory instead.
@node Android File System
@section What files Emacs can access under Android

View file

@ -281,7 +281,7 @@ else
# Upload the specified gdbserver binary to the device.
adb -s $device push "$gdbserver" "$gdbserver_bin"
if (adb -s $device pull /system/bin/tee /dev/null &> /dev/null); then
if adb -s $device shell ls /system/bin/tee; then
# Copy it to the user directory.
adb -s $device shell "$gdbserver_cat"
adb -s $device shell "run-as $package chmod +x gdbserver"

View file

@ -31,6 +31,10 @@ public class EmacsNative
initialization. */
private static final String[] libraryDeps;
/* Like `dup' in C. */
public static native int dup (int fd);
/* Obtain the fingerprint of this build of Emacs. The fingerprint
can be used to determine the dump file name. */
public static native String getFingerprint ();

View file

@ -57,6 +57,8 @@
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@ -69,6 +71,8 @@
public class EmacsOpenActivity extends Activity
implements DialogInterface.OnClickListener
{
private static final String TAG = "EmacsOpenActivity";
private class EmacsClientThread extends Thread
{
private ProcessBuilder builder;
@ -178,12 +182,16 @@ private class EmacsClientThread extends Thread
dialog.show ();
}
/* Check that the specified FILE is readable. If it is not, then
copy the file in FD to a location in the system cache
directory and return the name of that file. */
/* Check that the specified FILE is readable. If Android 4.4 or
later is being used, return URI formatted into a `/content/' file
name.
If it is not, then copy the file in FD to a location in the
system cache directory and return the name of that file. */
private String
checkReadableOrCopy (String file, ParcelFileDescriptor fd)
checkReadableOrCopy (String file, ParcelFileDescriptor fd,
Uri uri)
throws IOException, FileNotFoundException
{
File inFile;
@ -191,12 +199,34 @@ private class EmacsClientThread extends Thread
InputStream stream;
byte buffer[];
int read;
String content;
Log.d (TAG, "checkReadableOrCopy: " + file);
inFile = new File (file);
if (inFile.setReadable (true))
if (inFile.canRead ())
return file;
Log.d (TAG, "checkReadableOrCopy: NO");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
content = "/content/" + uri.getEncodedAuthority ();
for (String segment : uri.getPathSegments ())
content += "/" + Uri.encode (segment);
/* Append the URI query. */
if (uri.getEncodedQuery () != null)
content += "?" + uri.getEncodedQuery ();
Log.d (TAG, "checkReadableOrCopy: " + content);
return content;
}
/* inFile is now the file being written to. */
inFile = new File (getCacheDir (), inFile.getName ());
buffer = new byte[4098];
@ -398,7 +428,7 @@ else if (apiLevel >= Build.VERSION_CODES.DONUT)
if (names != null)
fileName = new String (names, "UTF-8");
fileName = checkReadableOrCopy (fileName, fd);
fileName = checkReadableOrCopy (fileName, fd, uri);
}
catch (FileNotFoundException exception)
{

View file

@ -19,7 +19,10 @@
package org.gnu.emacs;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.ArrayList;
@ -41,22 +44,31 @@
import android.content.ClipboardManager;
import android.content.Context;
import android.content.ContentResolver;
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.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.os.IBinder;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.Vibrator;
import android.os.VibratorManager;
import android.os.VibrationEffect;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import android.util.DisplayMetrics;
@ -79,6 +91,7 @@ public class EmacsService extends Service
private EmacsThread thread;
private Handler handler;
private ContentResolver resolver;
/* Keep this in synch with androidgui.h. */
public static final int IC_MODE_NULL = 0;
@ -193,6 +206,7 @@ else if (apiLevel >= Build.VERSION_CODES.DONUT)
metrics = getResources ().getDisplayMetrics ();
pixelDensityX = metrics.xdpi;
pixelDensityY = metrics.ydpi;
resolver = getContentResolver ();
try
{
@ -643,4 +657,109 @@ invocation of app_process (through android-emacs) can
window.view.setICMode (icMode);
window.view.imManager.restartInput (window.view);
}
/* Open a content URI described by the bytes BYTES, a non-terminated
string; make it writable if WRITABLE, and readable if READABLE.
Truncate the file if TRUNCATE.
Value is the resulting file descriptor or -1 upon failure. */
public int
openContentUri (byte[] bytes, boolean writable, boolean readable,
boolean truncate)
{
String name, mode;
ParcelFileDescriptor fd;
int i;
/* Figure out the file access mode. */
mode = "";
if (readable)
mode += "r";
if (writable)
mode += "w";
if (truncate)
mode += "t";
/* Try to open an associated ParcelFileDescriptor. */
try
{
/* The usual file name encoding question rears its ugly head
again. */
name = new String (bytes, "UTF-8");
Log.d (TAG, "openContentUri: " + Uri.parse (name));
fd = resolver.openFileDescriptor (Uri.parse (name), mode);
/* Use detachFd on newer versions of Android or plain old
dup. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
{
i = fd.detachFd ();
fd.close ();
return i;
}
else
{
i = EmacsNative.dup (fd.getFd ());
fd.close ();
return i;
}
}
catch (Exception exception)
{
return -1;
}
}
public boolean
checkContentUri (byte[] string, boolean readable, boolean writable)
{
String mode, name;
ParcelFileDescriptor fd;
/* Decode this into a URI. */
try
{
/* The usual file name encoding question rears its ugly head
again. */
name = new String (string, "UTF-8");
Log.d (TAG, "checkContentUri: " + Uri.parse (name));
}
catch (UnsupportedEncodingException exception)
{
name = null;
throw new RuntimeException (exception);
}
mode = "r";
if (writable)
mode += "w";
try
{
fd = resolver.openFileDescriptor (Uri.parse (name), mode);
fd.close ();
Log.d (TAG, "checkContentUri: YES");
return true;
}
catch (Exception exception)
{
Log.d (TAG, "checkContentUri: NO");
Log.d (TAG, exception.toString ());
return false;
}
}
};

View file

@ -108,6 +108,8 @@ struct android_emacs_service
jmethodID restart_emacs;
jmethodID update_ic;
jmethodID reset_ic;
jmethodID open_content_uri;
jmethodID check_content_uri;
};
struct android_emacs_pixmap
@ -941,6 +943,102 @@ android_get_asset_name (const char *filename)
return NULL;
}
/* Return whether or not the specified FILENAME actually resolves to a
content resolver URI. */
static bool
android_content_name_p (const char *filename)
{
return (!strcmp (filename, "/content")
|| !strncmp (filename, "/content/",
sizeof "/content/" - 1));
}
/* Return the content URI corresponding to a `/content' file name,
or NULL if it is not a content URI.
This function is not reentrant. */
static const char *
android_get_content_name (const char *filename)
{
static char buffer[PATH_MAX + 1];
char *head, *token, *saveptr, *copy;
size_t n;
n = PATH_MAX;
/* First handle content ``URIs'' without a provider. */
if (!strcmp (filename, "/content")
|| !strcmp (filename, "/content/"))
return "content://";
/* Next handle ordinary file names. */
if (strncmp (filename, "/content/", sizeof "/content/" - 1))
return NULL;
/* Forward past the first directory specifying the schema. */
copy = xstrdup (filename + sizeof "/content");
token = saveptr = NULL;
head = stpcpy (buffer, "content:/");
/* Split FILENAME by slashes. */
while ((token = strtok_r (!token ? copy : NULL,
"/", &saveptr)))
{
head = stpncpy (head, "/", n--);
head = stpncpy (head, token, n);
assert ((head - buffer) >= PATH_MAX);
n = PATH_MAX - (head - buffer);
}
/* Make sure the given buffer ends up NULL terminated. */
buffer[PATH_MAX] = '\0';
xfree (copy);
return buffer;
}
/* Return whether or not the specified FILENAME is an accessible
content URI. MODE specifies what to check. */
static bool
android_check_content_access (const char *filename, int mode)
{
const char *name;
jobject string;
size_t length;
jboolean rc;
name = android_get_content_name (filename);
length = strlen (name);
string = (*android_java_env)->NewByteArray (android_java_env,
length);
android_exception_check ();
(*android_java_env)->SetByteArrayRegion (android_java_env,
string, 0, length,
(jbyte *) name);
rc = (*android_java_env)->CallBooleanMethod (android_java_env,
emacs_service,
service_class.check_content_uri,
string,
(jboolean) ((mode & R_OK)
!= 0),
(jboolean) ((mode & W_OK)
!= 0));
android_exception_check ();
ANDROID_DELETE_LOCAL_REF (string);
return rc;
}
/* Like fstat. However, look up the asset corresponding to the file
descriptor. If it exists, return the right information. */
@ -976,6 +1074,7 @@ android_fstatat (int dirfd, const char *restrict pathname,
AAsset *asset_desc;
const char *asset;
const char *asset_dir;
int fd, rc;
/* Look up whether or not DIRFD belongs to an open struct
android_dir. */
@ -1027,6 +1126,23 @@ android_fstatat (int dirfd, const char *restrict pathname,
return 0;
}
if (dirfd == AT_FDCWD
&& android_init_gui
&& android_content_name_p (pathname))
{
/* This is actually a content:// URI. Open that file and call
stat on it. */
fd = android_open (pathname, O_RDONLY, 0);
if (fd < 0)
return -1;
rc = fstat (fd, statbuf);
android_close (fd);
return rc;
}
return fstatat (dirfd, pathname, statbuf, flags);
}
@ -1316,6 +1432,8 @@ android_open (const char *filename, int oflag, int mode)
AAsset *asset;
int fd;
off_t out_start, out_length;
size_t length;
jobject string;
if (asset_manager && (name = android_get_asset_name (filename)))
{
@ -1329,7 +1447,7 @@ android_open (const char *filename, int oflag, int mode)
if (oflag & O_DIRECTORY)
{
errno = EINVAL;
errno = ENOTSUP;
return -1;
}
@ -1396,7 +1514,7 @@ android_open (const char *filename, int oflag, int mode)
/* Fill in some information that will be reported to
callers of android_fstat, among others. */
android_table[fd].statb.st_mode
= S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;;
= S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
/* Owned by root. */
android_table[fd].statb.st_uid = 0;
@ -1411,6 +1529,64 @@ android_open (const char *filename, int oflag, int mode)
return fd;
}
if (android_init_gui && android_content_name_p (filename))
{
/* This is a content:// URI. Ask the system for a descriptor to
that file. */
name = android_get_content_name (filename);
length = strlen (name);
/* Check if the mode is valid. */
if (oflag & O_DIRECTORY)
{
errno = ENOTSUP;
return -1;
}
/* Allocate a buffer to hold the file name. */
string = (*android_java_env)->NewByteArray (android_java_env,
length);
if (!string)
{
(*android_java_env)->ExceptionClear (android_java_env);
errno = ENOMEM;
return -1;
}
(*android_java_env)->SetByteArrayRegion (android_java_env,
string, 0, length,
(jbyte *) name);
/* Try to open the file descriptor. */
fd
= (*android_java_env)->CallIntMethod (android_java_env,
emacs_service,
service_class.open_content_uri,
string,
(jboolean) ((mode & O_WRONLY
|| mode & O_RDWR)
!= 0),
(jboolean) !(mode & O_WRONLY),
(jboolean) ((mode & O_TRUNC)
!= 0));
if ((*android_java_env)->ExceptionCheck (android_java_env))
{
(*android_java_env)->ExceptionClear (android_java_env);
errno = ENOMEM;
ANDROID_DELETE_LOCAL_REF (string);
return -1;
}
if (mode & O_CLOEXEC)
android_close_on_exec (fd);
ANDROID_DELETE_LOCAL_REF (string);
return fd;
}
return open (filename, oflag, mode);
}
@ -1488,6 +1664,12 @@ android_proc_name (int fd, char *buffer, size_t size)
#pragma GCC diagnostic ignored "-Wmissing-prototypes"
#endif
JNIEXPORT jint JNICALL
NATIVE_NAME (dup) (JNIEnv *env, jobject object, jint fd)
{
return dup (fd);
}
JNIEXPORT jstring JNICALL
NATIVE_NAME (getFingerprint) (JNIEnv *env, jobject object)
{
@ -1795,6 +1977,10 @@ android_init_emacs_service (void)
"(Lorg/gnu/emacs/EmacsWindow;IIII)V");
FIND_METHOD (reset_ic, "resetIC",
"(Lorg/gnu/emacs/EmacsWindow;I)V");
FIND_METHOD (open_content_uri, "openContentUri",
"([BZZZ)I");
FIND_METHOD (check_content_uri, "checkContentUri",
"([BZZ)Z");
#undef FIND_METHOD
}
@ -4577,7 +4763,28 @@ android_faccessat (int dirfd, const char *pathname, int mode, int flags)
if (dirfd == AT_FDCWD
&& asset_manager
&& (asset = android_get_asset_name (pathname)))
return !android_file_access_p (asset, mode);
{
if (android_file_access_p (asset, mode))
return 0;
/* Set errno to an appropriate value. */
errno = ENOENT;
return 1;
}
/* Check if pathname is actually a content resolver URI. */
if (dirfd == AT_FDCWD
&& android_init_gui
&& android_content_name_p (pathname))
{
if (android_check_content_access (pathname, mode))
return 0;
/* Set errno to an appropriate value. */
errno = ENOENT;
return 1;
}
#if __ANDROID_API__ >= 16
return faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS);