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:
parent
d197d73491
commit
7aa4ffddd8
6 changed files with 379 additions and 13 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ();
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
213
src/android.c
213
src/android.c
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue