import android.widget.FrameLayout;
public class EmacsActivity extends Activity
- implements EmacsWindowAttachmentManager.WindowConsumer,
+ implements EmacsWindowManager.WindowConsumer,
ViewTreeObserver.OnGlobalLayoutListener
{
public static final String TAG = "EmacsActivity";
}
@Override
- public final void
+ public void
onCreate (Bundle savedInstanceState)
{
FrameLayout.LayoutParams params;
EmacsService.startEmacsService (this);
/* Add this activity to the list of available activities. */
- EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this);
+ EmacsWindowManager.MANAGER.registerWindowConsumer (this);
/* Start observing global layout changes between Jelly Bean and Q.
This is required to restore the fullscreen state whenever the
public final void
onDestroy ()
{
- EmacsWindowAttachmentManager manager;
- boolean isMultitask;
+ EmacsWindowManager manager;
+ boolean isMultitask, reallyFinishing;
- manager = EmacsWindowAttachmentManager.MANAGER;
+ manager = EmacsWindowManager.MANAGER;
/* The activity will die shortly hereafter. If there is a window
attached, close it now. */
isMultitask = this instanceof EmacsMultitaskActivity;
- manager.removeWindowConsumer (this, (isMultitask
- || isReallyFinishing ()));
+ reallyFinishing = isReallyFinishing ();
+ manager.removeWindowConsumer (this, isMultitask || reallyFinishing);
focusedActivities.remove (this);
invalidateFocus (2);
{
isPaused = true;
- EmacsWindowAttachmentManager.MANAGER.noticeIconified (this);
+ EmacsWindowManager.MANAGER.noticeIconified (this);
super.onPause ();
}
isPaused = false;
timeOfLastInteraction = 0;
- EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this);
+ EmacsWindowManager.MANAGER.noticeDeiconified (this);
super.onResume ();
}
EmacsNative.sendNotificationAction (tag, action);
}
+
+ @Override
+ public long
+ getAttachmentToken ()
+ {
+ return -1; /* This is overridden by EmacsMultitaskActivity. */
+ }
+
\f
@Override
package org.gnu.emacs;
-/* This class only exists because EmacsActivity is already defined as
- an activity, and the system wants a new class in order to define a
- new activity. */
+import android.content.Intent;
+
+import android.os.Bundle;
+
+/* In large measure, this class only exists because EmacsActivity is
+ already defined as an activity, and the system requires that every
+ new activity be defined by a new class. */
public final class EmacsMultitaskActivity extends EmacsActivity
{
-
-}
+ /* Token provided by the creator. */
+ private long activityToken;
+
+ @Override
+ public final void
+ onCreate (Bundle savedInstanceState)
+ {
+ Intent intent;
+ String token;
+
+ intent = getIntent ();
+ token = EmacsWindowManager.ACTIVITY_TOKEN;
+
+ if (intent != null)
+ activityToken = intent.getLongExtra (token, -2);
+
+ super.onCreate (savedInstanceState);
+ }
+
+ @Override
+ public final long
+ getAttachmentToken ()
+ {
+ return activityToken;
+ }
+};
if (window == null)
/* Just return all the windows without a parent. */
- windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows ();
+ windowList = EmacsWindowManager.MANAGER.copyWindows ();
else
windowList = window.children;
&& !EmacsNative.shouldForwardMultimediaButtons ())
return false;
+ Log.d (TAG, "onKeyDown: " + event.toString ());
+
window.onKeyDown (keyCode, event);
return true;
}
contextMenu = null;
popupActive = false;
- /* It is not possible to know with 100% certainty which activity
- is currently displaying the context menu. Loop through each
- activity and call `closeContextMenu' instead. */
+ /* It is not possible to know with 100% certainty which activity is
+ currently displaying the context menu. Loop over each activity
+ and call `closeContextMenu' instead. */
- for (EmacsWindowAttachmentManager.WindowConsumer consumer
- : EmacsWindowAttachmentManager.MANAGER.consumers)
+ for (EmacsWindowManager.WindowConsumer consumer
+ : EmacsWindowManager.MANAGER.consumers)
{
if (consumer instanceof EmacsActivity)
((EmacsActivity) consumer).closeContextMenu ();
private SparseArray<Coordinate> pointerMap;
/* The window consumer currently attached, if it exists. */
- private EmacsWindowAttachmentManager.WindowConsumer attached;
+ private EmacsWindowManager.WindowConsumer attached;
/* The window background scratch GC. foreground is always the
window background. */
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;
+
public
EmacsWindow (short handle, final EmacsWindow parent, int x, int y,
int width, int height, boolean overrideRedirect)
run ()
{
ViewManager parent;
- EmacsWindowAttachmentManager manager;
+ EmacsWindowManager manager;
if (EmacsActivity.focusedWindow == EmacsWindow.this)
EmacsActivity.focusedWindow = null;
- manager = EmacsWindowAttachmentManager.MANAGER;
+ manager = EmacsWindowManager.MANAGER;
view.setVisibility (View.GONE);
/* If the window manager is set, use that instead. */
}
public void
- setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer)
+ setConsumer (EmacsWindowManager.WindowConsumer consumer)
{
attached = consumer;
}
- public EmacsWindowAttachmentManager.WindowConsumer
+ public EmacsWindowManager.WindowConsumer
getAttachedConsumer ()
{
return attached;
public void
run ()
{
- EmacsWindowAttachmentManager manager;
+ EmacsWindowManager manager;
WindowManager windowManager;
Activity ctx;
Object tem;
if (!overrideRedirect)
{
- manager = EmacsWindowAttachmentManager.MANAGER;
+ manager = EmacsWindowManager.MANAGER;
/* If parent is the root window, notice that there are new
children available for interested activities to pick
public void
run ()
{
- EmacsWindowAttachmentManager manager;
+ EmacsWindowManager manager;
- manager = EmacsWindowAttachmentManager.MANAGER;
+ manager = EmacsWindowManager.MANAGER;
view.setVisibility (View.GONE);
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. */
+ /* Notice that the activity (or its task) has been detached or
+ destroyed by explicit user action. */
public void
- onActivityDetached (boolean isFinishing)
+ onActivityDetached ()
{
- /* Destroy the associated frame when the activity is detached in
- response to explicit user action. */
-
- if (isFinishing)
- EmacsNative.sendWindowAction (this.handle, 0);
+ EmacsNative.sendWindowAction (this.handle, 0);
}
\f
public void
run ()
{
- EmacsWindowAttachmentManager manager;
+ EmacsWindowManager manager;
ViewManager parent;
/* First, detach this window if necessary. */
- manager = EmacsWindowAttachmentManager.MANAGER;
+ manager = EmacsWindowManager.MANAGER;
manager.detachWindow (EmacsWindow.this);
+ /* Reset window management state. */
+ previouslyAttached = false;
+ attachmentToken = false;
+
/* Also unparent this view. */
/* If the window manager is set, use that instead. */
public void
recreateActivity ()
{
- final EmacsWindowAttachmentManager.WindowConsumer attached;
+ final EmacsWindowManager.WindowConsumer attached;
attached = this.attached;
+++ /dev/null
-/* 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.util.ArrayList;
-import java.util.List;
-
-import android.app.ActivityOptions;
-import android.content.Intent;
-import android.os.Build;
-import android.util.Log;
-
-/* Code to paper over the differences in lifecycles between
- "activities" and windows. There are four interfaces to an instance
- of this class:
-
- registerWindowConsumer (WindowConsumer)
- registerWindow (EmacsWindow)
- removeWindowConsumer (WindowConsumer)
- removeWindow (EmacsWindow)
-
- A WindowConsumer is expected to allow an EmacsWindow to be attached
- to it, and be created or destroyed.
-
- Every time a window is created, registerWindow checks the list of
- window consumers. If a consumer exists and does not currently have
- a window of its own attached, it gets the new window. Otherwise,
- the window attachment manager starts a new consumer.
-
- Every time a consumer is registered, registerWindowConsumer checks
- the list of available windows. If a window exists and is not
- currently attached to a consumer, then the consumer gets it.
-
- Finally, every time a window is removed, the consumer is
- destroyed. */
-
-public final class EmacsWindowAttachmentManager
-{
- private final static String TAG = "EmacsWindowAttachmentManager";
-
- /* The single window attachment manager ``object''. */
- public static final EmacsWindowAttachmentManager MANAGER;
-
- static
- {
- MANAGER = new EmacsWindowAttachmentManager ();
- };
-
- public interface WindowConsumer
- {
- public void attachWindow (EmacsWindow window);
- public EmacsWindow getAttachedWindow ();
- public void detachWindow ();
- public void destroy ();
- };
-
- /* List of currently attached window consumers. */
- public List<WindowConsumer> consumers;
-
- /* List of currently attached windows. */
- public List<EmacsWindow> windows;
-
- public
- EmacsWindowAttachmentManager ()
- {
- consumers = new ArrayList<WindowConsumer> ();
- windows = new ArrayList<EmacsWindow> ();
- }
-
- public void
- registerWindowConsumer (WindowConsumer consumer)
- {
- consumers.add (consumer);
-
- for (EmacsWindow window : windows)
- {
- if (window.getAttachedConsumer () == null)
- {
- consumer.attachWindow (window);
- return;
- }
- }
-
- EmacsNative.sendWindowAction ((short) 0, 0);
- }
-
- public synchronized void
- registerWindow (EmacsWindow window)
- {
- Intent intent;
- ActivityOptions options;
-
- if (windows.contains (window))
- /* The window is already registered. */
- return;
-
- windows.add (window);
-
- for (WindowConsumer consumer : consumers)
- {
- if (consumer.getAttachedWindow () == null)
- {
- consumer.attachWindow (window);
- return;
- }
- }
-
- intent = new Intent (EmacsService.SERVICE,
- EmacsMultitaskActivity.class);
-
- intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
-
- /* Intent.FLAG_ACTIVITY_NEW_DOCUMENT is lamentably unavailable on
- older systems than Lolipop. */
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
- intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
- EmacsService.SERVICE.startActivity (intent);
- else
- {
- /* Specify the desired window size. */
- options = ActivityOptions.makeBasic ();
- options.setLaunchBounds (window.getGeometry ());
- EmacsService.SERVICE.startActivity (intent,
- options.toBundle ());
- }
- }
-
- public void
- removeWindowConsumer (WindowConsumer consumer, boolean isFinishing)
- {
- EmacsWindow window;
-
- window = consumer.getAttachedWindow ();
-
- if (window != null)
- {
- consumer.detachWindow ();
- window.onActivityDetached (isFinishing);
- }
-
- consumers.remove (consumer);
- }
-
- public synchronized void
- detachWindow (EmacsWindow window)
- {
- WindowConsumer consumer;
-
- if (window.getAttachedConsumer () != null)
- {
- consumer = window.getAttachedConsumer ();
-
- consumers.remove (consumer);
- consumer.destroy ();
- }
-
- windows.remove (window);
- }
-
- public void
- noticeIconified (WindowConsumer consumer)
- {
- EmacsWindow window;
-
- /* If a window is attached, send the appropriate iconification
- events. */
- window = consumer.getAttachedWindow ();
-
- if (window != null)
- window.noticeIconified ();
- }
-
- public void
- noticeDeiconified (WindowConsumer consumer)
- {
- EmacsWindow window;
-
- /* If a window is attached, send the appropriate iconification
- events. */
- window = consumer.getAttachedWindow ();
-
- if (window != null)
- window.noticeDeiconified ();
- }
-
- public synchronized List<EmacsWindow>
- copyWindows ()
- {
- return new ArrayList<EmacsWindow> (windows);
- }
-};
--- /dev/null
+/* 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.util.ArrayList;
+import java.util.List;
+
+import android.app.ActivityManager.AppTask;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.TaskInfo;
+
+import android.content.Context;
+import android.content.Intent;
+
+import android.os.Build;
+
+import android.util.Log;
+
+/* Code to paper over the differences in lifecycles between
+ "activities" and windows.
+
+ Four of the five interfaces to be implemented by an instance of this
+ class are relevant on all versions of Android:
+
+ registerWindowConsumer (WindowConsumer)
+ registerWindow (EmacsWindow)
+ removeWindowConsumer (WindowConsumer)
+ removeWindow (EmacsWindow)
+
+ A WindowConsumer is expected to allow an EmacsWindow to be attached
+ to it, and be created or destroyed.
+
+ Whenever a window is created, registerWindow examines the list of
+ window consumers. If a consumer exists and does not currently have a
+ window of its own attached, it gets the new window, while otherwise,
+ the window attachment manager starts a new consumer. Whenever a
+ consumer is registered, registerWindowConsumer checks the list of
+ available windows. If a window exists and is not currently attached
+ to a consumer, then the consumer gets it. Finally, every time a
+ window is removed, the consumer is destroyed.
+
+ getAttachmentToken ()
+
+ should return a token uniquely identifying a consumer, which, on API
+ 29 and up, enables attributing the tasks of activities to the windows
+ for which they were created, and with that, consistent interaction
+ between user-visible window state and their underlying frames. */
+
+public final class EmacsWindowManager
+{
+ private static final String TAG = "EmacsWindowManager";
+ public final static String ACTIVITY_TOKEN = "emacs:activity_token";
+
+ /* The single window attachment manager ``object''. */
+ public static final EmacsWindowManager MANAGER;
+
+ /* Monotonically increasing counter from which multitasking activity
+ tokens are produced. */
+ private static long nextActivityToken;
+
+ /* The ActivityManager. */
+ private ActivityManager activityManager;
+
+ static
+ {
+ MANAGER = new EmacsWindowManager ();
+ };
+
+ public interface WindowConsumer
+ {
+ public void attachWindow (EmacsWindow window);
+ public EmacsWindow getAttachedWindow ();
+ public void detachWindow ();
+ public void destroy ();
+ public long getAttachmentToken ();
+ };
+
+ /* List of currently attached window consumers. */
+ public List<WindowConsumer> consumers;
+
+ /* List of currently attached windows. */
+ public List<EmacsWindow> windows;
+
+ public
+ EmacsWindowManager ()
+ {
+ consumers = new ArrayList<WindowConsumer> ();
+ windows = new ArrayList<EmacsWindow> ();
+ }
+
+
+\f
+
+ /* Return whether the provided WINDOW should be attached to the window
+ consumer CONSUMER. */
+
+ public static boolean
+ isWindowEligible (WindowConsumer consumer, EmacsWindow window)
+ {
+ return (/* The window has yet to be bound. */
+ window.attachmentToken == 0
+ /* Or has already been bound to CONSUMER. */
+ || (window.attachmentToken
+ == consumer.getAttachmentToken ()));
+ }
+
+\f
+
+ public synchronized void
+ registerWindowConsumer (WindowConsumer consumer)
+ {
+ consumers.add (consumer);
+ pruneWindows ();
+
+ for (EmacsWindow window : windows)
+ {
+ if (window.getAttachedConsumer () == null
+ /* Don't attach this window to CONSUMER if incompatible. */
+ && isWindowEligible (consumer, window))
+ {
+ /* Permantly bind this window to the consumer. */
+ window.attachmentToken = consumer.getAttachmentToken ();
+ window.previouslyAttached = true;
+ consumer.attachWindow (window);
+ return;
+ }
+ }
+
+ EmacsNative.sendWindowAction ((short) 0, 0);
+ }
+
+ public synchronized void
+ registerWindow (EmacsWindow window)
+ {
+ Intent intent;
+ ActivityOptions options;
+ long token;
+
+ if (windows.contains (window))
+ /* The window is already registered. */
+ return;
+
+ windows.add (window);
+
+ for (WindowConsumer consumer : consumers)
+ {
+ if (consumer.getAttachedWindow () == null
+ && isWindowEligible (consumer, window))
+ {
+ /* Permantly bind this window to the consumer. */
+ window.attachmentToken = consumer.getAttachmentToken ();
+ window.previouslyAttached = true;
+ consumer.attachWindow (window);
+ return;
+ }
+ }
+
+ intent = new Intent (EmacsService.SERVICE,
+ EmacsMultitaskActivity.class);
+
+ intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ /* Intent.FLAG_ACTIVITY_NEW_DOCUMENT is lamentably unavailable on
+ older systems than Lolipop. */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ EmacsService.SERVICE.startActivity (intent);
+ else
+ {
+ /* Specify the desired window size. */
+ options = ActivityOptions.makeBasic ();
+ options.setLaunchBounds (window.getGeometry ());
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ /* Bind this window to the activity in advance, i.e., before
+ its creation, so that its ID will be recorded in the
+ RecentTasks list. */
+ token = ++nextActivityToken;
+ else
+ /* APIs required for linking activities to windows are not
+ available in earlier Android versions. */
+ token = -2;
+
+ window.attachmentToken = token;
+ intent.putExtra (ACTIVITY_TOKEN, token);
+ EmacsService.SERVICE.startActivity (intent, options.toBundle ());
+ }
+
+ pruneWindows ();
+ }
+
+ public synchronized void
+ removeWindowConsumer (WindowConsumer consumer, boolean isFinishing)
+ {
+ EmacsWindow window;
+
+ window = consumer.getAttachedWindow ();
+
+ if (window != null)
+ {
+ consumer.detachWindow ();
+
+ /* Though pruneWindows will likely remove the same windows, call
+ onActivityDetached anyway if isFinishing is set, as in
+ obscure circumstances pruneWindows will not remove frames
+ bound to the system-started task. */
+ if (isFinishing)
+ window.onActivityDetached ();
+ }
+
+ pruneWindows ();
+ consumers.remove (consumer);
+ }
+
+ public synchronized void
+ detachWindow (EmacsWindow window)
+ {
+ WindowConsumer consumer;
+
+ if (window.getAttachedConsumer () != null)
+ {
+ consumer = window.getAttachedConsumer ();
+
+ consumers.remove (consumer);
+ consumer.destroy ();
+ }
+
+ pruneWindows ();
+ windows.remove (window);
+ }
+
+ public void
+ noticeIconified (WindowConsumer consumer)
+ {
+ EmacsWindow window;
+
+ /* If a window is attached, send the appropriate iconification
+ events. */
+ window = consumer.getAttachedWindow ();
+
+ if (window != null)
+ window.noticeIconified ();
+ }
+
+ public void
+ noticeDeiconified (WindowConsumer consumer)
+ {
+ EmacsWindow window;
+
+ /* If a window is attached, send the appropriate iconification
+ events. */
+ window = consumer.getAttachedWindow ();
+
+ if (window != null)
+ window.noticeDeiconified ();
+ }
+
+ public synchronized List<EmacsWindow>
+ copyWindows ()
+ {
+ return new ArrayList<EmacsWindow> (windows);
+ }
+
+\f
+
+ /* Return the activity token specified in the intent giving rise to
+ TASK, or 0 if absent. */
+
+ private static long
+ getTaskToken (AppTask task)
+ {
+ TaskInfo info;
+
+ info = (TaskInfo) task.getTaskInfo ();
+ return (info.baseIntent != null
+ ? info.baseIntent.getLongExtra (ACTIVITY_TOKEN,
+ -1l)
+ : 0);
+ }
+
+ /* Iterate over each of Emacs's tasks and remove remaining registered
+ windows whose tasks no longer exist. This function should be
+ called upon any event that could plausibly indicate changes in the
+ task list or as to window management. */
+
+ private synchronized void
+ pruneWindows ()
+ {
+ Object object;
+ List<AppTask> appTasks;
+ long taskToken;
+ boolean set;
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
+ || EmacsService.SERVICE == null)
+ return;
+
+ if (activityManager == null)
+ {
+ object
+ = EmacsService.SERVICE.getSystemService (Context.ACTIVITY_SERVICE);
+ activityManager = (ActivityManager) object;
+ }
+
+ appTasks = activityManager.getAppTasks ();
+
+ /* Clear the preserve flag on all toplevel windows. */
+
+ for (EmacsWindow window : windows)
+ window.preserve = false;
+
+ for (AppTask task : appTasks)
+ {
+ taskToken = getTaskToken (task);
+ set = false;
+
+ if (taskToken == 0)
+ continue;
+
+ /* Search for a window with this token. */
+ for (EmacsWindow window : windows)
+ {
+ if (window.attachmentToken == taskToken)
+ {
+ window.preserve = true;
+ set = true;
+ }
+ }
+
+ if (!set)
+ task.finishAndRemoveTask ();
+ }
+
+ /* Now remove toplevel windows without activity tasks. */
+
+ for (EmacsWindow window : windows)
+ {
+ if (window.preserve
+ /* This is not the initial window. */
+ || (window.attachmentToken < 1)
+ /* Nor has it never been attached. */
+ || !window.previouslyAttached)
+ continue;
+
+ window.onActivityDetached ();
+ }
+ }
+};