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:
parent
03cf3bbb5c
commit
0709e03f88
7 changed files with 1127 additions and 483 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
922
java/org/gnu/emacs/EmacsSafThread.java
Normal file
922
java/org/gnu/emacs/EmacsSafThread.java
Normal 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;
|
||||
}
|
||||
};
|
|
@ -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
|
||||
|
|
132
src/androidvfs.c
132
src/androidvfs.c
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue