diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java
index 0c9f74125b..fe546e6de3 100644
--- a/app/src/main/java/com/termux/app/TermuxActivity.java
+++ b/app/src/main/java/com/termux/app/TermuxActivity.java
@@ -152,7 +152,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
* If between onResume() and onStop(). Note that only one session is in the foreground of the terminal view at the
* time, so if the session causing a change is not in the foreground it should probably be treated as background.
*/
- private boolean mIsVisible;
+ private volatile boolean mIsVisible;
/**
* If onResume() was called after onCreate().
@@ -856,7 +856,9 @@ public boolean isTerminalToolbarTextInputViewSelected() {
public void termuxSessionListNotifyUpdated() {
- mTermuxSessionListViewController.notifyDataSetChanged();
+ // Ensure adapter notification always runs on the UI thread to prevent
+ // IllegalStateException from ListView when modified from background. (#5027)
+ runOnUiThread(() -> mTermuxSessionListViewController.notifyDataSetChanged());
}
public boolean isVisible() {
diff --git a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java
index f27f922cab..e91844f537 100644
--- a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java
+++ b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java
@@ -94,10 +94,14 @@ public void onReceive(Context context, Intent intent) {
if (contentTypeExtra == null) {
String fileName = fileToShare.getName();
int lastDotIndex = fileName.lastIndexOf('.');
- String fileExtension = fileName.substring(lastDotIndex + 1);
- MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
- // Lower casing makes it work with e.g. "JPG":
- contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase());
+ if (lastDotIndex >= 0 && lastDotIndex < fileName.length() - 1) {
+ String fileExtension = fileName.substring(lastDotIndex + 1);
+ MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
+ // Lower casing makes it work with e.g. "JPG":
+ contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase());
+ } else {
+ contentTypeToUse = null;
+ }
if (contentTypeToUse == null) contentTypeToUse = "application/octet-stream";
} else {
contentTypeToUse = contentTypeExtra;
diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java
index 8025d0bd2c..55aa2d73a8 100644
--- a/app/src/main/java/com/termux/app/TermuxService.java
+++ b/app/src/main/java/com/termux/app/TermuxService.java
@@ -275,8 +275,11 @@ private synchronized void killAllTermuxExecutionCommands() {
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
termuxSessions.get(i).killIfExecuting(this, processResult);
- if (!processResult)
- mShellManager.mTermuxSessions.remove(termuxSessions.get(i));
+ if (!processResult) {
+ synchronized (mShellManager.mTermuxSessions) {
+ mShellManager.mTermuxSessions.remove(termuxSessions.get(i));
+ }
+ }
}
@@ -311,13 +314,17 @@ private void actionAcquireWakeLock() {
Logger.logDebug(LOG_TAG, "Acquiring WakeLocks");
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock");
- mWakeLock.acquire();
+ if (pm != null) {
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock");
+ mWakeLock.acquire();
+ }
// http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
- mWifiLock.acquire();
+ if (wm != null) {
+ mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
+ mWifiLock.acquire();
+ }
if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) {
PermissionUtils.requestDisableBatteryOptimizations(this);
@@ -510,11 +517,12 @@ public void onAppShellExited(final AppShell termuxTask) {
if (termuxTask != null) {
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
- Logger.logVerbose(LOG_TAG, "The onTermuxTaskExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
-
// If the execution command was started for a plugin, then process the results
- if (executionCommand != null && executionCommand.isPluginExecutionCommand)
- TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ if (executionCommand != null) {
+ Logger.logVerbose(LOG_TAG, "The onTermuxTaskExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
+ if (executionCommand.isPluginExecutionCommand)
+ TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ }
mShellManager.mTermuxTasks.remove(termuxTask);
}
@@ -641,13 +649,19 @@ public void onTermuxSessionExited(final TermuxSession termuxSession) {
if (termuxSession != null) {
ExecutionCommand executionCommand = termuxSession.getExecutionCommand();
- Logger.logVerbose(LOG_TAG, "The onTermuxSessionExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
-
// If the execution command was started for a plugin, then process the results
- if (executionCommand != null && executionCommand.isPluginExecutionCommand)
- TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ if (executionCommand != null) {
+ if (executionCommand.isPluginExecutionCommand) {
+ Logger.logVerbose(LOG_TAG, "The onTermuxSessionExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
+ TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ }
+ }
- mShellManager.mTermuxSessions.remove(termuxSession);
+ // Synchronize on the sessions list to prevent concurrent modification
+ // when other synchronized methods access it from different threads.
+ synchronized (mShellManager.mTermuxSessions) {
+ mShellManager.mTermuxSessions.remove(termuxSession);
+ }
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
// activity in is foreground
@@ -760,8 +774,10 @@ public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClie
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
- for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
- mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
+ synchronized (mShellManager.mTermuxSessions) {
+ for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
+ mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
+ }
}
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
@@ -769,8 +785,10 @@ public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionAct
* clients do not hold an activity references.
*/
public synchronized void unsetTermuxTerminalSessionClient() {
- for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
- mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
+ synchronized (mShellManager.mTermuxSessions) {
+ for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
+ mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
+ }
mTermuxTerminalSessionActivityClient = null;
}
@@ -784,7 +802,7 @@ private Notification buildNotification() {
// Set pending intent to be launched when notification is clicked
Intent notificationIntent = TermuxActivity.newInstance(this);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
// Set notification text
@@ -858,6 +876,11 @@ private synchronized void updateNotification() {
}
}
+ /** Public wrapper for {@link #updateNotification()} to allow session title changes to refresh the notification. */
+ public void updateNotificationPublic() {
+ updateNotification();
+ }
+
@@ -917,10 +940,11 @@ public synchronized int getIndexOfSession(TerminalSession terminalSession) {
}
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
+ if (sessionHandle == null) return null;
TerminalSession terminalSession;
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
terminalSession = mShellManager.mTermuxSessions.get(i).getTerminalSession();
- if (terminalSession.mHandle.equals(sessionHandle))
+ if (terminalSession.mHandle != null && terminalSession.mHandle.equals(sessionHandle))
return terminalSession;
}
return null;
@@ -931,8 +955,8 @@ public synchronized AppShell getTermuxTaskForShellName(String name) {
AppShell appShell;
for (int i = 0, len = mShellManager.mTermuxTasks.size(); i < len; i++) {
appShell = mShellManager.mTermuxTasks.get(i);
- String shellName = appShell.getExecutionCommand().shellName;
- if (shellName != null && shellName.equals(name))
+ ExecutionCommand ec = appShell.getExecutionCommand();
+ if (ec != null && ec.shellName != null && ec.shellName.equals(name))
return appShell;
}
return null;
@@ -943,8 +967,8 @@ public synchronized TermuxSession getTermuxSessionForShellName(String name) {
TermuxSession termuxSession;
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
termuxSession = mShellManager.mTermuxSessions.get(i);
- String shellName = termuxSession.getExecutionCommand().shellName;
- if (shellName != null && shellName.equals(name))
+ ExecutionCommand ec = termuxSession.getExecutionCommand();
+ if (ec != null && ec.shellName != null && ec.shellName.equals(name))
return termuxSession;
}
return null;
diff --git a/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java b/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java
index ca5c07407c..16348a155f 100644
--- a/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java
+++ b/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java
@@ -135,6 +135,10 @@ void showErrorDialogAndQuit(String message) {
}
void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
+ if (uri == null) {
+ showErrorDialogAndQuit("No file URI received");
+ return;
+ }
try {
Logger.logVerbose(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\"");
@@ -151,8 +155,21 @@ void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true);
- InputStream in = getContentResolver().openInputStream(uri);
- promptNameAndSave(in, attachmentFileName);
+ final String finalAttachmentFileName = attachmentFileName;
+
+ // Offload potentially blocking I/O to a background thread
+ new Thread(() -> {
+ try {
+ InputStream in = getContentResolver().openInputStream(uri);
+ // Switch back to UI thread for dialog interaction
+ runOnUiThread(() -> promptNameAndSave(in, finalAttachmentFileName));
+ } catch (Exception e) {
+ runOnUiThread(() -> {
+ showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
+ Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
+ });
+ }
+ }).start();
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
@@ -160,38 +177,50 @@ void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
+ if (in == null) {
+ showErrorDialogAndQuit("Unable to open input stream for file");
+ return;
+ }
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName,
R.string.action_file_received_edit, text -> {
- File outFile = saveStreamWithName(in, text);
- if (outFile == null) return;
-
- final File editorProgramFile = new File(EDITOR_PROGRAM);
- if (!editorProgramFile.isFile()) {
- showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
- + "Create this file as a script or a symlink - it will be called with the received file as only argument.");
- return;
- }
-
- // Do this for the user if necessary:
- //noinspection ResultOfMethodCallIgnored
- editorProgramFile.setExecutable(true);
-
- final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM);
-
- Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
- executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
- executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
- startService(executeIntent);
- finish();
+ // Offload file copy to background thread
+ new Thread(() -> {
+ File outFile = saveStreamWithName(in, text);
+ if (outFile == null) return;
+ // UI updates after copy
+ runOnUiThread(() -> {
+ final File editorProgramFile = new File(EDITOR_PROGRAM);
+ if (!editorProgramFile.isFile()) {
+ showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
+ + "Create this file as a script or a symlink - it will be called with the received file as only argument.");
+ return;
+ }
+ // Do this for the user if necessary:
+ //noinspection ResultOfMethodCallIgnored
+ editorProgramFile.setExecutable(true);
+
+ final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM);
+ Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
+ executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
+ executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
+ startService(executeIntent);
+ finish();
+ });
+ }).start();
},
R.string.action_file_received_open_directory, text -> {
- if (saveStreamWithName(in, text) == null) return;
-
- Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
- executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
- executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
- startService(executeIntent);
- finish();
+ // Offload file copy to background thread
+ new Thread(() -> {
+ if (saveStreamWithName(in, text) == null) return;
+ // UI updates after copy
+ runOnUiThread(() -> {
+ Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
+ executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
+ executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
+ startService(executeIntent);
+ finish();
+ });
+ }).start();
},
android.R.string.cancel, text -> finish(), dialog -> {
if (mFinishOnDismissNameDialog) finish();
@@ -202,12 +231,12 @@ public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (DataUtils.isNullOrEmpty(attachmentFileName)) {
- showErrorDialogAndQuit("File name cannot be null or empty");
+ runOnUiThread(() -> showErrorDialogAndQuit("File name cannot be null or empty"));
return null;
}
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
- showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
+ runOnUiThread(() -> showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath()));
return null;
}
@@ -222,9 +251,13 @@ public File saveStreamWithName(InputStream in, String attachmentFileName) {
}
return outFile;
} catch (IOException e) {
- showErrorDialogAndQuit("Error saving file:\n\n" + e);
- Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
+ runOnUiThread(() -> {
+ showErrorDialogAndQuit("Error saving file:\n\n" + e);
+ Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
+ });
return null;
+ } finally {
+ try { in.close(); } catch (IOException ignored) {}
}
}
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java
index bf914b977b..963ef19d03 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java
@@ -51,7 +51,12 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
- TerminalSession sessionAtRow = getItem(position).getTerminalSession();
+ TermuxSession termuxSession = getItem(position);
+ if (termuxSession == null) {
+ sessionTitleView.setText("null session");
+ return sessionRowView;
+ }
+ TerminalSession sessionAtRow = termuxSession.getTerminalSession();
if (sessionAtRow == null) {
sessionTitleView.setText("null session");
return sessionRowView;
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
index bd789145f2..968f9427ac 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
@@ -27,6 +27,7 @@
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import com.termux.shared.termux.terminal.io.BellHandler;
import com.termux.shared.logger.Logger;
+import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
@@ -133,6 +134,13 @@ public void onTitleChanged(@NonNull TerminalSession updatedSession) {
}
termuxSessionListNotifyUpdated();
+
+ // Update the persistent notification so it reflects the new session name (#5048)
+ if (updatedSession == mActivity.getCurrentSession()) {
+ TermuxService service = mActivity.getTermuxService();
+ if (service != null)
+ service.updateNotificationPublic();
+ }
}
@Override
@@ -153,9 +161,12 @@ public void onSessionFinished(@NonNull TerminalSession finishedSession) {
boolean isPluginExecutionCommandWithPendingResult = false;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null) {
- isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult();
- if (isPluginExecutionCommandWithPendingResult)
- Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
+ ExecutionCommand ec = termuxSession.getExecutionCommand();
+ if (ec != null) {
+ isPluginExecutionCommandWithPendingResult = ec.isPluginExecutionCommandWithPendingResult();
+ if (isPluginExecutionCommandWithPendingResult)
+ Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
+ }
}
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
@@ -302,6 +313,9 @@ public void setCurrentSession(TerminalSession session) {
// be stale, like current session not selected or scrolled to.
checkAndScrollToSession(session);
updateBackgroundColor();
+ // Also check for font and colors when session changes, to handle the case where
+ // Termux is started from a widget/shortcut and the session is ready immediately. (#4849)
+ checkForFontAndColors();
}
void notifyOfSessionChange() {
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java
index 700c5e5098..c9a5b677ff 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java
@@ -641,7 +641,9 @@ public void onFocusChange(View view, boolean hasFocus) {
// will also show keyboard even if it was closed before opening url. #2111
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
mActivity.getTerminalView().requestFocus();
- mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
+ // Use a retry-based approach instead of a fixed 300ms delay to handle
+ // cases where window focus arrives late (e.g. returning via launcher). (#5014)
+ showSoftKeyboardWithRetry(3);
}
}
@@ -654,6 +656,24 @@ private Runnable getShowSoftKeyboardRunnable() {
return mShowSoftKeyboardRunnable;
}
+ /**
+ * Show the soft keyboard with retry logic to handle late window focus delivery.
+ * When returning to Termux via launcher (not recents), the window focus may arrive
+ * after a fixed delay, causing showSoftInput to be ignored. This retries up to
+ * {@code maxRetries} times with increasing delays. (#5014)
+ */
+ private void showSoftKeyboardWithRetry(int maxRetries) {
+ if (maxRetries <= 0) return;
+ long delay = 100;
+ mActivity.getTerminalView().postDelayed(() -> {
+ if (mActivity.getTerminalView().hasWindowFocus()) {
+ KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
+ } else if (maxRetries > 1) {
+ showSoftKeyboardWithRetry(maxRetries - 1);
+ }
+ }, delay);
+ }
+
public void setTerminalCursorBlinkerState(boolean start) {
diff --git a/app/src/main/java/com/termux/app/terminal/io/SafeViewPager.java b/app/src/main/java/com/termux/app/terminal/io/SafeViewPager.java
new file mode 100644
index 0000000000..e62eee7d43
--- /dev/null
+++ b/app/src/main/java/com/termux/app/terminal/io/SafeViewPager.java
@@ -0,0 +1,35 @@
+package com.termux.app.terminal.io;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import androidx.viewpager.widget.ViewPager;
+
+/**
+ * A safe wrapper around {@link ViewPager} that guards against IllegalArgumentException
+ * caused by {@code MotionEvent} pointer index out of range errors on certain Android versions.
+ *
+ * If {@link ViewPager#onInterceptTouchEvent(MotionEvent)} throws an
+ * {@link IllegalArgumentException}, we catch it and return {@code false} to avoid the crash.
+ * This mirrors the common workaround for the AndroidX bug (see issue #3478).
+ */
+public class SafeViewPager extends ViewPager {
+ public SafeViewPager(Context context) {
+ super(context);
+ }
+
+ public SafeViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ try {
+ return super.onInterceptTouchEvent(ev);
+ } catch (IllegalArgumentException e) {
+ // Pointer index out of range – swallow the exception and prevent a crash.
+ // Returning false means the ViewPager will not intercept the touch event.
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java
index 7974d6dbc1..b01eeab808 100644
--- a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java
+++ b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
- private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
+ private static final File BASE_DIR = TermuxConstants.TERMUX_FILES_DIR;
// The default columns to return information about a root if no specific
diff --git a/app/src/main/res/layout/activity_termux.xml b/app/src/main/res/layout/activity_termux.xml
index 831ea7cfb8..64161795b9 100644
--- a/app/src/main/res/layout/activity_termux.xml
+++ b/app/src/main/res/layout/activity_termux.xml
@@ -94,7 +94,7 @@
- 0) mTerminalToProcessIOQueue.write(data, offset, count);
+ if (mShellPid > 0 && data != null && count > 0) mTerminalToProcessIOQueue.write(data, offset, count);
}
/** Write the Unicode code point to the terminal encoded in UTF-8. */
diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java
index 0b3f515682..28b2494f71 100644
--- a/terminal-view/src/main/java/com/termux/view/TerminalView.java
+++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java
@@ -926,6 +926,7 @@ public boolean handleKeyCode(int keyCode, int keyMod) {
public boolean handleKeyCodeAction(int keyCode, int keyMod) {
boolean shiftDown = (keyMod & KeyHandler.KEYMOD_SHIFT) != 0;
+ boolean ctrlDown = (keyMod & KeyHandler.KEYMOD_CTRL) != 0;
switch (keyCode) {
case KeyEvent.KEYCODE_PAGE_UP:
@@ -939,6 +940,12 @@ public boolean handleKeyCodeAction(int keyCode, int keyMod) {
motionEvent.recycle();
return true;
}
+ // When Ctrl is pressed (without shift), do not consume the event here.
+ // Let it fall through to KeyHandler.getCode() so the Ctrl modifier
+ // is included in the escape sequence (e.g. \033[5;5~ for Ctrl+PgUp).
+ if (ctrlDown) {
+ return false;
+ }
}
return false;
diff --git a/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java b/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java
index bca47578e9..fe7f7a8a0f 100644
--- a/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java
@@ -179,14 +179,16 @@ public static Properties getSystemProperties() {
// multiline values will be ignored
Pattern propertiesPattern = Pattern.compile("^\\[([^]]+)]: \\[(.+)]$");
+ Process process = null;
+ BufferedReader bufferedReader = null;
try {
- Process process = new ProcessBuilder()
+ process = new ProcessBuilder()
.command("/system/bin/getprop")
.redirectErrorStream(true)
.start();
InputStream inputStream = process.getInputStream();
- BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+ bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line, key, value;
while ((line = bufferedReader.readLine()) != null) {
@@ -199,11 +201,20 @@ public static Properties getSystemProperties() {
}
}
- bufferedReader.close();
- process.destroy();
-
} catch (IOException e) {
Logger.logStackTraceWithMessage("Failed to get run \"/system/bin/getprop\" to get system properties.", e);
+ } finally {
+ // Fix for issue #5144: always close streams and destroy process to prevent resource leaks
+ if (bufferedReader != null) {
+ try {
+ bufferedReader.close();
+ } catch (IOException e) {
+ Logger.logStackTraceWithMessage("Failed to close getprop reader.", e);
+ }
+ }
+ if (process != null) {
+ process.destroy();
+ }
}
//for (String key : systemProperties.stringPropertyNames()) {
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java
index 1cd4ce0668..ee737b1561 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java
@@ -342,11 +342,11 @@ public static void sendCrashReportNotification(final Context currentPackageConte
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(termuxPackageContext);
- PendingIntent contentIntent = PendingIntent.getActivity(termuxPackageContext, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent contentIntent = PendingIntent.getActivity(termuxPackageContext, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
- deleteIntent = PendingIntent.getBroadcast(termuxPackageContext, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ deleteIntent = PendingIntent.getBroadcast(termuxPackageContext, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// Setup the notification channel if not already set up
setupCrashReportsNotificationChannel(termuxPackageContext);
@@ -363,8 +363,16 @@ public static void sendCrashReportNotification(final Context currentPackageConte
// Send the notification
NotificationManager notificationManager = NotificationUtils.getNotificationManager(termuxPackageContext);
- if (notificationManager != null)
- notificationManager.notify(nextNotificationId, builder.build());
+ if (notificationManager != null) {
+ try {
+ notificationManager.notify(nextNotificationId, builder.build());
+ } catch (SecurityException e) {
+ // On some devices (e.g. when a plugin sends a crash notification from a
+ // different package context), the notification manager may reject the
+ // notify call with "Package X does not belong to Y". Log and continue. (#5145)
+ Logger.logError(logTag, "Failed to send crash report notification: " + e.getMessage());
+ }
+ }
}
/**
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java
index b747381c02..06431d5538 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java
@@ -94,6 +94,7 @@ public static class EXTRA_KEY_DISPLAY_MAPS {
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
put("PASTE", "⎘"); // U+2398
put("SCROLL", "⇳"); // U+21F3
+ put("SPACE", "␣"); // U+2423
}};
public static final ExtraKeyDisplayMap LESS_KNOWN_CHARACTERS_DISPLAY = new ExtraKeyDisplayMap() {{
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java
index 4fbbf1e171..29d45d0c68 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java
@@ -23,6 +23,8 @@
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.view.Gravity;
+import android.graphics.Paint;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.GridLayout;
@@ -409,10 +411,15 @@ public void reload(ExtraKeysInfo extraKeysInfo, float heightPx) {
button = new MaterialButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
- button.setText(buttonInfo.getDisplay());
+ String displayText = buttonInfo.getDisplay();
+ button.setText(displayText);
button.setTextColor(mButtonTextColor);
- button.setAllCaps(mButtonTextAllCaps);
+ // Only apply all-caps for ASCII labels to avoid corrupting
+ // non-ASCII glyphs (arrows, special chars) via toUpperCase().
+ button.setAllCaps(mButtonTextAllCaps && isAscii(displayText));
button.setPadding(0, 0, 0, 0);
+ button.setGravity(Gravity.CENTER);
+ button.setPaintFlags(button.getPaintFlags() | Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
button.setOnClickListener(view -> {
performExtraKeyButtonHapticFeedback(view, buttonInfo, button);
@@ -678,4 +685,12 @@ public static int maximumLength(Object[][] matrix) {
return m;
}
+ private static boolean isAscii(String text) {
+ if (text == null) return true;
+ for (int i = 0; i < text.length(); i++) {
+ if (text.charAt(i) > 127) return false;
+ }
+ return true;
+ }
+
}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java
index cb37ddc3fe..f202d5fd70 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java
@@ -380,11 +380,11 @@ public static void sendPluginCommandErrorNotification(Context currentPackageCont
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(termuxPackageContext);
- PendingIntent contentIntent = PendingIntent.getActivity(termuxPackageContext, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent contentIntent = PendingIntent.getActivity(termuxPackageContext, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
- deleteIntent = PendingIntent.getBroadcast(termuxPackageContext, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ deleteIntent = PendingIntent.getBroadcast(termuxPackageContext, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// Setup the notification channel if not already set up
setupPluginCommandErrorsNotificationChannel(termuxPackageContext);