Partially implement rename operations on SAF files

* java/org/gnu/emacs/EmacsSafThread.java
(postInvalidateCacheDir):
* java/org/gnu/emacs/EmacsService.java (renameDocument): New
functions.
* src/android.c (android_init_emacs_service):
* src/android.h (struct android_emacs_service): Link to new JNI
function.
* src/androidvfs.c (android_saf_rename_document): New function.
(android_saf_tree_rename): Implement in terms of that function
if possible.
This commit is contained in:
Po Lu 2023-07-30 13:39:27 +08:00
parent 7ce7a004f6
commit 37f68e8696
5 changed files with 293 additions and 5 deletions

View file

@ -134,7 +134,7 @@ public final class EmacsSafThread extends HandlerThread
}
private final class CacheToplevel
private static final class CacheToplevel
{
/* Map between document names and children. */
HashMap<String, DocIdEntry> children;
@ -232,7 +232,7 @@ private final class DocIdEntry
}
};
private final class CacheEntry
private static final class CacheEntry
{
/* The type of this document. */
String type;
@ -545,6 +545,80 @@ private final class CacheEntry
});
}
/* 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);
/* 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;
}
}
}
});
}
/* ``Prototypes'' for nested functions that are run within the SAF

View file

@ -1698,4 +1698,41 @@ In addition, arbitrary runtime exceptions (such as
return -1;
}
/* Rename the document designated by DOCID inside the directory tree
identified by URI, which should be within the directory
designated by DIR, to NAME. If the file can't be renamed because
it doesn't support renaming, return -1, 0 otherwise. */
public int
renameDocument (String uri, String docId, String dir, String name)
throws FileNotFoundException
{
Uri tree, uriObject;
tree = Uri.parse (uri);
uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId);
try
{
if (DocumentsContract.renameDocument (resolver, uriObject,
name)
!= null)
{
/* Invalidate the cache. */
if (storageThread != null)
storageThread.postInvalidateCacheDir (tree, docId,
name);
return 0;
}
}
catch (UnsupportedOperationException e)
{
;;
}
/* Handle unsupported operation exceptions specially, so
`android_rename' can return ENXDEV. */
return -1;
}
};

View file

@ -1583,6 +1583,9 @@ android_init_emacs_service (void)
FIND_METHOD (delete_document, "deleteDocument",
"(Ljava/lang/String;Ljava/lang/String;"
"Ljava/lang/String;)I");
FIND_METHOD (rename_document, "renameDocument",
"(Ljava/lang/String;Ljava/lang/String;"
"Ljava/lang/String;Ljava/lang/String;)I");
#undef FIND_METHOD
}

View file

@ -281,6 +281,7 @@ struct android_emacs_service
jmethodID create_document;
jmethodID create_directory;
jmethodID delete_document;
jmethodID rename_document;
};
extern JNIEnv *android_java_env;

View file

@ -4035,6 +4035,71 @@ android_saf_delete_document (const char *tree, const char *doc_id,
return 0;
}
/* Declared further below. */
static int android_document_id_from_name (const char *, char *, char **);
/* Rename the document designated by DOC_ID inside the directory tree
identified by URI, which should be within the directory by the name
of DIR, to NAME. If the document can't be renamed, return -1 and
set errno to a value describing the error. Return 0 if the rename
is successful.
Android permits the same document to appear in multiple
directories, but stores the display name inside the document
``inode'' itself instead of the directory entries that refer to it.
Because of this, this operation may cause other directory entries
outside DIR to be renamed. */
static int
android_saf_rename_document (const char *uri, const char *doc_id,
const char *dir, const char *name)
{
int rc;
jstring uri1, doc_id1, dir1, name1;
jmethodID method;
/* Now build the strings for the URI, document ID, directory name
and directory ID. */
uri1 = (*android_java_env)->NewStringUTF (android_java_env, uri);
android_exception_check ();
doc_id1 = (*android_java_env)->NewStringUTF (android_java_env, doc_id);
android_exception_check_1 (uri1);
dir1 = (*android_java_env)->NewStringUTF (android_java_env, dir);
android_exception_check_2 (doc_id1, uri1);
name1 = (*android_java_env)->NewStringUTF (android_java_env, name);
android_exception_check_3 (dir1, doc_id1, uri1);
method = service_class.rename_document;
rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env,
emacs_service,
service_class.class,
method, uri1, doc_id1,
dir1, name1);
/* Check for exceptions. */
if (android_saf_exception_check (4, uri1, doc_id1, dir1, name1))
return -1;
/* Delete unused local references. */
ANDROID_DELETE_LOCAL_REF (uri1);
ANDROID_DELETE_LOCAL_REF (doc_id1);
ANDROID_DELETE_LOCAL_REF (dir1);
ANDROID_DELETE_LOCAL_REF (name1);
/* Then check for errors handled within the Java code. */
if (rc == -1)
{
/* UnsupportedOperationException. Trick the caller into falling
back on delete-then-copy code. */
errno = EXDEV;
return -1;
}
return 0;
}
/* SAF directory vnode. A file within a SAF directory tree is
@ -4591,9 +4656,117 @@ android_saf_tree_rename (struct android_vnode *src,
struct android_vnode *dst,
bool keep_existing)
{
/* TODO */
errno = ENOSYS;
return -1;
char *last, *dst_last;
struct android_saf_tree_vnode *vp, *vdst;
char path[PATH_MAX], *fill;
/* If dst isn't a tree, file or new vnode, return EXDEV. */
if (dst->type != ANDROID_VNODE_SAF_TREE
&& dst->type != ANDROID_VNODE_SAF_FILE
&& dst->type != ANDROID_VNODE_SAF_NEW)
{
errno = EXDEV;
return -1;
}
vp = (struct android_saf_tree_vnode *) src;
vdst = (struct android_saf_tree_vnode *) dst;
/* if vp and vdst refer to different tree URIs, return EXDEV. */
if (strcmp (vp->tree_uri, vdst->tree_uri))
{
errno = EXDEV;
return -1;
}
/* If `keep_existing' and the destination vnode designates an
existing file, return EEXIST. */
if (keep_existing && dst->type != ANDROID_VNODE_SAF_NEW)
{
errno = EEXIST;
return -1;
}
/* Unix `rename' maps to two Android content provider operations.
The first case is a simple rename, where src and dst are both
located within the same directory. Compare the file names of
both up to the component before the last. */
last = strrchr (vp->name, '/');
eassert (last != NULL);
if (last[1] == '\0')
{
if (last == vp->name)
{
/* This means the caller is trying to rename the root
directory of the tree. */
errno = EROFS;
return -1;
}
/* The name is terminated by a trailing directory separator.
Search backwards for the preceding directory separator. */
last = memrchr (vp->name, '/', last - vp->name);
eassert (last != NULL);
}
/* Find the end of the second-to-last component in vdst's name. */
dst_last = strrchr (vdst->name, '/');
eassert (dst_last != NULL);
if (dst_last[1] == '\0')
{
if (dst_last == vdst->name)
{
/* Forbid overwriting the root of the tree either. */
errno = EROFS;
return -1;
}
dst_last = memrchr (vdst->name, '/', dst_last - vdst->name);
eassert (dst_last != NULL);
}
if (dst_last - vdst->name != last - vp->name
|| memcmp (vp->name, vdst->name, last - vp->name))
{
/* The second case is where the file must be moved from one
directory to the other, and possibly then recreated under a
new name. */
errno = EXDEV; /* TODO */
return -1;
}
/* Otherwise, do this simple rename. The name of the parent
directory is required, as it provides the directory whose entries
will be modified. */
if (last - vp->name >= PATH_MAX)
{
errno = ENAMETOOLONG;
return -1;
}
/* If the destination document exists, delete it. */
if (dst->type != ANDROID_VNODE_SAF_NEW
&& android_saf_delete_document (vdst->tree_uri,
vdst->document_id,
vdst->name))
return -1;
fill = mempcpy (path, vp->name, last - vp->name);
*fill = '\0';
return android_saf_rename_document (vp->tree_uri,
vp->document_id,
path,
dst_last + 1);
}
static int