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
# 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"
initialization. */
private static final String[] libraryDeps;
+\f
+ /* 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 ();
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class EmacsOpenActivity extends Activity
implements DialogInterface.OnClickListener
{
+ private static final String TAG = "EmacsOpenActivity";
+
private class EmacsClientThread extends Thread
{
private ProcessBuilder builder;
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;
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];
if (names != null)
fileName = new String (names, "UTF-8");
- fileName = checkReadableOrCopy (fileName, fd);
+ fileName = checkReadableOrCopy (fileName, fd, uri);
}
catch (FileNotFoundException exception)
{
package org.gnu.emacs;
+import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
import java.util.List;
import java.util.ArrayList;
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;
private EmacsThread thread;
private Handler handler;
+ private ContentResolver resolver;
/* Keep this in synch with androidgui.h. */
public static final int IC_MODE_NULL = 0;
metrics = getResources ().getDisplayMetrics ();
pixelDensityX = metrics.xdpi;
pixelDensityY = metrics.ydpi;
+ resolver = getContentResolver ();
try
{
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;
+ }
+ }
};
jmethodID restart_emacs;
jmethodID update_ic;
jmethodID reset_ic;
+ jmethodID open_content_uri;
+ jmethodID check_content_uri;
};
struct android_emacs_pixmap
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. */
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. */
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);
}
AAsset *asset;
int fd;
off_t out_start, out_length;
+ size_t length;
+ jobject string;
if (asset_manager && (name = android_get_asset_name (filename)))
{
if (oflag & O_DIRECTORY)
{
- errno = EINVAL;
+ errno = ENOTSUP;
return -1;
}
/* 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;
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);
}
#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)
{
"(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
}
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);