/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- Copyright (C) 2023-2025 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 . */ package org.gnu.emacs; import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; 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.ViewGroup; 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"; /* Whether any windows have yet been created in this session. */ private static boolean initialWindowCreated; 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 children; /* Map between pointer identifiers and last known position. Used to compute which pointer changed upon a touch event. */ private SparseArray pointerMap; /* The window consumer currently attached, if it exists. */ private EmacsWindowManager.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 release of the quit keycode, generally KEYCODE_VOLUME_DOWN. This is used to signal quit upon two rapid presses of such key. */ private long lastQuitKeyRelease; /* Linked list of character strings which were recently sent as events. */ public LinkedHashMap 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; /* Identifier binding this window to the activity created for it, or -1 if the window should be attached to system-created activities (i.e. the activity launched by the system at startup). Value is meaningless under API level 29 and earlier. */ public long attachmentToken; /* Whether this window should be preserved during window pruning, and whether this window has previously been attached to a task. */ public boolean preserve, previouslyAttached; /* The window manager name of this window, which supplies the name of activities in which it is displayed as a toplevel window, or NULL. */ public String wmName; public EmacsWindow (final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) { rect = new Rect (x, y, x + width, y + height); pointerMap = new SparseArray (); /* 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; /* The initial frame should always be bound to the startup activity. */ if (!initialWindowCreated) { this.attachmentToken = -1; initialWindowCreated = true; } /* Create the list of children. */ children = new ArrayList (); 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 (); /* Create the map of input method-committed strings. Keep at most ten strings in the map. */ eventStrings = new LinkedHashMap () { @Override protected boolean removeEldestEntry (Map.Entry 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); } } /* This is just a sanity test and is not reliable since `children' may be modified between isEmpty and handle destruction. */ 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 () { @Override public void run () { ViewManager parent; EmacsWindowManager manager; /* Invalidate the focus; this should transfer the input focus to the next eligible window as this window is no longer present in parent.children. */ EmacsActivity.invalidateFocus (4); if (EmacsActivity.focusedWindow == EmacsWindow.this) EmacsActivity.focusedWindow = null; manager = EmacsWindowManager.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 (EmacsWindowManager.WindowConsumer consumer) { attached = consumer; } public EmacsWindowManager.WindowConsumer getAttachedConsumer () { return attached; } public synchronized long viewLayout (int left, int top, int right, int bottom) { int rectWidth, rectHeight; /* If this is an override-redirect window, don't ever modify rect.left and rect.top, as its WM window will always have been moved in unison with itself. */ if (overrideRedirect) { rect.right = rect.left + (right - left); rect.bottom = rect.top + (bottom - top); } /* If parent is null, use xPosition and yPosition instead of the geometry rectangle positions. */ else if (parent == null) { rect.left = xPosition; rect.top = yPosition; rect.right = rect.left + (right - left); rect.bottom = rect.top + (bottom - top); } /* Otherwise accept the new position offered by the toolkit. FIXME: isn't there a potential race condition here if the toolkit lays out EmacsView after a child frame's rect is set but before it calls onLayout to read the modified rect? */ else { rect.left = left; rect.top = top; rect.right = right; rect.bottom = bottom; } rectWidth = right - left; rectHeight = bottom - top; 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) { WindowManager.LayoutParams params; /* Set the layout parameters again. */ params = getWindowLayoutParams (); view.setLayoutParams (params); /* Announce this update to the window server. */ if (windowManager != null) windowManager.updateViewLayout (view, params); } 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_PANEL; 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 () { EmacsWindowManager manager; WindowManager windowManager; Activity ctx; Object tem; WindowManager.LayoutParams params; /* Make the view visible, first of all. */ view.setVisibility (View.VISIBLE); if (!overrideRedirect) { manager = EmacsWindowManager.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 { 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 () { 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 () { EmacsWindowManager manager; manager = EmacsWindowManager.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 representing 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 = KeyEvent.normalizeMetaState (event.getMetaState ()); 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 boolean onKeyDown (int keyCode, KeyEvent event) { int state, state_1, extra_ignored, unicode_char; 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 true; } state = eventModifiers (event); /* Meta isn't supported by systems older than Android 3.0. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) extra_ignored = 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; } unicode_char = getEventUnicodeChar (event, state_1); /* If a NUMPAD_ key is detected for which no character is returned, return false without sending the key event, as this will prompt the system to send an event with the corresponding action key. */ if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN && unicode_char == 0) return false; synchronized (eventStrings) { serial = EmacsNative.sendKeyPress (this.handle, event.getEventTime (), state, keyCode, unicode_char); characters = event.getCharacters (); if (characters != null && characters.length () > 1) saveUnicodeString ((int) serial, characters); } return true; } public boolean onKeyUp (int keyCode, KeyEvent event) { int state, state_1, unicode_char, extra_ignored; long time; /* Compute the event's modifier mask. */ state = eventModifiers (event); /* Meta isn't supported by systems older than Android 3.0. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) extra_ignored = 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 true; /* Dispatch the key press event that was deferred till now. */ EmacsNative.sendKeyPress (this.handle, event.getEventTime (), state, keyCode, unicode_char); } /* If a NUMPAD_ key is detected for which no character is returned, return false without sending the key event, as this will prompt the system to send an event with the corresponding action key. */ else if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN && unicode_char == 0) return false; EmacsNative.sendKeyRelease (this.handle, event.getEventTime (), state, keyCode, unicode_char); if (keyCode == EmacsNative.getQuitKeycode ()) { /* 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 - lastQuitKeyRelease < 350) EmacsNative.quit (); lastQuitKeyRelease = time; } return true; } public void onFocusChanged (boolean gainFocus) { EmacsActivity.invalidateFocus (gainFocus ? 6 : 5); } /* Notice that the activity (or its task) has been detached or destroyed by explicit user action. */ public void onActivityDetached () { 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 () { EmacsWindowManager manager; ViewManager parent; /* Invalidate the focus; this should transfer the input focus to the next eligible window as this window is no longer present in parent.children. */ EmacsActivity.invalidateFocus (7); /* First, detach this window if necessary. */ manager = EmacsWindowManager.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 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 void setWmName (final String wmName) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override public void run () { EmacsActivity activity; Object tem; EmacsWindow.this.wmName = wmName; /* If an activity is already attached, replace its task description. */ tem = getAttachedConsumer (); if (tem != null && tem instanceof EmacsActivity) { activity = (EmacsActivity) tem; activity.updateWmName (); } } }); } public int[] translateCoordinates (int x, int y) { int[] array; /* This is supposed to translate coordinates to the root window, whose origin point, in this context, is that of the toplevel activity host to this view. */ array = new int[2]; EmacsService.SERVICE.getLocationInWindow (view, array); /* Now, the coordinates of the view should be in array. Offset X and Y by them. */ array[0] += x; array[1] += y; /* In the case of an override redirect window, the WM window's extents and position match the Emacs window exactly. */ if (overrideRedirect) { synchronized (this) { array[0] += rect.left; array[1] += rect.top; } } /* Return the resulting coordinates. */ return array; } public void toggleOnScreenKeyboard (final boolean on) { FutureTask task; /* 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'. */ task = new FutureTask (new Callable () { @Override public Void call () { if (on) view.showOnScreenKeyboard (); else view.hideOnScreenKeyboard (); return null; } }); /* Block Lisp until this request to display the on-screen keyboard is registered by the UI thread, or updates arising from a redisplay that are reported between the two events will be liable to run afoul of the IMM's cache of selection positions and never reach the input method, if it is currently hidden, as input methods receive outdated selection information reported during the previous call to `onCreateInputConnection' when first displayed. Chances are this is a long-standing bug in the system. */ EmacsService.syncRunnable (task); } 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 && tem instanceof EmacsActivity) { 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 EmacsWindowManager.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 (); } }); } };