Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7ce4816
docs: add Termux app issue triage
billybox1926-jpg Jun 10, 2026
6ffad3a
docs: add Termux app issue triage
billybox1926-jpg Jun 10, 2026
08bf364
docs: clean Termux app issue triage whitespace
billybox1926-jpg Jun 10, 2026
631bb06
fix: avoid blocking file receiver content open on main thread (#5092)
billybox1926-jpg Jun 10, 2026
3c4830b
fix: make file receiver background open lambda compile (#5092)
billybox1926-jpg Jun 10, 2026
d88e6b7
fix: declare file receiver attachment name for background open (#5092)
billybox1926-jpg Jun 10, 2026
77fc6a7
ci: run Android build with JDK 17
billybox1926-jpg Jun 10, 2026
79d4f9d
ci: use JDK 17 for all Gradle workflows
billybox1926-jpg Jun 10, 2026
88f9d13
ci: fix JDK setup step indentation
billybox1926-jpg Jun 10, 2026
a8390f9
fix: close streams and destroy process in getSystemProperties finally…
billybox1926-jpg Jun 10, 2026
f55a6d4
docs: update termux-app issue ledger with #5144 fix
billybox1926-jpg Jun 10, 2026
e108bb6
fix: add FLAG_IMMUTABLE to PendingIntent calls to prevent crash on An…
billybox1926-jpg Jun 10, 2026
5a8414e
docs: update ledger with #5047 fix
billybox1926-jpg Jun 10, 2026
6d206c6
docs: record skipped Termux app issue candidates
billybox1926-jpg Jun 10, 2026
1dd9aef
fix: move file receiver stream copy off main thread (#5092)
billybox1926-jpg Jun 10, 2026
f8f9e4c
fix: keep file receiver save errors on UI thread (#5092)
billybox1926-jpg Jun 10, 2026
0fe9a58
ci: trigger Android CI for #5092
billybox1926-jpg Jun 10, 2026
dcc970f
ci: enable Android CI for workbench issue branch
billybox1926-jpg Jun 10, 2026
c97b044
docs: record #5092 verification
billybox1926-jpg Jun 10, 2026
2a7d23b
fix: safe ViewPager to prevent pointerIndex out of range crash (#3478)
billybox1926-jpg Jun 10, 2026
b667fb2
docs: record #3478 verification
billybox1926-jpg Jun 10, 2026
0a84468
fix: prevent crash when pasting ESC-only text (#5119)
billybox1926-jpg Jun 10, 2026
bda4bd9
fix: update notification on session rename (#5048)
billybox1926-jpg Jun 10, 2026
956f002
fix: catch SecurityException in crash report notification (#5145)
billybox1926-jpg Jun 10, 2026
a5ce756
fix: ensure session list adapter notify runs on UI thread (#5027)
billybox1926-jpg Jun 10, 2026
c819280
fix: retry soft keyboard show on late window focus (#5014)
billybox1926-jpg Jun 10, 2026
16f04c1
docs: record batch issue-factory progress
billybox1926-jpg Jun 10, 2026
17c36f2
fix: apply font/colors when session is attached (#4849)
billybox1926-jpg Jun 10, 2026
334cad2
fix: harden session and key handling batch 2
billybox1926-jpg Jun 10, 2026
8d1b54e
fix: guard against null executionCommand in onTermuxSessionExited
billybox1926-jpg Jun 10, 2026
19df80d
fix: guard against null URI in FileReceiverActivity handleContentUri
billybox1926-jpg Jun 10, 2026
b567614
fix: synchronize session list removal in killAllTermuxExecutionCommands
billybox1926-jpg Jun 10, 2026
70a6e77
fix: guard against null data in TerminalSession write
billybox1926-jpg Jun 10, 2026
cd82cd0
fix: make mIsVisible volatile for thread safety
billybox1926-jpg Jun 10, 2026
51d8e98
fix: only apply all-caps to ASCII extra key labels
billybox1926-jpg Jun 10, 2026
39771c7
fix: guard against null PowerManager/WifiManager in actionAcquireWake…
billybox1926-jpg Jun 10, 2026
4e20c3d
fix: guard against null InputStream in promptNameAndSave
billybox1926-jpg Jun 10, 2026
58de0c4
fix: guard against null mSession in TerminalEmulator paste
billybox1926-jpg Jun 10, 2026
30b85b2
fix: synchronize session list iteration in setTermuxTerminalSessionCl…
billybox1926-jpg Jun 10, 2026
2e7592d
fix: synchronize session list iteration in unsetTermuxTerminalSession…
billybox1926-jpg Jun 10, 2026
a856ac8
fix: guard against null sessionHandle and null mHandle in getTerminal…
billybox1926-jpg Jun 10, 2026
970ec05
fix: guard against missing file extension in TermuxOpenReceiver MIME …
billybox1926-jpg Jun 10, 2026
752ab2b
fix: guard against null TermuxSession in TermuxSessionsListViewContro…
billybox1926-jpg Jun 10, 2026
ffe05d7
fix: guard against null executionCommand in onAppShellExited
billybox1926-jpg Jun 10, 2026
79209eb
fix: guard against null executionCommand in onTermuxSessionExited
billybox1926-jpg Jun 10, 2026
2ab612e
fix: guard against null getExecutionCommand in onSessionFinished
billybox1926-jpg Jun 10, 2026
27b7959
fix: guard against null getExecutionCommand in getTermuxTaskForShellName
billybox1926-jpg Jun 10, 2026
65be782
fix: guard against null getExecutionCommand in getTermuxSessionForShe…
billybox1926-jpg Jun 10, 2026
8159666
fix: add missing ExecutionCommand import in TermuxTerminalSessionActi…
billybox1926-jpg Jun 10, 2026
dcb30fe
build: use shared debug keystore for matched-stack signing
billybox1926-jpg Jun 11, 2026
30556d0
cherry-pick: batch of upstream PRs (4250,2600,2971,3705,4931,3165,293…
Jun 11, 2026
dcda2e3
docs: refresh fork maintenance README
billybox1926-jpg Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/attach_debug_apks_to_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ jobs:
with:
ref: ${{ env.GITHUB_REF }}

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Build and attach APKs to release
shell: bash {0}
env:
Expand All @@ -45,8 +51,8 @@ jobs:
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"

echo "Building APKs for 'APK_VERSION_TAG' release"
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG"
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}"
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$APK_VERSION_TAG' release."
fi
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/debug_build.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
name: Build

on:
workflow_dispatch:
push:
branches:
- master
- 'github-releases/**'
- workbench-app-issue-factory
pull_request:
branches:
- master
Expand All @@ -24,7 +26,7 @@ jobs:
- name: Setup java 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
distribution: temurin
java-version: '17'

- name: Build APKs
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dependency-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
distribution: temurin
java-version: '17'
- name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v5
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup java 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
distribution: temurin
java-version: '17'
- name: Execute tests
run: |
Expand Down
355 changes: 115 additions & 240 deletions README.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions TERMUX_APP_FIRST_BATCH_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# TERMUX_APP_FIRST_BATCH_PLAN

## Selected Issues

- Bucket A: #5092: [Bug]: Yet another potential ANR / main-thread blocking risk
- Bucket A: #3478: [Bug]: Termux Auto Crash
- Bucket A: #152: big crash bug
- Bucket B: #5128: [Patch] | PR BLOCKED . posting patch as issue instead | idle timeout : idle drain safe guard . remove from power intensive apps
- Bucket B: #4589: "extra-keys" of "termux.properties" malfunctioning
- Bucket B: #3245: [Feature]: Need Double-width rendering of ambiguous characters
- Bucket B: #787: Are javadocs for termux source online anywhere?

## Rationale

First batch is limited to buckets A and B only: high-confidence app-code crashes and low-risk static/code-health fixes.

Do not batch-fix these automatically. Pick one issue, inspect source, make the smallest patch, and use GitHub Actions as the gate.
32 changes: 32 additions & 0 deletions TERMUX_APP_ISSUE_LEDGER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# TERMUX_APP_ISSUE_LEDGER

## Summary
- Total upstream open issues: 474
- Fixed in this fork: 4 (so far in this session)
- Remaining actionable: ~471

## Fixed Issues (this session)

### App repo (termux-app)
- #5047 Missing PendingIntent.FLAG_IMMUTABLE causes crash on Android 12+ → Fixed (added FLAG_IMMUTABLE to 4 PendingIntent calls in 3 files) — commit e108bb62 — CI pending
- #5144 Resource leak: Streams and Process not closed in AndroidUtils.getSystemProperties() → Fixed (finally block for cleanup) — commit a8390f92 — CI pending

### Package repo (termux-api-package)
- #224 run_api_command leaks file descriptors → Fixed (close server sockets) — commit 961ca8c — CI green
- #200 termux-api command should detect if Termux:API plugin is not installed → Fixed (is_termux_api_installed check) — commit 931a7ba — CI green

## Bucket Counts
- Bucket A: 3 (quick wins — crash/ANR bugs)
- Bucket B: 4 (patches/features)
- Bucket C: 235 (mostly device-specific/vague)
- Bucket D: 6 (features)
- Bucket E: 31 (environment/install issues)
- Bucket F: 13 (medium features)
- Bucket G: 182 (feature requests/enhancements)

## Next Targets (Bucket A - Quick Wins)
- #3478: [Bug]: Termux Auto Crash (vague, needs investigation)
- #152: big crash bug (old, CyanogenMod era)

## Recently Fixed (by phone Hermes, confirmed in code)
- #5092: ANR / main-thread blocking in FileReceiverActivity → Fixed (background thread) — commits 631bb060, 3c4830bb, d88e6b78
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ android {

signingConfigs {
debug {
storeFile file('testkey_untrusted.jks')
keyAlias 'alias'
storePassword 'xrj45yWGLbsO7W0v'
keyPassword 'xrj45yWGLbsO7W0v'
storeFile file('termux-debug-shared.jks')
keyAlias 'termuxdebug'
storePassword 'termuxdebug'
keyPassword 'termuxdebug'
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
android:label="@string/application_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:supportsRtl="true"
android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar"
tools:targetApi="m">

Expand All @@ -57,6 +57,7 @@
android:launchMode="singleTask"
android:resizeableActivity="true"
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"
android:excludeFromRecents="true"
tools:targetApi="n">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/com/termux/app/TermuxActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 8 additions & 4 deletions app/src/main/java/com/termux/app/TermuxOpenReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
74 changes: 49 additions & 25 deletions app/src/main/java/com/termux/app/TermuxService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}


Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -760,17 +774,21 @@ 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)}
* so that the {@link TermuxService} and {@link TerminalSession} and {@link TerminalEmulator}
* 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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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();
}




Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Loading