emacs-diffs
[Top][All Lists]
Advanced

[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)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]