1709 lines
45 KiB
Java
1709 lines
45 KiB
Java
/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
|
|
|
|
Copyright (C) 2023-2026 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 java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
|
|
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.OperationCanceledException;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.os.SystemClock;
|
|
|
|
import android.util.Log;
|
|
|
|
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.
|
|
|
|
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 split into two levels: the first caches the
|
|
relationships between display names and document IDs, while the
|
|
second caches individual document IDs and their contents (children,
|
|
type, etc.)
|
|
|
|
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
|
|
{
|
|
private static final String TAG = "EmacsSafThread";
|
|
|
|
/* The content resolver used by this thread. */
|
|
private final ContentResolver resolver;
|
|
|
|
/* Map between tree URIs and the cache entry representing its
|
|
toplevel directory. */
|
|
private final HashMap<Uri, CacheToplevel> cacheToplevels;
|
|
|
|
/* 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_IXUSR = 0000100;
|
|
public static final int S_IFCHR = 0020000;
|
|
public static final int S_IFDIR = 0040000;
|
|
public static final int S_IFREG = 0100000;
|
|
|
|
/* Number of seconds in between each attempt to prune the storage
|
|
cache. */
|
|
public static final int CACHE_PRUNE_TIME = 10;
|
|
|
|
/* Number of seconds after which an entry in the cache is to be
|
|
considered invalid. */
|
|
public static final int CACHE_INVALID_TIME = 10;
|
|
|
|
public
|
|
EmacsSafThread (ContentResolver resolver)
|
|
{
|
|
super ("Document provider access thread");
|
|
this.resolver = resolver;
|
|
this.cacheToplevels = new HashMap<Uri, CacheToplevel> ();
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public void
|
|
start ()
|
|
{
|
|
super.start ();
|
|
|
|
/* Set up the handler after the thread starts. */
|
|
handler = new Handler (getLooper ());
|
|
|
|
/* And start periodically pruning the cache. */
|
|
postPruneMessage ();
|
|
}
|
|
|
|
|
|
private static final class CacheToplevel
|
|
{
|
|
/* Map between document names and children. */
|
|
HashMap<String, DocIdEntry> children;
|
|
|
|
/* Map between document names and file status. */
|
|
HashMap<String, StatCacheEntry> statCache;
|
|
|
|
/* Map between document IDs and cache items. */
|
|
HashMap<String, CacheEntry> idCache;
|
|
};
|
|
|
|
private static final class StatCacheEntry
|
|
{
|
|
/* The time at which this cache entry was created. */
|
|
long time;
|
|
|
|
/* Flags, size, and modification time of this file. */
|
|
long flags, size, mtime;
|
|
|
|
/* Whether or not this file is a directory. */
|
|
boolean isDirectory;
|
|
|
|
public
|
|
StatCacheEntry ()
|
|
{
|
|
time = SystemClock.uptimeMillis ();
|
|
}
|
|
|
|
public boolean
|
|
isValid ()
|
|
{
|
|
return ((SystemClock.uptimeMillis () - time)
|
|
< CACHE_INVALID_TIME * 1000);
|
|
}
|
|
};
|
|
|
|
private static final class DocIdEntry
|
|
{
|
|
/* The document ID. */
|
|
String documentId;
|
|
|
|
/* The time this entry was created. */
|
|
long time;
|
|
|
|
public
|
|
DocIdEntry ()
|
|
{
|
|
time = SystemClock.uptimeMillis ();
|
|
}
|
|
|
|
/* Return a cache entry comprised of the state of the file
|
|
identified by `documentId'. TREE is the URI of the tree
|
|
containing this entry, and TOPLEVEL is the toplevel
|
|
representing it. SIGNAL is a cancellation signal.
|
|
|
|
RESOLVER is the content provider used to retrieve file
|
|
information.
|
|
|
|
Value is NULL if the file cannot be found. */
|
|
|
|
public CacheEntry
|
|
getCacheEntry (ContentResolver resolver, Uri tree,
|
|
CacheToplevel toplevel,
|
|
CancellationSignal signal)
|
|
{
|
|
Uri uri;
|
|
String[] projection;
|
|
String type;
|
|
Cursor cursor;
|
|
int column;
|
|
CacheEntry entry;
|
|
|
|
/* Create a document URI representing DOCUMENTID within URI's
|
|
authority. */
|
|
|
|
uri = DocumentsContract.buildDocumentUriUsingTree (tree,
|
|
documentId);
|
|
projection = new String[] {
|
|
Document.COLUMN_MIME_TYPE,
|
|
};
|
|
|
|
cursor = null;
|
|
|
|
try
|
|
{
|
|
cursor = resolver.query (uri, projection, null,
|
|
null, null, signal);
|
|
|
|
if (!cursor.moveToFirst ())
|
|
return null;
|
|
|
|
column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
|
|
|
|
if (column < 0)
|
|
return null;
|
|
|
|
type = cursor.getString (column);
|
|
|
|
if (type == null)
|
|
return null;
|
|
|
|
entry = new CacheEntry ();
|
|
entry.type = type;
|
|
toplevel.idCache.put (documentId, entry);
|
|
return entry;
|
|
}
|
|
catch (OperationCanceledException e)
|
|
{
|
|
throw e;
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
if (cursor != null)
|
|
cursor.close ();
|
|
}
|
|
}
|
|
|
|
public boolean
|
|
isValid ()
|
|
{
|
|
return ((SystemClock.uptimeMillis () - time)
|
|
< CACHE_INVALID_TIME * 1000);
|
|
}
|
|
};
|
|
|
|
private static final class CacheEntry
|
|
{
|
|
/* The type of this document. */
|
|
String type;
|
|
|
|
/* Map between document names and children. */
|
|
HashMap<String, DocIdEntry> children;
|
|
|
|
/* The time this entry was created. */
|
|
long time;
|
|
|
|
public
|
|
CacheEntry ()
|
|
{
|
|
children = new HashMap<String, DocIdEntry> ();
|
|
time = SystemClock.uptimeMillis ();
|
|
}
|
|
|
|
public boolean
|
|
isValid ()
|
|
{
|
|
return ((SystemClock.uptimeMillis () - time)
|
|
< CACHE_INVALID_TIME * 1000);
|
|
}
|
|
};
|
|
|
|
/* Create or return a toplevel for the given tree URI. */
|
|
|
|
private CacheToplevel
|
|
getCache (Uri uri)
|
|
{
|
|
CacheToplevel toplevel;
|
|
|
|
toplevel = cacheToplevels.get (uri);
|
|
|
|
if (toplevel != null)
|
|
return toplevel;
|
|
|
|
toplevel = new CacheToplevel ();
|
|
toplevel.children = new HashMap<String, DocIdEntry> ();
|
|
toplevel.statCache = new HashMap<String, StatCacheEntry> ();
|
|
toplevel.idCache = new HashMap<String, CacheEntry> ();
|
|
cacheToplevels.put (uri, toplevel);
|
|
return toplevel;
|
|
}
|
|
|
|
/* Remove each cache entry within COLLECTION older than
|
|
CACHE_INVALID_TIME. */
|
|
|
|
private void
|
|
pruneCache1 (Collection<DocIdEntry> collection)
|
|
{
|
|
Iterator<DocIdEntry> iter;
|
|
DocIdEntry tem;
|
|
|
|
iter = collection.iterator ();
|
|
while (iter.hasNext ())
|
|
{
|
|
/* Get the cache entry. */
|
|
tem = iter.next ();
|
|
|
|
/* If it's not valid anymore, remove it. Iterating over a
|
|
collection whose contents are being removed is undefined
|
|
unless the removal is performed using the iterator's own
|
|
`remove' function, so tem.remove cannot be used here. */
|
|
|
|
if (tem.isValid ())
|
|
continue;
|
|
|
|
iter.remove ();
|
|
}
|
|
}
|
|
|
|
/* Remove every entry older than CACHE_INVALID_TIME from each
|
|
toplevel inside `cachedToplevels'. */
|
|
|
|
private void
|
|
pruneCache ()
|
|
{
|
|
Iterator<CacheEntry> iter;
|
|
Iterator<StatCacheEntry> statIter;
|
|
CacheEntry tem;
|
|
StatCacheEntry stat;
|
|
|
|
for (CacheToplevel toplevel : cacheToplevels.values ())
|
|
{
|
|
/* First, clean up expired cache entries. */
|
|
iter = toplevel.idCache.values ().iterator ();
|
|
|
|
while (iter.hasNext ())
|
|
{
|
|
/* Get the cache entry. */
|
|
tem = iter.next ();
|
|
|
|
/* If it's not valid anymore, remove it. Iterating over a
|
|
collection whose contents are being removed is
|
|
undefined unless the removal is performed using the
|
|
iterator's own `remove' function, so tem.remove cannot
|
|
be used here. */
|
|
|
|
if (tem.isValid ())
|
|
{
|
|
/* Otherwise, clean up expired items in its document
|
|
ID cache. */
|
|
pruneCache1 (tem.children.values ());
|
|
continue;
|
|
}
|
|
|
|
iter.remove ();
|
|
}
|
|
|
|
statIter = toplevel.statCache.values ().iterator ();
|
|
|
|
while (statIter.hasNext ())
|
|
{
|
|
/* Get the cache entry. */
|
|
stat = statIter.next ();
|
|
|
|
/* If it's not valid anymore, remove it. Iterating over a
|
|
collection whose contents are being removed is
|
|
undefined unless the removal is performed using the
|
|
iterator's own `remove' function, so tem.remove cannot
|
|
be used here. */
|
|
|
|
if (stat.isValid ())
|
|
continue;
|
|
|
|
statIter.remove ();
|
|
}
|
|
}
|
|
|
|
postPruneMessage ();
|
|
}
|
|
|
|
/* Cache file information within TOPLEVEL, under the list of
|
|
children CHILDREN.
|
|
|
|
NAME, ID, and TYPE should respectively be the display name of the
|
|
document within its parent document (the CacheEntry whose
|
|
`children' field is CHILDREN), its document ID, and its MIME
|
|
type.
|
|
|
|
If ID_ENTRY_EXISTS, don't create a new document ID entry within
|
|
CHILDREN indexed by NAME.
|
|
|
|
Value is the cache entry saved for the document ID. */
|
|
|
|
private CacheEntry
|
|
cacheChild (CacheToplevel toplevel,
|
|
HashMap<String, DocIdEntry> children,
|
|
String name, String id, String type,
|
|
boolean id_entry_exists)
|
|
{
|
|
DocIdEntry idEntry;
|
|
CacheEntry cacheEntry;
|
|
|
|
if (!id_entry_exists)
|
|
{
|
|
idEntry = new DocIdEntry ();
|
|
idEntry.documentId = id;
|
|
children.put (name, idEntry);
|
|
}
|
|
|
|
cacheEntry = new CacheEntry ();
|
|
cacheEntry.type = type;
|
|
toplevel.idCache.put (id, cacheEntry);
|
|
return cacheEntry;
|
|
}
|
|
|
|
/* Cache file status for DOCUMENTID within TOPLEVEL. Value is the
|
|
new cache entry. CURSOR is the cursor from where to retrieve the
|
|
file status, in the form of the columns COLUMN_FLAGS,
|
|
COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED.
|
|
|
|
If NO_CACHE, don't cache the file status; just return the
|
|
entry. */
|
|
|
|
private StatCacheEntry
|
|
cacheFileStatus (String documentId, CacheToplevel toplevel,
|
|
Cursor cursor, boolean no_cache)
|
|
{
|
|
StatCacheEntry entry;
|
|
int flagsIndex, columnIndex, typeIndex;
|
|
int sizeIndex, mtimeIndex;
|
|
String type;
|
|
|
|
/* Obtain the indices for columns wanted from this cursor. */
|
|
flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
|
|
sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
|
|
typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
|
|
mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
|
|
|
|
/* COLUMN_LAST_MODIFIED is allowed to be absent in a
|
|
conforming documents provider. */
|
|
if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
|
|
return null;
|
|
|
|
/* Get the file status from CURSOR. */
|
|
entry = new StatCacheEntry ();
|
|
entry.flags = cursor.getInt (flagsIndex);
|
|
type = cursor.getString (typeIndex);
|
|
|
|
if (type == null)
|
|
return null;
|
|
|
|
entry.isDirectory = type.equals (Document.MIME_TYPE_DIR);
|
|
|
|
if (cursor.isNull (sizeIndex))
|
|
/* The size is unknown. */
|
|
entry.size = -1;
|
|
else
|
|
entry.size = cursor.getLong (sizeIndex);
|
|
|
|
/* mtimeIndex is potentially unset, since document providers
|
|
aren't obligated to provide modification times. */
|
|
|
|
if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
|
|
entry.mtime = cursor.getLong (mtimeIndex);
|
|
|
|
/* Finally, add this entry to the cache and return. */
|
|
if (!no_cache)
|
|
toplevel.statCache.put (documentId, entry);
|
|
return entry;
|
|
}
|
|
|
|
/* Cache the type and as many of the children of the directory
|
|
designated by DOCUMENTID as possible into TOPLEVEL.
|
|
|
|
CURSOR should be a cursor representing an open directory stream,
|
|
with its projection consisting of at least the display name,
|
|
document ID and MIME type columns.
|
|
|
|
Rewind the position of CURSOR to before its first element after
|
|
completion. */
|
|
|
|
private void
|
|
cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId,
|
|
Cursor cursor)
|
|
{
|
|
CacheEntry entry, constituent;
|
|
int nameColumn, idColumn, typeColumn;
|
|
String id, name, type;
|
|
DocIdEntry idEntry;
|
|
|
|
/* Find the numbers of the columns wanted. */
|
|
|
|
nameColumn
|
|
= cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
|
|
idColumn
|
|
= cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
|
|
typeColumn
|
|
= cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
|
|
|
|
if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
|
|
return;
|
|
|
|
entry = new CacheEntry ();
|
|
|
|
/* We know this is a directory already. */
|
|
entry.type = Document.MIME_TYPE_DIR;
|
|
toplevel.idCache.put (documentId, entry);
|
|
|
|
/* Now, try to cache each of its constituents. */
|
|
|
|
while (cursor.moveToNext ())
|
|
{
|
|
try
|
|
{
|
|
name = cursor.getString (nameColumn);
|
|
id = cursor.getString (idColumn);
|
|
type = cursor.getString (typeColumn);
|
|
|
|
if (name == null || id == null || type == null)
|
|
continue;
|
|
|
|
/* First, add the name and ID to ENTRY's map of
|
|
children. */
|
|
idEntry = new DocIdEntry ();
|
|
idEntry.documentId = id;
|
|
entry.children.put (id, idEntry);
|
|
|
|
/* Cache the file status for ID within TOPELVEL too; if a
|
|
directory listing is being requested, it's very likely
|
|
that a series of calls for file status will follow. */
|
|
|
|
cacheFileStatus (id, toplevel, cursor, false);
|
|
|
|
/* If this constituent is a directory, don't cache any
|
|
information about it. It cannot be cached without
|
|
knowing its children. */
|
|
|
|
if (type.equals (Document.MIME_TYPE_DIR))
|
|
continue;
|
|
|
|
/* Otherwise, create a new cache entry comprised of its
|
|
type. */
|
|
constituent = new CacheEntry ();
|
|
constituent.type = type;
|
|
toplevel.idCache.put (documentId, entry);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
e.printStackTrace ();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* Rewind cursor back to the beginning. */
|
|
cursor.moveToPosition (-1);
|
|
}
|
|
|
|
/* Post a message to run `pruneCache' every CACHE_PRUNE_TIME
|
|
seconds. */
|
|
|
|
private void
|
|
postPruneMessage ()
|
|
{
|
|
handler.postDelayed (new Runnable () {
|
|
@Override
|
|
public void
|
|
run ()
|
|
{
|
|
pruneCache ();
|
|
}
|
|
}, CACHE_PRUNE_TIME * 1000);
|
|
}
|
|
|
|
/* Invalidate the cache entry denoted by DOCUMENT_ID, within the
|
|
document tree URI.
|
|
Call this after deleting a document or directory.
|
|
|
|
At the same time, remove the final component within the file name
|
|
CACHENAME from the cache if it exists. */
|
|
|
|
public void
|
|
postInvalidateCache (final Uri uri, final String documentId,
|
|
final String cacheName)
|
|
{
|
|
handler.post (new Runnable () {
|
|
@Override
|
|
public void
|
|
run ()
|
|
{
|
|
CacheToplevel toplevel;
|
|
HashMap<String, DocIdEntry> children;
|
|
String[] components;
|
|
CacheEntry entry;
|
|
DocIdEntry idEntry;
|
|
|
|
toplevel = getCache (uri);
|
|
toplevel.idCache.remove (documentId);
|
|
toplevel.statCache.remove (documentId);
|
|
|
|
/* If the parent of CACHENAME is cached, remove it. */
|
|
|
|
children = toplevel.children;
|
|
components = cacheName.split ("/");
|
|
|
|
for (String component : components)
|
|
{
|
|
/* Java `split' removes trailing empty matches but not
|
|
leading or intermediary ones. */
|
|
if (component.isEmpty ())
|
|
continue;
|
|
|
|
if (component == components[components.length - 1])
|
|
{
|
|
/* This is the last component, so remove it from
|
|
children. */
|
|
children.remove (component);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
/* Search for this component within the last level
|
|
of the cache. */
|
|
|
|
idEntry = children.get (component);
|
|
|
|
if (idEntry == null)
|
|
/* Not cached, so return. */
|
|
return;
|
|
|
|
entry = toplevel.idCache.get (idEntry.documentId);
|
|
|
|
if (entry == null)
|
|
/* Not cached, so return. */
|
|
return;
|
|
|
|
/* Locate the next component within this
|
|
directory. */
|
|
children = entry.children;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Invalidate the cache entry denoted by DOCUMENT_ID, within the
|
|
document tree URI.
|
|
Call this after deleting a document or directory.
|
|
|
|
At the same time, remove the child referring to DOCUMENTID from
|
|
within CACHENAME's cache entry if it exists. */
|
|
|
|
public void
|
|
postInvalidateCacheDir (final Uri uri, final String documentId,
|
|
final String cacheName)
|
|
{
|
|
handler.post (new Runnable () {
|
|
@Override
|
|
public void
|
|
run ()
|
|
{
|
|
CacheToplevel toplevel;
|
|
HashMap<String, DocIdEntry> children;
|
|
String[] components;
|
|
CacheEntry entry;
|
|
DocIdEntry idEntry;
|
|
Iterator<DocIdEntry> iter;
|
|
|
|
toplevel = getCache (uri);
|
|
toplevel.idCache.remove (documentId);
|
|
toplevel.statCache.remove (documentId);
|
|
|
|
/* Now remove DOCUMENTID from CACHENAME's cache entry, if
|
|
any. */
|
|
|
|
children = toplevel.children;
|
|
components = cacheName.split ("/");
|
|
|
|
for (String component : components)
|
|
{
|
|
/* Java `split' removes trailing empty matches but not
|
|
leading or intermediary ones. */
|
|
if (component.isEmpty ())
|
|
continue;
|
|
|
|
/* Search for this component within the last level
|
|
of the cache. */
|
|
|
|
idEntry = children.get (component);
|
|
|
|
if (idEntry == null)
|
|
/* Not cached, so return. */
|
|
return;
|
|
|
|
entry = toplevel.idCache.get (idEntry.documentId);
|
|
|
|
if (entry == null)
|
|
/* Not cached, so return. */
|
|
return;
|
|
|
|
/* Locate the next component within this
|
|
directory. */
|
|
children = entry.children;
|
|
}
|
|
|
|
iter = children.values ().iterator ();
|
|
while (iter.hasNext ())
|
|
{
|
|
idEntry = iter.next ();
|
|
|
|
if (idEntry.documentId.equals (documentId))
|
|
{
|
|
iter.remove ();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Invalidate the file status cache entry for DOCUMENTID within URI.
|
|
Call this when the contents of a file (i.e. the constituents of a
|
|
directory file) may have changed, but the document's display name
|
|
has not. */
|
|
|
|
public void
|
|
postInvalidateStat (final Uri uri, final String documentId)
|
|
{
|
|
handler.post (new Runnable () {
|
|
@Override
|
|
public void
|
|
run ()
|
|
{
|
|
CacheToplevel toplevel;
|
|
|
|
toplevel = getCache (uri);
|
|
toplevel.statCache.remove (documentId);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/* ``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 idiosyncrasy 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 idiosyncrasy 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, newId, newType;
|
|
String[] components, projection;
|
|
Cursor cursor;
|
|
int nameColumn, idColumn, typeColumn;
|
|
CacheToplevel toplevel;
|
|
DocIdEntry idEntry;
|
|
HashMap<String, DocIdEntry> children, next;
|
|
CacheEntry cache;
|
|
|
|
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;
|
|
|
|
/* Obtain the top level of this cache. */
|
|
toplevel = getCache (uri);
|
|
|
|
/* Set the current map of children to this top level. */
|
|
children = toplevel.children;
|
|
|
|
/* 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;
|
|
|
|
/* Search for component within the currently cached list
|
|
of children. */
|
|
|
|
idEntry = children.get (component);
|
|
|
|
if (idEntry != null)
|
|
{
|
|
/* The document ID is known. Now find the
|
|
corresponding document ID cache. */
|
|
|
|
cache = toplevel.idCache.get (idEntry.documentId);
|
|
|
|
/* Fetch just the information for this document. */
|
|
|
|
if (cache == null)
|
|
cache = idEntry.getCacheEntry (resolver, uri, toplevel,
|
|
signal);
|
|
|
|
if (cache == null)
|
|
{
|
|
/* File status matching idEntry could not be
|
|
obtained. Treat this as if the file does not
|
|
exist. */
|
|
|
|
children.remove (component);
|
|
|
|
if (id == null)
|
|
id = DocumentsContract.getTreeDocumentId (uri);
|
|
|
|
id_return[0] = id;
|
|
|
|
if ((type == null
|
|
|| type.equals (Document.MIME_TYPE_DIR))
|
|
/* ... and type and id currently represent the
|
|
penultimate component. */
|
|
&& component == components[components.length - 1])
|
|
return -2;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* Otherwise, use the cached information. */
|
|
id = idEntry.documentId;
|
|
type = cache.type;
|
|
children = cache.children;
|
|
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
|
|
+ " = ?"),
|
|
new String[] { component, },
|
|
null, signal);
|
|
|
|
if (cursor == null)
|
|
return -1;
|
|
|
|
/* Find the column numbers for each of the columns that
|
|
are wanted. */
|
|
|
|
nameColumn
|
|
= cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
|
|
idColumn
|
|
= cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
|
|
typeColumn
|
|
= cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
|
|
|
|
if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
|
|
return -1;
|
|
|
|
next = null;
|
|
|
|
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 a component has been found, break out of the
|
|
loop. */
|
|
|
|
if (next != null)
|
|
break;
|
|
|
|
/* 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. */
|
|
|
|
name = cursor.getString (nameColumn);
|
|
newId = cursor.getString (idColumn);
|
|
newType = cursor.getString (typeColumn);
|
|
|
|
/* Any of the three variables above may be NULL if the
|
|
column data is of the wrong type depending on how
|
|
the Cursor returned is implemented. */
|
|
|
|
if (name == null || newId == null || newType == null)
|
|
return -1;
|
|
|
|
/* Cache this name, even if it isn't the document
|
|
that's being searched for. */
|
|
|
|
cache = cacheChild (toplevel, children, name,
|
|
newId, newType,
|
|
idEntry != null);
|
|
|
|
/* Record the desired component once it is located,
|
|
but continue reading and caching items from the
|
|
cursor. */
|
|
|
|
if (name.equals (component))
|
|
{
|
|
id = newId;
|
|
next = cache.children;
|
|
type = newType;
|
|
}
|
|
}
|
|
|
|
children = next;
|
|
|
|
/* 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 cancellation
|
|
signal. */
|
|
|
|
private long[]
|
|
statDocument1 (String uri, String documentId,
|
|
CancellationSignal signal, boolean noCache)
|
|
{
|
|
Uri uriObject, tree;
|
|
String[] projection;
|
|
long[] stat;
|
|
Cursor cursor;
|
|
CacheToplevel toplevel;
|
|
StatCacheEntry cache;
|
|
|
|
tree = Uri.parse (uri);
|
|
|
|
if (documentId == null)
|
|
documentId = DocumentsContract.getTreeDocumentId (tree);
|
|
|
|
/* Create a document URI representing DOCUMENTID within URI's
|
|
authority. */
|
|
|
|
uriObject
|
|
= DocumentsContract.buildDocumentUriUsingTree (tree, documentId);
|
|
|
|
/* See if the file status cache currently contains this
|
|
document. */
|
|
|
|
toplevel = getCache (tree);
|
|
cache = toplevel.statCache.get (documentId);
|
|
|
|
if (cache == null || !cache.isValid ())
|
|
{
|
|
/* Stat this document and enter its information into the
|
|
cache. */
|
|
|
|
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;
|
|
|
|
try
|
|
{
|
|
if (!cursor.moveToFirst ())
|
|
return null;
|
|
|
|
cache = cacheFileStatus (documentId, toplevel, cursor,
|
|
noCache);
|
|
}
|
|
finally
|
|
{
|
|
cursor.close ();
|
|
}
|
|
|
|
/* If cache is still null, return null. */
|
|
|
|
if (cache == null)
|
|
return null;
|
|
}
|
|
|
|
/* Create the array of file status and populate it with the
|
|
information within cache. */
|
|
stat = new long[3];
|
|
|
|
stat[0] |= S_IRUSR;
|
|
if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
|
|
stat[0] |= S_IWUSR;
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
|
&& (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
|
|
stat[0] |= S_IFCHR;
|
|
|
|
stat[1] = cache.size;
|
|
|
|
/* Check if this is a directory file. */
|
|
if (cache.isDirectory
|
|
/* 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 | S_IXUSR;
|
|
|
|
/* Directory files cannot be modified if
|
|
FLAG_DIR_SUPPORTS_CREATE is not set. */
|
|
|
|
if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
|
|
stat[0] &= ~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;
|
|
|
|
stat[2] = cache.mtime;
|
|
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.
|
|
|
|
If NOCACHE, refrain from placing the file status within the
|
|
status cache.
|
|
|
|
OperationCanceledException and other typical exceptions may be
|
|
signaled upon receiving async input or other errors. */
|
|
|
|
public long[]
|
|
statDocument (final String uri, final String documentId,
|
|
final boolean noCache)
|
|
{
|
|
return (long[]) runObjectFunction (new SafObjectFunction () {
|
|
@Override
|
|
public Object
|
|
runObject (CancellationSignal signal)
|
|
{
|
|
return statDocument1 (uri, documentId, signal, noCache);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* 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;
|
|
CacheToplevel toplevel;
|
|
CacheEntry entry;
|
|
|
|
uriObject = Uri.parse (uri);
|
|
|
|
if (documentId == null)
|
|
documentId = DocumentsContract.getTreeDocumentId (uriObject);
|
|
|
|
/* If WRITABLE is false and the document ID is cached, use its
|
|
cached value instead. This speeds up
|
|
`directory-files-with-attributes' a little. */
|
|
|
|
if (!writable)
|
|
{
|
|
toplevel = getCache (uriObject);
|
|
entry = toplevel.idCache.get (documentId);
|
|
|
|
if (entry != null)
|
|
return 0;
|
|
}
|
|
|
|
/* 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. */
|
|
|
|
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, tree;
|
|
Cursor cursor;
|
|
String projection[];
|
|
CacheToplevel toplevel;
|
|
|
|
tree = 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_DOCUMENT_ID,
|
|
Document.COLUMN_MIME_TYPE,
|
|
Document.COLUMN_FLAGS,
|
|
Document.COLUMN_LAST_MODIFIED,
|
|
Document.COLUMN_SIZE,
|
|
};
|
|
|
|
cursor = resolver.query (uriObject, projection, null, null,
|
|
null, signal);
|
|
|
|
/* Create a new cache entry tied to this document ID. */
|
|
|
|
if (cursor != null)
|
|
{
|
|
toplevel = getCache (tree);
|
|
cacheDirectoryFromCursor (toplevel, documentId,
|
|
cursor);
|
|
}
|
|
|
|
/* 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 read,
|
|
boolean write, boolean truncate,
|
|
CancellationSignal signal)
|
|
throws Throwable
|
|
{
|
|
Uri treeUri, documentUri;
|
|
String mode;
|
|
ParcelFileDescriptor fileDescriptor;
|
|
CacheToplevel toplevel;
|
|
|
|
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);
|
|
|
|
/* Select the mode used to open the file. */
|
|
|
|
if (write)
|
|
{
|
|
if (read)
|
|
{
|
|
if (truncate)
|
|
mode = "rwt";
|
|
else
|
|
mode = "rw";
|
|
}
|
|
else
|
|
/* Set mode to w when WRITE && !READ, disregarding TRUNCATE.
|
|
In contradiction with the ContentResolver documentation,
|
|
document providers seem to truncate files whenever w is
|
|
specified, at least superficially. (But see below.) */
|
|
mode = "w";
|
|
}
|
|
else
|
|
mode = "r";
|
|
|
|
fileDescriptor
|
|
= resolver.openFileDescriptor (documentUri, mode,
|
|
signal);
|
|
|
|
/* If a writable on-disk file descriptor is requested and TRUNCATE
|
|
is set, then probe the file descriptor to detect if it is
|
|
actually readable. If not, close this file descriptor and
|
|
reopen it with MODE set to rw; some document providers granting
|
|
access to Samba shares don't implement rwt, but these document
|
|
providers invariably truncate the file opened even when the
|
|
mode is merely w.
|
|
|
|
This may be ascribed to a mix-up in Android's documentation
|
|
regarding DocumentsProvider: the `openDocument' function is only
|
|
documented to accept r or rw, whereas the default implementation
|
|
of the `openFile' function (which documents rwt) delegates to
|
|
`openDocument'. */
|
|
|
|
if (read && write && truncate && fileDescriptor != null
|
|
&& !EmacsNative.ftruncate (fileDescriptor.getFd ()))
|
|
{
|
|
try
|
|
{
|
|
fileDescriptor.closeWithError ("File descriptor requested"
|
|
+ " is not writable");
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
Log.w (TAG, "Leaking unclosed file descriptor " + e);
|
|
}
|
|
|
|
fileDescriptor
|
|
= resolver.openFileDescriptor (documentUri, "rw", signal);
|
|
|
|
/* Try to truncate fileDescriptor just to stay on the safe
|
|
side. */
|
|
if (fileDescriptor != null)
|
|
EmacsNative.ftruncate (fileDescriptor.getFd ());
|
|
}
|
|
else if (!read && write && truncate && fileDescriptor != null)
|
|
/* Moreover, document providers that return actual seekable
|
|
files characteristically neglect to truncate the file
|
|
returned when the access mode is merely w, so attempt to
|
|
truncate it by hand. */
|
|
EmacsNative.ftruncate (fileDescriptor.getFd ());
|
|
|
|
/* Every time a document is opened, remove it from the file status
|
|
cache. */
|
|
toplevel = getCache (treeUri);
|
|
toplevel.statCache.remove (documentId);
|
|
|
|
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.
|
|
|
|
If READ && WRITE, open the file under either the `rw' or `rwt'
|
|
access mode, which implies that the value must be a seekable
|
|
on-disk file. If WRITE && !READ or TRUNC && WRITE, also truncate
|
|
the file after it is opened.
|
|
|
|
If only READ or WRITE is set, value may be a non-seekable FIFO or
|
|
one end of a socket pair.
|
|
|
|
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 read, final boolean write,
|
|
final boolean truncate)
|
|
{
|
|
Object tem;
|
|
|
|
tem = runObjectFunction (new SafObjectFunction () {
|
|
@Override
|
|
public Object
|
|
runObject (CancellationSignal signal)
|
|
throws Throwable
|
|
{
|
|
return openDocument1 (uri, documentId, read,
|
|
write, truncate, signal);
|
|
}
|
|
});
|
|
|
|
return (ParcelFileDescriptor) tem;
|
|
}
|
|
};
|