Refactor TermuxActivity

This commit majorly refactors `TermuxActivity` and moves its view components and functions into dedicated classes.

- The view layouts and ids have been given meaningful names, like `termux_activity.xml`.
- The `TerminalToolbarViewPager` class has been created to handle the now called toolbar that shows on the bottom of the terminal view. It currently contains extra keys view defined by `terminal_toolbar_extra_keys_view.xml` file and a text input view defined by `terminal_toolbar_text_input_view.xml` file when user can switch to by swiping left. The input text will now be preserved if android destroys the activity or its recreated.
- The `TermuxSessionsListViewController` class has been created to handle view related functionality of the termux sessions list shown in the left drawer, namely view creation, `onItemClick()`, `onItemLongClick()`, etc. Its list view is defined by `termux_activity.xml` file and each item's layout is defined by the `terminal_sessions_list_item.xml` file.
- The `TextDataUtils` class has been added to the `com.termux.app.utils` package for text utils.
- The design for the `SessionChangedCallback` interface for `TerminalSession` has been majorly changed. Firstly, it has been renamed and moved from `TerminalSession` to the dedicated `TerminalSessionClient` class file. The interface now also supports the termux app centralized logging framework so that `TerminalSession` and `TerminalEmulator` can use them. Previously, `TermuxService` was implementing a wrapper interface, which would then call the real interface defined by the `TermuxActivity` if it was currently bound to the service. This cluttered and partially duplicated the code. Now, the implementation is defined by the `TermuxSessionClientBase` and `TermuxSessionClient` classes. The `TermuxSessionClientBase` implements the `TerminalSessionClient` interface but the definition of the activity related functions do not do anything, only the background ones like the logging functions are fully implemented. The `TermuxSessionClient` class inherits from the `TermuxSessionClientBase` class and provides the implementation for the activity related functions. The design for how this works is that if the `TermuxService` is not bound to `TermuxActivity`, it just passes the `TermuxSessionClientBase` implementation to `TerminalSession`. If the activity is bound at some point, then in `onServiceConnected()` it replaces/updates the client objects stored in `TerminalSession` and `TerminalEmulator` with `TermuxSessionClient`, and then replaces them back with `TermuxSessionClientBase` in `onDestroy()`. This seems to be working for now without an issue.
This commit is contained in:
agnostic-apollo
2021-03-16 05:01:09 +05:00
parent 5e0b29bb6d
commit c9e18e5b93
19 changed files with 1659 additions and 998 deletions

View File

@@ -1,7 +1,6 @@
package com.termux.terminal;
import android.util.Base64;
import android.util.Log;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -153,6 +152,8 @@ public final class TerminalEmulator {
/** The terminal session this emulator is bound to. */
private final TerminalOutput mSession;
TerminalSessionClient mClient;
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
private int mArgIndex;
/** Holds the arguments of the current escape sequence. */
@@ -227,6 +228,8 @@ public final class TerminalEmulator {
public final TerminalColors mColors = new TerminalColors();
private static final String LOG_TAG = "TerminalEmulator";
private boolean isDecsetInternalBitSet(int bit) {
return (mCurrentDecSetFlags & bit) != 0;
}
@@ -279,16 +282,21 @@ public final class TerminalEmulator {
}
}
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows) {
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
mSession = session;
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
mAltBuffer = new TerminalBuffer(columns, rows, rows);
mClient = client;
mRows = rows;
mColumns = columns;
mTabStop = new boolean[mColumns];
reset();
}
public void updateTerminalSessionClient(TerminalSessionClient client) {
mClient = client;
}
public TerminalBuffer getScreen() {
return mScreen;
}
@@ -751,7 +759,7 @@ public final class TerminalEmulator {
if (internalBit != -1) {
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
} else {
Log.e(EmulatorDebug.LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
}
}
@@ -888,7 +896,7 @@ public final class TerminalEmulator {
case "&8": // Undo key - ignore.
break;
default:
Log.w(EmulatorDebug.LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
}
// Respond with invalid request:
mSession.write("\033P0+r" + part + "\033\\");
@@ -900,12 +908,12 @@ public final class TerminalEmulator {
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
}
} else {
Log.e(EmulatorDebug.LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
}
}
} else {
if (LOG_ESCAPE_SEQUENCES)
Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs);
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
}
finishSequence();
}
@@ -995,7 +1003,7 @@ public final class TerminalEmulator {
int externalBit = mArgs[i];
int internalBit = mapDecSetBitToInternalBit(externalBit);
if (internalBit == -1) {
Log.w(EmulatorDebug.LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
} else {
if (b == 's') {
mSavedDecSetFlags |= internalBit;
@@ -1182,7 +1190,7 @@ public final class TerminalEmulator {
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
// some special control character cases, e.g., Control-Space to make a NUL.
// (2) enables this feature for keys including the exceptions listed.
Log.e(EmulatorDebug.LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
break;
default:
parseArg(b);
@@ -1729,7 +1737,7 @@ public final class TerminalEmulator {
int firstArg = mArgs[i + 1];
if (firstArg == 2) {
if (i + 4 > mArgIndex) {
Log.w(EmulatorDebug.LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else {
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
@@ -1754,7 +1762,7 @@ public final class TerminalEmulator {
mBackColor = color;
}
} else {
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, "Invalid color index: " + color);
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
}
} else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
@@ -1771,7 +1779,7 @@ public final class TerminalEmulator {
mBackColor = code - 100 + 8;
} else {
if (LOG_ESCAPE_SEQUENCES)
Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code));
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
}
}
}
@@ -1905,7 +1913,7 @@ public final class TerminalEmulator {
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
mSession.clipboardText(clipboardText);
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
}
break;
case 104:
@@ -2101,7 +2109,7 @@ public final class TerminalEmulator {
}
private void finishSequenceAndLogError(String error) {
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, error);
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
finishSequence();
}

View File

@@ -6,7 +6,6 @@ import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
@@ -31,41 +30,6 @@ import java.util.UUID;
*/
public final class TerminalSession extends TerminalOutput {
/** Callback to be invoked when a {@link TerminalSession} changes. */
public interface SessionChangedCallback {
void onTextChanged(TerminalSession changedSession);
void onTitleChanged(TerminalSession changedSession);
void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text);
void onBell(TerminalSession session);
void onColorsChanged(TerminalSession session);
}
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
FileDescriptor result = new FileDescriptor();
try {
Field descriptorField;
try {
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
} catch (NoSuchFieldException e) {
// For desktop java:
descriptorField = FileDescriptor.class.getDeclaredField("fd");
}
descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1);
}
return result;
}
private static final int MSG_NEW_INPUT = 1;
private static final int MSG_PROCESS_EXITED = 4;
@@ -87,7 +51,7 @@ public final class TerminalSession extends TerminalOutput {
private final byte[] mUtf8InputBuffer = new byte[5];
/** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback;
TerminalSessionClient mClient;
/** The pid of the shell process. 0 if not started and -1 if finished running. */
int mShellPid;
@@ -104,52 +68,32 @@ public final class TerminalSession extends TerminalOutput {
/** Set by the application for user identification of session, not by terminal. */
public String mSessionName;
@SuppressLint("HandlerLeak")
final Handler mMainThreadHandler = new Handler() {
final byte[] mReceiveBuffer = new byte[4 * 1024];
@Override
public void handleMessage(Message msg) {
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
if (bytesRead > 0) {
mEmulator.append(mReceiveBuffer, bytesRead);
notifyScreenUpdate();
}
if (msg.what == MSG_PROCESS_EXITED) {
int exitCode = (Integer) msg.obj;
cleanupResources(exitCode);
mChangeCallback.onSessionFinished(TerminalSession.this);
String exitDescription = "\r\n[Process completed";
if (exitCode > 0) {
// Non-zero process exit.
exitDescription += " (code " + exitCode + ")";
} else if (exitCode < 0) {
// Negated signal.
exitDescription += " (signal " + (-exitCode) + ")";
}
exitDescription += " - press Enter]";
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
mEmulator.append(bytesToWrite, bytesToWrite.length);
notifyScreenUpdate();
}
}
};
final Handler mMainThreadHandler = new MainThreadHandler();
private final String mShellPath;
private final String mCwd;
private final String[] mArgs;
private final String[] mEnv;
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
mChangeCallback = changeCallback;
private static final String LOG_TAG = "TerminalSession";
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
this.mShellPath = shellPath;
this.mCwd = cwd;
this.mArgs = args;
this.mEnv = env;
this.mClient = client;
}
/**
* @param client The {@link TerminalSessionClient} interface implementation to allow
* for communication between {@link TerminalSession} and its client.
*/
public void updateTerminalSessionClient(TerminalSessionClient client) {
mClient = client;
if (mEmulator != null)
mEmulator.updateTerminalSessionClient(client);
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
@@ -174,13 +118,13 @@ public final class TerminalSession extends TerminalOutput {
* @param rows The number of rows in the terminal window.
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override
@@ -246,23 +190,23 @@ public final class TerminalSession extends TerminalOutput {
} else if (codePoint <= /* 11 bits */0b11111111111) {
/* 110xxxxx leading byte with leading 5 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
/* 10xxxxxx continuation byte with following 6 bits */
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
/* 1110xxxx leading byte with leading 4 bits */
/* 1110xxxx leading byte with leading 4 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
/* 10xxxxxx continuation byte with following 6 bits */
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
/* 11110xxx leading byte with leading 3 bits */
/* 11110xxx leading byte with leading 3 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
/* 10xxxxxx continuation byte with following 6 bits */
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
}
write(mUtf8InputBuffer, 0, bufferPosition);
@@ -272,9 +216,9 @@ public final class TerminalSession extends TerminalOutput {
return mEmulator;
}
/** Notify the {@link #mChangeCallback} that the screen has changed. */
/** Notify the {@link #mClient} that the screen has changed. */
protected void notifyScreenUpdate() {
mChangeCallback.onTextChanged(this);
mClient.onTextChanged(this);
}
/** Reset state for terminal emulator state. */
@@ -289,7 +233,7 @@ public final class TerminalSession extends TerminalOutput {
try {
Os.kill(mShellPid, OsConstants.SIGKILL);
} catch (ErrnoException e) {
Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
}
}
}
@@ -309,7 +253,7 @@ public final class TerminalSession extends TerminalOutput {
@Override
public void titleChanged(String oldTitle, String newTitle) {
mChangeCallback.onTitleChanged(this);
mClient.onTitleChanged(this);
}
public synchronized boolean isRunning() {
@@ -323,17 +267,17 @@ public final class TerminalSession extends TerminalOutput {
@Override
public void clipboardText(String text) {
mChangeCallback.onClipboardText(this, text);
mClient.onClipboardText(this, text);
}
@Override
public void onBell() {
mChangeCallback.onBell(this);
mClient.onBell(this);
}
@Override
public void onColorsChanged() {
mChangeCallback.onColorsChanged(this);
mClient.onColorsChanged(this);
}
public int getPid() {
@@ -356,10 +300,64 @@ public final class TerminalSession extends TerminalOutput {
return outputPath;
}
} catch (IOException | SecurityException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error getting current directory", e);
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
}
return null;
}
private static FileDescriptor wrapFileDescriptor(int fileDescriptor, TerminalSessionClient client) {
FileDescriptor result = new FileDescriptor();
try {
Field descriptorField;
try {
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
} catch (NoSuchFieldException e) {
// For desktop java:
descriptorField = FileDescriptor.class.getDeclaredField("fd");
}
descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1);
}
return result;
}
@SuppressLint("HandlerLeak")
class MainThreadHandler extends Handler {
final byte[] mReceiveBuffer = new byte[4 * 1024];
@Override
public void handleMessage(Message msg) {
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
if (bytesRead > 0) {
mEmulator.append(mReceiveBuffer, bytesRead);
notifyScreenUpdate();
}
if (msg.what == MSG_PROCESS_EXITED) {
int exitCode = (Integer) msg.obj;
cleanupResources(exitCode);
mClient.onSessionFinished(TerminalSession.this);
String exitDescription = "\r\n[Process completed";
if (exitCode > 0) {
// Non-zero process exit.
exitDescription += " (code " + exitCode + ")";
} else if (exitCode < 0) {
// Negated signal.
exitDescription += " (signal " + (-exitCode) + ")";
}
exitDescription += " - press Enter]";
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
mEmulator.append(bytesToWrite, bytesToWrite.length);
notifyScreenUpdate();
}
}
}
}

View File

@@ -0,0 +1,37 @@
package com.termux.terminal;
/**
* The interface for communication between {@link TerminalSession} and its client. It is used to
* send callbacks to the client when {@link TerminalSession} changes or for sending other
* back data to the client like logs.
*/
public interface TerminalSessionClient {
void onTextChanged(TerminalSession changedSession);
void onTitleChanged(TerminalSession changedSession);
void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text);
void onBell(TerminalSession session);
void onColorsChanged(TerminalSession session);
void logError(String tag, String message);
void logWarn(String tag, String message);
void logInfo(String tag, String message);
void logDebug(String tag, String message);
void logVerbose(String tag, String message);
void logStackTraceWithMessage(String tag, String message, Exception e);
void logStackTrace(String tag, Exception e);
}