From 5a8130ab967cb296d028539d10c749ee35f62e6a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 31 Jul 2023 10:50:12 +0800 Subject: [PATCH] Implement cross-directory SAF rename operations * java/org/gnu/emacs/EmacsService.java (renameDocument): Don't catch UnsupportedOperationException; handle ENOSYS in android_saf_rename_document instead. (moveDocument): New function. * lisp/subr.el (y-or-n-p): Always change the text conversion style. * src/android.c (android_init_emacs_service) (android_exception_check_4): New function. * src/android.h: Update Java function table. * src/androidvfs.c (android_saf_rename_document): Handle ENOSYS here by setting errno to EXDEV. (android_saf_move_document): New function. (android_document_id_from_name): Take const `dir_name'. (android_saf_tree_rename): Use delete-move-rename to implement cross-directory renames. --- java/org/gnu/emacs/EmacsService.java | 67 ++++++-- lisp/subr.el | 9 +- src/android.c | 27 +++ src/android.h | 2 + src/androidvfs.c | 237 ++++++++++++++++++++++++++- 5 files changed, 314 insertions(+), 28 deletions(-) diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 07e585ad37c..e714f75fdf2 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1713,26 +1713,59 @@ public final class EmacsService extends Service 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) + if (DocumentsContract.renameDocument (resolver, uriObject, + name) + != null) { - ;; + /* Invalidate the cache. */ + if (storageThread != null) + storageThread.postInvalidateCacheDir (tree, docId, + name); + return 0; } - /* Handle unsupported operation exceptions specially, so - `android_rename' can return ENXDEV. */ + /* Handle errors specially, so `android_saf_rename_document' can + return ENXDEV. */ return -1; } + + /* Move the document designated by DOCID from the directory under + DIR_NAME designated by SRCID to the directory designated by + DSTID. If the ID of the document being moved changes as a + consequence of the movement, return the new ID, else NULL. + + URI is the document tree containing all three documents. */ + + public String + moveDocument (String uri, String docId, String dirName, + String dstId, String srcId) + throws FileNotFoundException + { + Uri uri1, docId1, dstId1, srcId1; + Uri name; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + throw new UnsupportedOperationException ("Documents aren't capable" + + " of being moved on Android" + + " versions before 7.0."); + + uri1 = Uri.parse (uri); + docId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, docId); + dstId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, dstId); + srcId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, srcId); + + /* Move the document; this function returns the new ID of the + document should it change. */ + name = DocumentsContract.moveDocument (resolver, docId1, + srcId1, dstId1); + + /* Now invalidate the caches for both DIRNAME and DOCID. */ + + if (storageThread != null) + storageThread.postInvalidateCacheDir (uri1, docId, dirName); + + return (name != null + ? DocumentsContract.getDocumentId (name) + : null); + } }; diff --git a/lisp/subr.el b/lisp/subr.el index 4346f99fa38..36aeeabea47 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -3796,11 +3796,10 @@ like) while `y-or-n-p' is running)." ;; Protect this-command when called from pre-command-hook (bug#45029) (this-command this-command) (str (progn - (when (active-minibuffer-window) - ;; If the minibuffer is already active, the - ;; selected window might not change. Disable - ;; text conversion by hand. - (set-text-conversion-style text-conversion-style)) + ;; If the minibuffer is already active, the + ;; selected window might not change. Disable + ;; text conversion by hand. + (set-text-conversion-style text-conversion-style) (read-from-minibuffer prompt nil keymap nil (or y-or-n-p-history-variable t))))) diff --git a/src/android.c b/src/android.c index 0bdbc4e0c4b..8c0232a51f8 100644 --- a/src/android.c +++ b/src/android.c @@ -1586,6 +1586,10 @@ android_init_emacs_service (void) FIND_METHOD (rename_document, "renameDocument", "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;)I"); + FIND_METHOD (move_document, "moveDocument", + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)Ljava/lang/String;"); #undef FIND_METHOD } @@ -5667,6 +5671,29 @@ android_exception_check_3 (jobject object, jobject object1, memory_full (0); } +/* Like android_exception_check_3, except it takes more than three + local reference arguments. */ + +void +android_exception_check_4 (jobject object, jobject object1, + jobject object2, jobject object3) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + ANDROID_DELETE_LOCAL_REF (object1); + ANDROID_DELETE_LOCAL_REF (object2); + ANDROID_DELETE_LOCAL_REF (object3); + memory_full (0); +} + /* Check for JNI problems based on the value of OBJECT. Signal out of memory if OBJECT is NULL. OBJECT1 means the diff --git a/src/android.h b/src/android.h index 591f1a1e43c..8440fb9bc75 100644 --- a/src/android.h +++ b/src/android.h @@ -110,6 +110,7 @@ extern void android_exception_check (void); extern void android_exception_check_1 (jobject); extern void android_exception_check_2 (jobject, jobject); extern void android_exception_check_3 (jobject, jobject, jobject); +extern void android_exception_check_4 (jobject, jobject, jobject, jobject); extern void android_exception_check_nonnull (void *, jobject); extern void android_exception_check_nonnull_1 (void *, jobject, jobject); @@ -282,6 +283,7 @@ struct android_emacs_service jmethodID create_directory; jmethodID delete_document; jmethodID rename_document; + jmethodID move_document; }; extern JNIEnv *android_java_env; diff --git a/src/androidvfs.c b/src/androidvfs.c index c529e1fb30f..9acc8f2b139 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -4036,7 +4036,8 @@ android_saf_delete_document (const char *tree, const char *doc_id, } /* Declared further below. */ -static int android_document_id_from_name (const char *, char *, char **); +static int android_document_id_from_name (const char *, const 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 @@ -4078,8 +4079,17 @@ android_saf_rename_document (const char *uri, const char *doc_id, dir1, name1); /* Check for exceptions. */ + if (android_saf_exception_check (4, uri1, doc_id1, dir1, name1)) - return -1; + { + /* Substitute EXDEV for ENOSYS, so callers fall back on + delete-then-copy. */ + + if (errno == ENOSYS) + errno = EXDEV; + + return -1; + } /* Delete unused local references. */ ANDROID_DELETE_LOCAL_REF (uri1); @@ -4100,6 +4110,109 @@ android_saf_rename_document (const char *uri, const char *doc_id, return 0; } +/* Move the document designated by *DOC_ID from the directory under + DIR_NAME to the directory designated by DST_ID. All three + directories are located within the tree identified by the given + URI. + + If the document's ID changes as a result of the movement, free + *DOC_ID and store the new document ID within. + + Value is 0 upon success, -1 otherwise with errno set. */ + +static int +android_saf_move_document (const char *uri, char **doc_id, + const char *dir_name, const char *dst_id) +{ + char *src_id, *id; + jobject uri1, doc_id1, dir_name1, dst_id1, src_id1; + jstring result; + jmethodID method; + int rc; + const char *new_id; + + /* Obtain the name of the source directory. */ + src_id = NULL; + rc = android_document_id_from_name (uri, dir_name, &src_id); + + if (rc != 1) + { + /* This file is either not a directory or nonexistent. */ + xfree (src_id); + + switch (rc) + { + case 0: + errno = ENOTDIR; + return -1; + + case -1: + case -2: + errno = ENOENT; + return -1; + + default: + emacs_abort (); + } + } + + /* Build Java strings for all five arguments. */ + id = *doc_id; + uri1 = (*android_java_env)->NewStringUTF (android_java_env, uri); + android_exception_check (); + doc_id1 = (*android_java_env)->NewStringUTF (android_java_env, id); + android_exception_check_1 (uri1); + dir_name1 = (*android_java_env)->NewStringUTF (android_java_env, dir_name); + android_exception_check_2 (doc_id1, uri1); + dst_id1 = (*android_java_env)->NewStringUTF (android_java_env, dst_id); + android_exception_check_3 (dir_name1, doc_id1, uri1); + src_id1 = (*android_java_env)->NewStringUTF (android_java_env, src_id); + xfree (src_id); + android_exception_check_4 (dst_id1, dir_name1, doc_id1, uri1); + + /* Do the rename. */ + method = service_class.move_document; + result = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + method, uri1, + doc_id1, dir_name1, + dst_id1, src_id1); + if (android_saf_exception_check (5, src_id1, dst_id1, dir_name1, + doc_id1, uri1)) + { + /* Substitute EXDEV for ENOSYS, so callers fall back on + delete-then-copy. */ + + if (errno == ENOSYS) + errno = EXDEV; + + return -1; + } + + /* Delete unused local references. */ + ANDROID_DELETE_LOCAL_REF (src_id1); + ANDROID_DELETE_LOCAL_REF (dst_id1); + ANDROID_DELETE_LOCAL_REF (dir_name1); + ANDROID_DELETE_LOCAL_REF (doc_id1); + ANDROID_DELETE_LOCAL_REF (uri1); + + if (result) + { + /* The document ID changed. Free id and replace *DOC_ID with + the new ID. */ + xfree (id); + new_id = (*android_java_env)->GetStringUTFChars (android_java_env, + result, NULL); + android_exception_check_nonnull ((void *) new_id, result); + *doc_id = xstrdup (new_id); + (*android_java_env)->ReleaseStringUTFChars (android_java_env, result, + new_id); + ANDROID_DELETE_LOCAL_REF (result); + } + + return 0; +} + /* SAF directory vnode. A file within a SAF directory tree is @@ -4282,7 +4395,7 @@ android_verify_jni_string (const char *name) ID lookup to be canceled. */ static int -android_document_id_from_name (const char *tree_uri, char *name, +android_document_id_from_name (const char *tree_uri, const char *name, char **id) { jobjectArray result; @@ -4658,7 +4771,9 @@ android_saf_tree_rename (struct android_vnode *src, { char *last, *dst_last; struct android_saf_tree_vnode *vp, *vdst; - char path[PATH_MAX], *fill; + char path[PATH_MAX], path1[PATH_MAX]; + char *fill, *dst_id; + int rc; /* If dst isn't a tree, file or new vnode, return EXDEV. */ @@ -4739,8 +4854,118 @@ android_saf_tree_rename (struct android_vnode *src, directory to the other, and possibly then recreated under a new name. */ - errno = EXDEV; /* TODO */ - return -1; + /* The names of the source and destination directories will have + to be copied to path. */ + + if (last - vp->name >= PATH_MAX + || dst_last - vdst->name >= PATH_MAX) + { + errno = ENAMETOOLONG; + return -1; + } + + fill = mempcpy (path, vp->name, last - vp->name); + *fill = '\0'; + + /* If vdst doesn't already exist, its document_id field is + already the name of its parent directory. */ + + if (dst->type == ANDROID_VNODE_SAF_NEW) + { + /* First, move the document. This will update + VP->document_id if it changes. */ + + if (android_saf_move_document (vp->tree_uri, + &vp->document_id, + path, + vdst->document_id)) + return -1; + + fill = mempcpy (path, vdst->name, dst_last - vdst->name); + *fill = '\0'; + + /* Next, rename the document, if its display name differs + from that of the source. */ + + if (strcmp (dst_last + 1, last + 1) + /* By now vp->document_id is already in the destination + directory. */ + && android_saf_rename_document (vp->tree_uri, + vp->document_id, + path, + dst_last + 1)) + return -1; + + return 0; + } + + /* Retrieve the ID designating the destination document's parent + directory. */ + + fill = mempcpy (path1, vdst->name, dst_last - vdst->name); + *fill = '\0'; + + rc = android_document_id_from_name (vp->tree_uri, + path1, &dst_id); + + if (rc != 1) + { + /* This file is either not a directory or nonexistent. */ + + switch (rc) + { + case 0: + errno = ENOTDIR; + goto error; + + case -1: + /* dst_id is not set here, as the penultimate component + also couldn't be located. */ + errno = ENOENT; + return -1; + + case -2: + errno = ENOENT; + goto error; + + default: + emacs_abort (); + } + } + + /* vdst already exists, so it needs to be deleted first. */ + + if (android_saf_delete_document (vdst->tree_uri, + vdst->document_id, + vdst->name)) + goto error; + + /* First, move the document. This will update + VP->document_id if it changes. */ + + if (android_saf_move_document (vp->tree_uri, + &vp->document_id, + path, dst_id)) + goto error; + + /* Next, rename the document, if its display name differs from + that of the source. */ + + if (strcmp (dst_last + 1, last + 1) + /* By now vp->document_id is already in the destination + directory. */ + && android_saf_rename_document (vp->tree_uri, + vp->document_id, + path1, + dst_last + 1)) + goto error; + + xfree (dst_id); + return 0; + + error: + xfree (dst_id); + return 1; } /* Otherwise, do this simple rename. The name of the parent -- 2.39.2