mTitleStack = new Stack<>();
- /** If processing first character of first parameter of {@link #ESC_CSI}. */
- private boolean mIsCSIStart;
- /** The last character processed of a parameter of {@link #ESC_CSI}. */
- private Integer mLastCSIArg;
-
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
private int mCursorRow, mCursorCol;
/** The number of character rows and columns in the terminal screen. */
public int mRows, mColumns;
+ /** Size of a terminal cell in pixels. */
+ private int mCellWidthPixels, mCellHeightPixels;
+
/** The number of terminal transcript rows that can be scrolled back to. */
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
@@ -176,6 +178,8 @@ public final class TerminalEmulator {
private int mArgIndex;
/** Holds the arguments of the current escape sequence. */
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
+ /** Holds the bit flags which arguments are sub parameters (after a colon) - bit N is set if mArgs[N] is a sub parameter. */
+ private int mArgsSubParamsBitSet = 0;
/** Holds OSC and device control arguments, which can be strings. */
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
@@ -236,15 +240,17 @@ public final class TerminalEmulator {
private boolean mCursorBlinkState;
/**
- * Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
+ * Current foreground, background and underline colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* For a 24-bit value the top byte (0xff000000) is set.
*
+ * Note that the underline color is currently parsed but not yet used during rendering.
+ *
* @see TextStyle
*/
- int mForeColor, mBackColor;
+ int mForeColor, mBackColor, mUnderlineColor;
/** Current {@link TextStyle} effect. */
- private int mEffect;
+ int mEffect;
/**
* The number of scrolled lines since last calling {@link #clearScrollCounter()}. Used for moving selection up along
@@ -252,6 +258,9 @@ public final class TerminalEmulator {
*/
private int mScrollCounter = 0;
+ /** If automatic scrolling of terminal is disabled */
+ private boolean mAutoScrollDisabled;
+
private byte mUtf8ToFollow, mUtf8Index;
private final byte[] mUtf8InputBuffer = new byte[4];
private int mLastEmittedCodePoint = -1;
@@ -312,13 +321,15 @@ static int mapDecSetBitToInternalBit(int decsetBit) {
}
}
- public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
+ public TerminalEmulator(TerminalOutput session, int columns, int rows, int cellWidthPixels, int cellHeightPixels, Integer transcriptRows, TerminalSessionClient client) {
mSession = session;
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
mAltBuffer = new TerminalBuffer(columns, rows, rows);
mClient = client;
mRows = rows;
mColumns = columns;
+ mCellWidthPixels = cellWidthPixels;
+ mCellHeightPixels = cellHeightPixels;
mTabStop = new boolean[mColumns];
reset();
}
@@ -368,7 +379,10 @@ public void sendMouseEvent(int mouseButton, int column, int row, boolean pressed
}
}
- public void resize(int columns, int rows) {
+ public void resize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
+ this.mCellWidthPixels = cellWidthPixels;
+ this.mCellHeightPixels = cellHeightPixels;
+
if (mRows == rows && mColumns == columns) {
return;
} else if (columns < 2 || rows < 2) {
@@ -550,6 +564,15 @@ private void processByte(byte byteToProcess) {
}
public void processCodePoint(int b) {
+ // The Application Program-Control (APC) string might be arbitrary non-printable characters, so handle that early.
+ if (mEscapeState == ESC_APC) {
+ doApc(b);
+ return;
+ } else if (mEscapeState == ESC_APC_ESCAPE) {
+ doApcEscape(b);
+ return;
+ }
+
switch (b) {
case 0: // Null character (NUL, ^@). Do nothing.
break;
@@ -1006,6 +1029,30 @@ private void doDeviceControl(int b) {
}
}
+ /**
+ * When in {@link #ESC_APC} (APC, Application Program Command) sequence.
+ */
+ private void doApc(int b) {
+ if (b == 27) {
+ continueSequence(ESC_APC_ESCAPE);
+ }
+ // Eat APC sequences silently for now.
+ }
+
+ /**
+ * When in {@link #ESC_APC} (APC, Application Program Command) sequence.
+ */
+ private void doApcEscape(int b) {
+ if (b == '\\') {
+ // A String Terminator (ST), ending the APC escape sequence.
+ finishSequence();
+ } else {
+ // The Escape character was not the start of a String Terminator (ST),
+ // but instead just data inside of the APC escape sequence.
+ continueSequence(ESC_APC);
+ }
+ }
+
private int nextTabStop(int numTabs) {
for (int i = mCursorCol + 1; i < mColumns; i++)
if (mTabStop[i] && --numTabs == 0) return Math.min(i, mRightMargin);
@@ -1281,6 +1328,7 @@ private void startEscapeSequence() {
mEscapeState = ESC;
mArgIndex = 0;
Arrays.fill(mArgs, -1);
+ mArgsSubParamsBitSet = 0;
}
private void doLinefeed() {
@@ -1375,8 +1423,8 @@ private void doEsc(int b) {
// http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal
// position on the preceding line. If the active position is at the top margin, a scroll down is performed".
if (mCursorRow <= mTopMargin) {
- mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1);
- blockClear(0, mTopMargin, mColumns);
+ mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, mBottomMargin - (mTopMargin + 1), mLeftMargin, mTopMargin + 1);
+ blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin);
} else {
mCursorRow--;
}
@@ -1390,8 +1438,6 @@ private void doEsc(int b) {
break;
case '[':
continueSequence(ESC_CSI);
- mIsCSIStart = true;
- mLastCSIArg = null;
break;
case '=': // DECKPAM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
@@ -1403,6 +1449,9 @@ private void doEsc(int b) {
case '>': // DECKPNM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
break;
+ case '_': // APC - Application Program Command.
+ continueSequence(ESC_APC);
+ break;
default:
unknownSequence(b);
break;
@@ -1584,8 +1633,8 @@ private void doCsi(int b) {
final int linesToScrollArg = getArg0(1);
final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin;
final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg);
- mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll);
- blockClear(0, mTopMargin, mColumns, linesToScroll);
+ mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesBetweenTopAndBottomMargins - linesToScroll, mLeftMargin, mTopMargin + linesToScroll);
+ blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesToScroll);
} else {
// "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking.
unimplementedSequence(b);
@@ -1712,8 +1761,10 @@ private void doCsi(int b) {
mSession.write("\033[3;0;0t");
break;
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
- // We just report characters time 12 here.
- mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
+ mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * mCellHeightPixels, mColumns * mCellWidthPixels));
+ break;
+ case 16: // Report xterm character cell size in pixels. Result is CSI 6 ; height ; width t
+ mSession.write(String.format(Locale.US, "\033[6;%d;%dt", mCellHeightPixels, mCellWidthPixels));
break;
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
@@ -1762,7 +1813,12 @@ private void doCsi(int b) {
private void selectGraphicRendition() {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) {
- int code = mArgs[i];
+ // Skip leading sub parameters:
+ if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
+ continue;
+ }
+
+ int code = getArg(i, 0, false);
if (code < 0) {
if (mArgIndex > 0) {
continue;
@@ -1781,7 +1837,19 @@ private void selectGraphicRendition() {
} else if (code == 3) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
} else if (code == 4) {
- mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
+ if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
+ // Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
+ i++;
+ if (mArgs[i] == 0) {
+ // No underline.
+ mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
+ } else {
+ // Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
+ mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
+ }
+ } else {
+ mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
+ }
} else if (code == 5) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
} else if (code == 7) {
@@ -1810,8 +1878,8 @@ private void selectGraphicRendition() {
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
} else if (code >= 30 && code <= 37) {
mForeColor = code - 30;
- } else if (code == 38 || code == 48) {
- // Extended set foreground(38)/background (48) color.
+ } else if (code == 38 || code == 48 || code == 58) {
+ // Extended set foreground(38)/background(48)/underline(58) color.
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
// "5;$INDEX" to set an indexed color.
if (i + 2 > mArgIndex) continue;
@@ -1820,27 +1888,30 @@ private void selectGraphicRendition() {
if (i + 4 > mArgIndex) {
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else {
- int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
+ int red = getArg(i + 2, 0, false);
+ int green = getArg(i + 3, 0, false);
+ int blue = getArg(i + 4, 0, false);
+
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
} else {
- int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
- if (code == 38) {
- mForeColor = argbColor;
- } else {
- mBackColor = argbColor;
+ int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
+ switch (code) {
+ case 38: mForeColor = argbColor; break;
+ case 48: mBackColor = argbColor; break;
+ case 58: mUnderlineColor = argbColor; break;
}
}
i += 4; // "2;P_r;P_g;P_r"
}
} else if (firstArg == 5) {
- int color = mArgs[i + 2];
+ int color = getArg(i + 2, 0, false);
i += 2; // "5;P_s"
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
- if (code == 38) {
- mForeColor = color;
- } else {
- mBackColor = color;
+ switch (code) {
+ case 38: mForeColor = color; break;
+ case 48: mBackColor = color; break;
+ case 58: mUnderlineColor = color; break;
}
} else {
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
@@ -1854,6 +1925,8 @@ private void selectGraphicRendition() {
mBackColor = code - 40;
} else if (code == 49) { // Set default background color.
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
+ } else if (code == 59) { // Set default underline color.
+ mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
mForeColor = code - 90 + 8;
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
@@ -2089,67 +2162,64 @@ private void setCursorPosition(int x, int y) {
private void scrollDownOneLine() {
mScrollCounter++;
+ long currentStyle = getStyle();
if (mLeftMargin != 0 || mRightMargin != mColumns) {
// Horizontal margin: Do not put anything into scroll history, just non-margin part of screen up.
mScreen.blockCopy(mLeftMargin, mTopMargin + 1, mRightMargin - mLeftMargin, mBottomMargin - mTopMargin - 1, mLeftMargin, mTopMargin);
// .. and blank bottom row between margins:
- mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', mEffect);
+ mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', currentStyle);
} else {
- mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, getStyle());
+ mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, currentStyle);
}
}
/**
* Process the next ASCII character of a parameter.
*
- * Parameter characters modify the action or interpretation of the sequence. You can use up to
- * 16 parameters per sequence. You must use the ; character to separate parameters.
- * All parameters are unsigned, positive decimal integers, with the most significant
+ *
You must use the ; character to separate parameters and : to separate sub-parameters.
+ *
+ *
Parameter characters modify the action or interpretation of the sequence. Originally
+ * you can use up to 16 parameters per sequence, but following at least xterm and alacritty
+ * we use a common space for parameters and sub-parameters, allowing 32 in total.
+ *
+ *
All parameters are unsigned, positive decimal integers, with the most significant
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
* or omitted parameter indicates a default value for the sequence. For most
* sequences, the default value is 1.
*
- * https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
+ *
References:
+ * VT510 Video Terminal Programmer Information: Control Sequences
+ * alacritty/vte: Implement colon separated CSI parameters
* */
- private void parseArg(int inputByte) {
- int[] bytes = new int[]{inputByte};
- // Only doing this for ESC_CSI and not for other ESC_CSI_* since they seem to be using their
- // own defaults with getArg*() calls, but there may be missed cases
- if (mEscapeState == ESC_CSI) {
- if ((mIsCSIStart && inputByte == ';') || // If sequence starts with a ; character, like \033[;m
- (!mIsCSIStart && mLastCSIArg != null && mLastCSIArg == ';' && inputByte == ';')) { // If sequence contains sequential ; characters, like \033[;;m
- bytes = new int[]{'0', ';'}; // Assume 0 was passed
- }
- }
-
- mIsCSIStart = false;
-
- for (int b : bytes) {
- if (b >= '0' && b <= '9') {
- if (mArgIndex < mArgs.length) {
- int oldValue = mArgs[mArgIndex];
- int thisDigit = b - '0';
- int value;
- if (oldValue >= 0) {
- value = oldValue * 10 + thisDigit;
- } else {
- value = thisDigit;
- }
- if (value > 9999)
- value = 9999;
- mArgs[mArgIndex] = value;
+ private void parseArg(int b) {
+ if (b >= '0' && b <= '9') {
+ if (mArgIndex < mArgs.length) {
+ int oldValue = mArgs[mArgIndex];
+ int thisDigit = b - '0';
+ int value;
+ if (oldValue >= 0) {
+ value = oldValue * 10 + thisDigit;
+ } else {
+ value = thisDigit;
}
- continueSequence(mEscapeState);
- } else if (b == ';') {
- if (mArgIndex < mArgs.length) {
- mArgIndex++;
+ if (value > 9999)
+ value = 9999;
+ mArgs[mArgIndex] = value;
+ }
+ continueSequence(mEscapeState);
+ } else if (b == ';' || b == ':') {
+ if (mArgIndex + 1 < mArgs.length) {
+ mArgIndex++;
+ if (b == ':') {
+ mArgsSubParamsBitSet |= 1 << mArgIndex;
}
- continueSequence(mEscapeState);
} else {
- unknownSequence(b);
+ logError("Too many parameters when in state: " + mEscapeState);
}
- mLastCSIArg = b;
+ continueSequence(mEscapeState);
+ } else {
+ unknownSequence(b);
}
}
@@ -2412,6 +2482,15 @@ public void clearScrollCounter() {
mScrollCounter = 0;
}
+ public boolean isAutoScrollDisabled() {
+ return mAutoScrollDisabled;
+ }
+
+ public void toggleAutoScrollDisabled() {
+ mAutoScrollDisabled = !mAutoScrollDisabled;
+ }
+
+
/** Reset terminal state so user can interact with it regardless of present state. */
public void reset() {
setCursorStyle();
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java
index cbeaf52243..d68dc32623 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java
@@ -11,11 +11,37 @@ public final class TerminalRow {
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
+ /**
+ * Max combining characters that can exist in a column, that are separate from the base character
+ * itself. Any additional combining characters will be ignored and not added to the column.
+ *
+ * There does not seem to be limit in unicode standard for max number of combination characters
+ * that can be combined but such characters are primarily under 10.
+ *
+ * "Section 3.6 Combination" of unicode standard contains combining characters info.
+ * - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
+ * - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
+ * - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
+ *
+ * UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
+ * > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
+ * > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
+ * > yet is well within the buffer size limits of practical implementations.
+ * - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
+ * - https://stackoverflow.com/a/11983435/14686958
+ *
+ * We choose the value 15 because it should be enough for terminal based applications and keep
+ * the memory usage low for a terminal row, won't affect performance or cause terminal to
+ * lag or hang, and will keep malicious applications from causing harm. The value can be
+ * increased if ever needed for legitimate applications.
+ */
+ private static final int MAX_COMBINING_CHARACTERS_PER_COLUMN = 15;
+
/** The number of columns in this terminal row. */
private final int mColumns;
/** The text filling this terminal row. */
public char[] mText;
- /** The number of java char:s used in {@link #mText}. */
+ /** The number of java chars used in {@link #mText}. */
private short mSpaceUsed;
/** If this row has been line wrapped due to text output at the end of line. */
boolean mLineWrap;
@@ -163,18 +189,25 @@ public void setChar(int columnToSet, int codePoint, long style) {
// Get the number of elements in the mText array this column uses now
int oldCharactersUsedForColumn;
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
- oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
+ int oldEndOfColumnIndex = findStartOfColumn(columnToSet + oldCodePointDisplayWidth);
+ oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex;
} else {
// Last character.
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
}
+ // If MAX_COMBINING_CHARACTERS_PER_COLUMN already exist in column, then ignore adding additional combining characters.
+ if (newIsCombining) {
+ int combiningCharsCount = WcWidth.zeroWidthCharsCount(mText, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn);
+ if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
+ return;
+ }
+
// Find how many chars this column will need
int newCharactersUsedForColumn = Character.charCount(codePoint);
if (newIsCombining) {
// Combining characters are added to the contents of the column instead of overwriting them, so that they
// modify the existing contents.
- // FIXME: Put a limit of combining characters.
// FIXME: Unassigned characters also get width=0.
newCharactersUsedForColumn += oldCharactersUsedForColumn;
}
@@ -189,7 +222,7 @@ public void setChar(int columnToSet, int codePoint, long style) {
if (mSpaceUsed + javaCharDifference > text.length) {
// We need to grow the array
char[] newText = new char[text.length + mColumns];
- System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
+ System.arraycopy(text, 0, newText, 0, oldNextColumnIndex);
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
mText = text = newText;
} else {
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java
index c081108d8f..b068be203b 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java
@@ -21,7 +21,7 @@
* A terminal session, consisting of a process coupled to a terminal interface.
*
* The subprocess will be executed by the constructor, and when the size is made known by a call to
- * {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
+ * {@link #updateSize(int, int, int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* All terminal emulation and callback methods will be performed on the main thread.
*
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
@@ -61,7 +61,7 @@ public final class TerminalSession extends TerminalOutput {
/**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
- * {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
+ * {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int, int, int)}.
*/
private int mTerminalFileDescriptor;
@@ -100,12 +100,12 @@ public void updateTerminalSessionClient(TerminalSessionClient client) {
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
- public void updateSize(int columns, int rows) {
+ public void updateSize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
if (mEmulator == null) {
- initializeEmulator(columns, rows);
+ initializeEmulator(columns, rows, cellWidthPixels, cellHeightPixels);
} else {
- JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
- mEmulator.resize(columns, rows);
+ JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns, cellWidthPixels, cellHeightPixels);
+ mEmulator.resize(columns, rows, cellWidthPixels, cellHeightPixels);
}
}
@@ -120,11 +120,11 @@ public String getTitle() {
* @param columns The number of columns in the terminal window.
* @param rows The number of rows in the terminal window.
*/
- public void initializeEmulator(int columns, int rows) {
- mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
+ public void initializeEmulator(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
+ mEmulator = new TerminalEmulator(this, columns, rows, cellWidthPixels, cellHeightPixels, mTranscriptRows, mClient);
int[] processId = new int[1];
- mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
+ mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns, cellWidthPixels, cellHeightPixels);
mShellPid = processId[0];
mClient.setTerminalShellPid(this, mShellPid);
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java b/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java
index 25660b30e8..d71cc277b4 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java
@@ -1,7 +1,7 @@
package com.termux.terminal;
/**
- * Implementation of wcwidth(3) for Unicode 9.
+ * Implementation of wcwidth(3) for Unicode 15.
*
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
*
@@ -9,12 +9,13 @@
* Must be kept in sync with the following:
* https://github.com/termux/wcwidth
* https://github.com/termux/libandroid-support
- * https://github.com/termux/termux-packages/tree/master/libandroid-support
+ * https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
*/
public final class WcWidth {
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
- // at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
+ // from https://github.com/jquast/wcwidth/pull/64
+ // at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private static final int[][] ZERO_WIDTH = {
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
{0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
@@ -40,7 +41,8 @@ public final class WcWidth {
{0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
{0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
- {0x008d3, 0x008e1}, // Arabic Small Low Waw ..Arabic Small High Sign S
+ {0x00898, 0x0089f}, // Arabic Small High Word A..Arabic Half Madda Over M
+ {0x008ca, 0x008e1}, // Arabic Small High Farsi ..Arabic Small High Sign S
{0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
{0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
@@ -74,13 +76,14 @@ public final class WcWidth {
{0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
{0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama
- {0x00b55, 0x00b56}, // (nil) ..Oriya Ai Length Mark
+ {0x00b55, 0x00b56}, // Oriya Sign Overline ..Oriya Ai Length Mark
{0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
{0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An
+ {0x00c3c, 0x00c3c}, // Telugu Sign Nukta ..Telugu Sign Nukta
{0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
{0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
@@ -97,7 +100,7 @@ public final class WcWidth {
{0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
{0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
- {0x00d81, 0x00d81}, // (nil) ..(nil)
+ {0x00d81, 0x00d81}, // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
{0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
{0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
@@ -106,7 +109,7 @@ public final class WcWidth {
{0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
{0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo
- {0x00ec8, 0x00ecd}, // Lao Tone Mai Ek ..Lao Niggahita
+ {0x00ec8, 0x00ece}, // Lao Tone Mai Ek ..(nil)
{0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
{0x00f37, 0x00f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
@@ -130,7 +133,7 @@ public final class WcWidth {
{0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
{0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
- {0x01732, 0x01734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
+ {0x01732, 0x01733}, // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
{0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
{0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
@@ -139,6 +142,7 @@ public final class WcWidth {
{0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
{0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation
+ {0x0180f, 0x0180f}, // Mongolian Free Variation..Mongolian Free Variation
{0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x018a9, 0x018a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x01920, 0x01922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
@@ -154,7 +158,7 @@ public final class WcWidth {
{0x01a65, 0x01a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
{0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
{0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
- {0x01ab0, 0x01ac0}, // Combining Doubled Circum..(nil)
+ {0x01ab0, 0x01ace}, // Combining Doubled Circum..Combining Latin Small Le
{0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
{0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
@@ -177,8 +181,7 @@ public final class WcWidth {
{0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
- {0x01dc0, 0x01df9}, // Combining Dotted Grave A..Combining Wide Inverted
- {0x01dfb, 0x01dff}, // Combining Deletion Mark ..Combining Right Arrowhea
+ {0x01dc0, 0x01dff}, // Combining Dotted Grave A..Combining Right Arrowhea
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
{0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
@@ -193,7 +196,7 @@ public final class WcWidth {
{0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
{0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
- {0x0a82c, 0x0a82c}, // (nil) ..(nil)
+ {0x0a82c, 0x0a82c}, // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
{0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
@@ -233,13 +236,18 @@ public final class WcWidth {
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
{0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
- {0x10eab, 0x10eac}, // (nil) ..(nil)
+ {0x10eab, 0x10eac}, // Yezidi Combining Hamza M..Yezidi Combining Madda M
+ {0x10efd, 0x10eff}, // (nil) ..(nil)
{0x10f46, 0x10f50}, // Sogdian Combining Dot Be..Sogdian Combining Stroke
+ {0x10f82, 0x10f85}, // Old Uyghur Combining Dot..Old Uyghur Combining Two
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
+ {0x11070, 0x11070}, // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
+ {0x11073, 0x11074}, // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
+ {0x110c2, 0x110c2}, // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
@@ -247,11 +255,12 @@ public final class WcWidth {
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
{0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe
- {0x111cf, 0x111cf}, // (nil) ..(nil)
+ {0x111cf, 0x111cf}, // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
+ {0x11241, 0x11241}, // (nil) ..(nil)
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
@@ -283,9 +292,9 @@ public final class WcWidth {
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
- {0x1193b, 0x1193c}, // (nil) ..(nil)
- {0x1193e, 0x1193e}, // (nil) ..(nil)
- {0x11943, 0x11943}, // (nil) ..(nil)
+ {0x1193b, 0x1193c}, // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
+ {0x1193e, 0x1193e}, // Dives Akuru Virama ..Dives Akuru Virama
+ {0x11943, 0x11943}, // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
{0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama
@@ -313,12 +322,20 @@ public final class WcWidth {
{0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
{0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U
+ {0x11f00, 0x11f01}, // (nil) ..(nil)
+ {0x11f36, 0x11f3a}, // (nil) ..(nil)
+ {0x11f40, 0x11f40}, // (nil) ..(nil)
+ {0x11f42, 0x11f42}, // (nil) ..(nil)
+ {0x13440, 0x13440}, // (nil) ..(nil)
+ {0x13447, 0x13455}, // (nil) ..(nil)
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
- {0x16fe4, 0x16fe4}, // (nil) ..(nil)
+ {0x16fe4, 0x16fe4}, // Khitan Small Script Fill..Khitan Small Script Fill
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
+ {0x1cf00, 0x1cf2d}, // Znamenny Combining Mark ..Znamenny Combining Mark
+ {0x1cf30, 0x1cf46}, // Znamenny Combining Tonal..Znamenny Priznak Modifie
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
@@ -335,15 +352,19 @@ public final class WcWidth {
{0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e026, 0x1e02a}, // Combining Glagolitic Let..Combining Glagolitic Let
+ {0x1e08f, 0x1e08f}, // (nil) ..(nil)
{0x1e130, 0x1e136}, // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
+ {0x1e2ae, 0x1e2ae}, // Toto Sign Rising Tone ..Toto Sign Rising Tone
{0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini
+ {0x1e4ec, 0x1e4ef}, // (nil) ..(nil)
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
};
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
- // at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
+ // from https://github.com/jquast/wcwidth/pull/64
+ // at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private static final int[][] WIDE_EASTASIAN = {
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
{0x0231a, 0x0231b}, // Watch ..Hourglass
@@ -392,7 +413,7 @@ public final class WcWidth {
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
{0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto
- {0x03250, 0x04dbf}, // Partnership Sign ..(nil)
+ {0x03250, 0x04dbf}, // Partnership Sign ..Cjk Unified Ideograph-4d
{0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
{0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
@@ -404,13 +425,18 @@ public final class WcWidth {
{0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
- {0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..(nil)
- {0x16ff0, 0x16ff1}, // (nil) ..(nil)
+ {0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..Khitan Small Script Fill
+ {0x16ff0, 0x16ff1}, // Vietnamese Alternate Rea..Vietnamese Alternate Rea
{0x17000, 0x187f7}, // (nil) ..(nil)
- {0x18800, 0x18cd5}, // Tangut Component-001 ..(nil)
+ {0x18800, 0x18cd5}, // Tangut Component-001 ..Khitan Small Script Char
{0x18d00, 0x18d08}, // (nil) ..(nil)
- {0x1b000, 0x1b11e}, // Katakana Letter Archaic ..Hentaigana Letter N-mu-m
+ {0x1aff0, 0x1aff3}, // Katakana Letter Minnan T..Katakana Letter Minnan T
+ {0x1aff5, 0x1affb}, // Katakana Letter Minnan T..Katakana Letter Minnan N
+ {0x1affd, 0x1affe}, // Katakana Letter Minnan N..Katakana Letter Minnan N
+ {0x1b000, 0x1b122}, // Katakana Letter Archaic ..Katakana Letter Archaic
+ {0x1b132, 0x1b132}, // (nil) ..(nil)
{0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo
+ {0x1b155, 0x1b155}, // (nil) ..(nil)
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
@@ -443,24 +469,24 @@ public final class WcWidth {
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
{0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley
- {0x1f6d5, 0x1f6d7}, // Hindu Temple ..(nil)
+ {0x1f6d5, 0x1f6d7}, // Hindu Temple ..Elevator
+ {0x1f6dc, 0x1f6df}, // (nil) ..Ring Buoy
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
- {0x1f6f4, 0x1f6fc}, // Scooter ..(nil)
+ {0x1f6f4, 0x1f6fc}, // Scooter ..Roller Skate
{0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square
- {0x1f90c, 0x1f93a}, // (nil) ..Fencer
+ {0x1f7f0, 0x1f7f0}, // Heavy Equals Sign ..Heavy Equals Sign
+ {0x1f90c, 0x1f93a}, // Pinched Fingers ..Fencer
{0x1f93c, 0x1f945}, // Wrestlers ..Goal Net
- {0x1f947, 0x1f978}, // First Place Medal ..(nil)
- {0x1f97a, 0x1f9cb}, // Face With Pleading Eyes ..(nil)
- {0x1f9cd, 0x1f9ff}, // Standing Person ..Nazar Amulet
- {0x1fa70, 0x1fa74}, // Ballet Shoes ..(nil)
- {0x1fa78, 0x1fa7a}, // Drop Of Blood ..Stethoscope
- {0x1fa80, 0x1fa86}, // Yo-yo ..(nil)
- {0x1fa90, 0x1faa8}, // Ringed Planet ..(nil)
- {0x1fab0, 0x1fab6}, // (nil) ..(nil)
- {0x1fac0, 0x1fac2}, // (nil) ..(nil)
- {0x1fad0, 0x1fad6}, // (nil) ..(nil)
+ {0x1f947, 0x1f9ff}, // First Place Medal ..Nazar Amulet
+ {0x1fa70, 0x1fa7c}, // Ballet Shoes ..Crutch
+ {0x1fa80, 0x1fa88}, // Yo-yo ..(nil)
+ {0x1fa90, 0x1fabd}, // Ringed Planet ..(nil)
+ {0x1fabf, 0x1fac5}, // (nil) ..Person With Crown
+ {0x1face, 0x1fadb}, // (nil) ..(nil)
+ {0x1fae0, 0x1fae8}, // Melting Face ..(nil)
+ {0x1faf0, 0x1faf8}, // Hand With Index Finger A..(nil)
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(nil)
- {0x30000, 0x3fffd}, // (nil) ..(nil)
+ {0x30000, 0x3fffd}, // Cjk Unified Ideograph-30..(nil)
};
@@ -512,4 +538,29 @@ public static int width(char[] chars, int index) {
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
}
+ /**
+ * The zero width characters count like combining characters in the `chars` array from start
+ * index to end index (exclusive).
+ */
+ public static int zeroWidthCharsCount(char[] chars, int start, int end) {
+ if (start < 0 || start >= chars.length)
+ return 0;
+
+ int count = 0;
+ for (int i = start; i < end && i < chars.length;) {
+ if (Character.isHighSurrogate(chars[i])) {
+ if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
+ count++;
+ }
+ i += 2;
+ } else {
+ if (width(chars[i]) <= 0) {
+ count++;
+ }
+ i++;
+ }
+ }
+ return count;
+ }
+
}
diff --git a/terminal-emulator/src/main/jni/termux.c b/terminal-emulator/src/main/jni/termux.c
index 22f1d507d1..8cd5e7891d 100644
--- a/terminal-emulator/src/main/jni/termux.c
+++ b/terminal-emulator/src/main/jni/termux.c
@@ -29,7 +29,9 @@ static int create_subprocess(JNIEnv* env,
char** envp,
int* pProcessId,
jint rows,
- jint columns)
+ jint columns,
+ jint cell_width,
+ jint cell_height)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
@@ -57,7 +59,7 @@ static int create_subprocess(JNIEnv* env,
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
- struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
+ struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns, .ws_xpixel = (unsigned short) (columns * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height)};
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
@@ -121,7 +123,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
jobjectArray envVars,
jintArray processIdArray,
jint rows,
- jint columns)
+ jint columns,
+ jint cell_width,
+ jint cell_height)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
@@ -156,7 +160,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
- int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
+ int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns, cell_width, cell_height);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
@@ -178,9 +182,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
return ptm;
}
-JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
+JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols, jint cell_width, jint cell_height)
{
- struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
+ struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols, .ws_xpixel = (unsigned short) (cols * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height) };
ioctl(fd, TIOCSWINSZ, &sz);
}
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java
new file mode 100644
index 0000000000..4f6292aaa9
--- /dev/null
+++ b/terminal-emulator/src/test/java/com/termux/terminal/ApcTest.java
@@ -0,0 +1,21 @@
+package com.termux.terminal;
+
+public class ApcTest extends TerminalTestCase {
+
+ public void testApcConsumed() {
+ // At time of writing this is part of what yazi sends for probing for kitty graphics protocol support:
+ // https://github.com/sxyazi/yazi/blob/0cdaff98d0b3723caff63eebf1974e7907a43a2c/yazi-adapter/src/emulator.rs#L129
+ // This should not result in anything being written to the screen: If kitty graphics protocol support
+ // is implemented it should instead result in an error code on stdin, and if not it should be consumed
+ // silently just as xterm does. See https://sw.kovidgoyal.net/kitty/graphics-protocol/.
+ withTerminalSized(2, 2)
+ .enterString("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\")
+ .assertLinesAre(" ", " ");
+
+ // It is ok for the APC content to be non printable characters:
+ withTerminalSized(12, 2)
+ .enterString("hello \033_some\023\033_\\apc#end\033\\ world")
+ .assertLinesAre("hello world", " ");
+ }
+
+}
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java
index 5a12edd749..9f123bc1d1 100644
--- a/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java
+++ b/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java
@@ -1,5 +1,7 @@
package com.termux.terminal;
+import java.util.List;
+
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
public class ControlSequenceIntroducerTest extends TerminalTestCase {
@@ -62,4 +64,68 @@ public void testCsi3J() {
assertEquals("y\nz", mTerminal.getScreen().getTranscriptText());
}
+ public void testReportPixelSize() {
+ int columns = 3;
+ int rows = 3;
+ withTerminalSized(columns, rows);
+ int cellWidth = TerminalTest.INITIAL_CELL_WIDTH_PIXELS;
+ int cellHeight = TerminalTest.INITIAL_CELL_HEIGHT_PIXELS;
+ assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
+ assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
+ columns = 23;
+ rows = 33;
+ resize(columns, rows);
+ assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
+ assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
+ cellWidth = 8;
+ cellHeight = 18;
+ mTerminal.resize(columns, rows, cellWidth, cellHeight);
+ assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
+ assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
+ }
+
+ /**
+ * See Colored and styled underlines:
+ *
+ *
+ * [4:0m # no underline
+ * [4:1m # straight underline
+ * [4:2m # double underline
+ * [4:3m # curly underline
+ * [4:4m # dotted underline
+ * [4:5m # dashed underline
+ * [4m # straight underline (for backwards compat)
+ * [24m # no underline (for backwards compat)
+ *
+ *
+ * We currently parse the variants, but map them to normal/no underlines as appropriate
+ */
+ public void testUnderlineVariants() {
+ for (String suffix : List.of("", ":1", ":2", ":3", ":4", ":5")) {
+ for (String stop : List.of("24", "4:0")) {
+ withTerminalSized(3, 3);
+ enterString("\033[4" + suffix + "m").assertLinesAre(" ", " ", " ");
+ assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
+ enterString("\033[4;1m").assertLinesAre(" ", " ", " ");
+ assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
+ enterString("\033[" + stop + "m").assertLinesAre(" ", " ", " ");
+ assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD, mTerminal.mEffect);
+ }
+ }
+ }
+
+ public void testManyParameters() {
+ StringBuilder b = new StringBuilder("\033[");
+ for (int i = 0; i < 30; i++) {
+ b.append("0;");
+ }
+ b.append("4:2");
+ // This clearing of underline should be ignored as the parameters pass the threshold for too many parameters:
+ b.append("4:0m");
+ withTerminalSized(3, 3)
+ .enterString(b.toString())
+ .assertLinesAre(" ", " ", " ");
+ assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
+ }
+
}
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java b/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java
index a252e1a8cb..17f8111205 100644
--- a/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java
+++ b/terminal-emulator/src/test/java/com/termux/terminal/HistoryTest.java
@@ -11,10 +11,10 @@ public void testHistory() {
assertLinesAre("777", "888", "999");
assertHistoryStartsWith("666", "555");
- mTerminal.resize(cols, 2);
+ resize(cols, 2);
assertHistoryStartsWith("777", "666", "555");
- mTerminal.resize(cols, 3);
+ resize(cols, 3);
assertHistoryStartsWith("666", "555");
}
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java
index fa4a702473..6c32d663b4 100644
--- a/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java
+++ b/terminal-emulator/src/test/java/com/termux/terminal/ResizeTest.java
@@ -72,11 +72,11 @@ public void testResizeAfterHistoryWraparound() {
enterString("\r\n");
}
assertLinesAre("998 ", "999 ", " ");
- mTerminal.resize(cols, 2);
+ resize(cols, 2);
assertLinesAre("999 ", " ");
- mTerminal.resize(cols, 5);
+ resize(cols, 5);
assertLinesAre("996 ", "997 ", "998 ", "999 ", " ");
- mTerminal.resize(cols, rows);
+ resize(cols, rows);
assertLinesAre("998 ", "999 ", " ");
}
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java
index cc01c0454b..039605a144 100644
--- a/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java
+++ b/terminal-emulator/src/test/java/com/termux/terminal/ScrollRegionTest.java
@@ -75,6 +75,16 @@ public void testNELRespectsLeftMargin() {
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033[?6h\033ED").assertLinesAre("ABC", " D ", " ");
}
+ public void testRiRespectsLeftMargin() {
+ // Reverse Index (RI), ${ESC}M, should respect horizontal margins:
+ withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033M").assertLinesAre("A D", " BC ", " ");
+ }
+
+ public void testSdRespectsLeftMargin() {
+ // Scroll Down (SD), ${CSI}${N}T, should respect horizontal margins:
+ withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033[2T").assertLinesAre("A D", " ", " BC ");
+ }
+
public void testBackwardIndex() {
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 7.
// Without margins:
@@ -127,4 +137,31 @@ public void testScrollRegionDoesNotLimitCursorMovement() {
" xxx"
);
}
+
+ /**
+ * See reported issue.
+ */
+ public void testClearingWhenScrollingWithMargins() {
+ int newForeground = 2;
+ int newBackground = 3;
+ int size = 3;
+ TerminalTestCase terminal = withTerminalSized(size, size)
+ // Enable horizontal margin and set left margin to 1:
+ .enterString("\033[?69h\033[2s")
+ // Set foreground and background color:
+ .enterString("\033[" + (30 + newForeground) + ";" + (40 + newBackground) + "m")
+ // Enter newlines to scroll down:
+ .enterString("\r\n\r\n\r\n\r\n\r\n");
+ for (int row = 0; row < size; row++) {
+ for (int col = 0; col < size; col++) {
+ // The first column (outside of the scrolling area, due to us setting a left scroll
+ // margin of 1) should be unmodified, the others should use the current style:
+ int expectedForeground = col == 0 ? TextStyle.COLOR_INDEX_FOREGROUND : newForeground;
+ int expectedBackground = col == 0 ? TextStyle.COLOR_INDEX_BACKGROUND : newBackground;
+ terminal.assertForegroundColorAt(row, col, expectedForeground);
+ terminal.assertBackgroundColorAt(row, col, expectedBackground);
+ }
+ }
+ }
+
}
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java
index 89ad9eca67..3e1f824265 100644
--- a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java
+++ b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java
@@ -82,7 +82,7 @@ public void testReportTerminalSize() throws Exception {
assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
for (int width = 3; width < 12; width++) {
for (int height = 3; height < 12; height++) {
- mTerminal.resize(width, height);
+ resize(width, height);
assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
}
}
@@ -137,6 +137,11 @@ public void testPaste() {
}
public void testSelectGraphics() {
+ selectGraphicsTestRun(';');
+ selectGraphicsTestRun(':');
+ }
+
+ public void selectGraphicsTestRun(char separator) {
withTerminalSized(5, 5);
enterString("\033[31m");
assertEquals(mTerminal.mForeColor, 1);
@@ -155,40 +160,59 @@ public void testSelectGraphics() {
// Check TerminalEmulator.parseArg()
enterString("\033[31m\033[m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
- enterString("\033[31m\033[;m");
+ enterString("\033[31m\033[;m".replace(';', separator));
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[0m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
- enterString("\033[31m\033[0;m");
+ enterString("\033[31m\033[0;m".replace(';', separator));
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31;;m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
+ enterString("\033[31::m");
+ assertEquals(1, mTerminal.mForeColor);
+ enterString("\033[31;m");
+ assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
+ enterString("\033[31:m");
+ assertEquals(1, mTerminal.mForeColor);
+ enterString("\033[31;;41m");
+ assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
+ assertEquals(1, mTerminal.mBackColor);
+ enterString("\033[0m");
+ assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
// 256 colors:
- enterString("\033[38;5;119m");
+ enterString("\033[38;5;119m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
- enterString("\033[48;5;129m");
+ enterString("\033[48;5;129m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Invalid parameter:
- enterString("\033[48;8;129m");
+ enterString("\033[48;8;129m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Multiple parameters at once:
- enterString("\033[38;5;178;48;5;179;m");
+ enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator));
assertEquals(178, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
+ // Omitted parameter means zero:
+ enterString("\033[38;5;m".replace(';', separator));
+ assertEquals(0, mTerminal.mForeColor);
+ assertEquals(179, mTerminal.mBackColor);
+ enterString("\033[48;5;m".replace(';', separator));
+ assertEquals(0, mTerminal.mForeColor);
+ assertEquals(0, mTerminal.mBackColor);
+
// 24 bit colors:
enterString(("\033[0m")); // Reset fg and bg colors.
- enterString("\033[38;2;255;127;2m");
+ enterString("\033[38;2;255;127;2m".replace(';', separator));
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
- enterString("\033[48;2;1;2;254m");
+ enterString("\033[48;2;1;2;254m".replace(';', separator));
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
@@ -197,14 +221,30 @@ public void testSelectGraphics() {
enterString(("\033[0m")); // Reset fg and bg colors.
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
- enterString("\033[38;2;255;127;2;48;2;1;2;254m");
+ enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator));
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, invalid input:
- enterString("\033[38;2;300;127;2;48;2;1;300;254m");
+ enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator));
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
+
+ // 24 bit colors, omitted parameter means zero:
+ enterString("\033[38;2;255;127;m".replace(';', separator));
+ expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
+ assertEquals(expectedForeground, mTerminal.mForeColor);
+ assertEquals(expectedBackground, mTerminal.mBackColor);
+ enterString("\033[38;2;123;;77m".replace(';', separator));
+ expectedForeground = 0xff000000 | (123 << 16) | 77;
+ assertEquals(expectedForeground, mTerminal.mForeColor);
+ assertEquals(expectedBackground, mTerminal.mBackColor);
+
+ // 24 bit colors, extra sub-parameters are skipped:
+ expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
+ enterString("\033[0;38:2:255:127:2:48:2:1:2:254m");
+ assertEquals(expectedForeground, mTerminal.mForeColor);
+ assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
}
public void testBackgroundColorErase() {
diff --git a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java
index eb1da8c094..4490207932 100644
--- a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java
+++ b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java
@@ -13,7 +13,10 @@
public abstract class TerminalTestCase extends TestCase {
- public static class MockTerminalOutput extends TerminalOutput {
+ public static final int INITIAL_CELL_WIDTH_PIXELS = 13;
+ public static final int INITIAL_CELL_HEIGHT_PIXELS = 15;
+
+ public static class MockTerminalOutput extends TerminalOutput {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public final List titleChanges = new ArrayList<>();
public final List clipboardPuts = new ArrayList<>();
@@ -108,7 +111,7 @@ protected void setUp() throws Exception {
protected TerminalTestCase withTerminalSized(int columns, int rows) {
// The tests aren't currently using the client, so a null client will suffice, a dummy client should be implemented if needed
- mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2, null);
+ mTerminal = new TerminalEmulator(mOutput, columns, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS, rows * 2, null);
return this;
}
@@ -201,7 +204,7 @@ public TerminalTestCase assertLinesAre(String... lines) {
}
public TerminalTestCase resize(int cols, int rows) {
- mTerminal.resize(cols, rows);
+ mTerminal.resize(cols, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS);
assertInvariants();
return this;
}
@@ -301,6 +304,11 @@ public void assertForegroundColorAt(int externalRow, int column, int color) {
assertEquals(color, TextStyle.decodeForeColor(style));
}
+ public void assertBackgroundColorAt(int externalRow, int column, int color) {
+ long style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
+ assertEquals(color, TextStyle.decodeBackColor(style));
+ }
+
public TerminalTestCase assertColor(int colorIndex, int expected) {
int actual = mTerminal.mColors.mCurrentColors[colorIndex];
if (expected != actual) {
diff --git a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java
index 307e422694..a4bef7d37c 100644
--- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java
+++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java
@@ -233,7 +233,7 @@ private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int
mTextPaint.setColor(foreColor);
// The text alignment is the default Paint.Align.LEFT.
- canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
+ canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint);
}
if (savedMatrix) canvas.restore();
diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java
index 2bed7cabd4..435c102515 100644
--- a/terminal-view/src/main/java/com/termux/view/TerminalView.java
+++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java
@@ -2,6 +2,7 @@
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -10,6 +11,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
+import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
@@ -19,17 +21,20 @@
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
+import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.termux.terminal.KeyHandler;
@@ -81,6 +86,43 @@ public final class TerminalView extends View {
/** If non-zero, this is the last unicode code point received if that was a combining character. */
int mCombiningAccent;
+ /**
+ * The current AutoFill type returned for {@link View#getAutofillType()} by {@link #getAutofillType()}.
+ *
+ * The default is {@link #AUTOFILL_TYPE_NONE} so that AutoFill UI, like toolbar above keyboard
+ * is not shown automatically, like on Activity starts/View create. This value should be updated
+ * to required value, like {@link #AUTOFILL_TYPE_TEXT} before calling
+ * {@link AutofillManager#requestAutofill(View)} so that AutoFill UI shows. The updated value
+ * set will automatically be restored to {@link #AUTOFILL_TYPE_NONE} in
+ * {@link #autofill(AutofillValue)} so that AutoFill UI isn't shown anymore by calling
+ * {@link #resetAutoFill()}.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private int mAutoFillType = AUTOFILL_TYPE_NONE;
+
+ /**
+ * The current AutoFill type returned for {@link View#getImportantForAutofill()} by
+ * {@link #getImportantForAutofill()}.
+ *
+ * The default is {@link #IMPORTANT_FOR_AUTOFILL_NO} so that view is not considered important
+ * for AutoFill. This value should be updated to required value, like
+ * {@link #IMPORTANT_FOR_AUTOFILL_YES} before calling {@link AutofillManager#requestAutofill(View)}
+ * so that Android and apps consider the view as important for AutoFill to process the request.
+ * The updated value set will automatically be restored to {@link #IMPORTANT_FOR_AUTOFILL_NO} in
+ * {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private int mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
+
+ /**
+ * The current AutoFill hints returned for {@link View#getAutofillHints()} ()} by {@link #getAutofillHints()} ()}.
+ *
+ * The default is an empty `string[]`. This value should be updated to required value. The
+ * updated value set will automatically be restored an empty `string[]` in
+ * {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
+ */
+ private String[] mAutoFillHints = new String[0];
+
private final boolean mAccessibilityEnabled;
/** The {@link KeyEvent} is generated from a virtual keyboard, like manually with the {@link KeyEvent#KeyEvent(int, int)} constructor. */
@@ -409,19 +451,29 @@ protected int computeVerticalScrollOffset() {
}
public void onScreenUpdated() {
+ onScreenUpdated(false);
+ }
+
+ public void onScreenUpdated(boolean skipScrolling) {
if (mEmulator == null) return;
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
if (mTopRow < -rowsInHistory) mTopRow = -rowsInHistory;
- boolean skipScrolling = false;
- if (isSelectingText()) {
+ if (isSelectingText() || mEmulator.isAutoScrollDisabled()) {
+
// Do not scroll when selecting text.
int rowShift = mEmulator.getScrollCounter();
if (-mTopRow + rowShift > rowsInHistory) {
// .. unless we're hitting the end of history transcript, in which
// case we abort text selection and scroll to end.
- stopTextSelectionMode();
+ if (isSelectingText())
+ stopTextSelectionMode();
+
+ if (mEmulator.isAutoScrollDisabled()) {
+ mTopRow = -rowsInHistory;
+ skipScrolling = true;
+ }
} else {
skipScrolling = true;
mTopRow -= rowShift;
@@ -446,6 +498,14 @@ public void onScreenUpdated() {
if (mAccessibilityEnabled) setContentDescription(getText());
}
+ /** This must be called by the hosting activity in {@link Activity#onContextMenuClosed(Menu)}
+ * when context menu for the {@link TerminalView} is started by
+ * {@link TextSelectionCursorController#ACTION_MORE} is closed. */
+ public void onContextMenuClosed(Menu menu) {
+ // Unset the stored text since it shouldn't be used anymore and should be cleared from memory
+ unsetStoredSelectedText();
+ }
+
/**
* Sets the text size, which in turn sets the number of rows and columns.
*
@@ -556,11 +616,14 @@ public boolean onTouchEvent(MotionEvent event) {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
- ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clipData = clipboard.getPrimaryClip();
+ ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData != null) {
- CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
- if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
+ ClipData.Item clipItem = clipData.getItemAt(0);
+ if (clipItem != null) {
+ CharSequence text = clipItem.coerceToText(getContext());
+ if (!TextUtils.isEmpty(text)) mEmulator.paste(text.toString());
+ }
}
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
switch (event.getAction()) {
@@ -584,6 +647,7 @@ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
+ cancelRequestAutoFill();
if (isSelectingText()) {
stopTextSelectionMode();
return true;
@@ -848,6 +912,9 @@ public boolean handleKeyCode(int keyCode, int keyMod) {
if (mEmulator != null)
mEmulator.setCursorBlinkState(true);
+ if (handleKeyCodeAction(keyCode, keyMod))
+ return true;
+
TerminalEmulator term = mTermSession.getEmulator();
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
if (code == null) return false;
@@ -855,6 +922,26 @@ public boolean handleKeyCode(int keyCode, int keyMod) {
return true;
}
+ public boolean handleKeyCodeAction(int keyCode, int keyMod) {
+ boolean shiftDown = (keyMod & KeyHandler.KEYMOD_SHIFT) != 0;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_PAGE_UP:
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ // shift+page_up and shift+page_down should scroll scrollback history instead of
+ // scrolling command history or changing pages
+ if (shiftDown) {
+ long time = SystemClock.uptimeMillis();
+ MotionEvent motionEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -mEmulator.mRows : mEmulator.mRows);
+ motionEvent.recycle();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Called when a key is released in the view.
*
@@ -902,7 +989,7 @@ public void updateSize() {
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
- mTermSession.updateSize(newColumns, newRows);
+ mTermSession.updateSize(newColumns, newRows, (int) mRenderer.getFontWidth(), mRenderer.getFontLineSpacing());
mEmulator = mTermSession.getEmulator();
mClient.onEmulatorSet();
@@ -980,12 +1067,20 @@ public void autofill(AutofillValue value) {
if (value.isText()) {
mTermSession.write(value.getTextValue().toString());
}
+
+ resetAutoFill();
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getAutofillType() {
- return AUTOFILL_TYPE_TEXT;
+ return mAutoFillType;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ public String[] getAutofillHints() {
+ return mAutoFillHints;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -994,6 +1089,95 @@ public AutofillValue getAutofillValue() {
return AutofillValue.forText("");
}
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ public int getImportantForAutofill() {
+ return mAutoFillImportance;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private synchronized void resetAutoFill() {
+ // Restore none type so that AutoFill UI isn't shown anymore.
+ mAutoFillType = AUTOFILL_TYPE_NONE;
+ mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
+ mAutoFillHints = new String[0];
+ }
+
+ public AutofillManager getAutoFillManagerService() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
+
+ try {
+ Context context = getContext();
+ if (context == null) return null;
+ return context.getSystemService(AutofillManager.class);
+ } catch (Exception e) {
+ mClient.logStackTraceWithMessage(LOG_TAG, "Failed to get AutofillManager service", e);
+ return null;
+ }
+ }
+
+ public boolean isAutoFillEnabled() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
+
+ try {
+ AutofillManager autofillManager = getAutoFillManagerService();
+ return autofillManager != null && autofillManager.isEnabled();
+ } catch (Exception e) {
+ mClient.logStackTraceWithMessage(LOG_TAG, "Failed to check if Autofill is enabled", e);
+ return false;
+ }
+ }
+
+ public synchronized void requestAutoFillUsername() {
+ requestAutoFill(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_USERNAME} :
+ null);
+ }
+
+ public synchronized void requestAutoFillPassword() {
+ requestAutoFill(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_PASSWORD} :
+ null);
+ }
+
+ public synchronized void requestAutoFill(String[] autoFillHints) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
+ if (autoFillHints == null || autoFillHints.length < 1) return;
+
+ try {
+ AutofillManager autofillManager = getAutoFillManagerService();
+ if (autofillManager != null && autofillManager.isEnabled()) {
+ // Update type that will be returned by `getAutofillType()` so that AutoFill UI is shown.
+ mAutoFillType = AUTOFILL_TYPE_TEXT;
+ // Update importance that will be returned by `getImportantForAutofill()` so that
+ // AutoFill considers the view as important.
+ mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_YES;
+ // Update hints that will be returned by `getAutofillHints()` for which to show AutoFill UI.
+ mAutoFillHints = autoFillHints;
+ autofillManager.requestAutofill(this);
+ }
+ } catch (Exception e) {
+ mClient.logStackTraceWithMessage(LOG_TAG, "Failed to request Autofill", e);
+ }
+ }
+
+ public synchronized void cancelRequestAutoFill() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
+ if (mAutoFillType == AUTOFILL_TYPE_NONE) return;
+
+ try {
+ AutofillManager autofillManager = getAutoFillManagerService();
+ if (autofillManager != null && autofillManager.isEnabled()) {
+ resetAutoFill();
+ autofillManager.cancel();
+ }
+ } catch (Exception e) {
+ mClient.logStackTraceWithMessage(LOG_TAG, "Failed to cancel Autofill request", e);
+ }
+ }
+
+
+
/**
@@ -1193,6 +1377,25 @@ public boolean isSelectingText() {
}
}
+ /** Get the currently selected text if selecting. */
+ public String getSelectedText() {
+ if (isSelectingText() && mTextSelectionCursorController != null)
+ return mTextSelectionCursorController.getSelectedText();
+ else
+ return null;
+ }
+
+ /** Get the selected text stored before "MORE" button was pressed on the context menu. */
+ @Nullable
+ public String getStoredSelectedText() {
+ return mTextSelectionCursorController != null ? mTextSelectionCursorController.getStoredSelectedText() : null;
+ }
+
+ /** Unset the selected text stored before "MORE" button was pressed on the context menu. */
+ public void unsetStoredSelectedText() {
+ if (mTextSelectionCursorController != null) mTextSelectionCursorController.unsetStoredSelectedText();
+ }
+
private ActionMode getTextSelectionActionMode() {
if (mTextSelectionCursorController != null) {
return mTextSelectionCursorController.getActionMode();
diff --git a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java
index c08dca4470..c2cd7c6c0a 100644
--- a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java
+++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java
@@ -11,6 +11,8 @@
import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.WcWidth;
import com.termux.view.R;
@@ -20,6 +22,7 @@ public class TextSelectionCursorController implements CursorController {
private final TerminalView terminalView;
private final TextSelectionHandleView mStartHandle, mEndHandle;
+ private String mStoredSelectedText;
private boolean mIsSelectingText = false;
private long mShowStartTime = System.currentTimeMillis();
@@ -27,9 +30,9 @@ public class TextSelectionCursorController implements CursorController {
private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
private ActionMode mActionMode;
- private final int ACTION_COPY = 1;
- private final int ACTION_PASTE = 2;
- private final int ACTION_MORE = 3;
+ public final int ACTION_COPY = 1;
+ public final int ACTION_PASTE = 2;
+ public final int ACTION_MORE = 3;
public TextSelectionCursorController(TerminalView terminalView) {
this.terminalView = terminalView;
@@ -112,7 +115,7 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show);
- menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
+ menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard != null && clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, R.string.text_selection_more);
return true;
}
@@ -131,7 +134,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case ACTION_COPY:
- String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
+ String selectedText = getSelectedText();
terminalView.mTermSession.onCopyTextToClipboard(selectedText);
terminalView.stopTextSelectionMode();
break;
@@ -140,7 +143,13 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
terminalView.mTermSession.onPasteTextFromClipboard();
break;
case ACTION_MORE:
- terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup
+ // We first store the selected text in case TerminalViewClient needs the
+ // selected text before MORE button was pressed since we are going to
+ // stop selection mode
+ mStoredSelectedText = getSelectedText();
+ // The text selection needs to be stopped before showing context menu,
+ // otherwise handles will show above popup
+ terminalView.stopTextSelectionMode();
terminalView.showContextMenu();
break;
}
@@ -361,6 +370,22 @@ public void getSelectors(int[] sel) {
sel[3] = mSelX2;
}
+ /** Get the currently selected text. */
+ public String getSelectedText() {
+ return terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2);
+ }
+
+ /** Get the selected text stored before "MORE" button was pressed on the context menu. */
+ @Nullable
+ public String getStoredSelectedText() {
+ return mStoredSelectedText;
+ }
+
+ /** Unset the selected text stored before "MORE" button was pressed on the context menu. */
+ public void unsetStoredSelectedText() {
+ mStoredSelectedText = null;
+ }
+
public ActionMode getActionMode() {
return mActionMode;
}
diff --git a/termux-shared/src/main/java/com/termux/shared/errors/Errno.java b/termux-shared/src/main/java/com/termux/shared/errors/Errno.java
index 1ac9fd089b..5dfa745c20 100644
--- a/termux-shared/src/main/java/com/termux/shared/errors/Errno.java
+++ b/termux-shared/src/main/java/com/termux/shared/errors/Errno.java
@@ -110,4 +110,9 @@ public Error getError(List throwablesList, Object... args) {
}
}
+ public boolean equalsErrorTypeAndCode(Error error) {
+ if (error == null) return false;
+ return type.equals(error.getType()) && code == error.getCode();
+ }
+
}
diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
index 308bc77e66..602c4982c5 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
@@ -33,6 +33,7 @@
import java.nio.charset.Charset;
import java.nio.file.LinkOption;
import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
@@ -185,6 +186,118 @@ public static boolean isPathInDirPaths(String path, final List dirPaths,
}
+
+ /**
+ * Validate that directory is empty or contains only files in {@code ignoredSubFilePaths}.
+ *
+ * If parent path of an ignored file exists, but ignored file itself does not exist, then directory
+ * is not considered empty.
+ *
+ * @param label The optional label for directory to check. This can optionally be {@code null}.
+ * @param filePath The {@code path} for directory to check.
+ * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir.
+ * Validation is done for the paths.
+ * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
+ * error if file to be checked doesn't exist.
+ * @return Returns {@code null} if directory is empty or contains only files in {@code ignoredSubFilePaths}.
+ * Returns {@code FileUtilsErrno#ERRNO_NON_EMPTY_DIRECTORY_FILE} if a file was found that did not
+ * exist in the {@code ignoredSubFilePaths}, otherwise returns an appropriate {@code error} if
+ * checking was not successful.
+ */
+ public static Error validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String label, String filePath,
+ final List ignoredSubFilePaths,
+ final boolean ignoreNonExistentFile) {
+ label = (label == null || label.isEmpty() ? "" : label + " ");
+ if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "isDirectoryFileEmptyOrOnlyContainsSpecificFiles");
+
+ try {
+ File file = new File(filePath);
+ FileType fileType = getFileType(filePath, false);
+
+ // If file exists but not a directory file
+ if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
+ return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
+ }
+
+ // If file does not exist
+ if (fileType == FileType.NO_EXIST) {
+ // If checking is to be ignored if file does not exist
+ if (ignoreNonExistentFile)
+ return null;
+ else {
+ label += "directory to check if is empty or only contains specific files";
+ return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
+ }
+ }
+
+ File[] subFiles = file.listFiles();
+ if (subFiles == null || subFiles.length == 0)
+ return null;
+
+ // If sub files exists but no file should be ignored
+ if (ignoredSubFilePaths == null || ignoredSubFilePaths.size() == 0)
+ return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath);
+
+ // If a sub file does not exist in ignored file path
+ if (nonIgnoredSubFileExists(subFiles, ignoredSubFilePaths)) {
+ return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath);
+ }
+
+ } catch (Exception e) {
+ return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if {@code subFiles} contains contains a file not in {@code ignoredSubFilePaths}.
+ *
+ * If parent path of an ignored file exists, but ignored file itself does not exist, then directory
+ * is not considered empty.
+ *
+ * This function should ideally not be called by itself but through
+ * {@link #validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String, String, List, boolean)}.
+ *
+ * @param subFiles The list of files of a directory to check.
+ * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir.
+ * Validation is done for the paths.
+ * @return Returns {@code true} if a file was found that did not exist in the {@code ignoredSubFilePaths},
+ * otherwise {@code false}.
+ */
+ public static boolean nonIgnoredSubFileExists(File[] subFiles, @NonNull List ignoredSubFilePaths) {
+ if (subFiles == null || subFiles.length == 0) return false;
+
+ String subFilePath;
+ for (File subFile : subFiles) {
+ subFilePath = subFile.getAbsolutePath();
+ // If sub file does not exist in ignored sub file paths
+ if (!ignoredSubFilePaths.contains(subFilePath)) {
+ boolean isParentPath = false;
+ for (String ignoredSubFilePath : ignoredSubFilePaths) {
+ if (ignoredSubFilePath.startsWith(subFilePath + "/") && fileExists(ignoredSubFilePath, false)) {
+ isParentPath = true;
+ break;
+ }
+ }
+ // If sub file is not a parent of any existing ignored sub file paths
+ if (!isParentPath) {
+ return true;
+ }
+ }
+
+ if (getFileType(subFilePath, false) == FileType.DIRECTORY) {
+ // If non ignored sub file found, then early exit, otherwise continue looking
+ if (nonIgnoredSubFileExists(subFile.listFiles(), ignoredSubFilePaths))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+
/**
* Checks whether a regular file exists at {@code filePath}.
*
diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java
index 4a0d73f442..7e336aad37 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java
@@ -34,9 +34,11 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path.");
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" of type \"%3$s\" is not one of allowed file types \"%4$s\".");
+ public static final Errno ERRNO_NON_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 159, "The %1$s directory at path \"%2$s\" is not empty.");
- public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 159, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
- public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
+ public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
+ public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 161, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
+ public static final Errno ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION = new Errno(TYPE, 162, "Validating directory is empty or only contains specific files of %1$s at path \"%2$s\" failed.\nException: %3$s");
diff --git a/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java b/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java
index 927a20a885..bf1c210c00 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java
@@ -4,12 +4,16 @@
import androidx.annotation.NonNull;
+import com.termux.shared.errors.Errno;
import com.termux.shared.file.FileUtils;
+import com.termux.shared.file.FileUtilsErrno;
import com.termux.shared.logger.Logger;
import com.termux.shared.errors.Error;
import java.io.File;
import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
public class FileUtilsTests {
@@ -68,6 +72,15 @@ private static void runTestsInner(@NonNull final String testRootDirectoryPath) t
String dir1__sub_dir1_label = "dir1/sub_dir1";
String dir1__sub_dir1_path = dir1_path + "/sub_dir1";
+ String dir1__sub_dir2_label = "dir1/sub_dir2";
+ String dir1__sub_dir2_path = dir1_path + "/sub_dir2";
+
+ String dir1__sub_dir3_label = "dir1/sub_dir3";
+ String dir1__sub_dir3_path = dir1_path + "/sub_dir3";
+
+ String dir1__sub_dir3__sub_reg1_label = "dir1/sub_dir3/sub_reg1";
+ String dir1__sub_dir3__sub_reg1_path = dir1__sub_dir3_path + "/sub_reg1";
+
String dir1__sub_reg1_label = "dir1/sub_reg1";
String dir1__sub_reg1_path = dir1_path + "/sub_reg1";
@@ -274,6 +287,72 @@ private static void runTestsInner(@NonNull final String testRootDirectoryPath) t
if (FileUtils.fileExists(path, false))
throwException("The " + label + " regular file still exist after deletion");
+
+ List ignoredSubFilePaths = Arrays.asList(dir1__sub_dir2_path, dir1__sub_dir3__sub_reg1_path);
+
+ // Create dir1 directory file
+ error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
+ assertEqual("Failed to create " + dir1_label + " directory file", null, error);
+
+ // Test empty dir
+ error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
+ assertEqual("Failed to validate if " + dir1_label + " directory file is empty", null, error);
+
+
+ // Create dir1/sub_dir3 directory file
+ label = dir1__sub_dir3_label; path = dir1__sub_dir3_path;
+ error = FileUtils.createDirectoryFile(label, path);
+ assertEqual("Failed to create " + label + " directory file", null, error);
+ if (!FileUtils.directoryFileExists(path, false))
+ throwException("The " + label + " directory file does not exist as expected after creation");
+
+ // Test parent dir existing of non existing ignored regular file
+ error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
+ assertErrnoEqual("Failed to validate if " + dir1_label + " directory file is empty with parent dir existing of non existing ignored regular file", FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE, error);
+
+
+ // Write "line1" to dir1/sub_dir3/sub_reg1 regular file
+ label = dir1__sub_dir3__sub_reg1_label; path = dir1__sub_dir3__sub_reg1_path;
+ error = FileUtils.writeTextToFile(label, path, Charset.defaultCharset(), "line1", false);
+ assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
+ if (!FileUtils.regularFileExists(path, false))
+ throwException("The " + label + " file does not exist as expected after writing to it with append mode false");
+
+ // Test ignored regular file existing
+ error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
+ assertEqual("Failed to validate if " + dir1_label + " directory file is empty with ignored regular file existing", null, error);
+
+
+ // Create dir1/sub_dir2 directory file
+ label = dir1__sub_dir2_label; path = dir1__sub_dir2_path;
+ error = FileUtils.createDirectoryFile(label, path);
+ assertEqual("Failed to create " + label + " directory file", null, error);
+ if (!FileUtils.directoryFileExists(path, false))
+ throwException("The " + label + " directory file does not exist as expected after creation");
+
+ // Test ignored dir file existing
+ error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
+ assertEqual("Failed to validate if " + dir1_label + " directory file is empty with ignored dir file existing", null, error);
+
+
+ // Create dir1/sub_dir1 directory file
+ label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
+ error = FileUtils.createDirectoryFile(label, path);
+ assertEqual("Failed to create " + label + " directory file", null, error);
+ if (!FileUtils.directoryFileExists(path, false))
+ throwException("The " + label + " directory file does not exist as expected after creation");
+
+ // Test non ignored dir file existing
+ error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
+ assertErrnoEqual("Failed to validate if " + dir1_label + " directory file is empty with non ignored dir file existing", FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE, error);
+
+
+ // Delete dir1 directory file
+ label = dir1_label; path = dir1_path;
+ error = FileUtils.deleteDirectoryFile(label, path, false);
+ assertEqual("Failed to delete " + label + " directory file", null, error);
+
+
FileUtils.getFileType("/dev/ptmx", false);
FileUtils.getFileType("/dev/null", false);
}
@@ -299,6 +378,13 @@ private static boolean equalsRegardingNull(final String expected, final String a
return isEquals(expected, actual);
}
+ public static void assertErrnoEqual(@NonNull final String message, final Errno expected, final Error actual) throws Exception {
+ if ((expected == null && actual != null) || (expected != null && !expected.equalsErrorTypeAndCode(actual)))
+ throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
+ }
+
+
+
private static boolean isEquals(String expected, String actual) {
return expected.equals(actual);
}
diff --git a/termux-shared/src/main/java/com/termux/shared/interact/ShareUtils.java b/termux-shared/src/main/java/com/termux/shared/interact/ShareUtils.java
index 4d17af86f6..969502447c 100644
--- a/termux-shared/src/main/java/com/termux/shared/interact/ShareUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/interact/ShareUtils.java
@@ -12,7 +12,6 @@
import android.os.Environment;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
@@ -81,26 +80,82 @@ public static void shareText(final Context context, final String subject, final
openSystemAppChooser(context, shareTextIntent, DataUtils.isNullOrEmpty(title) ? context.getString(R.string.title_share_with) : title);
}
+
+
+ /** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel` and `toastString`. */
+ public static void copyTextToClipboard(Context context, final String text) {
+ copyTextToClipboard(context, null, text, null);
+ }
+
+ /** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel`. */
+ public static void copyTextToClipboard(Context context, final String text, final String toastString) {
+ copyTextToClipboard(context, null, text, toastString);
+ }
+
/**
- * Copy the text to clipboard.
+ * Copy the text to primary clip of the clipboard.
*
* @param context The context for operations.
+ * @param clipDataLabel The label to show to the user describing the copied text.
* @param text The text to copy.
* @param toastString If this is not {@code null} or empty, then a toast is shown if copying to
* clipboard is successful.
*/
- public static void copyTextToClipboard(final Context context, final String text, final String toastString) {
+ public static void copyTextToClipboard(Context context, @Nullable final String clipDataLabel,
+ final String text, final String toastString) {
if (context == null || text == null) return;
- final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class);
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clipboardManager == null) return;
- if (clipboardManager != null) {
- clipboardManager.setPrimaryClip(ClipData.newPlainText(null, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false)));
- if (toastString != null && !toastString.isEmpty())
- Logger.showToast(context, toastString, true);
- }
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(clipDataLabel,
+ DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES,
+ true, false, false)));
+
+ if (toastString != null && !toastString.isEmpty())
+ Logger.showToast(context, toastString, true);
+ }
+
+
+
+ /**
+ * Wrapper for {@link #getTextFromClipboard(Context, boolean)} that returns primary text {@link String}
+ * if its set and not empty.
+ */
+ @Nullable
+ public static String getTextStringFromClipboardIfSet(Context context, boolean coerceToText) {
+ CharSequence textCharSequence = getTextFromClipboard(context, coerceToText);
+ if (textCharSequence == null) return null;
+ String textString = textCharSequence.toString();
+ return !textString.isEmpty() ? textString : null;
}
+ /**
+ * Get the text from primary clip of the clipboard.
+ *
+ * @param context The context for operations.
+ * @param coerceToText Whether to call {@link ClipData.Item#coerceToText(Context)} to coerce
+ * non-text data to text.
+ * @return Returns the {@link CharSequence} of primary text. This will be `null` if failed to get it.
+ */
+ @Nullable
+ public static CharSequence getTextFromClipboard(Context context, boolean coerceToText) {
+ if (context == null) return null;
+
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clipboardManager == null) return null;
+
+ ClipData clipData = clipboardManager.getPrimaryClip();
+ if (clipData == null) return null;
+
+ ClipData.Item clipItem = clipData.getItemAt(0);
+ if (clipItem == null) return null;
+
+ return coerceToText ? clipItem.coerceToText(context) : clipItem.getText();
+ }
+
+
+
/**
* Open a url.
*
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java
index 53ef895720..c09f5bef31 100644
--- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java
@@ -16,7 +16,7 @@
public class LocalSocketRunConfig implements Serializable {
/** The {@link LocalSocketManager} title. */
- private final String mTitle;
+ protected final String mTitle;
/**
* The {@link LocalServerSocket} path.
@@ -40,19 +40,19 @@ public class LocalSocketRunConfig implements Serializable {
*
* Max allowed length is 108 bytes as per sun_path size (UNIX_PATH_MAX) on Linux.
*/
- private final String mPath;
+ protected final String mPath;
/** If abstract namespace {@link LocalServerSocket} instead of filesystem. */
protected final boolean mAbstractNamespaceSocket;
/** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */
- private final ILocalSocketManager mLocalSocketManagerClient;
+ protected final ILocalSocketManager mLocalSocketManagerClient;
/**
* The {@link LocalServerSocket} file descriptor.
* Value will be `>= 0` if socket has been created successfully and `-1` if not created or closed.
*/
- private int mFD = -1;
+ protected int mFD = -1;
/**
* The {@link LocalClientSocket} receiving (SO_RCVTIMEO) timeout in milliseconds.
@@ -61,7 +61,7 @@ public class LocalSocketRunConfig implements Serializable {
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/NativeCrashListener.java;l=55
* Defaults to {@link #DEFAULT_RECEIVE_TIMEOUT}.
*/
- private Integer mReceiveTimeout;
+ protected Integer mReceiveTimeout;
public static final int DEFAULT_RECEIVE_TIMEOUT = 10000;
/**
@@ -71,7 +71,7 @@ public class LocalSocketRunConfig implements Serializable {
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/NativeCrashListener.java;l=55
* Defaults to {@link #DEFAULT_SEND_TIMEOUT}.
*/
- private Integer mSendTimeout;
+ protected Integer mSendTimeout;
public static final int DEFAULT_SEND_TIMEOUT = 10000;
/**
@@ -80,7 +80,7 @@ public class LocalSocketRunConfig implements Serializable {
* deadline.
* Defaults to {@link #DEFAULT_DEADLINE}.
*/
- private Long mDeadline;
+ protected Long mDeadline;
public static final int DEFAULT_DEADLINE = 0;
/**
@@ -91,7 +91,7 @@ public class LocalSocketRunConfig implements Serializable {
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/net/LocalSocketManager.java;l=31
* Defaults to {@link #DEFAULT_BACKLOG}.
*/
- private Integer mBacklog;
+ protected Integer mBacklog;
public static final int DEFAULT_BACKLOG = 50;
diff --git a/termux-shared/src/main/java/com/termux/shared/settings/preferences/AppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/settings/preferences/AppSharedPreferences.java
new file mode 100644
index 0000000000..03a75e03b5
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/settings/preferences/AppSharedPreferences.java
@@ -0,0 +1,49 @@
+package com.termux.shared.settings.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** A class that holds {@link SharedPreferences} objects for apps. */
+public class AppSharedPreferences {
+
+ /** The {@link Context} for operations. */
+ protected final Context mContext;
+
+ /** The {@link SharedPreferences} that ideally should be created with {@link SharedPreferenceUtils#getPrivateSharedPreferences(Context, String)}. */
+ protected final SharedPreferences mSharedPreferences;
+
+ /** The {@link SharedPreferences}that ideally should be created with {@link SharedPreferenceUtils#getPrivateAndMultiProcessSharedPreferences(Context, String)}. */
+ protected final SharedPreferences mMultiProcessSharedPreferences;
+
+ protected AppSharedPreferences(@NonNull Context context, @Nullable SharedPreferences sharedPreferences) {
+ this(context, sharedPreferences, null);
+ }
+
+ protected AppSharedPreferences(@NonNull Context context, @Nullable SharedPreferences sharedPreferences,
+ @Nullable SharedPreferences multiProcessSharedPreferences) {
+ mContext = context;
+ mSharedPreferences = sharedPreferences;
+ mMultiProcessSharedPreferences = multiProcessSharedPreferences;
+ }
+
+
+
+ /** Get {@link #mContext}. */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /** Get {@link #mSharedPreferences}. */
+ public SharedPreferences getSharedPreferences() {
+ return mSharedPreferences;
+ }
+
+ /** Get {@link #mMultiProcessSharedPreferences}. */
+ public SharedPreferences getMultiProcessSharedPreferences() {
+ return mMultiProcessSharedPreferences;
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java
index f94e345311..e316ec0662 100644
--- a/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java
+++ b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java
@@ -1,5 +1,6 @@
package com.termux.shared.shell.am;
+import android.Manifest;
import android.app.Application;
import android.content.Context;
@@ -7,6 +8,9 @@
import androidx.annotation.Nullable;
import com.termux.am.Am;
+import com.termux.shared.R;
+import com.termux.shared.android.PackageUtils;
+import com.termux.shared.android.PermissionUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.logger.Logger;
import com.termux.shared.net.socket.local.ILocalSocketManager;
@@ -40,8 +44,9 @@
* 1. Optionally extend {@link AmSocketServerClient}, the implementation for
* {@link ILocalSocketManager} that will receive call backs from the server including
* when client connects via {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)}.
- * 2. Create a {@link LocalSocketRunConfig} instance with the run config of the am server. It would
- * be better to use a filesystem socket instead of abstract namespace socket for security reasons.
+ * 2. Create a {@link AmSocketServerRunConfig} instance which extends from {@link LocalSocketRunConfig}
+ * with the run config of the am server. It would be better to use a filesystem socket instead
+ * of abstract namespace socket for security reasons.
* 3. Call {@link #start(Context, LocalSocketRunConfig)} to start the server and store the {@link LocalSocketManager}
* instance returned.
* 4. Stop server if needed with a call to {@link LocalSocketManager#stop()} on the
@@ -104,10 +109,13 @@ public static void processAmClient(@NonNull LocalSocketManager localSocketManage
Logger.logDebug(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() +
"\n" + ExecutionCommand.getArgumentsLogString("am command", amCommandArray));
+ AmSocketServerRunConfig amSocketServerRunConfig = (AmSocketServerRunConfig) localSocketManager.getLocalSocketRunConfig();
+
// Run am command and send its result to the client
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = new StringBuilder();
- error = runAmCommand(localSocketManager.getContext(), amCommandArray, stdout, stderr);
+ error = runAmCommand(localSocketManager.getContext(), amCommandArray, stdout, stderr,
+ amSocketServerRunConfig.shouldCheckDisplayOverAppsPermission());
if (error != null) {
sendResultToClient(localSocketManager, clientSocket, 1, stdout.toString(),
!stderr.toString().isEmpty() ? stderr + "\n\n" + error : error.toString());
@@ -194,16 +202,27 @@ public static Error parseAmCommand(String amCommandString, List amComman
* @param amCommandArray The am command array.
* @param stdout The {@link StringBuilder} to set stdout in that is returned by the am command.
* @param stderr The {@link StringBuilder} to set stderr in that is returned by the am command.
+ * @param checkDisplayOverAppsPermission Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW}
+ * has been granted if running on Android `>= 10` and
+ * starting activity or service.
* @return Returns the {@code error} if am command failed, otherwise {@code null}.
*/
public static Error runAmCommand(@NonNull Context context,
String[] amCommandArray,
- @NonNull StringBuilder stdout, @NonNull StringBuilder stderr) {
+ @NonNull StringBuilder stdout, @NonNull StringBuilder stderr,
+ boolean checkDisplayOverAppsPermission) {
try (ByteArrayOutputStream stdoutByteStream = new ByteArrayOutputStream();
PrintStream stdoutPrintStream = new PrintStream(stdoutByteStream);
ByteArrayOutputStream stderrByteStream = new ByteArrayOutputStream();
PrintStream stderrPrintStream = new PrintStream(stderrByteStream)) {
+ if (checkDisplayOverAppsPermission && amCommandArray.length >= 1 &&
+ (amCommandArray[0].equals("start") || amCommandArray[0].equals("startservice")) &&
+ !PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(context, true)) {
+ throw new IllegalStateException(context.getString(R.string.error_display_over_other_apps_permission_not_granted,
+ PackageUtils.getAppNameForPackage(context)));
+ }
+
new Am(stdoutPrintStream, stderrPrintStream, (Application) context.getApplicationContext()).run(amCommandArray);
// Set stdout to value set by am command in stdoutPrintStream
diff --git a/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerRunConfig.java b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerRunConfig.java
new file mode 100644
index 0000000000..f36cbece58
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerRunConfig.java
@@ -0,0 +1,108 @@
+package com.termux.shared.shell.am;
+
+import android.Manifest;
+
+import androidx.annotation.NonNull;
+
+import com.termux.shared.logger.Logger;
+import com.termux.shared.markdown.MarkdownUtils;
+import com.termux.shared.net.socket.local.ILocalSocketManager;
+import com.termux.shared.net.socket.local.LocalSocketRunConfig;
+
+import java.io.Serializable;
+
+/**
+ * Run config for {@link AmSocketServer}.
+ */
+public class AmSocketServerRunConfig extends LocalSocketRunConfig implements Serializable {
+
+ /**
+ * Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW} has been granted if running on Android `>= 10`
+ * if starting activities. Will also check when starting services in case starting foreground
+ * service is not allowed.
+ *
+ * https://developer.android.com/guide/components/activities/background-starts
+ */
+ private Boolean mCheckDisplayOverAppsPermission;
+ public static final boolean DEFAULT_CHECK_DISPLAY_OVER_APPS_PERMISSION = true;
+
+ /**
+ * Create an new instance of {@link AmSocketServerRunConfig}.
+ *
+ * @param title The {@link #mTitle} value.
+ * @param path The {@link #mPath} value.
+ * @param localSocketManagerClient The {@link #mLocalSocketManagerClient} value.
+ */
+ public AmSocketServerRunConfig(@NonNull String title, @NonNull String path, @NonNull ILocalSocketManager localSocketManagerClient) {
+ super(title, path, localSocketManagerClient);
+ }
+
+
+ /** Get {@link #mCheckDisplayOverAppsPermission} if set, otherwise {@link #DEFAULT_CHECK_DISPLAY_OVER_APPS_PERMISSION}. */
+ public boolean shouldCheckDisplayOverAppsPermission() {
+ return mCheckDisplayOverAppsPermission != null ? mCheckDisplayOverAppsPermission : DEFAULT_CHECK_DISPLAY_OVER_APPS_PERMISSION;
+ }
+
+ /** Set {@link #mCheckDisplayOverAppsPermission}. */
+ public void setCheckDisplayOverAppsPermission(Boolean checkDisplayOverAppsPermission) {
+ mCheckDisplayOverAppsPermission = checkDisplayOverAppsPermission;
+ }
+
+
+
+ /**
+ * Get a log {@link String} for {@link AmSocketServerRunConfig}.
+ *
+ * @param config The {@link AmSocketServerRunConfig} to get info of.
+ * @return Returns the log {@link String}.
+ */
+ @NonNull
+ public static String getRunConfigLogString(final AmSocketServerRunConfig config) {
+ if (config == null) return "null";
+ return config.getLogString();
+ }
+
+ /** Get a log {@link String} for the {@link AmSocketServerRunConfig}. */
+ @NonNull
+ public String getLogString() {
+ StringBuilder logString = new StringBuilder();
+ logString.append(super.getLogString()).append("\n\n\n");
+
+ logString.append("Am Command:");
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("CheckDisplayOverAppsPermission", shouldCheckDisplayOverAppsPermission(), "-"));
+
+ return logString.toString();
+ }
+
+ /**
+ * Get a markdown {@link String} for {@link AmSocketServerRunConfig}.
+ *
+ * @param config The {@link AmSocketServerRunConfig} to get info of.
+ * @return Returns the markdown {@link String}.
+ */
+ public static String getRunConfigMarkdownString(final AmSocketServerRunConfig config) {
+ if (config == null) return "null";
+ return config.getMarkdownString();
+ }
+
+ /** Get a markdown {@link String} for the {@link AmSocketServerRunConfig}. */
+ @NonNull
+ public String getMarkdownString() {
+ StringBuilder markdownString = new StringBuilder();
+ markdownString.append(super.getMarkdownString()).append("\n\n\n");
+
+ markdownString.append("## ").append("Am Command");
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("CheckDisplayOverAppsPermission", shouldCheckDisplayOverAppsPermission(), "-"));
+
+ return markdownString.toString();
+ }
+
+
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getLogString();
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java
index 203f514f3c..59f523af72 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java
@@ -11,7 +11,7 @@
import java.util.List;
/*
- * Version: v0.51.0
+ * Version: v0.52.0
* SPDX-License-Identifier: MIT
*
* Changelog
@@ -274,6 +274,9 @@
*
* - 0.51.0 (2022-06-13)
* - Added `TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME` and `TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME`.
+ *
+ * - 0.52.0 (2022-06-18)
+ * - Added `TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY`.
*/
/**
@@ -322,9 +325,9 @@ public final class TermuxConstants {
* Termux organization variables.
*/
- /** Termux Github organization name */
+ /** Termux GitHub organization name */
public static final String TERMUX_GITHUB_ORGANIZATION_NAME = "termux"; // Default: "termux"
- /** Termux Github organization url */
+ /** Termux GitHub organization url */
public static final String TERMUX_GITHUB_ORGANIZATION_URL = "https://github.com" + "/" + TERMUX_GITHUB_ORGANIZATION_NAME; // Default: "https://github.com/termux"
/** F-Droid packages base url */
@@ -342,11 +345,11 @@ public final class TermuxConstants {
public static final String TERMUX_APP_NAME = "Termux"; // Default: "Termux"
/** Termux package name */
public static final String TERMUX_PACKAGE_NAME = "com.termux"; // Default: "com.termux"
- /** Termux Github repo name */
+ /** Termux GitHub repo name */
public static final String TERMUX_GITHUB_REPO_NAME = "termux-app"; // Default: "termux-app"
- /** Termux Github repo url */
+ /** Termux GitHub repo url */
public static final String TERMUX_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-app"
- /** Termux Github issues repo url */
+ /** Termux GitHub issues repo url */
public static final String TERMUX_GITHUB_ISSUES_REPO_URL = TERMUX_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-app/issues"
/** Termux F-Droid package url */
public static final String TERMUX_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux"
@@ -356,11 +359,11 @@ public final class TermuxConstants {
public static final String TERMUX_API_APP_NAME = "Termux:API"; // Default: "Termux:API"
/** Termux:API app package name */
public static final String TERMUX_API_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".api"; // Default: "com.termux.api"
- /** Termux:API Github repo name */
+ /** Termux:API GitHub repo name */
public static final String TERMUX_API_GITHUB_REPO_NAME = "termux-api"; // Default: "termux-api"
- /** Termux:API Github repo url */
+ /** Termux:API GitHub repo url */
public static final String TERMUX_API_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_API_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-api"
- /** Termux:API Github issues repo url */
+ /** Termux:API GitHub issues repo url */
public static final String TERMUX_API_GITHUB_ISSUES_REPO_URL = TERMUX_API_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-api/issues"
/** Termux:API F-Droid package url */
public static final String TERMUX_API_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_API_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.api"
@@ -370,11 +373,11 @@ public final class TermuxConstants {
public static final String TERMUX_BOOT_APP_NAME = "Termux:Boot"; // Default: "Termux:Boot"
/** Termux:Boot app package name */
public static final String TERMUX_BOOT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".boot"; // Default: "com.termux.boot"
- /** Termux:Boot Github repo name */
+ /** Termux:Boot GitHub repo name */
public static final String TERMUX_BOOT_GITHUB_REPO_NAME = "termux-boot"; // Default: "termux-boot"
- /** Termux:Boot Github repo url */
+ /** Termux:Boot GitHub repo url */
public static final String TERMUX_BOOT_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_BOOT_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-boot"
- /** Termux:Boot Github issues repo url */
+ /** Termux:Boot GitHub issues repo url */
public static final String TERMUX_BOOT_GITHUB_ISSUES_REPO_URL = TERMUX_BOOT_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-boot/issues"
/** Termux:Boot F-Droid package url */
public static final String TERMUX_BOOT_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_BOOT_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.boot"
@@ -384,11 +387,11 @@ public final class TermuxConstants {
public static final String TERMUX_FLOAT_APP_NAME = "Termux:Float"; // Default: "Termux:Float"
/** Termux:Float app package name */
public static final String TERMUX_FLOAT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".window"; // Default: "com.termux.window"
- /** Termux:Float Github repo name */
+ /** Termux:Float GitHub repo name */
public static final String TERMUX_FLOAT_GITHUB_REPO_NAME = "termux-float"; // Default: "termux-float"
- /** Termux:Float Github repo url */
+ /** Termux:Float GitHub repo url */
public static final String TERMUX_FLOAT_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_FLOAT_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-float"
- /** Termux:Float Github issues repo url */
+ /** Termux:Float GitHub issues repo url */
public static final String TERMUX_FLOAT_GITHUB_ISSUES_REPO_URL = TERMUX_FLOAT_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-float/issues"
/** Termux:Float F-Droid package url */
public static final String TERMUX_FLOAT_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_FLOAT_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.window"
@@ -398,11 +401,11 @@ public final class TermuxConstants {
public static final String TERMUX_STYLING_APP_NAME = "Termux:Styling"; // Default: "Termux:Styling"
/** Termux:Styling app package name */
public static final String TERMUX_STYLING_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".styling"; // Default: "com.termux.styling"
- /** Termux:Styling Github repo name */
+ /** Termux:Styling GitHub repo name */
public static final String TERMUX_STYLING_GITHUB_REPO_NAME = "termux-styling"; // Default: "termux-styling"
- /** Termux:Styling Github repo url */
+ /** Termux:Styling GitHub repo url */
public static final String TERMUX_STYLING_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_STYLING_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-styling"
- /** Termux:Styling Github issues repo url */
+ /** Termux:Styling GitHub issues repo url */
public static final String TERMUX_STYLING_GITHUB_ISSUES_REPO_URL = TERMUX_STYLING_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-styling/issues"
/** Termux:Styling F-Droid package url */
public static final String TERMUX_STYLING_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_STYLING_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.styling"
@@ -412,11 +415,11 @@ public final class TermuxConstants {
public static final String TERMUX_TASKER_APP_NAME = "Termux:Tasker"; // Default: "Termux:Tasker"
/** Termux:Tasker app package name */
public static final String TERMUX_TASKER_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".tasker"; // Default: "com.termux.tasker"
- /** Termux:Tasker Github repo name */
+ /** Termux:Tasker GitHub repo name */
public static final String TERMUX_TASKER_GITHUB_REPO_NAME = "termux-tasker"; // Default: "termux-tasker"
- /** Termux:Tasker Github repo url */
+ /** Termux:Tasker GitHub repo url */
public static final String TERMUX_TASKER_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_TASKER_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-tasker"
- /** Termux:Tasker Github issues repo url */
+ /** Termux:Tasker GitHub issues repo url */
public static final String TERMUX_TASKER_GITHUB_ISSUES_REPO_URL = TERMUX_TASKER_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-tasker/issues"
/** Termux:Tasker F-Droid package url */
public static final String TERMUX_TASKER_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_TASKER_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.tasker"
@@ -426,11 +429,11 @@ public final class TermuxConstants {
public static final String TERMUX_WIDGET_APP_NAME = "Termux:Widget"; // Default: "Termux:Widget"
/** Termux:Widget app package name */
public static final String TERMUX_WIDGET_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".widget"; // Default: "com.termux.widget"
- /** Termux:Widget Github repo name */
+ /** Termux:Widget GitHub repo name */
public static final String TERMUX_WIDGET_GITHUB_REPO_NAME = "termux-widget"; // Default: "termux-widget"
- /** Termux:Widget Github repo url */
+ /** Termux:Widget GitHub repo url */
public static final String TERMUX_WIDGET_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_WIDGET_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-widget"
- /** Termux:Widget Github issues repo url */
+ /** Termux:Widget GitHub issues repo url */
public static final String TERMUX_WIDGET_GITHUB_ISSUES_REPO_URL = TERMUX_WIDGET_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-widget/issues"
/** Termux:Widget F-Droid package url */
public static final String TERMUX_WIDGET_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_WIDGET_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.widget"
@@ -473,10 +476,10 @@ public final class TermuxConstants {
/** F-Droid APK release signing certificate SHA-256 digest */
public static final String APK_RELEASE_FDROID_SIGNING_CERTIFICATE_SHA256_DIGEST = "228FB2CFE90831C1499EC3CCAF61E96E8E1CE70766B9474672CE427334D41C42"; // Default: "228FB2CFE90831C1499EC3CCAF61E96E8E1CE70766B9474672CE427334D41C42"
- /** Github APK release */
+ /** GitHub APK release */
public static final String APK_RELEASE_GITHUB = "Github"; // Default: "Github"
- /** Github APK release signing certificate SHA-256 digest */
+ /** GitHub APK release signing certificate SHA-256 digest */
public static final String APK_RELEASE_GITHUB_SIGNING_CERTIFICATE_SHA256_DIGEST = "B6DA01480EEFD5FBF2CD3771B8D1021EC791304BDD6C4BF41D3FAABAD48EE5E1"; // Default: "B6DA01480EEFD5FBF2CD3771B8D1021EC791304BDD6C4BF41D3FAABAD48EE5E1"
/** Google Play Store APK release */
@@ -499,21 +502,21 @@ public final class TermuxConstants {
* Termux packages urls.
*/
- /** Termux Packages Github repo name */
+ /** Termux Packages GitHub repo name */
public static final String TERMUX_PACKAGES_GITHUB_REPO_NAME = "termux-packages"; // Default: "termux-packages"
- /** Termux Packages Github repo url */
+ /** Termux Packages GitHub repo url */
public static final String TERMUX_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-packages"
- /** Termux Packages Github issues repo url */
+ /** Termux Packages GitHub issues repo url */
public static final String TERMUX_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-packages/issues"
/** Termux API apt package name */
public static final String TERMUX_API_APT_PACKAGE_NAME = "termux-api"; // Default: "termux-api"
- /** Termux API apt Github repo name */
+ /** Termux API apt GitHub repo name */
public static final String TERMUX_API_APT_GITHUB_REPO_NAME = "termux-api-package"; // Default: "termux-api-package"
- /** Termux API apt Github repo url */
+ /** Termux API apt GitHub repo url */
public static final String TERMUX_API_APT_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_API_APT_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-api-package"
- /** Termux API apt Github issues repo url */
+ /** Termux API apt GitHub issues repo url */
public static final String TERMUX_API_APT_GITHUB_ISSUES_REPO_URL = TERMUX_API_APT_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-api-package/issues"
@@ -536,7 +539,7 @@ public final class TermuxConstants {
/** Termux Wiki url */
public static final String TERMUX_WIKI_URL = "https://wiki.termux.com"; // Default: "https://wiki.termux.com"
- /** Termux Github wiki repo url */
+ /** Termux GitHub wiki repo url */
public static final String TERMUX_GITHUB_WIKI_REPO_URL = TERMUX_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-app/wiki"
/** Termux Packages wiki repo url */
@@ -681,6 +684,11 @@ public final class TermuxConstants {
public static final File TERMUX_APPS_DIR = new File(TERMUX_APPS_DIR_PATH);
+ /** Termux app $PREFIX directory path ignored sub file paths to consider it empty */
+ public static final List TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY = Arrays.asList(
+ TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, TermuxConstants.TERMUX_ENV_TEMP_FILE_PATH, TermuxConstants.TERMUX_ENV_FILE_PATH);
+
+
/*
* Termux app and plugin preferences and properties file paths.
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java
index 793968afcb..8ac94da162 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java
@@ -518,7 +518,7 @@ public static String getReportIssueMarkdownString(@NonNull final Context context
markdownString.append("\n\n### Reddit\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_REDDIT_SUBREDDIT, TermuxConstants.TERMUX_REDDIT_SUBREDDIT_URL)).append(" ");
- markdownString.append("\n\n### Github Issues for Termux apps\n");
+ markdownString.append("\n\n### GitHub Issues for Termux apps\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_API_APP_NAME, TermuxConstants.TERMUX_API_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_BOOT_APP_NAME, TermuxConstants.TERMUX_BOOT_GITHUB_ISSUES_REPO_URL)).append(" ");
@@ -527,7 +527,7 @@ public static String getReportIssueMarkdownString(@NonNull final Context context
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_TASKER_APP_NAME, TermuxConstants.TERMUX_TASKER_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_WIDGET_APP_NAME, TermuxConstants.TERMUX_WIDGET_GITHUB_ISSUES_REPO_URL)).append(" ");
- markdownString.append("\n\n### Github Issues for Termux packages\n");
+ markdownString.append("\n\n### GitHub Issues for Termux packages\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n##\n");
@@ -548,7 +548,7 @@ public static String getImportantLinksMarkdownString(@NonNull final Context cont
markdownString.append("## Important Links");
- markdownString.append("\n\n### Github\n");
+ markdownString.append("\n\n### GitHub\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_API_APP_NAME, TermuxConstants.TERMUX_API_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_BOOT_APP_NAME, TermuxConstants.TERMUX_BOOT_GITHUB_REPO_URL)).append(" ");
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/data/TermuxUrlUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/data/TermuxUrlUtils.java
index 6eabde8eab..d35d9f81d1 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/data/TermuxUrlUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/data/TermuxUrlUtils.java
@@ -60,7 +60,7 @@ public static Pattern getUrlMatchRegex() {
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
- regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
+ regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*){1,}[a-z\\u00a1-\\uffff0-9]{1,}))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java
index f67a127a35..b747381c02 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java
@@ -10,7 +10,10 @@
public class ExtraKeysConstants {
/** Defines the repetitive keys that can be passed to {@link ExtraKeysView#setRepetitiveKeys(List)}. */
- public static List PRIMARY_REPETITIVE_KEYS = Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL");
+ public static List PRIMARY_REPETITIVE_KEYS = Arrays.asList(
+ "UP", "DOWN", "LEFT", "RIGHT",
+ "BKSP", "DEL",
+ "PGUP", "PGDN");
@@ -90,6 +93,7 @@ public static class EXTRA_KEY_DISPLAY_MAPS {
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
put("PASTE", "⎘"); // U+2398
+ put("SCROLL", "⇳"); // U+21F3
}};
public static final ExtraKeyDisplayMap LESS_KNOWN_CHARACTERS_DISPLAY = new ExtraKeyDisplayMap() {{
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java
index 2f73c689c7..6fc79a7e2b 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java
@@ -1,5 +1,7 @@
package com.termux.shared.termux.file;
+import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
+
import android.content.Context;
import android.os.Environment;
@@ -325,6 +327,21 @@ public static Error isAppsTermuxAppDirectoryAccessible(boolean createDirectoryIf
false, false);
}
+ /**
+ * If {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} doesn't exist, is empty or only contains
+ * files in {@link TermuxConstants#TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY}.
+ */
+ public static boolean isTermuxPrefixDirectoryEmpty() {
+ Error error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles("termux prefix",
+ TERMUX_PREFIX_DIR_PATH, TermuxConstants.TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY, true);
+ if (error == null)
+ return true;
+
+ if (!FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.equalsErrorTypeAndCode(error))
+ Logger.logErrorExtended(LOG_TAG, "Failed to check if termux prefix directory is empty:\n" + error.getErrorLogString());
+ return false;
+ }
+
/**
* Get a markdown {@link String} for stat output for various Termux app files paths.
*
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAPIAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAPIAppSharedPreferences.java
index 228f9cc4f3..67c00a752f 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAPIAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAPIAppSharedPreferences.java
@@ -1,31 +1,28 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_API_APP;
import com.termux.shared.termux.TermuxConstants;
-public class TermuxAPIAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
-
+public class TermuxAPIAppSharedPreferences extends AppSharedPreferences {
private static final String LOG_TAG = "TermuxAPIAppSharedPreferences";
private TermuxAPIAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
}
/**
@@ -61,16 +58,6 @@ public static TermuxAPIAppSharedPreferences build(@NonNull final Context context
return new TermuxAPIAppSharedPreferences(termuxAPIPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public int getLogLevel(boolean readFromFile) {
@@ -85,4 +72,13 @@ public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_API_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
+
+ public int getLastPendingIntentRequestCode() {
+ return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_API_APP.KEY_LAST_PENDING_INTENT_REQUEST_CODE, TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE);
+ }
+
+ public void setLastPendingIntentRequestCode(int lastPendingIntentRequestCode) {
+ SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_API_APP.KEY_LAST_PENDING_INTENT_REQUEST_CODE, lastPendingIntentRequestCode, true);
+ }
+
}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAppSharedPreferences.java
index a33be8e68e..cd3812fe49 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAppSharedPreferences.java
@@ -1,13 +1,13 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
@@ -15,11 +15,7 @@
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
-public class TermuxAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
+public class TermuxAppSharedPreferences extends AppSharedPreferences {
private int MIN_FONTSIZE;
private int MAX_FONTSIZE;
@@ -28,10 +24,11 @@ public class TermuxAppSharedPreferences {
private static final String LOG_TAG = "TermuxAppSharedPreferences";
private TermuxAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
-
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
setFontVariables(context);
}
@@ -69,17 +66,6 @@ public static TermuxAppSharedPreferences build(@NonNull final Context context, f
return new TermuxAppSharedPreferences(termuxPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public boolean shouldShowTerminalToolbar() {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxBootAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxBootAppSharedPreferences.java
index 49db9cef16..317f93ce1e 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxBootAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxBootAppSharedPreferences.java
@@ -1,31 +1,28 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_BOOT_APP;
import com.termux.shared.termux.TermuxConstants;
-public class TermuxBootAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
-
+public class TermuxBootAppSharedPreferences extends AppSharedPreferences {
private static final String LOG_TAG = "TermuxBootAppSharedPreferences";
private TermuxBootAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
}
/**
@@ -61,16 +58,6 @@ public static TermuxBootAppSharedPreferences build(@NonNull final Context contex
return new TermuxBootAppSharedPreferences(termuxBootPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public int getLogLevel(boolean readFromFile) {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxFloatAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxFloatAppSharedPreferences.java
index 4c0c7939d1..e56e29acdd 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxFloatAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxFloatAppSharedPreferences.java
@@ -1,7 +1,6 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -9,16 +8,13 @@
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_FLOAT_APP;
import com.termux.shared.termux.TermuxConstants;
-public class TermuxFloatAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
+public class TermuxFloatAppSharedPreferences extends AppSharedPreferences {
private int MIN_FONTSIZE;
private int MAX_FONTSIZE;
@@ -27,9 +23,11 @@ public class TermuxFloatAppSharedPreferences {
private static final String LOG_TAG = "TermuxFloatAppSharedPreferences";
private TermuxFloatAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
setFontVariables(context);
}
@@ -67,16 +65,6 @@ public static TermuxFloatAppSharedPreferences build(@NonNull final Context conte
return new TermuxFloatAppSharedPreferences(termuxFloatPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public int getWindowX() {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxPreferenceConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxPreferenceConstants.java
index 80226914d7..15bc74c9f2 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxPreferenceConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxPreferenceConstants.java
@@ -194,6 +194,13 @@ public static final class TERMUX_API_APP {
*/
public static final String KEY_LOG_LEVEL = "log_level";
+
+ /**
+ * Defines the key for last used PendingIntent request code.
+ */
+ public static final String KEY_LAST_PENDING_INTENT_REQUEST_CODE = "last_pending_intent_request_code";
+ public static final int DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE = 0;
+
}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxStylingAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxStylingAppSharedPreferences.java
index 5e6b25a268..0e1b539ee9 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxStylingAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxStylingAppSharedPreferences.java
@@ -1,31 +1,28 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_STYLING_APP;
import com.termux.shared.termux.TermuxConstants;
-public class TermuxStylingAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
-
+public class TermuxStylingAppSharedPreferences extends AppSharedPreferences {
private static final String LOG_TAG = "TermuxStylingAppSharedPreferences";
private TermuxStylingAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
}
/**
@@ -61,16 +58,6 @@ public static TermuxStylingAppSharedPreferences build(@NonNull final Context con
return new TermuxStylingAppSharedPreferences(termuxStylingPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public int getLogLevel(boolean readFromFile) {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxTaskerAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxTaskerAppSharedPreferences.java
index b647f09661..5c518a67c4 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxTaskerAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxTaskerAppSharedPreferences.java
@@ -1,31 +1,28 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_TASKER_APP;
import com.termux.shared.logger.Logger;
-public class TermuxTaskerAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
-
+public class TermuxTaskerAppSharedPreferences extends AppSharedPreferences {
private static final String LOG_TAG = "TermuxTaskerAppSharedPreferences";
private TermuxTaskerAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
}
/**
@@ -61,16 +58,6 @@ public static TermuxTaskerAppSharedPreferences build(@NonNull final Context con
return new TermuxTaskerAppSharedPreferences(termuxTaskerPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public int getLogLevel(boolean readFromFile) {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxWidgetAppSharedPreferences.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxWidgetAppSharedPreferences.java
index 301feec299..40ce92264a 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxWidgetAppSharedPreferences.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxWidgetAppSharedPreferences.java
@@ -1,13 +1,13 @@
package com.termux.shared.termux.settings.preferences;
import android.content.Context;
-import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.android.PackageUtils;
+import com.termux.shared.settings.preferences.AppSharedPreferences;
import com.termux.shared.settings.preferences.SharedPreferenceUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_WIDGET_APP;
@@ -15,19 +15,16 @@
import java.util.UUID;
-public class TermuxWidgetAppSharedPreferences {
-
- private final Context mContext;
- private final SharedPreferences mSharedPreferences;
- private final SharedPreferences mMultiProcessSharedPreferences;
-
+public class TermuxWidgetAppSharedPreferences extends AppSharedPreferences {
private static final String LOG_TAG = "TermuxWidgetAppSharedPreferences";
private TermuxWidgetAppSharedPreferences(@NonNull Context context) {
- mContext = context;
- mSharedPreferences = getPrivateSharedPreferences(mContext);
- mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
+ super(context,
+ SharedPreferenceUtils.getPrivateSharedPreferences(context,
+ TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION),
+ SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context,
+ TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION));
}
/**
@@ -63,16 +60,6 @@ public static TermuxWidgetAppSharedPreferences build(@NonNull final Context cont
return new TermuxWidgetAppSharedPreferences(termuxWidgetPackageContext);
}
- private static SharedPreferences getPrivateSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
- private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
- if (context == null) return null;
- return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
- }
-
public static String getGeneratedToken(@NonNull Context context) {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java
index 438744e1d4..0d68ae5c66 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java
@@ -13,6 +13,7 @@
import com.termux.shared.net.socket.local.LocalSocketManager;
import com.termux.shared.net.socket.local.LocalSocketManagerClientBase;
import com.termux.shared.net.socket.local.LocalSocketRunConfig;
+import com.termux.shared.shell.am.AmSocketServerRunConfig;
import com.termux.shared.shell.am.AmSocketServer;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.crash.TermuxCrashUtils;
@@ -101,10 +102,10 @@ public static void setupTermuxAmSocketServer(@NonNull Context context) {
public static synchronized void start(@NonNull Context context) {
stop();
- LocalSocketRunConfig localSocketRunConfig = new LocalSocketRunConfig(TITLE,
+ AmSocketServerRunConfig amSocketServerRunConfig = new AmSocketServerRunConfig(TITLE,
TermuxConstants.TERMUX_APP.TERMUX_AM_SOCKET_FILE_PATH, new TermuxAmSocketServerClient());
- termuxAmSocketServer = AmSocketServer.start(context, localSocketRunConfig);
+ termuxAmSocketServer = AmSocketServer.start(context, amSocketServerRunConfig);
}
/**
diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml
index 9124a889ae..d585e24f97 100644
--- a/termux-shared/src/main/res/values/strings.xml
+++ b/termux-shared/src/main/res/values/strings.xml
@@ -37,6 +37,12 @@
Attempted to ask for permissions that have not been requested in app manifest: %1$s
The \"%1$s\" package is targeting targetSdkVersion %2$d and is running on android sdk %3$d but has not set requestLegacyExternalStorage to true in app manifest
Requires `DUMP` and `PACKAGE_USAGE_STATS` permission
+ %1$s requires
+ \"Display over other apps\" permission to start activities and services from background on Android >= 10.
+ Grants it from Android Settings -> Apps -> %1$s -> Advanced -> Draw over other apps.
+ The permission name may be different on different devices, like on Xiaomi, its called
+ \"Display pop-up windows while running in the background\", check https://dontkillmyapp.com
+ for device specific issues.
@@ -78,12 +84,12 @@
an issue on one of the following links.
\n\nIf you are posting a Termux app crash report, then please provide details on what you were doing that
caused the crash and how to reproduce it, if possible.
- \n\nIf you are posting an issue on Github, then post it in the repository at which the report belongs at.
+ \n\nIf you are posting an issue on GitHub, then post it in the repository at which the report belongs at.
Issues opened or emails sent with **(partial) screenshots** instead of copied text or a file of this report
**will likely be automatically closed/deleted**. You may optionally remove any device specific info that
you consider private or don\'t want to share or that is not relevant to the issue.
\n\nWe do not provide support for any hacking related tools/scripts. Any questions asked about them over email,
- on github or other official termux community forums **will likely be automatically closed/deleted** and may
+ on GitHub or other official termux community forums **will likely be automatically closed/deleted** and may
even result in **temporary or permanent** ban. Check %1$s/wiki/Hacking for details.
The &TERMUX_APP_NAME; is required by the %1$s app to run termux commands.