summaryrefslogtreecommitdiff
path: root/java/org/gnu/emacs/EmacsWindowManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/org/gnu/emacs/EmacsWindowManager.java')
-rw-r--r--java/org/gnu/emacs/EmacsWindowManager.java369
1 files changed, 369 insertions, 0 deletions
diff --git a/java/org/gnu/emacs/EmacsWindowManager.java b/java/org/gnu/emacs/EmacsWindowManager.java
new file mode 100644
index 00000000000..fb4ef6344b2
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsWindowManager.java
@@ -0,0 +1,369 @@
+/* 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> ();
+ }
+
+
+
+
+ /* 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 ()));
+ }
+
+
+
+ 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);
+ }
+
+
+
+ /* 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 ();
+ }
+ }
+};