Compare commits

..

40 Commits

Author SHA1 Message Date
agnostic-apollo
5b657c6adf Release: v0.118.3 2025-05-23 02:53:15 +05:00
agnostic-apollo
4f033b3445 Fixed: Add explicit serialVersionUID to Serializable classes like ReportInfo and TextIOInfo
Reading `ReportInfo` with `Bundle.getSerializable()` by `ReportActivity` is triggering exception when default algorithm is used for `serialVersionUID` in Termux:API plugin app when error notification created in `ResultReturner.returnData()` by `TermuxPluginUtils.sendPluginCommandErrorNotification()` is clicked.

```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.termux/com.termux.shared.activities.ReportActivity}: android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object (name = com.termux.shared.models.ReportInfo)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4280)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4467)
	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:222)
	at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133)
	at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2823)
	at android.os.Handler.dispatchMessage(Handler.java:110)
	at android.os.Looper.loopOnce(Looper.java:248)
	at android.os.Looper.loop(Looper.java:338)
	at android.app.ActivityThread.main(ActivityThread.java:9067)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932)
Caused by: android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object (name = com.termux.shared.models.ReportInfo)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5520)
	at android.os.Parcel.readValue(Parcel.java:5038)
	at android.os.Parcel.readValue(Parcel.java:4702)
	at android.os.Parcel.-$$Nest$mreadValue(Unknown Source:0)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4811)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4764)
	at android.os.BaseBundle.unwrapLazyValueFromMapLocked(BaseBundle.java:446)
	at android.os.BaseBundle.getValueAt(BaseBundle.java:426)
	at android.os.BaseBundle.getValue(BaseBundle.java:397)
	at android.os.BaseBundle.getValue(BaseBundle.java:380)
	at android.os.BaseBundle.getValue(BaseBundle.java:373)
	at android.os.BaseBundle.getSerializable(BaseBundle.java:1522)
	at android.os.Bundle.getSerializable(Bundle.java:1339)
	at com.termux.shared.activities.ReportActivity.updateUI(ReportActivity.java:140)
	at com.termux.shared.activities.ReportActivity.onCreate(ReportActivity.java:93)
	at android.app.Activity.performCreate(Activity.java:9155)
	at android.app.Activity.performCreate(Activity.java:9133)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1521)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4262)
	... 13 more
Caused by: java.io.InvalidClassException: com.termux.shared.models.ReportInfo; local class incompatible: stream classdesc serialVersionUID = -5165426368218339031, local class serialVersionUID = 1
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:652)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1743)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1624)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1902)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1442)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5507)
	... 31 more

```

If using release APK with obfuscation enabled, then following exception will be triggered.

```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.termux/com.termux.shared.activities.ReportActivity}: android.os.BadParcelableException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = I0.a)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3864)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4006)
	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:111)
	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2462)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loopOnce(Looper.java:240)
	at android.os.Looper.loop(Looper.java:351)
	at android.app.ActivityThread.main(ActivityThread.java:8377)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)
Caused by: android.os.BadParcelableException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = I0.a)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5113)
	at android.os.Parcel.readValue(Parcel.java:4655)
	at android.os.Parcel.readValue(Parcel.java:4363)
	at android.os.Parcel.-$$Nest$mreadValue(Unknown Source:0)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4461)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4420)
	at android.os.BaseBundle.getValueAt(BaseBundle.java:394)
	at android.os.BaseBundle.getValue(BaseBundle.java:374)
	at android.os.BaseBundle.getValue(BaseBundle.java:357)
	at android.os.BaseBundle.getValue(BaseBundle.java:350)
	at android.os.BaseBundle.getSerializable(BaseBundle.java:1451)
	at android.os.Bundle.getSerializable(Bundle.java:1144)
	at com.termux.shared.activities.ReportActivity.updateUI(ReportActivity.java:136)
	at com.termux.shared.activities.ReportActivity.onCreate(ReportActivity.java:89)
	at android.app.Activity.performCreate(Activity.java:8397)
	at android.app.Activity.performCreate(Activity.java:8370)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1403)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3837)
	... 12 more
Caused by: java.lang.ClassNotFoundException: I0.a
	at java.lang.Class.classForName(Native Method)
	at java.lang.Class.forName(Class.java:536)
	at android.os.Parcel$2.resolveClass(Parcel.java:5090)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1733)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1624)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1902)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1442)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5096)
	... 29 more
Caused by: java.lang.ClassNotFoundException: I0.a
	... 38 more
```

Related issue https://github.com/termux/termux-api/issues/762
2025-05-23 02:48:30 +05:00
Wang Han
fda17afaa0 Changed|Fixed: Bump org.lsposed.hiddenapibypass:hiddenapibypass to 6.1 to fix crash on Android 16 QPR1
```
Build fingerprint: 'google/shiba_beta/shiba:16/BP31.250502.008/13497110:user/release-keys'
Revision: 'MP1.0'
ABI: 'arm64'
Executable: /system/bin/app_process64
Cmdline: com.termux
pid: 22617, tid: 22617, name: com.termux  >>> com.termux <<<
uid: 10323
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
esr: 0000000092000006 (Data Abort Exception 0x24)
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x000000000000000c
Cause: null pointer dereference
    x0  0000000070b6f798  x1  0000000002159228  x2  000000000000000c  x3  00000000ebad6073
    x4  00000000ebad6074  x5  00000000ebad6075  x6  00000000ebad6076  x7  00000000ebad6077
    x8  00000000ebad6078  x9  00000000ebad6079  x10 00000000ebad607a  x11 00000000ebad607b
    x12 00000000ebad607c  x13 00000000ebad607d  x14 00000000ebad607e  x15 00000000ebad607f
    x16 0000007feda203f0  x17 0000007976776a9c  x18 0000007cd0e44000  x19 b400007b850b8be0
    x20 0000000000000000  x21 b400007b850b8ca0  x22 0000000000000000  x23 00000000021592b8
    x24 000000006413b378  x25 0000000070b99ee0  x26 0000000070b9fc78  x27 0000000000000000
    x28 0000000000000053  x29 0000000070b99ee0
    lr  0000007976776a98  sp  0000007feda203f0  pc  0000007976776b0c  pst 0000000080001000
    esr 0000000092000006
26 total frames
backtrace:
      #00 pc 00000000000c8b0c  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (org.lsposed.hiddenapibypass.HiddenApiBypass.getDeclaredMethods+780)
      #01 pc 00000000000c8dc4  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (org.lsposed.hiddenapibypass.HiddenApiBypass.setHiddenApiExemptions+68)
      #02 pc 00000000000ae560  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.shared.reflection.ReflectionUtils.bypassHiddenAPIReflectionRestrictions+448)
      #03 pc 00000000000a342c  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.shared.android.SELinuxUtils.getContext+108)
      #04 pc 00000000000c0b60  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.shared.termux.shell.command.environment.TermuxAppShellEnvironment.setTermuxAppEnvironment+3216)
      #05 pc 000000000009eafc  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.app.TermuxApplication.onCreate+1596)
      ...
      #22 pc 0000000000105c98  /system/lib64/libandroid_runtime.so (_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)+104) (BuildId: ea93df5e792bbd2e0cbb71b2a7599aaa)
      #23 pc 000000000013121c  /system/lib64/libandroid_runtime.so (android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)+908) (BuildId: ea93df5e792bbd2e0cbb71b2a7599aaa)
      #24 pc 000000000000459c  /system/bin/app_process64 (main+1212) (BuildId: a237cfae6965d7f0b950e5955c73432c)
      #25 pc 000000000006bb88  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+120) (BuildId: eecb0fc2ec8128ec92c091c0160586d1)
```

Related commit https://github.com/LSPosed/AndroidHiddenApiBypass/commit/9efadf06
Related commit https://android-review.googlesource.com/c/platform/libcore/+/3380841
Related commit https://cs.android.com/android/_/android/platform/libcore/+/0dc31afe

Closes #4556
2025-05-23 02:48:30 +05:00
agnostic-apollo
431dc1d9d1 Fixed: Use openjdk11 for jitpack builds of termux libraries
`Unrecognized option: --add-exports=java.base/sun.nio.ch=ALL-UNNAMED`

Related commit 52da00e5
2025-05-23 02:47:41 +05:00
agnostic-apollo
849e6b10de Changed!: Remove TERMUX_APP__BUILD_DATA_DIR as a different naming scheme will be used for build variables and rename TERMUX__ROOTFS to TERMUX__ROOTFS_DIR 2025-05-23 01:38:33 +05:00
agnostic-apollo
2375652891 Fixed: Declare MANAGE_EXTERNAL_STORAGE as it being added in Termux:API version 0.51.0 is revoking the storage permission of Termux app after updates and sometimes later
Related issue #4486
2025-05-23 01:37:35 +05:00
agnostic-apollo
21c0d51fb6 Release: v0.118.2 2025-03-29 09:27:24 +05:00
agnostic-apollo
a8f8be18cb Changed: Bump apt-android-7 bootstraps to 2025.03.28-r1 2025-03-29 09:27:24 +05:00
agnostic-apollo
8f156dc58f 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:24 +05:00
agnostic-apollo
2522c62173 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:20 +05:00
agnostic-apollo
7f632cf4d2 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:20 +05:00
agnostic-apollo
999f10d7c7 Added: Add package SE_PROCESS_CONTEXT, SE_FILE_CONTEXT and SE_INFO when generating app info markdown string 2025-03-29 03:24:19 +05:00
agnostic-apollo
3aa986cf7b 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-29 03:24:19 +05:00
agnostic-apollo
b7d2a4b663 Added: Export additional primary TERMUX_* scoped shell environment variables
- TERMUX__ROOTFS
- TERMUX__HOME
- TERMUX__PREFIX
- TERMUX_APP__DATA_DIR (Actual app data directory path assigned by Android for the `com.termux` app returned by `ApplicationInfo`)
- 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__SE_PROCESS_CONTEXT (SeLinux process context for current process)
- TERMUX_APP__SE_FILE_CONTEXT (SeLinux file context for the `TERMUX_APP__DATA_DIR`)
- TERMUX_APP__SE_INFO (Termux app seInfo tag found in selinux policy used to set app process and app data files selinux context)

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:19 +05:00
agnostic-apollo
8440a72805 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:24 +05:00
agnostic-apollo
0cdbe9869e Added: Add functions to PackageUtils to get seInfo and seInfoUser of package 2025-03-28 11:10:42 +05:00
agnostic-apollo
f84853fc30 Added: Add SELinuxUtils to get process and file paths security contexts 2025-03-28 11:04:31 +05:00
agnostic-apollo
fcc3a4d951 Added: Add ReflectionUtils and add dependency for org.lsposed.hiddenapibypass:hiddenapibypass
The call to bypassHiddenAPIReflectionRestrictions() must be made before trying to reflect hidden or non-sdk APIs.

Reflection will be used for accessing hidden (@hide) APIs by Termux and its plugins later.

https://github.com/LSPosed/AndroidHiddenApiBypass
https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
2025-03-28 11:04:31 +05:00
Johannes Altmanninger
3f2375029e 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 10:06:50 +05:00
agnostic-apollo
e5b578be55 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 10:06:50 +05:00
agnostic-apollo
c09d867b94 Fixed: Fix tests added in b84dc703 2025-03-28 10:06:49 +05:00
Jason Yu
49018f6d91 Added: Add empty and null strings tests for invalid urls to FileReceiverActivityTest 2025-03-28 10:06:49 +05:00
Fredrik Fornwall
0415681d53 Fixed: Implement colon separated CSI parameters 2025-03-28 10:06:49 +05:00
Evgeny Zhabotinsky
4baf12bae3 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 10:06:49 +05:00
Tom Kranz
c5f000a162 Added: Basic MIME type recognition in ContentProvider 2025-03-28 10:06:49 +05:00
Matan Ziv-Av
c154407624 Fixed: Use Canvas.drawTextRun instead of drawText
drawText does (very) basic BiDi, which causes inconsistent behaviour.
This ensures everything is LtR.
2025-03-28 10:06:49 +05:00
Fredrik Fornwall
254e885fcb 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 10:06:49 +05:00
Artem Chepurnyi
ee9b31a0ee 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 10:06:49 +05:00
Dvd-Znf
68865fb148 Update latest version in README.md to v0.118.1 2025-03-28 10:06:48 +05:00
Krunal Patel
bb7fa88fb3 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 10:06:48 +05:00
Fredrik Fornwall
f35063da74 Fixed: Parse (but ignore for now) terminal APC sequences 2025-03-28 10:06:48 +05:00
agnostic-apollo
043923e674 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 10:06:48 +05:00
agnostic-apollo
2d38a1ca6e Added: Add support for Share selected text of terminal in long hold MORE menu so that users don't have to copy and paste to move text between apps 2025-03-28 10:06:48 +05:00
agnostic-apollo
096464d2af Changed: Use ShareUtils to copy and paste text and prevent potential NPE
The `copyTextToClipboard()` method has been updated to pass clip label when copying text to clipboard and `getTextFromClipboard()` and `getTextStringFromClipboardIfSet()` methods have been added to get current clipboard.
2025-03-28 10:06:48 +05:00
agnostic-apollo
b48f788a94 Changed: Share terminal transcript with ShareUtils 2025-03-28 10:06:48 +05:00
Josh Triplett
915e053a10 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:37:45 +05:00
agnostic-apollo
791bf5f88a Fixed: Fix SHIFT+PAGE_UP and SHIFT+PAGE_DOWN behaviour to scroll 1 line of scrollback history instead of scrolling command history or changing pages
This will work for both `SHIFT` extra key and hardware keyboards. The `SHIFT` extra key can be long held to lock it in an enabled state and `PGUP` and `PGDN` keys can be long held to repeat scrolling.

Closes #867
2025-03-28 09:37:35 +05:00
agnostic-apollo
fa710c47e9 Changed: Add PGUP and PGDN extra keys to repetitive keys so that long holding them triggers page scrolling instead of having to repeatedly press the key to change pages 2025-03-28 09:37:29 +05:00
Fredrik Fornwall
5b5e71949b 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:31:54 +05:00
Fredrik Fornwall
f3e73880c9 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:31:54 +05:00
38 changed files with 1334 additions and 207 deletions

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`.
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).

View File

@@ -30,8 +30,8 @@ android {
applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 1000
versionName "0.118.1"
versionCode 1002
versionName "0.118.3"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
@@ -195,11 +195,11 @@ clean {
task downloadBootstraps() {
doLast {
def version = "2024.06.17-r1+apt-android-7"
downloadBootstrap("aarch64", "91a90661597fe14bb3c3563f5f65b243c0baaec42f2bc3d2243ff459e3942fb6", version)
downloadBootstrap("arm", "d54b5eb2a305d72f267f9704deaca721b2bebbd3d4cca134aec31da719707997", version)
downloadBootstrap("i686", "06a51ac1c679d68d52045509f1a705622c8f41748ef753660e31e3b6a846eba2", version)
downloadBootstrap("x86_64", "4c8e43474c8d9543e01d4cbf3c4d7f59bbe4d696c38f6dece2b6ab3ba8881f2e", version)
def version = "2025.03.28-r1+apt-android-7"
downloadBootstrap("aarch64", "c8d702b6f742935001c37cda81b8ac69504a95d5cf28f2899532dd8cd4b057eb", version)
downloadBootstrap("arm", "f3bb9d1b32552b34fff41861dbf193ec5ba2848d67d779ac1c7256da6640f85d", version)
downloadBootstrap("i686", "36db3e1ac3547f9a174fd763bd9a484fa1a3449cdd81e1cf2408ff0454f839c6", version)
downloadBootstrap("x86_64", "1c124ec2396ee70a51b0b0a574f29aa659526aa2b9f558f993b2fb05d1e51855", version)
}
}

View File

@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

View File

@@ -14,7 +14,6 @@ import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -25,7 +24,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
@@ -36,6 +34,7 @@ import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.app.activities.HelpActivity;
@@ -162,7 +161,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10;
private static final int CONTEXT_MENU_AUTOFILL_USERNAME = 11;
private static final int CONTEXT_MENU_AUTOFILL_PASSWORD = 2;
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
private static final int CONTEXT_MENU_STYLING_ID = 5;
@@ -587,17 +588,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TerminalSession currentSession = getCurrentSession();
if (currentSession == null) return;
boolean addAutoFillMenu = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
addAutoFillMenu = true;
}
}
boolean autoFillEnabled = mTerminalView.isAutoFillEnabled();
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText()))
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text);
if (autoFillEnabled)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_USERNAME, Menu.NONE, R.string.action_autofill_username);
if (autoFillEnabled)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_PASSWORD, Menu.NONE, R.string.action_autofill_password);
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
@@ -625,8 +625,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
mTermuxTerminalViewClient.shareSessionTranscript();
return true;
case CONTEXT_MENU_AUTOFILL_ID:
requestAutoFill();
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
mTermuxTerminalViewClient.shareSelectedText();
return true;
case CONTEXT_MENU_AUTOFILL_USERNAME:
mTerminalView.requestAutoFillUsername();
return true;
case CONTEXT_MENU_AUTOFILL_PASSWORD:
mTerminalView.requestAutoFillPassword();
return true;
case CONTEXT_MENU_RESET_TERMINAL_ID:
onResetTerminalSession(session);
@@ -654,6 +660,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
@Override
public void onContextMenuClosed(Menu menu) {
super.onContextMenuClosed(menu);
// onContextMenuClosed() is triggered twice if back button is pressed to dismiss instead of tap for some reason
mTerminalView.onContextMenuClosed(menu);
}
private void showKillSessionDialog(TerminalSession session) {
if (session == null) return;
@@ -700,15 +713,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
private void requestAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
autofillManager.requestAutofill(mTerminalView);
}
}
}
/**

View File

@@ -212,24 +212,30 @@ final class TermuxInstaller {
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
}
// Run Termux bootstrap second stage
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
// Run Termux bootstrap second stage.
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
if (FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
if (!FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since script not found at \"" + termuxBootstrapSecondStageFile + "\" path.");
} else {
if (!FileUtils.fileExists(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", true)) {
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since bash not found.");
}
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
ExecutionCommand executionCommand = new ExecutionCommand(-1,
termuxBootstrapSecondStageFile, null, null,
null, true, false);
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
TermuxTask termuxTask = TermuxTask.execute(activity, executionCommand, null, new TermuxShellEnvironmentClient(), true);
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0 || stderrSet) {
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
// Generate debug report before deleting broken prefix directory to get `stat` info at time of failure.
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into.
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null)
Logger.logErrorExtended(LOG_TAG, error.toString());
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
return;
}
}

View File

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

View File

@@ -15,6 +15,7 @@ import android.widget.ListView;
import com.termux.R;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.app.TermuxActivity;
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.termux.TermuxConstants;
@@ -177,20 +178,16 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
public void onCopyTextToClipboard(TerminalSession session, String text) {
if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
ShareUtils.copyTextToClipboard(mActivity, text);
}
@Override
public void onPasteTextFromClipboard(TerminalSession session) {
if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste)) mActivity.getTerminalView().mEmulator.paste(paste.toString());
}
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
mActivity.getTerminalView().mEmulator.paste(text);
}
@Override

View File

@@ -6,7 +6,6 @@ import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Environment;
@@ -647,17 +646,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
try {
// See https://github.com/termux/termux-app/issues/1166.
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
}
// See https://github.com/termux/termux-app/issues/1166.
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
}
public void shareSelectedText() {
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
if (DataUtils.isNullOrEmpty(selectedText)) return;
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
}
public void showUrlSelection() {
@@ -678,9 +677,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
String url = (String) urls[which];
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
}).setTitle(R.string.title_select_url_dialog).create();
// Long press to open URL:
@@ -756,12 +753,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (session == null) return;
if (!session.isRunning()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste))
session.getEmulator().paste(paste.toString());
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
session.getEmulator().paste(text);
}
}

View File

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

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

@@ -65,6 +65,11 @@
<string name="title_share_transcript">Terminal transcript</string>
<string name="title_share_transcript_with">Send transcript to:</string>
<string name="action_share_selected_text">Share selected text</string>
<string name="title_share_selected_text">Terminal Text</string>
<string name="title_share_selected_text_with">Send selected text to:</string>
<string name="action_autofill_username">Autofill username</string>
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>

View File

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

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=24

View File

@@ -1,2 +1,4 @@
jdk:
- openjdk11
env:
JITPACK_NDK_VERSION: "21.1.6352462"

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
@@ -312,13 +322,15 @@ public final class TerminalEmulator {
}
}
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
public TerminalEmulator(TerminalOutput session, int columns, int rows, int cellWidthPixels, int cellHeightPixels, Integer transcriptRows, TerminalSessionClient client) {
mSession = session;
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
mAltBuffer = new TerminalBuffer(columns, rows, rows);
mClient = client;
mRows = rows;
mColumns = columns;
mCellWidthPixels = cellWidthPixels;
mCellHeightPixels = cellHeightPixels;
mTabStop = new boolean[mColumns];
reset();
}
@@ -368,7 +380,10 @@ public final class TerminalEmulator {
}
}
public void resize(int columns, int rows) {
public void resize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
this.mCellWidthPixels = cellWidthPixels;
this.mCellHeightPixels = cellHeightPixels;
if (mRows == rows && mColumns == columns) {
return;
} else if (columns < 2 || rows < 2) {
@@ -550,6 +565,15 @@ public final class TerminalEmulator {
}
public void processCodePoint(int b) {
// The Application Program-Control (APC) string might be arbitrary non-printable characters, so handle that early.
if (mEscapeState == ESC_APC) {
doApc(b);
return;
} else if (mEscapeState == ESC_APC_ESCAPE) {
doApcEscape(b);
return;
}
switch (b) {
case 0: // Null character (NUL, ^@). Do nothing.
break;
@@ -635,6 +659,10 @@ public final class TerminalEmulator {
case ESC_CSI:
doCsi(b);
break;
case ESC_CSI_UNSUPPORTED_PARAMETER_BYTE:
case ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE:
doCsiUnsupportedParameterOrIntermediateByte(b);
break;
case ESC_CSI_EXCLAMATION:
if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR).
reset();
@@ -1006,12 +1034,67 @@ public final class TerminalEmulator {
}
}
/**
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
*/
private void doApc(int b) {
if (b == 27) {
continueSequence(ESC_APC_ESCAPE);
}
// Eat APC sequences silently for now.
}
/**
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
*/
private void doApcEscape(int b) {
if (b == '\\') {
// A String Terminator (ST), ending the APC escape sequence.
finishSequence();
} else {
// The Escape character was not the start of a String Terminator (ST),
// but instead just data inside of the APC escape sequence.
continueSequence(ESC_APC);
}
}
private int nextTabStop(int numTabs) {
for (int i = mCursorCol + 1; i < mColumns; i++)
if (mTabStop[i] && --numTabs == 0) return Math.min(i, mRightMargin);
return mRightMargin - 1;
}
/**
* Process byte while in the {@link #ESC_CSI_UNSUPPORTED_PARAMETER_BYTE} or
* {@link #ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE} escape state.
*
* Parse unsupported parameter, intermediate and final bytes but ignore them.
*
* > For Control Sequence Introducer, ... the ESC [ is followed by
* > - any number (including none) of "parameter bytes" in the range 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) {
@@ -1281,6 +1364,7 @@ public final class TerminalEmulator {
mEscapeState = ESC;
mArgIndex = 0;
Arrays.fill(mArgs, -1);
mArgsSubParamsBitSet = 0;
}
private void doLinefeed() {
@@ -1375,8 +1459,8 @@ public final class TerminalEmulator {
// http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal
// position on the preceding line. If the active position is at the top margin, a scroll down is performed".
if (mCursorRow <= mTopMargin) {
mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1);
blockClear(0, mTopMargin, mColumns);
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, mBottomMargin - (mTopMargin + 1), mLeftMargin, mTopMargin + 1);
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin);
} else {
mCursorRow--;
}
@@ -1390,8 +1474,6 @@ public final class TerminalEmulator {
break;
case '[':
continueSequence(ESC_CSI);
mIsCSIStart = true;
mLastCSIArg = null;
break;
case '=': // DECKPAM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
@@ -1403,6 +1485,9 @@ public final class TerminalEmulator {
case '>': // DECKPNM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
break;
case '_': // APC - Application Program Command.
continueSequence(ESC_APC);
break;
default:
unknownSequence(b);
break;
@@ -1584,8 +1669,8 @@ public final class TerminalEmulator {
final int linesToScrollArg = getArg0(1);
final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin;
final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg);
mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll);
blockClear(0, mTopMargin, mColumns, linesToScroll);
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesBetweenTopAndBottomMargins - linesToScroll, mLeftMargin, mTopMargin + linesToScroll);
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesToScroll);
} else {
// "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking.
unimplementedSequence(b);
@@ -1607,12 +1692,16 @@ public final class TerminalEmulator {
}
mCursorCol = newCol;
break;
case '?': // Esc [ ? -- start of a private mode set
case '?': // Esc [ ? -- start of a private parameter byte
continueSequence(ESC_CSI_QUESTIONMARK);
break;
case '>': // "Esc [ >" --
case '>': // "Esc [ >" -- start of a private parameter byte
continueSequence(ESC_CSI_BIGGERTHAN);
break;
case '<': // "Esc [ <" -- start of a private parameter byte
case '=': // "Esc [ =" -- start of a private parameter byte
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
break;
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
setCursorColRespectingOriginMode(getArg0(1) - 1);
break;
@@ -1712,8 +1801,10 @@ public final class TerminalEmulator {
mSession.write("\033[3;0;0t");
break;
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
// We just report characters time 12 here.
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * mCellHeightPixels, mColumns * mCellWidthPixels));
break;
case 16: // Report xterm character cell size in pixels. Result is CSI 6 ; height ; width t
mSession.write(String.format(Locale.US, "\033[6;%d;%dt", mCellHeightPixels, mCellWidthPixels));
break;
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
@@ -1762,7 +1853,12 @@ public final class TerminalEmulator {
private void selectGraphicRendition() {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) {
int code = mArgs[i];
// Skip leading sub parameters:
if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
continue;
}
int code = getArg(i, 0, false);
if (code < 0) {
if (mArgIndex > 0) {
continue;
@@ -1781,7 +1877,19 @@ public final class TerminalEmulator {
} else if (code == 3) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
} else if (code == 4) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
// Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
i++;
if (mArgs[i] == 0) {
// No underline.
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
} else {
// Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
}
} else {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
}
} else if (code == 5) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
} else if (code == 7) {
@@ -1810,8 +1918,8 @@ public final class TerminalEmulator {
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
} else if (code >= 30 && code <= 37) {
mForeColor = code - 30;
} else if (code == 38 || code == 48) {
// Extended set foreground(38)/background (48) color.
} else if (code == 38 || code == 48 || code == 58) {
// Extended set foreground(38)/background(48)/underline(58) color.
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
// "5;$INDEX" to set an indexed color.
if (i + 2 > mArgIndex) continue;
@@ -1820,27 +1928,30 @@ public final class TerminalEmulator {
if (i + 4 > mArgIndex) {
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else {
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
int red = getArg(i + 2, 0, false);
int green = getArg(i + 3, 0, false);
int blue = getArg(i + 4, 0, false);
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
} else {
int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
if (code == 38) {
mForeColor = argbColor;
} else {
mBackColor = argbColor;
int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
switch (code) {
case 38: mForeColor = argbColor; break;
case 48: mBackColor = argbColor; break;
case 58: mUnderlineColor = argbColor; break;
}
}
i += 4; // "2;P_r;P_g;P_r"
}
} else if (firstArg == 5) {
int color = mArgs[i + 2];
int color = getArg(i + 2, 0, false);
i += 2; // "5;P_s"
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
if (code == 38) {
mForeColor = color;
} else {
mBackColor = color;
switch (code) {
case 38: mForeColor = color; break;
case 48: mBackColor = color; break;
case 58: mUnderlineColor = color; break;
}
} else {
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
@@ -1854,6 +1965,8 @@ public final class TerminalEmulator {
mBackColor = code - 40;
} else if (code == 49) { // Set default background color.
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (code == 59) { // Set default underline color.
mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
mForeColor = code - 90 + 8;
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
@@ -2089,67 +2202,64 @@ public final class TerminalEmulator {
private void scrollDownOneLine() {
mScrollCounter++;
long currentStyle = getStyle();
if (mLeftMargin != 0 || mRightMargin != mColumns) {
// Horizontal margin: Do not put anything into scroll history, just non-margin part of screen up.
mScreen.blockCopy(mLeftMargin, mTopMargin + 1, mRightMargin - mLeftMargin, mBottomMargin - mTopMargin - 1, mLeftMargin, mTopMargin);
// .. and blank bottom row between margins:
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', mEffect);
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', currentStyle);
} else {
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, getStyle());
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, currentStyle);
}
}
/**
* Process the next ASCII character of a parameter.
*
* Parameter characters modify the action or interpretation of the sequence. You can use up to
* 16 parameters per sequence. You must use the ; character to separate parameters.
* All parameters are unsigned, positive decimal integers, with the most significant
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
*
* <p>Parameter characters modify the action or interpretation of the sequence. Originally
* you can use up to 16 parameters per sequence, but following at least xterm and alacritty
* we use a common space for parameters and sub-parameters, allowing 32 in total.
*
* <p>All parameters are unsigned, positive decimal integers, with the most significant
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
* or omitted parameter indicates a default value for the sequence. For most
* sequences, the default value is 1.
*
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
* <p>References:
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
* */
private void parseArg(int inputByte) {
int[] bytes = new int[]{inputByte};
// Only doing this for ESC_CSI and not for other ESC_CSI_* since they seem to be using their
// own defaults with getArg*() calls, but there may be missed cases
if (mEscapeState == ESC_CSI) {
if ((mIsCSIStart && inputByte == ';') || // If sequence starts with a ; character, like \033[;m
(!mIsCSIStart && mLastCSIArg != null && mLastCSIArg == ';' && inputByte == ';')) { // If sequence contains sequential ; characters, like \033[;;m
bytes = new int[]{'0', ';'}; // Assume 0 was passed
private void parseArg(int b) {
if (b >= '0' && b <= '9') {
if (mArgIndex < mArgs.length) {
int oldValue = mArgs[mArgIndex];
int thisDigit = b - '0';
int value;
if (oldValue >= 0) {
value = oldValue * 10 + thisDigit;
} else {
value = thisDigit;
}
if (value > 9999)
value = 9999;
mArgs[mArgIndex] = value;
}
}
mIsCSIStart = false;
for (int b : bytes) {
if (b >= '0' && b <= '9') {
if (mArgIndex < mArgs.length) {
int oldValue = mArgs[mArgIndex];
int thisDigit = b - '0';
int value;
if (oldValue >= 0) {
value = oldValue * 10 + thisDigit;
} else {
value = thisDigit;
}
if (value > 9999)
value = 9999;
mArgs[mArgIndex] = value;
continueSequence(mEscapeState);
} else if (b == ';' || b == ':') {
if (mArgIndex + 1 < mArgs.length) {
mArgIndex++;
if (b == ':') {
mArgsSubParamsBitSet |= 1 << mArgIndex;
}
continueSequence(mEscapeState);
} else if (b == ';') {
if (mArgIndex < mArgs.length) {
mArgIndex++;
}
continueSequence(mEscapeState);
} else {
unknownSequence(b);
logError("Too many parameters when in state: " + mEscapeState);
}
mLastCSIArg = b;
continueSequence(mEscapeState);
} else {
unknownSequence(b);
}
}

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];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);

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

@@ -2,6 +2,7 @@ package com.termux.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -10,6 +11,7 @@ import android.graphics.Typeface;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
@@ -19,17 +21,20 @@ import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.termux.terminal.KeyHandler;
@@ -81,6 +86,43 @@ public final class TerminalView extends View {
/** If non-zero, this is the last unicode code point received if that was a combining character. */
int mCombiningAccent;
/**
* The current AutoFill type returned for {@link View#getAutofillType()} by {@link #getAutofillType()}.
*
* The default is {@link #AUTOFILL_TYPE_NONE} so that AutoFill UI, like toolbar above keyboard
* is not shown automatically, like on Activity starts/View create. This value should be updated
* to required value, like {@link #AUTOFILL_TYPE_TEXT} before calling
* {@link AutofillManager#requestAutofill(View)} so that AutoFill UI shows. The updated value
* set will automatically be restored to {@link #AUTOFILL_TYPE_NONE} in
* {@link #autofill(AutofillValue)} so that AutoFill UI isn't shown anymore by calling
* {@link #resetAutoFill()}.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int mAutoFillType = AUTOFILL_TYPE_NONE;
/**
* The current AutoFill type returned for {@link View#getImportantForAutofill()} by
* {@link #getImportantForAutofill()}.
*
* The default is {@link #IMPORTANT_FOR_AUTOFILL_NO} so that view is not considered important
* for AutoFill. This value should be updated to required value, like
* {@link #IMPORTANT_FOR_AUTOFILL_YES} before calling {@link AutofillManager#requestAutofill(View)}
* so that Android and apps consider the view as important for AutoFill to process the request.
* The updated value set will automatically be restored to {@link #IMPORTANT_FOR_AUTOFILL_NO} in
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
/**
* The current AutoFill hints returned for {@link View#getAutofillHints()} ()} by {@link #getAutofillHints()} ()}.
*
* The default is an empty `string[]`. This value should be updated to required value. The
* updated value set will automatically be restored an empty `string[]` in
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
*/
private String[] mAutoFillHints = new String[0];
private final boolean mAccessibilityEnabled;
private static final String LOG_TAG = "TerminalView";
@@ -440,6 +482,14 @@ public final class TerminalView extends View {
if (mAccessibilityEnabled) setContentDescription(getText());
}
/** This must be called by the hosting activity in {@link Activity#onContextMenuClosed(Menu)}
* when context menu for the {@link TerminalView} is started by
* {@link TextSelectionCursorController#ACTION_MORE} is closed. */
public void onContextMenuClosed(Menu menu) {
// Unset the stored text since it shouldn't be used anymore and should be cleared from memory
unsetStoredSelectedText();
}
/**
* Sets the text size, which in turn sets the number of rows and columns.
*
@@ -550,11 +600,14 @@ public final class TerminalView extends View {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
ClipData.Item clipItem = clipData.getItemAt(0);
if (clipItem != null) {
CharSequence text = clipItem.coerceToText(getContext());
if (!TextUtils.isEmpty(text)) mEmulator.paste(text.toString());
}
}
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
switch (event.getAction()) {
@@ -578,6 +631,7 @@ public final class TerminalView extends View {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
cancelRequestAutoFill();
if (isSelectingText()) {
stopTextSelectionMode();
return true;
@@ -839,6 +893,9 @@ public final class TerminalView extends View {
if (mEmulator != null)
mEmulator.setCursorBlinkState(true);
if (handleKeyCodeAction(keyCode, keyMod))
return true;
TerminalEmulator term = mTermSession.getEmulator();
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
if (code == null) return false;
@@ -846,6 +903,26 @@ public final class TerminalView extends View {
return true;
}
public boolean handleKeyCodeAction(int keyCode, int keyMod) {
boolean shiftDown = (keyMod & KeyHandler.KEYMOD_SHIFT) != 0;
switch (keyCode) {
case KeyEvent.KEYCODE_PAGE_UP:
case KeyEvent.KEYCODE_PAGE_DOWN:
// shift+page_up and shift+page_down should scroll scrollback history instead of
// scrolling command history or changing pages
if (shiftDown) {
long time = SystemClock.uptimeMillis();
MotionEvent motionEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -mEmulator.mRows : mEmulator.mRows);
motionEvent.recycle();
return true;
}
}
return false;
}
/**
* Called when a key is released in the view.
*
@@ -893,7 +970,7 @@ public final class TerminalView extends View {
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows);
mTermSession.updateSize(newColumns, newRows, (int) mRenderer.getFontWidth(), mRenderer.getFontLineSpacing());
mEmulator = mTermSession.getEmulator();
mClient.onEmulatorSet();
@@ -971,12 +1048,20 @@ public final class TerminalView extends View {
if (value.isText()) {
mTermSession.write(value.getTextValue().toString());
}
resetAutoFill();
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getAutofillType() {
return AUTOFILL_TYPE_TEXT;
return mAutoFillType;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public String[] getAutofillHints() {
return mAutoFillHints;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -985,6 +1070,95 @@ public final class TerminalView extends View {
return AutofillValue.forText("");
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getImportantForAutofill() {
return mAutoFillImportance;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private synchronized void resetAutoFill() {
// Restore none type so that AutoFill UI isn't shown anymore.
mAutoFillType = AUTOFILL_TYPE_NONE;
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
mAutoFillHints = new String[0];
}
public AutofillManager getAutoFillManagerService() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
try {
Context context = getContext();
if (context == null) return null;
return context.getSystemService(AutofillManager.class);
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to get AutofillManager service", e);
return null;
}
}
public boolean isAutoFillEnabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
try {
AutofillManager autofillManager = getAutoFillManagerService();
return autofillManager != null && autofillManager.isEnabled();
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to check if Autofill is enabled", e);
return false;
}
}
public synchronized void requestAutoFillUsername() {
requestAutoFill(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_USERNAME} :
null);
}
public synchronized void requestAutoFillPassword() {
requestAutoFill(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_PASSWORD} :
null);
}
public synchronized void requestAutoFill(String[] autoFillHints) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
if (autoFillHints == null || autoFillHints.length < 1) return;
try {
AutofillManager autofillManager = getAutoFillManagerService();
if (autofillManager != null && autofillManager.isEnabled()) {
// Update type that will be returned by `getAutofillType()` so that AutoFill UI is shown.
mAutoFillType = AUTOFILL_TYPE_TEXT;
// Update importance that will be returned by `getImportantForAutofill()` so that
// AutoFill considers the view as important.
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_YES;
// Update hints that will be returned by `getAutofillHints()` for which to show AutoFill UI.
mAutoFillHints = autoFillHints;
autofillManager.requestAutofill(this);
}
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to request Autofill", e);
}
}
public synchronized void cancelRequestAutoFill() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
if (mAutoFillType == AUTOFILL_TYPE_NONE) return;
try {
AutofillManager autofillManager = getAutoFillManagerService();
if (autofillManager != null && autofillManager.isEnabled()) {
resetAutoFill();
autofillManager.cancel();
}
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to cancel Autofill request", e);
}
}
/**
@@ -1184,6 +1358,25 @@ public final class TerminalView extends View {
}
}
/** Get the currently selected text if selecting. */
public String getSelectedText() {
if (isSelectingText() && mTextSelectionCursorController != null)
return mTextSelectionCursorController.getSelectedText();
else
return null;
}
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
@Nullable
public String getStoredSelectedText() {
return mTextSelectionCursorController != null ? mTextSelectionCursorController.getStoredSelectedText() : null;
}
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
public void unsetStoredSelectedText() {
if (mTextSelectionCursorController != null) mTextSelectionCursorController.unsetStoredSelectedText();
}
private ActionMode getTextSelectionActionMode() {
if (mTextSelectionCursorController != null) {
return mTextSelectionCursorController.getActionMode();

View File

@@ -12,6 +12,8 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.WcWidth;
import com.termux.view.R;
@@ -21,6 +23,7 @@ public class TextSelectionCursorController implements CursorController {
private final TerminalView terminalView;
private final TextSelectionHandleView mStartHandle, mEndHandle;
private String mStoredSelectedText;
private boolean mIsSelectingText = false;
private long mShowStartTime = System.currentTimeMillis();
@@ -28,9 +31,9 @@ public class TextSelectionCursorController implements CursorController {
private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
private ActionMode mActionMode;
private final int ACTION_COPY = 1;
private final int ACTION_PASTE = 2;
private final int ACTION_MORE = 3;
public final int ACTION_COPY = 1;
public final int ACTION_PASTE = 2;
public final int ACTION_MORE = 3;
public TextSelectionCursorController(TerminalView terminalView) {
this.terminalView = terminalView;
@@ -113,7 +116,7 @@ public class TextSelectionCursorController implements CursorController {
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard != null && clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, R.string.text_selection_more);
return true;
}
@@ -132,7 +135,7 @@ public class TextSelectionCursorController implements CursorController {
switch (item.getItemId()) {
case ACTION_COPY:
String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
String selectedText = getSelectedText();
terminalView.mTermSession.onCopyTextToClipboard(selectedText);
terminalView.stopTextSelectionMode();
break;
@@ -141,7 +144,13 @@ public class TextSelectionCursorController implements CursorController {
terminalView.mTermSession.onPasteTextFromClipboard();
break;
case ACTION_MORE:
terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup
// We first store the selected text in case TerminalViewClient needs the
// selected text before MORE button was pressed since we are going to
// stop selection mode
mStoredSelectedText = getSelectedText();
// The text selection needs to be stopped before showing context menu,
// otherwise handles will show above popup
terminalView.stopTextSelectionMode();
terminalView.showContextMenu();
break;
}
@@ -356,6 +365,22 @@ public class TextSelectionCursorController implements CursorController {
sel[3] = mSelX2;
}
/** Get the currently selected text. */
public String getSelectedText() {
return terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2);
}
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
@Nullable
public String getStoredSelectedText() {
return mStoredSelectedText;
}
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
public void unsetStoredSelectedText() {
mStoredSelectedText = null;
}
public ActionMode getActionMode() {
return mActionMode;
}

View File

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

View File

@@ -0,0 +1,96 @@
package com.termux.shared.android;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.reflection.ReflectionUtils;
import java.lang.reflect.Method;
public class SELinuxUtils {
public static final String ANDROID_OS_SELINUX_CLASS = "android.os.SELinux";
private static final String LOG_TAG = "SELinuxUtils";
/**
* Gets the security context of the current process.
*
* @return Returns a {@link String} representing the security context of the current process.
* This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getContext() {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
String methodName = "getContext";
try {
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName);
if (method == null) {
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
return null;
}
return (String) ReflectionUtils.invokeMethod(method, null).value;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
return null;
}
}
/**
* Get the security context of a given process id.
*
* @param pid The pid of process.
* @return Returns a {@link String} representing the security context of the given pid.
* This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getPidContext(int pid) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
String methodName = "getPidContext";
try {
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, int.class);
if (method == null) {
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
return null;
}
return (String) ReflectionUtils.invokeMethod(method, null, pid).value;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
return null;
}
}
/**
* Get the security context of a file object.
*
* @param path The pathname of the file object.
* @return Returns a {@link String} representing the security context of the file.
* This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getFileContext(@NonNull String path) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
String methodName = "getFileContext";
try {
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, String.class);
if (method == null) {
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
return null;
}
return (String) ReflectionUtils.invokeMethod(method, null, path).value;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
return null;
}
}
}

View File

@@ -11,7 +11,6 @@ import android.net.Uri;
import android.os.Environment;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
@@ -23,6 +22,8 @@ import com.termux.shared.packages.PermissionUtils;
import java.nio.charset.Charset;
import javax.annotation.Nullable;
public class ShareUtils {
private static final String LOG_TAG = "ShareUtils";
@@ -56,6 +57,18 @@ public class ShareUtils {
* @param text The text to share.
*/
public static void shareText(final Context context, final String subject, final String text) {
shareText(context, subject, text, null);
}
/**
* Share text.
*
* @param context The context for operations.
* @param subject The subject for sharing.
* @param text The text to share.
* @param title The title for share menu.
*/
public static void shareText(final Context context, final String subject, final String text, @Nullable final String title) {
if (context == null || text == null) return;
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
@@ -63,29 +76,85 @@ public class ShareUtils {
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false));
openSystemAppChooser(context, shareTextIntent, context.getString(R.string.title_share_with));
openSystemAppChooser(context, shareTextIntent, DataUtils.isNullOrEmpty(title) ? context.getString(R.string.title_share_with) : title);
}
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel` and `toastString`. */
public static void copyTextToClipboard(Context context, final String text) {
copyTextToClipboard(context, null, text, null);
}
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel`. */
public static void copyTextToClipboard(Context context, final String text, final String toastString) {
copyTextToClipboard(context, null, text, toastString);
}
/**
* Copy the text to clipboard.
* Copy the text to primary clip of the clipboard.
*
* @param context The context for operations.
* @param clipDataLabel The label to show to the user describing the copied text.
* @param text The text to copy.
* @param toastString If this is not {@code null} or empty, then a toast is shown if copying to
* clipboard is successful.
*/
public static void copyTextToClipboard(final Context context, final String text, final String toastString) {
public static void copyTextToClipboard(Context context, @Nullable final String clipDataLabel,
final String text, final String toastString) {
if (context == null || text == null) return;
final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class);
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) return;
if (clipboardManager != null) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false)));
if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true);
}
clipboardManager.setPrimaryClip(ClipData.newPlainText(clipDataLabel,
DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES,
true, false, false)));
if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true);
}
/**
* Wrapper for {@link #getTextFromClipboard(Context, boolean)} that returns primary text {@link String}
* if its set and not empty.
*/
@Nullable
public static String getTextStringFromClipboardIfSet(Context context, boolean coerceToText) {
CharSequence textCharSequence = getTextFromClipboard(context, coerceToText);
if (textCharSequence == null) return null;
String textString = textCharSequence.toString();
return !textString.isEmpty() ? textString : null;
}
/**
* Get the text from primary clip of the clipboard.
*
* @param context The context for operations.
* @param coerceToText Whether to call {@link ClipData.Item#coerceToText(Context)} to coerce
* non-text data to text.
* @return Returns the {@link CharSequence} of primary text. This will be `null` if failed to get it.
*/
@Nullable
public static CharSequence getTextFromClipboard(Context context, boolean coerceToText) {
if (context == null) return null;
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) return null;
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) return null;
ClipData.Item clipItem = clipData.getItemAt(0);
if (clipItem == null) return null;
return coerceToText ? clipItem.coerceToText(context) : clipItem.getText();
}
/**
* Open a url.
*

View File

@@ -1,5 +1,7 @@
package com.termux.shared.models;
import androidx.annotation.Keep;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.AndroidUtils;
@@ -10,6 +12,25 @@ import java.io.Serializable;
*/
public class ReportInfo implements Serializable {
/**
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
*
* Like when calling `Bundle.getSerializable()` on Android.
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
* `java.io.InvalidClassException: <class_name>; local class incompatible`
*
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
* app is compiled, even if its kept during library compilation.
*
* **See Also:**
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
*/
@Keep
private static final long serialVersionUID = 1L;
/** The user action that was being processed for which the report was generated. */
public final String userAction;
/** The internal app component that sent the report. */

View File

@@ -3,6 +3,7 @@ package com.termux.shared.models;
import android.graphics.Color;
import android.graphics.Typeface;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import com.termux.shared.activities.TextIOActivity;
@@ -19,6 +20,25 @@ import java.io.Serializable;
*/
public class TextIOInfo implements Serializable {
/**
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
*
* Like when calling `Bundle.getSerializable()` on Android.
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
* `java.io.InvalidClassException: <class_name>; local class incompatible`
*
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
* app is compiled, even if its kept during library compilation.
*
* **See Also:**
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
*/
@Keep
private static final long serialVersionUID = 1L;
public static final int GENERAL_DATA_SIZE_LIMIT_IN_BYTES = 1000;
public static final int LABEL_SIZE_LIMIT_IN_BYTES = 4000;
public static final int TEXT_SIZE_LIMIT_IN_BYTES = 100000 - GENERAL_DATA_SIZE_LIMIT_IN_BYTES - LABEL_SIZE_LIMIT_IN_BYTES; // < 100KB

View File

@@ -8,6 +8,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.UserManager;
import androidx.annotation.NonNull;
@@ -17,8 +18,10 @@ import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.reflection.ReflectionUtils;
import com.termux.shared.termux.TermuxConstants;
import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.util.List;
@@ -94,6 +97,55 @@ public class PackageUtils {
}
}
/**
* Get the {@code seInfo} {@link Field} of the {@link ApplicationInfo} class.
*
* String retrieved from the seinfo tag found in selinux policy. This value can be set through
* the mac_permissions.xml policy construct. This value is used for setting an SELinux security
* context on the process as well as its data directory.
*
* https://cs.android.com/android/platform/superproject/+/android-7.1.0_r1:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=609
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=981
* https://cs.android.com/android/platform/superproject/+/android-7.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=282
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=375
* https://cs.android.com/android/_/android/platform/frameworks/base/+/be0b8896d1bc385d4c8fb54c21929745935dcbea
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the selinux info or {@code null} if an exception was raised.
*/
@Nullable
public static String getApplicationInfoSeInfoForPackage(@NonNull final ApplicationInfo applicationInfo) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "seinfo" : "seInfo", applicationInfo).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfo field value for ApplicationInfo class", e);
return null;
}
}
/**
* Get the {@code seInfoUser} {@link Field} of the {@link ApplicationInfo} class.
*
* Also check {@link #getApplicationInfoSeInfoForPackage(ApplicationInfo)}.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the selinux info user or {@code null} if an exception was raised.
*/
@Nullable
public static String getApplicationInfoSeInfoUserForPackage(@NonNull final ApplicationInfo applicationInfo) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, "seInfoUser", applicationInfo).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfoUser field value for ApplicationInfo class", e);
return null;
}
}
/**
* Get the app name for the package associated with the {@code context}.
*

View File

@@ -0,0 +1,275 @@
package com.termux.shared.reflection;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import org.lsposed.hiddenapibypass.HiddenApiBypass;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class ReflectionUtils {
private static boolean HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = Build.VERSION.SDK_INT < Build.VERSION_CODES.P;
private static final String LOG_TAG = "ReflectionUtils";
/**
* Bypass android hidden API reflection restrictions.
* https://github.com/LSPosed/AndroidHiddenApiBypass
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
*/
public static void bypassHiddenAPIReflectionRestrictions() {
if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Logger.logDebug(LOG_TAG, "Bypassing android hidden api reflection restrictions");
HiddenApiBypass.addHiddenApiExemptions("");
HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true;
}
}
/** Check if android hidden API reflection restrictions are bypassed. */
public static boolean areHiddenAPIReflectionRestrictionsBypassed() {
return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED;
}
/**
* Get a {@link Field} for the specified class.
*
* @param clazz The {@link Class} for which to return the field.
* @param fieldName The name of the {@link Field}.
* @return Returns the {@link Field} if getting the it was successful, otherwise {@code null}.
*/
@Nullable
public static Field getDeclaredField(@NonNull Class<?> clazz, @NonNull String fieldName) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field for \"" + clazz.getName() + "\" class", e);
return null;
}
}
/** Class that represents result of invoking a field. */
public static class FieldInvokeResult {
public boolean success;
public Object value;
FieldInvokeResult(boolean success, Object value) {
this.value = success;
this.value = value;
}
}
/**
* Get a value for a {@link Field} of an object for the specified class.
*
* @param clazz The {@link Class} to which the object belongs to.
* @param fieldName The name of the {@link Field}.
* @param object The {@link Object} instance from which to get the field value.
* @return Returns the {@link FieldInvokeResult} of invoking the field. The
* {@link FieldInvokeResult#success} will be {@code true} if invoking the field was successful,
* otherwise {@code false}. The {@link FieldInvokeResult#value} will contain the field
* {@link Object} value.
*/
@NonNull
public static <T> FieldInvokeResult invokeField(@NonNull Class<T> clazz, @NonNull String fieldName, T object) {
try {
Field field = getDeclaredField(clazz, fieldName);
if (field == null) return new FieldInvokeResult(false, null);
return new FieldInvokeResult(true, field.get(object));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for \"" + clazz.getName() + "\" class", e);
return new FieldInvokeResult(false, null);
}
}
/**
* Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters.
*/
@Nullable
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName) {
return getDeclaredMethod(clazz, methodName, new Class<?>[0]);
}
/**
* Get a {@link Method} for the specified class with the specified parameters.
*
* @param clazz The {@link Class} for which to return the method.
* @param methodName The name of the {@link Method}.
* @param parameterTypes The parameter types of the method.
* @return Returns the {@link Method} if getting the it was successful, otherwise {@code null}.
*/
@Nullable
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName, Class<?>... parameterTypes) {
try {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + methodName + "\" method for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
return null;
}
}
/**
* Wrapper for {@link #invokeVoidMethod(Method, Object, Object...)} without arguments.
*/
public static boolean invokeVoidMethod(@NonNull Method method, Object obj) {
return invokeVoidMethod(method, obj, new Object[0]);
}
/**
* Invoke a {@link Method} on the specified object with the specified arguments that returns
* {@code void}.
*
* @param method The {@link Method} to invoke.
* @param obj The {@link Object} the method should be invoked from.
* @param args The arguments to pass to the method.
* @return Returns {@code true} if invoking the method was successful, otherwise {@code false}.
*/
public static boolean invokeVoidMethod(@NonNull Method method, Object obj, Object... args) {
try {
method.setAccessible(true);
method.invoke(obj, args);
return true;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
return false;
}
}
/** Class that represents result of invoking a method that has a non-void return type. */
public static class MethodInvokeResult {
public boolean success;
public Object value;
MethodInvokeResult(boolean success, Object value) {
this.value = success;
this.value = value;
}
}
/**
* Wrapper for {@link #invokeMethod(Method, Object, Object...)} without arguments.
*/
@NonNull
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj) {
return invokeMethod(method, obj, new Object[0]);
}
/**
* Invoke a {@link Method} on the specified object with the specified arguments.
*
* @param method The {@link Method} to invoke.
* @param obj The {@link Object} the method should be invoked from.
* @param args The arguments to pass to the method.
* @return Returns the {@link MethodInvokeResult} of invoking the method. The
* {@link MethodInvokeResult#success} will be {@code true} if invoking the method was successful,
* otherwise {@code false}. The {@link MethodInvokeResult#value} will contain the {@link Object}
* returned by the method.
*/
@NonNull
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj, Object... args) {
try {
method.setAccessible(true);
return new MethodInvokeResult(true, method.invoke(obj, args));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
return new MethodInvokeResult(false, null);
}
}
/**
* Wrapper for {@link #getConstructor(String, Class[])} without parameters.
*/
@Nullable
public static Constructor<?> getConstructor(@NonNull String className) {
return getConstructor(className, new Class<?>[0]);
}
/**
* Wrapper for {@link #getConstructor(Class, Class[])} to get a {@link Constructor} for the
* {@code className}.
*/
@Nullable
public static Constructor<?> getConstructor(@NonNull String className, Class<?>... parameterTypes) {
try {
return getConstructor(Class.forName(className), parameterTypes);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + className + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
return null;
}
}
/**
* Get a {@link Constructor} for the specified class with the specified parameters.
*
* @param clazz The {@link Class} for which to return the constructor.
* @param parameterTypes The parameter types of the constructor.
* @return Returns the {@link Constructor} if getting the it was successful, otherwise {@code null}.
*/
@Nullable
public static Constructor<?> getConstructor(@NonNull Class<?> clazz, Class<?>... parameterTypes) {
try {
Constructor<?> constructor = clazz.getConstructor(parameterTypes);
constructor.setAccessible(true);
return constructor;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
return null;
}
}
/**
* Wrapper for {@link #invokeConstructor(Constructor, Object...)} without arguments.
*/
@Nullable
public static Object invokeConstructor(@NonNull Constructor<?> constructor) {
return invokeConstructor(constructor, new Object[0]);
}
/**
* Invoke a {@link Constructor} with the specified arguments.
*
* @param constructor The {@link Constructor} to invoke.
* @param args The arguments to pass to the constructor.
* @return Returns the new instance if invoking the constructor was successful, otherwise {@code null}.
*/
@Nullable
public static Object invokeConstructor(@NonNull Constructor<?> constructor, Object... args) {
try {
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + constructor.getName() + "\" constructor with args: " + Arrays.toString(args), e);
return null;
}
}
}

View File

@@ -1,9 +1,13 @@
package com.termux.shared.shell;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import androidx.annotation.NonNull;
import com.termux.shared.android.SELinuxUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.file.FileUtils;
@@ -57,10 +61,41 @@ public class TermuxShellUtils {
if (TERMUX_API_VERSION_NAME != null)
environment.add("TERMUX_API_VERSION=" + TERMUX_API_VERSION_NAME);
environment.add("TERM=xterm-256color");
environment.add("COLORTERM=truecolor");
try {
ApplicationInfo applicationInfo = currentPackageContext.getPackageManager().getApplicationInfo(
TermuxConstants.TERMUX_PACKAGE_NAME, 0);
if (applicationInfo != null && !applicationInfo.enabled) {
applicationInfo = null;
}
if (applicationInfo != null) {
environment.add("TERMUX_APP__DATA_DIR=" + applicationInfo.dataDir);
environment.add("TERMUX_APP__LEGACY_DATA_DIR=" + "/data/data/" + applicationInfo.packageName);
environment.add("TERMUX_APP__SE_FILE_CONTEXT=" + SELinuxUtils.getFileContext(applicationInfo.dataDir));
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
environment.add("TERMUX_APP__SE_INFO=" + PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
}
} catch (final Exception e) {
// Ignore
}
environment.add("TERMUX__ROOTFS_DIR=" + TermuxConstants.TERMUX_FILES_DIR_PATH);
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.add("TERMUX__HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.add("TERMUX__PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.add("TERMUX__SE_PROCESS_CONTEXT=" + SELinuxUtils.getContext());
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
@@ -75,6 +110,8 @@ public class TermuxShellUtils {
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
environment.add("ANDROID__BUILD_VERSION_SDK=" + Build.VERSION.SDK_INT);
if (isFailSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));

View File

@@ -10,7 +10,10 @@ import java.util.Map;
public class ExtraKeysConstants {
/** Defines the repetitive keys that can be passed to {@link ExtraKeysView#setRepetitiveKeys(List)}. */
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL");
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList(
"UP", "DOWN", "LEFT", "RIGHT",
"BKSP", "DEL",
"PGUP", "PGDN");

View File

@@ -2,11 +2,16 @@ package com.termux.shared.termux;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.system.Os;
import android.system.OsConstants;
import androidx.annotation.NonNull;
import com.google.common.base.Joiner;
import com.termux.shared.android.SELinuxUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.packages.PackageUtils;
@@ -32,6 +37,8 @@ public class AndroidUtils {
*/
public static String getAppInfoMarkdownString(@NonNull final Context context) {
StringBuilder markdownString = new StringBuilder();
ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) return null;
AndroidUtils.appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
AndroidUtils.appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
@@ -44,6 +51,13 @@ public class AndroidUtils {
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_INSTALLED_ON_EXTERNAL_STORAGE", true);
}
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_PROCESS_CONTEXT", SELinuxUtils.getContext());
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_FILE_CONTEXT", SELinuxUtils.getFileContext(context.getFilesDir().getAbsolutePath()));
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_INFO", PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
String filesDir = context.getFilesDir().getAbsolutePath();
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
@@ -99,7 +113,19 @@ public class AndroidUtils {
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
appendPropertyToMarkdown(markdownString, "SUPPORTED_32_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_32_BIT_ABIS));
appendPropertyToMarkdown(markdownString, "SUPPORTED_64_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_64_BIT_ABIS));
// If on Android >= 15
if (Build.VERSION.SDK_INT >= 35) {
try {
appendPropertyToMarkdownIfSet(markdownString, "PAGE_SIZE", Os.sysconf(OsConstants._SC_PAGESIZE));
} catch (Throwable t) {
// Ignore
}
}
markdownString.append("\n##\n");