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