mirror of https://github.com/termux/termux-app
466 lines
17 KiB
Java
466 lines
17 KiB
Java
package com.termux.app.terminal;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.app.AlertDialog;
|
|
import android.content.ClipData;
|
|
import android.content.ClipboardManager;
|
|
import android.content.Context;
|
|
import android.content.pm.PackageManager;
|
|
import android.graphics.Typeface;
|
|
import android.media.AudioAttributes;
|
|
import android.media.SoundPool;
|
|
import android.text.TextUtils;
|
|
import android.widget.ListView;
|
|
|
|
import com.termux.R;
|
|
import com.termux.shared.shell.TermuxSession;
|
|
import com.termux.shared.interact.DialogUtils;
|
|
import com.termux.app.TermuxActivity;
|
|
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
|
import com.termux.shared.termux.TermuxConstants;
|
|
import com.termux.app.TermuxService;
|
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
|
import com.termux.app.terminal.io.BellHandler;
|
|
import com.termux.shared.logger.Logger;
|
|
import com.termux.terminal.TerminalColors;
|
|
import com.termux.terminal.TerminalSession;
|
|
import com.termux.terminal.TextStyle;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.InputStream;
|
|
import java.util.Properties;
|
|
|
|
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
|
|
|
private final TermuxActivity mActivity;
|
|
|
|
private static final int MAX_SESSIONS = 8;
|
|
|
|
private SoundPool mBellSoundPool;
|
|
|
|
private int mBellSoundId;
|
|
|
|
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
|
|
|
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
|
this.mActivity = activity;
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onCreate() is called
|
|
*/
|
|
public void onCreate() {
|
|
// Set terminal fonts and colors
|
|
checkForFontAndColors();
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onStart() is called
|
|
*/
|
|
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());
|
|
termuxSessionListNotifyUpdated();
|
|
}
|
|
|
|
// The current terminal session may have changed while being away, force
|
|
// a refresh of the displayed terminal.
|
|
mActivity.getTerminalView().onScreenUpdated();
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onResume() is called
|
|
*/
|
|
public void onResume() {
|
|
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
|
|
// the first time bell key is pressed and play() is called, since sound may not be loaded
|
|
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
|
|
getBellSoundPool();
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onStop() is called
|
|
*/
|
|
public void onStop() {
|
|
// Store current session in shared preferences so that it can be restored later in
|
|
// {@link #onStart} if needed.
|
|
setCurrentStoredSession();
|
|
|
|
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
|
|
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
|
|
// Bell is not played in background anyways
|
|
// Related: https://stackoverflow.com/a/28708351/14686958
|
|
releaseBellSoundPool();
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.reloadActivityStyling() is called
|
|
*/
|
|
public void onReload() {
|
|
// Set terminal fonts and colors
|
|
checkForFontAndColors();
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public void onTextChanged(TerminalSession changedSession) {
|
|
if (!mActivity.isVisible()) return;
|
|
|
|
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
|
}
|
|
|
|
@Override
|
|
public void onTitleChanged(TerminalSession updatedSession) {
|
|
if (!mActivity.isVisible()) return;
|
|
|
|
if (updatedSession != mActivity.getCurrentSession()) {
|
|
// Only show toast for other sessions than the current one, since the user
|
|
// probably consciously caused the title change to change in the current session
|
|
// and don't want an annoying toast for that.
|
|
mActivity.showToast(toToastTitle(updatedSession), true);
|
|
}
|
|
|
|
termuxSessionListNotifyUpdated();
|
|
}
|
|
|
|
@Override
|
|
public void onSessionFinished(final TerminalSession finishedSession) {
|
|
TermuxService service = mActivity.getTermuxService();
|
|
|
|
if (service == null || service.wantsToStop()) {
|
|
// The service wants to stop as soon as possible.
|
|
mActivity.finishActivityIfNotFinishing();
|
|
return;
|
|
}
|
|
|
|
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
|
// Show toast for non-current sessions that exit.
|
|
int indexOfSession = service.getIndexOfSession(finishedSession);
|
|
// Verify that session was not removed before we got told about it finishing:
|
|
if (indexOfSession >= 0)
|
|
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
|
}
|
|
|
|
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
|
// On Android TV devices we need to use older behaviour because we may
|
|
// not be able to have multiple launcher icons.
|
|
if (service.getTermuxSessionsSize() > 1) {
|
|
removeFinishedSession(finishedSession);
|
|
}
|
|
} else {
|
|
// Once we have a separate launcher icon for the failsafe session, it
|
|
// should be safe to auto-close session on exit code '0' or '130'.
|
|
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
|
|
removeFinishedSession(finishedSession);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onClipboardText(TerminalSession session, String text) {
|
|
if (!mActivity.isVisible()) return;
|
|
|
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
|
}
|
|
|
|
@Override
|
|
public void onBell(TerminalSession session) {
|
|
if (!mActivity.isVisible()) return;
|
|
|
|
switch (mActivity.getProperties().getBellBehaviour()) {
|
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
|
|
BellHandler.getInstance(mActivity).doBell();
|
|
break;
|
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
|
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
|
break;
|
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
|
// Ignore the bell character.
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onColorsChanged(TerminalSession changedSession) {
|
|
if (mActivity.getCurrentSession() == changedSession)
|
|
updateBackgroundColor();
|
|
}
|
|
|
|
@Override
|
|
public void onTerminalCursorStateChange(boolean enabled) {
|
|
// Do not start cursor blinking thread if activity is not visible
|
|
if (enabled && !mActivity.isVisible()) {
|
|
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
|
|
return;
|
|
}
|
|
|
|
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
|
|
// otherwise stop cursor blinking
|
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public Integer getTerminalCursorStyle() {
|
|
return mActivity.getProperties().getTerminalCursorStyle();
|
|
}
|
|
|
|
|
|
|
|
/** Initialize and get mBellSoundPool */
|
|
private synchronized SoundPool getBellSoundPool() {
|
|
if (mBellSoundPool == null) {
|
|
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
|
|
|
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
|
}
|
|
|
|
return mBellSoundPool;
|
|
}
|
|
|
|
/** Release mBellSoundPool resources */
|
|
private synchronized void releaseBellSoundPool() {
|
|
if (mBellSoundPool != null) {
|
|
mBellSoundPool.release();
|
|
mBellSoundPool = null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Try switching to session. */
|
|
public void setCurrentSession(TerminalSession session) {
|
|
if (session == null) return;
|
|
|
|
if (mActivity.getTerminalView().attachSession(session)) {
|
|
// notify about switched session if not already displaying the session
|
|
notifyOfSessionChange();
|
|
}
|
|
|
|
// 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);
|
|
updateBackgroundColor();
|
|
}
|
|
|
|
void notifyOfSessionChange() {
|
|
if (!mActivity.isVisible()) return;
|
|
|
|
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
|
|
TerminalSession session = mActivity.getCurrentSession();
|
|
mActivity.showToast(toToastTitle(session), false);
|
|
}
|
|
}
|
|
|
|
public void switchToSession(boolean forward) {
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return;
|
|
|
|
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
|
|
int index = service.getIndexOfSession(currentTerminalSession);
|
|
int size = service.getTermuxSessionsSize();
|
|
if (forward) {
|
|
if (++index >= size) index = 0;
|
|
} else {
|
|
if (--index < 0) index = size - 1;
|
|
}
|
|
|
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
|
if (termuxSession != null)
|
|
setCurrentSession(termuxSession.getTerminalSession());
|
|
}
|
|
|
|
public void switchToSession(int index) {
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return;
|
|
|
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
|
if (termuxSession != null)
|
|
setCurrentSession(termuxSession.getTerminalSession());
|
|
}
|
|
|
|
@SuppressLint("InflateParams")
|
|
public void renameSession(final TerminalSession sessionToRename) {
|
|
if (sessionToRename == null) return;
|
|
|
|
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
|
sessionToRename.mSessionName = text;
|
|
termuxSessionListNotifyUpdated();
|
|
}, -1, null, -1, null, null);
|
|
}
|
|
|
|
public void addNewSession(boolean isFailSafe, String sessionName) {
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return;
|
|
|
|
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
|
|
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
|
|
.setPositiveButton(android.R.string.ok, null).show();
|
|
} else {
|
|
TerminalSession currentSession = mActivity.getCurrentSession();
|
|
|
|
String workingDirectory;
|
|
if (currentSession == null) {
|
|
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
|
|
} else {
|
|
workingDirectory = currentSession.getCwd();
|
|
}
|
|
|
|
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
|
|
if (newTermuxSession == null) return;
|
|
|
|
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
|
|
setCurrentSession(newTerminalSession);
|
|
|
|
mActivity.getDrawer().closeDrawers();
|
|
}
|
|
}
|
|
|
|
public void setCurrentStoredSession() {
|
|
TerminalSession currentSession = mActivity.getCurrentSession();
|
|
if (currentSession != null)
|
|
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
|
|
else
|
|
mActivity.getPreferences().setCurrentSession(null);
|
|
}
|
|
|
|
/** The current session as stored or the last one if that does not exist. */
|
|
public TerminalSession getCurrentStoredSessionOrLast() {
|
|
TerminalSession stored = getCurrentStoredSession();
|
|
|
|
if (stored != null) {
|
|
// If a stored session is in the list of currently running sessions, then return it
|
|
return stored;
|
|
} else {
|
|
// Else return the last session currently running
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return null;
|
|
|
|
TermuxSession termuxSession = service.getLastTermuxSession();
|
|
if (termuxSession != null)
|
|
return termuxSession.getTerminalSession();
|
|
else
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private TerminalSession getCurrentStoredSession() {
|
|
String sessionHandle = mActivity.getPreferences().getCurrentSession();
|
|
|
|
// If no session is stored in shared preferences
|
|
if (sessionHandle == null)
|
|
return null;
|
|
|
|
// Check if the session handle found matches one of the currently running sessions
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return null;
|
|
|
|
return service.getTerminalSessionForHandle(sessionHandle);
|
|
}
|
|
|
|
public void removeFinishedSession(TerminalSession finishedSession) {
|
|
// Return pressed with finished session - remove it.
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return;
|
|
|
|
int index = service.removeTermuxSession(finishedSession);
|
|
|
|
int size = service.getTermuxSessionsSize();
|
|
if (size == 0) {
|
|
// There are no sessions to show, so finish the activity.
|
|
mActivity.finishActivityIfNotFinishing();
|
|
} else {
|
|
if (index >= size) {
|
|
index = size - 1;
|
|
}
|
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
|
if (termuxSession != null)
|
|
setCurrentSession(termuxSession.getTerminalSession());
|
|
}
|
|
}
|
|
|
|
public void termuxSessionListNotifyUpdated() {
|
|
mActivity.termuxSessionListNotifyUpdated();
|
|
}
|
|
|
|
public void checkAndScrollToSession(TerminalSession session) {
|
|
if (!mActivity.isVisible()) return;
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return;
|
|
|
|
final int indexOfSession = service.getIndexOfSession(session);
|
|
if (indexOfSession < 0) return;
|
|
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);
|
|
}
|
|
|
|
|
|
String toToastTitle(TerminalSession session) {
|
|
TermuxService service = mActivity.getTermuxService();
|
|
if (service == null) return null;
|
|
|
|
final int indexOfSession = service.getIndexOfSession(session);
|
|
if (indexOfSession < 0) return null;
|
|
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
|
|
if (!TextUtils.isEmpty(session.mSessionName)) {
|
|
toastTitle.append(" ").append(session.mSessionName);
|
|
}
|
|
String title = session.getTitle();
|
|
if (!TextUtils.isEmpty(title)) {
|
|
// Space to "[${NR}] or newline after session name:
|
|
toastTitle.append(session.mSessionName == null ? " " : "\n");
|
|
toastTitle.append(title);
|
|
}
|
|
return toastTitle.toString();
|
|
}
|
|
|
|
|
|
public void checkForFontAndColors() {
|
|
try {
|
|
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
|
|
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
|
|
|
|
final Properties props = new Properties();
|
|
if (colorsFile.isFile()) {
|
|
try (InputStream in = new FileInputStream(colorsFile)) {
|
|
props.load(in);
|
|
}
|
|
}
|
|
|
|
TerminalColors.COLOR_SCHEME.updateWith(props);
|
|
TerminalSession session = mActivity.getCurrentSession();
|
|
if (session != null && session.getEmulator() != null) {
|
|
session.getEmulator().mColors.reset();
|
|
}
|
|
updateBackgroundColor();
|
|
|
|
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
|
mActivity.getTerminalView().setTypeface(newTypeface);
|
|
} catch (Exception e) {
|
|
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
|
|
}
|
|
}
|
|
|
|
public void updateBackgroundColor() {
|
|
if (!mActivity.isVisible()) return;
|
|
TerminalSession session = mActivity.getCurrentSession();
|
|
if (session != null && session.getEmulator() != null) {
|
|
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
|
|
}
|
|
}
|
|
|
|
}
|