Implement multi-window drag-and-drop under Android

* java/org/gnu/emacs/EmacsNative.java (sendDndDrag, sendDndUri)
(sendDndText): Declare new event-sending functions.

* java/org/gnu/emacs/EmacsView.java (onDragEvent): New function.

* java/org/gnu/emacs/EmacsWindow.java (onDragEvent): New
function; respond to each drag and drop event, request
permissions if necessary and transfer dropped data to Lisp.

* lisp/dnd.el (dnd-unescape-file-uris): New variable.
(dnd-get-local-file-name): If that variable is nil, refrain from
unescaping URLs provided.

* lisp/term/android-win.el (android-handle-dnd-event): New
function.
(special-event-map): Bind drag-n-drop-event.

* src/android.c (sendDndDrag, sendDndUri, sendDndText): New
functions.

* src/androidgui.h (enum android_event_type): New event types
ANDROID_DND_DRAG_EVENT, ANDROID_DND_URI_EVENT,
ANDROID_DND_TEXT_EVENT.
(struct android_dnd_event): New structure.
(union android_event) <dnd>: New field.

* src/androidterm.c (handle_one_android_event)
<ANDROID_DND_..._EVENT>: Generate drag-n-drop events for each
of these types.
(syms_of_androidterm) <Quri, Qtext>: New defsyms.
This commit is contained in:
Po Lu 2023-10-14 10:15:20 +08:00
parent 0dd7e6e3ae
commit 03f5a06a05
8 changed files with 394 additions and 4 deletions

View file

@ -175,6 +175,17 @@ public static native long sendContextMenu (short window, int menuEventID,
public static native long sendExpose (short window, int x, int y,
int width, int height);
/* Send an ANDROID_DND_DRAG event. */
public static native long sendDndDrag (short window, int x, int y);
/* Send an ANDROID_DND_URI event. */
public static native long sendDndUri (short window, int x, int y,
String text);
/* Send an ANDROID_DND_TEXT event. */
public static native long sendDndText (short window, int x, int y,
String text);
/* Return the file name associated with the specified file
descriptor, or NULL if there is none. */
public static native byte[] getProcName (int fd);

View file

@ -24,6 +24,7 @@
import android.text.InputType;
import android.view.ContextMenu;
import android.view.DragEvent;
import android.view.View;
import android.view.KeyEvent;
import android.view.MotionEvent;
@ -566,6 +567,19 @@ else if (child.getVisibility () != GONE)
return window.onTouchEvent (motion);
}
@Override
public boolean
onDragEvent (DragEvent drag)
{
/* Inter-program drag and drop isn't supported under Android 23
and earlier. */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
return false;
return window.onDragEvent (drag);
}
private void

View file

@ -27,6 +27,8 @@
import java.util.LinkedHashMap;
import java.util.Map;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.graphics.Rect;
@ -34,12 +36,15 @@
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.view.View;
import android.view.ViewManager;
import android.net.Uri;
import android.view.DragEvent;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.InputDevice;
import android.view.View;
import android.view.ViewManager;
import android.view.WindowManager;
import android.util.Log;
@ -1560,4 +1565,131 @@ else if (EmacsWindow.this.isMapped)
rect.width (), rect.height ());
}
}
/* Drag and drop.
Android 7.0 and later permit multiple windows to be juxtaposed
on-screen, consequently enabling items selected from one window
to be dragged onto another. Data is transferred across program
boundaries using ClipData items, much the same way clipboard data
is transferred.
When an item is dropped, Emacs must ascertain whether the clip
data represents plain text, a content URI incorporating a file,
or some other data. This is implemented by examining the clip
data's ``description'', which enumerates each of the MIME data
types the clip data is capable of providing data in.
If the clip data represents plain text, then that text is copied
into a string and conveyed to Lisp code. Otherwise, Emacs must
solicit rights to access the URI from the system, absent which it
is accounted plain text and reinterpreted as such, to cue the
user that something has gone awry.
Moreover, events are regularly sent as the item being dragged
travels across the frame, even if it might not be dropped. This
facilitates cursor motion and scrolling in response, as provided
by the options dnd-indicate-insertion-point and
dnd-scroll-margin. */
/* Register the drag and drop event EVENT. */
public boolean
onDragEvent (DragEvent event)
{
ClipData data;
ClipDescription description;
int i, x, y;
String type;
Uri uri;
EmacsActivity activity;
x = (int) event.getX ();
y = (int) event.getY ();
switch (event.getAction ())
{
case DragEvent.ACTION_DRAG_STARTED:
/* Return true to continue the drag and drop operation. */
return true;
case DragEvent.ACTION_DRAG_LOCATION:
/* Send this drag motion event to Emacs. */
EmacsNative.sendDndDrag (handle, x, y);
return true;
case DragEvent.ACTION_DROP:
/* Judge whether this is plain text, or if it's a file URI for
which permissions must be requested. */
data = event.getClipData ();
description = data.getDescription ();
/* If there are insufficient items within the clip data,
return false. */
if (data.getItemCount () < 1)
return false;
/* Search for plain text data within the clipboard. */
for (i = 0; i < description.getMimeTypeCount (); ++i)
{
type = description.getMimeType (i);
if (type.equals (ClipDescription.MIMETYPE_TEXT_PLAIN)
|| type.equals (ClipDescription.MIMETYPE_TEXT_HTML))
{
/* The data being dropped is plain text; encode it
suitably and send it to the main thread. */
type = (data.getItemAt (0).coerceToText (EmacsService.SERVICE)
.toString ());
EmacsNative.sendDndText (handle, x, y, type);
return true;
}
else if (type.equals (ClipDescription.MIMETYPE_TEXT_URILIST))
{
/* The data being dropped is a list of URIs; encode it
suitably and send it to the main thread. */
type = (data.getItemAt (0).coerceToText (EmacsService.SERVICE)
.toString ());
EmacsNative.sendDndUri (handle, x, y, type);
return true;
}
else
{
/* If the item dropped is a URI, send it to the main
thread. */
uri = data.getItemAt (0).getUri ();
/* Attempt to acquire permissions for this URI;
failing which, insert it as text instead. */
if (uri.getScheme () != null
&& uri.getScheme ().equals ("content")
&& (activity = EmacsActivity.lastFocusedActivity) != null)
{
if (activity.requestDragAndDropPermissions (event) == null)
uri = null;
}
if (uri != null)
EmacsNative.sendDndUri (handle, x, y, uri.toString ());
else
{
type = (data.getItemAt (0)
.coerceToText (EmacsService.SERVICE)
.toString ());
EmacsNative.sendDndText (handle, x, y, type);
}
return true;
}
}
}
return true;
}
};

View file

@ -201,6 +201,11 @@ Return nil if URI is not a local file."
(string-equal sysname-no-dot hostname)))
(concat "file://" (substring uri (+ 7 (length hostname))))))))
(defvar dnd-unescape-file-uris t
"Whether to unescape file: URIs before they are opened.
Bind this to nil when providing `dnd-get-local-file-name' with a
file name that may incorporate URI escape sequences.")
(defun dnd--unescape-uri (uri)
;; Merge with corresponding code in URL library.
(replace-regexp-in-string
@ -226,7 +231,10 @@ Return nil if URI is not a local file."
'utf-8
(or file-name-coding-system
default-file-name-coding-system))))
(and f (setq f (decode-coding-string (dnd--unescape-uri f) coding)))
(and f (setq f (decode-coding-string
(if dnd-unescape-file-uris
(dnd--unescape-uri f) f)
coding)))
(when (and f must-exist (not (file-readable-p f)))
(setq f nil))
f))

View file

@ -232,6 +232,64 @@ EVENT is a preedit-text event."
(defconst x-pointer-xterm 1008)
(defconst x-pointer-invisible 0)
;; Drag-and-drop. There are two formats of drag and drop event under
;; Android. The data field of the first is set to a cons of X and Y,
;; which represent a position within a frame that something is being
;; dragged over, whereas that of the second is a cons of either symbol
;; `uri' or `text' and a list of URIs or text to insert.
;;
;; If a content:// URI is encountered, then it in turn designates a
;; file within the special-purpose /content/by-authority directory,
;; which facilitates accessing such atypical files.
(declare-function url-type "url-parse")
(declare-function url-host "url-parse")
(declare-function url-filename "url-parse")
(defun android-handle-dnd-event (event)
"Respond to a drag-and-drop event EVENT.
If it reflects the motion of an item above a frame, call
`dnd-handle-movement' to move the cursor or scroll the window
under the item pursuant to the pertinent user options.
If it reflects dropped text, insert such text within window at
the location of the drop.
If it reflects a list of URIs, then open each URI, converting
content:// URIs into the special file names which represent them."
(interactive "e")
(let ((message (caddr event))
(posn (event-start event)))
(cond ((fixnump (car message))
(dnd-handle-movement posn))
((eq (car message) 'text)
(let ((window (posn-window posn)))
(with-selected-window window
(unless mouse-yank-at-point
(goto-char (posn-point (event-start event))))
(dnd-insert-text window 'copy (cdr message)))))
((eq (car message) 'uri)
(let ((uri-list (split-string (cdr message)
"[\0\r\n]" t))
(dnd-unescape-file-uris t))
(dolist (uri uri-list)
(ignore-errors
(let ((url (url-generic-parse-url uri)))
(when (equal (url-type url) "content")
;; Replace URI with a matching /content file
;; name.
(setq uri (format "file:/content/by-authority/%s%s"
(url-host url)
(url-filename url))
;; And guarantee that this file URI is not
;; subject to URI decoding, for it must be
;; transformed back into a content URI.
dnd-unescape-file-uris nil))))
(dnd-handle-one-url (posn-window posn) 'copy uri)))))))
(define-key special-event-map [drag-n-drop] 'android-handle-dnd-event)
(provide 'android-win)
;; android-win.el ends here.

View file

@ -2319,6 +2319,100 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object,
return event_serial;
}
JNIEXPORT jboolean JNICALL
NATIVE_NAME (sendDndDrag) (JNIEnv *env, jobject object,
jshort window, jint x, jint y)
{
JNI_STACK_ALIGNMENT_PROLOGUE;
union android_event event;
event.dnd.type = ANDROID_DND_DRAG_EVENT;
event.dnd.serial = ++event_serial;
event.dnd.window = window;
event.dnd.x = x;
event.dnd.y = y;
event.dnd.uri_or_string = NULL;
event.dnd.length = 0;
android_write_event (&event);
return event_serial;
}
JNIEXPORT jboolean JNICALL
NATIVE_NAME (sendDndUri) (JNIEnv *env, jobject object,
jshort window, jint x, jint y,
jstring string)
{
JNI_STACK_ALIGNMENT_PROLOGUE;
union android_event event;
const jchar *characters;
jsize length;
uint16_t *buffer;
event.dnd.type = ANDROID_DND_URI_EVENT;
event.dnd.serial = ++event_serial;
event.dnd.window = window;
event.dnd.x = x;
event.dnd.y = y;
length = (*env)->GetStringLength (env, string);
buffer = malloc (length * sizeof *buffer);
characters = (*env)->GetStringChars (env, string, NULL);
if (!characters)
/* The JVM has run out of memory; return and let the out of memory
error take its course. */
return 0;
memcpy (buffer, characters, length * sizeof *buffer);
(*env)->ReleaseStringChars (env, string, characters);
event.dnd.uri_or_string = buffer;
event.dnd.length = length;
android_write_event (&event);
return event_serial;
}
JNIEXPORT jboolean JNICALL
NATIVE_NAME (sendDndText) (JNIEnv *env, jobject object,
jshort window, jint x, jint y,
jstring string)
{
JNI_STACK_ALIGNMENT_PROLOGUE;
union android_event event;
const jchar *characters;
jsize length;
uint16_t *buffer;
event.dnd.type = ANDROID_DND_TEXT_EVENT;
event.dnd.serial = ++event_serial;
event.dnd.window = window;
event.dnd.x = x;
event.dnd.y = y;
length = (*env)->GetStringLength (env, string);
buffer = malloc (length * sizeof *buffer);
characters = (*env)->GetStringChars (env, string, NULL);
if (!characters)
/* The JVM has run out of memory; return and let the out of memory
error take its course. */
return 0;
memcpy (buffer, characters, length * sizeof *buffer);
(*env)->ReleaseStringChars (env, string, characters);
event.dnd.uri_or_string = buffer;
event.dnd.length = length;
android_write_event (&event);
return event_serial;
}
JNIEXPORT jboolean JNICALL
NATIVE_NAME (shouldForwardMultimediaButtons) (JNIEnv *env,
jobject object)

View file

@ -248,6 +248,9 @@ enum android_event_type
ANDROID_CONTEXT_MENU,
ANDROID_EXPOSE,
ANDROID_INPUT_METHOD,
ANDROID_DND_DRAG_EVENT,
ANDROID_DND_URI_EVENT,
ANDROID_DND_TEXT_EVENT,
};
struct android_any_event
@ -510,6 +513,28 @@ struct android_ime_event
unsigned long counter;
};
struct android_dnd_event
{
/* Type of the event. */
enum android_event_type type;
/* The event serial. */
unsigned long serial;
/* The window that gave rise to the event. */
android_window window;
/* X and Y coordinates of the event. */
int x, y;
/* Data tied to this event, such as a URI or clipboard string.
Must be deallocated with `free'. */
unsigned short *uri_or_string;
/* Length of that data. */
size_t length;
};
union android_event
{
enum android_event_type type;
@ -541,6 +566,11 @@ union android_event
/* This is used to dispatch input method editing requests. */
struct android_ime_event ime;
/* There is no analog under X because Android defines a strict DND
protocol, whereas there exist several competing X protocols
implemented in terms of X client messages. */
struct android_dnd_event dnd;
};
enum

View file

@ -1706,6 +1706,45 @@ handle_one_android_event (struct android_display_info *dpyinfo,
goto OTHER;
case ANDROID_DND_DRAG_EVENT:
if (!any)
goto OTHER;
/* Generate a drag and drop event to convey its position. */
inev.ie.kind = DRAG_N_DROP_EVENT;
XSETFRAME (inev.ie.frame_or_window, any);
inev.ie.timestamp = ANDROID_CURRENT_TIME;
XSETINT (inev.ie.x, event->dnd.x);
XSETINT (inev.ie.y, event->dnd.y);
inev.ie.arg = Fcons (inev.ie.x, inev.ie.y);
goto OTHER;
case ANDROID_DND_URI_EVENT:
case ANDROID_DND_TEXT_EVENT:
if (!any)
{
free (event->dnd.uri_or_string);
goto OTHER;
}
/* An item was dropped over ANY, and is a file in the form of a
content or file URI or a string to be inserted. Generate an
event with this information. */
inev.ie.kind = DRAG_N_DROP_EVENT;
XSETFRAME (inev.ie.frame_or_window, any);
inev.ie.timestamp = ANDROID_CURRENT_TIME;
XSETINT (inev.ie.x, event->dnd.x);
XSETINT (inev.ie.y, event->dnd.y);
inev.ie.arg = Fcons ((event->type == ANDROID_DND_TEXT_EVENT
? Qtext : Quri),
android_decode_utf16 (event->dnd.uri_or_string,
event->dnd.length));
free (event->dnd.uri_or_string);
goto OTHER;
default:
goto OTHER;
}
@ -6593,6 +6632,10 @@ Emacs is running on. */);
pdumper_do_now_and_after_load (android_set_build_fingerprint);
DEFSYM (Qx_underline_at_descent_line, "x-underline-at-descent-line");
/* Symbols defined for DND events. */
DEFSYM (Quri, "uri");
DEFSYM (Qtext, "text");
}
void