diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f45b30f69c..aceb349787 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ + diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 0c9f74125b..4c941076a0 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -10,6 +10,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.view.ContextMenu; @@ -22,6 +23,7 @@ import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.Toast; @@ -175,6 +177,11 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo private float mTerminalToolbarDefaultHeight; + /** + * Unique identifier for this activity instance. Used for session attachment tracking. + */ + private final int mActivityId = System.identityHashCode(this); + private static final int CONTEXT_MENU_SELECT_URL_ID = 0; private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1; @@ -251,6 +258,8 @@ public void onCreate(Bundle savedInstanceState) { setToggleKeyboardView(); + setNewWindowButtonView(); + registerForContextMenu(mTerminalView); FileReceiverActivity.updateFileReceiverActivityComponentsState(this); @@ -352,9 +361,22 @@ public void onDestroy() { if (mIsInvalidState) return; + // Detach all sessions owned by this activity and reset their clients if (mTermuxService != null) { - // Do not leave service and session clients with references to activity. - mTermuxService.unsetTermuxTerminalSessionClient(); + TerminalSession currentSession = getCurrentSession(); + if (currentSession != null) { + // Reset this session's client to the service client + mTermuxService.resetSessionClient(currentSession); + } + if (mTerminalView != null) { + mTerminalView.detachSession(); + } + + // Detach all sessions owned by this activity (single source of truth) + mTermuxService.detachAllSessionsForActivity(mActivityId); + + // Remove this activity's client from the service's set + mTermuxService.removeTermuxTerminalSessionClient(mTermuxTerminalSessionActivityClient); mTermuxService = null; } @@ -374,6 +396,23 @@ public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true); } + @Override + public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { + super.onMultiWindowModeChanged(isInMultiWindowMode); + Logger.logDebug(LOG_TAG, "onMultiWindowModeChanged: " + isInMultiWindowMode); + } + + /** Add a new window */ + public void addNewWindow() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Intent intent = new Intent(this, TermuxActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | + Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + startActivity(intent); + } + } + @@ -417,11 +456,33 @@ public void onServiceConnected(ComponentName componentName, IBinder service) { // then the original intent will be re-delivered, resulting in a new session being re-added // each time. if (!mIsActivityRecreated && intent != null && Intent.ACTION_RUN.equals(intent.getAction())) { - // Android 7.1 app shortcut from res/xml/shortcuts.xml. - boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false); - mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null); - } else { - mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast()); + if (intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_NEW_WINDOW, false)) { + // Android 7.1 app shortcut: open a new window + addNewWindow(); + } else { + // Android 7.1 app shortcut from res/xml/shortcuts.xml. + boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false); + mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null); + } + } + + // Ensure this activity is attached to a session + if (getCurrentSession() == null) { + TerminalSession sessionToAttach = mTermuxService.claimFirstUnattachedSession(mActivityId); + if (sessionToAttach != null) { + mTermuxTerminalSessionActivityClient.setCurrentSession(sessionToAttach); + } else { + // All sessions are attached, create a new one or use stored session + TerminalSession storedSession = mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast(); + if (storedSession != null && !mTermuxService.isSessionAttached(storedSession)) { + mTermuxTerminalSessionActivityClient.setCurrentSession(storedSession); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) { + // In multi-window mode with all sessions attached, create a new session + mTermuxTerminalSessionActivityClient.addNewSession(false, null); + } else { + mTermuxTerminalSessionActivityClient.setCurrentSession(storedSession); + } + } } } @@ -594,6 +655,21 @@ private void setToggleKeyboardView() { }); } + private void setNewWindowButtonView() { + View newWindowButton = findViewById(R.id.new_window_button); + if (newWindowButton != null) { + newWindowButton.setOnClickListener(v -> addNewWindow()); + // Show if multi-window API is available (N+) + newWindowButton.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? View.VISIBLE : View.GONE); + } + + // Use vertical orientation for drawer buttons if multi-window is available + LinearLayout drawerButtons = findViewById(R.id.drawer_buttons); + if (drawerButtons != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + drawerButtons.setOrientation(LinearLayout.VERTICAL); + } + } + @@ -877,6 +953,10 @@ public TermuxService getTermuxService() { return mTermuxService; } + public int getActivityId() { + return mActivityId; + } + public TerminalView getTerminalView() { return mTerminalView; } diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 8025d0bd2c..3cccd600d3 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -50,7 +50,11 @@ import com.termux.terminal.TerminalSessionClient; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** * A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link AppShell} @@ -77,11 +81,18 @@ class LocalBinder extends Binder { private final Handler mHandler = new Handler(); - /** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession} - * that holds activity references for activity related functions. - * Note that the service may often outlive the activity, so need to clear this reference. + /** The full implementations of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession} + * that hold activity references for activity related functions. In multi-window mode, multiple + * activities may be bound simultaneously. Note that the service may often outlive the activities, + * so need to clear these references when activities are destroyed. */ - private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; + private final Set mActivityClients = new HashSet<>(); + + /** + * Single source of truth for session attachment state. Maps session handle to the activity + * instance ID that owns it. If a session handle is not in this map, it is unattached. + */ + private final Map mSessionAttachments = new HashMap<>(); /** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession} * that does not hold activity references and only a service reference. @@ -195,7 +206,7 @@ public boolean onUnbind(Intent intent) { // Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete, // we unset clients here as well if it failed, so that we do not leave service and session // clients with references to the activity. - if (mTermuxTerminalSessionActivityClient != null) + if (!mActivityClients.isEmpty()) unsetTermuxTerminalSessionClient(); return false; } @@ -612,10 +623,8 @@ public synchronized TermuxSession createTermuxSession(ExecutionCommand execution if (executionCommand.isPluginExecutionCommand) mShellManager.mPendingPluginExecutionCommands.remove(executionCommand); - // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if - // activity in is foreground - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated(); + // Notify all activities that sessions list has been updated + notifyAllSessionListsUpdated(); updateNotification(); @@ -649,10 +658,8 @@ public void onTermuxSessionExited(final TermuxSession termuxSession) { mShellManager.mTermuxSessions.remove(termuxSession); - // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if - // activity in is foreground - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated(); + // Notify all activities that sessions list has been updated + notifyAllSessionListsUpdated(); } updateNotification(); @@ -687,8 +694,8 @@ private void handleSessionAction(int sessionAction, TerminalSession newTerminalS switch (sessionAction) { case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY: setCurrentStoredTerminalSession(newTerminalSession); - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession); + if (!mActivityClients.isEmpty()) + mActivityClients.iterator().next().setCurrentSession(newTerminalSession); startTermuxActivity(); break; case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY: @@ -698,8 +705,8 @@ private void handleSessionAction(int sessionAction, TerminalSession newTerminalS break; case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY: setCurrentStoredTerminalSession(newTerminalSession); - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession); + if (!mActivityClients.isEmpty()) + mActivityClients.iterator().next().setCurrentSession(newTerminalSession); break; case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY: if (getTermuxSessionsSize() == 1) @@ -731,37 +738,42 @@ private void startTermuxActivity() { - /** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then + /** If no {@link TermuxActivity} has bound to the {@link TermuxService} yet or all are destroyed, then * interface functions requiring the activity should not be available to the terminal sessions, * so we just return the {@link #mTermuxTerminalSessionServiceClient}. Once {@link TermuxActivity} bind - * callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the - * {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly - * passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the + * callback is received, it should call {@link #setTermuxTerminalSessionClient} to add its client + * to {@link TermuxService#mActivityClients} so that further terminal sessions are directly + * passed a {@link TermuxTerminalSessionActivityClient} object which fully implements the * {@link TerminalSessionClient} interface. * - * @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with - * {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}. + * @return Returns the first available {@link TermuxTerminalSessionActivityClient} if any {@link TermuxActivity} + * has bound with {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}. */ public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() { - if (mTermuxTerminalSessionActivityClient != null) - return mTermuxTerminalSessionActivityClient; + if (!mActivityClients.isEmpty()) + return mActivityClients.iterator().next(); else return mTermuxTerminalSessionServiceClient; } - /** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the - * {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession} - * and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient} - * earlier. + /** This should be called when {@link TermuxActivity#onServiceConnected} is called to add + * the activity's client to {@link TermuxService#mActivityClients}. In multi-window mode, + * multiple activities may be bound simultaneously. * * @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully * implements the {@link TerminalSessionClient} interface. */ public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) { - mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient; + mActivityClients.add(termuxTerminalSessionActivityClient); - for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) - mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient); + // Don't update all sessions' clients here - in multi-window mode, each activity + // should only set the client for its own attached session. The client is set + // when setCurrentSession() is called in TermuxTerminalSessionActivityClient. + } + + /** Remove an activity client when its activity is destroyed. */ + public synchronized void removeTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient client) { + mActivityClients.remove(client); } /** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)} @@ -772,11 +784,129 @@ public synchronized void unsetTermuxTerminalSessionClient() { for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient); - mTermuxTerminalSessionActivityClient = null; + mActivityClients.clear(); + } + + /** Reset a specific session's client to the service client. Used when an activity is destroyed + * in multi-window mode to avoid resetting all sessions' clients. */ + public synchronized void resetSessionClient(TerminalSession session) { + if (session != null) { + session.updateTerminalSessionClient(mTermuxTerminalSessionServiceClient); + } + } + + /** Notify all bound activities to update their session lists. Used when session attachment + * state changes in multi-window mode. */ + public synchronized void notifyAllSessionListsUpdated() { + for (TermuxTerminalSessionActivityClient client : mActivityClients) { + client.termuxSessionListNotifyUpdated(); + } + } + + /** + * Check if a session is attached to any activity. + * @param session The session to check + * @return true if the session is attached to any activity + */ + public synchronized boolean isSessionAttached(TerminalSession session) { + if (session == null) return false; + return mSessionAttachments.containsKey(session.mHandle); + } + + /** + * Check if a session is attached to a different activity than the one specified. + * @param session The session to check + * @param activityId The activity ID to compare against + * @return true if the session is attached to a different activity + */ + public synchronized boolean isSessionAttachedToOther(TerminalSession session, int activityId) { + if (session == null) return false; + Integer owner = mSessionAttachments.get(session.mHandle); + return owner != null && owner != activityId; + } + + /** + * Bring the activity that owns a session to the foreground. + * @param session The session whose owning activity should be focused + * @return true if the activity was found and brought to front + */ + @SuppressLint("MissingPermission") + public synchronized boolean focusActivityForSession(TerminalSession session) { + if (session == null) return false; + Integer ownerId = mSessionAttachments.get(session.mHandle); + if (ownerId == null) return false; + + for (TermuxTerminalSessionActivityClient client : mActivityClients) { + TermuxActivity activity = client.getActivity(); + if (activity != null && activity.getActivityId() == ownerId) { + // Bring the activity's task to front + android.app.ActivityManager am = (android.app.ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + am.moveTaskToFront(activity.getTaskId(), 0); + } + return true; + } + } + return false; + } + + /** + * Attempt to attach a session to an activity. Fails if already attached elsewhere. + * @param session The session to attach + * @param activityId The activity ID claiming the session + * @return true if successfully attached, false if already attached elsewhere + */ + public synchronized boolean attachSession(TerminalSession session, int activityId) { + if (session == null) return false; + Integer currentOwner = mSessionAttachments.get(session.mHandle); + if (currentOwner != null && currentOwner != activityId) { + return false; // Already attached to a different activity + } + mSessionAttachments.put(session.mHandle, activityId); + notifyAllSessionListsUpdated(); + return true; } + /** + * Detach a session from an activity. Only succeeds if the activity owns the session. + * @param session The session to detach + * @param activityId The activity ID releasing the session + */ + public synchronized void detachSession(TerminalSession session, int activityId) { + if (session == null) return; + Integer owner = mSessionAttachments.get(session.mHandle); + if (owner != null && owner == activityId) { + mSessionAttachments.remove(session.mHandle); + notifyAllSessionListsUpdated(); + } + } + /** + * Detach all sessions owned by an activity. Called when activity is destroyed. + * @param activityId The activity ID whose sessions should be detached + */ + public synchronized void detachAllSessionsForActivity(int activityId) { + mSessionAttachments.entrySet().removeIf(entry -> entry.getValue() == activityId); + notifyAllSessionListsUpdated(); + } + /** + * Atomically claim an unattached session by marking it as attached. + * @param activityId The activity ID claiming the session + * @return The claimed session, or null if no unattached sessions are available + */ + @Nullable + public synchronized TerminalSession claimFirstUnattachedSession(int activityId) { + for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) { + TerminalSession session = mShellManager.mTermuxSessions.get(i).getTerminalSession(); + if (!mSessionAttachments.containsKey(session.mHandle)) { + mSessionAttachments.put(session.mHandle, activityId); + notifyAllSessionListsUpdated(); + return session; + } + } + return null; + } private Notification buildNotification() { diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java index bf914b977b..a13c01b354 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java @@ -58,11 +58,15 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { } boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName()); + boolean isCurrentSession = sessionAtRow == mActivity.getCurrentSession(); - if (shouldEnableDarkTheme) { - sessionTitleView.setBackground( - ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected) - ); + // Set background based on whether this is the current session + if (isCurrentSession) { + sessionTitleView.setBackground(ContextCompat.getDrawable(mActivity, + shouldEnableDarkTheme ? R.drawable.current_session_black : R.drawable.current_session)); + } else { + sessionTitleView.setBackground(ContextCompat.getDrawable(mActivity, + shouldEnableDarkTheme ? R.drawable.session_background_black_selected : R.drawable.session_background_selected)); } String name = sessionAtRow.mSessionName; @@ -89,13 +93,34 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { int defaultColor = shouldEnableDarkTheme ? Color.WHITE : Color.BLACK; int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED; sessionTitleView.setTextColor(color); + + // Gray out sessions attached to other windows + boolean isAttachedToOtherWindow = mActivity.getTermuxService() != null && + mActivity.getTermuxService().isSessionAttachedToOther(sessionAtRow, mActivity.getActivityId()); + if (isAttachedToOtherWindow) { + sessionTitleView.setAlpha(0.5f); + } else { + sessionTitleView.setAlpha(1.0f); + } + return sessionRowView; } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { TermuxSession clickedSession = getItem(position); - mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession()); + TerminalSession session = clickedSession.getTerminalSession(); + if (mActivity.getTermuxService() == null) return; + + if (mActivity.getTermuxService().isSessionAttachedToOther(session, mActivity.getActivityId())) { + // Session is attached to another window - focus that window instead + if (!mActivity.getTermuxService().focusActivityForSession(session)) { + mActivity.showToast(mActivity.getString(R.string.msg_failed_to_focus_window), true); + } + } else { + // Session is unattached or attached to this window - switch to it + mActivity.getTermuxTerminalSessionClient().setCurrentSession(session); + } mActivity.getDrawer().closeDrawers(); } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java index bd789145f2..a856725555 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java @@ -54,6 +54,10 @@ public TermuxTerminalSessionActivityClient(TermuxActivity activity) { this.mActivity = activity; } + public TermuxActivity getActivity() { + return mActivity; + } + /** * Should be called when mActivity.onCreate() is called */ @@ -67,10 +71,13 @@ public void onCreate() { */ public void onStart() { // The service has connected, but data may have changed since we were last in the foreground. - // Get the session stored in shared preferences stored by {@link #onStop} if its valid, - // otherwise get the last session currently running. if (mActivity.getTermuxService() != null) { - setCurrentSession(getCurrentStoredSessionOrLast()); + TerminalSession currentSession = mActivity.getCurrentSession(); + // In multi-window mode, keep the current attached session if it's still valid. + // Only restore from preferences if we don't have a valid session attached. + if (currentSession == null || mActivity.getTermuxService().getTermuxSessionForTerminalSession(currentSession) == null) { + setCurrentSession(getCurrentStoredSessionOrLast()); + } termuxSessionListNotifyUpdated(); } @@ -293,14 +300,30 @@ private synchronized void releaseBellSoundPool() { public void setCurrentSession(TerminalSession session) { if (session == null) return; - if (mActivity.getTerminalView().attachSession(session)) { + TermuxService service = mActivity.getTermuxService(); + TerminalSession previousSession = mActivity.getTerminalView().attachSession(session); + if (previousSession != session) { + // Session changed - update attachment state through the service + if (service != null) { + // Detach the previous session from this activity + if (previousSession != null) { + service.detachSession(previousSession, mActivity.getActivityId()); + } + // Attach the new session to this activity + service.attachSession(session, mActivity.getActivityId()); + } + // notify about switched session if not already displaying the session notifyOfSessionChange(); } + // Set this activity's client on the session so it receives callbacks (render updates, etc.) + // This is important for multi-window support where each window needs its own client. + session.updateTerminalSessionClient(this); + // We call the following even when the session is already being displayed since config may // be stale, like current session not selected or scrolled to. - checkAndScrollToSession(session); + scrollToSession(session); updateBackgroundColor(); } @@ -318,17 +341,31 @@ public void switchToSession(boolean forward) { if (service == null) return; TerminalSession currentTerminalSession = mActivity.getCurrentSession(); - int index = service.getIndexOfSession(currentTerminalSession); + int currentIndex = service.getIndexOfSession(currentTerminalSession); int size = service.getTermuxSessionsSize(); - if (forward) { - if (++index >= size) index = 0; - } else { - if (--index < 0) index = size - 1; - } + int activityId = mActivity.getActivityId(); - TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + // Find the next session in the given direction + int index = currentIndex; + for (int i = 0; i < size; i++) { + if (forward) { + if (++index >= size) index = 0; + } else { + if (--index < 0) index = size - 1; + } + + TermuxSession termuxSession = service.getTermuxSession(index); + if (termuxSession != null) { + TerminalSession session = termuxSession.getTerminalSession(); + if (service.isSessionAttachedToOther(session, activityId)) { + // Session is attached to another window - focus that window + service.focusActivityForSession(session); + } else { + setCurrentSession(session); + } + return; + } + } } public void switchToSession(int index) { @@ -336,8 +373,15 @@ public void switchToSession(int index) { if (service == null) return; TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + if (termuxSession != null) { + TerminalSession session = termuxSession.getTerminalSession(); + if (service.isSessionAttachedToOther(session, mActivity.getActivityId())) { + // Session is attached to another window - focus that window + service.focusActivityForSession(session); + } else { + setCurrentSession(session); + } + } } @SuppressLint("InflateParams") @@ -442,12 +486,15 @@ public void removeFinishedSession(TerminalSession finishedSession) { // There are no sessions to show, so finish the activity. mActivity.finishActivityIfNotFinishing(); } else { - if (index >= size) { - index = size - 1; + // Try to atomically claim an unattached session to switch to + TerminalSession unattachedSession = service.claimFirstUnattachedSession(mActivity.getActivityId()); + if (unattachedSession != null) { + setCurrentSession(unattachedSession); + } else { + // All remaining sessions are attached to other windows. + // Close this activity instead of stealing a session from another window. + mActivity.finishActivityIfNotFinishing(); } - TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); } } @@ -455,7 +502,7 @@ public void termuxSessionListNotifyUpdated() { mActivity.termuxSessionListNotifyUpdated(); } - public void checkAndScrollToSession(TerminalSession session) { + public void scrollToSession(TerminalSession session) { if (!mActivity.isVisible()) return; TermuxService service = mActivity.getTermuxService(); if (service == null) return; @@ -465,7 +512,6 @@ public void checkAndScrollToSession(TerminalSession session) { final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list); if (termuxSessionsListView == null) return; - termuxSessionsListView.setItemChecked(indexOfSession, true); // Delay is necessary otherwise sometimes scroll to newly added session does not happen termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000); } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index 700c5e5098..a6e36cc105 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -269,6 +269,8 @@ public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession showUrlSelection(); } else if (unicodeChar == 'v') { doPaste(); + } else if (unicodeChar == 'w'/* new window */) { + mActivity.addNewWindow(); } else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') { // We also check for the shifted char here since shift may be required to produce '+', // see https://github.com/termux/termux-api/issues/2 diff --git a/app/src/main/res/layout/activity_termux.xml b/app/src/main/res/layout/activity_termux.xml index 831ea7cfb8..863534a3c2 100644 --- a/app/src/main/res/layout/activity_termux.xml +++ b/app/src/main/res/layout/activity_termux.xml @@ -65,10 +65,11 @@ android:layout_height="0dp" android:layout_gravity="top" android:layout_weight="1" - android:choiceMode="singleChoice" + android:choiceMode="none" android:longClickable="true" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbd2992ba1..2bfba5a505 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,9 +43,11 @@ New session + New window Failsafe Max terminals reached Close down existing ones before creating new. + Failed to focus window Set session name Set diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml index dcd8d341cc..894d7d9c47 100644 --- a/app/src/main/res/xml/shortcuts.xml +++ b/app/src/main/res/xml/shortcuts.xml @@ -23,6 +23,21 @@ android:name="android.shortcut.conversation"/> + + + + + +