summaryrefslogtreecommitdiff
path: root/java/org/gnu/emacs/EmacsWindow.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/org/gnu/emacs/EmacsWindow.java')
-rw-r--r--java/org/gnu/emacs/EmacsWindow.java1878
1 files changed, 1878 insertions, 0 deletions
diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java
new file mode 100644
index 00000000000..2baede1d2d0
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsWindow.java
@@ -0,0 +1,1878 @@
+/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
+
+Copyright (C) 2023-2024 Free Software Foundation, Inc.
+
+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 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/>. */
+
+package org.gnu.emacs;
+
+import java.lang.IllegalStateException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import android.app.Activity;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ContentResolver;
+import android.content.Context;
+
+import android.graphics.Rect;
+import android.graphics.Canvas;
+import android.graphics.Bitmap;
+import android.graphics.PixelFormat;
+
+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.View;
+import android.view.ViewManager;
+import android.view.WindowManager;
+
+import android.util.SparseArray;
+import android.util.Log;
+
+import android.os.Build;
+
+/* This defines a window, which is a handle. Windows represent a
+ rectangular subset of the screen with their own contents.
+
+ Windows either have a parent window, in which case their views are
+ attached to the parent's view, or are "floating", in which case
+ their views are attached to the parent activity (if any), else
+ nothing.
+
+ Views are also drawables, meaning they can accept drawing
+ requests. */
+
+public final class EmacsWindow extends EmacsHandleObject
+ implements EmacsDrawable
+{
+ private static final String TAG = "EmacsWindow";
+
+ private static class Coordinate
+ {
+ /* Integral coordinate. */
+ int x, y;
+
+ /* Button associated with the coordinate, or 0 if it is a touch
+ event. */
+ int button;
+
+ /* Pointer ID associated with the coordinate. */
+ int id;
+
+ public
+ Coordinate (int x, int y, int button, int id)
+ {
+ this.x = x;
+ this.y = y;
+ this.button = button;
+ this.id = id;
+ }
+ };
+
+ /* The view associated with the window. */
+ public EmacsView view;
+
+ /* The geometry of the window. */
+ private Rect rect;
+
+ /* The parent window, or null if it is the root window. */
+ public EmacsWindow parent;
+
+ /* List of all children in stacking order. This must be kept
+ consistent with their Z order!
+
+ Synchronize access to this list with itself. */
+ public ArrayList<EmacsWindow> children;
+
+ /* Map between pointer identifiers and last known position. Used to
+ compute which pointer changed upon a touch event. */
+ private SparseArray<Coordinate> pointerMap;
+
+ /* The window consumer currently attached, if it exists. */
+ private EmacsWindowAttachmentManager.WindowConsumer attached;
+
+ /* The window background scratch GC. foreground is always the
+ window background. */
+ private EmacsGC scratchGC;
+
+ /* The button state and keyboard modifier mask at the time of the
+ last button press or release event. */
+ public int lastButtonState;
+
+ /* Whether or not the window is mapped. */
+ private volatile boolean isMapped;
+
+ /* Whether or not to ask for focus upon being mapped. */
+ private boolean dontFocusOnMap;
+
+ /* Whether or not the window is override-redirect. An
+ override-redirect window always has its own system window. */
+ private boolean overrideRedirect;
+
+ /* The window manager that is the parent of this window. NULL if
+ there is no such window manager. */
+ private WindowManager windowManager;
+
+ /* The time of the last KEYCODE_VOLUME_DOWN release. This is used
+ to quit Emacs upon two rapid clicks of the volume down
+ button. */
+ private long lastVolumeButtonRelease;
+
+ /* Linked list of character strings which were recently sent as
+ events. */
+ public LinkedHashMap<Integer, String> eventStrings;
+
+ /* Whether or not this window is fullscreen. */
+ public boolean fullscreen;
+
+ /* The window background pixel. This is used by EmacsView when
+ creating new bitmaps. */
+ public volatile int background;
+
+ /* The position of this window relative to the root window. */
+ public int xPosition, yPosition;
+
+ /* The position of the last drag and drop event received; both
+ values are -1 if no drag and drop operation is under way. */
+ private int dndXPosition, dndYPosition;
+
+ public
+ EmacsWindow (short handle, final EmacsWindow parent, int x, int y,
+ int width, int height, boolean overrideRedirect)
+ {
+ super (handle);
+
+ rect = new Rect (x, y, x + width, y + height);
+ pointerMap = new SparseArray<Coordinate> ();
+
+ /* Create the view from the context's UI thread. The window is
+ unmapped, so the view is GONE. */
+ view = EmacsService.SERVICE.getEmacsView (this, View.GONE,
+ parent == null);
+ this.parent = parent;
+ this.overrideRedirect = overrideRedirect;
+
+ /* Create the list of children. */
+ children = new ArrayList<EmacsWindow> ();
+
+ if (parent != null)
+ {
+ synchronized (parent.children)
+ {
+ parent.children.add (this);
+ }
+
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ parent.view.addView (view);
+ }
+ });
+ }
+
+ scratchGC = new EmacsGC ((short) 0);
+
+ /* Create the map of input method-committed strings. Keep at most
+ ten strings in the map. */
+
+ eventStrings
+ = new LinkedHashMap<Integer, String> () {
+ @Override
+ protected boolean
+ removeEldestEntry (Map.Entry<Integer, String> entry)
+ {
+ return size () > 10;
+ }
+ };
+
+ dndXPosition = -1;
+ dndYPosition = -1;
+ }
+
+ public void
+ changeWindowBackground (int pixel)
+ {
+ /* scratchGC is used as the argument to a FillRectangles req. */
+ scratchGC.foreground = pixel;
+ scratchGC.markDirty (false);
+
+ /* Make the background known to the view as well. */
+ background = pixel;
+ }
+
+ public synchronized Rect
+ getGeometry ()
+ {
+ return new Rect (rect);
+ }
+
+ @Override
+ public synchronized void
+ destroyHandle () throws IllegalStateException
+ {
+ if (parent != null)
+ {
+ synchronized (parent.children)
+ {
+ parent.children.remove (this);
+ }
+ }
+
+ EmacsActivity.invalidateFocus (4);
+
+ if (!children.isEmpty ())
+ throw new IllegalStateException ("Trying to destroy window with "
+ + "children!");
+
+ /* Remove the view from its parent and make it invisible. */
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ public void
+ run ()
+ {
+ ViewManager parent;
+ EmacsWindowAttachmentManager manager;
+
+ if (EmacsActivity.focusedWindow == EmacsWindow.this)
+ EmacsActivity.focusedWindow = null;
+
+ manager = EmacsWindowAttachmentManager.MANAGER;
+ view.setVisibility (View.GONE);
+
+ /* If the window manager is set, use that instead. */
+ if (windowManager != null)
+ parent = windowManager;
+ else
+ parent = (ViewManager) view.getParent ();
+ windowManager = null;
+
+ if (parent != null)
+ parent.removeView (view);
+
+ manager.detachWindow (EmacsWindow.this);
+ }
+ });
+
+ super.destroyHandle ();
+ }
+
+ public void
+ setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer)
+ {
+ attached = consumer;
+ }
+
+ public EmacsWindowAttachmentManager.WindowConsumer
+ getAttachedConsumer ()
+ {
+ return attached;
+ }
+
+ public synchronized long
+ viewLayout (int left, int top, int right, int bottom)
+ {
+ int rectWidth, rectHeight;
+
+ rect.left = left;
+ rect.top = top;
+ rect.right = right;
+ rect.bottom = bottom;
+
+ rectWidth = right - left;
+ rectHeight = bottom - top;
+
+ /* If parent is null, use xPosition and yPosition instead of the
+ geometry rectangle positions. */
+
+ if (parent == null)
+ {
+ left = xPosition;
+ top = yPosition;
+ }
+
+ return EmacsNative.sendConfigureNotify (this.handle,
+ System.currentTimeMillis (),
+ left, top, rectWidth,
+ rectHeight);
+ }
+
+ public void
+ requestViewLayout ()
+ {
+ view.explicitlyDirtyBitmap ();
+
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ if (overrideRedirect)
+ /* Set the layout parameters again. */
+ view.setLayoutParams (getWindowLayoutParams ());
+
+ view.mustReportLayout = true;
+ view.requestLayout ();
+ }
+ });
+ }
+
+ public synchronized void
+ resizeWindow (int width, int height)
+ {
+ rect.right = rect.left + width;
+ rect.bottom = rect.top + height;
+
+ requestViewLayout ();
+ }
+
+ public synchronized void
+ moveWindow (int x, int y)
+ {
+ int width, height;
+
+ width = rect.width ();
+ height = rect.height ();
+
+ rect.left = x;
+ rect.top = y;
+ rect.right = x + width;
+ rect.bottom = y + height;
+
+ requestViewLayout ();
+ }
+
+ /* Return WM layout parameters for an override redirect window with
+ the geometry provided here. */
+
+ private WindowManager.LayoutParams
+ getWindowLayoutParams ()
+ {
+ WindowManager.LayoutParams params;
+ int flags, type;
+ Rect rect;
+
+ flags = 0;
+ rect = getGeometry ();
+ flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+
+ params
+ = new WindowManager.LayoutParams (rect.width (), rect.height (),
+ rect.left, rect.top,
+ type, flags,
+ PixelFormat.RGBA_8888);
+ params.gravity = Gravity.TOP | Gravity.LEFT;
+ return params;
+ }
+
+ private Activity
+ findSuitableActivityContext ()
+ {
+ /* Find a recently focused activity. */
+ if (!EmacsActivity.focusedActivities.isEmpty ())
+ return EmacsActivity.focusedActivities.get (0);
+
+ /* Resort to the last activity to be focused. */
+ return EmacsActivity.lastFocusedActivity;
+ }
+
+ public synchronized void
+ mapWindow ()
+ {
+ final int width, height;
+
+ if (isMapped)
+ return;
+
+ isMapped = true;
+ width = rect.width ();
+ height = rect.height ();
+
+ if (parent == null)
+ {
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ EmacsWindowAttachmentManager manager;
+ WindowManager windowManager;
+ Activity ctx;
+ Object tem;
+ WindowManager.LayoutParams params;
+
+ /* Make the view visible, first of all. */
+ view.setVisibility (View.VISIBLE);
+
+ if (!overrideRedirect)
+ {
+ manager = EmacsWindowAttachmentManager.MANAGER;
+
+ /* If parent is the root window, notice that there are new
+ children available for interested activities to pick
+ up. */
+ manager.registerWindow (EmacsWindow.this);
+
+ if (!getDontFocusOnMap ())
+ /* Eventually this should check no-focus-on-map. */
+ view.requestFocus ();
+ }
+ else
+ {
+ /* But if the window is an override-redirect window,
+ then:
+
+ - Find an activity that is currently active.
+
+ - Map the window as a panel on top of that
+ activity using the system window manager. */
+
+ ctx = findSuitableActivityContext ();
+
+ if (ctx == null)
+ {
+ Log.w (TAG, "failed to attach override-redirect window"
+ + " for want of activity");
+ return;
+ }
+
+ tem = ctx.getSystemService (Context.WINDOW_SERVICE);
+ windowManager = (WindowManager) tem;
+
+ /* Calculate layout parameters and propagate the
+ activity's token into it. */
+
+ params = getWindowLayoutParams ();
+ params.token = (ctx.findViewById (android.R.id.content)
+ .getWindowToken ());
+ view.setLayoutParams (params);
+
+ /* Attach the view. */
+ try
+ {
+ view.prepareForLayout (width, height);
+ windowManager.addView (view, params);
+
+ /* Record the window manager being used in the
+ EmacsWindow object. */
+ EmacsWindow.this.windowManager = windowManager;
+ }
+ catch (Exception e)
+ {
+ Log.w (TAG,
+ "failed to attach override-redirect window, " + e);
+ }
+ }
+ }
+ });
+ }
+ else
+ {
+ /* Do the same thing as above, but don't register this
+ window. */
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ /* Prior to mapping the view, set its measuredWidth and
+ measuredHeight to some reasonable value, in order to
+ avoid excessive bitmap dirtying. */
+
+ view.prepareForLayout (width, height);
+ view.setVisibility (View.VISIBLE);
+
+ if (!getDontFocusOnMap ())
+ view.requestFocus ();
+ }
+ });
+ }
+ }
+
+ public synchronized void
+ unmapWindow ()
+ {
+ if (!isMapped)
+ return;
+
+ isMapped = false;
+
+ view.post (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ EmacsWindowAttachmentManager manager;
+
+ manager = EmacsWindowAttachmentManager.MANAGER;
+
+ view.setVisibility (View.GONE);
+
+ /* Detach the view from the window manager if possible. */
+ if (windowManager != null)
+ windowManager.removeView (view);
+ windowManager = null;
+
+ /* Now that the window is unmapped, unregister it as
+ well. */
+ manager.detachWindow (EmacsWindow.this);
+ }
+ });
+ }
+
+ @Override
+ public Canvas
+ lockCanvas (EmacsGC gc)
+ {
+ return view.getCanvas (gc);
+ }
+
+ @Override
+ public void
+ damageRect (Rect damageRect)
+ {
+ view.damageRect (damageRect.left,
+ damageRect.top,
+ damageRect.right,
+ damageRect.bottom);
+ }
+
+ @Override
+ public void
+ damageRect (int left, int top, int right, int bottom)
+ {
+ view.damageRect (left, top, right, bottom);
+ }
+
+ public void
+ swapBuffers ()
+ {
+ view.swapBuffers ();
+ }
+
+ public void
+ clearWindow ()
+ {
+ EmacsService.SERVICE.fillRectangle (this, scratchGC,
+ 0, 0, rect.width (),
+ rect.height ());
+ }
+
+ public void
+ clearArea (int x, int y, int width, int height)
+ {
+ EmacsService.SERVICE.fillRectangle (this, scratchGC,
+ x, y, width, height);
+ }
+
+ @Override
+ public Bitmap
+ getBitmap ()
+ {
+ return view.getBitmap ();
+ }
+
+ /* event.getCharacters is used because older input methods still
+ require it. */
+ @SuppressWarnings ("deprecation")
+ public int
+ getEventUnicodeChar (KeyEvent event, int state)
+ {
+ String characters;
+
+ if (event.getUnicodeChar (state) != 0)
+ return event.getUnicodeChar (state);
+
+ characters = event.getCharacters ();
+
+ if (characters != null && characters.length () == 1)
+ return characters.charAt (0);
+
+ return characters == null ? 0 : -1;
+ }
+
+ public void
+ saveUnicodeString (int serial, String string)
+ {
+ eventStrings.put (serial, string);
+ }
+
+
+
+ /* Return the modifier mask associated with the specified keyboard
+ input EVENT. Replace bits corresponding to Left or Right keys
+ with their corresponding general modifier bits. */
+
+ public static int
+ eventModifiers (KeyEvent event)
+ {
+ int state;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2)
+ state = event.getModifiers ();
+ else
+ {
+ /* Replace this with getMetaState and manual
+ normalization. */
+ state = event.getMetaState ();
+
+ /* Normalize the state by setting the generic modifier bit if
+ either a left or right modifier is pressed. */
+
+ if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
+ || (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
+ state |= KeyEvent.META_ALT_MASK;
+
+ if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
+ || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
+ state |= KeyEvent.META_CTRL_MASK;
+ }
+
+ return state;
+ }
+
+ /* event.getCharacters is used because older input methods still
+ require it. */
+ @SuppressWarnings ("deprecation")
+ public void
+ onKeyDown (int keyCode, KeyEvent event)
+ {
+ int state, state_1, extra_ignored;
+ long serial;
+ String characters;
+
+ if (keyCode == KeyEvent.KEYCODE_BACK)
+ {
+ /* New Android systems display Back navigation buttons on a
+ row of virtual buttons at the bottom of the screen. These
+ buttons function much as physical buttons do, in that key
+ down events are produced when a finger taps them, even if
+ the finger is not ultimately released after the OS's
+ gesture navigation is activated.
+
+ Deliver onKeyDown events in onKeyUp instead, so as not to
+ navigate backwards during gesture navigation. */
+
+ return;
+ }
+
+ state = eventModifiers (event);
+
+ /* Num Lock, Scroll Lock and Meta aren't supported by systems older
+ than Android 3.0. */
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ extra_ignored = (KeyEvent.META_NUM_LOCK_ON
+ | KeyEvent.META_SCROLL_LOCK_ON
+ | KeyEvent.META_META_MASK);
+ else
+ extra_ignored = 0;
+
+ /* Ignore meta-state understood by Emacs for now, or key presses
+ such as Ctrl+C and Meta+C will not be recognized as ASCII key
+ press events. */
+
+ state_1
+ = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
+ | KeyEvent.META_SYM_ON | extra_ignored);
+
+ /* There's no distinction between Right Alt and Alt Gr on Android,
+ so restore META_ALT_RIGHT_ON if set in state to enable composing
+ characters. (bug#69321) */
+
+ if ((state & KeyEvent.META_ALT_RIGHT_ON) != 0)
+ {
+ state_1 |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
+
+ /* If Alt is also not depressed, remove its bit from the mask
+ reported to Emacs. */
+ if ((state & KeyEvent.META_ALT_LEFT_ON) == 0)
+ state &= ~KeyEvent.META_ALT_MASK;
+ }
+
+ synchronized (eventStrings)
+ {
+ serial
+ = EmacsNative.sendKeyPress (this.handle,
+ event.getEventTime (),
+ state, keyCode,
+ getEventUnicodeChar (event,
+ state_1));
+
+ characters = event.getCharacters ();
+
+ if (characters != null && characters.length () > 1)
+ saveUnicodeString ((int) serial, characters);
+ }
+ }
+
+ public void
+ onKeyUp (int keyCode, KeyEvent event)
+ {
+ int state, state_1, unicode_char, extra_ignored;
+ long time;
+
+ /* Compute the event's modifier mask. */
+ state = eventModifiers (event);
+
+ /* Num Lock, Scroll Lock and Meta aren't supported by systems older
+ than Android 3.0. */
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ extra_ignored = (KeyEvent.META_NUM_LOCK_ON
+ | KeyEvent.META_SCROLL_LOCK_ON
+ | KeyEvent.META_META_MASK);
+ else
+ extra_ignored = 0;
+
+ /* Ignore meta-state understood by Emacs for now, or key presses
+ such as Ctrl+C and Meta+C will not be recognized as ASCII key
+ press events. */
+
+ state_1
+ = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
+ | KeyEvent.META_SYM_ON | extra_ignored);
+
+ /* There's no distinction between Right Alt and Alt Gr on Android,
+ so restore META_ALT_RIGHT_ON if set in state to enable composing
+ characters. */
+
+ if ((state & KeyEvent.META_ALT_RIGHT_ON) != 0)
+ {
+ state_1 |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
+
+ /* If Alt is also not depressed, remove its bit from the mask
+ reported to Emacs. */
+ if ((state & KeyEvent.META_ALT_LEFT_ON) == 0)
+ state &= ~KeyEvent.META_ALT_MASK;
+ }
+
+ unicode_char = getEventUnicodeChar (event, state_1);
+
+ if (keyCode == KeyEvent.KEYCODE_BACK)
+ {
+ /* If the key press's been canceled, return immediately. */
+
+ if ((event.getFlags () & KeyEvent.FLAG_CANCELED) != 0)
+ return;
+
+ EmacsNative.sendKeyPress (this.handle, event.getEventTime (),
+ state, keyCode, unicode_char);
+ }
+
+ EmacsNative.sendKeyRelease (this.handle, event.getEventTime (),
+ state, keyCode, unicode_char);
+
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
+ {
+ /* Check if this volume down press should quit Emacs.
+ Most Android devices have no physical keyboard, so it
+ is unreasonably hard to press C-g. */
+
+ time = event.getEventTime ();
+
+ if (time - lastVolumeButtonRelease < 350)
+ EmacsNative.quit ();
+
+ lastVolumeButtonRelease = time;
+ }
+ }
+
+ public void
+ onFocusChanged (boolean gainFocus)
+ {
+ EmacsActivity.invalidateFocus (gainFocus ? 6 : 5);
+ }
+
+ /* Notice that the activity has been detached or destroyed.
+
+ ISFINISHING is set if the activity is not the main activity, or
+ if the activity was not destroyed in response to explicit user
+ action. */
+
+ public void
+ onActivityDetached (boolean isFinishing)
+ {
+ /* Destroy the associated frame when the activity is detached in
+ response to explicit user action. */
+
+ if (isFinishing)
+ EmacsNative.sendWindowAction (this.handle, 0);
+ }
+
+
+
+ /* Mouse and touch event handling.
+
+ Android does not conceptually distinguish between mouse events
+ (those coming from a device whose movement affects the on-screen
+ pointer image) and touch screen events. Each click or touch
+ starts a single pointer gesture sequence, and subsequent motion
+ of the device will result in updates being reported relative to
+ that sequence until the mouse button or touch is released.
+
+ When a touch, click, or pointer motion takes place, several kinds
+ of event can be sent:
+
+ ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate
+ and an associated ``pointer ID'' identifying the event and its
+ gesture sequence when a click or touch takes place. Emacs is
+ responsible for recording both the position and pointer ID of
+ this click for the purpose of determining future changes to its
+ position.
+
+ ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the
+ click associated with a previous ACTION_DOWN event is released.
+
+ ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent
+ if a similar situation transpires: the window system has chosen
+ to grab the click, and future changes to its position will no
+ longer be reported to Emacs.
+
+ ACTION_MOVE is sent if a coordinate tied to a click that has not
+ been released changes. Emacs processes this event by comparing
+ each of the coordinates within the event with its recollection of
+ those contained within prior ACTION_DOWN and ACTION_MOVE events;
+ the pointer ID of the differing coordinate is then reported
+ within a touch or pointer motion event along with its new
+ position.
+
+ The events described above are all sent for both touch and mouse
+ click events. Determining whether an ACTION_DOWN event is
+ associated with a button event is performed by inspecting the
+ mouse button state associated with that event. If it contains
+ any mouse buttons that were not contained in the button state at
+ the time of the last ACTION_DOWN or ACTION_UP event, the
+ coordinate contained within is assumed to be a mouse click,
+ leading to it and associated motion or ACTION_UP events being
+ reported as mouse button or motion events. Otherwise, those
+ events are reported as touch screen events, with the touch ID set
+ to the pointer ID.
+
+ In addition to the events illustrated above, Android also sends
+ several other types of event upon select types of activity from a
+ mouse device:
+
+ ACTION_HOVER_MOVE is sent with the coordinate of the mouse
+ pointer if it moves above a frame prior to any click taking
+ place. Emacs sends a mouse motion event containing the
+ coordinate.
+
+ ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent
+ when the mouse pointer enters and leaves a frame. Moreover,
+ ACTION_HOVER_LEAVE events are sent immediately before an
+ ACTION_DOWN event associated with a mouse click. These
+ extraneous events are distinct in that their button states always
+ contain an additional button compared to the button state
+ recorded at the time of the last ACTION_UP event.
+
+ On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the
+ coordinate of the mouse pointer if a mouse click occurs,
+ alongside a ACTION_DOWN event. ACTION_BUTTON_RELEASE is sent
+ with the same information upon a mouse click being released, also
+ accompanying an ACTION_UP event.
+
+ However, both types of button events are implemented in a buggy
+ fashion and cannot be used to report button events. */
+
+ /* Look through the button state to determine what button EVENT was
+ generated from. DOWN is true if EVENT is a button press event,
+ false otherwise. Value is the X number of the button. */
+
+ private int
+ whatButtonWasIt (MotionEvent event, boolean down)
+ {
+ int eventState, notIn;
+
+ /* Obtain the new button state. */
+ eventState = event.getButtonState ();
+
+ /* Compute which button is now set or no longer set. */
+
+ notIn = (down ? eventState & ~lastButtonState
+ : lastButtonState & ~eventState);
+
+ if ((notIn & (MotionEvent.BUTTON_PRIMARY
+ | MotionEvent.BUTTON_SECONDARY
+ | MotionEvent.BUTTON_TERTIARY)) == 0)
+ /* No buttons have been pressed, so this is a touch event. */
+ return 0;
+
+ if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0)
+ return 1;
+
+ if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0)
+ return 3;
+
+ if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0)
+ return 2;
+
+ /* Buttons 4, 5, 6 and 7 are actually scroll wheels under X.
+ Thus, report additional buttons starting at 8. */
+
+ if ((notIn & MotionEvent.BUTTON_BACK) != 0)
+ return 8;
+
+ if ((notIn & MotionEvent.BUTTON_FORWARD) != 0)
+ return 9;
+
+ /* Report stylus events as touch screen events. */
+
+ if ((notIn & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0)
+ return 0;
+
+ if ((notIn & MotionEvent.BUTTON_STYLUS_SECONDARY) != 0)
+ return 0;
+
+ /* Not a real value. */
+ return 11;
+ }
+
+ /* Return the mouse button associated with the specified ACTION_DOWN
+ or ACTION_POINTER_DOWN EVENT.
+
+ Value is 0 if no mouse button was pressed, or the X number of
+ that mouse button. */
+
+ private int
+ buttonForEvent (MotionEvent event)
+ {
+ /* ICS and earlier don't support true mouse button events, so
+ treat all down events as touch screen events. */
+
+ if (Build.VERSION.SDK_INT
+ < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ return 0;
+
+ return whatButtonWasIt (event, true);
+ }
+
+ /* Return the coordinate object associated with the specified
+ EVENT, or null if it is not known. */
+
+ private Coordinate
+ figureChange (MotionEvent event)
+ {
+ int i, truncatedX, truncatedY, pointerIndex, pointerID, count;
+ Coordinate coordinate;
+
+ /* Initialize this variable now. */
+ coordinate = null;
+
+ switch (event.getActionMasked ())
+ {
+ case MotionEvent.ACTION_DOWN:
+ /* Primary pointer pressed with index 0. */
+
+ pointerID = event.getPointerId (0);
+ coordinate = new Coordinate ((int) event.getX (0),
+ (int) event.getY (0),
+ buttonForEvent (event),
+ pointerID);
+ pointerMap.put (pointerID, coordinate);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ /* Primary pointer released with index 0. */
+ pointerID = event.getPointerId (0);
+ coordinate = pointerMap.get (pointerID);
+ pointerMap.delete (pointerID);
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ /* New pointer. Find the pointer ID from the index and place
+ it in the map. */
+ pointerIndex = event.getActionIndex ();
+ pointerID = event.getPointerId (pointerIndex);
+ coordinate = new Coordinate ((int) event.getX (pointerIndex),
+ (int) event.getY (pointerIndex),
+ buttonForEvent (event),
+ pointerID);
+ pointerMap.put (pointerID, coordinate);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ /* Pointer removed. Remove it from the map. */
+ pointerIndex = event.getActionIndex ();
+ pointerID = event.getPointerId (pointerIndex);
+ coordinate = pointerMap.get (pointerID);
+ pointerMap.delete (pointerID);
+ break;
+
+ default:
+
+ /* Loop through each pointer in the event. */
+
+ count = event.getPointerCount ();
+ for (i = 0; i < count; ++i)
+ {
+ pointerID = event.getPointerId (i);
+
+ /* Look up that pointer in the map. */
+ coordinate = pointerMap.get (pointerID);
+
+ if (coordinate != null)
+ {
+ /* See if coordinates have changed. */
+ truncatedX = (int) event.getX (i);
+ truncatedY = (int) event.getY (i);
+
+ if (truncatedX != coordinate.x
+ || truncatedY != coordinate.y)
+ {
+ /* The pointer changed. Update the coordinate and
+ break out of the loop. */
+ coordinate.x = truncatedX;
+ coordinate.y = truncatedY;
+
+ break;
+ }
+ }
+ }
+
+ /* Set coordinate to NULL if the loop failed to find any
+ matching pointer. */
+
+ if (i == count)
+ coordinate = null;
+ }
+
+ /* Return the pointer ID. */
+ return coordinate;
+ }
+
+ /* Return the modifier mask associated with the specified motion
+ EVENT. Replace bits corresponding to Left or Right keys with
+ their corresponding general modifier bits. */
+
+ private int
+ motionEventModifiers (MotionEvent event)
+ {
+ int state;
+
+ state = event.getMetaState ();
+
+ /* Normalize the state by setting the generic modifier bit if
+ either a left or right modifier is pressed. */
+
+ if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
+ || (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
+ state |= KeyEvent.META_ALT_MASK;
+
+ if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
+ || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
+ state |= KeyEvent.META_CTRL_MASK;
+
+ return state;
+ }
+
+ /* Process a single ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_UP,
+ ACTION_POINTER_UP, ACTION_CANCEL, or ACTION_MOVE event.
+
+ Ascertain which coordinate changed and send an appropriate mouse
+ or touch screen event. */
+
+ private void
+ motionEvent (MotionEvent event)
+ {
+ Coordinate coordinate;
+ int modifiers;
+ long time;
+
+ /* Find data associated with this event's pointer. Namely, its
+ current location, whether or not a change has taken place, and
+ whether or not it is a button event. */
+
+ coordinate = figureChange (event);
+
+ if (coordinate == null)
+ return;
+
+ time = event.getEventTime ();
+
+ if (coordinate.button != 0)
+ {
+ /* This event is tied to a mouse click, so report mouse motion
+ and button events. */
+
+ modifiers = motionEventModifiers (event);
+
+ switch (event.getAction ())
+ {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_DOWN:
+ EmacsNative.sendButtonPress (this.handle, coordinate.x,
+ coordinate.y, time, modifiers,
+ coordinate.button);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ EmacsNative.sendButtonRelease (this.handle, coordinate.x,
+ coordinate.y, time, modifiers,
+ coordinate.button);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ EmacsNative.sendMotionNotify (this.handle, coordinate.x,
+ coordinate.y, time);
+ break;
+ }
+ }
+ else
+ {
+ /* This event is a touch event, and the touch ID is the
+ pointer ID. */
+
+ switch (event.getActionMasked ())
+ {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ /* Touch down event. */
+ EmacsNative.sendTouchDown (this.handle, coordinate.x,
+ coordinate.y, time,
+ coordinate.id, 0);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ /* Touch up event. */
+ EmacsNative.sendTouchUp (this.handle, coordinate.x,
+ coordinate.y, time,
+ coordinate.id, 0);
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ /* Touch sequence cancellation event. */
+ EmacsNative.sendTouchUp (this.handle, coordinate.x,
+ coordinate.y, time,
+ coordinate.id,
+ 1 /* ANDROID_TOUCH_SEQUENCE_CANCELED */);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ /* Pointer motion event. */
+ EmacsNative.sendTouchMove (this.handle, coordinate.x,
+ coordinate.y, time,
+ coordinate.id, 0);
+ break;
+ }
+ }
+
+ if (Build.VERSION.SDK_INT
+ < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ return;
+
+ /* Now update the button state. */
+ lastButtonState = event.getButtonState ();
+ return;
+ }
+
+ public boolean
+ onTouchEvent (MotionEvent event)
+ {
+ switch (event.getActionMasked ())
+ {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_MOVE:
+ motionEvent (event);
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean
+ onGenericMotionEvent (MotionEvent event)
+ {
+ switch (event.getAction ())
+ {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ EmacsNative.sendEnterNotify (this.handle, (int) event.getX (),
+ (int) event.getY (),
+ event.getEventTime ());
+ return true;
+
+ case MotionEvent.ACTION_HOVER_MOVE:
+ EmacsNative.sendMotionNotify (this.handle, (int) event.getX (),
+ (int) event.getY (),
+ event.getEventTime ());
+ return true;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+
+ /* If the exit event comes from a button press, its button
+ state will have extra bits compared to the last known
+ button state. Since the exit event will interfere with
+ tool bar button presses, ignore such splurious events. */
+
+ if ((event.getButtonState () & ~lastButtonState) == 0)
+ EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (),
+ (int) event.getY (),
+ event.getEventTime ());
+
+ return true;
+
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_MOVE:
+ /* MotionEvents may either be sent to onGenericMotionEvent or
+ onTouchEvent depending on if Android thinks it is a mouse
+ event or not, but we detect them ourselves. */
+ motionEvent (event);
+ return true;
+
+ case MotionEvent.ACTION_SCROLL:
+ /* Send a scroll event with the specified deltas. */
+ EmacsNative.sendWheel (this.handle, (int) event.getX (),
+ (int) event.getY (),
+ event.getEventTime (),
+ motionEventModifiers (event),
+ event.getAxisValue (MotionEvent.AXIS_HSCROLL),
+ event.getAxisValue (MotionEvent.AXIS_VSCROLL));
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ public synchronized void
+ reparentTo (final EmacsWindow otherWindow, int x, int y)
+ {
+ int width, height;
+
+ /* Reparent this window to the other window. */
+
+ if (parent != null)
+ {
+ synchronized (parent.children)
+ {
+ parent.children.remove (this);
+ }
+ }
+
+ if (otherWindow != null)
+ {
+ synchronized (otherWindow.children)
+ {
+ otherWindow.children.add (this);
+ }
+ }
+
+ parent = otherWindow;
+
+ /* Move this window to the new location. */
+ width = rect.width ();
+ height = rect.height ();
+ rect.left = x;
+ rect.top = y;
+ rect.right = x + width;
+ rect.bottom = y + height;
+
+ /* Now do the work necessary on the UI thread to reparent the
+ window. */
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ EmacsWindowAttachmentManager manager;
+ ViewManager parent;
+
+ /* First, detach this window if necessary. */
+ manager = EmacsWindowAttachmentManager.MANAGER;
+ manager.detachWindow (EmacsWindow.this);
+
+ /* Also unparent this view. */
+
+ /* If the window manager is set, use that instead. */
+ if (windowManager != null)
+ parent = windowManager;
+ else
+ parent = (ViewManager) view.getParent ();
+ windowManager = null;
+
+ if (parent != null)
+ parent.removeView (view);
+
+ /* Next, either add this window as a child of the new
+ parent's view, or make it available again. */
+ if (otherWindow != null)
+ otherWindow.view.addView (view);
+ else if (EmacsWindow.this.isMapped)
+ manager.registerWindow (EmacsWindow.this);
+
+ /* Request relayout. */
+ view.requestLayout ();
+ }
+ });
+ }
+
+ public void
+ makeInputFocus (long time)
+ {
+ /* TIME is currently ignored. Request the input focus now. */
+
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ view.requestFocus ();
+ }
+ });
+ }
+
+ public synchronized void
+ raise ()
+ {
+ /* This does nothing here. */
+ if (parent == null)
+ return;
+
+ synchronized (parent.children)
+ {
+ /* Remove and add this view again. */
+ parent.children.remove (this);
+ parent.children.add (this);
+ }
+
+ /* Request a relayout. */
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ view.raise ();
+ }
+ });
+ }
+
+ public synchronized void
+ lower ()
+ {
+ /* This does nothing here. */
+ if (parent == null)
+ return;
+
+ synchronized (parent.children)
+ {
+ /* Remove and add this view again. */
+ parent.children.remove (this);
+ parent.children.add (this);
+ }
+
+ /* Request a relayout. */
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ view.lower ();
+ }
+ });
+ }
+
+ public synchronized void
+ reconfigure (final EmacsWindow window, final int stackMode)
+ {
+ ListIterator<EmacsWindow> iterator;
+ EmacsWindow object;
+
+ /* This does nothing here. */
+ if (parent == null)
+ return;
+
+ /* If window is NULL, call lower or upper subject to
+ stackMode. */
+
+ if (window == null)
+ {
+ if (stackMode == 1) /* ANDROID_BELOW */
+ lower ();
+ else
+ raise ();
+
+ return;
+ }
+
+ /* Otherwise, if window.parent is distinct from this, return. */
+ if (window.parent != this.parent)
+ return;
+
+ /* Synchronize with the parent's child list. Iterate over each
+ item until WINDOW is encountered, before moving this window to
+ the location prescribed by STACKMODE. */
+
+ synchronized (parent.children)
+ {
+ /* Remove this window from parent.children, for it will be
+ reinserted before or after WINDOW. */
+ parent.children.remove (this);
+
+ /* Create an iterator. */
+ iterator = parent.children.listIterator ();
+
+ while (iterator.hasNext ())
+ {
+ object = iterator.next ();
+
+ if (object == window)
+ {
+ /* Now place this before or after the cursor of the
+ iterator. */
+
+ if (stackMode == 0) /* ANDROID_ABOVE */
+ iterator.add (this);
+ else
+ {
+ iterator.previous ();
+ iterator.add (this);
+ }
+
+ /* Effect the same adjustment upon the view
+ hierarchy. */
+
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ if (stackMode == 0)
+ view.moveAbove (window.view);
+ else
+ view.moveBelow (window.view);
+ }
+ });
+ }
+ }
+
+ /* parent.children does not list WINDOW, which should never
+ transpire. */
+ EmacsNative.emacsAbort ();
+ }
+ }
+
+ public synchronized int[]
+ getWindowGeometry ()
+ {
+ int[] array;
+
+ array = new int[4];
+
+ array[0] = parent != null ? rect.left : xPosition;
+ array[1] = parent != null ? rect.top : yPosition;
+ array[2] = rect.width ();
+ array[3] = rect.height ();
+
+ return array;
+ }
+
+ public void
+ noticeIconified ()
+ {
+ EmacsNative.sendIconified (this.handle);
+ }
+
+ public void
+ noticeDeiconified ()
+ {
+ EmacsNative.sendDeiconified (this.handle);
+ }
+
+ public synchronized void
+ setDontAcceptFocus (final boolean dontAcceptFocus)
+ {
+ /* Update the view's focus state. */
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ view.setFocusable (!dontAcceptFocus);
+ view.setFocusableInTouchMode (!dontAcceptFocus);
+ }
+ });
+ }
+
+ public synchronized void
+ setDontFocusOnMap (final boolean dontFocusOnMap)
+ {
+ this.dontFocusOnMap = dontFocusOnMap;
+ }
+
+ public synchronized boolean
+ getDontFocusOnMap ()
+ {
+ return dontFocusOnMap;
+ }
+
+ public int[]
+ translateCoordinates (int x, int y)
+ {
+ int[] array;
+
+ /* This is supposed to translate coordinates to the root
+ window. */
+ array = new int[2];
+ EmacsService.SERVICE.getLocationOnScreen (view, array);
+
+ /* Now, the coordinates of the view should be in array. Offset X
+ and Y by them. */
+ array[0] += x;
+ array[1] += y;
+
+ /* Return the resulting coordinates. */
+ return array;
+ }
+
+ public void
+ toggleOnScreenKeyboard (final boolean on)
+ {
+ /* Even though InputMethodManager functions are thread safe,
+ `showOnScreenKeyboard' etc must be called from the UI thread in
+ order to avoid deadlocks if the calls happen in tandem with a
+ call to a synchronizing function within
+ `onCreateInputConnection'. */
+
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ if (on)
+ view.showOnScreenKeyboard ();
+ else
+ view.hideOnScreenKeyboard ();
+ }
+ });
+ }
+
+ public String
+ lookupString (int eventSerial)
+ {
+ String any;
+
+ synchronized (eventStrings)
+ {
+ any = eventStrings.remove (eventSerial);
+ }
+
+ return any;
+ }
+
+ public void
+ setFullscreen (final boolean isFullscreen)
+ {
+ EmacsService.SERVICE.runOnUiThread (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ EmacsActivity activity;
+ Object tem;
+
+ fullscreen = isFullscreen;
+ tem = getAttachedConsumer ();
+
+ if (tem != null)
+ {
+ activity = (EmacsActivity) tem;
+ activity.syncFullscreenWith (EmacsWindow.this);
+ }
+ }
+ });
+ }
+
+ public void
+ defineCursor (final EmacsCursor cursor)
+ {
+ /* Don't post this message if pointer icons aren't supported. */
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ view.post (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ if (cursor != null)
+ view.setPointerIcon (cursor.icon);
+ else
+ view.setPointerIcon (null);
+ }
+ });
+ }
+
+ public synchronized void
+ notifyContentRectPosition (int xPosition, int yPosition)
+ {
+ Rect geometry;
+
+ /* Ignore these notifications if not a child of the root
+ window. */
+ if (parent != null)
+ return;
+
+ /* xPosition and yPosition are the position of this window
+ relative to the screen. Set them and request a ConfigureNotify
+ event. */
+
+ if (this.xPosition != xPosition
+ || this.yPosition != yPosition)
+ {
+ this.xPosition = xPosition;
+ this.yPosition = yPosition;
+
+ EmacsNative.sendConfigureNotify (this.handle,
+ System.currentTimeMillis (),
+ xPosition, yPosition,
+ 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, j, x, y, itemCount;
+ String type, uriString;
+ Uri uri;
+ EmacsActivity activity;
+ StringBuilder builder;
+ ContentResolver resolver;
+
+ 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. Skip this when the
+ integer position hasn't changed, for Android sends events
+ even if the movement from the previous position of the drag
+ is less than 1 pixel on either axis. */
+
+ if (x != dndXPosition || y != dndYPosition)
+ {
+ EmacsNative.sendDndDrag (handle, x, y);
+ dndXPosition = x;
+ dndYPosition = y;
+ }
+
+ return true;
+
+ case DragEvent.ACTION_DROP:
+ /* Reset this view's record of the previous drag and drop
+ event's position. */
+ dndXPosition = -1;
+ dndYPosition = -1;
+
+ /* 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 ();
+ itemCount = data.getItemCount ();
+
+ /* If there are insufficient items within the clip data,
+ return false. */
+
+ if (itemCount < 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;
+ }
+ }
+
+ /* There's no plain text data within this clipboard item, so
+ each item within should be treated as a content URI
+ designating a file. */
+
+ /* Collect the URIs into a string with each suffixed
+ by newlines, much as in a text/uri-list. */
+ builder = new StringBuilder ();
+
+ for (i = 0; i < itemCount; ++i)
+ {
+ /* If the item dropped is a URI, send it to the
+ main thread. */
+
+ uri = data.getItemAt (i).getUri ();
+
+ /* Attempt to acquire permissions for this URI;
+ failing which, insert it as text instead. */
+
+ if (uri != null
+ && uri.getScheme () != null
+ && uri.getScheme ().equals ("content")
+ && (activity = EmacsActivity.lastFocusedActivity) != null)
+ {
+ if ((activity.requestDragAndDropPermissions (event) == null))
+ uri = null;
+ else
+ {
+ resolver = activity.getContentResolver ();
+
+ /* Substitute a content file name for the URI, if
+ possible. */
+ uriString = EmacsService.buildContentName (uri, resolver);
+
+ if (uriString != null)
+ {
+ builder.append (uriString).append ("\n");
+ continue;
+ }
+ }
+ }
+
+ if (uri != null)
+ builder.append (uri.toString ()).append ("\n");
+ else
+ {
+ /* Treat each URI that Emacs cannot secure
+ permissions for as plain text. */
+ type = (data.getItemAt (i)
+ .coerceToText (EmacsService.SERVICE)
+ .toString ());
+ EmacsNative.sendDndText (handle, x, y, type);
+ }
+ }
+
+ /* Now send each URI to Emacs. */
+
+ if (builder.length () > 0)
+ EmacsNative.sendDndUri (handle, x, y, builder.toString ());
+ return true;
+
+ default:
+ /* Reset this view's record of the previous drag and drop
+ event's position. */
+ dndXPosition = -1;
+ dndYPosition = -1;
+ }
+
+ return true;
+ }
+
+
+
+ /* Miscellaneous functions for debugging graphics code. */
+
+ /* Recreate the activity to which this window is attached, if any.
+ This is nonfunctional on Android 2.3.7 and earlier. */
+
+ public void
+ recreateActivity ()
+ {
+ final EmacsWindowAttachmentManager.WindowConsumer attached;
+
+ attached = this.attached;
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
+ return;
+
+ view.post (new Runnable () {
+ @Override
+ public void
+ run ()
+ {
+ if (attached instanceof EmacsActivity)
+ ((EmacsActivity) attached).recreate ();
+ }
+ });
+ }
+};