From 0709e03f88cdef8f785338cab9315b527db0854e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 28 Jul 2023 15:19:37 +0800 Subject: [PATCH] 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(-) create mode 100644 java/org/gnu/emacs/EmacsSafThread.java 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 . */ + +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 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 result; + final CancellationSignal signal; + Throwable throwable; + + result = new EmacsHolder (); + 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.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 result; + final CancellationSignal signal; + Throwable throwable; + + result = new EmacsHolder (); + 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.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 . */ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ along with GNU Emacs. If not, see . */ #include "android.h" #include "systime.h" +#include "blockinput.h" #if __ANDROID_API__ >= 9 #include @@ -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) -- 2.39.5