Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b657c6adf | ||
|
|
4f033b3445 | ||
|
|
fda17afaa0 | ||
|
|
431dc1d9d1 | ||
|
|
849e6b10de | ||
|
|
2375652891 | ||
|
|
21c0d51fb6 | ||
|
|
a8f8be18cb | ||
|
|
8f156dc58f | ||
|
|
2522c62173 | ||
|
|
7f632cf4d2 | ||
|
|
999f10d7c7 | ||
|
|
3aa986cf7b | ||
|
|
b7d2a4b663 | ||
|
|
8440a72805 | ||
|
|
0cdbe9869e | ||
|
|
f84853fc30 | ||
|
|
fcc3a4d951 | ||
|
|
3f2375029e | ||
|
|
e5b578be55 | ||
|
|
c09d867b94 | ||
|
|
49018f6d91 | ||
|
|
0415681d53 | ||
|
|
4baf12bae3 | ||
|
|
c5f000a162 | ||
|
|
c154407624 | ||
|
|
254e885fcb | ||
|
|
ee9b31a0ee | ||
|
|
68865fb148 | ||
|
|
bb7fa88fb3 | ||
|
|
f35063da74 | ||
|
|
043923e674 | ||
|
|
2d38a1ca6e | ||
|
|
096464d2af | ||
|
|
b48f788a94 | ||
|
|
915e053a10 | ||
|
|
791bf5f88a | ||
|
|
fa710c47e9 | ||
|
|
5b5e71949b | ||
|
|
f3e73880c9 |
@@ -49,7 +49,7 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
|
||||
|
||||
## Installation
|
||||
|
||||
Latest version is `v0.118.0`.
|
||||
Latest version is `v0.118.1`.
|
||||
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ android {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
versionCode 1000
|
||||
versionName "0.118.1"
|
||||
versionCode 1002
|
||||
versionName "0.118.3"
|
||||
|
||||
if (appVersionName) versionName = appVersionName
|
||||
validateVersionName(versionName)
|
||||
@@ -195,11 +195,11 @@ clean {
|
||||
|
||||
task downloadBootstraps() {
|
||||
doLast {
|
||||
def version = "2024.06.17-r1+apt-android-7"
|
||||
downloadBootstrap("aarch64", "91a90661597fe14bb3c3563f5f65b243c0baaec42f2bc3d2243ff459e3942fb6", version)
|
||||
downloadBootstrap("arm", "d54b5eb2a305d72f267f9704deaca721b2bebbd3d4cca134aec31da719707997", version)
|
||||
downloadBootstrap("i686", "06a51ac1c679d68d52045509f1a705622c8f41748ef753660e31e3b6a846eba2", version)
|
||||
downloadBootstrap("x86_64", "4c8e43474c8d9543e01d4cbf3c4d7f59bbe4d696c38f6dece2b6ab3ba8881f2e", version)
|
||||
def version = "2025.03.28-r1+apt-android-7"
|
||||
downloadBootstrap("aarch64", "c8d702b6f742935001c37cda81b8ac69504a95d5cf28f2899532dd8cd4b057eb", version)
|
||||
downloadBootstrap("arm", "f3bb9d1b32552b34fff41861dbf193ec5ba2848d67d779ac1c7256da6640f85d", version)
|
||||
downloadBootstrap("i686", "36db3e1ac3547f9a174fd763bd9a484fa1a3449cdd81e1cf2408ff0454f839c6", version)
|
||||
downloadBootstrap("x86_64", "1c124ec2396ee70a51b0b0a574f29aa659526aa2b9f558f993b2fb05d1e51855", version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.ContextMenu;
|
||||
@@ -25,7 +24,6 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListView;
|
||||
@@ -36,6 +34,7 @@ import com.termux.R;
|
||||
import com.termux.app.terminal.TermuxActivityRootView;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.app.activities.HelpActivity;
|
||||
@@ -162,7 +161,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
|
||||
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
|
||||
private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_USERNAME = 11;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_PASSWORD = 2;
|
||||
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
|
||||
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
|
||||
private static final int CONTEXT_MENU_STYLING_ID = 5;
|
||||
@@ -587,17 +588,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TerminalSession currentSession = getCurrentSession();
|
||||
if (currentSession == null) return;
|
||||
|
||||
boolean addAutoFillMenu = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillManager autofillManager = getSystemService(AutofillManager.class);
|
||||
if (autofillManager != null && autofillManager.isEnabled()) {
|
||||
addAutoFillMenu = true;
|
||||
}
|
||||
}
|
||||
boolean autoFillEnabled = mTerminalView.isAutoFillEnabled();
|
||||
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
|
||||
if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
|
||||
if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText()))
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text);
|
||||
if (autoFillEnabled)
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_USERNAME, Menu.NONE, R.string.action_autofill_username);
|
||||
if (autoFillEnabled)
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_PASSWORD, Menu.NONE, R.string.action_autofill_password);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
|
||||
@@ -625,8 +625,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
|
||||
mTermuxTerminalViewClient.shareSessionTranscript();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_ID:
|
||||
requestAutoFill();
|
||||
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
|
||||
mTermuxTerminalViewClient.shareSelectedText();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_USERNAME:
|
||||
mTerminalView.requestAutoFillUsername();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_PASSWORD:
|
||||
mTerminalView.requestAutoFillPassword();
|
||||
return true;
|
||||
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
||||
onResetTerminalSession(session);
|
||||
@@ -654,6 +660,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
super.onContextMenuClosed(menu);
|
||||
// onContextMenuClosed() is triggered twice if back button is pressed to dismiss instead of tap for some reason
|
||||
mTerminalView.onContextMenuClosed(menu);
|
||||
}
|
||||
|
||||
private void showKillSessionDialog(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
@@ -700,15 +713,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
}
|
||||
|
||||
private void requestAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillManager autofillManager = getSystemService(AutofillManager.class);
|
||||
if (autofillManager != null && autofillManager.isEnabled()) {
|
||||
autofillManager.requestAutofill(mTerminalView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -212,24 +212,30 @@ final class TermuxInstaller {
|
||||
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
|
||||
}
|
||||
|
||||
// Run Termux bootstrap second stage
|
||||
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
|
||||
// Run Termux bootstrap second stage.
|
||||
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
|
||||
if (FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
|
||||
if (!FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
|
||||
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since script not found at \"" + termuxBootstrapSecondStageFile + "\" path.");
|
||||
} else {
|
||||
if (!FileUtils.fileExists(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", true)) {
|
||||
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since bash not found.");
|
||||
}
|
||||
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(-1,
|
||||
termuxBootstrapSecondStageFile, null, null,
|
||||
null, true, false);
|
||||
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
|
||||
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
|
||||
TermuxTask termuxTask = TermuxTask.execute(activity, executionCommand, null, new TermuxShellEnvironmentClient(), true);
|
||||
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
|
||||
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0 || stderrSet) {
|
||||
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into
|
||||
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
|
||||
// Generate debug report before deleting broken prefix directory to get `stat` info at time of failure.
|
||||
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
|
||||
|
||||
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into.
|
||||
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||
if (error != null)
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
|
||||
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
String path = uri.getLastPathSegment();
|
||||
int extIndex = path.lastIndexOf('.') + 1;
|
||||
if (extIndex > 0) {
|
||||
MimeTypeMap mimeMap = MimeTypeMap.getSingleton();
|
||||
String ext = path.substring(extIndex).toLowerCase();
|
||||
return mimeMap.getMimeTypeFromExtension(ext);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.widget.ListView;
|
||||
import com.termux.R;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
@@ -177,20 +178,16 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
public void onCopyTextToClipboard(TerminalSession session, String text) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||
ShareUtils.copyTextToClipboard(mActivity, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasteTextFromClipboard(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste)) mActivity.getTerminalView().mEmulator.paste(paste.toString());
|
||||
}
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
if (text != null)
|
||||
mActivity.getTerminalView().mEmulator.paste(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
@@ -647,17 +646,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
try {
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||
}
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
|
||||
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
|
||||
}
|
||||
|
||||
public void shareSelectedText() {
|
||||
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
|
||||
if (DataUtils.isNullOrEmpty(selectedText)) return;
|
||||
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
|
||||
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
|
||||
}
|
||||
|
||||
public void showUrlSelection() {
|
||||
@@ -678,9 +677,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
// Click to copy url to clipboard:
|
||||
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||
String url = (String) urls[which];
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
|
||||
}).setTitle(R.string.title_select_url_dialog).create();
|
||||
|
||||
// Long press to open URL:
|
||||
@@ -756,12 +753,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
if (session == null) return;
|
||||
if (!session.isRunning()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData == null) return;
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste))
|
||||
session.getEmulator().paste(paste.toString());
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
if (text != null)
|
||||
session.getEmulator().paste(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||
|
||||
static boolean isSharedTextAnUrl(String sharedText) {
|
||||
if (sharedText == null || sharedText.isEmpty()) return false;
|
||||
|
||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="password"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -65,6 +65,11 @@
|
||||
<string name="title_share_transcript">Terminal transcript</string>
|
||||
<string name="title_share_transcript_with">Send transcript to:</string>
|
||||
|
||||
<string name="action_share_selected_text">Share selected text</string>
|
||||
<string name="title_share_selected_text">Terminal Text</string>
|
||||
<string name="title_share_selected_text_with">Send selected text to:</string>
|
||||
|
||||
<string name="action_autofill_username">Autofill username</string>
|
||||
<string name="action_autofill_password">Autofill password</string>
|
||||
|
||||
<string name="action_reset_terminal">Reset</string>
|
||||
|
||||
@@ -24,6 +24,8 @@ public class TermuxFileReceiverActivityTest {
|
||||
|
||||
List<String> invalidUrls = new ArrayList<>();
|
||||
invalidUrls.add("a test with example.com");
|
||||
invalidUrls.add("");
|
||||
invalidUrls.add(null);
|
||||
for (String url : invalidUrls) {
|
||||
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
org.gradle.jvmargs=-Xmx2048M \
|
||||
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.lang=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.io=ALL-UNNAMED \
|
||||
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
|
||||
android.useAndroidX=true
|
||||
|
||||
minSdkVersion=24
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
jdk:
|
||||
- openjdk11
|
||||
env:
|
||||
JITPACK_NDK_VERSION: "21.1.6352462"
|
||||
|
||||
@@ -23,10 +23,10 @@ final class JNI {
|
||||
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
|
||||
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
|
||||
*/
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns, int cellWidth, int cellHeight);
|
||||
|
||||
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
|
||||
public static native void setPtyWindowSize(int fd, int rows, int cols);
|
||||
public static native void setPtyWindowSize(int fd, int rows, int cols, int cellWidth, int cellHeight);
|
||||
|
||||
/**
|
||||
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
|
||||
|
||||
@@ -79,9 +79,17 @@ public final class TerminalEmulator {
|
||||
private static final int ESC_CSI_SINGLE_QUOTE = 18;
|
||||
/** Escape processing: CSI ! */
|
||||
private static final int ESC_CSI_EXCLAMATION = 19;
|
||||
/** Escape processing: "ESC _" or Application Program Command (APC). */
|
||||
private static final int ESC_APC = 20;
|
||||
/** Escape processing: "ESC _" or Application Program Command (APC), followed by Escape. */
|
||||
private static final int ESC_APC_ESCAPE = 21;
|
||||
/** Escape processing: ESC [ <parameter bytes> */
|
||||
private static final int ESC_CSI_UNSUPPORTED_PARAMETER_BYTE = 22;
|
||||
/** Escape processing: ESC [ <parameter bytes> <intermediate bytes> */
|
||||
private static final int ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE = 23;
|
||||
|
||||
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
|
||||
private static final int MAX_ESCAPE_PARAMETERS = 16;
|
||||
/** The number of parameter arguments including colon separated sub-parameters. */
|
||||
private static final int MAX_ESCAPE_PARAMETERS = 32;
|
||||
|
||||
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
|
||||
private static final int MAX_OSC_STRING_LENGTH = 8192;
|
||||
@@ -126,17 +134,15 @@ public final class TerminalEmulator {
|
||||
private String mTitle;
|
||||
private final Stack<String> 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 +182,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 <code>mArgs[N]</code> 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 +244,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.
|
||||
*
|
||||
* <p>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
|
||||
@@ -312,13 +322,15 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
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 +380,10 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
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 +565,15 @@ public final class TerminalEmulator {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -635,6 +659,10 @@ public final class TerminalEmulator {
|
||||
case ESC_CSI:
|
||||
doCsi(b);
|
||||
break;
|
||||
case ESC_CSI_UNSUPPORTED_PARAMETER_BYTE:
|
||||
case ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE:
|
||||
doCsiUnsupportedParameterOrIntermediateByte(b);
|
||||
break;
|
||||
case ESC_CSI_EXCLAMATION:
|
||||
if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR).
|
||||
reset();
|
||||
@@ -1006,12 +1034,67 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return mRightMargin - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process byte while in the {@link #ESC_CSI_UNSUPPORTED_PARAMETER_BYTE} or
|
||||
* {@link #ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE} escape state.
|
||||
*
|
||||
* Parse unsupported parameter, intermediate and final bytes but ignore them.
|
||||
*
|
||||
* > For Control Sequence Introducer, ... the ESC [ is followed by
|
||||
* > - any number (including none) of "parameter bytes" in the range 0x30–0x3F (ASCII 0–9:;<=>?),
|
||||
* > - then by any number of "intermediate bytes" in the range 0x20–0x2F (ASCII space and !"#$%&'()*+,-./),
|
||||
* > - then finally by a single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~).
|
||||
*
|
||||
* - https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
|
||||
* - https://invisible-island.net/xterm/ecma-48-parameter-format.html#section5.4
|
||||
*/
|
||||
private void doCsiUnsupportedParameterOrIntermediateByte(int b) {
|
||||
if (mEscapeState == ESC_CSI_UNSUPPORTED_PARAMETER_BYTE && b >= 0x30 && b <= 0x3F) {
|
||||
// Supported `0–9:;>?` or unsupported `<=` parameter byte after an
|
||||
// initial unsupported parameter byte in `doCsi()`, or a sequential parameter byte.
|
||||
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
|
||||
} else if (b >= 0x20 && b <= 0x2F) {
|
||||
// Optional intermediate byte `!"#$%&'()*+,-./` after parameter or intermediate byte.
|
||||
continueSequence(ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE);
|
||||
} else if (b >= 0x40 && b <= 0x7E) {
|
||||
// Final byte `@A–Z[\]^_`a–z{|}~` after parameter or intermediate byte.
|
||||
// Calling `unknownSequence()` would log an error with only a final byte, so ignore it for now.
|
||||
finishSequence();
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
}
|
||||
}
|
||||
|
||||
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
|
||||
private void doCsiQuestionMark(int b) {
|
||||
switch (b) {
|
||||
@@ -1281,6 +1364,7 @@ public final class TerminalEmulator {
|
||||
mEscapeState = ESC;
|
||||
mArgIndex = 0;
|
||||
Arrays.fill(mArgs, -1);
|
||||
mArgsSubParamsBitSet = 0;
|
||||
}
|
||||
|
||||
private void doLinefeed() {
|
||||
@@ -1375,8 +1459,8 @@ public final class TerminalEmulator {
|
||||
// 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 +1474,6 @@ public final class TerminalEmulator {
|
||||
break;
|
||||
case '[':
|
||||
continueSequence(ESC_CSI);
|
||||
mIsCSIStart = true;
|
||||
mLastCSIArg = null;
|
||||
break;
|
||||
case '=': // DECKPAM
|
||||
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
|
||||
@@ -1403,6 +1485,9 @@ public final class TerminalEmulator {
|
||||
case '>': // DECKPNM
|
||||
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
|
||||
break;
|
||||
case '_': // APC - Application Program Command.
|
||||
continueSequence(ESC_APC);
|
||||
break;
|
||||
default:
|
||||
unknownSequence(b);
|
||||
break;
|
||||
@@ -1584,8 +1669,8 @@ public final class TerminalEmulator {
|
||||
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);
|
||||
@@ -1607,12 +1692,16 @@ public final class TerminalEmulator {
|
||||
}
|
||||
mCursorCol = newCol;
|
||||
break;
|
||||
case '?': // Esc [ ? -- start of a private mode set
|
||||
case '?': // Esc [ ? -- start of a private parameter byte
|
||||
continueSequence(ESC_CSI_QUESTIONMARK);
|
||||
break;
|
||||
case '>': // "Esc [ >" --
|
||||
case '>': // "Esc [ >" -- start of a private parameter byte
|
||||
continueSequence(ESC_CSI_BIGGERTHAN);
|
||||
break;
|
||||
case '<': // "Esc [ <" -- start of a private parameter byte
|
||||
case '=': // "Esc [ =" -- start of a private parameter byte
|
||||
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
|
||||
break;
|
||||
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
|
||||
setCursorColRespectingOriginMode(getArg0(1) - 1);
|
||||
break;
|
||||
@@ -1712,8 +1801,10 @@ public final class TerminalEmulator {
|
||||
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 +1853,12 @@ public final class TerminalEmulator {
|
||||
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 +1877,19 @@ public final class TerminalEmulator {
|
||||
} 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 +1918,8 @@ public final class TerminalEmulator {
|
||||
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 +1928,30 @@ public final class TerminalEmulator {
|
||||
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 +1965,8 @@ public final class TerminalEmulator {
|
||||
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 +2202,64 @@ public final class TerminalEmulator {
|
||||
|
||||
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
|
||||
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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
|
||||
* <p>References:
|
||||
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
|
||||
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
|
||||
* */
|
||||
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
|
||||
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;
|
||||
}
|
||||
if (value > 9999)
|
||||
value = 9999;
|
||||
mArgs[mArgIndex] = value;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
continueSequence(mEscapeState);
|
||||
} else if (b == ';' || b == ':') {
|
||||
if (mArgIndex + 1 < mArgs.length) {
|
||||
mArgIndex++;
|
||||
if (b == ':') {
|
||||
mArgsSubParamsBitSet |= 1 << mArgIndex;
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else if (b == ';') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
mArgIndex++;
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
logError("Too many parameters when in state: " + mEscapeState);
|
||||
}
|
||||
mLastCSIArg = b;
|
||||
continueSequence(mEscapeState);
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.UUID;
|
||||
* A terminal session, consisting of a process coupled to a terminal interface.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 final class TerminalSession extends TerminalOutput {
|
||||
}
|
||||
|
||||
/** 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 final class TerminalSession extends TerminalOutput {
|
||||
* @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];
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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", " ");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 class ControlSequenceIntroducerTest extends TerminalTestCase {
|
||||
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 <a href="https://sw.kovidgoyal.net/kitty/underlines/">Colored and styled underlines</a>:
|
||||
*
|
||||
* <pre>
|
||||
* <ESC>[4:0m # no underline
|
||||
* <ESC>[4:1m # straight underline
|
||||
* <ESC>[4:2m # double underline
|
||||
* <ESC>[4:3m # curly underline
|
||||
* <ESC>[4:4m # dotted underline
|
||||
* <ESC>[4:5m # dashed underline
|
||||
* <ESC>[4m # straight underline (for backwards compat)
|
||||
* <ESC>[24m # no underline (for backwards compat)
|
||||
* </pre>
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ public class HistoryTest extends TerminalTestCase {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,11 @@ public class ResizeTest extends TerminalTestCase {
|
||||
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 ", " ");
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,16 @@ public class ScrollRegionTest extends TerminalTestCase {
|
||||
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 class ScrollRegionTest extends TerminalTestCase {
|
||||
" xxx"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* See <a href="https://github.com/termux/termux-packages/issues/12556">reported issue</a>.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public class TerminalTest extends TerminalTestCase {
|
||||
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 class TerminalTest extends TerminalTestCase {
|
||||
}
|
||||
|
||||
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 class TerminalTest extends TerminalTestCase {
|
||||
// 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 class TerminalTest extends TerminalTestCase {
|
||||
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() {
|
||||
|
||||
@@ -13,7 +13,10 @@ import java.util.Set;
|
||||
|
||||
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<ChangedTitle> titleChanges = new ArrayList<>();
|
||||
public final List<String> clipboardPuts = new ArrayList<>();
|
||||
@@ -108,7 +111,7 @@ public abstract class TerminalTestCase extends TestCase {
|
||||
|
||||
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 abstract class TerminalTestCase extends TestCase {
|
||||
}
|
||||
|
||||
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 abstract class TerminalTestCase extends TestCase {
|
||||
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) {
|
||||
|
||||
@@ -233,7 +233,7 @@ public final class TerminalRenderer {
|
||||
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();
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.termux.view;
|
||||
|
||||
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.graphics.Typeface;
|
||||
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.HapticFeedbackConstants;
|
||||
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;
|
||||
|
||||
private static final String LOG_TAG = "TerminalView";
|
||||
@@ -440,6 +482,14 @@ public final class TerminalView extends View {
|
||||
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.
|
||||
*
|
||||
@@ -550,11 +600,14 @@ public final class TerminalView extends View {
|
||||
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()) {
|
||||
@@ -578,6 +631,7 @@ public final class TerminalView extends View {
|
||||
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;
|
||||
@@ -839,6 +893,9 @@ public final class TerminalView extends View {
|
||||
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;
|
||||
@@ -846,6 +903,26 @@ public final class TerminalView extends View {
|
||||
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.
|
||||
*
|
||||
@@ -893,7 +970,7 @@ public final class TerminalView extends View {
|
||||
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();
|
||||
|
||||
@@ -971,12 +1048,20 @@ public final class TerminalView extends View {
|
||||
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)
|
||||
@@ -985,6 +1070,95 @@ public final class TerminalView extends View {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -1184,6 +1358,25 @@ public final class TerminalView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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();
|
||||
|
||||
@@ -12,6 +12,8 @@ import android.view.MenuItem;
|
||||
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;
|
||||
@@ -21,6 +23,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();
|
||||
|
||||
@@ -28,9 +31,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;
|
||||
@@ -113,7 +116,7 @@ public class TextSelectionCursorController implements CursorController {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -132,7 +135,7 @@ public class TextSelectionCursorController implements CursorController {
|
||||
|
||||
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;
|
||||
@@ -141,7 +144,13 @@ public class TextSelectionCursorController implements CursorController {
|
||||
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;
|
||||
}
|
||||
@@ -356,6 +365,22 @@ public class TextSelectionCursorController implements CursorController {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ android {
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
implementation "org.lsposed.hiddenapibypass:hiddenapibypass:6.1"
|
||||
|
||||
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
|
||||
// noinspection GradleDependency
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.termux.shared.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class SELinuxUtils {
|
||||
|
||||
public static final String ANDROID_OS_SELINUX_CLASS = "android.os.SELinux";
|
||||
|
||||
private static final String LOG_TAG = "SELinuxUtils";
|
||||
|
||||
/**
|
||||
* Gets the security context of the current process.
|
||||
*
|
||||
* @return Returns a {@link String} representing the security context of the current process.
|
||||
* This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getContext() {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
String methodName = "getContext";
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
|
||||
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName);
|
||||
if (method == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) ReflectionUtils.invokeMethod(method, null).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the security context of a given process id.
|
||||
*
|
||||
* @param pid The pid of process.
|
||||
* @return Returns a {@link String} representing the security context of the given pid.
|
||||
* This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getPidContext(int pid) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
String methodName = "getPidContext";
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
|
||||
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, int.class);
|
||||
if (method == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) ReflectionUtils.invokeMethod(method, null, pid).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the security context of a file object.
|
||||
*
|
||||
* @param path The pathname of the file object.
|
||||
* @return Returns a {@link String} representing the security context of the file.
|
||||
* This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getFileContext(@NonNull String path) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
String methodName = "getFileContext";
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
|
||||
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, String.class);
|
||||
if (method == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) ReflectionUtils.invokeMethod(method, null, path).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import android.net.Uri;
|
||||
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;
|
||||
@@ -23,6 +22,8 @@ import com.termux.shared.packages.PermissionUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ShareUtils {
|
||||
|
||||
private static final String LOG_TAG = "ShareUtils";
|
||||
@@ -56,6 +57,18 @@ public class ShareUtils {
|
||||
* @param text The text to share.
|
||||
*/
|
||||
public static void shareText(final Context context, final String subject, final String text) {
|
||||
shareText(context, subject, text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Share text.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param subject The subject for sharing.
|
||||
* @param text The text to share.
|
||||
* @param title The title for share menu.
|
||||
*/
|
||||
public static void shareText(final Context context, final String subject, final String text, @Nullable final String title) {
|
||||
if (context == null || text == null) return;
|
||||
|
||||
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
|
||||
@@ -63,29 +76,85 @@ public class ShareUtils {
|
||||
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false));
|
||||
|
||||
openSystemAppChooser(context, shareTextIntent, context.getString(R.string.title_share_with));
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.termux.shared.models;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
|
||||
@@ -10,6 +12,25 @@ import java.io.Serializable;
|
||||
*/
|
||||
public class ReportInfo implements Serializable {
|
||||
|
||||
/**
|
||||
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
|
||||
*
|
||||
* Like when calling `Bundle.getSerializable()` on Android.
|
||||
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
|
||||
* `java.io.InvalidClassException: <class_name>; local class incompatible`
|
||||
*
|
||||
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
|
||||
* app is compiled, even if its kept during library compilation.
|
||||
*
|
||||
* **See Also:**
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
|
||||
*/
|
||||
@Keep
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
|
||||
/** The user action that was being processed for which the report was generated. */
|
||||
public final String userAction;
|
||||
/** The internal app component that sent the report. */
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.termux.shared.models;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.activities.TextIOActivity;
|
||||
@@ -19,6 +20,25 @@ import java.io.Serializable;
|
||||
*/
|
||||
public class TextIOInfo implements Serializable {
|
||||
|
||||
/**
|
||||
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
|
||||
*
|
||||
* Like when calling `Bundle.getSerializable()` on Android.
|
||||
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
|
||||
* `java.io.InvalidClassException: <class_name>; local class incompatible`
|
||||
*
|
||||
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
|
||||
* app is compiled, even if its kept during library compilation.
|
||||
*
|
||||
* **See Also:**
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
|
||||
*/
|
||||
@Keep
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
|
||||
public static final int GENERAL_DATA_SIZE_LIMIT_IN_BYTES = 1000;
|
||||
public static final int LABEL_SIZE_LIMIT_IN_BYTES = 4000;
|
||||
public static final int TEXT_SIZE_LIMIT_IN_BYTES = 100000 - GENERAL_DATA_SIZE_LIMIT_IN_BYTES - LABEL_SIZE_LIMIT_IN_BYTES; // < 100KB
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.UserManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -17,8 +18,10 @@ import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
|
||||
@@ -94,6 +97,55 @@ public class PackageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code seInfo} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* String retrieved from the seinfo tag found in selinux policy. This value can be set through
|
||||
* the mac_permissions.xml policy construct. This value is used for setting an SELinux security
|
||||
* context on the process as well as its data directory.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-7.1.0_r1:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=609
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=981
|
||||
* https://cs.android.com/android/platform/superproject/+/android-7.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=282
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=375
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/be0b8896d1bc385d4c8fb54c21929745935dcbea
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the selinux info or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApplicationInfoSeInfoForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "seinfo" : "seInfo", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfo field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code seInfoUser} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* Also check {@link #getApplicationInfoSeInfoForPackage(ApplicationInfo)}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the selinux info user or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApplicationInfoSeInfoUserForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, "seInfoUser", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfoUser field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the app name for the package associated with the {@code context}.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package com.termux.shared.reflection;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ReflectionUtils {
|
||||
|
||||
private static boolean HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = Build.VERSION.SDK_INT < Build.VERSION_CODES.P;
|
||||
|
||||
private static final String LOG_TAG = "ReflectionUtils";
|
||||
|
||||
/**
|
||||
* Bypass android hidden API reflection restrictions.
|
||||
* https://github.com/LSPosed/AndroidHiddenApiBypass
|
||||
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
|
||||
*/
|
||||
public static void bypassHiddenAPIReflectionRestrictions() {
|
||||
if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Logger.logDebug(LOG_TAG, "Bypassing android hidden api reflection restrictions");
|
||||
HiddenApiBypass.addHiddenApiExemptions("");
|
||||
HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if android hidden API reflection restrictions are bypassed. */
|
||||
public static boolean areHiddenAPIReflectionRestrictionsBypassed() {
|
||||
return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get a {@link Field} for the specified class.
|
||||
*
|
||||
* @param clazz The {@link Class} for which to return the field.
|
||||
* @param fieldName The name of the {@link Field}.
|
||||
* @return Returns the {@link Field} if getting the it was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Field getDeclaredField(@NonNull Class<?> clazz, @NonNull String fieldName) {
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field for \"" + clazz.getName() + "\" class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Class that represents result of invoking a field. */
|
||||
public static class FieldInvokeResult {
|
||||
public boolean success;
|
||||
public Object value;
|
||||
|
||||
FieldInvokeResult(boolean success, Object value) {
|
||||
this.value = success;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value for a {@link Field} of an object for the specified class.
|
||||
*
|
||||
* @param clazz The {@link Class} to which the object belongs to.
|
||||
* @param fieldName The name of the {@link Field}.
|
||||
* @param object The {@link Object} instance from which to get the field value.
|
||||
* @return Returns the {@link FieldInvokeResult} of invoking the field. The
|
||||
* {@link FieldInvokeResult#success} will be {@code true} if invoking the field was successful,
|
||||
* otherwise {@code false}. The {@link FieldInvokeResult#value} will contain the field
|
||||
* {@link Object} value.
|
||||
*/
|
||||
@NonNull
|
||||
public static <T> FieldInvokeResult invokeField(@NonNull Class<T> clazz, @NonNull String fieldName, T object) {
|
||||
try {
|
||||
Field field = getDeclaredField(clazz, fieldName);
|
||||
if (field == null) return new FieldInvokeResult(false, null);
|
||||
return new FieldInvokeResult(true, field.get(object));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for \"" + clazz.getName() + "\" class", e);
|
||||
return new FieldInvokeResult(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters.
|
||||
*/
|
||||
@Nullable
|
||||
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName) {
|
||||
return getDeclaredMethod(clazz, methodName, new Class<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link Method} for the specified class with the specified parameters.
|
||||
*
|
||||
* @param clazz The {@link Class} for which to return the method.
|
||||
* @param methodName The name of the {@link Method}.
|
||||
* @param parameterTypes The parameter types of the method.
|
||||
* @return Returns the {@link Method} if getting the it was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName, Class<?>... parameterTypes) {
|
||||
try {
|
||||
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + methodName + "\" method for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #invokeVoidMethod(Method, Object, Object...)} without arguments.
|
||||
*/
|
||||
public static boolean invokeVoidMethod(@NonNull Method method, Object obj) {
|
||||
return invokeVoidMethod(method, obj, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a {@link Method} on the specified object with the specified arguments that returns
|
||||
* {@code void}.
|
||||
*
|
||||
* @param method The {@link Method} to invoke.
|
||||
* @param obj The {@link Object} the method should be invoked from.
|
||||
* @param args The arguments to pass to the method.
|
||||
* @return Returns {@code true} if invoking the method was successful, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean invokeVoidMethod(@NonNull Method method, Object obj, Object... args) {
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
method.invoke(obj, args);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Class that represents result of invoking a method that has a non-void return type. */
|
||||
public static class MethodInvokeResult {
|
||||
public boolean success;
|
||||
public Object value;
|
||||
|
||||
MethodInvokeResult(boolean success, Object value) {
|
||||
this.value = success;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #invokeMethod(Method, Object, Object...)} without arguments.
|
||||
*/
|
||||
@NonNull
|
||||
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj) {
|
||||
return invokeMethod(method, obj, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a {@link Method} on the specified object with the specified arguments.
|
||||
*
|
||||
* @param method The {@link Method} to invoke.
|
||||
* @param obj The {@link Object} the method should be invoked from.
|
||||
* @param args The arguments to pass to the method.
|
||||
* @return Returns the {@link MethodInvokeResult} of invoking the method. The
|
||||
* {@link MethodInvokeResult#success} will be {@code true} if invoking the method was successful,
|
||||
* otherwise {@code false}. The {@link MethodInvokeResult#value} will contain the {@link Object}
|
||||
* returned by the method.
|
||||
*/
|
||||
@NonNull
|
||||
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj, Object... args) {
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
return new MethodInvokeResult(true, method.invoke(obj, args));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
|
||||
return new MethodInvokeResult(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getConstructor(String, Class[])} without parameters.
|
||||
*/
|
||||
@Nullable
|
||||
public static Constructor<?> getConstructor(@NonNull String className) {
|
||||
return getConstructor(className, new Class<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getConstructor(Class, Class[])} to get a {@link Constructor} for the
|
||||
* {@code className}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Constructor<?> getConstructor(@NonNull String className, Class<?>... parameterTypes) {
|
||||
try {
|
||||
return getConstructor(Class.forName(className), parameterTypes);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + className + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link Constructor} for the specified class with the specified parameters.
|
||||
*
|
||||
* @param clazz The {@link Class} for which to return the constructor.
|
||||
* @param parameterTypes The parameter types of the constructor.
|
||||
* @return Returns the {@link Constructor} if getting the it was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Constructor<?> getConstructor(@NonNull Class<?> clazz, Class<?>... parameterTypes) {
|
||||
try {
|
||||
Constructor<?> constructor = clazz.getConstructor(parameterTypes);
|
||||
constructor.setAccessible(true);
|
||||
return constructor;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #invokeConstructor(Constructor, Object...)} without arguments.
|
||||
*/
|
||||
@Nullable
|
||||
public static Object invokeConstructor(@NonNull Constructor<?> constructor) {
|
||||
return invokeConstructor(constructor, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a {@link Constructor} with the specified arguments.
|
||||
*
|
||||
* @param constructor The {@link Constructor} to invoke.
|
||||
* @param args The arguments to pass to the constructor.
|
||||
* @return Returns the new instance if invoking the constructor was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Object invokeConstructor(@NonNull Constructor<?> constructor, Object... args) {
|
||||
try {
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(args);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + constructor.getName() + "\" constructor with args: " + Arrays.toString(args), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.android.SELinuxUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
@@ -57,10 +61,41 @@ public class TermuxShellUtils {
|
||||
if (TERMUX_API_VERSION_NAME != null)
|
||||
environment.add("TERMUX_API_VERSION=" + TERMUX_API_VERSION_NAME);
|
||||
|
||||
|
||||
|
||||
environment.add("TERM=xterm-256color");
|
||||
environment.add("COLORTERM=truecolor");
|
||||
|
||||
try {
|
||||
ApplicationInfo applicationInfo = currentPackageContext.getPackageManager().getApplicationInfo(
|
||||
TermuxConstants.TERMUX_PACKAGE_NAME, 0);
|
||||
if (applicationInfo != null && !applicationInfo.enabled) {
|
||||
applicationInfo = null;
|
||||
}
|
||||
|
||||
if (applicationInfo != null) {
|
||||
environment.add("TERMUX_APP__DATA_DIR=" + applicationInfo.dataDir);
|
||||
environment.add("TERMUX_APP__LEGACY_DATA_DIR=" + "/data/data/" + applicationInfo.packageName);
|
||||
|
||||
environment.add("TERMUX_APP__SE_FILE_CONTEXT=" + SELinuxUtils.getFileContext(applicationInfo.dataDir));
|
||||
|
||||
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
|
||||
environment.add("TERMUX_APP__SE_INFO=" + PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
|
||||
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
environment.add("TERMUX__ROOTFS_DIR=" + TermuxConstants.TERMUX_FILES_DIR_PATH);
|
||||
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
environment.add("TERMUX__HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
environment.add("TERMUX__PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
|
||||
environment.add("TERMUX__SE_PROCESS_CONTEXT=" + SELinuxUtils.getContext());
|
||||
|
||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||
@@ -75,6 +110,8 @@ public class TermuxShellUtils {
|
||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||
|
||||
environment.add("ANDROID__BUILD_VERSION_SDK=" + Build.VERSION.SDK_INT);
|
||||
|
||||
if (isFailSafe) {
|
||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||
environment.add("PATH= " + System.getenv("PATH"));
|
||||
|
||||
@@ -10,7 +10,10 @@ import java.util.Map;
|
||||
public class ExtraKeysConstants {
|
||||
|
||||
/** Defines the repetitive keys that can be passed to {@link ExtraKeysView#setRepetitiveKeys(List)}. */
|
||||
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL");
|
||||
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList(
|
||||
"UP", "DOWN", "LEFT", "RIGHT",
|
||||
"BKSP", "DEL",
|
||||
"PGUP", "PGDN");
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,16 @@ package com.termux.shared.termux;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Build;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.termux.shared.android.SELinuxUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
@@ -32,6 +37,8 @@ public class AndroidUtils {
|
||||
*/
|
||||
public static String getAppInfoMarkdownString(@NonNull final Context context) {
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
ApplicationInfo applicationInfo = context.getApplicationInfo();
|
||||
if (applicationInfo == null) return null;
|
||||
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
|
||||
@@ -44,6 +51,13 @@ public class AndroidUtils {
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_INSTALLED_ON_EXTERNAL_STORAGE", true);
|
||||
}
|
||||
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_PROCESS_CONTEXT", SELinuxUtils.getContext());
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_FILE_CONTEXT", SELinuxUtils.getFileContext(context.getFilesDir().getAbsolutePath()));
|
||||
|
||||
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_INFO", PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
|
||||
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
|
||||
|
||||
String filesDir = context.getFilesDir().getAbsolutePath();
|
||||
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
|
||||
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
|
||||
@@ -99,7 +113,19 @@ public class AndroidUtils {
|
||||
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
|
||||
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
|
||||
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
|
||||
|
||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
|
||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_32_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_32_BIT_ABIS));
|
||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_64_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_64_BIT_ABIS));
|
||||
|
||||
// If on Android >= 15
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
try {
|
||||
appendPropertyToMarkdownIfSet(markdownString, "PAGE_SIZE", Os.sysconf(OsConstants._SC_PAGESIZE));
|
||||
} catch (Throwable t) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
markdownString.append("\n##\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user