Update Android port

* java/org/gnu/emacs/EmacsSafThread.java (EmacsSafThread, getCache)
(pruneCache1, pruneCache, cacheChild, cacheDirectoryFromCursor)
(documentIdFromName1, openDocumentDirectory1): Implement the
cache referred to by the commentary.
* java/org/gnu/emacs/EmacsService.java (deleteDocument):
Invalidate the cache upon document removal.
* src/androidvfs.c (android_saf_exception_check)
(android_document_id_from_name): Correctly preserve or set errno
in error cases.
This commit is contained in:
Po Lu 2023-07-29 11:24:07 +08:00
parent 25ef0c3a89
commit 47f97b5ae4
3 changed files with 544 additions and 81 deletions

View file

@ -1,24 +1,30 @@
/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
Copyright (C) 2023 Free Software Foundation, Inc.
Copyright (C) 2023 Free Software Foundation, Inc.
This file is part of GNU Emacs.
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 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.
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/>. */
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 android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
@ -29,6 +35,8 @@
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
@ -38,7 +46,6 @@
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
@ -53,13 +60,10 @@
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
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
@ -76,9 +80,15 @@
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;
@ -89,11 +99,20 @@ public final class EmacsSafThread extends HandlerThread
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> ();
}
@ -106,6 +125,376 @@ public final class EmacsSafThread extends HandlerThread
/* Set up the handler after the thread starts. */
handler = new Handler (getLooper ());
/* And start periodically pruning the cache. */
postPruneMessage ();
}
private final class CacheToplevel
{
/* Map between document names and children. */
HashMap<String, DocIdEntry> children;
/* Map between document IDs and cache items. */
HashMap<String, CacheEntry> idCache;
};
private final class DocIdEntry
{
/* The document ID. */
String documentId;
/* The time this entry was created. */
long time;
public
DocIdEntry ()
{
time = System.currentTimeMillis ();
}
/* 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.
Value is NULL if the file cannot be found. */
public CacheEntry
getCacheEntry (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 (Throwable e)
{
if (e instanceof FileNotFoundException)
return null;
throw e;
}
finally
{
if (cursor != null)
cursor.close ();
}
}
public boolean
isValid ()
{
return ((System.currentTimeMillis () - time)
< CACHE_INVALID_TIME);
}
};
private 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 = System.currentTimeMillis ();
}
public boolean
isValid ()
{
return ((System.currentTimeMillis () - time)
< CACHE_INVALID_TIME);
}
};
/* 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.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;
CacheEntry tem;
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 ();
}
}
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 the type and as many of the children of the directory
designated by DOC_ID 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, constitutent;
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);
/* 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. */
constitutent = new CacheEntry ();
constitutent.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.
Caveat emptor: this does not remove the component name cache
entries linked to the name(s) of the directory being removed, the
assumption being that the next time `documentIdFromName1' is
called, it will notice that the document is missing and remove
the outdated cache entry. */
public void
postInvalidateCache (final Uri uri, final String documentId)
{
handler.post (new Runnable () {
@Override
public void
run ()
{
CacheToplevel toplevel;
toplevel = getCache (uri);
toplevel.idCache.remove (documentId);
}
});
}
@ -289,10 +678,14 @@ public abstract Object runObject (CancellationSignal signal)
String[] id_return, CancellationSignal signal)
{
Uri uri, treeUri;
String id, type;
String id, type, newId, newType;
String[] components, projection;
Cursor cursor;
int column;
int nameColumn, idColumn, typeColumn;
CacheToplevel toplevel;
DocIdEntry idEntry;
HashMap<String, DocIdEntry> children, next;
CacheEntry cache;
projection = new String[] {
Document.COLUMN_DISPLAY_NAME,
@ -310,6 +703,12 @@ public abstract Object runObject (CancellationSignal signal)
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
@ -321,6 +720,48 @@ public abstract Object runObject (CancellationSignal signal)
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 (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 (idEntry);
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. */
@ -342,6 +783,21 @@ public abstract Object runObject (CancellationSignal 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
@ -350,6 +806,12 @@ public abstract Object runObject (CancellationSignal signal)
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
@ -382,51 +844,37 @@ public abstract Object runObject (CancellationSignal signal)
/* So move CURSOR to a row with the right display
name. */
column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
name = cursor.getString (nameColumn);
newId = cursor.getString (idColumn);
newType = cursor.getString (typeColumn);
if (column < 0)
continue;
/* 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. */
name = cursor.getString (column);
if (name == null || newId == null || newType == null)
return -1;
/* Break out of the loop only once a matching
component is found. */
/* 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))
break;
{
id = newId;
next = cache.children;
type = newType;
}
}
/* 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;
}
children = next;
/* Now close the cursor. */
cursor.close ();
@ -771,11 +1219,12 @@ In addition, arbitrary runtime exceptions (such as
openDocumentDirectory1 (String uri, String documentId,
CancellationSignal signal)
{
Uri uriObject;
Uri uriObject, tree;
Cursor cursor;
String projection[];
CacheToplevel toplevel;
uriObject = Uri.parse (uri);
tree = uriObject = Uri.parse (uri);
/* If documentId is not set, use the document ID of the tree URI
itself. */
@ -792,11 +1241,22 @@ In addition, arbitrary runtime exceptions (such as
projection = new String [] {
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
};
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;
}

View file

@ -1680,14 +1680,18 @@ In addition, arbitrary runtime exceptions (such as
deleteDocument (String uri, String id)
throws FileNotFoundException
{
Uri uriObject;
Uri uriObject, tree;
uriObject = Uri.parse (uri);
uriObject = DocumentsContract.buildDocumentUriUsingTree (uriObject,
id);
tree = Uri.parse (uri);
uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id);
if (DocumentsContract.deleteDocument (resolver, uriObject))
return 0;
{
if (storageThread != null)
storageThread.postInvalidateCache (tree, id);
return 0;
}
return -1;
}

View file

@ -3728,6 +3728,7 @@ android_saf_exception_check (int n, ...)
jthrowable exception;
JNIEnv *env;
va_list ap;
int new_errno;
env = android_java_env;
va_start (ap, n);
@ -3744,8 +3745,9 @@ android_saf_exception_check (int n, ...)
/* JNI couldn't return a local reference to the exception. */
memory_full (0);
/* Clear the exception, making it safe to subsequently call other
JNI functions. */
/* Print and clear the exception, making it safe to subsequently
call other JNI functions. */
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
/* Delete each of the N arguments. */
@ -3760,16 +3762,16 @@ android_saf_exception_check (int n, ...)
if ((*env)->IsInstanceOf (env, (jobject) exception,
file_not_found_exception))
errno = ENOENT;
new_errno = ENOENT;
else if ((*env)->IsInstanceOf (env, (jobject) exception,
security_exception))
errno = EACCES;
new_errno = EACCES;
else if ((*env)->IsInstanceOf (env, (jobject) exception,
operation_canceled_exception))
errno = EINTR;
new_errno = EINTR;
else if ((*env)->IsInstanceOf (env, (jobject) exception,
unsupported_operation_exception))
errno = ENOSYS;
new_errno = ENOSYS;
else if ((*env)->IsInstanceOf (env, (jobject) exception,
out_of_memory_error))
{
@ -3777,10 +3779,11 @@ android_saf_exception_check (int n, ...)
memory_full (0);
}
else
errno = EIO;
new_errno = EIO;
/* expression is still a local reference! */
ANDROID_DELETE_LOCAL_REF ((jobject) exception);
errno = new_errno;
return 1;
}
@ -4238,10 +4241,7 @@ android_document_id_from_name (const char *tree_uri, char *name,
inside_saf_critical_section = false;
if (android_saf_exception_check (3, result, uri, java_name))
{
rc = -1;
goto finish;
}
return -1;
ANDROID_DELETE_LOCAL_REF (uri);
ANDROID_DELETE_LOCAL_REF (java_name);
@ -4251,7 +4251,8 @@ android_document_id_from_name (const char *tree_uri, char *name,
if (rc == -1)
{
ANDROID_DELETE_LOCAL_REF (result);
goto finish;
errno = ENOENT;
return -1;
}
eassert (rc == -2 || rc >= 0);
@ -4274,8 +4275,6 @@ android_document_id_from_name (const char *tree_uri, char *name,
(*android_java_env)->ReleaseStringUTFChars (android_java_env,
(jstring) uri, doc_id);
ANDROID_DELETE_LOCAL_REF (uri);
finish:
return rc;
}