Allow quitting from Android content provider operations

* doc/emacs/android.texi (Android Document Providers): Say that
quitting is now possible.
* java/org/gnu/emacs/EmacsNative.java (EmacsNative): New
functions `safSyncAndReadInput', `safync' and `safPostRequest'.
* java/org/gnu/emacs/EmacsSafThread.java: New file.  Move
cancel-able SAF operations here.
* java/org/gnu/emacs/EmacsService.java (EmacsService): Allow
quitting from most SAF operations.
* src/androidvfs.c (android_saf_exception_check): Return EINTR
if OperationCanceledException is received.
(android_saf_stat, android_saf_access)
(android_document_id_from_name, android_saf_tree_opendir_1)
(android_saf_file_open): Don't allow reentrant calls from async
input handlers.
(NATIVE_NAME): Implement new synchronization primitives for JNI.
(android_vfs_init): Initialize new class.

* src/dired.c (open_directory): Handle EINTR from opendir.
* src/sysdep.c: Describe which operations may return EINTR on
Android.
This commit is contained in:
Po Lu 2023-07-28 15:19:37 +08:00
parent 03cf3bbb5c
commit 0709e03f88
7 changed files with 1127 additions and 483 deletions

View file

@ -305,14 +305,10 @@ file-system. In addition, although Emacs can normally write and
create files inside these directories, it cannot create symlinks or
hard links.
@c TODO: fix this!
Since document providers are allowed to perform expensive network
operations to obtain file contents, a file access operation within one
of these directories will possibly take a significant amount of time.
Emacs presently does not support quitting out of such file system
operations, and the timeouts applied are fully subject to the
discretion of the system and the document provider that is responding
to these operations.
of these directories has the potential to take a significant amount of
time.
@node Android Environment
@section Running Emacs under Android

View file

@ -257,6 +257,23 @@ public static native void blitRect (Bitmap src, Bitmap dest, int x1,
public static native void notifyPixelsChanged (Bitmap bitmap);
/* Functions used to synchronize document provider access with the
main thread. */
/* Wait for a call to `safPostRequest' while also reading async
input.
If asynchronous input arrives and sets Vquit_flag, return 1. */
public static native int safSyncAndReadInput ();
/* Wait for a call to `safPostRequest'. */
public static native void safSync ();
/* Post the semaphore used to await the completion of SAF
operations. */
public static native void safPostRequest ();
static
{
/* Older versions of Android cannot link correctly with shared

View file

@ -0,0 +1,922 @@
/* 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;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
/* Emacs runs long-running SAF operations on a second thread running
its own handler. These operations include opening files and
maintaining the path to document ID cache.
#if 0
Because Emacs paths are based on file display names, while Android
document identifiers have no discernible hierarchy of their own,
each file name lookup must carry out a repeated search for
directory documents with the names of all of the file name's
constituent components, where each iteration searches within the
directory document identified by the previous iteration.
A time limited cache tying components to document IDs is maintained
in order to speed up consecutive searches for file names sharing
the same components. Since listening for changes to each document
in the cache is prohibitively expensive, Emacs instead elects to
periodically remove entries that are older than a predetermined
amount of a time.
The cache is structured much like the directory trees whose
information it records, with each entry in the cache containing a
list of entries for their children. File name lookup consults the
cache and populates it with missing information simultaneously.
This is not yet implemented.
#endif
Long-running operations are also run on this thread for another
reason: Android uses special cancellation objects to terminate
ongoing IPC operations. However, the functions that perform these
operations block instead of providing mechanisms for the caller to
wait for their completion while also reading async input, as a
consequence of which the calling thread is unable to signal the
cancellation objects that it provides. Performing the blocking
operations in this auxiliary thread enables the main thread to wait
for completion itself, signaling the cancellation objects when it
deems necessary. */
public final class EmacsSafThread extends HandlerThread
{
/* The content resolver used by this thread. */
private final ContentResolver resolver;
/* Handler for this thread's main loop. */
private Handler handler;
/* File access mode constants. See `man 7 inode'. */
public static final int S_IRUSR = 0000400;
public static final int S_IWUSR = 0000200;
public static final int S_IFCHR = 0020000;
public static final int S_IFDIR = 0040000;
public static final int S_IFREG = 0100000;
public
EmacsSafThread (ContentResolver resolver)
{
super ("Document provider access thread");
this.resolver = resolver;
}
@Override
public void
start ()
{
super.start ();
/* Set up the handler after the thread starts. */
handler = new Handler (getLooper ());
}
/* ``Prototypes'' for nested functions that are run within the SAF
thread and accepts a cancellation signal. They differ in their
return types. */
private abstract class SafIntFunction
{
/* The ``throws Throwable'' here is a Java idiosyncracy that tells
the compiler to allow arbitrary error objects to be signaled
from within this function.
Later, runIntFunction will try to re-throw any error object
generated by this function in the Emacs thread, using a trick
to avoid the compiler requirement to expressly declare that an
error (and which types of errors) will be signaled. */
public abstract int runInt (CancellationSignal signal)
throws Throwable;
};
private abstract class SafObjectFunction
{
/* The ``throws Throwable'' here is a Java idiosyncracy that tells
the compiler to allow arbitrary error objects to be signaled
from within this function.
Later, runObjectFunction will try to re-throw any error object
generated by this function in the Emacs thread, using a trick
to avoid the compiler requirement to expressly declare that an
error (and which types of errors) will be signaled. */
public abstract Object runObject (CancellationSignal signal)
throws Throwable;
};
/* Functions that run cancel-able queries. These functions are
internally run within the SAF thread. */
/* Throw the specified EXCEPTION. The type template T is erased by
the compiler before the object is compiled, so the compiled code
simply throws EXCEPTION without the cast being verified.
T should be RuntimeException to obtain the desired effect of
throwing an exception without a compiler check. */
@SuppressWarnings("unchecked")
private static <T extends Throwable> void
throwException (Throwable exception)
throws T
{
throw (T) exception;
}
/* Run the given function (or rather, its `runInt' field) within the
SAF thread, waiting for it to complete.
If async input arrives in the meantime and sets Vquit_flag,
signal the cancellation signal supplied to that function.
Rethrow any exception thrown from that function, and return its
value otherwise. */
private int
runIntFunction (final SafIntFunction function)
{
final EmacsHolder<Object> result;
final CancellationSignal signal;
Throwable throwable;
result = new EmacsHolder<Object> ();
signal = new CancellationSignal ();
handler.post (new Runnable () {
@Override
public void
run ()
{
try
{
result.thing
= Integer.valueOf (function.runInt (signal));
}
catch (Throwable throwable)
{
result.thing = throwable;
}
EmacsNative.safPostRequest ();
}
});
if (EmacsNative.safSyncAndReadInput () != 0)
{
signal.cancel ();
/* Now wait for the function to finish. Either the signal has
arrived after the query took place, in which case it will
finish normally, or an OperationCanceledException will be
thrown. */
EmacsNative.safSync ();
}
if (result.thing instanceof Throwable)
{
throwable = (Throwable) result.thing;
EmacsSafThread.<RuntimeException>throwException (throwable);
}
return (Integer) result.thing;
}
/* Run the given function (or rather, its `runObject' field) within
the SAF thread, waiting for it to complete.
If async input arrives in the meantime and sets Vquit_flag,
signal the cancellation signal supplied to that function.
Rethrow any exception thrown from that function, and return its
value otherwise. */
private Object
runObjectFunction (final SafObjectFunction function)
{
final EmacsHolder<Object> result;
final CancellationSignal signal;
Throwable throwable;
result = new EmacsHolder<Object> ();
signal = new CancellationSignal ();
handler.post (new Runnable () {
@Override
public void
run ()
{
try
{
result.thing = function.runObject (signal);
}
catch (Throwable throwable)
{
result.thing = throwable;
}
EmacsNative.safPostRequest ();
}
});
if (EmacsNative.safSyncAndReadInput () != 0)
{
signal.cancel ();
/* Now wait for the function to finish. Either the signal has
arrived after the query took place, in which case it will
finish normally, or an OperationCanceledException will be
thrown. */
EmacsNative.safSync ();
}
if (result.thing instanceof Throwable)
{
throwable = (Throwable) result.thing;
EmacsSafThread.<RuntimeException>throwException (throwable);
}
return result.thing;
}
/* The crux of `documentIdFromName1', run within the SAF thread.
SIGNAL should be a cancellation signal run upon quitting. */
private int
documentIdFromName1 (String tree_uri, String name,
String[] id_return, CancellationSignal signal)
{
Uri uri, treeUri;
String id, type;
String[] components, projection;
Cursor cursor;
int column;
projection = new String[] {
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
};
/* Parse the URI identifying the tree first. */
uri = Uri.parse (tree_uri);
/* Now, split NAME into its individual components. */
components = name.split ("/");
/* Set id and type to the value at the root of the tree. */
type = id = null;
cursor = null;
/* For each component... */
try
{
for (String component : components)
{
/* Java split doesn't behave very much like strtok when it
comes to trailing and leading delimiters... */
if (component.isEmpty ())
continue;
/* Create the tree URI for URI from ID if it exists, or
the root otherwise. */
if (id == null)
id = DocumentsContract.getTreeDocumentId (uri);
treeUri
= DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
/* Look for a file in this directory by the name of
component. */
cursor = resolver.query (treeUri, projection,
(Document.COLUMN_DISPLAY_NAME
+ " = ?s"),
new String[] { component, },
null, signal);
if (cursor == null)
return -1;
while (true)
{
/* Even though the query selects for a specific
display name, some content providers nevertheless
return every file within the directory. */
if (!cursor.moveToNext ())
{
/* If the last component considered is a
directory... */
if ((type == null
|| type.equals (Document.MIME_TYPE_DIR))
/* ... and type and id currently represent the
penultimate component. */
&& component == components[components.length - 1])
{
/* The cursor is empty. In this case, return
-2 and the current document ID (belonging
to the previous component) in
ID_RETURN. */
id_return[0] = id;
/* But return -1 on the off chance that id is
null. */
if (id == null)
return -1;
return -2;
}
/* The last component found is not a directory, so
return -1. */
return -1;
}
/* So move CURSOR to a row with the right display
name. */
column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
if (column < 0)
continue;
name = cursor.getString (column);
/* Break out of the loop only once a matching
component is found. */
if (name.equals (component))
break;
}
/* Look for a column by the name of
COLUMN_DOCUMENT_ID. */
column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
if (column < 0)
return -1;
/* Now replace ID with the document ID. */
id = cursor.getString (column);
/* If this is the last component, be sure to initialize
the document type. */
if (component == components[components.length - 1])
{
column
= cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (column < 0)
return -1;
type = cursor.getString (column);
/* Type may be NULL depending on how the Cursor
returned is implemented. */
if (type == null)
return -1;
}
/* Now close the cursor. */
cursor.close ();
cursor = null;
/* ID may have become NULL if the data is in an invalid
format. */
if (id == null)
return -1;
}
}
finally
{
/* If an error is thrown within the block above, let
android_saf_exception_check handle it, but make sure the
cursor is closed. */
if (cursor != null)
cursor.close ();
}
/* Here, id is either NULL (meaning the same as TREE_URI), and
type is either NULL (in which case id should also be NULL) or
the MIME type of the file. */
/* First return the ID. */
if (id == null)
id_return[0] = DocumentsContract.getTreeDocumentId (uri);
else
id_return[0] = id;
/* Next, return whether or not this is a directory. */
if (type == null || type.equals (Document.MIME_TYPE_DIR))
return 1;
return 0;
}
/* Find the document ID of the file within TREE_URI designated by
NAME.
NAME is a ``file name'' comprised of the display names of
individual files. Each constituent component prior to the last
must name a directory file within TREE_URI.
Upon success, return 0 or 1 (contingent upon whether or not the
last component within NAME is a directory) and place the document
ID of the named file in ID_RETURN[0].
If the designated file can't be located, but each component of
NAME up to the last component can and is a directory, return -2
and the ID of the last component located in ID_RETURN[0].
If the designated file can't be located, return -1, or signal one
of OperationCanceledException, SecurityException,
FileNotFoundException, or UnsupportedOperationException. */
public int
documentIdFromName (final String tree_uri, final String name,
final String[] id_return)
{
return runIntFunction (new SafIntFunction () {
@Override
public int
runInt (CancellationSignal signal)
{
return documentIdFromName1 (tree_uri, name, id_return,
signal);
}
});
}
/* The bulk of `statDocument'. SIGNAL should be a cancelation
signal. */
private long[]
statDocument1 (String uri, String documentId,
CancellationSignal signal)
{
Uri uriObject;
String[] projection;
long[] stat;
int index;
long tem;
String tem1;
Cursor cursor;
uriObject = Uri.parse (uri);
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (uriObject);
/* Create a document URI representing DOCUMENTID within URI's
authority. */
uriObject
= DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
/* Now stat this document. */
projection = new String[] {
Document.COLUMN_FLAGS,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_SIZE,
};
cursor = resolver.query (uriObject, projection, null,
null, null, signal);
if (cursor == null)
return null;
if (!cursor.moveToFirst ())
{
cursor.close ();
return null;
}
/* Create the array of file status. */
stat = new long[3];
try
{
index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
if (index < 0)
return null;
tem = cursor.getInt (index);
stat[0] |= S_IRUSR;
if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0)
stat[0] |= S_IWUSR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
stat[0] |= S_IFCHR;
index = cursor.getColumnIndex (Document.COLUMN_SIZE);
if (index < 0)
return null;
if (cursor.isNull (index))
stat[1] = -1; /* The size is unknown. */
else
stat[1] = cursor.getLong (index);
index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (index < 0)
return null;
tem1 = cursor.getString (index);
/* Check if this is a directory file. */
if (tem1.equals (Document.MIME_TYPE_DIR)
/* Files shouldn't be specials and directories at the same
time, but Android doesn't forbid document providers
from returning this information. */
&& (stat[0] & S_IFCHR) == 0)
/* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
just assume they're writable. */
stat[0] |= S_IFDIR | S_IWUSR;
/* If this file is neither a character special nor a
directory, indicate that it's a regular file. */
if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
stat[0] |= S_IFREG;
index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
if (index >= 0 && !cursor.isNull (index))
{
/* Content providers are allowed to not provide mtime. */
tem = cursor.getLong (index);
stat[2] = tem;
}
}
finally
{
cursor.close ();
}
return stat;
}
/* Return file status for the document designated by the given
DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
ID in URI itself.
Value is null upon failure, or an array of longs [MODE, SIZE,
MTIM] upon success, where MODE contains the file type and access
modes of the file as in `struct stat', SIZE is the size of the
file in BYTES or -1 if not known, and MTIM is the time of the
last modification to this file in milliseconds since 00:00,
January 1st, 1970.
OperationCanceledException and other typical exceptions may be
signaled upon receiving async input or other errors. */
public long[]
statDocument (final String uri, final String documentId)
{
return (long[]) runObjectFunction (new SafObjectFunction () {
@Override
public Object
runObject (CancellationSignal signal)
{
return statDocument1 (uri, documentId, signal);
}
});
}
/* The bulk of `accessDocument'. SIGNAL should be a cancellation
signal. */
private int
accessDocument1 (String uri, String documentId, boolean writable,
CancellationSignal signal)
{
Uri uriObject;
String[] projection;
int tem, index;
String tem1;
Cursor cursor;
uriObject = Uri.parse (uri);
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (uriObject);
/* Create a document URI representing DOCUMENTID within URI's
authority. */
uriObject
= DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
/* Now stat this document. */
projection = new String[] {
Document.COLUMN_FLAGS,
Document.COLUMN_MIME_TYPE,
};
cursor = resolver.query (uriObject, projection, null,
null, null, signal);
if (cursor == null)
return -1;
try
{
if (!cursor.moveToFirst ())
return -1;
if (!writable)
return 0;
index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (index < 0)
return -3;
/* Get the type of this file to check if it's a directory. */
tem1 = cursor.getString (index);
/* Check if this is a directory file. */
if (tem1.equals (Document.MIME_TYPE_DIR))
{
/* If so, don't check for FLAG_SUPPORTS_WRITE.
Check for FLAG_DIR_SUPPORTS_CREATE instead. */
if (!writable)
return 0;
index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
if (index < 0)
return -3;
tem = cursor.getInt (index);
if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
return -3;
return 0;
}
index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
if (index < 0)
return -3;
tem = cursor.getInt (index);
if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
return -3;
}
finally
{
/* Close the cursor if an exception occurs. */
cursor.close ();
}
return 0;
}
/* Find out whether Emacs has access to the document designated by
the specified DOCUMENTID within the tree URI. If DOCUMENTID is
NULL, use the document ID in URI itself.
If WRITABLE, also check that the file is writable, which is true
if it is either a directory or its flags contains
FLAG_SUPPORTS_WRITE.
Value is 0 if the file is accessible, and one of the following if
not:
-1, if the file does not exist.
-2, if WRITABLE and the file is not writable.
-3, upon any other error.
In addition, arbitrary runtime exceptions (such as
SecurityException or UnsupportedOperationException) may be
thrown. */
public int
accessDocument (final String uri, final String documentId,
final boolean writable)
{
return runIntFunction (new SafIntFunction () {
@Override
public int
runInt (CancellationSignal signal)
{
return accessDocument1 (uri, documentId, writable,
signal);
}
});
}
/* The crux of openDocumentDirectory. SIGNAL must be a cancellation
signal. */
private Cursor
openDocumentDirectory1 (String uri, String documentId,
CancellationSignal signal)
{
Uri uriObject;
Cursor cursor;
String projection[];
uriObject = Uri.parse (uri);
/* If documentId is not set, use the document ID of the tree URI
itself. */
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (uriObject);
/* Build a URI representing each directory entry within
DOCUMENTID. */
uriObject
= DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
documentId);
projection = new String [] {
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
};
cursor = resolver.query (uriObject, projection, null, null,
null, signal);
/* Return the cursor. */
return cursor;
}
/* Open a cursor representing each entry within the directory
designated by the specified DOCUMENTID within the tree URI.
If DOCUMENTID is NULL, use the document ID within URI itself.
Value is NULL upon failure.
In addition, arbitrary runtime exceptions (such as
SecurityException or UnsupportedOperationException) may be
thrown. */
public Cursor
openDocumentDirectory (final String uri, final String documentId)
{
return (Cursor) runObjectFunction (new SafObjectFunction () {
@Override
public Object
runObject (CancellationSignal signal)
{
return openDocumentDirectory1 (uri, documentId, signal);
}
});
}
/* The crux of `openDocument'. SIGNAL must be a cancellation
signal. */
public ParcelFileDescriptor
openDocument1 (String uri, String documentId, boolean write,
boolean truncate, CancellationSignal signal)
throws Throwable
{
Uri treeUri, documentUri;
String mode;
ParcelFileDescriptor fileDescriptor;
treeUri = Uri.parse (uri);
/* documentId must be set for this request, since it doesn't make
sense to ``open'' the root of the directory tree. */
documentUri
= DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
{
/* Select the mode used to open the file. `rw' means open
a stat-able file, while `rwt' means that and to
truncate the file as well. */
if (truncate)
mode = "rwt";
else
mode = "rw";
fileDescriptor
= resolver.openFileDescriptor (documentUri, mode,
signal);
}
else
{
/* Select the mode used to open the file. `openFile'
below means always open a stat-able file. */
if (truncate)
/* Invalid mode! */
return null;
else
mode = "r";
fileDescriptor = resolver.openFile (documentUri, mode,
signal);
}
return fileDescriptor;
}
/* Open a file descriptor for a file document designated by
DOCUMENTID within the document tree identified by URI. If
TRUNCATE and the document already exists, truncate its contents
before returning.
On Android 9.0 and earlier, always open the document in
``read-write'' mode; this instructs the document provider to
return a seekable file that is stored on disk and returns correct
file status.
Under newer versions of Android, open the document in a
non-writable mode if WRITE is false. This is possible because
these versions allow Emacs to explicitly request a seekable
on-disk file.
Value is NULL upon failure or a parcel file descriptor upon
success. Call `ParcelFileDescriptor.close' on this file
descriptor instead of using the `close' system call.
FileNotFoundException and/or SecurityException and/or
UnsupportedOperationException and/or OperationCanceledException
may be thrown upon failure. */
public ParcelFileDescriptor
openDocument (final String uri, final String documentId,
final boolean write, final boolean truncate)
{
Object tem;
tem = runObjectFunction (new SafObjectFunction () {
@Override
public Object
runObject (CancellationSignal signal)
throws Throwable
{
return openDocument1 (uri, documentId, write, truncate,
signal);
}
});
return (ParcelFileDescriptor) tem;
}
};

View file

@ -109,13 +109,6 @@ public final class EmacsService extends Service
public static final int IC_MODE_ACTION = 1;
public static final int IC_MODE_TEXT = 2;
/* File access mode constants. See `man 7 inode'. */
public static final int S_IRUSR = 0000400;
public static final int S_IWUSR = 0000200;
public static final int S_IFCHR = 0020000;
public static final int S_IFDIR = 0040000;
public static final int S_IFREG = 0100000;
/* Display metrics used by font backends. */
public DisplayMetrics metrics;
@ -134,6 +127,10 @@ public final class EmacsService extends Service
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;
static
{
servicingQuery = new AtomicInteger ();
@ -1160,10 +1157,7 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
/* Document tree management functions. These functions shouldn't be
called before Android 5.0.
TODO: a timeout, let alone quitting, has yet to be implemented
for any of these functions. */
called before Android 5.0. */
/* Return an array of each document authority providing at least one
tree URI that Emacs holds the rights to persistently access. */
@ -1319,223 +1313,26 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
If the designated file can't be located, but each component of
NAME up to the last component can and is a directory, return -2
and the ID of the last component located in ID_RETURN[0];
and the ID of the last component located in ID_RETURN[0].
If the designated file can't be located, return -1. */
If the designated file can't be located, return -1, or signal one
of OperationCanceledException, SecurityException,
FileNotFoundException, or UnsupportedOperationException. */
private int
documentIdFromName (String tree_uri, String name, String[] id_return)
{
Uri uri, treeUri;
String id, type;
String[] components, projection;
Cursor cursor;
int column;
/* Start the thread used to run SAF requests if it isn't already
running. */
projection = new String[] {
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
};
/* Parse the URI identifying the tree first. */
uri = Uri.parse (tree_uri);
/* Now, split NAME into its individual components. */
components = name.split ("/");
/* Set id and type to the value at the root of the tree. */
type = id = null;
/* For each component... */
for (String component : components)
if (storageThread == null)
{
/* Java split doesn't behave very much like strtok when it
comes to trailing and leading delimiters... */
if (component.isEmpty ())
continue;
/* Create the tree URI for URI from ID if it exists, or the
root otherwise. */
if (id == null)
id = DocumentsContract.getTreeDocumentId (uri);
treeUri
= DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
/* Look for a file in this directory by the name of
component. */
try
{
cursor = resolver.query (treeUri, projection,
(Document.COLUMN_DISPLAY_NAME
+ " = ?s"),
new String[] { component, }, null);
}
catch (SecurityException exception)
{
/* A SecurityException can be thrown if Emacs doesn't have
access to treeUri. */
return -1;
}
catch (Exception exception)
{
exception.printStackTrace ();
/* Why is this? */
return -1;
}
if (cursor == null)
return -1;
while (true)
{
/* Even though the query selects for a specific display
name, some content providers nevertheless return every
file within the directory. */
if (!cursor.moveToNext ())
{
cursor.close ();
/* If the last component considered is a
directory... */
if ((type == null
|| type.equals (Document.MIME_TYPE_DIR))
/* ... and type and id currently represent the
penultimate component. */
&& component == components[components.length - 1])
{
/* The cursor is empty. In this case, return -2
and the current document ID (belonging to the
previous component) in ID_RETURN. */
id_return[0] = id;
/* But return -1 on the off chance that id is
null. */
if (id == null)
return -1;
return -2;
}
/* The last component found is not a directory, so
return -1. */
return -1;
}
/* So move CURSOR to a row with the right display
name. */
column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
if (column < 0)
continue;
try
{
name = cursor.getString (column);
}
catch (Exception exception)
{
cursor.close ();
return -1;
}
/* Break out of the loop only once a matching component is
found. */
if (name.equals (component))
break;
}
/* Look for a column by the name of COLUMN_DOCUMENT_ID. */
column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
if (column < 0)
{
cursor.close ();
return -1;
}
/* Now replace ID with the document ID. */
try
{
id = cursor.getString (column);
}
catch (Exception exception)
{
cursor.close ();
return -1;
}
/* If this is the last component, be sure to initialize the
document type. */
if (component == components[components.length - 1])
{
column
= cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (column < 0)
{
cursor.close ();
return -1;
}
try
{
type = cursor.getString (column);
}
catch (Exception exception)
{
cursor.close ();
return -1;
}
/* Type may be NULL depending on how the Cursor returned
is implemented. */
if (type == null)
{
cursor.close ();
return -1;
}
}
/* Now close the cursor. */
cursor.close ();
/* ID may have become NULL if the data is in an invalid
format. */
if (id == null)
return -1;
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
/* Here, id is either NULL (meaning the same as TREE_URI), and
type is either NULL (in which case id should also be NULL) or
the MIME type of the file. */
/* First return the ID. */
if (id == null)
id_return[0] = DocumentsContract.getTreeDocumentId (uri);
else
id_return[0] = id;
/* Next, return whether or not this is a directory. */
if (type == null || type.equals (Document.MIME_TYPE_DIR))
return 1;
return 0;
return storageThread.documentIdFromName (tree_uri, name,
id_return);
}
/* Return an encoded document URI representing a tree with the
@ -1585,130 +1382,24 @@ type is either NULL (in which case id should also be NULL) or
modes of the file as in `struct stat', SIZE is the size of the
file in BYTES or -1 if not known, and MTIM is the time of the
last modification to this file in milliseconds since 00:00,
January 1st, 1970. */
January 1st, 1970.
OperationCanceledException and other typical exceptions may be
signaled upon receiving async input or other errors. */
public long[]
statDocument (String uri, String documentId)
{
Uri uriObject;
String[] projection;
long[] stat;
int index;
long tem;
String tem1;
Cursor cursor;
/* Start the thread used to run SAF requests if it isn't already
running. */
uriObject = Uri.parse (uri);
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (uriObject);
/* Create a document URI representing DOCUMENTID within URI's
authority. */
uriObject
= DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
/* Now stat this document. */
projection = new String[] {
Document.COLUMN_FLAGS,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_SIZE,
};
try
if (storageThread == null)
{
cursor = resolver.query (uriObject, projection, null,
null, null);
}
catch (SecurityException exception)
{
/* A SecurityException can be thrown if Emacs doesn't have
access to uriObject. */
return null;
}
catch (UnsupportedOperationException exception)
{
exception.printStackTrace ();
/* Why is this? */
return null;
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
if (cursor == null || !cursor.moveToFirst ())
return null;
/* Create the array of file status. */
stat = new long[3];
try
{
index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
if (index < 0)
return null;
tem = cursor.getInt (index);
stat[0] |= S_IRUSR;
if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0)
stat[0] |= S_IWUSR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
stat[0] |= S_IFCHR;
index = cursor.getColumnIndex (Document.COLUMN_SIZE);
if (index < 0)
return null;
if (cursor.isNull (index))
stat[1] = -1; /* The size is unknown. */
else
stat[1] = cursor.getLong (index);
index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (index < 0)
return null;
tem1 = cursor.getString (index);
/* Check if this is a directory file. */
if (tem1.equals (Document.MIME_TYPE_DIR)
/* Files shouldn't be specials and directories at the same
time, but Android doesn't forbid document providers
from returning this information. */
&& (stat[0] & S_IFCHR) == 0)
/* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
just assume they're writable. */
stat[0] |= S_IFDIR | S_IWUSR;
/* If this file is neither a character special nor a
directory, indicate that it's a regular file. */
if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
stat[0] |= S_IFREG;
index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
if (index >= 0 && !cursor.isNull (index))
{
/* Content providers are allowed to not provide mtime. */
tem = cursor.getLong (index);
stat[2] = tem;
}
}
catch (Exception exception)
{
/* Whether or not type errors cause exceptions to be signaled
is defined ``by the implementation of Cursor'', whatever
that means. */
exception.printStackTrace ();
return null;
}
return stat;
return storageThread.statDocument (uri, documentId);
}
/* Find out whether Emacs has access to the document designated by
@ -1733,83 +1424,16 @@ In addition, arbitrary runtime exceptions (such as
public int
accessDocument (String uri, String documentId, boolean writable)
{
Uri uriObject;
String[] projection;
int tem, index;
String tem1;
Cursor cursor;
/* Start the thread used to run SAF requests if it isn't already
running. */
uriObject = Uri.parse (uri);
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (uriObject);
/* Create a document URI representing DOCUMENTID within URI's
authority. */
uriObject
= DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
/* Now stat this document. */
projection = new String[] {
Document.COLUMN_FLAGS,
Document.COLUMN_MIME_TYPE,
};
cursor = resolver.query (uriObject, projection, null,
null, null);
if (cursor == null || !cursor.moveToFirst ())
return -1;
if (!writable)
return 0;
try
if (storageThread == null)
{
index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (index < 0)
return -3;
/* Get the type of this file to check if it's a directory. */
tem1 = cursor.getString (index);
/* Check if this is a directory file. */
if (tem1.equals (Document.MIME_TYPE_DIR))
{
/* If so, don't check for FLAG_SUPPORTS_WRITE.
Check for FLAG_DIR_SUPPORTS_CREATE instead. */
if (!writable)
return 0;
index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
if (index < 0)
return -3;
tem = cursor.getInt (index);
if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
return -3;
return 0;
}
index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
if (index < 0)
return -3;
tem = cursor.getInt (index);
if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
return -3;
}
finally
{
/* Close the cursor if an exception occurs. */
cursor.close ();
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return 0;
return storageThread.accessDocument (uri, documentId, writable);
}
/* Open a cursor representing each entry within the directory
@ -1825,34 +1449,16 @@ In addition, arbitrary runtime exceptions (such as
public Cursor
openDocumentDirectory (String uri, String documentId)
{
Uri uriObject;
Cursor cursor;
String projection[];
/* Start the thread used to run SAF requests if it isn't already
running. */
uriObject = Uri.parse (uri);
if (storageThread == null)
{
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
/* If documentId is not set, use the document ID of the tree URI
itself. */
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (uriObject);
/* Build a URI representing each directory entry within
DOCUMENTID. */
uriObject
= DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
documentId);
projection = new String [] {
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
};
cursor = resolver.query (uriObject, projection, null, null,
null);
/* Return the cursor. */
return cursor;
return storageThread.openDocumentDirectory (uri, documentId);
}
/* Read a single directory entry from the specified CURSOR. Return
@ -1945,50 +1551,18 @@ In addition, arbitrary runtime exceptions (such as
public ParcelFileDescriptor
openDocument (String uri, String documentId, boolean write,
boolean truncate)
throws FileNotFoundException
{
Uri treeUri, documentUri;
String mode;
ParcelFileDescriptor fileDescriptor;
/* Start the thread used to run SAF requests if it isn't already
running. */
treeUri = Uri.parse (uri);
/* documentId must be set for this request, since it doesn't make
sense to ``open'' the root of the directory tree. */
documentUri
= DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
if (storageThread == null)
{
/* Select the mode used to open the file. `rw' means open
a stat-able file, while `rwt' means that and to
truncate the file as well. */
if (truncate)
mode = "rwt";
else
mode = "rw";
fileDescriptor
= resolver.openFileDescriptor (documentUri, mode,
null);
}
else
{
/* Select the mode used to open the file. `openFile'
below means always open a stat-able file. */
if (truncate)
/* Invalid mode! */
return null;
else
mode = "r";
fileDescriptor = resolver.openFile (documentUri, mode, null);
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return fileDescriptor;
return storageThread.openDocument (uri, documentId, write,
truncate);
}
/* Create a new document with the given display NAME within the

View file

@ -26,6 +26,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#include <errno.h>
#include <minmax.h>
#include <string.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <sys/mman.h>
@ -34,6 +35,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#include "android.h"
#include "systime.h"
#include "blockinput.h"
#if __ANDROID_API__ >= 9
#include <android/asset_manager.h>
@ -278,6 +280,7 @@ static struct android_parcel_file_descriptor_class fd_class;
/* Global references to several exception classes. */
static jclass file_not_found_exception, security_exception;
static jclass operation_canceled_exception;
static jclass unsupported_operation_exception, out_of_memory_error;
/* Initialize `cursor_class' using the given JNI environment ENV.
@ -3692,6 +3695,10 @@ android_saf_root_get_directory (int dirfd)
/* Functions common to both SAF directory and file nodes. */
/* Whether or not Emacs is within an operation running from the SAF
thread. */
static bool inside_saf_critical_section;
/* Check for JNI exceptions, clear them, and set errno accordingly.
Also, free each of the N local references given as arguments if an
exception takes place.
@ -3704,6 +3711,9 @@ android_saf_root_get_directory (int dirfd)
If the exception thrown derives from SecurityException, set errno
to EACCES.
If the exception thrown derives from OperationCanceledException,
set errno to EINTR.
If the exception thrown derives from UnsupportedOperationException,
set errno to ENOSYS.
@ -3754,6 +3764,9 @@ android_saf_exception_check (int n, ...)
else if ((*env)->IsInstanceOf (env, (jobject) exception,
security_exception))
errno = EACCES;
else if ((*env)->IsInstanceOf (env, (jobject) exception,
operation_canceled_exception))
errno = EINTR;
else if ((*env)->IsInstanceOf (env, (jobject) exception,
unsupported_operation_exception))
errno = ENOSYS;
@ -3786,6 +3799,15 @@ android_saf_stat (const char *uri_name, const char *id_name,
jobject status;
jlong mode, size, mtim, *longs;
/* Now guarantee that it is safe to call functions which
synchronize with the SAF thread. */
if (inside_saf_critical_section)
{
errno = EIO;
return -1;
}
/* Build strings for both URI and ID. */
uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name);
android_exception_check ();
@ -3801,10 +3823,12 @@ android_saf_stat (const char *uri_name, const char *id_name,
/* Try to retrieve the file status. */
method = service_class.stat_document;
inside_saf_critical_section = true;
status = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env,
emacs_service,
service_class.class,
method, uri, id);
inside_saf_critical_section = false;
/* Check for exceptions and release unneeded local references. */
@ -3870,6 +3894,15 @@ android_saf_access (const char *uri_name, const char *id_name,
jstring uri, id;
jint rc;
/* Now guarantee that it is safe to call functions which
synchronize with the SAF thread. */
if (inside_saf_critical_section)
{
errno = EIO;
return -1;
}
/* Build strings for both URI and ID. */
uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name);
android_exception_check ();
@ -3885,11 +3918,13 @@ android_saf_access (const char *uri_name, const char *id_name,
/* Try to retrieve the file status. */
method = service_class.access_document;
inside_saf_critical_section = true;
rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env,
emacs_service,
service_class.class,
method, uri, id,
(jboolean) writable);
inside_saf_critical_section = false;
/* Check for exceptions and release unneeded local references. */
@ -4161,7 +4196,19 @@ android_document_id_from_name (const char *tree_uri, char *name,
contain characters that can't be encoded in Java. */
if (android_verify_jni_string (name))
return -1;
{
errno = ENOENT;
return -1;
}
/* Now guarantee that it is safe to call
`document_id_from_name'. */
if (inside_saf_critical_section)
{
errno = EIO;
return -1;
}
/* First, create the array that will hold the result. */
result = (*android_java_env)->NewObjectArray (android_java_env, 1,
@ -4176,14 +4223,17 @@ android_document_id_from_name (const char *tree_uri, char *name,
uri = (*android_java_env)->NewStringUTF (android_java_env, tree_uri);
android_exception_check_2 (result, java_name);
/* Now, call documentIdFromName. */
/* Now, call documentIdFromName. This will synchronize with the SAF
thread, so make sure reentrant calls don't happen. */
method = service_class.document_id_from_name;
inside_saf_critical_section = true;
rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env,
emacs_service,
service_class.class,
method,
uri, java_name,
result);
inside_saf_critical_section = false;
if (android_saf_exception_check (3, result, uri, java_name))
goto finish;
@ -4562,6 +4612,12 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp)
jobject uri, id, cursor;
jmethodID method;
if (inside_saf_critical_section)
{
errno = EIO;
return NULL;
}
/* Build strings for both URI and ID. */
uri = (*android_java_env)->NewStringUTF (android_java_env,
vp->tree_uri);
@ -4578,11 +4634,13 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp)
/* Try to open the cursor. */
method = service_class.open_document_directory;
inside_saf_critical_section = true;
cursor
= (*android_java_env)->CallNonvirtualObjectMethod (android_java_env,
emacs_service,
service_class.class,
method, uri, id);
inside_saf_critical_section = false;
if (id)
{
@ -5001,6 +5059,12 @@ android_saf_file_open (struct android_vnode *vnode, int flags,
struct android_parcel_fd *info;
struct stat statb;
if (inside_saf_critical_section)
{
errno = EIO;
return -1;
}
/* Build strings for both the URI and ID. */
vp = (struct android_saf_file_vnode *) vnode;
@ -5016,12 +5080,14 @@ android_saf_file_open (struct android_vnode *vnode, int flags,
method = service_class.open_document;
trunc = flags & O_TRUNC;
write = ((flags & O_RDWR) == O_RDWR || (flags & O_WRONLY));
inside_saf_critical_section = true;
descriptor
= (*android_java_env)->CallNonvirtualObjectMethod (android_java_env,
emacs_service,
service_class.class,
method, uri, id,
write, trunc);
inside_saf_critical_section = false;
if (android_saf_exception_check (2, uri, id))
return -1;
@ -5468,6 +5534,48 @@ android_saf_new_opendir (struct android_vnode *vnode)
/* Synchronization between SAF and Emacs. Consult EmacsSafThread.java
for more details. */
/* Semaphore posted upon the completion of an SAF operation. */
static sem_t saf_completion_sem;
JNIEXPORT jint JNICALL
NATIVE_NAME (safSyncAndReadInput) (JNIEnv *env, jobject object)
{
while (sem_wait (&saf_completion_sem) < 0)
{
if (input_blocked_p ())
continue;
process_pending_signals ();
if (!NILP (Vquit_flag))
{
__android_log_print (ANDROID_LOG_VERBOSE, __func__,
"quitting from IO operation");
return 1;
}
}
return 0;
}
JNIEXPORT void JNICALL
NATIVE_NAME (safSync) (JNIEnv *env, jobject object)
{
while (sem_wait (&saf_completion_sem) < 0)
process_pending_signals ();
}
JNIEXPORT void JNICALL
NATIVE_NAME (safPostRequest) (JNIEnv *env, jobject object)
{
sem_post (&saf_completion_sem);
}
/* Root vnode. This vnode represents the root inode, and is a regular
Unix vnode with modifications to `name' that make it return asset
vnodes. */
@ -5692,6 +5800,11 @@ android_vfs_init (JNIEnv *env, jobject manager)
(*env)->DeleteLocalRef (env, old);
eassert (security_exception);
old = (*env)->FindClass (env, "android/os/OperationCanceledException");
operation_canceled_exception = (*env)->NewGlobalRef (env, old);
(*env)->DeleteLocalRef (env, old);
eassert (operation_canceled_exception);
old = (*env)->FindClass (env, "java/lang/UnsupportedOperationException");
unsupported_operation_exception = (*env)->NewGlobalRef (env, old);
(*env)->DeleteLocalRef (env, old);
@ -5701,6 +5814,12 @@ android_vfs_init (JNIEnv *env, jobject manager)
out_of_memory_error = (*env)->NewGlobalRef (env, old);
(*env)->DeleteLocalRef (env, old);
eassert (out_of_memory_error);
/* Initialize the semaphore used to wait for SAF operations to
complete. */
if (sem_init (&saf_completion_sem, 0, 0) < 0)
emacs_abort ();
}
/* The replacement functions that follow have several major
@ -5754,6 +5873,12 @@ android_vfs_init (JNIEnv *env, jobject manager)
The sixth is that flags and other argument checking is nowhere near
exhaustive on vnode types other than Unix vnodes.
The seventh is that certain vnode types may read async input and
return EINTR not upon the arrival of a signal itself, but instead
if subsequently read input causes Vquit_flag to be set. These
vnodes may not be reentrant, but operating on them from within an
async input handler will at worst cause an error to be returned.
And the final drawback is that directories cannot be directly
opened. Instead, `dirfd' must be called on a directory stream used
by `openat'.
@ -6409,7 +6534,8 @@ android_asset_fstat (struct android_fd_or_asset asset,
/* Directory listing emulation. */
/* Open a directory stream from the VFS node designated by NAME.
Value is NULL upon failure with errno set accordingly.
Value is NULL upon failure with errno set accordingly. `errno' may
be set to EINTR.
The directory stream returned holds local references to JNI objects
and shouldn't be used after the current local reference frame is

View file

@ -115,10 +115,19 @@ open_directory (Lisp_Object dirname, Lisp_Object encoded_dirname, int *fdp)
#ifndef HAVE_ANDROID
d = opendir (name);
#else
/* `android_opendir' can return EINTR if DIRNAME designates a file
within a slow-to-respond document provider. */
again:
d = android_opendir (name);
if (d)
fd = android_dirfd (d);
else if (errno == EINTR)
{
maybe_quit ();
goto again;
}
#endif
opendir_errno = errno;
#else

View file

@ -2656,7 +2656,7 @@ emacs_fclose (FILE *stream)
/* Wrappers around unlink, symlink, rename, renameat_noreplace, and
rmdir. These operations handle asset and content directories on
Android. */
Android, and may return EINTR. */
int
emacs_unlink (const char *name)