[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
feature/android 0709e03f88c: Allow quitting from Android content provide
From: |
Po Lu |
Subject: |
feature/android 0709e03f88c: Allow quitting from Android content provider operations |
Date: |
Fri, 28 Jul 2023 03:21:51 -0400 (EDT) |
branch: feature/android
commit 0709e03f88cdef8f785338cab9315b527db0854e
Author: Po Lu <luangruo@yahoo.com>
Commit: Po Lu <luangruo@yahoo.com>
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.
---
doc/emacs/android.texi | 8 +-
java/org/gnu/emacs/EmacsNative.java | 17 +
java/org/gnu/emacs/EmacsSafThread.java | 922 +++++++++++++++++++++++++++++++++
java/org/gnu/emacs/EmacsService.java | 520 ++-----------------
src/androidvfs.c | 132 ++++-
src/dired.c | 9 +
src/sysdep.c | 2 +-
7 files changed, 1127 insertions(+), 483 deletions(-)
diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi
index b86c71cea49..0330e9b5890 100644
--- a/doc/emacs/android.texi
+++ b/doc/emacs/android.texi
@@ -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
diff --git a/java/org/gnu/emacs/EmacsNative.java
b/java/org/gnu/emacs/EmacsNative.java
index d4d502ede5a..ea200037218 100644
--- a/java/org/gnu/emacs/EmacsNative.java
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -257,6 +257,23 @@ public final class EmacsNative
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
diff --git a/java/org/gnu/emacs/EmacsSafThread.java
b/java/org/gnu/emacs/EmacsSafThread.java
new file mode 100644
index 00000000000..fd06603fab3
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSafThread.java
@@ -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;
+ }
+};
diff --git a/java/org/gnu/emacs/EmacsService.java
b/java/org/gnu/emacs/EmacsService.java
index aa672994f12..e410754071b 100644
--- a/java/org/gnu/emacs/EmacsService.java
+++ b/java/org/gnu/emacs/EmacsService.java
@@ -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 @@ public final class EmacsService extends Service
/* 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 @@ public final class EmacsService extends Service
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;
-
- 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 ("/");
+ /* Start the thread used to run SAF requests if it isn't already
+ running. */
- /* 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 @@ public final class EmacsService extends Service
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;
-
- 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
- {
- 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;
- }
-
- if (cursor == null || !cursor.moveToFirst ())
- return null;
-
- /* Create the array of file status. */
- stat = new long[3];
+ /* Start the thread used to run SAF requests if it isn't already
+ running. */
- try
+ if (storageThread == null)
{
- 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;
+ storageThread = new EmacsSafThread (resolver);
+ storageThread.start ();
}
- return stat;
+ return storageThread.statDocument (uri, documentId);
}
/* Find out whether Emacs has access to the document designated by
@@ -1733,83 +1424,16 @@ public final class EmacsService extends Service
public int
accessDocument (String uri, String documentId, boolean writable)
{
- 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. */
+ /* Start the thread used to run SAF requests if it isn't already
+ running. */
- 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 @@ public final class EmacsService extends Service
public Cursor
openDocumentDirectory (String uri, String documentId)
{
- 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);
+ /* Start the thread used to run SAF requests if it isn't already
+ running. */
- projection = new String [] {
- Document.COLUMN_DISPLAY_NAME,
- Document.COLUMN_MIME_TYPE,
- };
+ if (storageThread == null)
+ {
+ storageThread = new EmacsSafThread (resolver);
+ storageThread.start ();
+ }
- 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 @@ public final class EmacsService extends Service
public ParcelFileDescriptor
openDocument (String uri, String documentId, boolean write,
boolean truncate)
- throws FileNotFoundException
{
- 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. */
+ /* Start the thread used to run SAF requests if it isn't already
+ running. */
- 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
diff --git a/src/androidvfs.c b/src/androidvfs.c
index 2cd50963e97..4f485622ff4 100644
--- a/src/androidvfs.c
+++ b/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
diff --git a/src/dired.c b/src/dired.c
index f2a123dc168..c10531cdb16 100644
--- a/src/dired.c
+++ b/src/dired.c
@@ -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
diff --git a/src/sysdep.c b/src/sysdep.c
index 88938d15b91..0a1905c9196 100644
--- a/src/sysdep.c
+++ b/src/sysdep.c
@@ -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)
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- feature/android 0709e03f88c: Allow quitting from Android content provider operations,
Po Lu <=