diff --git a/.github/workflows/attach_debug_apks_to_release.yml b/.github/workflows/attach_debug_apks_to_release.yml index 0c6e3f7635..fa3f0b10cd 100644 --- a/.github/workflows/attach_debug_apks_to_release.yml +++ b/.github/workflows/attach_debug_apks_to_release.yml @@ -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: @@ -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 diff --git a/.github/workflows/debug_build.yml b/.github/workflows/debug_build.yml index 1e1cedccb1..f0d3f7e1f8 100644 --- a/.github/workflows/debug_build.yml +++ b/.github/workflows/debug_build.yml @@ -1,10 +1,12 @@ name: Build on: + workflow_dispatch: push: branches: - master - 'github-releases/**' + - workbench-app-issue-factory pull_request: branches: - master @@ -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 diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index ae332ae17e..138227aa6c 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -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 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 69de0254df..2ab1cb19aa 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -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: | diff --git a/README.md b/README.md index 662198d369..92008a465c 100644 --- a/README.md +++ b/README.md @@ -1,295 +1,170 @@ -# Termux application +# Termux app fork maintenance -[![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions) -[![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions) -[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) -[![Join the Termux discord server](https://img.shields.io/discord/641256914684084234.svg?label=&logo=discord&logoColor=ffffff&color=5865F2)](https://discord.gg/HXpF69X) -[![Termux library releases at Jitpack](https://jitpack.io/v/termux/termux-app.svg)](https://jitpack.io/#termux/termux-app) +This repository is a maintenance fork of the upstream [Termux application](https://github.com/termux/termux-app). It is used for focused bug-fix work, runtime testing, and preparing changes before they are split into clean upstream pull requests. +This is not the official Termux release repository. Users who only want to install Termux should normally use the official Termux channels and documentation. This fork exists so we can keep a controlled workbench while learning the app, testing fixes, and building a reliable local Android/Termux stack. -[Termux](https://termux.dev) is an Android terminal application and Linux environment. +## 2026 maintenance status -Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages). +Current maintenance rule: keep one active workbench lane, make small fixes, push only after the branch is clean, and let GitHub Actions decide whether the source is healthy. -Quick how-to about Termux package management is available at [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management). It also has info on how to fix **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. +The expected active branch for this fork is: -**We are looking for Termux Android application maintainers.** - -*** - -**NOTICE: Termux may be unstable on Android 12+.** Android OS will kill any (phantom) processes greater than 32 (limit is for all apps combined) and also kill any processes using excessive CPU. You may get `[Process completed (signal 9) - press Enter]` message in the terminal without actually exiting the shell process yourself. Check the related issue [#2366](https://github.com/termux/termux-app/issues/2366), [issue tracker](https://issuetracker.google.com/u/1/issues/205156966), [phantom cached and empty processes docs](https://github.com/agnostic-apollo/Android-Docs/blob/master/en/docs/apps/processes/phantom-cached-and-empty-processes.md) and [this TLDR comment](https://github.com/termux/termux-app/issues/2366#issuecomment-1237468220) on how to disable trimming of phantom and excessive cpu usage processes. A proper docs page will be added later. An option to disable the killing should be available in Android 12L or 13, so upgrade at your own risk if you are on Android 11, specially if you are not rooted. - -*** - -## Contents -- [Termux App and Plugins](#termux-app-and-plugins) -- [Installation](#installation) -- [Uninstallation](#uninstallation) -- [Important Links](#important-links) -- [Debugging](#debugging) -- [For Maintainers and Contributors](#for-maintainers-and-contributors) -- [Forking](#forking) -- [Sponsors and Funders](#sponsors-and-funders) -## - - - -## Termux App and Plugins - -The core [Termux](https://github.com/termux/termux-app) app comes with the following optional plugin apps. - -- [Termux:API](https://github.com/termux/termux-api) -- [Termux:Boot](https://github.com/termux/termux-boot) -- [Termux:Float](https://github.com/termux/termux-float) -- [Termux:Styling](https://github.com/termux/termux-styling) -- [Termux:Tasker](https://github.com/termux/termux-tasker) -- [Termux:Widget](https://github.com/termux/termux-widget) -## - - - -## Installation - -Latest version is `v0.118.3`. - -**NOTICE: It is highly recommended that you update to `v0.118.0` or higher ASAP for various bug fixes, including a critical world-readable vulnerability reported [here](https://termux.github.io/general/2022/02/15/termux-apps-vulnerability-disclosures.html). See [below](#google-play-store-experimental-branch) for information regarding Termux on Google Play.** - -Termux can be obtained through various sources listed below for **only** Android `>= 7` with full support for apps and packages. - -Support for both app and packages 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`, however it was re-added just for the app *without any support for package updates* on [2022-05-24](https://github.com/termux/termux-app/pull/2740) via the [GitHub](#github) sources. Check [here](https://github.com/termux/termux-app/wiki/Termux-on-android-5-or-6) for the details. - -The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [`sharedUserId`](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from `F-Droid` and another one from a different source like `GitHub`. Android Package Manager will also normally not allow installation of APKs with different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms. - -If you wish to install from a different source, then you must **uninstall any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.dev/wiki/Backing_up_Termux) before the uninstallation so that you can restore it after re-installing from Termux different source. - -In the following paragraphs, *"bootstrap"* refers to the minimal packages that are shipped with the `termux-app` itself to start a working shell environment. Its zips are built and released [here](https://github.com/termux/termux-packages/releases). - -### F-Droid - -Termux application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux/). - -You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install Termux. You can download the Termux APK directly from the site by clicking the `Download APK` link at the bottom of each version section. - -It usually takes a few days (or even a week or more) for updates to be available on `F-Droid` once an update has been released on `GitHub`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml) a new `GitHub` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps on `F-Droid`. Moreover, the Termux maintainers also do not have access to the APK signing keys of `F-Droid` releases, so we cannot release an APK ourselves on `GitHub` that would be compatible with `F-Droid` releases. - -The `F-Droid` app often may not notify you of updates and you will manually have to do a pull down swipe action in the `Updates` tab of the app for it to check updates. Make sure battery optimizations are disabled for the app, check https://dontkillmyapp.com/ for details on how to do that. - -Only a universal APK is released, which will work on all supported architectures. The APK and bootstrap installation size will be `~180MB`. `F-Droid` does [not support](https://github.com/termux/termux-app/pull/1904) architecture specific APKs. - -### GitHub - -Termux application can be obtained on `GitHub` either from [`GitHub Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`GitHub Build Action`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml?query=branch%3Amaster+event%3Apush) workflows. **For android `>= 7`, only install `apt-android-7` variants. For android `5` and `6`, only install `apt-android-5` variants.** - -The APKs for `GitHub Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released. - -The APKs for `GitHub Build` action workflows will be listed under `Artifacts` section of a workflow run. These are created for each commit/push done to the repository and can be used by users who don't want to wait for releases and want to try out the latest features immediately or want to test their pull requests. Note that for action workflows, you need to be [**logged into a `GitHub` account**](https://github.com/login) for the `Artifacts` links to be enabled/clickable. If you are using the [`GitHub` app](https://github.com/mobile), then make sure to open workflow link in a browser like Chrome or Firefox that has your GitHub account logged in since the in-app browser may not be logged in. - -The APKs for both of these are [`debuggable`](https://developer.android.com/studio/debug) and are compatible with each other but they are not compatible with other sources. - -Both universal and architecture specific APKs are released. The APK and bootstrap installation size will be `~180MB` if using universal and `~120MB` if using architecture specific. Check [here](https://github.com/termux/termux-app/issues/2153) for details. - -**Security warning**: APK files on GitHub are signed with a test key that has been [shared with community](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks). This IS NOT an official developer key and everyone can use it to generate releases for own testing. Be very careful when using Termux GitHub builds obtained elsewhere except https://github.com/termux/termux-app. Everyone is able to use it to forge a malicious Termux update installable over the GitHub build. Think twice about installing Termux builds distributed via Telegram or other social media. If your device get caught by malware, we will not be able to help you. - -The [test key](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks) shall not be used to impersonate @termux and can't be used for this anyway. This key is not trusted by us and it is quite easy to detect its use in user generated content. - -
-Keystore information - -``` -Alias name: alias -Creation date: Oct 4, 2019 -Entry type: PrivateKeyEntry -Certificate chain length: 1 -Certificate[1]: -Owner: CN=APK Signer, OU=Earth, O=Earth -Issuer: CN=APK Signer, OU=Earth, O=Earth -Serial number: 29be297b -Valid from: Wed Sep 04 02:03:24 EEST 2019 until: Tue Oct 26 02:03:24 EEST 2049 -Certificate fingerprints: - SHA1: 51:79:55:EA:BF:69:FC:05:7C:41:C7:D3:79:DB:BC:EF:20:AD:85:F2 - SHA256: B6:DA:01:48:0E:EF:D5:FB:F2:CD:37:71:B8:D1:02:1E:C7:91:30:4B:DD:6C:4B:F4:1D:3F:AA:BA:D4:8E:E5:E1 -Signature algorithm name: SHA1withRSA (disabled) -Subject Public Key Algorithm: 2048-bit RSA key -Version: 3 +```text +workbench-app-issue-factory ``` -
- -### Google Play Store **(Experimental branch)** - -There is currently a build of Termux available on Google Play for Android 11+ devices, with extensive adjustments in order to pass policy requirements there. This is under development and has missing functionality and bugs (see [here](https://github.com/termux-play-store/) for status updates) compared to the stable F-Droid build, which is why most users who can should still use F-Droid or GitHub build as mentioned above. - -Currently, Google Play will try to update installations away from F-Droid ones. Updating will still fail as [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element#uid) has been removed. A planned 0.118.1 F-Droid release will fix this by setting a higher version code than used for the PlayStore app. Meanwhile, to prevent Google Play from attempting to download and then fail to install the Google Play releases over existing installations, you can open the Termux apps pages on Google Play and then click on the 3 dots options button in the top right and then disable the Enable auto update toggle. However, the Termux apps updates will still show in the PlayStore app updates list. - -If you want to help out with testing the Google Play build (or cannot install Termux from other sources), be aware that it's built from a separate repository (https://github.com/termux-play-store/) - be sure to report issues [there](https://github.com/termux-play-store/termux-issues/issues/new/choose), as any issues encountered might very well be specific to that repository. - -## Uninstallation - -Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation. - -To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#termux-app-and-plugins). - -Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if it’s available on your device and search `termux` in the applications list. - -Even if you think you have not installed any of the plugins, it's strongly suggested to go through the application list in Android settings and double-check. -## - - - -## Important Links - -### Community -All community links are available [here](https://wiki.termux.com/wiki/Community). - -The main ones are the following. - -- [Termux Reddit community](https://reddit.com/r/termux) -- [Termux User Matrix Channel](https://matrix.to/#/#termux_termux:gitter.im) ([Gitter](https://gitter.im/termux/termux)) -- [Termux Dev Matrix Channel](https://matrix.to/#/#termux_dev:gitter.im) ([Gitter](https://gitter.im/termux/dev)) -- [Termux X (Twitter)](https://twitter.com/termuxdevs) -- [Termux Support Email](mailto:support@termux.dev) - -### Wikis +Do not push directly to `master` or `main` unless that is explicitly requested. Treat the workbench branch as the integration lane and create upstream PR branches from clean, reviewable slices. -- [Termux Wiki](https://wiki.termux.com/wiki/) -- [Termux App Wiki](https://github.com/termux/termux-app/wiki) -- [Termux Packages Wiki](https://github.com/termux/termux-packages/wiki) +## What this repo contains -### Miscellaneous -- [FAQ](https://wiki.termux.com/wiki/FAQ) -- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout) -- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux) -- [Package Management](https://wiki.termux.com/wiki/Package_Management) -- [Remote Access](https://wiki.termux.com/wiki/Remote_Access) -- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) -- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings) -- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard) -- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage) -- [Android APIs](https://wiki.termux.com/wiki/Termux:API) -- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348) -- [Running Commands in Termux From Other Apps via `RUN_COMMAND` intent](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent) -- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) +The `termux-app` repository is the Android app itself: the terminal UI, session management, app lifecycle, terminal emulator integration, storage access, Android intents, and plugin-facing integration points. +Packages installed inside Termux are maintained separately in [termux/termux-packages](https://github.com/termux/termux-packages). Termux:API command wrappers and plugin behavior live in separate repositories. When debugging app/API behavior, keep the boundary clear: -### Terminal - -
- - -### Terminal resources - -- [XTerm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) -- [vt100.net](https://vt100.net/) -- [Terminal codes (ANSI and terminfo equivalents)](https://wiki.bash-hackers.org/scripting/terminalcodes) +```text +Termux app -> terminal, sessions, UI, app lifecycle +Termux:API app -> Android API receiver/listener implementation +termux-api package -> shell commands and native wrapper binaries +termux-packages -> packages installed inside Termux +``` -### Terminal emulators +## Branch discipline -- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED). +Use this branch model unless the maintainer intentionally changes it: -- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](https://iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](https://iterm2.com/documentation-escape-codes.html)). +```text +origin/master or origin/main -> upstream/default history +workbench-app-issue-factory -> local integration lane +fix/ -> optional short-lived PR branch +``` -- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole). +Rules: -- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm). +- keep `workbench-app-issue-factory` clean and buildable; +- prefer five small source fixes over one giant rewrite; +- do not rewrite Gradle, SDK, workflow, or manifest configuration while fixing unrelated issues; +- use revert or `--force-with-lease` only when deliberately repairing a workbench branch; +- preserve phone/runtime fixes unless CI proves they broke source; +- preserve upstream behavior unless the issue specifically requires a behavior change. -- xterm: The grandfather of terminal emulators. [Source](https://invisible-island.net/datafiles/release/xterm.tar.gz). +## Build gate -- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot) +GitHub Actions is the main green/red gate for this fork. Local Windows or phone builds are useful for quick feedback, but local tooling failures are suspect until Actions confirms the problem. -- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator). -
+For a local Windows build, use PowerShell from the repository root: -## +```powershell +cd C:\Users\Billy\Documents\GitHub\termux-app +.\gradlew.bat clean :app:assembleDebug --no-daemon --console=plain +``` +When a build fails, chase the first real compiler, lint, or test error. Ignore surrounding Gradle noise until the first source failure is understood. +## Runtime testing -### Debugging +A green build only proves the app compiles. It does not prove Android package identity, signatures, shared UID behavior, plugin wiring, or phone runtime behavior. -You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time. +Before declaring a fix done, test the relevant path on a device when possible: -The plugin apps **do not execute the commands themselves** but send execution intents to `Termux` app, which has its own log level which can be set in `Termux` app settings -> `Termux` -> `Debugging` -> `Log Level`. So you must set log level for both `Termux` and the respective plugin app settings to get all the info. +```powershell +adb devices +adb install -r path\to\app-debug.apk +adb shell am start -n com.termux/.app.TermuxActivity +adb logcat -d | findstr /i "termux crash exception fatal" +``` -Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (`Ctrl+c` to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat). +For plugin/API work, test the whole stack that participates in the behavior. Many bugs sit in the seams between the app, plugin APK, package wrapper, socket listener, broadcast receiver, and Android process lifecycle. -Moreover, users can generate termux files `stat` info and `logcat` dump automatically too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead. +## Package identity and signing warning -Users must post complete report (optionally without sensitive info) when reporting issues. Issues opened with **(partial) screenshots of error reports** instead of text will likely be automatically closed/deleted. +Termux and its plugins use Android package identity and signing rules that are easy to break. Do not mix APKs from unrelated sources. -##### Log Levels +Important rules: -- `Off` - Log nothing. -- `Normal` - Start logging error, warn and info messages and stacktraces. -- `Debug` - Start logging debug messages. -- `Verbose` - Start logging verbose messages. -## +- F-Droid Termux and F-Droid plugins must stay together. +- GitHub/debug builds must stay with matching GitHub/debug plugin builds. +- Different signing keys can trigger `INSTALL_FAILED_UPDATE_INCOMPATIBLE` or `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`. +- Side-by-side debug builds need deliberate package names and wrappers that target the matching package. +- Do not install a debug build over a working phone stack unless the whole stack is intentionally being replaced. +If runtime behavior makes no sense, verify the installed packages first: +```powershell +adb shell pm list packages | findstr termux +adb shell dumpsys package com.termux | findstr /i "versionName userId signatures" +adb shell dumpsys package com.termux.api | findstr /i "versionName userId signatures" +``` -## For Maintainers and Contributors +## Workflow maintenance -The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured. +The workflows should prove source health, not hide source problems. Keep workflow edits small and boring. -The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info. +Do not casually change: -Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins. +- Gradle wrapper version; +- Android Gradle Plugin version; +- compile SDK / target SDK; +- signing configuration; +- manifest package identity; +- shared UID behavior; +- artifact names used by install/test scripts. -The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on GitHub, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec. +If a workflow breaks after a source fix, inspect the first failing command and compare with the last green commit before changing build infrastructure. -### Commit Messages Guidelines +## Issue-fix workflow -Commit messages **must** use the [Conventional Commits](https://www.conventionalcommits.org) spec so that chagelogs as per the [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) spec can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. **The first letter for `type` and `description` must be capital and description should be in the present tense.** The space after the colon `:` is necessary. For a breaking change, add an exclamation mark `!` before the colon `:`, so that it is highlighted in the chagelog automatically. +For each issue or bug: -``` -[optional scope]: +1. Start from the clean workbench branch. +2. Understand the runtime path before editing. +3. Make the smallest source change that can fix the behavior. +4. Run a targeted local check if it is cheap. +5. Commit with a clear message. +6. Push to the workbench branch. +7. Use GitHub Actions as the gate. +8. Batch PR work later from clean slices. -[optional body] +Preferred commit style: -[optional footer(s)] +```text +fix: guard session exit null command +fix: preserve ctrl modifier for page navigation +docs: refresh fork maintenance README ``` -**Only the `types` listed below must be used exactly as they are used in the changelog headings.** For example, `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Fix some bug`. **Do not use anything else as type, like `add` instead of `Added`, etc.** +## Android areas that matter most -- **Added** for new features. -- **Changed** for changes in existing functionality. -- **Deprecated** for soon-to-be removed features. -- **Removed** for now removed features. -- **Fixed** for any bug fixes. -- **Security** in case of vulnerabilities. -## +For 2026 maintenance, the important study lanes are: +- Android app lifecycle: `Application`, `Activity`, `Service`, `BroadcastReceiver`, process death, and background limits. +- Android package identity: `applicationId`, debug suffixes, signatures, `sharedUserId`, and plugin compatibility. +- Terminal/session lifecycle: session creation, session exit, command execution, notification updates, and null-safety around process state. +- Storage and file intents: SAF, content URIs, permissions, and Android version differences. +- GitHub Actions: build/test gates, artifacts, signing paths, and avoiding workflow churn. +## Relationship to upstream -## Forking +Keep this fork upstream-friendly. Workbench commits can be practical and messy while a problem is being understood, but upstream pull requests should be small, focused, and easy to review. -- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name. -- You also need to recompile bootstrap zip for the new package name. Check [building bootstrap](https://github.com/termux/termux-packages/wiki/For-maintainers#build-bootstrap-archives), [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111). -- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched. -- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins. -## +Before opening upstream PRs: +- split unrelated fixes; +- remove fork-only notes from code changes; +- verify formatting and tests; +- explain the user-visible bug; +- explain why the fix is narrow; +- include runtime proof when the issue depends on Android behavior. +## Useful links -## Sponsors and Funders - -[GitHub Accelerator](https://github.com) -*[GitHub Accelerator](https://github.com/accelerator) ([1](https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next))* - -  - -[GitHub Secure Open Source Fund](https://github.com) -*[GitHub Secure Open Source Fund](https://resources.github.com/github-secure-open-source-fund) ([1](https://github.blog/open-source/maintainers/securing-the-supply-chain-at-scale-starting-with-71-important-open-source-projects), [2](https://termux.dev/en/posts/general/2025/08/11/termux-selected-for-github-secure-open-source-fund-session-2.html))* - -  - -[NLnet NGI Mobifree](https://nlnet.nl/mobifree) -*[NLnet NGI Mobifree](https://nlnet.nl/mobifree) ([1](https://nlnet.nl/news/2024/20241111-NGI-Mobifree-grants.html), [2](https://termux.dev/en/posts/general/2024/11/11/termux-selected-for-nlnet-ngi-mobifree-grant.html))* - -  - -[Cloudflare](https://www.cloudflare.com) -*[Cloudflare](https://www.cloudflare.com) ([1](https://packages-cf.termux.dev))* +- [Official Termux site](https://termux.dev) +- [Official Termux app repository](https://github.com/termux/termux-app) +- [Termux packages](https://github.com/termux/termux-packages) +- [Termux Wiki](https://wiki.termux.dev/wiki/Main_Page) +- [Termux:API](https://github.com/termux/termux-api) +- [Android logcat documentation](https://developer.android.com/studio/command-line/logcat) -  +## Maintainer note -[Warp](https://www.warp.dev/?utm_source=github&utm_medium=readme&utm_campaign=termux) -[*Warp, built for coding with multiple AI agents*](https://www.warp.dev/?utm_source=github&utm_medium=readme&utm_campaign=termux) +The goal is not only to patch Termux. The goal is to build enough operational experience to maintain our own Android/Termux-style tools with discipline: clean branches, boring builds, clear runtime tests, and no mystery changes hiding in the machinery. diff --git a/TERMUX_APP_FIRST_BATCH_PLAN.md b/TERMUX_APP_FIRST_BATCH_PLAN.md new file mode 100644 index 0000000000..bb0f7e955b --- /dev/null +++ b/TERMUX_APP_FIRST_BATCH_PLAN.md @@ -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. diff --git a/TERMUX_APP_ISSUE_LEDGER.md b/TERMUX_APP_ISSUE_LEDGER.md new file mode 100644 index 0000000000..f57c436f8d --- /dev/null +++ b/TERMUX_APP_ISSUE_LEDGER.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 33c88d1d4a..2f6f018015 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d0dfe1948..b878256147 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> @@ -57,6 +57,7 @@ android:launchMode="singleTask" android:resizeableActivity="true" android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar" + android:excludeFromRecents="true" tools:targetApi="n"> 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 @@ -