From: Po Lu Date: Thu, 6 Apr 2023 01:56:23 +0000 (+0800) Subject: Implement `yank-media' on Android X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=3b07a4b3158d024c6eb19ce0e7c67b799ae0d1fc;p=emacs.git Implement `yank-media' on Android * doc/emacs/android.texi (Android Windowing): Update selection restrictions. * java/org/gnu/emacs/EmacsClipboard.java (EmacsClipboard): New functions `getClipboardTargets' and `getClipboardData'. * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard, getClipboardTargets, getClipboardData): Implement. * java/org/gnu/emacs/EmacsSdk8Clipboard.java: Stub out new functions. * lisp/term/android-win.el (android-get-clipboard-1): Implement MIME type targets. * src/android.c (android_exception_check) (android_exception_check_1, android_exception_check_2): Fix punctuation in warning message. (android_exception_check_nonnull_1): New function. * src/android.h: Update prototypes. * src/androidselect.c (struct android_emacs_clipboard): New methods. (android_init_emacs_clipboard): Initialize new methods. (Fandroid_get_clipboard_targets, android_xfree_inside) (Fandroid_get_clipboard_data): New functions. (syms_of_androidselect): Define new subrs. --- diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 97f72e98cee..67faea7f76d 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -476,7 +476,7 @@ Android, it can also be @code{fullscreen}. Emacs does not implement all selection related features supported under the X Window System on Android. For example, only the @code{CLIPBOARD} and @code{PRIMARY} selections (@pxref{Cut and Paste}) -are supported, and plain text is the only supported data type. +are supported, and Emacs is only able to set selections to plain text. In addition, the Android system itself places certain restrictions on what selection data Emacs can access: diff --git a/java/org/gnu/emacs/EmacsClipboard.java b/java/org/gnu/emacs/EmacsClipboard.java index cd6bcebfe0e..5cd48af6e3a 100644 --- a/java/org/gnu/emacs/EmacsClipboard.java +++ b/java/org/gnu/emacs/EmacsClipboard.java @@ -31,6 +31,9 @@ public abstract class EmacsClipboard public abstract boolean clipboardExists (); public abstract byte[] getClipboard (); + public abstract byte[][] getClipboardTargets (); + public abstract long[] getClipboardData (byte[] target); + /* Create the correct kind of clipboard for this system. */ public static EmacsClipboard diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index a05184513cd..4959ec36eed 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -21,12 +21,20 @@ package org.gnu.emacs; import android.content.ClipboardManager; import android.content.Context; +import android.content.ContentResolver; import android.content.ClipData; +import android.content.ClipDescription; + +import android.content.res.AssetFileDescriptor; + +import android.net.Uri; import android.util.Log; import android.os.Build; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.UnsupportedEncodingException; /* This class implements EmacsClipboard for Android 3.0 and later @@ -40,6 +48,7 @@ public final class EmacsSdk11Clipboard extends EmacsClipboard private boolean ownsClipboard; private int clipboardChangedCount; private int monitoredClipboardChangedCount; + private ContentResolver resolver; public EmacsSdk11Clipboard () @@ -51,6 +60,11 @@ public final class EmacsSdk11Clipboard extends EmacsClipboard if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) manager.addPrimaryClipChangedListener (this); + + /* Now obtain the content resolver used to open file + descriptors. */ + + resolver = EmacsService.SERVICE.getContentResolver (); } @Override @@ -157,4 +171,120 @@ public final class EmacsSdk11Clipboard extends EmacsClipboard return null; } + + /* Return an array of targets currently provided by the + clipboard, or NULL if there are none. */ + + @Override + public byte[][] + getClipboardTargets () + { + ClipData clip; + ClipDescription description; + byte[][] typeArray; + int i; + + /* N.B. that Android calls the clipboard the ``primary clip''; it + is not related to the X primary selection. */ + clip = manager.getPrimaryClip (); + description = clip.getDescription (); + i = description.getMimeTypeCount (); + typeArray = new byte[i][i]; + + try + { + for (i = 0; i < description.getMimeTypeCount (); ++i) + typeArray[i] = description.getMimeType (i).getBytes ("UTF-8"); + } + catch (UnsupportedEncodingException exception) + { + return null; + } + + return typeArray; + } + + /* Return the clipboard data for the given target, or NULL if it + does not exist. + + Value is normally an array of three longs: the file descriptor, + the start offset of the data, and its length; length may be + AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends + from that offset to the end of the file. + + Do not use this function to open text targets; use `getClipboard' + for that instead, as it will handle selection data consisting + solely of a URI. */ + + @Override + public long[] + getClipboardData (byte[] target) + { + ClipData data; + String mimeType; + int fd; + AssetFileDescriptor assetFd; + Uri uri; + long[] value; + + /* Decode the target given by Emacs. */ + try + { + mimeType = new String (target, "UTF-8"); + } + catch (UnsupportedEncodingException exception) + { + return null; + } + + Log.d (TAG, "getClipboardData: "+ mimeType); + + /* Now obtain the clipboard data and the data corresponding to + that MIME type. */ + + data = manager.getPrimaryClip (); + + if (data.getItemCount () < 1) + return null; + + try + { + uri = data.getItemAt (0).getUri (); + + if (uri == null) + return null; + + Log.d (TAG, "getClipboardData: "+ uri); + + /* Now open the file descriptor. */ + assetFd = resolver.openTypedAssetFileDescriptor (uri, mimeType, + null); + + /* Duplicate the file descriptor. */ + fd = assetFd.getParcelFileDescriptor ().getFd (); + fd = EmacsNative.dup (fd); + + /* Return the relevant information. */ + value = new long[] { fd, assetFd.getStartOffset (), + assetFd.getLength (), }; + + /* Close the original offset. */ + assetFd.close (); + + Log.d (TAG, "getClipboardData: "+ value); + } + catch (FileNotFoundException e) + { + return null; + } + catch (IOException e) + { + return null; + } + + /* Don't return value if the file descriptor couldn't be + created. */ + + return fd != -1 ? value : null; + } }; diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 5a40128b0ac..9622641810f 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -115,4 +115,33 @@ public final class EmacsSdk8Clipboard extends EmacsClipboard return null; } + + /* Return an array of targets currently provided by the + clipboard, or NULL if there are none. */ + + @Override + public byte[][] + getClipboardTargets () + { + return null; + } + + /* Return the clipboard data for the given target, or NULL if it + does not exist. + + Value is normally an array of three longs: the file descriptor, + the start offset of the data, and its length; length may be + AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends + from that offset to the end of the file. + + Do not use this function to open text targets; use `getClipboard' + for that instead, as it will handle selection data consisting + solely of a URI. */ + + @Override + public long[] + getClipboardData (byte[] target) + { + return null; + } }; diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index c7610ae2ca3..d425ea401a9 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -70,6 +70,8 @@ DISPLAY is ignored on Android." (declare-function android-get-clipboard "androidselect.c") (declare-function android-set-clipboard "androidselect.c") (declare-function android-clipboard-owner-p "androidselect.c") +(declare-function android-get-clipboard-targets "androidselect.c") +(declare-function android-get-clipboard-data "androidselect.c") (defvar android-primary-selection nil "The last string placed in the primary selection. @@ -80,13 +82,25 @@ emulates one inside Lisp.") (defun android-get-clipboard-1 (data-type) "Return the clipboard data. -DATA-TYPE is a selection conversion target; only STRING and -TARGETS are supported." +DATA-TYPE is a selection conversion target. `STRING' means to +return the contents of the clipboard as a string. `TARGETS' +means to return supported data types as a vector. + +Interpret any other symbol as a MIME type, and return its +corresponding data." (or (and (eq data-type 'STRING) (android-get-clipboard)) (and (eq data-type 'TARGETS) (android-clipboard-exists-p) - [TARGETS STRING]))) + (vconcat [TARGETS STRING] + (let ((i nil)) + (dolist (type (android-get-clipboard-targets)) + ;; Don't report plain text as a valid target. + (unless (equal type "text/plain") + (push (intern type) i))) + (nreverse i)))) + (and (symbolp data-type) + (android-get-clipboard-data (symbol-name data-type))))) (defun android-get-primary (data-type) "Return the last string placed in the primary selection, or nil. diff --git a/src/android.c b/src/android.c index 1da8bec316e..7852590acf4 100644 --- a/src/android.c +++ b/src/android.c @@ -5530,7 +5530,7 @@ android_exception_check (void) if ((*android_java_env)->ExceptionCheck (android_java_env)) { __android_log_print (ANDROID_LOG_WARN, __func__, - "Possible out of memory error." + "Possible out of memory error. " " The Java exception follows: "); /* Describe exactly what went wrong. */ (*android_java_env)->ExceptionDescribe (android_java_env); @@ -5549,7 +5549,7 @@ android_exception_check_1 (jobject object) if ((*android_java_env)->ExceptionCheck (android_java_env)) { __android_log_print (ANDROID_LOG_WARN, __func__, - "Possible out of memory error." + "Possible out of memory error. " " The Java exception follows: "); /* Describe exactly what went wrong. */ (*android_java_env)->ExceptionDescribe (android_java_env); @@ -5568,7 +5568,7 @@ android_exception_check_2 (jobject object, jobject object1) if ((*android_java_env)->ExceptionCheck (android_java_env)) { __android_log_print (ANDROID_LOG_WARN, __func__, - "Possible out of memory error." + "Possible out of memory error. " " The Java exception follows: "); /* Describe exactly what went wrong. */ (*android_java_env)->ExceptionDescribe (android_java_env); @@ -5600,6 +5600,27 @@ android_exception_check_nonnull (void *object, jobject object1) memory_full (0); } +/* Check for JNI problems based on the value of OBJECT. + + Signal out of memory if OBJECT is NULL. OBJECT1 and OBJECT2 mean + the same as in `android_exception_check_2'. */ + +void +android_exception_check_nonnull_1 (void *object, jobject object1, + jobject object2) +{ + if (object) + return; + + if (object1) + ANDROID_DELETE_LOCAL_REF (object1); + + if (object2) + ANDROID_DELETE_LOCAL_REF (object2); + + memory_full (0); +} + /* Native image transforms. */ diff --git a/src/android.h b/src/android.h index 03592bd955d..24666aaf989 100644 --- a/src/android.h +++ b/src/android.h @@ -88,6 +88,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_nonnull (void *, jobject); +extern void android_exception_check_nonnull_1 (void *, jobject, jobject); extern void android_get_keysym_name (int, char *, size_t); extern void android_wait_event (void); diff --git a/src/androidselect.c b/src/androidselect.c index dfbe0240941..54c712ca93b 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -19,6 +19,8 @@ along with GNU Emacs. If not, see . */ #include #include +#include +#include #include "lisp.h" #include "blockinput.h" @@ -27,12 +29,15 @@ along with GNU Emacs. If not, see . */ #include "androidterm.h" /* Selection support on Android is confined to copying and pasting of - plain text from the clipboard. There is no primary selection. + plain text and MIME data from the clipboard. There is no primary + selection. While newer versions of Android are supposed to have the necessary interfaces for transferring other kinds of selection data, doing so is too complicated, and involves registering ``content providers'' - and all kinds of other stuff. */ + and all kinds of other stuff; for this reason, Emacs does not + support setting the clipboard contents to anything other than plain + text. */ @@ -46,6 +51,8 @@ struct android_emacs_clipboard jmethodID clipboard_exists; jmethodID get_clipboard; jmethodID make_clipboard; + jmethodID get_clipboard_targets; + jmethodID get_clipboard_data; }; /* Methods associated with the EmacsClipboard class. */ @@ -86,6 +93,10 @@ android_init_emacs_clipboard (void) FIND_METHOD (owns_clipboard, "ownsClipboard", "()I"); FIND_METHOD (clipboard_exists, "clipboardExists", "()Z"); FIND_METHOD (get_clipboard, "getClipboard", "()[B"); + FIND_METHOD (get_clipboard_targets, "getClipboardTargets", + "()[[B"); + FIND_METHOD (get_clipboard_data, "getClipboardData", + "([B)[J"); clipboard_class.make_clipboard = (*android_java_env)->GetStaticMethodID (android_java_env, @@ -244,6 +255,210 @@ URL with a scheme specified. Signal an error upon failure. */) +/* MIME clipboard support. This provides support for reading MIME + data (but not text) from the clipboard. */ + +DEFUN ("android-get-clipboard-targets", Fandroid_get_clipboard_targets, + Sandroid_get_clipboard_targets, 0, 0, 0, + doc: /* Return a list of data types in the clipboard. +Value is a list of MIME types as strings, each defining a single extra +data type available from the clipboard. */) + (void) +{ + jarray bytes_array; + jbyteArray bytes; + jmethodID method; + size_t length, length1, i; + jbyte *data; + Lisp_Object targets, tem; + + if (!android_init_gui) + error ("No Android display connection!"); + + targets = Qnil; + block_input (); + method = clipboard_class.get_clipboard_targets; + bytes_array = (*android_java_env)->CallObjectMethod (android_java_env, + clipboard, method); + android_exception_check (); + + if (!bytes_array) + goto fail; + + length = (*android_java_env)->GetArrayLength (android_java_env, + bytes_array); + for (i = 0; i < length; ++i) + { + /* Retireve the MIME type. */ + bytes + = (*android_java_env)->GetObjectArrayElement (android_java_env, + bytes_array, i); + android_exception_check_nonnull (bytes, bytes_array); + + /* Cons it onto the list of targets. */ + length1 = (*android_java_env)->GetArrayLength (android_java_env, + bytes); + data = (*android_java_env)->GetByteArrayElements (android_java_env, + bytes, NULL); + android_exception_check_nonnull_1 (data, bytes, bytes_array); + + /* Decode the string. */ + tem = make_unibyte_string ((char *) data, length1); + tem = code_convert_string_norecord (tem, Qutf_8, Qnil); + targets = Fcons (tem, targets); + + /* Delete the retrieved data. */ + (*android_java_env)->ReleaseByteArrayElements (android_java_env, + bytes, data, + JNI_ABORT); + ANDROID_DELETE_LOCAL_REF (bytes); + } + unblock_input (); + + ANDROID_DELETE_LOCAL_REF (bytes_array); + return Fnreverse (targets); + + fail: + unblock_input (); + return Qnil; +} + +/* Free the memory inside PTR, a pointer to a char pointer. */ + +static void +android_xfree_inside (void *ptr) +{ + xfree (*(char **) ptr); +} + +DEFUN ("android-get-clipboard-data", Fandroid_get_clipboard_data, + Sandroid_get_clipboard_data, 1, 1, 0, + doc: /* Return the clipboard data of the given MIME TYPE. +Value is a unibyte string containing the entire contents of the +clipboard, after its owner has converted the data to the given +MIME type. Value is nil if the conversion fails, or if the data +is not present. + +Value is also nil if the clipboard data consists of a single URL which +does not have any corresponding data. In that case, use +`android-get-clipboard' instead. */) + (Lisp_Object type) +{ + jlongArray array; + jbyteArray bytes; + jmethodID method; + int fd; + ptrdiff_t rc; + jlong offset, length, *longs; + specpdl_ref ref; + char *buffer, *start; + + if (!android_init_gui) + error ("No Android display connection!"); + + /* Encode the string as UTF-8. */ + CHECK_STRING (type); + type = ENCODE_UTF_8 (type); + + /* Then give it to the selection code. */ + block_input (); + bytes = (*android_java_env)->NewByteArray (android_java_env, + SBYTES (type)); + (*android_java_env)->SetByteArrayRegion (android_java_env, bytes, + 0, SBYTES (type), + (jbyte *) SDATA (type)); + android_exception_check (); + + method = clipboard_class.get_clipboard_data; + array = (*android_java_env)->CallObjectMethod (android_java_env, + clipboard, method, + bytes); + android_exception_check_1 (bytes); + ANDROID_DELETE_LOCAL_REF (bytes); + + if (!array) + goto fail; + + longs = (*android_java_env)->GetLongArrayElements (android_java_env, + array, NULL); + android_exception_check_nonnull (longs, array); + + /* longs[0] is the file descriptor. + longs[1] is an offset to apply to the file. + longs[2] is either -1, or the number of bytes to read from the + file. */ + fd = longs[0]; + offset = longs[1]; + length = longs[2]; + + (*android_java_env)->ReleaseLongArrayElements (android_java_env, + array, longs, + JNI_ABORT); + ANDROID_DELETE_LOCAL_REF (array); + unblock_input (); + + /* Now begin reading from longs[0]. */ + ref = SPECPDL_INDEX (); + record_unwind_protect_int (close_file_unwind, fd); + + if (length != -1) + { + buffer = xmalloc (MIN (length, PTRDIFF_MAX)); + record_unwind_protect_ptr (xfree, buffer); + + rc = emacs_read_quit (fd, buffer, + MIN (length, PTRDIFF_MAX)); + + /* Return nil upon an IO problem. */ + if (rc < 0) + return unbind_to (ref, Qnil); + + /* Return the data as a unibyte string. */ + return unbind_to (ref, make_unibyte_string (buffer, rc)); + } + + /* Otherwise, read BUFSIZ bytes at a time. */ + buffer = xmalloc (BUFSIZ); + length = 0; + start = buffer; + + record_unwind_protect_ptr (android_xfree_inside, &buffer); + + /* Seek to the start of the data. */ + + if (offset) + { + if (lseek (fd, offset, SEEK_SET) < 0) + return unbind_to (ref, Qnil); + } + + while (true) + { + rc = emacs_read_quit (fd, start, BUFSIZ); + + if (!INT_ADD_OK (rc, length, &length) + || PTRDIFF_MAX - length < BUFSIZ) + memory_full (PTRDIFF_MAX); + + if (rc < 0) + return unbind_to (ref, Qnil); + + if (rc < BUFSIZ) + break; + + buffer = xrealloc (buffer, length + BUFSIZ); + start = buffer + length; + } + + return unbind_to (ref, make_unibyte_string (buffer, rc)); + + fail: + unblock_input (); + return Qnil; +} + + + void init_androidselect (void) { @@ -279,4 +494,6 @@ syms_of_androidselect (void) defsubr (&Sandroid_get_clipboard); defsubr (&Sandroid_clipboard_exists_p); defsubr (&Sandroid_browse_url); + defsubr (&Sandroid_get_clipboard_targets); + defsubr (&Sandroid_get_clipboard_data); }