Compare commits

...

40 Commits

Author SHA1 Message Date
agnostic-apollo e344951534
Release: v0.119.0-beta.2 2025-03-29 09:27:32 +05:00
agnostic-apollo 4165c85f0d
Changed: Bump `apt-android-7` bootstraps to `2025.03.28-r1` and `apt-android-5` bootstraps to `2025.03.28-r3` 2025-03-29 09:27:32 +05:00
agnostic-apollo 46cd514343
Fixed: Set `minSdkVersion` and `maxSdkVersion` depending on apk bootstrap variant and abort bootstrap installation if trying to install wrong variant
- `apt-android-7` variant will set `minSdkVersion=24` (Android `7.0`) and bootstrap installation will fail if installing on Android `< 7`. Normally, APK installation will fail with `INSTALL_FAILED_OLDER_SDK` if trying to install on an old version, or if APK is manually installed in `/data/app`, it should fail to run and app will be disabled/removed on reboot.
- `apt-android-5` variant will set `minSdkVersion=21` (Android `5.0`) and `maxSdkVersion=23` (Android `6.0`) and bootstrap installation will fail if installing on Android `>= 7`. Android does not care for `maxSdkVersion` value and will still allow installation on higher Android versions as the OS provides backward compatibility for apps, but Termux packages do not provide that, so must not be installed on higher Android versions.

This should solve the issue for `0.119.0*` releases showing on F-Droid for Android `5`/`6` users as previously the `apt-android-7` variant was still using `minSdkVersion=21`. Android `5`/`6` releases with `apt-android-5` variant are only to be provided from GitHub.
2025-03-29 09:27:32 +05:00
agnostic-apollo 97f2537c31
Fixed: Do not assume failure if `stderr` is set for bootstrap second stage as a `postinst` script could be using `stderr` for logging
Exit code `0` should be enough to check for failure.
2025-03-29 09:27:32 +05:00
agnostic-apollo 7c0b251c7a
Changed: Log a message if `termux-bootstrap-second-stage.sh` or `bash` is not found to run bootstrap second stage 2025-03-29 03:24:38 +05:00
agnostic-apollo 1e686e55f3
Fixed: Generate debug report before deleting broken prefix directory after bootstrap second stage failure to get `stat` info at time of failure 2025-03-29 03:24:38 +05:00
agnostic-apollo 6eb533cb2e
Reverted: Rename `TERMUX__SE_FILE_CONTEXT` back to `TERMUX_APP__SE_FILE_CONTEXT` and `TERMUX__SE_FILE_CONTEXT` back to `TERMUX__SE_INFO` that was changed in 3f6ebd33 as they are for the main Termux app and not for current Termux process like a plugin 2025-03-29 03:24:37 +05:00
agnostic-apollo 1149d4291f
Added: Export additional primary `TERMUX_*` scoped shell environment variables
- TERMUX__APPS_DIR
- TERMUX__ROOTFS
- TERMUX__HOME
- TERMUX__PREFIX
- TERMUX_APP__LEGACY_DATA_DIR (Expected legacy app data directory path for the `com.termux` app, mainly accessible on primary user `0` and is a symlink/bind mount to/from `TERMUX_APP__DATA_DIR`)
- TERMUX_APP__BUILD_DATA_DIR (App data directory path used by `termux-packages` `properties.sh` while building packages and `TermuxConstants.java` while building Termux app)

`TERMUX_APP__DATA_DIR` variable already exported is for the actual app data directory path assigned by Android for the `com.termux` app returned by `ApplicationInfo`.

This is also required for `libtermux-exec-*-ld-preload.so` `$LD_PRELOAD` library as well for `termux-exec` version `>= 2.0.0`.

- https://github.com/termux/termux-exec-package/commit/db738a11

See also https://github.com/termux/termux-packages/blob/96eefda/scripts/properties.sh
2025-03-29 03:24:37 +05:00
agnostic-apollo 36be6abc89
Added: Export `ANDROID__BUILD_VERSION_SDK` in shell environment variable for the Android build SDK version
This is also required for `libtermux-exec-*-ld-preload.so` `$LD_PRELOAD` library as well for `termux-exec` version `>= 2.0.0`.

- https://github.com/termux/termux-exec-package/commit/db738a11
2025-03-28 11:27:19 +05:00
agnostic-apollo e3a8a1f873
Added: Add info to device info for supported ABIs for armv8l and 64-bit-only aarch64 devices and `PAGE_SIZE` for `16KB` page size devices
- https://source.android.com/docs/core/architecture/16kb-page-size/16kb
2025-03-28 11:19:51 +05:00
Johannes Altmanninger 6a588d3d24
Fixed: Fully consume unknown CSI sequences containing unsupported parameter and intermediate bytes
Standard ECMA-48: Control Functions for Coded Character Sets specifies the format of CSI commands.
- https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
- https://invisible-island.net/xterm/ecma-48-parameter-format.html#section5.4

Previously unsupported bytes would be echoed to the terminal.

```shell
$ printf '\x1b[=u' # PF
u
$ printf '\x1b[=5u' # PPF
5u
$ printf '\x1b[=5!u' # PPIF
5!u
$ printf '\x1b[=5!%u' # PPIIF
5!0
$ printf '\x1b[=?5!%u' # PPPIIF
?5!0
```

This fixes a problem with fish shell 4.0.0 which uses that sequence.

Closes #4338

Co-authored-by: @krobelus <aclopte@gmail.com>
Co-authored-by: @agnostic-apollo  <agnosticapollo@gmail.com>
2025-03-28 09:28:59 +05:00
agnostic-apollo d6a0ff7fc6
Changed|Fixed: Bump `org.lsposed.hiddenapibypass:hiddenapibypass` to `5.0` to fix crash on Android 16
```
Build fingerprint: 'google/sdk_gphone64_x86_64/emu64xa:Baklava/BP22.250103.008/12932282:userdebug/dev-keys'
Revision: '0'
ABI: 'x86_64'
Timestamp: 2025-01-25
Process uptime: 1s
Cmdline: com.termux
pid: 4700, tid: 4700, name: com.termux  >>> com.termux <<<
uid: 10212
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x000000000000000c
Cause: null pointer dereference
    rax 0000000000000000  rbx 000071ad67c247b0  rcx 000000000000000c  rdx 000071abc520c888
    r8  00005f2000006018  r9  0000607c00006004  r10 000071abc423d68c  r11 000071abc4cca7c0
    r12 00007ffc65da8240  r13 000071ad67c24858  r14 00007ffc65da85d8  r15 000071ad67c247b0
    rdi 000071ad87c26110  rsi 00007ffc65da8148
    rbp 00007ffc65da8050  rsp 00007ffc65da8040  rip 000071abc4cca817
124 total frames
backtrace:
      #00 pc 00000000008ca817  /apex/com.android.art/lib64/libart.so (art::Unsafe_getObject(_JNIEnv*, _jobject*, _jobject*, long) (.__uniq.306581074569039686346581217366878976736)+87) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #01 pc 000000000022c80b  /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+219) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #02 pc 0000000000211dd4  /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+756) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #03 pc 0000000000556155  /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+181) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #04 pc 00000000006dd182  /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)+2434) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #05 pc 0000000000233564  /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)+10804) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #06 pc 000000000022eb25  /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+5) (BuildId: 99c067c739342eb9769974bbb229d3b3)
      #07 pc 0000000000080eb4  <anonymous:71ab3d335000> (org.lsposed.hiddenapibypass.HiddenApiBypass.getDeclaredMethods+0)
      ...
      #13 pc 0000000000080dd0  <anonymous:71ab3d335000> (org.lsposed.hiddenapibypass.HiddenApiBypass.setHiddenApiExemptions+0)
      ...
      #19 pc 0000000000080cf8  <anonymous:71ab3d335000> (org.lsposed.hiddenapibypass.HiddenApiBypass.addHiddenApiExemptions+0)
      ...
      #25 pc 0000000000005ab4  <anonymous:71ae82992000> (com.termux.shared.reflection.ReflectionUtils.bypassHiddenAPIReflectionRestrictions+0)
      ...
      #31 pc 0000000000004738  <anonymous:71ae86607000> (com.termux.shared.android.SELinuxUtils.getContext+0)
      ...
      #37 pc 0000000000005b48  <anonymous:71ae825d8000> (com.termux.shared.termux.shell.command.environment.TermuxAppShellEnvironment.setTermuxAppEnvironment+0)
      ...
      #43 pc 000000000000603c  <anonymous:71ae825d8000> (com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment.init+0)
      ...
      #49 pc 0000000000006a68  <anonymous:71ae7e1c2000> (com.termux.app.TermuxApplication.onCreate+0)
```

Related commit 40b4cafa47
Related issue https://github.com/LSPosed/AndroidHiddenApiBypass/issues/52

Closes #4368
2025-03-28 09:28:59 +05:00
agnostic-apollo b7902d4b33
Fixed: Fix gradle build error if using jdk `17`
`Unable to make field private final java.lang.String java.io.File.path accessible: module java.base does not "opens java.io" to unnamed module`
2025-03-28 09:28:59 +05:00
agnostic-apollo a3ed94f310
Fixed: Use `TERMUX_STYLING_APP` for `TERMUX_STYLING_ACTIVITY_NAME` as per 078eea2b 2025-03-28 09:28:59 +05:00
agnostic-apollo aa84ce43b8
Added: Rename app classes in `TermuxConstants` with `_APP` suffix added and add `TERMUX_*_MAIN_ACTIVITY_NAME` and `TERMUX_*_LAUNCHER_ACTIVITY_NAME` constants to each app class 2025-03-28 09:28:58 +05:00
agnostic-apollo e6fc257632
Fixed: Fix tests added in b84dc703 2025-03-28 09:28:58 +05:00
Jason Yu 79c81491d8
Added: Add empty and null strings tests for invalid urls to `FileReceiverActivityTest` 2025-03-28 09:28:58 +05:00
Fredrik Fornwall 15069cea34
Fixed: Implement colon separated CSI parameters 2025-03-28 09:28:58 +05:00
Evgeny Zhabotinsky 6f46efcf21
Fixed: Make ScrollDown escape respect margins
SD sequence (`${CSI}${N}T`) was scrolling the whole width
 of the terminal instead of just between the margins.
RI sequence (`${ESC}M`, move cursor up 1 line) was doing the same.
Fixed that.

Fixes #2576 where in tmux scrolling one of several
 side-by-side panels down resulted in all visually scrolling.
2025-03-28 09:28:58 +05:00
Tom Kranz 3042bb5f75
Added: Basic MIME type recognition in ContentProvider 2025-03-28 09:28:58 +05:00
Matan Ziv-Av 312567a374
Fixed: Use Canvas.drawTextRun instead of drawText
drawText does (very) basic BiDi, which causes inconsistent behaviour.
This ensures everything is LtR.
2025-03-28 09:28:58 +05:00
Fredrik Fornwall 6337f1b056
Added: Terminal CSI reporting of window and cell pixel size
Implement the following CSI escape sequences from
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html:

> CSI Ps ; Ps ; Ps t
> [..]
>    Ps = 1 4  ⇒  Report xterm text area size in pixels.
>    Result is CSI  4 ;  height ;  width t
> [..]
>    Ps = 1 6  ⇒  Report xterm character cell size in pixels.
>    Result is CSI  6 ;  height ;  width t

Extracted from changes in https://github.com/termux/termux-app/pull/2973
by @MatanZ and adopted to play well with the just merged #3098 (.ws_xpixel
and .ws_ypixel values in winsize).
2025-03-28 09:28:57 +05:00
Artem Chepurnyi 37771df8ba
Fixed: Mark view as important for AutoFill before requesting an AutoFill
Co-authored-by: @AChep <mail@artemchep.com>
Co-authored-by: @agnostic-apollo  <agnosticapollo@gmail.com>
2025-03-28 09:28:57 +05:00
Dvd-Znf fdef2783e4
Update latest version in README.md to v0.118.1 2025-03-28 09:28:57 +05:00
Krunal Patel e11ce2a833
Added|Changed: Fill `.ws_xpixel` and `.ws_ypixel` in `winsize`
This allows to get terminal size in pixel using `TIOCGWINSZ` ioctl.
Set `.ws_xpixel` using `columns * cell_width` and set `.ws_ypixel` using `rows * cell_height`.
Cell width and height is font width and line spacing, respectively.
2025-03-28 09:28:57 +05:00
Fredrik Fornwall a4437083a0
Fixed: Parse (but ignore for now) terminal APC sequences 2025-03-28 09:28:57 +05:00
agnostic-apollo 31e0052928
Added|Fixed: Do not show AutoFill UI on Termux start and add support for usernames
- The AutoFill type and hints are no longer hardcoded in `TerminalView` class and `TermuxActivity` layout xml. They are dynamically set to required values before making a manual AutoFill request and reverted back afterwards to default values. The hardcoded value `AUTOFILL_TYPE_TEXT` returned by `getAutofillType()` was causing the AutoFill UI to show on Activity starts, this will return `AUTOFILL_TYPE_NONE` by default now so that AutoFill UI isn't shown automatically.
- The AutoFill importance is no longer hardcoded in `TermuxActivity` layout xml and is returned by `TerminalView` class itself by `getImportantForAutofill()`.
- The AutoFill function in `TermuxActivity` for making a manual AutoFill request is moved to `TerminalView` class. This and moving of hardcoded values to `TerminalView` class mentioned above is done as complete logic of AutoFill should be handled by `TerminalView` class itself and not scattered in various places.
- The Terminal context menu now supports AutoFilling a username. Note that GBoard/Google Password Manager seems to have a bug where it will still show `Pick a saved password` instead of username, even though `AUTOFILL_HINT_USERNAME` is being requested, however it will still AutoFill a username of selected entry correctly.
- Pressing the back button to close the keyboard will also cancel the current manually requested AutoFill request and UI will not show when keyboard is opened again.

Closes #3909
2025-03-28 09:28:57 +05:00
Josh Triplett f8e9812eb2
Make Shift-PgUp and Shift-PgDn scroll by pages rather than lines
In other terminals, such as gnome-terminal, Shift-PgUp and Shift-PgDn
scroll the screen by a full page, rather than a single line. Adjust
termux to match.
2025-03-28 09:28:57 +05:00
Fredrik Fornwall f8ee16dff5
Fixed: Improve handling of empty ';' SGR sequences
Currently the Termux terminal emulator prints "HI" in red with:

```sh
printf "\e[31;m HI \e[0m"
```

This is not how other terminals (tested on xterm, gnome-terminal,
alacritty and the mac built in terminal) handle it, since they parse
""\e[31;m" as "\e[31;0m", where the "0" resets the colors.

This change aligns with other terminals, as well as improves performance
by avoiding allocating a new int[] array for each byte processed by
`parseArg()`, and most importantly simplifies things by removing the
`mIsCSIStart` and `mLastCSIArg` state, preparing for supporting ':'
separated sub parameters such as used in
https://sw.kovidgoyal.net/kitty/underlines/
2025-03-28 09:28:57 +05:00
Fredrik Fornwall 4bce0c0438
Fixed: Use current bg color when scrolling with horizontal margins
Fixes https://github.com/termux/termux-packages/issues/12556

Issue was also reported here:
https://www.reddit.com/r/termux/comments/1df1dii/how_can_i_fix_this_annoying_screenfilling_thing/
2025-03-28 09:28:56 +05:00
agnostic-apollo 7c5992d379
Release: v0.119.0-beta.1
The `versionCode` has been bumped to `1020` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.120` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `118` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

The `v0.118.1` was released under `versionCode` `1000`, we bump `versionCode` to `1020` so that there are `20` version codes in between that can be used as patch releases for `0.118.x` in case needed, like for `v0.118.2`.

- https://github.com/termux/termux-app/discussions/4000
- https://github.com/termux/termux-app/issues/4012
2024-06-18 04:12:29 +05:00
agnostic-apollo 2cfbfcd79f
Changed: Replace `/` extra key with `DRAWER` key with `PASTE` popup and `-` key with `SCROLL` 2024-06-18 02:11:03 +05:00
agnostic-apollo afe22941ce
Added: Add support for Termux bootstrap second stage by running `termux-bootstrap-second-stage.sh`
- 7827140577
- 7827140577/scripts/bootstrap/termux-bootstrap-second-stage.sh
2024-06-18 02:11:03 +05:00
agnostic-apollo e85d078f04
Changed: Bump `apt-android-7` bootstraps to `2024.06.17-r1` 2024-06-18 02:11:03 +05:00
agnostic-apollo 8cdeb55271
Changed|Fixed: Always request `MANAGE_EXTERNAL_STORAGE if on Android `>= 11` when running `termux-setup-storage`
Requesting `MANAGE_EXTERNAL_STORAGE` should additionally grant access to unreliable/removable volumes like USB OTG devices under the `/mnt/media_rw/XXXX-XXXX` paths on `Android >= 12`, so request that if possible. Check https://github.com/termux/termux-app/issues/71#issuecomment-1869222653 for more info.

Fixes issue on Android `14`, where using `targetSdkVersion=28`, that requests the legacy `WRITE_EXTERNAL_STORAGE` will actually request the `photos, music, video, and other files` permissions (`READ_MEDIA_AUDIO`/`READ_MEDIA_IMAGES`/`READ_MEDIA_VIDEO`) and apparently access to full external storage `/sdcard` is not available for some users, maybe because `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions are not granted for those device automatically in addition to `READ_MEDIA_*` permission. The issue is not reproducible on Android `13-15` avd. To solve this, we request the singular `MANAGE_EXTERNAL_STORAGE` permission instead so that full access is always available.

Related: https://github.com/termux/termux-app/issues/3647#issuecomment-2137266012

See also:
- https://developer.android.com/training/data-storage/shared/media#access-other-apps-files
- https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_IMAGES
2024-06-18 02:11:03 +05:00
agnostic-apollo 3ae0d601db
Patched: Disable export of `TERMUX_API_APP__*` environment variables as app variables will be written to `termux-apps-info.env` file for the stable `v0.119.0` release
Removes `TERMUX_API_APP__APP_VERSION_NAME`.
2024-06-18 02:11:03 +05:00
agnostic-apollo 3f6ebd33cd
Patched: Rename `TERMUX_APP__*` and `SHELL_CMD__*` environment variables as variable names will be changed for the stable `v0.119.0` release
- `TERMUX_APP__VERSION_NAME` to `TERMUX_APP__APP_VERSION_NAME`
- `TERMUX_APP__VERSION_CODE` to `TERMUX_APP__APP_VERSION_CODE`
- `TERMUX_APP__UID` to `TERMUX__UID`
- `TERMUX_APP__APK_PATH` to `TERMUX_APP__APK_FILE`
- `TERMUX_APP__SE_PROCESS_CONTEXT` to `TERMUX__SE_PROCESS_CONTEXT`
- `TERMUX_APP__SE_FILE_CONTEXT` to `TERMUX__SE_FILE_CONTEXT`
- `TERMUX_APP__SE_INFO` to `TERMUX__SE_INFO`
- `TERMUX_APP__USER_ID` to `TERMUX__USER_ID`
- `TERMUX_APP__PROFILE_OWNER` to `TERMUX__PROFILE_OWNER`
- `TERMUX_APP__FILES_DIR` to `TERMUX_APP__DATA_DIR`
- `SHELL_CMD__TERMINAL_SESSION_NUMBER_SINCE_BOOT` to `SHELL_CMD__APP_TERMINAL_SESSION_NUMBER_SINCE_BOOT`
- `SHELL_CMD__TERMINAL_SESSION_NUMBER_SINCE_APP_START` to `SHELL_CMD__APP_TERMINAL_SESSION_NUMBER_SINCE_APP_START`
2024-06-18 02:11:03 +05:00
agnostic-apollo f12697a0f8
Patched: Disable export `$TERMUX_APP__PACKAGE_MANAGER` and `$TERMUX_APP__PACKAGE_VARIANT` as variable names and values will be changed for the stable `v0.119.0` release 2024-06-18 02:11:02 +05:00
agnostic-apollo b466e9c88d
Patched: Disable creation of `TERMUX_APPS_DIR` and `TermuxAmSocketServer` as paths will be changed for the stable `v0.119.0` release 2024-06-18 02:11:02 +05:00
agnostic-apollo bf33a54fe9
Changed: Use GitHub `cli` instead of `hub` for uploading GitHub release files as later has been removed from runner images
- https://github.com/actions/runner-images/issues/8362
2024-06-18 02:11:02 +05:00
35 changed files with 874 additions and 219 deletions

View File

@ -71,14 +71,13 @@ jobs:
fi
echo "Attaching APKs to github release"
if ! hub release edit \
-m "" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
"$RELEASE_VERSION_NAME"; then
if ! gh release upload "$RELEASE_VERSION_NAME" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
; then
exit_on_error "Attach APKs to release failed for '$APK_VERSION_TAG' release."
fi

View File

@ -49,7 +49,7 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
## Installation
Latest version is `v0.118.0`.
Latest version is `v0.118.1`.
**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.**

View File

@ -10,6 +10,13 @@ ext {
// by replacing $PREFIX since app code is dependant on the variant used to build the APK.
// Currently supported values are: [ "apt-android-7" "apt-android-5" ]
packageVariant = System.getenv("TERMUX_PACKAGE_VARIANT") ?: "apt-android-7" // Default: "apt-android-7"
bootstrapMinSdk = packageVariant == "apt-android-5" ? 21 : 24
bootstrapMinRelease = packageVariant == "apt-android-5" ? "5.0" : "7.0"
bootstrapMaxSdk = packageVariant == "apt-android-5" ? 23 : null
bootstrapMaxRelease = packageVariant == "apt-android-5" ? "6.0" : null
buildMinSdk = bootstrapMinSdk
buildTargetSdk = project.properties.targetSdkVersion.toInteger()
}
android {
@ -39,14 +46,20 @@ android {
defaultConfig {
applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 118
versionName "0.118.0"
minSdk buildMinSdk
targetSdk buildTargetSdk
versionCode 1021
versionName "0.119.0-beta.2"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
buildConfigField "Integer", "TERMUX_APP__BOOTSTRAP_MIN_SDK", project.ext.bootstrapMinSdk.toString()
buildConfigField "String", "TERMUX_APP__BOOTSTRAP_MIN_RELEASE",
project.ext.bootstrapMinRelease ? "\"" + project.ext.bootstrapMinRelease + "\"" : "null"
buildConfigField "Integer", "TERMUX_APP__BOOTSTRAP_MAX_SDK", project.ext.bootstrapMaxSdk.toString()
buildConfigField "String", "TERMUX_APP__BOOTSTRAP_MAX_RELEASE",
project.ext.bootstrapMaxRelease ? "\"" + project.ext.bootstrapMaxRelease + "\"" : "null"
buildConfigField "String", "TERMUX_PACKAGE_VARIANT", "\"" + project.ext.packageVariant + "\"" // Used by TermuxApplication class
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
@ -214,17 +227,17 @@ task downloadBootstraps() {
doLast {
def packageVariant = project.ext.packageVariant
if (packageVariant == "apt-android-7") {
def version = "2022.04.28-r5" + "+" + packageVariant
downloadBootstrap("aarch64", "4a51a7eb209fe82efc24d52e3cccc13165f27377290687cb82038cbd8e948430", version)
downloadBootstrap("arm", "6459a786acbae50d4c8a36fa1c3de6a4dd2d482572f6d54f73274709bd627325", version)
downloadBootstrap("i686", "919d212b2f19e08600938db4079e794e947365022dbfd50ac342c50fcedcd7be", version)
downloadBootstrap("x86_64", "61b02fdc03ea4f5d9da8d8cf018013fdc6659e6da6cbf44e9b24d1c623580b89", version)
def version = "2025.03.28-r1" + "+" + packageVariant
downloadBootstrap("aarch64", "c8d702b6f742935001c37cda81b8ac69504a95d5cf28f2899532dd8cd4b057eb", version)
downloadBootstrap("arm", "f3bb9d1b32552b34fff41861dbf193ec5ba2848d67d779ac1c7256da6640f85d", version)
downloadBootstrap("i686", "36db3e1ac3547f9a174fd763bd9a484fa1a3449cdd81e1cf2408ff0454f839c6", version)
downloadBootstrap("x86_64", "1c124ec2396ee70a51b0b0a574f29aa659526aa2b9f558f993b2fb05d1e51855", version)
} else if (packageVariant == "apt-android-5") {
def version = "2022.04.28-r6" + "+" + packageVariant
downloadBootstrap("aarch64", "913609d439415c828c5640be1b0561467e539cb1c7080662decaaca2fb4820e7", version)
downloadBootstrap("arm", "26bfb45304c946170db69108e5eb6e3641aad751406ce106c80df80cad2eccf8", version)
downloadBootstrap("i686", "46dcfeb5eef67ba765498db9fe4c50dc4690805139aa0dd141a9d8ee0693cd27", version)
downloadBootstrap("x86_64", "615b590679ee6cd885b7fd2ff9473c845e920f9b422f790bb158c63fe42b8481", version)
def version = "2025.03.28-r3" + "+" + packageVariant
downloadBootstrap("aarch64", "147c98e610a30588665a89776314833c293006b12c70e65dcde6eb54c2344113", version)
downloadBootstrap("arm", "363c28dd4b70c995302498beae79fb5917af7d3b0ca9fbd9da7de96ab64c6122", version)
downloadBootstrap("i686", "a2e742381ab24cf8c9a78ae4e2425ee44d8fb9625a2b0ef63e4cd32e292f7186", version)
downloadBootstrap("x86_64", "4f6866c222b0f1ae1b180220ffc6e8e1afbc10b9cbb7a462c062c490eda90044", version)
} else {
throw new GradleException("Unsupported TERMUX_PACKAGE_VARIANT \"" + packageVariant + "\"")
}

View File

@ -10,7 +10,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@ -21,7 +20,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
@ -181,7 +179,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10;
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
private static final int CONTEXT_MENU_AUTOFILL_USERNAME = 11;
private static final int CONTEXT_MENU_AUTOFILL_PASSWORD = 2;
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
private static final int CONTEXT_MENU_STYLING_ID = 5;
@ -632,20 +631,16 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
TerminalSession currentSession = getCurrentSession();
if (currentSession == null) return;
boolean addAutoFillMenu = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
addAutoFillMenu = true;
}
}
boolean autoFillEnabled = mTerminalView.isAutoFillEnabled();
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText()))
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text);
if (addAutoFillMenu)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
if (autoFillEnabled)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_USERNAME, Menu.NONE, R.string.action_autofill_username);
if (autoFillEnabled)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_PASSWORD, Menu.NONE, R.string.action_autofill_password);
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
@ -676,8 +671,11 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
mTermuxTerminalViewClient.shareSelectedText();
return true;
case CONTEXT_MENU_AUTOFILL_ID:
requestAutoFill();
case CONTEXT_MENU_AUTOFILL_USERNAME:
mTerminalView.requestAutoFillUsername();
return true;
case CONTEXT_MENU_AUTOFILL_PASSWORD:
mTerminalView.requestAutoFillPassword();
return true;
case CONTEXT_MENU_RESET_TERMINAL_ID:
onResetTerminalSession(session);
@ -738,7 +736,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
private void showStylingDialog() {
Intent stylingIntent = new Intent();
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING.TERMUX_STYLING_ACTIVITY_NAME);
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING_APP.TERMUX_STYLING_ACTIVITY_NAME);
try {
startActivity(stylingIntent);
} catch (ActivityNotFoundException | IllegalArgumentException e) {
@ -760,15 +758,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
}
}
private void requestAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
autofillManager.requestAutofill(mTerminalView);
}
}
}
/**
@ -785,7 +774,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
// If permission is granted, then also setup storage symlinks.
if(PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission(
TermuxActivity.this, requestCode, !isPermissionCallback)) {
TermuxActivity.this, requestCode, true, !isPermissionCallback)) {
if (isPermissionCallback)
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
getString(com.termux.shared.R.string.msg_storage_permission_granted_on_request));

View File

@ -52,7 +52,7 @@ public class TermuxApplication extends Application {
boolean isTermuxFilesDirectoryAccessible = error == null;
if (isTermuxFilesDirectoryAccessible) {
Logger.logInfo(LOG_TAG, "Termux files directory is accessible");
/*
error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error);
@ -61,6 +61,7 @@ public class TermuxApplication extends Application {
// Setup termux-am-socket server
TermuxAmSocketServer.setupTermuxAmSocketServer(context);
*/
} else {
Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error);
}

View File

@ -10,8 +10,12 @@ import android.system.Os;
import android.util.Pair;
import android.view.WindowManager;
import com.termux.BuildConfig;
import com.termux.R;
import com.termux.shared.file.FileUtils;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.runner.app.AppShell;
import com.termux.shared.termux.TermuxBootstrap;
import com.termux.shared.termux.crash.TermuxCrashUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils;
@ -102,6 +106,12 @@ final class TermuxInstaller {
return;
}
if (!checkIfMinOrMaxSdkVersionIsIncompatible(activity,
BuildConfig.TERMUX_APP__BOOTSTRAP_MIN_SDK, BuildConfig.TERMUX_APP__BOOTSTRAP_MIN_RELEASE,
BuildConfig.TERMUX_APP__BOOTSTRAP_MAX_SDK, BuildConfig.TERMUX_APP__BOOTSTRAP_MAX_RELEASE)) {
return;
}
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
@ -195,7 +205,8 @@ final class TermuxInstaller {
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods") ||
zipEntryName.equals("etc/termux/bootstrap/termux-bootstrap-second-stage.sh")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
@ -216,6 +227,35 @@ final class TermuxInstaller {
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
}
// Run Termux bootstrap second stage.
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
if (!FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since script not found at \"" + termuxBootstrapSecondStageFile + "\" path.");
} else {
if (!FileUtils.fileExists(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", true)) {
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since bash not found.");
}
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
ExecutionCommand executionCommand = new ExecutionCommand(-1,
termuxBootstrapSecondStageFile, null, null,
null, ExecutionCommand.Runner.APP_SHELL.getName(), false);
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
AppShell appShell = AppShell.execute(activity, executionCommand, null, new TermuxShellEnvironment(), null, true);
if (appShell == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
// Generate debug report before deleting broken prefix directory to get `stat` info at time of failure.
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into.
Logger.logInfo(LOG_TAG, "Deleting broken termux prefix.");
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null)
Logger.logErrorExtended(LOG_TAG, error.toString());
return;
}
}
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
// Recreate env file since termux prefix was wiped earlier
@ -239,6 +279,42 @@ final class TermuxInstaller {
}.start();
}
public static boolean checkIfMinOrMaxSdkVersionIsIncompatible(Activity activity,
Integer minSdk, String minRelease,
Integer maxSdk, String maxRelease) {
if (minSdk != null && Build.VERSION.SDK_INT < minSdk) {
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_apk_bootstrap_variant_min_sdk_incompatible,
MarkdownUtils.getMarkdownCodeForString(TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT.getName(), false),
MarkdownUtils.getMarkdownCodeForString(Build.VERSION.RELEASE, false),
Build.VERSION.SDK_INT,
MarkdownUtils.getMarkdownCodeForString(minRelease, false),
minSdk);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
return false;
}
if (maxSdk != null && Build.VERSION.SDK_INT > maxSdk) {
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_apk_bootstrap_variant_max_sdk_incompatible,
MarkdownUtils.getMarkdownCodeForString(TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT.getName(), false),
MarkdownUtils.getMarkdownCodeForString(Build.VERSION.RELEASE, false),
Build.VERSION.SDK_INT,
MarkdownUtils.getMarkdownCodeForString(maxRelease, false),
maxSdk);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
return false;
}
return true;
}
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);

View File

@ -172,6 +172,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
@Override
public String getType(@NonNull Uri uri) {
String path = uri.getLastPathSegment();
int extIndex = path.lastIndexOf('.') + 1;
if (extIndex > 0) {
MimeTypeMap mimeMap = MimeTypeMap.getSingleton();
String ext = path.substring(extIndex).toLowerCase();
return mimeMap.getMimeTypeFromExtension(ext);
}
return null;
}

View File

@ -55,6 +55,8 @@ public class FileReceiverActivity extends AppCompatActivity {
private static final String LOG_TAG = "FileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
if (sharedText == null || sharedText.isEmpty()) return false;
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}

View File

@ -31,8 +31,6 @@
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password"
tools:ignore="UnusedAttribute" />
<LinearLayout

View File

@ -27,10 +27,18 @@
<!-- Termux Bootstrap Packages Installation -->
<string name="bootstrap_installer_body">Installing bootstrap packages…</string>
<string name="bootstrap_error_title">Unable to install bootstrap</string>
<string name="bootstrap_error_title">&TERMUX_APP_NAME; Bootstrap Error</string>
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_apk_bootstrap_variant_min_sdk_incompatible">The APK bootstrap variant %1$s
of currently installed &TERMUX_APP_NAME; app is not compatible with the Android version %2$s
(sdk `%3$d`) of the device and it requires minimum Android version %4$s (sdk `%5$d`).
\n\nUninstall the &TERMUX_APP_NAME; app and reinstall the correct APK build variant.</string>
<string name="bootstrap_error_apk_bootstrap_variant_max_sdk_incompatible">The APK bootstrap variant %1$s
of currently installed &TERMUX_APP_NAME; app is not compatible with the Android version %2$s
(sdk `%3$d`) of the device and it requires maximum Android version %4$s (sdk `%5$d`).
\n\nUninstall the &TERMUX_APP_NAME; app and reinstall the correct APK build variant.</string>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
under any path other than:\n%1$s.</string>
@ -73,6 +81,7 @@
<string name="title_share_selected_text">Terminal Text</string>
<string name="title_share_selected_text_with">Send selected text to:</string>
<string name="action_autofill_username">Autofill username</string>
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>

View File

@ -26,6 +26,8 @@ public class FileReceiverActivityTest {
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
invalidUrls.add("");
invalidUrls.add(null);
for (String url : invalidUrls) {
Assert.assertFalse(FileReceiverActivity.isSharedTextAnUrl(url));
}

View File

@ -12,7 +12,12 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048M
org.gradle.jvmargs=-Xmx2048M \
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens=java.base/java.io=ALL-UNNAMED \
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
android.useAndroidX=true
minSdkVersion=21

View File

@ -23,10 +23,10 @@ final class JNI {
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns, int cellWidth, int cellHeight);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);
public static native void setPtyWindowSize(int fd, int rows, int cols, int cellWidth, int cellHeight);
/**
* Causes the calling thread to wait for the process associated with the receiver to finish executing.

View File

@ -79,9 +79,17 @@ public final class TerminalEmulator {
private static final int ESC_CSI_SINGLE_QUOTE = 18;
/** Escape processing: CSI ! */
private static final int ESC_CSI_EXCLAMATION = 19;
/** Escape processing: "ESC _" or Application Program Command (APC). */
private static final int ESC_APC = 20;
/** Escape processing: "ESC _" or Application Program Command (APC), followed by Escape. */
private static final int ESC_APC_ESCAPE = 21;
/** Escape processing: ESC [ <parameter bytes> */
private static final int ESC_CSI_UNSUPPORTED_PARAMETER_BYTE = 22;
/** Escape processing: ESC [ <parameter bytes> <intermediate bytes> */
private static final int ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE = 23;
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
private static final int MAX_ESCAPE_PARAMETERS = 16;
/** The number of parameter arguments including colon separated sub-parameters. */
private static final int MAX_ESCAPE_PARAMETERS = 32;
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
private static final int MAX_OSC_STRING_LENGTH = 8192;
@ -126,17 +134,15 @@ public final class TerminalEmulator {
private String mTitle;
private final Stack<String> mTitleStack = new Stack<>();
/** If processing first character of first parameter of {@link #ESC_CSI}. */
private boolean mIsCSIStart;
/** The last character processed of a parameter of {@link #ESC_CSI}. */
private Integer mLastCSIArg;
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
private int mCursorRow, mCursorCol;
/** The number of character rows and columns in the terminal screen. */
public int mRows, mColumns;
/** Size of a terminal cell in pixels. */
private int mCellWidthPixels, mCellHeightPixels;
/** The number of terminal transcript rows that can be scrolled back to. */
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
@ -176,6 +182,8 @@ public final class TerminalEmulator {
private int mArgIndex;
/** Holds the arguments of the current escape sequence. */
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
/** Holds the bit flags which arguments are sub parameters (after a colon) - bit N is set if <code>mArgs[N]</code> is a sub parameter. */
private int mArgsSubParamsBitSet = 0;
/** Holds OSC and device control arguments, which can be strings. */
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
@ -236,15 +244,17 @@ public final class TerminalEmulator {
private boolean mCursorBlinkState;
/**
* Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* Current foreground, background and underline colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* For a 24-bit value the top byte (0xff000000) is set.
*
* <p>Note that the underline color is currently parsed but not yet used during rendering.
*
* @see TextStyle
*/
int mForeColor, mBackColor;
int mForeColor, mBackColor, mUnderlineColor;
/** Current {@link TextStyle} effect. */
private int mEffect;
int mEffect;
/**
* The number of scrolled lines since last calling {@link #clearScrollCounter()}. Used for moving selection up along
@ -315,13 +325,15 @@ public final class TerminalEmulator {
}
}
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
public TerminalEmulator(TerminalOutput session, int columns, int rows, int cellWidthPixels, int cellHeightPixels, Integer transcriptRows, TerminalSessionClient client) {
mSession = session;
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
mAltBuffer = new TerminalBuffer(columns, rows, rows);
mClient = client;
mRows = rows;
mColumns = columns;
mCellWidthPixels = cellWidthPixels;
mCellHeightPixels = cellHeightPixels;
mTabStop = new boolean[mColumns];
reset();
}
@ -371,7 +383,10 @@ public final class TerminalEmulator {
}
}
public void resize(int columns, int rows) {
public void resize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
this.mCellWidthPixels = cellWidthPixels;
this.mCellHeightPixels = cellHeightPixels;
if (mRows == rows && mColumns == columns) {
return;
} else if (columns < 2 || rows < 2) {
@ -553,6 +568,15 @@ public final class TerminalEmulator {
}
public void processCodePoint(int b) {
// The Application Program-Control (APC) string might be arbitrary non-printable characters, so handle that early.
if (mEscapeState == ESC_APC) {
doApc(b);
return;
} else if (mEscapeState == ESC_APC_ESCAPE) {
doApcEscape(b);
return;
}
switch (b) {
case 0: // Null character (NUL, ^@). Do nothing.
break;
@ -638,6 +662,10 @@ public final class TerminalEmulator {
case ESC_CSI:
doCsi(b);
break;
case ESC_CSI_UNSUPPORTED_PARAMETER_BYTE:
case ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE:
doCsiUnsupportedParameterOrIntermediateByte(b);
break;
case ESC_CSI_EXCLAMATION:
if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR).
reset();
@ -1009,12 +1037,67 @@ public final class TerminalEmulator {
}
}
/**
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
*/
private void doApc(int b) {
if (b == 27) {
continueSequence(ESC_APC_ESCAPE);
}
// Eat APC sequences silently for now.
}
/**
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
*/
private void doApcEscape(int b) {
if (b == '\\') {
// A String Terminator (ST), ending the APC escape sequence.
finishSequence();
} else {
// The Escape character was not the start of a String Terminator (ST),
// but instead just data inside of the APC escape sequence.
continueSequence(ESC_APC);
}
}
private int nextTabStop(int numTabs) {
for (int i = mCursorCol + 1; i < mColumns; i++)
if (mTabStop[i] && --numTabs == 0) return Math.min(i, mRightMargin);
return mRightMargin - 1;
}
/**
* Process byte while in the {@link #ESC_CSI_UNSUPPORTED_PARAMETER_BYTE} or
* {@link #ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE} escape state.
*
* Parse unsupported parameter, intermediate and final bytes but ignore them.
*
* > For Control Sequence Introducer, ... the ESC [ is followed by
* > - any number (including none) of "parameter bytes" in the range 0x300x3F (ASCII 09:;<=>?),
* > - then by any number of "intermediate bytes" in the range 0x200x2F (ASCII space and !"#$%&'()*+,-./),
* > - then finally by a single "final byte" in the range 0x400x7E (ASCII @AZ[\]^_`az{|}~).
*
* - https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
* - https://invisible-island.net/xterm/ecma-48-parameter-format.html#section5.4
*/
private void doCsiUnsupportedParameterOrIntermediateByte(int b) {
if (mEscapeState == ESC_CSI_UNSUPPORTED_PARAMETER_BYTE && b >= 0x30 && b <= 0x3F) {
// Supported `09:;>?` or unsupported `<=` parameter byte after an
// initial unsupported parameter byte in `doCsi()`, or a sequential parameter byte.
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
} else if (b >= 0x20 && b <= 0x2F) {
// Optional intermediate byte `!"#$%&'()*+,-./` after parameter or intermediate byte.
continueSequence(ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE);
} else if (b >= 0x40 && b <= 0x7E) {
// Final byte `@AZ[\]^_`az{|}~` after parameter or intermediate byte.
// Calling `unknownSequence()` would log an error with only a final byte, so ignore it for now.
finishSequence();
} else {
unknownSequence(b);
}
}
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
private void doCsiQuestionMark(int b) {
switch (b) {
@ -1284,6 +1367,7 @@ public final class TerminalEmulator {
mEscapeState = ESC;
mArgIndex = 0;
Arrays.fill(mArgs, -1);
mArgsSubParamsBitSet = 0;
}
private void doLinefeed() {
@ -1378,8 +1462,8 @@ public final class TerminalEmulator {
// http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal
// position on the preceding line. If the active position is at the top margin, a scroll down is performed".
if (mCursorRow <= mTopMargin) {
mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1);
blockClear(0, mTopMargin, mColumns);
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, mBottomMargin - (mTopMargin + 1), mLeftMargin, mTopMargin + 1);
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin);
} else {
mCursorRow--;
}
@ -1393,8 +1477,6 @@ public final class TerminalEmulator {
break;
case '[':
continueSequence(ESC_CSI);
mIsCSIStart = true;
mLastCSIArg = null;
break;
case '=': // DECKPAM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
@ -1406,6 +1488,9 @@ public final class TerminalEmulator {
case '>': // DECKPNM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
break;
case '_': // APC - Application Program Command.
continueSequence(ESC_APC);
break;
default:
unknownSequence(b);
break;
@ -1587,8 +1672,8 @@ public final class TerminalEmulator {
final int linesToScrollArg = getArg0(1);
final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin;
final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg);
mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll);
blockClear(0, mTopMargin, mColumns, linesToScroll);
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesBetweenTopAndBottomMargins - linesToScroll, mLeftMargin, mTopMargin + linesToScroll);
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesToScroll);
} else {
// "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking.
unimplementedSequence(b);
@ -1610,12 +1695,16 @@ public final class TerminalEmulator {
}
mCursorCol = newCol;
break;
case '?': // Esc [ ? -- start of a private mode set
case '?': // Esc [ ? -- start of a private parameter byte
continueSequence(ESC_CSI_QUESTIONMARK);
break;
case '>': // "Esc [ >" --
case '>': // "Esc [ >" -- start of a private parameter byte
continueSequence(ESC_CSI_BIGGERTHAN);
break;
case '<': // "Esc [ <" -- start of a private parameter byte
case '=': // "Esc [ =" -- start of a private parameter byte
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
break;
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
setCursorColRespectingOriginMode(getArg0(1) - 1);
break;
@ -1715,8 +1804,10 @@ public final class TerminalEmulator {
mSession.write("\033[3;0;0t");
break;
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
// We just report characters time 12 here.
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * mCellHeightPixels, mColumns * mCellWidthPixels));
break;
case 16: // Report xterm character cell size in pixels. Result is CSI 6 ; height ; width t
mSession.write(String.format(Locale.US, "\033[6;%d;%dt", mCellHeightPixels, mCellWidthPixels));
break;
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
@ -1765,7 +1856,12 @@ public final class TerminalEmulator {
private void selectGraphicRendition() {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) {
int code = mArgs[i];
// Skip leading sub parameters:
if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
continue;
}
int code = getArg(i, 0, false);
if (code < 0) {
if (mArgIndex > 0) {
continue;
@ -1784,7 +1880,19 @@ public final class TerminalEmulator {
} else if (code == 3) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
} else if (code == 4) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
// Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
i++;
if (mArgs[i] == 0) {
// No underline.
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
} else {
// Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
}
} else {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
}
} else if (code == 5) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
} else if (code == 7) {
@ -1813,8 +1921,8 @@ public final class TerminalEmulator {
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
} else if (code >= 30 && code <= 37) {
mForeColor = code - 30;
} else if (code == 38 || code == 48) {
// Extended set foreground(38)/background (48) color.
} else if (code == 38 || code == 48 || code == 58) {
// Extended set foreground(38)/background(48)/underline(58) color.
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
// "5;$INDEX" to set an indexed color.
if (i + 2 > mArgIndex) continue;
@ -1823,27 +1931,30 @@ public final class TerminalEmulator {
if (i + 4 > mArgIndex) {
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else {
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
int red = getArg(i + 2, 0, false);
int green = getArg(i + 3, 0, false);
int blue = getArg(i + 4, 0, false);
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
} else {
int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
if (code == 38) {
mForeColor = argbColor;
} else {
mBackColor = argbColor;
int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
switch (code) {
case 38: mForeColor = argbColor; break;
case 48: mBackColor = argbColor; break;
case 58: mUnderlineColor = argbColor; break;
}
}
i += 4; // "2;P_r;P_g;P_r"
}
} else if (firstArg == 5) {
int color = mArgs[i + 2];
int color = getArg(i + 2, 0, false);
i += 2; // "5;P_s"
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
if (code == 38) {
mForeColor = color;
} else {
mBackColor = color;
switch (code) {
case 38: mForeColor = color; break;
case 48: mBackColor = color; break;
case 58: mUnderlineColor = color; break;
}
} else {
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
@ -1857,6 +1968,8 @@ public final class TerminalEmulator {
mBackColor = code - 40;
} else if (code == 49) { // Set default background color.
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (code == 59) { // Set default underline color.
mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
mForeColor = code - 90 + 8;
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
@ -2092,67 +2205,64 @@ public final class TerminalEmulator {
private void scrollDownOneLine() {
mScrollCounter++;
long currentStyle = getStyle();
if (mLeftMargin != 0 || mRightMargin != mColumns) {
// Horizontal margin: Do not put anything into scroll history, just non-margin part of screen up.
mScreen.blockCopy(mLeftMargin, mTopMargin + 1, mRightMargin - mLeftMargin, mBottomMargin - mTopMargin - 1, mLeftMargin, mTopMargin);
// .. and blank bottom row between margins:
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', mEffect);
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', currentStyle);
} else {
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, getStyle());
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, currentStyle);
}
}
/**
* Process the next ASCII character of a parameter.
*
* Parameter characters modify the action or interpretation of the sequence. You can use up to
* 16 parameters per sequence. You must use the ; character to separate parameters.
* All parameters are unsigned, positive decimal integers, with the most significant
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
*
* <p>Parameter characters modify the action or interpretation of the sequence. Originally
* you can use up to 16 parameters per sequence, but following at least xterm and alacritty
* we use a common space for parameters and sub-parameters, allowing 32 in total.
*
* <p>All parameters are unsigned, positive decimal integers, with the most significant
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
* or omitted parameter indicates a default value for the sequence. For most
* sequences, the default value is 1.
*
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
* <p>References:
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
* */
private void parseArg(int inputByte) {
int[] bytes = new int[]{inputByte};
// Only doing this for ESC_CSI and not for other ESC_CSI_* since they seem to be using their
// own defaults with getArg*() calls, but there may be missed cases
if (mEscapeState == ESC_CSI) {
if ((mIsCSIStart && inputByte == ';') || // If sequence starts with a ; character, like \033[;m
(!mIsCSIStart && mLastCSIArg != null && mLastCSIArg == ';' && inputByte == ';')) { // If sequence contains sequential ; characters, like \033[;;m
bytes = new int[]{'0', ';'}; // Assume 0 was passed
private void parseArg(int b) {
if (b >= '0' && b <= '9') {
if (mArgIndex < mArgs.length) {
int oldValue = mArgs[mArgIndex];
int thisDigit = b - '0';
int value;
if (oldValue >= 0) {
value = oldValue * 10 + thisDigit;
} else {
value = thisDigit;
}
if (value > 9999)
value = 9999;
mArgs[mArgIndex] = value;
}
}
mIsCSIStart = false;
for (int b : bytes) {
if (b >= '0' && b <= '9') {
if (mArgIndex < mArgs.length) {
int oldValue = mArgs[mArgIndex];
int thisDigit = b - '0';
int value;
if (oldValue >= 0) {
value = oldValue * 10 + thisDigit;
} else {
value = thisDigit;
}
if (value > 9999)
value = 9999;
mArgs[mArgIndex] = value;
continueSequence(mEscapeState);
} else if (b == ';' || b == ':') {
if (mArgIndex + 1 < mArgs.length) {
mArgIndex++;
if (b == ':') {
mArgsSubParamsBitSet |= 1 << mArgIndex;
}
continueSequence(mEscapeState);
} else if (b == ';') {
if (mArgIndex < mArgs.length) {
mArgIndex++;
}
continueSequence(mEscapeState);
} else {
unknownSequence(b);
logError("Too many parameters when in state: " + mEscapeState);
}
mLastCSIArg = b;
continueSequence(mEscapeState);
} else {
unknownSequence(b);
}
}

View File

@ -21,7 +21,7 @@ import java.util.UUID;
* A terminal session, consisting of a process coupled to a terminal interface.
* <p>
* The subprocess will be executed by the constructor, and when the size is made known by a call to
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* {@link #updateSize(int, int, int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* All terminal emulation and callback methods will be performed on the main thread.
* <p>
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
@ -61,7 +61,7 @@ public final class TerminalSession extends TerminalOutput {
/**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int, int, int)}.
*/
private int mTerminalFileDescriptor;
@ -100,12 +100,12 @@ public final class TerminalSession extends TerminalOutput {
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) {
public void updateSize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
if (mEmulator == null) {
initializeEmulator(columns, rows);
initializeEmulator(columns, rows, cellWidthPixels, cellHeightPixels);
} else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows);
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns, cellWidthPixels, cellHeightPixels);
mEmulator.resize(columns, rows, cellWidthPixels, cellHeightPixels);
}
}
@ -120,11 +120,11 @@ public final class TerminalSession extends TerminalOutput {
* @param columns The number of columns in the terminal window.
* @param rows The number of rows in the terminal window.
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
public void initializeEmulator(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
mEmulator = new TerminalEmulator(this, columns, rows, cellWidthPixels, cellHeightPixels, mTranscriptRows, mClient);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns, cellWidthPixels, cellHeightPixels);
mShellPid = processId[0];
mClient.setTerminalShellPid(this, mShellPid);

View File

@ -29,7 +29,9 @@ static int create_subprocess(JNIEnv* env,
char** envp,
int* pProcessId,
jint rows,
jint columns)
jint columns,
jint cell_width,
jint cell_height)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
@ -57,7 +59,7 @@ static int create_subprocess(JNIEnv* env,
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns, .ws_xpixel = (unsigned short) (columns * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height)};
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
@ -121,7 +123,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
jobjectArray envVars,
jintArray processIdArray,
jint rows,
jint columns)
jint columns,
jint cell_width,
jint cell_height)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
@ -156,7 +160,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns, cell_width, cell_height);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
@ -178,9 +182,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
return ptm;
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols, jint cell_width, jint cell_height)
{
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols, .ws_xpixel = (unsigned short) (cols * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height) };
ioctl(fd, TIOCSWINSZ, &sz);
}

View File

@ -0,0 +1,21 @@
package com.termux.terminal;
public class ApcTest extends TerminalTestCase {
public void testApcConsumed() {
// At time of writing this is part of what yazi sends for probing for kitty graphics protocol support:
// https://github.com/sxyazi/yazi/blob/0cdaff98d0b3723caff63eebf1974e7907a43a2c/yazi-adapter/src/emulator.rs#L129
// This should not result in anything being written to the screen: If kitty graphics protocol support
// is implemented it should instead result in an error code on stdin, and if not it should be consumed
// silently just as xterm does. See https://sw.kovidgoyal.net/kitty/graphics-protocol/.
withTerminalSized(2, 2)
.enterString("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\")
.assertLinesAre(" ", " ");
// It is ok for the APC content to be non printable characters:
withTerminalSized(12, 2)
.enterString("hello \033_some\023\033_\\apc#end\033\\ world")
.assertLinesAre("hello world", " ");
}
}

View File

@ -1,5 +1,7 @@
package com.termux.terminal;
import java.util.List;
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
public class ControlSequenceIntroducerTest extends TerminalTestCase {
@ -62,4 +64,68 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
assertEquals("y\nz", mTerminal.getScreen().getTranscriptText());
}
public void testReportPixelSize() {
int columns = 3;
int rows = 3;
withTerminalSized(columns, rows);
int cellWidth = TerminalTest.INITIAL_CELL_WIDTH_PIXELS;
int cellHeight = TerminalTest.INITIAL_CELL_HEIGHT_PIXELS;
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
columns = 23;
rows = 33;
resize(columns, rows);
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
cellWidth = 8;
cellHeight = 18;
mTerminal.resize(columns, rows, cellWidth, cellHeight);
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
}
/**
* See <a href="https://sw.kovidgoyal.net/kitty/underlines/">Colored and styled underlines</a>:
*
* <pre>
* <ESC>[4:0m # no underline
* <ESC>[4:1m # straight underline
* <ESC>[4:2m # double underline
* <ESC>[4:3m # curly underline
* <ESC>[4:4m # dotted underline
* <ESC>[4:5m # dashed underline
* <ESC>[4m # straight underline (for backwards compat)
* <ESC>[24m # no underline (for backwards compat)
* </pre>
* <p>
* We currently parse the variants, but map them to normal/no underlines as appropriate
*/
public void testUnderlineVariants() {
for (String suffix : List.of("", ":1", ":2", ":3", ":4", ":5")) {
for (String stop : List.of("24", "4:0")) {
withTerminalSized(3, 3);
enterString("\033[4" + suffix + "m").assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
enterString("\033[4;1m").assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
enterString("\033[" + stop + "m").assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD, mTerminal.mEffect);
}
}
}
public void testManyParameters() {
StringBuilder b = new StringBuilder("\033[");
for (int i = 0; i < 30; i++) {
b.append("0;");
}
b.append("4:2");
// This clearing of underline should be ignored as the parameters pass the threshold for too many parameters:
b.append("4:0m");
withTerminalSized(3, 3)
.enterString(b.toString())
.assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
}
}

View File

@ -11,10 +11,10 @@ public class HistoryTest extends TerminalTestCase {
assertLinesAre("777", "888", "999");
assertHistoryStartsWith("666", "555");
mTerminal.resize(cols, 2);
resize(cols, 2);
assertHistoryStartsWith("777", "666", "555");
mTerminal.resize(cols, 3);
resize(cols, 3);
assertHistoryStartsWith("666", "555");
}

View File

@ -72,11 +72,11 @@ public class ResizeTest extends TerminalTestCase {
enterString("\r\n");
}
assertLinesAre("998 ", "999 ", " ");
mTerminal.resize(cols, 2);
resize(cols, 2);
assertLinesAre("999 ", " ");
mTerminal.resize(cols, 5);
resize(cols, 5);
assertLinesAre("996 ", "997 ", "998 ", "999 ", " ");
mTerminal.resize(cols, rows);
resize(cols, rows);
assertLinesAre("998 ", "999 ", " ");
}

View File

@ -75,6 +75,16 @@ public class ScrollRegionTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033[?6h\033ED").assertLinesAre("ABC", " D ", " ");
}
public void testRiRespectsLeftMargin() {
// Reverse Index (RI), ${ESC}M, should respect horizontal margins:
withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033M").assertLinesAre("A D", " BC ", " ");
}
public void testSdRespectsLeftMargin() {
// Scroll Down (SD), ${CSI}${N}T, should respect horizontal margins:
withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033[2T").assertLinesAre("A D", " ", " BC ");
}
public void testBackwardIndex() {
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 7.
// Without margins:
@ -127,4 +137,31 @@ public class ScrollRegionTest extends TerminalTestCase {
" xxx"
);
}
/**
* See <a href="https://github.com/termux/termux-packages/issues/12556">reported issue</a>.
*/
public void testClearingWhenScrollingWithMargins() {
int newForeground = 2;
int newBackground = 3;
int size = 3;
TerminalTestCase terminal = withTerminalSized(size, size)
// Enable horizontal margin and set left margin to 1:
.enterString("\033[?69h\033[2s")
// Set foreground and background color:
.enterString("\033[" + (30 + newForeground) + ";" + (40 + newBackground) + "m")
// Enter newlines to scroll down:
.enterString("\r\n\r\n\r\n\r\n\r\n");
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
// The first column (outside of the scrolling area, due to us setting a left scroll
// margin of 1) should be unmodified, the others should use the current style:
int expectedForeground = col == 0 ? TextStyle.COLOR_INDEX_FOREGROUND : newForeground;
int expectedBackground = col == 0 ? TextStyle.COLOR_INDEX_BACKGROUND : newBackground;
terminal.assertForegroundColorAt(row, col, expectedForeground);
terminal.assertBackgroundColorAt(row, col, expectedBackground);
}
}
}
}

View File

@ -82,7 +82,7 @@ public class TerminalTest extends TerminalTestCase {
assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
for (int width = 3; width < 12; width++) {
for (int height = 3; height < 12; height++) {
mTerminal.resize(width, height);
resize(width, height);
assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
}
}
@ -137,6 +137,11 @@ public class TerminalTest extends TerminalTestCase {
}
public void testSelectGraphics() {
selectGraphicsTestRun(';');
selectGraphicsTestRun(':');
}
public void selectGraphicsTestRun(char separator) {
withTerminalSized(5, 5);
enterString("\033[31m");
assertEquals(mTerminal.mForeColor, 1);
@ -155,40 +160,59 @@ public class TerminalTest extends TerminalTestCase {
// Check TerminalEmulator.parseArg()
enterString("\033[31m\033[m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[;m");
enterString("\033[31m\033[;m".replace(';', separator));
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[0m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[0;m");
enterString("\033[31m\033[0;m".replace(';', separator));
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31;;m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31::m");
assertEquals(1, mTerminal.mForeColor);
enterString("\033[31;m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31:m");
assertEquals(1, mTerminal.mForeColor);
enterString("\033[31;;41m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(1, mTerminal.mBackColor);
enterString("\033[0m");
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
// 256 colors:
enterString("\033[38;5;119m");
enterString("\033[38;5;119m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;5;129m");
enterString("\033[48;5;129m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Invalid parameter:
enterString("\033[48;8;129m");
enterString("\033[48;8;129m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Multiple parameters at once:
enterString("\033[38;5;178;48;5;179;m");
enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator));
assertEquals(178, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
// Omitted parameter means zero:
enterString("\033[38;5;m".replace(';', separator));
assertEquals(0, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
enterString("\033[48;5;m".replace(';', separator));
assertEquals(0, mTerminal.mForeColor);
assertEquals(0, mTerminal.mBackColor);
// 24 bit colors:
enterString(("\033[0m")); // Reset fg and bg colors.
enterString("\033[38;2;255;127;2m");
enterString("\033[38;2;255;127;2m".replace(';', separator));
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;2;1;2;254m");
enterString("\033[48;2;1;2;254m".replace(';', separator));
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
@ -197,14 +221,30 @@ public class TerminalTest extends TerminalTestCase {
enterString(("\033[0m")); // Reset fg and bg colors.
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[38;2;255;127;2;48;2;1;2;254m");
enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator));
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, invalid input:
enterString("\033[38;2;300;127;2;48;2;1;300;254m");
enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator));
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, omitted parameter means zero:
enterString("\033[38;2;255;127;m".replace(';', separator));
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
enterString("\033[38;2;123;;77m".replace(';', separator));
expectedForeground = 0xff000000 | (123 << 16) | 77;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, extra sub-parameters are skipped:
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
enterString("\033[0;38:2:255:127:2:48:2:1:2:254m");
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
}
public void testBackgroundColorErase() {

View File

@ -13,7 +13,10 @@ import java.util.Set;
public abstract class TerminalTestCase extends TestCase {
public static class MockTerminalOutput extends TerminalOutput {
public static final int INITIAL_CELL_WIDTH_PIXELS = 13;
public static final int INITIAL_CELL_HEIGHT_PIXELS = 15;
public static class MockTerminalOutput extends TerminalOutput {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public final List<ChangedTitle> titleChanges = new ArrayList<>();
public final List<String> clipboardPuts = new ArrayList<>();
@ -108,7 +111,7 @@ public abstract class TerminalTestCase extends TestCase {
protected TerminalTestCase withTerminalSized(int columns, int rows) {
// The tests aren't currently using the client, so a null client will suffice, a dummy client should be implemented if needed
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2, null);
mTerminal = new TerminalEmulator(mOutput, columns, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS, rows * 2, null);
return this;
}
@ -201,7 +204,7 @@ public abstract class TerminalTestCase extends TestCase {
}
public TerminalTestCase resize(int cols, int rows) {
mTerminal.resize(cols, rows);
mTerminal.resize(cols, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS);
assertInvariants();
return this;
}
@ -301,6 +304,11 @@ public abstract class TerminalTestCase extends TestCase {
assertEquals(color, TextStyle.decodeForeColor(style));
}
public void assertBackgroundColorAt(int externalRow, int column, int color) {
long style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
assertEquals(color, TextStyle.decodeBackColor(style));
}
public TerminalTestCase assertColor(int colorIndex, int expected) {
int actual = mTerminal.mColors.mCurrentColors[colorIndex];
if (expected != actual) {

View File

@ -233,7 +233,7 @@ public final class TerminalRenderer {
mTextPaint.setColor(foreColor);
// The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint);
}
if (savedMatrix) canvas.restore();

View File

@ -27,6 +27,7 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
@ -85,6 +86,43 @@ public final class TerminalView extends View {
/** If non-zero, this is the last unicode code point received if that was a combining character. */
int mCombiningAccent;
/**
* The current AutoFill type returned for {@link View#getAutofillType()} by {@link #getAutofillType()}.
*
* The default is {@link #AUTOFILL_TYPE_NONE} so that AutoFill UI, like toolbar above keyboard
* is not shown automatically, like on Activity starts/View create. This value should be updated
* to required value, like {@link #AUTOFILL_TYPE_TEXT} before calling
* {@link AutofillManager#requestAutofill(View)} so that AutoFill UI shows. The updated value
* set will automatically be restored to {@link #AUTOFILL_TYPE_NONE} in
* {@link #autofill(AutofillValue)} so that AutoFill UI isn't shown anymore by calling
* {@link #resetAutoFill()}.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int mAutoFillType = AUTOFILL_TYPE_NONE;
/**
* The current AutoFill type returned for {@link View#getImportantForAutofill()} by
* {@link #getImportantForAutofill()}.
*
* The default is {@link #IMPORTANT_FOR_AUTOFILL_NO} so that view is not considered important
* for AutoFill. This value should be updated to required value, like
* {@link #IMPORTANT_FOR_AUTOFILL_YES} before calling {@link AutofillManager#requestAutofill(View)}
* so that Android and apps consider the view as important for AutoFill to process the request.
* The updated value set will automatically be restored to {@link #IMPORTANT_FOR_AUTOFILL_NO} in
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
/**
* The current AutoFill hints returned for {@link View#getAutofillHints()} ()} by {@link #getAutofillHints()} ()}.
*
* The default is an empty `string[]`. This value should be updated to required value. The
* updated value set will automatically be restored an empty `string[]` in
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
*/
private String[] mAutoFillHints = new String[0];
private final boolean mAccessibilityEnabled;
/** The {@link KeyEvent} is generated from a virtual keyboard, like manually with the {@link KeyEvent#KeyEvent(int, int)} constructor. */
@ -609,6 +647,7 @@ public final class TerminalView extends View {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
cancelRequestAutoFill();
if (isSelectingText()) {
stopTextSelectionMode();
return true;
@ -894,7 +933,7 @@ public final class TerminalView extends View {
if (shiftDown) {
long time = SystemClock.uptimeMillis();
MotionEvent motionEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -1 : 1);
doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -mEmulator.mRows : mEmulator.mRows);
motionEvent.recycle();
return true;
}
@ -950,7 +989,7 @@ public final class TerminalView extends View {
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows);
mTermSession.updateSize(newColumns, newRows, (int) mRenderer.getFontWidth(), mRenderer.getFontLineSpacing());
mEmulator = mTermSession.getEmulator();
mClient.onEmulatorSet();
@ -1028,12 +1067,20 @@ public final class TerminalView extends View {
if (value.isText()) {
mTermSession.write(value.getTextValue().toString());
}
resetAutoFill();
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getAutofillType() {
return AUTOFILL_TYPE_TEXT;
return mAutoFillType;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public String[] getAutofillHints() {
return mAutoFillHints;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ -1042,6 +1089,95 @@ public final class TerminalView extends View {
return AutofillValue.forText("");
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getImportantForAutofill() {
return mAutoFillImportance;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private synchronized void resetAutoFill() {
// Restore none type so that AutoFill UI isn't shown anymore.
mAutoFillType = AUTOFILL_TYPE_NONE;
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
mAutoFillHints = new String[0];
}
public AutofillManager getAutoFillManagerService() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
try {
Context context = getContext();
if (context == null) return null;
return context.getSystemService(AutofillManager.class);
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to get AutofillManager service", e);
return null;
}
}
public boolean isAutoFillEnabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
try {
AutofillManager autofillManager = getAutoFillManagerService();
return autofillManager != null && autofillManager.isEnabled();
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to check if Autofill is enabled", e);
return false;
}
}
public synchronized void requestAutoFillUsername() {
requestAutoFill(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_USERNAME} :
null);
}
public synchronized void requestAutoFillPassword() {
requestAutoFill(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_PASSWORD} :
null);
}
public synchronized void requestAutoFill(String[] autoFillHints) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
if (autoFillHints == null || autoFillHints.length < 1) return;
try {
AutofillManager autofillManager = getAutoFillManagerService();
if (autofillManager != null && autofillManager.isEnabled()) {
// Update type that will be returned by `getAutofillType()` so that AutoFill UI is shown.
mAutoFillType = AUTOFILL_TYPE_TEXT;
// Update importance that will be returned by `getImportantForAutofill()` so that
// AutoFill considers the view as important.
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_YES;
// Update hints that will be returned by `getAutofillHints()` for which to show AutoFill UI.
mAutoFillHints = autoFillHints;
autofillManager.requestAutofill(this);
}
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to request Autofill", e);
}
}
public synchronized void cancelRequestAutoFill() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
if (mAutoFillType == AUTOFILL_TYPE_NONE) return;
try {
AutofillManager autofillManager = getAutoFillManagerService();
if (autofillManager != null && autofillManager.isEnabled()) {
resetAutoFill();
autofillManager.cancel();
}
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to cancel Autofill request", e);
}
}
/**

View File

@ -14,7 +14,7 @@ android {
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
implementation "org.lsposed.hiddenapibypass:hiddenapibypass:2.0"
implementation "org.lsposed.hiddenapibypass:hiddenapibypass:5.0"
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
// noinspection GradleDependency

View File

@ -5,6 +5,8 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.system.Os;
import android.system.OsConstants;
import androidx.annotation.NonNull;
@ -158,7 +160,19 @@ public class AndroidUtils {
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
appendPropertyToMarkdown(markdownString, "SUPPORTED_32_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_32_BIT_ABIS));
appendPropertyToMarkdown(markdownString, "SUPPORTED_64_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_64_BIT_ABIS));
// If on Android >= 15
if (Build.VERSION.SDK_INT >= 35) {
try {
appendPropertyToMarkdownIfSet(markdownString, "PAGE_SIZE", Os.sysconf(OsConstants._SC_PAGESIZE));
} catch (Throwable t) {
// Ignore
}
}
markdownString.append("\n##\n");

View File

@ -209,31 +209,44 @@ public class PermissionUtils {
/** If path is under primary external storage directory and storage permission is missing,
* then legacy or manage external storage permission will be requested from the user via a call
* to {@link #checkAndRequestLegacyOrManageExternalStoragePermission(Context, int, boolean)}.
* to {@link #checkAndRequestLegacyOrManageExternalStoragePermission(Context, int, boolean, boolean)}.
*
* @param context The context for operations.
* @param filePath The path to check.
* @param requestCode The request code to use while asking for permission.
* @param prioritizeManageExternalStoragePermission If {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE}
* permission should be requested if on
* Android `>= 11` instead of getting legacy
* storage permission.
* @param showErrorMessage If an error message toast should be shown if permission is not granted.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
@SuppressLint("SdCardPath")
public static boolean checkAndRequestLegacyOrManageExternalStoragePermissionIfPathOnPrimaryExternalStorage(
@NonNull Context context, String filePath, int requestCode, boolean showErrorMessage) {
@NonNull Context context, String filePath, int requestCode,
boolean prioritizeManageExternalStoragePermission, boolean showErrorMessage) {
// If path is under primary external storage directory, then check for missing permissions.
if (!FileUtils.isPathInDirPaths(filePath,
Arrays.asList(Environment.getExternalStorageDirectory().getAbsolutePath(), "/sdcard"), true))
return true;
return checkAndRequestLegacyOrManageExternalStoragePermission(context, requestCode, showErrorMessage);
return checkAndRequestLegacyOrManageExternalStoragePermission(context, requestCode, prioritizeManageExternalStoragePermission, showErrorMessage);
}
/**
* Check if legacy or manage external storage permissions has been granted. If
* {@link #isLegacyExternalStoragePossible(Context)} returns {@code true}, them it will be
* checked if app has has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions, otherwise it will be checked
* if app has been granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission.
* Check if legacy or manage external storage permissions has been granted.
*
* - If `prioritizeManageExternalStoragePermission` is `true and running on Android `>= 11`, then
* it will be checked if app has been granted the
* {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE}.
* - If `prioritizeManageExternalStoragePermission` is `false` and running on Android `>= 11`, then
* if {@link #isLegacyExternalStoragePossible(Context)} returns `true`, them it will be
* checked if app has has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions, otherwise it will be checked
* if app has been granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission.
* - If running on Android `< 11`, then it will only be checked if app has been granted
* {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions.
*
* If storage permission is missing, it will be requested from the user if {@code context} is an
* instance of {@link Activity} or {@link AppCompatActivity} and {@code requestCode}
@ -256,16 +269,34 @@ public class PermissionUtils {
*}
* @param context The context for operations.
* @param requestCode The request code to use while asking for permission.
* @param prioritizeManageExternalStoragePermission If {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE}
* permission should be requested if on
* Android `>= 11` instead of getting legacy
* storage permission.
* @param showErrorMessage If an error message toast should be shown if permission is not granted.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean checkAndRequestLegacyOrManageExternalStoragePermission(@NonNull Context context,
int requestCode,
boolean prioritizeManageExternalStoragePermission,
boolean showErrorMessage) {
Logger.logVerbose(LOG_TAG, "Checking storage permission");
String errmsg;
boolean requestLegacyStoragePermission = isLegacyExternalStoragePossible(context);
Boolean requestLegacyStoragePermission = null;
if (prioritizeManageExternalStoragePermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
requestLegacyStoragePermission = false;
if (requestLegacyStoragePermission == null)
requestLegacyStoragePermission = isLegacyExternalStoragePossible(context);
boolean checkIfHasRequestedLegacyExternalStorage = checkIfHasRequestedLegacyExternalStorage(context);
Logger.logVerbose(LOG_TAG, "prioritizeManageExternalStoragePermission=" + prioritizeManageExternalStoragePermission +
", requestLegacyStoragePermission=" + requestLegacyStoragePermission +
", checkIfHasRequestedLegacyExternalStorage=" + checkIfHasRequestedLegacyExternalStorage);
if (requestLegacyStoragePermission && checkIfHasRequestedLegacyExternalStorage) {
// Check if requestLegacyExternalStorage is set to true in app manifest
if (!hasRequestedLegacyExternalStorage(context, showErrorMessage))

View File

@ -1,6 +1,7 @@
package com.termux.shared.shell.command.environment;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
@ -20,6 +21,20 @@ import java.util.HashMap;
*/
public class AndroidShellEnvironment extends UnixShellEnvironment {
/** Environment variable scope for Android. */
public static final String ANDROID_ENV_SCOPE = "ANDROID__"; // Default: "ANDROID__"
/**
* Environment variable for the Android build SDK version currently running on the device that
* is defined by {@link Build.VERSION#SDK_INT} and `ro.build.version.sdk` system property.
*
* - https://developer.android.com/reference/android/os/Build.VERSION#SDK_INT
* - https://developer.android.com/reference/android/os/Build.VERSION_CODES
*
* Default value: `ANDROID__BUILD_VERSION_SDK`
*/
public static final String ENV_ANDROID__BUILD_VERSION_SDK = ANDROID_ENV_SCOPE + "BUILD_VERSION_SDK";
protected ShellCommandShellEnvironment shellCommandShellEnvironment;
public AndroidShellEnvironment() {
@ -61,6 +76,8 @@ public class AndroidShellEnvironment extends UnixShellEnvironment {
ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "DEX2OATBOOTCLASSPATH");
ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "SYSTEMSERVERCLASSPATH");
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_ANDROID__BUILD_VERSION_SDK, String.valueOf(Build.VERSION.SDK_INT));
return environment;
}

View File

@ -33,13 +33,13 @@ public class ShellCommandShellEnvironment {
public static final String ENV_SHELL_CMD__APP_SHELL_NUMBER_SINCE_BOOT = SHELL_CMD_ENV_PREFIX + "APP_SHELL_NUMBER_SINCE_BOOT";
/** Environment variable for the {{@link ExecutionCommand.Runner#TERMINAL_SESSION} number since boot. */
public static final String ENV_SHELL_CMD__TERMINAL_SESSION_NUMBER_SINCE_BOOT = SHELL_CMD_ENV_PREFIX + "TERMINAL_SESSION_NUMBER_SINCE_BOOT";
public static final String ENV_SHELL_CMD__APP_TERMINAL_SESSION_NUMBER_SINCE_BOOT = SHELL_CMD_ENV_PREFIX + "APP_TERMINAL_SESSION_NUMBER_SINCE_BOOT";
/** Environment variable for the {@link ExecutionCommand.Runner#APP_SHELL} number since app start. */
public static final String ENV_SHELL_CMD__APP_SHELL_NUMBER_SINCE_APP_START = SHELL_CMD_ENV_PREFIX + "APP_SHELL_NUMBER_SINCE_APP_START";
/** Environment variable for the {@link ExecutionCommand.Runner#TERMINAL_SESSION} number since app start. */
public static final String ENV_SHELL_CMD__TERMINAL_SESSION_NUMBER_SINCE_APP_START = SHELL_CMD_ENV_PREFIX + "TERMINAL_SESSION_NUMBER_SINCE_APP_START";
public static final String ENV_SHELL_CMD__APP_TERMINAL_SESSION_NUMBER_SINCE_APP_START = SHELL_CMD_ENV_PREFIX + "APP_TERMINAL_SESSION_NUMBER_SINCE_APP_START";
/** Get shell environment containing info for {@link ExecutionCommand}. */

View File

@ -1,6 +1,7 @@
package com.termux.shared.termux;
import android.annotation.SuppressLint;
import android.content.Intent;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.ExecutionCommand.Runner;
@ -11,7 +12,7 @@ import java.util.Formatter;
import java.util.List;
/*
* Version: v0.52.0
* Version: v0.53.0
* SPDX-License-Identifier: MIT
*
* Changelog
@ -277,6 +278,10 @@ import java.util.List;
*
* - 0.52.0 (2022-06-18)
* - Added `TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY`.
*
* - 0.53.0 (2025-01-12)
* - Renamed `TERMUX_API`, `TERMUX_STYLING`, `TERMUX_TASKER`, `TERMUX_WIDGET` classes with `_APP` suffix added.
* - Added `TERMUX_*_MAIN_ACTIVITY_NAME` and `TERMUX_*_LAUNCHER_ACTIVITY_NAME` constants to each app class.
*/
/**
@ -1192,10 +1197,30 @@ public final class TermuxConstants {
/**
* Termux:API app constants.
*/
public static final class TERMUX_API {
public static final class TERMUX_API_APP {
/** Termux:API app core activity name. */
public static final String TERMUX_API_ACTIVITY_NAME = TERMUX_API_PACKAGE_NAME + ".activities.TermuxAPIActivity"; // Default: "com.termux.tasker.activities.TermuxAPIActivity"
/** Termux:API app main activity name. */
public static final String TERMUX_API_MAIN_ACTIVITY_NAME = TERMUX_API_PACKAGE_NAME + ".activities.TermuxAPIMainActivity"; // Default: "com.termux.api.activities.TermuxAPIMainActivity"
/** Termux:API app launcher activity name. This is an `activity-alias` for {@link #TERMUX_API_MAIN_ACTIVITY_NAME} used for launchers with {@link Intent#CATEGORY_LAUNCHER}. */
public static final String TERMUX_API_LAUNCHER_ACTIVITY_NAME = TERMUX_API_PACKAGE_NAME + ".activities.TermuxAPILauncherActivity"; // Default: "com.termux.api.activities.TermuxAPILauncherActivity"
}
/**
* Termux:Boot app constants.
*/
public static final class TERMUX_BOOT_APP {
/** Termux:Boot app main activity name. */
public static final String TERMUX_BOOT_MAIN_ACTIVITY_NAME = TERMUX_BOOT_PACKAGE_NAME + ".activities.TermuxBootMainActivity"; // Default: "com.termux.boot.activities.TermuxBootMainActivity"
/** Termux:Boot app launcher activity name. This is an `activity-alias` for {@link #TERMUX_BOOT_MAIN_ACTIVITY_NAME} used for launchers with {@link Intent#CATEGORY_LAUNCHER}. */
public static final String TERMUX_BOOT_LAUNCHER_ACTIVITY_NAME = TERMUX_BOOT_PACKAGE_NAME + ".activities.TermuxBootLauncherActivity"; // Default: "com.termux.boot.activities.TermuxBootLauncherActivity"
}
@ -1208,6 +1233,9 @@ public final class TermuxConstants {
*/
public static final class TERMUX_FLOAT_APP {
/** Termux:Float app core activity name. */
public static final String TERMUX_FLOAT_ACTIVITY_NAME = TERMUX_FLOAT_PACKAGE_NAME + ".TermuxFloatActivity"; // Default: "com.termux.window.TermuxFloatActivity"
/** Termux:Float app core service name. */
public static final String TERMUX_FLOAT_SERVICE_NAME = TERMUX_FLOAT_PACKAGE_NAME + ".TermuxFloatService"; // Default: "com.termux.window.TermuxFloatService"
@ -1236,11 +1264,18 @@ public final class TermuxConstants {
/**
* Termux:Styling app constants.
*/
public static final class TERMUX_STYLING {
public static final class TERMUX_STYLING_APP {
/** Termux:Styling app core activity name. */
public static final String TERMUX_STYLING_ACTIVITY_NAME = TERMUX_STYLING_PACKAGE_NAME + ".TermuxStyleActivity"; // Default: "com.termux.styling.TermuxStyleActivity"
/** Termux:Styling app main activity name. */
public static final String TERMUX_STYLING_MAIN_ACTIVITY_NAME = TERMUX_STYLING_PACKAGE_NAME + ".activities.TermuxStylingMainActivity"; // Default: "com.termux.styling.activities.TermuxStylingMainActivity"
/** Termux:Styling app launcher activity name. This is an `activity-alias` for {@link #TERMUX_STYLING_MAIN_ACTIVITY_NAME} used for launchers with {@link Intent#CATEGORY_LAUNCHER}. */
public static final String TERMUX_STYLING_LAUNCHER_ACTIVITY_NAME = TERMUX_STYLING_PACKAGE_NAME + ".activities.TermuxStylingLauncherActivity"; // Default: "com.termux.styling.activities.TermuxStylingLauncherActivity"
}
@ -1250,10 +1285,13 @@ public final class TermuxConstants {
/**
* Termux:Tasker app constants.
*/
public static final class TERMUX_TASKER {
public static final class TERMUX_TASKER_APP {
/** Termux:Tasker app core activity name. */
public static final String TERMUX_TASKER_ACTIVITY_NAME = TERMUX_TASKER_PACKAGE_NAME + ".activities.TermuxTaskerActivity"; // Default: "com.termux.tasker.activities.TermuxTaskerActivity"
/** Termux:Tasker app main activity name. */
public static final String TERMUX_TASKER_MAIN_ACTIVITY_NAME = TERMUX_TASKER_PACKAGE_NAME + ".activities.TermuxTaskerMainActivity"; // Default: "com.termux.tasker.activities.TermuxTaskerMainActivity"
/** Termux:Tasker app launcher activity name. This is an `activity-alias` for {@link #TERMUX_TASKER_MAIN_ACTIVITY_NAME} used for launchers with {@link Intent#CATEGORY_LAUNCHER}. */
public static final String TERMUX_TASKER_LAUNCHER_ACTIVITY_NAME = TERMUX_TASKER_PACKAGE_NAME + ".activities.TermuxTaskerLauncherActivity"; // Default: "com.termux.tasker.activities.TermuxTaskerLauncherActivity"
}
@ -1264,15 +1302,19 @@ public final class TermuxConstants {
/**
* Termux:Widget app constants.
*/
public static final class TERMUX_WIDGET {
public static final class TERMUX_WIDGET_APP {
/** Termux:Widget app core activity name. */
public static final String TERMUX_WIDGET_ACTIVITY_NAME = TERMUX_WIDGET_PACKAGE_NAME + ".activities.TermuxWidgetActivity"; // Default: "com.termux.widget.activities.TermuxWidgetActivity"
/** Termux:Widget app main activity name. */
public static final String TERMUX_WIDGET_MAIN_ACTIVITY_NAME = TERMUX_WIDGET_PACKAGE_NAME + ".activities.TermuxWidgetMainActivity"; // Default: "com.termux.widget.activities.TermuxWidgetMainActivity"
/** Termux:Widget app launcher activity name. This is an `activity-alias` for {@link #TERMUX_WIDGET_MAIN_ACTIVITY_NAME} used for launchers with {@link Intent#CATEGORY_LAUNCHER}. */
public static final String TERMUX_WIDGET_LAUNCHER_ACTIVITY_NAME = TERMUX_WIDGET_PACKAGE_NAME + ".activities.TermuxWidgetLauncherActivity"; // Default: "com.termux.widget.activities.TermuxWidgetLauncherActivity"
/** Intent {@code String} extra for the token of the Termux:Widget app shortcuts. */
public static final String EXTRA_TOKEN_NAME = TERMUX_PACKAGE_NAME + ".shortcut.token"; // Default: "com.termux.shortcut.token"
/**
* Termux:Widget app {@link android.appwidget.AppWidgetProvider} class.
*/

View File

@ -326,7 +326,7 @@ public final class TermuxPropertyConstants {
/** Defines the key for extra keys */
public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys"
//public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; // Single row
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]"; // Double row
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[['ESC',{key: 'DRAWER', popup: 'PASTE'},'SCROLL','HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]"; // Double row
/** Defines the key for extra keys style */
public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style"

View File

@ -27,6 +27,12 @@ public class TermuxAppShellEnvironment {
/** Termux app environment variables. */
public static HashMap<String, String> termuxAppEnvironment;
/** Environment variable root scope. */
public static final String TERMUX_ENV__S_ROOT = "TERMUX_"; // Default: "TERMUX_"
/** Environment variable scope for Termux. */
public static final String TERMUX_ENV__S_TERMUX = TERMUX_ENV__S_ROOT + "_"; // Default: "TERMUX__"
/** Environment variable for the Termux app version. */
public static final String ENV_TERMUX_VERSION = TermuxConstants.TERMUX_ENV_PREFIX_ROOT + "_VERSION";
@ -34,15 +40,15 @@ public class TermuxAppShellEnvironment {
public static final String TERMUX_APP_ENV_PREFIX = TermuxConstants.TERMUX_ENV_PREFIX_ROOT + "_APP__";
/** Environment variable for the Termux app version name. */
public static final String ENV_TERMUX_APP__VERSION_NAME = TERMUX_APP_ENV_PREFIX + "VERSION_NAME";
public static final String ENV_TERMUX_APP__APP_VERSION_NAME = TERMUX_APP_ENV_PREFIX + "APP_VERSION_NAME";
/** Environment variable for the Termux app version code. */
public static final String ENV_TERMUX_APP__VERSION_CODE = TERMUX_APP_ENV_PREFIX + "VERSION_CODE";
public static final String ENV_TERMUX_APP__APP_VERSION_CODE = TERMUX_APP_ENV_PREFIX + "APP_VERSION_CODE";
/** Environment variable for the Termux app package name. */
public static final String ENV_TERMUX_APP__PACKAGE_NAME = TERMUX_APP_ENV_PREFIX + "PACKAGE_NAME";
/** Environment variable for the Termux app process id. */
public static final String ENV_TERMUX_APP__PID = TERMUX_APP_ENV_PREFIX + "PID";
/** Environment variable for the Termux app uid. */
public static final String ENV_TERMUX_APP__UID = TERMUX_APP_ENV_PREFIX + "UID";
public static final String ENV_TERMUX__UID = TERMUX_ENV__S_TERMUX + "UID";
/** Environment variable for the Termux app targetSdkVersion. */
public static final String ENV_TERMUX_APP__TARGET_SDK = TERMUX_APP_ENV_PREFIX + "TARGET_SDK";
/** Environment variable for the Termux app is debuggable apk build. */
@ -50,28 +56,29 @@ public class TermuxAppShellEnvironment {
/** Environment variable for the Termux app {@link TermuxConstants} APK_RELEASE_*. */
public static final String ENV_TERMUX_APP__APK_RELEASE = TERMUX_APP_ENV_PREFIX + "APK_RELEASE";
/** Environment variable for the Termux app install path. */
public static final String ENV_TERMUX_APP__APK_PATH = TERMUX_APP_ENV_PREFIX + "APK_PATH";
public static final String ENV_TERMUX_APP__APK_FILE = TERMUX_APP_ENV_PREFIX + "APK_FILE";
/** Environment variable for the Termux app is installed on external/portable storage. */
public static final String ENV_TERMUX_APP__IS_INSTALLED_ON_EXTERNAL_STORAGE = TERMUX_APP_ENV_PREFIX + "IS_INSTALLED_ON_EXTERNAL_STORAGE";
/** Environment variable for the Termux app process selinux context. */
public static final String ENV_TERMUX_APP__SE_PROCESS_CONTEXT = TERMUX_APP_ENV_PREFIX + "SE_PROCESS_CONTEXT";
/** Environment variable for the Termux app data files selinux context. */
public static final String ENV_TERMUX_APP__SE_FILE_CONTEXT = TERMUX_APP_ENV_PREFIX + "SE_FILE_CONTEXT";
/** Environment variable for the Termux app seInfo tag found in selinux policy used to set app process and app data files selinux context. */
public static final String ENV_TERMUX_APP__SE_INFO = TERMUX_APP_ENV_PREFIX + "SE_INFO";
/** Environment variable for the current Termux process selinux context. */
public static final String ENV_TERMUX__SE_PROCESS_CONTEXT = TERMUX_ENV__S_TERMUX + "SE_PROCESS_CONTEXT";
/** Environment variable for the Termux app user id. */
public static final String ENV_TERMUX_APP__USER_ID = TERMUX_APP_ENV_PREFIX + "USER_ID";
public static final String ENV_TERMUX__USER_ID = TERMUX_ENV__S_TERMUX + "USER_ID";
/** Environment variable for the Termux app profile owner. */
public static final String ENV_TERMUX_APP__PROFILE_OWNER = TERMUX_APP_ENV_PREFIX + "PROFILE_OWNER";
public static final String ENV_TERMUX__PROFILE_OWNER = TERMUX_ENV__S_TERMUX + "PROFILE_OWNER";
/** Environment variable for the Termux app {@link TermuxBootstrap#TERMUX_APP_PACKAGE_MANAGER}. */
public static final String ENV_TERMUX_APP__PACKAGE_MANAGER = TERMUX_APP_ENV_PREFIX + "PACKAGE_MANAGER";
/** Environment variable for the Termux app {@link TermuxBootstrap#TERMUX_APP_PACKAGE_VARIANT}. */
public static final String ENV_TERMUX_APP__PACKAGE_VARIANT = TERMUX_APP_ENV_PREFIX + "PACKAGE_VARIANT";
/** Environment variable for the Termux app files directory. */
public static final String ENV_TERMUX_APP__FILES_DIR = TERMUX_APP_ENV_PREFIX + "FILES_DIR";
public static final String ENV_TERMUX_APP__DATA_DIR = TERMUX_APP_ENV_PREFIX + "DATA_DIR";
public static final String ENV_TERMUX_APP__LEGACY_DATA_DIR = TERMUX_APP_ENV_PREFIX + "LEGACY_DATA_DIR";
public static final String ENV_TERMUX_APP__BUILD_DATA_DIR = TERMUX_APP_ENV_PREFIX + "BUILD_DATA_DIR";
/** Environment variable for the Termux app {@link TermuxAmSocketServer#getTermuxAppAMSocketServerEnabled(Context)}. */
public static final String ENV_TERMUX_APP__AM_SOCKET_SERVER_ENABLED = TERMUX_APP_ENV_PREFIX + "AM_SOCKET_SERVER_ENABLED";
@ -105,21 +112,22 @@ public class TermuxAppShellEnvironment {
HashMap<String, String> environment = new HashMap<>();
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_VERSION, PackageUtils.getVersionNameForPackage(packageInfo));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__VERSION_NAME, PackageUtils.getVersionNameForPackage(packageInfo));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__VERSION_CODE, String.valueOf(PackageUtils.getVersionCodeForPackage(packageInfo)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__APP_VERSION_NAME, PackageUtils.getVersionNameForPackage(packageInfo));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__APP_VERSION_CODE, String.valueOf(PackageUtils.getVersionCodeForPackage(packageInfo)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__PACKAGE_NAME, packageName);
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__PID, TermuxUtils.getTermuxAppPID(currentPackageContext));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__UID, String.valueOf(PackageUtils.getUidForPackage(applicationInfo)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX__UID, String.valueOf(PackageUtils.getUidForPackage(applicationInfo)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__TARGET_SDK, String.valueOf(PackageUtils.getTargetSDKForPackage(applicationInfo)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__IS_DEBUGGABLE_BUILD, PackageUtils.isAppForPackageADebuggableBuild(applicationInfo));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__APK_PATH, PackageUtils.getBaseAPKPathForPackage(applicationInfo));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__APK_FILE, PackageUtils.getBaseAPKPathForPackage(applicationInfo));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__IS_INSTALLED_ON_EXTERNAL_STORAGE, PackageUtils.isAppInstalledOnExternalStorage(applicationInfo));
putTermuxAPKSignature(currentPackageContext, environment);
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
if (termuxPackageContext != null) {
/*
// An app that does not have the same sharedUserId as termux app will not be able to get
// get termux context's classloader to get BuildConfig.TERMUX_PACKAGE_VARIANT via reflection.
// Check TermuxBootstrap.setTermuxPackageManagerAndVariantFromTermuxApp()
@ -127,24 +135,28 @@ public class TermuxAppShellEnvironment {
environment.put(ENV_TERMUX_APP__PACKAGE_MANAGER, TermuxBootstrap.TERMUX_APP_PACKAGE_MANAGER.getName());
if (TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT != null)
environment.put(ENV_TERMUX_APP__PACKAGE_VARIANT, TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT.getName());
*/
/*
// Will not be set for plugins
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__AM_SOCKET_SERVER_ENABLED,
TermuxAmSocketServer.getTermuxAppAMSocketServerEnabled(currentPackageContext));
*/
String filesDirPath = currentPackageContext.getFilesDir().getAbsolutePath();
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__FILES_DIR, filesDirPath);
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__DATA_DIR, applicationInfo.dataDir);
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__LEGACY_DATA_DIR, "/data/data/" + applicationInfo.packageName);
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__BUILD_DATA_DIR, TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH);
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__SE_PROCESS_CONTEXT, SELinuxUtils.getContext());
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__SE_FILE_CONTEXT, SELinuxUtils.getFileContext(filesDirPath));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX__SE_PROCESS_CONTEXT, SELinuxUtils.getContext());
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__SE_FILE_CONTEXT, SELinuxUtils.getFileContext(applicationInfo.dataDir));
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__SE_INFO, PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__USER_ID, String.valueOf(PackageUtils.getUserIdForPackage(currentPackageContext)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX_APP__PROFILE_OWNER, PackageUtils.getProfileOwnerPackageNameForUser(currentPackageContext));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX__USER_ID, String.valueOf(PackageUtils.getUserIdForPackage(currentPackageContext)));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_TERMUX__PROFILE_OWNER, PackageUtils.getProfileOwnerPackageNameForUser(currentPackageContext));
}
termuxAppEnvironment = environment;

View File

@ -34,9 +34,9 @@ public class TermuxShellCommandShellEnvironment extends ShellCommandShellEnviron
String.valueOf(TermuxShellManager.getAndIncrementAppShellNumberSinceAppStart()));
} else if (ExecutionCommand.Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) {
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_SHELL_CMD__TERMINAL_SESSION_NUMBER_SINCE_BOOT,
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_SHELL_CMD__APP_TERMINAL_SESSION_NUMBER_SINCE_BOOT,
String.valueOf(preferences.getAndIncrementTerminalSessionNumberSinceBoot()));
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_SHELL_CMD__TERMINAL_SESSION_NUMBER_SINCE_APP_START,
ShellEnvironmentUtils.putToEnvIfSet(environment, ENV_SHELL_CMD__APP_TERMINAL_SESSION_NUMBER_SINCE_APP_START,
String.valueOf(TermuxShellManager.getAndIncrementTerminalSessionNumberSinceAppStart()));
} else {
return environment;

View File

@ -1,9 +1,12 @@
package com.termux.shared.termux.shell.command.environment;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import androidx.annotation.NonNull;
import com.termux.shared.android.PackageUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
@ -71,12 +74,25 @@ public class TermuxShellEnvironment extends AndroidShellEnvironment {
if (termuxAppEnvironment != null)
environment.putAll(termuxAppEnvironment);
/*
HashMap<String, String> termuxApiAppEnvironment = TermuxAPIShellEnvironment.getEnvironment(currentPackageContext);
if (termuxApiAppEnvironment != null)
environment.putAll(termuxApiAppEnvironment);
*/
ApplicationInfo applicationInfo = PackageUtils.getApplicationInfoForPackage(currentPackageContext, TermuxConstants.TERMUX_PACKAGE_NAME);
if (applicationInfo != null && !applicationInfo.enabled) {
applicationInfo = null;
}
if (applicationInfo != null) {
environment.put("TERMUX__APPS_DIR", applicationInfo.dataDir + "/termux/apps");
}
environment.put("TERMUX__ROOTFS", TermuxConstants.TERMUX_FILES_DIR_PATH);
environment.put(ENV_HOME, TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.put("TERMUX__HOME", TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.put(ENV_PREFIX, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.put("TERMUX__PREFIX", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
// If failsafe is not enabled, then we keep default PATH and TMPDIR so that system binaries can be used
if (!isFailSafe) {