diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c2a95f7d0..f3962f444c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,8 +23,8 @@ diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 7490d62c30..ac887bcce1 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -55,6 +55,7 @@ import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ArrayAdapter; import android.widget.EditText; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -115,6 +116,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection TermuxPreferences mSettings; + SessionChangedCallback mSessionChangedCallback; + /** * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to * {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in @@ -206,6 +209,7 @@ public boolean ensureStoragePermissionGranted() { } @Override + @TargetApi(Build.VERSION_CODES.N) public void onCreate(Bundle bundle) { super.onCreate(bundle); @@ -307,6 +311,14 @@ public void onTextSet(String text) { } }); + View newWindowButton = findViewById(R.id.new_window_button); + newWindowButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + addNewWindow(); + } + }); + findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -335,6 +347,10 @@ public boolean onLongClick(View v) { checkForFontAndColors(); mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + onMultiWindowModeChanged(isInMultiWindowMode()); + } } void toggleShowExtraKeys() { @@ -356,7 +372,7 @@ void toggleShowExtraKeys() { public void onServiceConnected(ComponentName componentName, IBinder service) { mTermService = ((TermuxService.LocalBinder) service).service; - mTermService.mSessionChangeCallback = new SessionChangedCallback() { + mSessionChangedCallback = new SessionChangedCallback() { @Override public void onTextChanged(TerminalSession changedSession) { if (!mIsVisible) return; @@ -375,6 +391,12 @@ public void onTitleChanged(TerminalSession updatedSession) { mListViewAdapter.notifyDataSetChanged(); } + @Override + public void onSessionCreated(TerminalSession createdSession) { + if (!mIsVisible) return; + mListViewAdapter.notifyDataSetChanged(); + } + @Override public void onSessionFinished(final TerminalSession finishedSession) { if (mTermService.mWantsToStop) { @@ -392,6 +414,18 @@ public void onSessionFinished(final TerminalSession finishedSession) { mListViewAdapter.notifyDataSetChanged(); } + @Override + public void onSessionRemoved(TerminalSession removedSession) { + if (!mIsVisible) return; + mListViewAdapter.notifyDataSetChanged(); + } + + @Override + public void onSessionRenamed(TerminalSession renamedSession) { + if (!mIsVisible) return; + mListViewAdapter.notifyDataSetChanged(); + } + @Override public void onClipboardText(TerminalSession session, String text) { if (!mIsVisible) return; @@ -422,7 +456,20 @@ public void onBell(TerminalSession session) { public void onColorsChanged(TerminalSession changedSession) { if (getCurrentTermSession() == changedSession) updateBackgroundColor(); } + + @Override + public void onAttach(TerminalSession attachedSession) { + if (!mIsVisible) return; + mListViewAdapter.notifyDataSetChanged(); + } + + @Override + public void onDetach(TerminalSession detachedSession) { + if (!mIsVisible) return; + mListViewAdapter.notifyDataSetChanged(); + } }; + mTermService.mSessionChangeCallbacks.add(mSessionChangedCallback); ListView listView = (ListView) findViewById(R.id.left_drawer_list); mListViewAdapter = new ArrayAdapter(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) { @@ -464,8 +511,15 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { } int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED; firstLineView.setTextColor(color); + row.setEnabled(isEnabled(position)); + return row; } + + @Override + public boolean isEnabled(int position) { + return !getItem(position).mAttached || getCurrentTermSession() == getItem(position); + } }; listView.setAdapter(mListViewAdapter); listView.setOnItemClickListener(new OnItemClickListener() { @@ -485,7 +539,9 @@ public boolean onItemLongClick(AdapterView parent, View view, final int posit } }); - if (mTermService.getSessions().isEmpty()) { + TerminalSession sessionToAttach = mTermService.getFirstUnattachedSession(); + + if (sessionToAttach == null) { if (mIsVisible) { TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() { @Override @@ -514,7 +570,7 @@ public void onClick(DialogInterface dialog, int which) { finish(); } } else { - switchToSession(getStoredCurrentSessionOrLast()); + switchToSession(sessionToAttach); } } @@ -535,7 +591,7 @@ void renameSession(final TerminalSession sessionToRename) { @Override public void onTextSet(String text) { sessionToRename.mSessionName = text; - mListViewAdapter.notifyDataSetChanged(); + mTermService.onSessionRenamed(sessionToRename); } }, -1, null, -1, null, null); } @@ -544,6 +600,13 @@ public void onTextSet(String text) { public void onServiceDisconnected(ComponentName name) { if (mTermService != null) { // Respect being stopped from the TermuxService notification action. + TerminalSession currentSession = getCurrentTermSession(); + + if (currentSession != null) { + currentSession.mAttached = false; + mTermService.onDetach(currentSession); + } + finish(); } } @@ -560,7 +623,9 @@ public void onStart() { if (mTermService != null) { // The service has connected, but data may have changed since we were last in the foreground. - switchToSession(getStoredCurrentSessionOrLast()); + if (getCurrentTermSession() == null) { + switchToSession(getStoredCurrentSessionOrLast()); + } mListViewAdapter.notifyDataSetChanged(); } @@ -595,7 +660,9 @@ public void onDestroy() { super.onDestroy(); if (mTermService != null) { // Do not leave service with references to activity. - mTermService.mSessionChangeCallback = null; + mTermService.mSessionChangeCallbacks.remove(mSessionChangedCallback); + getCurrentTermSession().mAttached = false; + mTermService.onDetach(getCurrentTermSession()); mTermService = null; } unbindService(this); @@ -620,9 +687,22 @@ void addNewSession(boolean failSafe, String sessionName) { } } + @TargetApi(Build.VERSION_CODES.N) + void addNewWindow() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !isInMultiWindowMode()) return; + + Intent intent = new Intent(TermuxActivity.this, TermuxActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + startActivity(intent); + } + /** Try switching to session and note about it, but do nothing if already displaying the session. */ void switchToSession(TerminalSession session) { + TerminalSession oldSession = getCurrentTermSession(); + if (mTerminalView.attachSession(session)) { + if(oldSession != null) mTermService.onDetach(oldSession); + mTermService.onAttach(session); noteSessionInfo(); updateBackgroundColor(); } @@ -648,7 +728,6 @@ void noteSessionInfo() { TerminalSession session = getCurrentTermSession(); final int indexOfSession = mTermService.getSessions().indexOf(session); showToast(toToastTitle(session), false); - mListViewAdapter.notifyDataSetChanged(); final ListView lv = ((ListView) findViewById(R.id.left_drawer_list)); lv.setItemChecked(indexOfSession, true); lv.smoothScrollToPosition(indexOfSession); @@ -857,18 +936,22 @@ void showToast(String text, boolean longDuration) { public void removeFinishedSession(TerminalSession finishedSession) { // Return pressed with finished session - remove it. TermuxService service = mTermService; + service.removeTermSession(finishedSession); + + TerminalSession unattachedSession = mTermService.getFirstUnattachedSession(); - int index = service.removeTermSession(finishedSession); - mListViewAdapter.notifyDataSetChanged(); - if (mTermService.getSessions().isEmpty()) { + if (unattachedSession == null) { // There are no sessions to show, so finish the activity. finish(); } else { - if (index >= service.getSessions().size()) { - index = service.getSessions().size() - 1; - } - switchToSession(service.getSessions().get(index)); + switchToSession(unattachedSession); } } + @Override + @TargetApi(Build.VERSION_CODES.N) + public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { + findViewById(R.id.new_window_button).setVisibility(isInMultiWindowMode ? View.VISIBLE : View.GONE); + ((LinearLayout) findViewById(R.id.drawer_buttons)).setOrientation(isInMultiWindowMode ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + } } diff --git a/app/src/main/java/com/termux/app/TermuxKeyListener.java b/app/src/main/java/com/termux/app/TermuxKeyListener.java index 3e7e375846..4fc025eae6 100644 --- a/app/src/main/java/com/termux/app/TermuxKeyListener.java +++ b/app/src/main/java/com/termux/app/TermuxKeyListener.java @@ -84,6 +84,8 @@ public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession mActivity.renameSession(currentSession); } else if (unicodeChar == 'c'/* create */) { mActivity.addNewSession(false, null); + } else if (unicodeChar == 'w'/* window */) { + mActivity.addNewWindow(); } else if (unicodeChar == 'u' /* urls */) { mActivity.showUrlSelection(); } else if (unicodeChar == 'v') { diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index f7489351fe..25a32bf0f9 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -1,6 +1,7 @@ package com.termux.app; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -56,6 +57,8 @@ public final class TermuxService extends Service implements SessionChangedCallba private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock"; /** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */ public static final String ACTION_EXECUTE = "com.termux.service_execute"; + /** Intent action to open a terminal window to attach to an already running terminal session. */ + public static final String ACTION_SHOW_TERMINAL = "com.termux.service_show_terminal"; public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments"; @@ -77,7 +80,7 @@ class LocalBinder extends Binder { final List mTerminalSessions = new ArrayList<>(); /** Note that the service may often outlive the activity, so need to clear this reference. */ - SessionChangedCallback mSessionChangeCallback; + final List mSessionChangeCallbacks = new ArrayList<>(); private PowerManager.WakeLock mWakeLock; private WifiManager.WifiLock mWifiLock; @@ -139,6 +142,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Launch the main Termux app, which will now show to current session: startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } + } else if (ACTION_SHOW_TERMINAL.equals(action)) { + ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager.getAppTasks().isEmpty()) { + startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } else { + activityManager.getAppTasks().get(0).moveToFront(); + } } else if (action != null) { Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); } @@ -169,11 +179,9 @@ private void updateNotification() { } private Notification buildNotification() { - Intent notifyIntent = new Intent(this, TermuxActivity.class); - // PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing - // activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent": - notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); + Intent notifyIntent = new Intent(this, TermuxService.class); + notifyIntent.setAction(ACTION_SHOW_TERMINAL); + PendingIntent pendingIntent = PendingIntent.getService(this, 0, notifyIntent, 0); int sessionCount = mTerminalSessions.size(); String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s"); @@ -236,6 +244,15 @@ public List getSessions() { return mTerminalSessions; } + public TerminalSession getFirstUnattachedSession() { + for (TerminalSession session : mTerminalSessions) { + if (!session.mAttached) + return session; + } + + return null; + } + TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { new File(HOME_PATH).mkdirs(); @@ -306,6 +323,7 @@ public int removeTermSession(TerminalSession sessionToRemove) { // holding wake lock since there may be daemon processes (e.g. sshd) running. stopSelf(); } else { + onSessionRemoved(sessionToRemove); updateNotification(); } return indexOfRemoved; @@ -313,33 +331,78 @@ public int removeTermSession(TerminalSession sessionToRemove) { @Override public void onTitleChanged(TerminalSession changedSession) { - if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession); + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onTitleChanged(changedSession); + } + } + + @Override + public void onSessionCreated(TerminalSession createdSession) { + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onSessionCreated(createdSession); + } } @Override public void onSessionFinished(final TerminalSession finishedSession) { - if (mSessionChangeCallback != null) - mSessionChangeCallback.onSessionFinished(finishedSession); + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onSessionFinished(finishedSession); + } + } + + @Override + public void onSessionRemoved(TerminalSession removedSession) { + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onSessionRemoved(removedSession); + } + } + + @Override + public void onSessionRenamed(TerminalSession renamedSession) { + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onSessionRenamed(renamedSession); + } } @Override public void onTextChanged(TerminalSession changedSession) { - if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession); + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onTextChanged(changedSession); + } } @Override public void onClipboardText(TerminalSession session, String text) { - if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text); + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onClipboardText(session, text); + } } @Override public void onBell(TerminalSession session) { - if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session); + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onBell(session); + } } @Override public void onColorsChanged(TerminalSession session) { - if (mSessionChangeCallback != null) mSessionChangeCallback.onColorsChanged(session); + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onColorsChanged(session); + } + } + + @Override + public void onAttach(TerminalSession attachedSession) { + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onAttach(attachedSession); + } } + @Override + public void onDetach(TerminalSession detachedSession) { + for (SessionChangedCallback callback : mSessionChangeCallbacks) { + callback.onDetach(detachedSession); + } + } } diff --git a/app/src/main/java/com/termux/terminal/TerminalSession.java b/app/src/main/java/com/termux/terminal/TerminalSession.java index 2eb446a062..1d6c8a4828 100644 --- a/app/src/main/java/com/termux/terminal/TerminalSession.java +++ b/app/src/main/java/com/termux/terminal/TerminalSession.java @@ -36,14 +36,23 @@ public interface SessionChangedCallback { void onTitleChanged(TerminalSession changedSession); + void onSessionCreated(TerminalSession createdSession); + void onSessionFinished(TerminalSession finishedSession); + void onSessionRemoved(TerminalSession removedSession); + + void onSessionRenamed(TerminalSession renamedSession); + void onClipboardText(TerminalSession session, String text); void onBell(TerminalSession session); void onColorsChanged(TerminalSession session); + void onAttach(TerminalSession attachedSession); + + void onDetach(TerminalSession detachedSession); } private static FileDescriptor wrapFileDescriptor(int fileDescriptor) { @@ -103,6 +112,8 @@ private static FileDescriptor wrapFileDescriptor(int fileDescriptor) { /** Set by the application for user identification of session, not by terminal. */ public String mSessionName; + public boolean mAttached; + @SuppressLint("HandlerLeak") final Handler mMainThreadHandler = new Handler() { final byte[] mReceiveBuffer = new byte[4 * 1024]; @@ -149,6 +160,8 @@ public TerminalSession(String shellPath, String cwd, String[] args, String[] env this.mCwd = cwd; this.mArgs = args; this.mEnv = env; + + mChangeCallback.onSessionCreated(this); } /** Inform the attached pty of the new size and reflow or initialize the emulator. */ diff --git a/app/src/main/java/com/termux/view/TerminalView.java b/app/src/main/java/com/termux/view/TerminalView.java index 803b4c2ac7..c4388b4ede 100644 --- a/app/src/main/java/com/termux/view/TerminalView.java +++ b/app/src/main/java/com/termux/view/TerminalView.java @@ -215,7 +215,9 @@ public boolean attachSession(TerminalSession session) { if (session == mTermSession) return false; mTopRow = 0; + if (mTermSession != null) mTermSession.mAttached = false; mTermSession = session; + session.mAttached = true; mEmulator = null; mCombiningAccent = 0; diff --git a/app/src/main/res/drawable/selected_session_background.xml b/app/src/main/res/drawable/selected_session_background.xml index 3db6d6e502..c6412ff6ef 100644 --- a/app/src/main/res/drawable/selected_session_background.xml +++ b/app/src/main/res/drawable/selected_session_background.xml @@ -1,5 +1,6 @@ + - \ No newline at end of file + diff --git a/app/src/main/res/drawable/session_disabled.xml b/app/src/main/res/drawable/session_disabled.xml new file mode 100644 index 0000000000..3324dc6094 --- /dev/null +++ b/app/src/main/res/drawable/session_disabled.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/layout/drawer_layout.xml b/app/src/main/res/layout/drawer_layout.xml index abb00b6fb4..b7161301dd 100644 --- a/app/src/main/res/layout/drawer_layout.xml +++ b/app/src/main/res/layout/drawer_layout.xml @@ -1,4 +1,5 @@ @@ -37,13 +38,16 @@ android:layout_gravity="top" android:layout_weight="1" android:choiceMode="singleChoice" - android:longClickable="true" /> + android:longClickable="true" + android:divider="@null" + android:dividerHeight="0dp" /> + android:orientation="vertical" + android:id="@+id/drawer_buttons">